test: the component suite has not run since cypress 15, on any machine (#661)
* test: the component suite has not run since cypress 15, on any machine
Six component spec files -- 334 tests, including the 4,300-line layout suite -- could not
start. Cypress exited before printing anything, and under its default Electron browser it
did not even do that: it hung at 0% CPU until killed. Chrome prints the error in seconds,
which is the only reason this was findable.
Three things broke, all of them from the 13.17.0 -> 15.20.0 bump.
`baseUrl: "src"`. Cypress 15 loads its config with tsx, and tsx honours tsconfig path
resolution. Cypress requires Node's core `events` module; with that baseUrl the bare
specifier resolves against `src`, and on a case-insensitive filesystem it matches
`src/Events.js`. That file imports the app, so the whole component tree was required in
Node: JSX in `.js` files, which esbuild will not parse.
This is why CI stayed green while every local run died. Linux is case-sensitive, `events`
never matched `Events.js`, and it resolved to the built-in. The baseUrl buys nothing --
there is not one absolute import in the codebase -- so it goes.
`framework: "create-react-app"`. Cypress 14 removed that dev server; the handler is in the
13.17.0 binary and absent from 15. The webpack config is handed over explicitly now, taken
from react-scripts so components compile exactly as `npm start` compiles them. Two things
follow from that: `webpack-dev-server` 5 is a devDependency because Cypress requires 5 and
react-scripts pins 4 (npm nests react-scripts' copy, and the production build is unchanged),
and babel-loader is widened to `cypress/`, which react-scripts does not compile -- the
support file is JSX and was reaching babel with no preset.
The suite ran nowhere. `cypress run` means e2e -- its specPattern is `cypress/e2e/**` --
and these specs sit beside the components in `src`, so no job has ever executed them. A
`cypress-component-tests` job runs them now; without it this breaks again in silence.
One test fails the moment the suite can run, and it is mine, from #660: it asserted the URL
column is the widest in the row. It is not, and never was -- at that width the fixed columns
take ~307px of 400, leaving the URL ~93 against Control's 146. It shipped green because
nothing could run it. It asserts a usable remainder instead.
334 component tests pass, 69 suites / 1214 jest tests pass, `npm run build` is clean.
* review: an assertion with a floor, and comments that told the story instead of the rule
The leftover-width test had a lower bound and nothing else, so it passed on any column
wide enough to have padding -- including one that had grown, which is what losing
table-layout: fixed does. It measures the leftover now: the sized columns are held to
the widths they declare, and the truncating column to exactly what they did not take.
Comments in the config, the CI job and the spec cut back to the rule a reader needs. The
version history and the measurements are in the PR.
2026-08-12 21:57:07 -06:00
|
|
|
const path = require("path");
|
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:26:46 -06:00
|
|
|
const fs = require("fs");
|
2024-11-02 15:14:02 -06:00
|
|
|
const { defineConfig } = require("cypress");
|
|
|
|
|
|
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:26:46 -06:00
|
|
|
/*
|
|
|
|
|
* 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;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2024-11-02 15:14:02 -06:00
|
|
|
module.exports = defineConfig({
|
2025-10-29 22:16:33 -06:00
|
|
|
e2e: {
|
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:26:46 -06:00
|
|
|
setupNodeEvents(on) {
|
|
|
|
|
on("task", fileBrowserTasks);
|
|
|
|
|
},
|
2025-10-29 22:16:33 -06:00
|
|
|
// 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,
|
2026-03-10 20:26:00 -06:00
|
|
|
env: {
|
|
|
|
|
CI: !!process.env.CI,
|
|
|
|
|
},
|
2025-10-29 22:16:33 -06:00
|
|
|
},
|
2024-11-02 15:14:02 -06:00
|
|
|
component: {
|
|
|
|
|
devServer: {
|
test: the component suite has not run since cypress 15, on any machine (#661)
* test: the component suite has not run since cypress 15, on any machine
Six component spec files -- 334 tests, including the 4,300-line layout suite -- could not
start. Cypress exited before printing anything, and under its default Electron browser it
did not even do that: it hung at 0% CPU until killed. Chrome prints the error in seconds,
which is the only reason this was findable.
Three things broke, all of them from the 13.17.0 -> 15.20.0 bump.
`baseUrl: "src"`. Cypress 15 loads its config with tsx, and tsx honours tsconfig path
resolution. Cypress requires Node's core `events` module; with that baseUrl the bare
specifier resolves against `src`, and on a case-insensitive filesystem it matches
`src/Events.js`. That file imports the app, so the whole component tree was required in
Node: JSX in `.js` files, which esbuild will not parse.
This is why CI stayed green while every local run died. Linux is case-sensitive, `events`
never matched `Events.js`, and it resolved to the built-in. The baseUrl buys nothing --
there is not one absolute import in the codebase -- so it goes.
`framework: "create-react-app"`. Cypress 14 removed that dev server; the handler is in the
13.17.0 binary and absent from 15. The webpack config is handed over explicitly now, taken
from react-scripts so components compile exactly as `npm start` compiles them. Two things
follow from that: `webpack-dev-server` 5 is a devDependency because Cypress requires 5 and
react-scripts pins 4 (npm nests react-scripts' copy, and the production build is unchanged),
and babel-loader is widened to `cypress/`, which react-scripts does not compile -- the
support file is JSX and was reaching babel with no preset.
The suite ran nowhere. `cypress run` means e2e -- its specPattern is `cypress/e2e/**` --
and these specs sit beside the components in `src`, so no job has ever executed them. A
`cypress-component-tests` job runs them now; without it this breaks again in silence.
One test fails the moment the suite can run, and it is mine, from #660: it asserted the URL
column is the widest in the row. It is not, and never was -- at that width the fixed columns
take ~307px of 400, leaving the URL ~93 against Control's 146. It shipped green because
nothing could run it. It asserts a usable remainder instead.
334 component tests pass, 69 suites / 1214 jest tests pass, `npm run build` is clean.
* review: an assertion with a floor, and comments that told the story instead of the rule
The leftover-width test had a lower bound and nothing else, so it passed on any column
wide enough to have padding -- including one that had grown, which is what losing
table-layout: fixed does. It measures the leftover now: the sized columns are held to
the widths they declare, and the truncating column to exactly what they did not take.
Comments in the config, the CI job and the spec cut back to the rule a reader needs. The
version history and the measurements are in the PR.
2026-08-12 21:57:07 -06:00
|
|
|
// 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",
|
2024-11-02 15:14:02 -06:00
|
|
|
bundler: "webpack",
|
test: the component suite has not run since cypress 15, on any machine (#661)
* test: the component suite has not run since cypress 15, on any machine
Six component spec files -- 334 tests, including the 4,300-line layout suite -- could not
start. Cypress exited before printing anything, and under its default Electron browser it
did not even do that: it hung at 0% CPU until killed. Chrome prints the error in seconds,
which is the only reason this was findable.
Three things broke, all of them from the 13.17.0 -> 15.20.0 bump.
`baseUrl: "src"`. Cypress 15 loads its config with tsx, and tsx honours tsconfig path
resolution. Cypress requires Node's core `events` module; with that baseUrl the bare
specifier resolves against `src`, and on a case-insensitive filesystem it matches
`src/Events.js`. That file imports the app, so the whole component tree was required in
Node: JSX in `.js` files, which esbuild will not parse.
This is why CI stayed green while every local run died. Linux is case-sensitive, `events`
never matched `Events.js`, and it resolved to the built-in. The baseUrl buys nothing --
there is not one absolute import in the codebase -- so it goes.
`framework: "create-react-app"`. Cypress 14 removed that dev server; the handler is in the
13.17.0 binary and absent from 15. The webpack config is handed over explicitly now, taken
from react-scripts so components compile exactly as `npm start` compiles them. Two things
follow from that: `webpack-dev-server` 5 is a devDependency because Cypress requires 5 and
react-scripts pins 4 (npm nests react-scripts' copy, and the production build is unchanged),
and babel-loader is widened to `cypress/`, which react-scripts does not compile -- the
support file is JSX and was reaching babel with no preset.
The suite ran nowhere. `cypress run` means e2e -- its specPattern is `cypress/e2e/**` --
and these specs sit beside the components in `src`, so no job has ever executed them. A
`cypress-component-tests` job runs them now; without it this breaks again in silence.
One test fails the moment the suite can run, and it is mine, from #660: it asserted the URL
column is the widest in the row. It is not, and never was -- at that width the fixed columns
take ~307px of 400, leaving the URL ~93 against Control's 146. It shipped green because
nothing could run it. It asserts a usable remainder instead.
334 component tests pass, 69 suites / 1214 jest tests pass, `npm run build` is clean.
* review: an assertion with a floor, and comments that told the story instead of the rule
The leftover-width test had a lower bound and nothing else, so it passed on any column
wide enough to have padding -- including one that had grown, which is what losing
table-layout: fixed does. It measures the leftover now: the sized columns are held to
the widths they declare, and the truncating column to exactly what they did not take.
Comments in the config, the CI job and the spec cut back to the rule a reader needs. The
version history and the measurements are in the PR.
2026-08-12 21:57:07 -06:00
|
|
|
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;
|
|
|
|
|
},
|
2024-11-02 15:14:02 -06:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|