wrolpi/app/cypress/support/component.js

207 lines
8.9 KiB
JavaScript
Raw Normal View History

// ***********************************************************
// This example support/component.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import './commands'
build: uninstall Semantic UI `semantic-ui-react`, `react-semantic-toasts-2` and `semantic-ui-offline` are gone from package.json, the lockfile and node_modules -- 19 MB of dependencies that nothing had imported since the migration finished. Four things still referenced them and would have broken on uninstall. None was caught by a build or by jest: Four test files still ran `jest.mock('react-semantic-toasts-2', ...)` for components that no longer use it. A jest.mock of a module that does not exist throws, so the packages could not have come out without this. `src/components/Download.cy.js` and `cypress/support/component.js` both imported semantic.min.css. The support file now loads what the app loads -- index.css, the font and token stylesheets, and Mantine's. The ratchet changes job rather than being deleted. An import of a missing module fails loudly now, but three quieter mistakes would not: a `className="ui button"` that styles nothing, a file reaching back through Theme.tsx, and someone reinstalling a package to "just use one Semantic component". It asserts all three, plus that package.json names nothing matching /semantic/i. Two pre-existing problems in the cypress component harness surfaced while checking the uninstall had not broken it, both fixed because that file had to be edited anyway: It imported `react-router-dom`, which is not installed -- this app is on react-router v7. Dates to 5621d473, so the component suite has been un-runnable since well before this migration. Its mount helper supplied no MantineProvider, so every library component threw -- the same gap test-utils.js had for jest. The component specs now load and four pass; eleven assertions are still written against markup from before they stopped running. Repairing those is its own piece of work, deliberately not bundled in here. The e2e suite is unaffected: 43/43.
2026-07-30 14:50:47 -06:00
// Component tests mount real components, so they need the same styles the app loads.
import '../../src/index.css';
import '../../src/themes/fonts.css';
import '../../src/themes/tokens.css';
import '@mantine/core/styles.css';
test: prove the toast contrast fix in a browser, and fix the harness that hid it The title contrast was verified by hand and by nothing else. jsdom cannot check it: it resolves no `var()`, so the title's colour there is the literal string `var(--mantine-color-white)` and every contrast assertion passes. Six browser tests now cover it -- title and description at WCAG AA in each of the four themes, the title staying heavier than the message where night and amber drop its muting, and the toast taking `--panel` for its surface. The three that can fail were checked against the fix removed: 1.13:1 in dark, night and amber. Light passes either way, correctly, because Mantine's light-scheme title uses `--mantine-color-black`, which the bridge maps to `--text`. Getting there took three corrections, all of them mine: * `cy.mountUI` never set `forceColorScheme`, so every theme rendered against Mantine's LIGHT scheme. Mantine keys its own rules on data-mantine-color-scheme, and ThemeProvider sets it from the theme -- night and amber ride on dark. Any per-theme test could therefore pass on a page that was broken in the app. This is the harness the tag contrast tests use too. * The support file never imported `@mantine/notifications/styles.css`, which App.js does, so a Notification rendered unstyled and had no surface or title colour of Mantine's to be wrong about. * The spec's own helper mounted a second time to raise the toast, which reset the theme to the default. All four per-theme cases ran in light. For the record, the chain the fix breaks: `.m_3feedf16` (the title) in dark scheme is `color: var(--mantine-color-white)`, the bridge aliases that to `--btn-text`, and `--btn-text` is the text drawn on a filled button -- near-black in dark, night and amber. Mantine writes it through `:where(...)`, which has zero specificity, so a single-class override plus `html` outranks it.
2026-08-01 15:07:48 -06:00
// App.js loads this too. Without it a Notification renders unstyled here, so a toast had no
// surface and no title color of Mantine's -- and its contrast test could not fail.
test: prove the toast contrast fix in a browser, and fix the harness that hid it The title contrast was verified by hand and by nothing else. jsdom cannot check it: it resolves no `var()`, so the title's colour there is the literal string `var(--mantine-color-white)` and every contrast assertion passes. Six browser tests now cover it -- title and description at WCAG AA in each of the four themes, the title staying heavier than the message where night and amber drop its muting, and the toast taking `--panel` for its surface. The three that can fail were checked against the fix removed: 1.13:1 in dark, night and amber. Light passes either way, correctly, because Mantine's light-scheme title uses `--mantine-color-black`, which the bridge maps to `--text`. Getting there took three corrections, all of them mine: * `cy.mountUI` never set `forceColorScheme`, so every theme rendered against Mantine's LIGHT scheme. Mantine keys its own rules on data-mantine-color-scheme, and ThemeProvider sets it from the theme -- night and amber ride on dark. Any per-theme test could therefore pass on a page that was broken in the app. This is the harness the tag contrast tests use too. * The support file never imported `@mantine/notifications/styles.css`, which App.js does, so a Notification rendered unstyled and had no surface or title colour of Mantine's to be wrong about. * The spec's own helper mounted a second time to raise the toast, which reset the theme to the default. All four per-theme cases ran in light. For the record, the chain the fix breaks: `.m_3feedf16` (the title) in dark scheme is `color: var(--mantine-color-white)`, the bridge aliases that to `--btn-text`, and `--btn-text` is the text drawn on a filled button -- near-black in dark, night and amber. Mantine writes it through `:where(...)`, which has zero specificity, so a single-class override plus `html` outranks it.
2026-08-01 15:07:48 -06:00
import '@mantine/notifications/styles.css';
ui: give tables a real header, drop the column grid, quiet the row rules Three things the file browser made obvious. The header was not a surface. Mantine gives thead no background at all, so the header painted whatever was behind the table -- which is exactly what an unstriped body row paints. Against a striped body it read as one more stripe, and bold type was the only thing distinguishing it. It now carries `--head`, the token that already means "recessed surface", with a full-strength 2px rule under it: the header/body division is the one line in a table that carries meaning, so it is the one allowed to be loud. Column borders are gone. A line around every cell is a lot of ink to repeat what the column headings already say, and it was half of why the header disappeared -- a bordered header cell looks like a cell in a grid. Row rules were `--border`, the same weight as the outline around the table, so a long table read as a stack of boxes. They now use `--table-line`, a new token per theme sitting between the surface and `--border`. The table outline is not a row rule and keeps `--border`; Mantine draws both from `--table-border-color`, so that had to be restated. In the file browser an icon and a filename are inline siblings in one cell. An inline SVG sits on the text baseline, so a 24px glyph beside 13px text hung above it -- the icon looked centred in the row and the name looked like it was resting on the icon's floor. `vertical-align: middle` centres it, and the margin is a gap that was never there at all: the folder icon touched the first letter of its own name. Cypress now loads App.css, as the app does. Without it a spec covering the file browser rows measures a component the app never renders. One jest test and eleven browser tests, each checked red against a planted regression. Two notes for the next person: - `html .mantine-Table-table` is specificity, not decoration. Mantine declares --table-border-color on its own hashed class, which ties with `.mantine-Table-table` and wins on source order. - the "quieter than the outline" test measures both lines against `--panel`. Nothing paints <html> under a mounted component, so measuring against the root background compares them to black, which ranks the *paler* line as the louder one -- the assertion passed backwards until it was pointed at the surface the rules actually sit on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:46:50 -06:00
/*
* App.js loads this as well. Rules for things outside the component library live here --
* the nav bar, the file browser rows -- and without it a spec covering one of them measures
* a component the app never renders.
*/
import '../../src/App.css';
build: uninstall Semantic UI `semantic-ui-react`, `react-semantic-toasts-2` and `semantic-ui-offline` are gone from package.json, the lockfile and node_modules -- 19 MB of dependencies that nothing had imported since the migration finished. Four things still referenced them and would have broken on uninstall. None was caught by a build or by jest: Four test files still ran `jest.mock('react-semantic-toasts-2', ...)` for components that no longer use it. A jest.mock of a module that does not exist throws, so the packages could not have come out without this. `src/components/Download.cy.js` and `cypress/support/component.js` both imported semantic.min.css. The support file now loads what the app loads -- index.css, the font and token stylesheets, and Mantine's. The ratchet changes job rather than being deleted. An import of a missing module fails loudly now, but three quieter mistakes would not: a `className="ui button"` that styles nothing, a file reaching back through Theme.tsx, and someone reinstalling a package to "just use one Semantic component". It asserts all three, plus that package.json names nothing matching /semantic/i. Two pre-existing problems in the cypress component harness surfaced while checking the uninstall had not broken it, both fixed because that file had to be edited anyway: It imported `react-router-dom`, which is not installed -- this app is on react-router v7. Dates to 5621d473, so the component suite has been un-runnable since well before this migration. Its mount helper supplied no MantineProvider, so every library component threw -- the same gap test-utils.js had for jest. The component specs now load and four pass; eleven assertions are still written against markup from before they stopped running. Repairing those is its own piece of work, deliberately not bundled in here. The e2e suite is unaffected: 43/43.
2026-07-30 14:50:47 -06:00
// react-router v7 ships these directly; `react-router-dom` is not installed.
import {MemoryRouter, Route, Routes} from "react-router";
import {QueryProvider} from "../../src/hooks/customHooks";
import {TagsProvider} from "../../src/Tags";
test: the component suite's twelve failures, none of them about the component The cypress component suite has been failing 12 of 311 for long enough to read as background noise. Every one of them was the test's own fault, and four distinct causes account for all twelve. Dead selectors. `div.ui.label.large`, `div.divider.text`, `.item` and `have.attr 'error'` all named markup the old library rendered. None of them started failing when the library was replaced -- they went on matching nothing, or looking for an attribute no control carries, which is the failure mode that makes a test look broken rather than the code. A tag chip is `.wrolpi-tag`, a Select is a real input whose value can be read, an option carries the ARIA role, and an invalid field says `aria-invalid`. Both directions of that last one are asserted by value: a valid field says `aria-invalid="false"` rather than dropping the attribute, so `not.have.attr` would have passed on absence. Missing context. `useFileWorkerStatus` throws rather than defaulting when its provider is absent, so the channel edit page died on a modal buried three levels down while every other page mounted fine. App.js wraps the whole app in that provider; the mount helper now does too, with the poll slowed to a minute. Unstubbed fetches. The inventory page loads the food catalog from its own endpoint, and the RSS edit form fetched downloaders before its intercept was declared. Both rejected with "Failed to fetch" out of an effect, which Cypress counts as an uncaught application error and fails the running test on -- so three inventory tests and one download test died for a reason unrelated to anything they assert. Assertions reading the wrong thing. `cy.contains('Food Storage')` found the hidden `.inventory-print` copy, since contains takes the deepest match and the print sheet is mounted at all times. And the PUT body arrives as a JSON string, so `req.body.items` was undefined both in the reply the interceptor built and in the assertion waiting on it. The RSS submit test deep-equalled one exact JSON string and broke the moment the video downloader gained eight settings the form fills in by default; the envelope is still matched exactly and the settings are checked for the values the fixture set. It also never waited on its own alias, so an interceptor that never ran would have passed in silence. Separately, the navbar's shrink test read its link count in a bare `.then()` right after the corner finished growing -- before the recalculation that growth triggers had run, so `shrunken` was sometimes already the full count and the assertion came out "expected 10 to be above 10" under load. It now waits for the bar to settle to one row, which is the same signal the test above it asserts, and requires the "More" trigger to exist so it cannot pass having measured a bar that never overflowed. New: two tests for the file browser filter's width, desktop and phone, which is what caught the dead selector behind it. 311 passing, 0 failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 19:16:17 -06:00
import {FileWorkerStatusProvider} from "../../src/contexts/FileWorkerStatusContext";
build: uninstall Semantic UI `semantic-ui-react`, `react-semantic-toasts-2` and `semantic-ui-offline` are gone from package.json, the lockfile and node_modules -- 19 MB of dependencies that nothing had imported since the migration finished. Four things still referenced them and would have broken on uninstall. None was caught by a build or by jest: Four test files still ran `jest.mock('react-semantic-toasts-2', ...)` for components that no longer use it. A jest.mock of a module that does not exist throws, so the packages could not have come out without this. `src/components/Download.cy.js` and `cypress/support/component.js` both imported semantic.min.css. The support file now loads what the app loads -- index.css, the font and token stylesheets, and Mantine's. The ratchet changes job rather than being deleted. An import of a missing module fails loudly now, but three quieter mistakes would not: a `className="ui button"` that styles nothing, a file reaching back through Theme.tsx, and someone reinstalling a package to "just use one Semantic component". It asserts all three, plus that package.json names nothing matching /semantic/i. Two pre-existing problems in the cypress component harness surfaced while checking the uninstall had not broken it, both fixed because that file had to be edited anyway: It imported `react-router-dom`, which is not installed -- this app is on react-router v7. Dates to 5621d473, so the component suite has been un-runnable since well before this migration. Its mount helper supplied no MantineProvider, so every library component threw -- the same gap test-utils.js had for jest. The component specs now load and four pass; eleven assertions are still written against markup from before they stopped running. Repairing those is its own piece of work, deliberately not bundled in here. The e2e suite is unaffected: 43/43.
2026-07-30 14:50:47 -06:00
import {MantineProvider} from "@mantine/core";
import {cssVariablesResolver, mantineTheme} from "../../src/themes/mantine";
test: prove the toast contrast fix in a browser, and fix the harness that hid it The title contrast was verified by hand and by nothing else. jsdom cannot check it: it resolves no `var()`, so the title's colour there is the literal string `var(--mantine-color-white)` and every contrast assertion passes. Six browser tests now cover it -- title and description at WCAG AA in each of the four themes, the title staying heavier than the message where night and amber drop its muting, and the toast taking `--panel` for its surface. The three that can fail were checked against the fix removed: 1.13:1 in dark, night and amber. Light passes either way, correctly, because Mantine's light-scheme title uses `--mantine-color-black`, which the bridge maps to `--text`. Getting there took three corrections, all of them mine: * `cy.mountUI` never set `forceColorScheme`, so every theme rendered against Mantine's LIGHT scheme. Mantine keys its own rules on data-mantine-color-scheme, and ThemeProvider sets it from the theme -- night and amber ride on dark. Any per-theme test could therefore pass on a page that was broken in the app. This is the harness the tag contrast tests use too. * The support file never imported `@mantine/notifications/styles.css`, which App.js does, so a Notification rendered unstyled and had no surface or title colour of Mantine's to be wrong about. * The spec's own helper mounted a second time to raise the toast, which reset the theme to the default. All four per-theme cases ran in light. For the record, the chain the fix breaks: `.m_3feedf16` (the title) in dark scheme is `color: var(--mantine-color-white)`, the bridge aliases that to `--btn-text`, and `--btn-text` is the text drawn on a filled button -- near-black in dark, night and amber. Mantine writes it through `:where(...)`, which has zero specificity, so a single-class override plus `html` outranks it.
2026-08-01 15:07:48 -06:00
import {isDarkTheme} from "../../src/themes/names";
ui: a browser layer for the library, and the .media reset that never applied The PathInput overlap could not have been caught by jest -- jsdom computes no layout -- so the only guard was a grep for one spelling of one mistake. This adds the layer that can actually see it, and it found a real bug immediately. ui-layout.cy.js mounts library components in a real browser through a new cy.mountUI(), which supplies Mantine's provider and a stamped data-theme and nothing else. That spartan setup is the point: these components read no ambient state, so a spec can assert what jsdom cannot compute -- real geometry, resolved custom properties, applied filters -- across all four themes. It reproduces the leftSectionWidth='auto' overlap on purpose and asserts it still overlaps, because a geometry check that has never gone red is indistinguishable from one that cannot fail. The bug it found: the rule resetting `filter` on leaf media inside a `.media` wrapper scored (0,2,2) against the (0,3,1) rule it was correcting, so it lost every time and never applied to anything. Every image inside a filtered wrapper has been through the luminance matrix twice since that rule landed -- markedly darker than its surroundings, which is exactly what it was written to prevent. tokens.test.js asserted only that the rule's text was present, and so stayed green throughout; it now compares the two selectors' specificity, which is computable without a browser and goes red on the old selector. Three guards in ui.test.js keep the library mock-free by construction rather than by habit: no jest.mock in its own tests, no fetch in any library file, and no useContext outside the theme controls, whose job is to read the theme. All three were verified red against planted violations. 959 jest tests, 14 new browser tests, clean cache-busted build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:31:09 -06:00
import {MediaFilterDefs} from "../../src/themes/MediaFilterDefs";
import React from "react";
Cypress.on('uncaught:exception', (err, runnable) => {
if (err.message.includes('ChunkLoadError')) {
return false; // Ignore this error which happens after CircleCI tests.
}
// Otherwise, fail like normal
return true;
});
Cypress.Commands.add('mountWithRouter', (component, options) => {
options = options || {};
const initialEntries = options?.initialEntries || ['/'];
build: uninstall Semantic UI `semantic-ui-react`, `react-semantic-toasts-2` and `semantic-ui-offline` are gone from package.json, the lockfile and node_modules -- 19 MB of dependencies that nothing had imported since the migration finished. Four things still referenced them and would have broken on uninstall. None was caught by a build or by jest: Four test files still ran `jest.mock('react-semantic-toasts-2', ...)` for components that no longer use it. A jest.mock of a module that does not exist throws, so the packages could not have come out without this. `src/components/Download.cy.js` and `cypress/support/component.js` both imported semantic.min.css. The support file now loads what the app loads -- index.css, the font and token stylesheets, and Mantine's. The ratchet changes job rather than being deleted. An import of a missing module fails loudly now, but three quieter mistakes would not: a `className="ui button"` that styles nothing, a file reaching back through Theme.tsx, and someone reinstalling a package to "just use one Semantic component". It asserts all three, plus that package.json names nothing matching /semantic/i. Two pre-existing problems in the cypress component harness surfaced while checking the uninstall had not broken it, both fixed because that file had to be edited anyway: It imported `react-router-dom`, which is not installed -- this app is on react-router v7. Dates to 5621d473, so the component suite has been un-runnable since well before this migration. Its mount helper supplied no MantineProvider, so every library component threw -- the same gap test-utils.js had for jest. The component specs now load and four pass; eleven assertions are still written against markup from before they stopped running. Repairing those is its own piece of work, deliberately not bundled in here. The e2e suite is unaffected: 43/43.
2026-07-30 14:50:47 -06:00
/*
* MantineProvider, configured exactly as ThemeProvider configures it. Every component
* in src/components/ui renders Mantine internals and throws without it -- the same
* thing that had to be added to test-utils.js for jest.
test: the component suite's twelve failures, none of them about the component The cypress component suite has been failing 12 of 311 for long enough to read as background noise. Every one of them was the test's own fault, and four distinct causes account for all twelve. Dead selectors. `div.ui.label.large`, `div.divider.text`, `.item` and `have.attr 'error'` all named markup the old library rendered. None of them started failing when the library was replaced -- they went on matching nothing, or looking for an attribute no control carries, which is the failure mode that makes a test look broken rather than the code. A tag chip is `.wrolpi-tag`, a Select is a real input whose value can be read, an option carries the ARIA role, and an invalid field says `aria-invalid`. Both directions of that last one are asserted by value: a valid field says `aria-invalid="false"` rather than dropping the attribute, so `not.have.attr` would have passed on absence. Missing context. `useFileWorkerStatus` throws rather than defaulting when its provider is absent, so the channel edit page died on a modal buried three levels down while every other page mounted fine. App.js wraps the whole app in that provider; the mount helper now does too, with the poll slowed to a minute. Unstubbed fetches. The inventory page loads the food catalog from its own endpoint, and the RSS edit form fetched downloaders before its intercept was declared. Both rejected with "Failed to fetch" out of an effect, which Cypress counts as an uncaught application error and fails the running test on -- so three inventory tests and one download test died for a reason unrelated to anything they assert. Assertions reading the wrong thing. `cy.contains('Food Storage')` found the hidden `.inventory-print` copy, since contains takes the deepest match and the print sheet is mounted at all times. And the PUT body arrives as a JSON string, so `req.body.items` was undefined both in the reply the interceptor built and in the assertion waiting on it. The RSS submit test deep-equalled one exact JSON string and broke the moment the video downloader gained eight settings the form fills in by default; the envelope is still matched exactly and the settings are checked for the values the fixture set. It also never waited on its own alias, so an interceptor that never ran would have passed in silence. Separately, the navbar's shrink test read its link count in a bare `.then()` right after the corner finished growing -- before the recalculation that growth triggers had run, so `shrunken` was sometimes already the full count and the assertion came out "expected 10 to be above 10" under load. It now waits for the bar to settle to one row, which is the same signal the test above it asserts, and requires the "More" trigger to exist so it cannot pass having measured a bar that never overflowed. New: two tests for the file browser filter's width, desktop and phone, which is what caught the dead selector behind it. 311 passing, 0 failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 19:16:17 -06:00
*
* FileWorkerStatusProvider is here for the same reason: App.js wraps the whole app in
* it, and `useFileWorkerStatus` THROWS rather than returning a default when it is
* missing. A page that reaches it only through a modal buried a few levels down --
* the channel edit page reaches it via CollectionReorganizeModal -- therefore mounted
* fine everywhere else and died here, which is what the edit-channel spec was failing
* on. The poll is slowed right down because a spec is over long before the second
* request would tell it anything, and its fetch errors are swallowed by the provider.
build: uninstall Semantic UI `semantic-ui-react`, `react-semantic-toasts-2` and `semantic-ui-offline` are gone from package.json, the lockfile and node_modules -- 19 MB of dependencies that nothing had imported since the migration finished. Four things still referenced them and would have broken on uninstall. None was caught by a build or by jest: Four test files still ran `jest.mock('react-semantic-toasts-2', ...)` for components that no longer use it. A jest.mock of a module that does not exist throws, so the packages could not have come out without this. `src/components/Download.cy.js` and `cypress/support/component.js` both imported semantic.min.css. The support file now loads what the app loads -- index.css, the font and token stylesheets, and Mantine's. The ratchet changes job rather than being deleted. An import of a missing module fails loudly now, but three quieter mistakes would not: a `className="ui button"` that styles nothing, a file reaching back through Theme.tsx, and someone reinstalling a package to "just use one Semantic component". It asserts all three, plus that package.json names nothing matching /semantic/i. Two pre-existing problems in the cypress component harness surfaced while checking the uninstall had not broken it, both fixed because that file had to be edited anyway: It imported `react-router-dom`, which is not installed -- this app is on react-router v7. Dates to 5621d473, so the component suite has been un-runnable since well before this migration. Its mount helper supplied no MantineProvider, so every library component threw -- the same gap test-utils.js had for jest. The component specs now load and four pass; eleven assertions are still written against markup from before they stopped running. Repairing those is its own piece of work, deliberately not bundled in here. The e2e suite is unaffected: 43/43.
2026-07-30 14:50:47 -06:00
*/
return cy.mount(
<MemoryRouter initialEntries={initialEntries}>
build: uninstall Semantic UI `semantic-ui-react`, `react-semantic-toasts-2` and `semantic-ui-offline` are gone from package.json, the lockfile and node_modules -- 19 MB of dependencies that nothing had imported since the migration finished. Four things still referenced them and would have broken on uninstall. None was caught by a build or by jest: Four test files still ran `jest.mock('react-semantic-toasts-2', ...)` for components that no longer use it. A jest.mock of a module that does not exist throws, so the packages could not have come out without this. `src/components/Download.cy.js` and `cypress/support/component.js` both imported semantic.min.css. The support file now loads what the app loads -- index.css, the font and token stylesheets, and Mantine's. The ratchet changes job rather than being deleted. An import of a missing module fails loudly now, but three quieter mistakes would not: a `className="ui button"` that styles nothing, a file reaching back through Theme.tsx, and someone reinstalling a package to "just use one Semantic component". It asserts all three, plus that package.json names nothing matching /semantic/i. Two pre-existing problems in the cypress component harness surfaced while checking the uninstall had not broken it, both fixed because that file had to be edited anyway: It imported `react-router-dom`, which is not installed -- this app is on react-router v7. Dates to 5621d473, so the component suite has been un-runnable since well before this migration. Its mount helper supplied no MantineProvider, so every library component threw -- the same gap test-utils.js had for jest. The component specs now load and four pass; eleven assertions are still written against markup from before they stopped running. Repairing those is its own piece of work, deliberately not bundled in here. The e2e suite is unaffected: 43/43.
2026-07-30 14:50:47 -06:00
<MantineProvider theme={mantineTheme} cssVariablesResolver={cssVariablesResolver}>
<QueryProvider>
test: the component suite's twelve failures, none of them about the component The cypress component suite has been failing 12 of 311 for long enough to read as background noise. Every one of them was the test's own fault, and four distinct causes account for all twelve. Dead selectors. `div.ui.label.large`, `div.divider.text`, `.item` and `have.attr 'error'` all named markup the old library rendered. None of them started failing when the library was replaced -- they went on matching nothing, or looking for an attribute no control carries, which is the failure mode that makes a test look broken rather than the code. A tag chip is `.wrolpi-tag`, a Select is a real input whose value can be read, an option carries the ARIA role, and an invalid field says `aria-invalid`. Both directions of that last one are asserted by value: a valid field says `aria-invalid="false"` rather than dropping the attribute, so `not.have.attr` would have passed on absence. Missing context. `useFileWorkerStatus` throws rather than defaulting when its provider is absent, so the channel edit page died on a modal buried three levels down while every other page mounted fine. App.js wraps the whole app in that provider; the mount helper now does too, with the poll slowed to a minute. Unstubbed fetches. The inventory page loads the food catalog from its own endpoint, and the RSS edit form fetched downloaders before its intercept was declared. Both rejected with "Failed to fetch" out of an effect, which Cypress counts as an uncaught application error and fails the running test on -- so three inventory tests and one download test died for a reason unrelated to anything they assert. Assertions reading the wrong thing. `cy.contains('Food Storage')` found the hidden `.inventory-print` copy, since contains takes the deepest match and the print sheet is mounted at all times. And the PUT body arrives as a JSON string, so `req.body.items` was undefined both in the reply the interceptor built and in the assertion waiting on it. The RSS submit test deep-equalled one exact JSON string and broke the moment the video downloader gained eight settings the form fills in by default; the envelope is still matched exactly and the settings are checked for the values the fixture set. It also never waited on its own alias, so an interceptor that never ran would have passed in silence. Separately, the navbar's shrink test read its link count in a bare `.then()` right after the corner finished growing -- before the recalculation that growth triggers had run, so `shrunken` was sometimes already the full count and the assertion came out "expected 10 to be above 10" under load. It now waits for the bar to settle to one row, which is the same signal the test above it asserts, and requires the "More" trigger to exist so it cannot pass having measured a bar that never overflowed. New: two tests for the file browser filter's width, desktop and phone, which is what caught the dead selector behind it. 311 passing, 0 failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 19:16:17 -06:00
<FileWorkerStatusProvider pollInterval={60000}>
<Routes>
<Route path='/videos/channels/new' element={component}/>
<Route path='/videos/channel/:channelId/edit' element={component}/>
<Route path='/videos/channels/:channelId' element={component}/>
<Route path='*' element={component}/>
</Routes>
</FileWorkerStatusProvider>
</QueryProvider>
build: uninstall Semantic UI `semantic-ui-react`, `react-semantic-toasts-2` and `semantic-ui-offline` are gone from package.json, the lockfile and node_modules -- 19 MB of dependencies that nothing had imported since the migration finished. Four things still referenced them and would have broken on uninstall. None was caught by a build or by jest: Four test files still ran `jest.mock('react-semantic-toasts-2', ...)` for components that no longer use it. A jest.mock of a module that does not exist throws, so the packages could not have come out without this. `src/components/Download.cy.js` and `cypress/support/component.js` both imported semantic.min.css. The support file now loads what the app loads -- index.css, the font and token stylesheets, and Mantine's. The ratchet changes job rather than being deleted. An import of a missing module fails loudly now, but three quieter mistakes would not: a `className="ui button"` that styles nothing, a file reaching back through Theme.tsx, and someone reinstalling a package to "just use one Semantic component". It asserts all three, plus that package.json names nothing matching /semantic/i. Two pre-existing problems in the cypress component harness surfaced while checking the uninstall had not broken it, both fixed because that file had to be edited anyway: It imported `react-router-dom`, which is not installed -- this app is on react-router v7. Dates to 5621d473, so the component suite has been un-runnable since well before this migration. Its mount helper supplied no MantineProvider, so every library component threw -- the same gap test-utils.js had for jest. The component specs now load and four pass; eleven assertions are still written against markup from before they stopped running. Repairing those is its own piece of work, deliberately not bundled in here. The e2e suite is unaffected: 43/43.
2026-07-30 14:50:47 -06:00
</MantineProvider>
</MemoryRouter>,
options
);
});
ui: a browser layer for the library, and the .media reset that never applied The PathInput overlap could not have been caught by jest -- jsdom computes no layout -- so the only guard was a grep for one spelling of one mistake. This adds the layer that can actually see it, and it found a real bug immediately. ui-layout.cy.js mounts library components in a real browser through a new cy.mountUI(), which supplies Mantine's provider and a stamped data-theme and nothing else. That spartan setup is the point: these components read no ambient state, so a spec can assert what jsdom cannot compute -- real geometry, resolved custom properties, applied filters -- across all four themes. It reproduces the leftSectionWidth='auto' overlap on purpose and asserts it still overlaps, because a geometry check that has never gone red is indistinguishable from one that cannot fail. The bug it found: the rule resetting `filter` on leaf media inside a `.media` wrapper scored (0,2,2) against the (0,3,1) rule it was correcting, so it lost every time and never applied to anything. Every image inside a filtered wrapper has been through the luminance matrix twice since that rule landed -- markedly darker than its surroundings, which is exactly what it was written to prevent. tokens.test.js asserted only that the rule's text was present, and so stayed green throughout; it now compares the two selectors' specificity, which is computable without a browser and goes red on the old selector. Three guards in ui.test.js keep the library mock-free by construction rather than by habit: no jest.mock in its own tests, no fetch in any library file, and no useContext outside the theme controls, whose job is to read the theme. All three were verified red against planted violations. 959 jest tests, 14 new browser tests, clean cache-busted build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:31:09 -06:00
/*
* Mount a component from src/components/ui with nothing but Mantine's provider and the
* theme stamped on <html> -- no router, no query client, no tag fixtures.
*
* That spartan setup is the whole point. These components take props and read no ambient
* state, so a real browser can render one in isolation, which is what lets a spec assert
* things jsdom cannot compute: whether two boxes overlap, whether text is clipped, whether
* a token actually resolved. The Settings page shipped five unreadable inputs because
* `padding-inline-start: auto` silently became zero, and no jsdom test could have seen it --
* getBoundingClientRect() returns zeros there.
*
* `theme` stamps data-theme the way ThemeProvider does; `mediaFilter` stamps
* data-media-filter and mounts the SVG filter definitions, since a filter referenced but
* never defined silently does nothing.
test: prove the toast contrast fix in a browser, and fix the harness that hid it The title contrast was verified by hand and by nothing else. jsdom cannot check it: it resolves no `var()`, so the title's colour there is the literal string `var(--mantine-color-white)` and every contrast assertion passes. Six browser tests now cover it -- title and description at WCAG AA in each of the four themes, the title staying heavier than the message where night and amber drop its muting, and the toast taking `--panel` for its surface. The three that can fail were checked against the fix removed: 1.13:1 in dark, night and amber. Light passes either way, correctly, because Mantine's light-scheme title uses `--mantine-color-black`, which the bridge maps to `--text`. Getting there took three corrections, all of them mine: * `cy.mountUI` never set `forceColorScheme`, so every theme rendered against Mantine's LIGHT scheme. Mantine keys its own rules on data-mantine-color-scheme, and ThemeProvider sets it from the theme -- night and amber ride on dark. Any per-theme test could therefore pass on a page that was broken in the app. This is the harness the tag contrast tests use too. * The support file never imported `@mantine/notifications/styles.css`, which App.js does, so a Notification rendered unstyled and had no surface or title colour of Mantine's to be wrong about. * The spec's own helper mounted a second time to raise the toast, which reset the theme to the default. All four per-theme cases ran in light. For the record, the chain the fix breaks: `.m_3feedf16` (the title) in dark scheme is `color: var(--mantine-color-white)`, the bridge aliases that to `--btn-text`, and `--btn-text` is the text drawn on a filled button -- near-black in dark, night and amber. Mantine writes it through `:where(...)`, which has zero specificity, so a single-class override plus `html` outranks it.
2026-08-01 15:07:48 -06:00
*
* `forceColorScheme` matters as much as the token table, and was missing. Mantine keys its
* own rules on data-mantine-color-scheme, which ThemeProvider sets from the theme -- night and
* amber ride on dark. Without it every theme rendered against Mantine's LIGHT scheme here, so
* a per-theme test could pass on a page that was broken in the app: the toast title, which
* Mantine paints with `--mantine-color-white` in dark scheme only, resolved correctly by
* accident and its contrast test could not fail.
ui: a browser layer for the library, and the .media reset that never applied The PathInput overlap could not have been caught by jest -- jsdom computes no layout -- so the only guard was a grep for one spelling of one mistake. This adds the layer that can actually see it, and it found a real bug immediately. ui-layout.cy.js mounts library components in a real browser through a new cy.mountUI(), which supplies Mantine's provider and a stamped data-theme and nothing else. That spartan setup is the point: these components read no ambient state, so a spec can assert what jsdom cannot compute -- real geometry, resolved custom properties, applied filters -- across all four themes. It reproduces the leftSectionWidth='auto' overlap on purpose and asserts it still overlaps, because a geometry check that has never gone red is indistinguishable from one that cannot fail. The bug it found: the rule resetting `filter` on leaf media inside a `.media` wrapper scored (0,2,2) against the (0,3,1) rule it was correcting, so it lost every time and never applied to anything. Every image inside a filtered wrapper has been through the luminance matrix twice since that rule landed -- markedly darker than its surroundings, which is exactly what it was written to prevent. tokens.test.js asserted only that the rule's text was present, and so stayed green throughout; it now compares the two selectors' specificity, which is computable without a browser and goes red on the old selector. Three guards in ui.test.js keep the library mock-free by construction rather than by habit: no jest.mock in its own tests, no fetch in any library file, and no useContext outside the theme controls, whose job is to read the theme. All three were verified red against planted violations. 959 jest tests, 14 new browser tests, clean cache-busted build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:31:09 -06:00
*/
Cypress.Commands.add('mountUI', (component, options = {}) => {
const {theme = 'light', mediaFilter, ...mountOptions} = options;
const html = document.documentElement;
html.dataset.theme = theme;
if (mediaFilter) html.dataset.mediaFilter = mediaFilter;
else delete html.dataset.mediaFilter;
return cy.mount(
test: prove the toast contrast fix in a browser, and fix the harness that hid it The title contrast was verified by hand and by nothing else. jsdom cannot check it: it resolves no `var()`, so the title's colour there is the literal string `var(--mantine-color-white)` and every contrast assertion passes. Six browser tests now cover it -- title and description at WCAG AA in each of the four themes, the title staying heavier than the message where night and amber drop its muting, and the toast taking `--panel` for its surface. The three that can fail were checked against the fix removed: 1.13:1 in dark, night and amber. Light passes either way, correctly, because Mantine's light-scheme title uses `--mantine-color-black`, which the bridge maps to `--text`. Getting there took three corrections, all of them mine: * `cy.mountUI` never set `forceColorScheme`, so every theme rendered against Mantine's LIGHT scheme. Mantine keys its own rules on data-mantine-color-scheme, and ThemeProvider sets it from the theme -- night and amber ride on dark. Any per-theme test could therefore pass on a page that was broken in the app. This is the harness the tag contrast tests use too. * The support file never imported `@mantine/notifications/styles.css`, which App.js does, so a Notification rendered unstyled and had no surface or title colour of Mantine's to be wrong about. * The spec's own helper mounted a second time to raise the toast, which reset the theme to the default. All four per-theme cases ran in light. For the record, the chain the fix breaks: `.m_3feedf16` (the title) in dark scheme is `color: var(--mantine-color-white)`, the bridge aliases that to `--btn-text`, and `--btn-text` is the text drawn on a filled button -- near-black in dark, night and amber. Mantine writes it through `:where(...)`, which has zero specificity, so a single-class override plus `html` outranks it.
2026-08-01 15:07:48 -06:00
<MantineProvider
theme={mantineTheme}
cssVariablesResolver={cssVariablesResolver}
forceColorScheme={isDarkTheme(theme) ? 'dark' : 'light'}
>
ui: a browser layer for the library, and the .media reset that never applied The PathInput overlap could not have been caught by jest -- jsdom computes no layout -- so the only guard was a grep for one spelling of one mistake. This adds the layer that can actually see it, and it found a real bug immediately. ui-layout.cy.js mounts library components in a real browser through a new cy.mountUI(), which supplies Mantine's provider and a stamped data-theme and nothing else. That spartan setup is the point: these components read no ambient state, so a spec can assert what jsdom cannot compute -- real geometry, resolved custom properties, applied filters -- across all four themes. It reproduces the leftSectionWidth='auto' overlap on purpose and asserts it still overlaps, because a geometry check that has never gone red is indistinguishable from one that cannot fail. The bug it found: the rule resetting `filter` on leaf media inside a `.media` wrapper scored (0,2,2) against the (0,3,1) rule it was correcting, so it lost every time and never applied to anything. Every image inside a filtered wrapper has been through the luminance matrix twice since that rule landed -- markedly darker than its surroundings, which is exactly what it was written to prevent. tokens.test.js asserted only that the rule's text was present, and so stayed green throughout; it now compares the two selectors' specificity, which is computable without a browser and goes red on the old selector. Three guards in ui.test.js keep the library mock-free by construction rather than by habit: no jest.mock in its own tests, no fetch in any library file, and no useContext outside the theme controls, whose job is to read the theme. All three were verified red against planted violations. 959 jest tests, 14 new browser tests, clean cache-busted build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:31:09 -06:00
{mediaFilter ? <MediaFilterDefs/> : null}
{component}
</MantineProvider>,
mountOptions
);
});
/**
* Fail unless two elements are laid out side by side.
*
* Reads real geometry, so it catches an element painted over another regardless of how the
* overlap came about -- an invalid length, a collapsed flex row, a stale absolute position.
*/
Cypress.Commands.add('shouldNotOverlap', {prevSubject: false}, (firstSelector, secondSelector) => {
cy.get(firstSelector).then(($first) => {
cy.get(secondSelector).then(($second) => {
const a = $first[0].getBoundingClientRect();
const b = $second[0].getBoundingClientRect();
ui: name the directory fields, announce their help text, let long prefixes shrink Greptile's three findings on #590, all confirmed real before fixing, and each one verified red against the original code afterwards. P1, the directory fields had no accessible names. All five rendered their visible label as a bare <label> with no htmlFor while passing no label prop, so their inputs had a description ("/media/wrolpi/") and no name at all -- five fields in one grid, holding similar relative paths, indistinguishable to a screen reader. The labels now belong to the inputs, InfoPopup content and all. This predates PathInput; the old TextInput call sites had the same gap, and moving to a component that supports labels is what made it fixable. P1, description and error were rendered but never referenced. aria-describedby pointed only at the prefix, so the help text listing the variables a path accepts, and the reason a path was rejected, were both invisible to assistive technology. Both are now in the description, and an errored field reports aria-invalid. P2 was worse than reported: at `flex: none` a long prefix took the entire row and left the input eighteen pixels wide -- not "consumes the editable area" but a field nobody can type in. The media directory is whatever the API reports, so its length was never ours to assume. The prefix now shrinks and ellipsises at half the control, with the full path on `title` and in the accessible description. The naming fault could not have been caught where the others were. jsdom cannot see layout, and the Settings page needs enough mocking that it had no test at all -- which is why these five fields were uncovered when they broke. The new e2e spec intercepts the API and lets the real page render, so it measures the names and the geometry a user actually gets. Reverting one field to a bare label fails it; a long prefix fails the browser spec at 18px. 962 jest, 15 browser component, 46 e2e, clean cache-busted build, tsc clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:46:54 -06:00
const overlapsHorizontally = (a.right > b.left + 0.5) && (b.right > a.left + 0.5);
const overlapsVertically = (a.bottom > b.top + 0.5) && (b.bottom > a.top + 0.5);
const overlaps = overlapsHorizontally && overlapsVertically;
ui: a browser layer for the library, and the .media reset that never applied The PathInput overlap could not have been caught by jest -- jsdom computes no layout -- so the only guard was a grep for one spelling of one mistake. This adds the layer that can actually see it, and it found a real bug immediately. ui-layout.cy.js mounts library components in a real browser through a new cy.mountUI(), which supplies Mantine's provider and a stamped data-theme and nothing else. That spartan setup is the point: these components read no ambient state, so a spec can assert what jsdom cannot compute -- real geometry, resolved custom properties, applied filters -- across all four themes. It reproduces the leftSectionWidth='auto' overlap on purpose and asserts it still overlaps, because a geometry check that has never gone red is indistinguishable from one that cannot fail. The bug it found: the rule resetting `filter` on leaf media inside a `.media` wrapper scored (0,2,2) against the (0,3,1) rule it was correcting, so it lost every time and never applied to anything. Every image inside a filtered wrapper has been through the luminance matrix twice since that rule landed -- markedly darker than its surroundings, which is exactly what it was written to prevent. tokens.test.js asserted only that the rule's text was present, and so stayed green throughout; it now compares the two selectors' specificity, which is computable without a browser and goes red on the old selector. Three guards in ui.test.js keep the library mock-free by construction rather than by habit: no jest.mock in its own tests, no fetch in any library file, and no useContext outside the theme controls, whose job is to read the theme. All three were verified red against planted violations. 959 jest tests, 14 new browser tests, clean cache-busted build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:31:09 -06:00
expect(
overlaps,
`${firstSelector} [${a.left}, ${a.right}] must not overlap `
+ `${secondSelector} [${b.left}, ${b.right}]`,
).to.equal(false);
});
});
});
ui: tags look like tags, and stay readable in every theme Four things, three cosmetic and one a real legibility bug. * Tags were square chips. They now have a pointed left edge and an eyelet, like a tag tied to a garment -- the shape Semantic gave them, and the reason a tag reads as a tag rather than as a small button. The point is a square rotated behind the left edge, sized so its diagonal equals the body height and its two edges meet the body at the corners. Only where something is actually a tag. `Label` carries the count badges in the Tags table too -- file, zim, channel and domain counts -- and a point on a number reads as an arrow, so the shape is behind a `tag` prop and `NameToTagLabel` opts in. * They were smaller than what they replaced: 11.5px text with 3px of padding, against Semantic's 12px and ~7px. Now 12.5px and 5px, which also keeps them a reasonable thing to tap. * Wrapped rows of tags sat on the row above, so they carry a bottom margin, plus a left margin for the point to occupy. * The text colour was unreadable in night and wrong in amber. Tags.js calculates black-or-light against the colour the user chose for each tag, and wrote the answer to `color` inline -- a declaration no stylesheet rule can outrank. Night turns a tag into an outline over a near-black page, so a near-white tag kept the black text calculated for its fill and was left legible only by its border: 1.1:1, measured. Amber replaces every fill with its own bright orange and was painting `#dddddd` on `#ff9500`, about 1.9:1, and a neutral grey in a theme whose whole point is that no other hue reaches the eye. The fix is to stop hard-coding `color` so the theme can decide: the calculated colour goes into `--label-text`, and night and amber override it alongside the fill they already override. There is no second contrast function to write, which is what this looked like it needed -- once a theme has discarded the user's colour there is nothing left to calculate against, and the answer is just the theme's own token. `.wrolpi-label-white` and `-black` moved to the same variable. Setting `color` there outranked the base rule, so amber was painting `--white` on `--text`: light orange on orange. `contrastingColor` was also picking the worse of its two options. It compared a brightness figure against a threshold, and the figure weighted the gamma-encoded bytes without linearising them, so Semantic's blue `#2185d0` got light text at 2.9:1 where dark text gives 5.3:1. Every mid-tone blue, teal and purple a user might pick sat in that band. It now measures the WCAG contrast ratio of both candidates and returns the better one, which needs no threshold at all. Tests ----- `Tags.test.js` is new, and covers the chip nothing covered: every other spec takes `NameToTagLabel` from `tagsContextFixture`, which stubs it as a function returning the name. That is how a tag could be unreadable without a single test noticing. Two of these were verified red against the inline `color`. `cy.contrastRatio()` measures text against the surface *actually* behind it, walking up to the first ancestor that paints something -- a transparent tag has no background of its own, so a naive check reads rgba(0,0,0,0) and concludes anything is legible. Twelve cases, three tag colours across four themes, at WCAG AA. Plus a test asserting the old inline-`color` version still measures below 1.5:1, so the check is provably able to fail. The gallery had label swatches in theme colours and no user-coloured tag, which is why none of this was visible in it. It has eleven now, spanning the range where the black-or-light decision flips.
2026-07-30 19:46:10 -06:00
/**
* Contrast ratio between an element's text and the surface actually behind it, per WCAG.
*
* "The surface actually behind it" is the point. A transparent element -- which is what a
* tag becomes in night mode -- has no background of its own to compare against, so a naive
* check reads `rgba(0,0,0,0)` and concludes anything is legible. This walks up to the first
* ancestor that paints something, which is what the eye sees.
*/
const parseRgb = (value) => {
const parts = (value.match(/[\d.]+/g) || []).map(Number);
return {r: parts[0], g: parts[1], b: parts[2], a: parts.length > 3 ? parts[3] : 1};
};
const relativeLuminance = ({r, g, b}) => {
const channel = (v) => {
const s = v / 255;
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
};
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
};
const effectiveBackground = (el) => {
let node = el;
while (node && node !== document.documentElement) {
const bg = parseRgb(getComputedStyle(node).backgroundColor);
if (bg.a > 0) return bg;
node = node.parentElement;
}
return parseRgb(getComputedStyle(document.documentElement).backgroundColor);
};
Cypress.Commands.add('contrastRatio', {prevSubject: false}, (selector) => {
return cy.get(selector).then(($el) => {
const el = $el[0];
const text = relativeLuminance(parseRgb(getComputedStyle(el).color));
const behind = relativeLuminance(effectiveBackground(el));
const lighter = Math.max(text, behind);
const darker = Math.min(text, behind);
return (lighter + 0.05) / (darker + 0.05);
});
});
Cypress.Commands.add('mountWithTags', (component, options) => {
cy.fixture('tagsMock.json').then((mockTags) => {
cy.intercept('GET', '**/api/tag', {
statusCode: 200,
body: mockTags
}).as('getTags');
});
return cy.mountWithRouter(
<TagsProvider>
{component}
</TagsProvider>,
options
);
});