wrolpi/app/cypress.config.js
lrnselfreliance 9278103554
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run
e2e: exercise the FileBrowser footer buttons against real files (#670)
* 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>
2026-08-19 20:27:29 -06:00

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;
},
},
},
});