mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
* e2e: exercise the FileBrowser footer buttons against real files Sixteen Cypress tests covering Rename, Move, Delete (including the tagged-file force-delete flow), Ignore/Unignore, Upload, and scoped Refresh, with no API mocking: fb:* tasks seed a cypress-fb/ sandbox inside the media directory, the UI mutates it through the real API, and assertions check the disk as well as the table. Skipped in CI, which runs only the React dev server. Background jobs (move, refresh) only queue work and worker_status reads idle while jobs wait, so cy.refreshFiles / cy.waitForEvent poll the events feed for completion events newer than the server's clock, and fail loudly when one never arrives. Every suite cleans up unconditionally: sandbox files, DB rows via a final refresh, test tags and their tags/ directories, and any leaked ignored-directory config. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * e2e: act on review — drain stragglers, teardown that cannot be skipped Events carry no job id, so waitForEvent cannot prove WHICH job finished. The rename tests now drain the parent-directory refresh their rename queues before ending, so the next test's refresh wait cannot be satisfied by a straggler while the DB still reflects the old tree; move waits match the destination in the completion message. Teardown must always reach the disk wipe: a failed command aborts the rest of its hook, so the tag cleanup and the DB-purge refresh are now best-effort (failOnStatusCode: false, no assertions), and the unignore runs after every test instead of once per spec. History and forward-looking product notes trimmed from comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
124 lines
5 KiB
JavaScript
124 lines
5 KiB
JavaScript
const path = require("path");
|
|
const fs = require("fs");
|
|
const { defineConfig } = require("cypress");
|
|
|
|
/*
|
|
* Real-file tasks for the FileBrowser e2e tests.
|
|
*
|
|
* The tests exercise the live stack (delete/rename/move really happen), so every task is
|
|
* jailed to one sandbox directory inside the media directory. `test/` also holds shared
|
|
* fixtures the whole suite depends on -- a task that could write or delete outside the
|
|
* sandbox would let a buggy test destroy them.
|
|
*/
|
|
const MEDIA_DIR = process.env.WROLPI_MEDIA_DIR || path.resolve(__dirname, "..", "test");
|
|
const SANDBOX_NAME = "cypress-fb";
|
|
const SANDBOX_DIR = path.join(MEDIA_DIR, SANDBOX_NAME);
|
|
|
|
function sandboxPath(relativePath) {
|
|
const resolved = path.resolve(SANDBOX_DIR, relativePath || ".");
|
|
if (resolved !== SANDBOX_DIR && !resolved.startsWith(SANDBOX_DIR + path.sep)) {
|
|
throw new Error(`Refusing to touch path outside the sandbox: ${relativePath}`);
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
const fileBrowserTasks = {
|
|
// Wipe the sandbox (including leftovers from a crashed run) and create the given files.
|
|
// `files` maps sandbox-relative paths to file content; a trailing '/' makes a bare directory.
|
|
"fb:reset": ({ files }) => {
|
|
fs.rmSync(SANDBOX_DIR, { recursive: true, force: true });
|
|
fs.mkdirSync(SANDBOX_DIR, { recursive: true });
|
|
for (const [relativePath, content] of Object.entries(files || {})) {
|
|
if (relativePath.endsWith("/")) {
|
|
fs.mkdirSync(sandboxPath(relativePath), { recursive: true });
|
|
} else {
|
|
const filePath = sandboxPath(relativePath);
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, content ?? "");
|
|
}
|
|
}
|
|
return SANDBOX_NAME;
|
|
},
|
|
// Add files to the existing sandbox without wiping it (e.g. behind the UI's back).
|
|
"fb:seed": ({ files }) => {
|
|
for (const [relativePath, content] of Object.entries(files || {})) {
|
|
const filePath = sandboxPath(relativePath);
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, content ?? "");
|
|
}
|
|
return true;
|
|
},
|
|
"fb:exists": (relativePath) => fs.existsSync(sandboxPath(relativePath)),
|
|
"fb:readdir": (relativePath) => {
|
|
const dir = sandboxPath(relativePath);
|
|
return fs.existsSync(dir) ? fs.readdirSync(dir).sort() : null;
|
|
},
|
|
"fb:cleanup": () => {
|
|
fs.rmSync(SANDBOX_DIR, { recursive: true, force: true });
|
|
return true;
|
|
},
|
|
// Tagging creates a hardlink under <media>/tags/<TagName>/, and a force-deleted file
|
|
// leaves that link behind (the tags-directory sync refuses to remove it). Only tag
|
|
// directories created by these tests may be removed: the name must start with 'CypressFB'.
|
|
"fb:cleanup-tag-dir": (tagName) => {
|
|
if (!/^CypressFB[A-Za-z0-9]*$/.test(tagName)) {
|
|
throw new Error(`Refusing to remove tag directory not owned by the tests: ${tagName}`);
|
|
}
|
|
fs.rmSync(path.join(MEDIA_DIR, "tags", tagName), { recursive: true, force: true });
|
|
return true;
|
|
},
|
|
};
|
|
|
|
module.exports = defineConfig({
|
|
e2e: {
|
|
setupNodeEvents(on) {
|
|
on("task", fileBrowserTasks);
|
|
},
|
|
// Use HTTP for CI, HTTPS for local development
|
|
baseUrl: process.env.CI ? 'http://localhost:3000' : 'https://localhost:8443',
|
|
specPattern: 'cypress/e2e/**/*.cy.js',
|
|
supportFile: 'cypress/support/e2e.js',
|
|
video: process.env.CI ? true : false,
|
|
screenshotOnRunFailure: true,
|
|
env: {
|
|
CI: !!process.env.CI,
|
|
},
|
|
},
|
|
component: {
|
|
devServer: {
|
|
// Cypress removed its create-react-app framework, so hand it the same webpack config
|
|
// `npm start` uses, minus the entry, output and HTML plugins Cypress supplies itself.
|
|
framework: "react",
|
|
bundler: "webpack",
|
|
webpackConfig: () => {
|
|
// react-scripts reads both of these at require time.
|
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
|
process.env.BABEL_ENV = process.env.BABEL_ENV || 'development';
|
|
const config = require('react-scripts/config/webpack.config')('development');
|
|
delete config.entry;
|
|
delete config.output;
|
|
delete config.optimization;
|
|
const cypressOwnsThese = [
|
|
'HtmlWebpackPlugin', 'InterpolateHtmlPlugin', 'MiniCssExtractPlugin',
|
|
'WebpackManifestPlugin', 'ManifestPlugin', 'ModuleNotFoundPlugin',
|
|
'ForkTsCheckerWebpackPlugin',
|
|
];
|
|
config.plugins = (config.plugins || [])
|
|
.filter(plugin => !cypressOwnsThese.includes(plugin.constructor.name));
|
|
|
|
// react-scripts only babel-loads `src`; the JSX support file lives in `cypress/`.
|
|
const alsoCompile = [path.resolve(__dirname, 'cypress')];
|
|
const widenBabel = (rules) => rules.forEach((rule) => {
|
|
if (rule.oneOf) return widenBabel(rule.oneOf);
|
|
const loader = String(rule.loader || '');
|
|
if (loader.includes('babel-loader') && rule.include) {
|
|
rule.include = [].concat(rule.include, alsoCompile);
|
|
}
|
|
});
|
|
widenBabel(config.module.rules);
|
|
|
|
return config;
|
|
},
|
|
},
|
|
},
|
|
});
|