mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
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>
132 lines
5.3 KiB
JavaScript
132 lines
5.3 KiB
JavaScript
// ***********************************************************
|
|
// 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'
|
|
// 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';
|
|
// 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";
|
|
import {MantineProvider} from "@mantine/core";
|
|
import {cssVariablesResolver, mantineTheme} from "../../src/themes/mantine";
|
|
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 || ['/'];
|
|
/*
|
|
* 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.
|
|
*/
|
|
return cy.mount(
|
|
<MemoryRouter initialEntries={initialEntries}>
|
|
<MantineProvider theme={mantineTheme} cssVariablesResolver={cssVariablesResolver}>
|
|
<QueryProvider>
|
|
<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>
|
|
</QueryProvider>
|
|
</MantineProvider>
|
|
</MemoryRouter>,
|
|
options
|
|
);
|
|
});
|
|
|
|
/*
|
|
* 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.
|
|
*/
|
|
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(
|
|
<MantineProvider theme={mantineTheme} cssVariablesResolver={cssVariablesResolver}>
|
|
{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();
|
|
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;
|
|
expect(
|
|
overlaps,
|
|
`${firstSelector} [${a.left}, ${a.right}] must not overlap `
|
|
+ `${secondSelector} [${b.left}, ${b.right}]`,
|
|
).to.equal(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
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
|
|
);
|
|
});
|