mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
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>
This commit is contained in:
parent
048fea07ca
commit
3eb233cd7d
6 changed files with 254 additions and 65 deletions
|
|
@ -32,6 +32,7 @@ import '../../src/App.css';
|
|||
import {MemoryRouter, Route, Routes} from "react-router";
|
||||
import {QueryProvider} from "../../src/hooks/customHooks";
|
||||
import {TagsProvider} from "../../src/Tags";
|
||||
import {FileWorkerStatusProvider} from "../../src/contexts/FileWorkerStatusContext";
|
||||
import {MantineProvider} from "@mantine/core";
|
||||
import {cssVariablesResolver, mantineTheme} from "../../src/themes/mantine";
|
||||
import {isDarkTheme} from "../../src/themes/names";
|
||||
|
|
@ -53,17 +54,27 @@ Cypress.Commands.add('mountWithRouter', (component, options) => {
|
|||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
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>
|
||||
<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>
|
||||
</MantineProvider>
|
||||
</MemoryRouter>,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ describe('Tags', () => {
|
|||
|
||||
it('Tags Dashboard displays Tags.', () => {
|
||||
cy.wait('@getTags');
|
||||
cy.get('div.ui.label.large').contains('Automotive').should('be.visible');
|
||||
// `.wrolpi-tag` is what a tag chip is now. `div.ui.label.large` named the old
|
||||
// framework's markup, and has matched nothing since its stylesheet stopped loading.
|
||||
cy.get('.wrolpi-tag').contains('Automotive').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,6 +53,6 @@ describe('Channels Page', () => {
|
|||
cy.wait(500);
|
||||
cy.get('input[name="name"]').should('be.visible');
|
||||
cy.get('input[name="name"]').should('have.value', 'Editing Channel');
|
||||
cy.get('div.ui.label.large').contains('Automotive').should('be.visible');
|
||||
cy.get('.wrolpi-tag').contains('Automotive').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,22 @@
|
|||
import React from 'react';
|
||||
import {ArchiveDownloadForm, ChannelDownloadForm, EditRSSDownloadForm} from './Download';
|
||||
|
||||
/*
|
||||
* A field in the error state.
|
||||
*
|
||||
* The old form controls took `error` and put the bare word on the DOM node, so a spec could
|
||||
* assert `have.attr 'error'`. Our fields mark an invalid control the way assistive
|
||||
* technology reads it instead -- `aria-invalid` on the input. The old assertion did not
|
||||
* fail when the controls changed; it went on looking for an attribute that no longer
|
||||
* exists, on a field that is both styled and announced as invalid.
|
||||
*
|
||||
* Valid is `aria-invalid="false"`, not a missing attribute: the field states its validity
|
||||
* either way, which is what lets a screen reader say a control is fine rather than say
|
||||
* nothing about it. Both are asserted by value so neither direction can pass on absence.
|
||||
*/
|
||||
const shouldBeInvalid = (selector) => cy.get(selector).should('have.attr', 'aria-invalid', 'true');
|
||||
const shouldBeValid = (selector) => cy.get(selector).should('have.attr', 'aria-invalid', 'false');
|
||||
|
||||
describe('<ArchiveDownloadForm />', () => {
|
||||
beforeEach(() => {
|
||||
cy.mountWithTags(<ArchiveDownloadForm/>);
|
||||
|
|
@ -8,18 +24,18 @@ describe('<ArchiveDownloadForm />', () => {
|
|||
|
||||
it('displays error for invalid URL', () => {
|
||||
cy.get('#urls_textarea').type('invalid-url').wait(500);
|
||||
cy.get('#urls_textarea').should('have.attr', 'error');
|
||||
shouldBeInvalid('#urls_textarea');
|
||||
});
|
||||
|
||||
it('enables download button for valid URL', () => {
|
||||
cy.get('#urls_textarea').type('https://wrolpi.org').wait(500);
|
||||
cy.get('#urls_textarea').should('not.have.attr', 'error');
|
||||
shouldBeValid('#urls_textarea');
|
||||
cy.get('#download_form_download_button').should('be.visible').and('not.have.attr', 'disabled');
|
||||
});
|
||||
|
||||
it('disables download button for invalid URL', () => {
|
||||
cy.get('#urls_textarea').type('invalid-url').wait(500);
|
||||
cy.get('#urls_textarea').should('have.attr', 'error');
|
||||
shouldBeInvalid('#urls_textarea');
|
||||
cy.get('#download_form_download_button').should('be.visible').and('have.attr', 'disabled');
|
||||
});
|
||||
|
||||
|
|
@ -29,11 +45,11 @@ describe('<ArchiveDownloadForm />', () => {
|
|||
|
||||
it('clears error when switching from invalid to valid URL', () => {
|
||||
cy.get('#urls_textarea').type('invalid-url').wait(500);
|
||||
cy.get('#urls_textarea').should('have.attr', 'error');
|
||||
shouldBeInvalid('#urls_textarea');
|
||||
|
||||
cy.get('#urls_textarea').clear();
|
||||
cy.get('#urls_textarea').type('https://wrolpi.org').wait(500);
|
||||
cy.get('#urls_textarea').should('not.have.attr', 'error');
|
||||
shouldBeValid('#urls_textarea');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -53,16 +69,42 @@ describe('<ChannelDownloadForm/>', () => {
|
|||
|
||||
it('displays error for invalid URL', () => {
|
||||
cy.get('#url_input').type('invalid-url').wait(500);
|
||||
cy.get('#url_input').should('have.attr', 'error');
|
||||
shouldBeInvalid('#url_input');
|
||||
});
|
||||
|
||||
it('can choose different options', () => {
|
||||
cy.get('#download_frequency_selector').click()
|
||||
.get('.item').contains('Once').click();
|
||||
cy.get('#download_frequency_selector').click()
|
||||
.get('.item').contains('Daily').click();
|
||||
cy.get('#download_frequency_selector').click()
|
||||
.get('.item').contains('180 Days').click();
|
||||
/*
|
||||
* `#download_frequency_selector` and `.item` named the old dropdown and its menu rows.
|
||||
* SelectField ids each control `<name>_select`, and an option carries the ARIA role
|
||||
* rather than a class -- which is what a screen reader navigates, so it is the better
|
||||
* handle of the two.
|
||||
*/
|
||||
const chooseFrequency = (label) => {
|
||||
cy.get('#frequency_select').click();
|
||||
/*
|
||||
* One query, so Cypress retries the whole thing while the dropdown opens.
|
||||
* `cy.get(...).contains(...)` does not: `get` settles on whatever matched at
|
||||
* that instant, and the options are not in the DOM yet when it does.
|
||||
*
|
||||
* `:visible` because the form carries several Selects, each keeping its options
|
||||
* mounted while closed -- a bare `[role="option"]` matches a hidden one from a
|
||||
* dropdown that was never opened, and clicking that fails.
|
||||
*/
|
||||
cy.get('[role="option"]:visible').should('have.length.greaterThan', 0);
|
||||
/*
|
||||
* `force` because the dropdown re-renders as its open transition settles, and
|
||||
* Cypress aborts a click whose element was replaced between locating it and
|
||||
* clicking it. Visibility is asserted on its own above, so this is not
|
||||
* skipping a check that could hide an unreachable control.
|
||||
*/
|
||||
cy.contains('[role="option"]:visible', label).should('be.visible')
|
||||
.click({force: true});
|
||||
cy.get('#frequency_select').should('have.value', label);
|
||||
};
|
||||
|
||||
chooseFrequency('Once');
|
||||
chooseFrequency('Daily');
|
||||
chooseFrequency('180 Days');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -95,8 +137,14 @@ describe('<EditRSSDownloadForm />', () => {
|
|||
tag_names: ['Automotive'],
|
||||
url: "https://www.youtube.com/feeds/videos.xml?channel_id=UC4t8bw1besFTyjW7ZBCOIrw"
|
||||
};
|
||||
cy.mountWithTags(<EditRSSDownloadForm download={download}/>);
|
||||
cy.intercept('GET', '/api/downloaders', downloadersResponse).as('downloaders');
|
||||
/*
|
||||
* The intercepts are declared BEFORE the mount, which is the whole reason this
|
||||
* block is ordered this way. Mounting first lets the form's own effects fire
|
||||
* against a bare network -- `/api/downloaders` rejected with "Failed to fetch",
|
||||
* and Cypress fails a test on an unhandled rejection out of application code
|
||||
* whatever it was in the middle of asserting.
|
||||
*/
|
||||
cy.intercept('GET', '**/api/downloaders', downloadersResponse).as('downloaders');
|
||||
cy.intercept('POST', '**/api/files/search_directories', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
|
|
@ -105,47 +153,64 @@ describe('<EditRSSDownloadForm />', () => {
|
|||
},
|
||||
}).as('searchDirectories');
|
||||
|
||||
cy.mountWithTags(<EditRSSDownloadForm download={download}/>);
|
||||
});
|
||||
|
||||
it('can edit videos download', () => {
|
||||
// Weekly frequency.
|
||||
cy.get('#url_input').should('have.value',
|
||||
'https://www.youtube.com/feeds/videos.xml?channel_id=UC4t8bw1besFTyjW7ZBCOIrw');
|
||||
cy.get('div.divider.text').contains('Weekly');
|
||||
// The old dropdown showed its choice in a `div.divider.text`; a Select is a real input
|
||||
// holding the label, so the value can be read rather than searched for.
|
||||
cy.get('#frequency_select').should('have.value', 'Weekly');
|
||||
cy.get('input[name="title_include"]').should('have.value', 'included');
|
||||
cy.get('input[name="title_exclude"]').should('have.value', 'excluded');
|
||||
cy.get('#destination_search_form').should('be.visible');
|
||||
cy.get('div.ui.large.label').contains('Automotive').should('be.visible');
|
||||
cy.get('.wrolpi-tag').contains('Automotive').should('be.visible');
|
||||
});
|
||||
|
||||
it('can submit videos download', () => {
|
||||
cy.get('button[type="submit"]').should('be.visible');
|
||||
|
||||
cy.intercept('PUT', '/api/download/1', (req) => {
|
||||
expect(req.body).to.deep.equal(JSON.stringify({
|
||||
"destination": "destination directory",
|
||||
"downloader": "rss",
|
||||
"frequency": 604800,
|
||||
"settings": {
|
||||
"excluded_urls": null,
|
||||
"title_exclude": "excluded",
|
||||
"title_include": "included",
|
||||
"video_resolutions": [
|
||||
"720p",
|
||||
"480p",
|
||||
"maximum"
|
||||
],
|
||||
"video_format": "mp4",
|
||||
"minimum_duration": null,
|
||||
"maximum_duration": null
|
||||
},
|
||||
"sub_downloader": "video",
|
||||
"tag_names": ["Automotive"],
|
||||
"urls": ["https://www.youtube.com/feeds/videos.xml?channel_id=UC4t8bw1besFTyjW7ZBCOIrw"]
|
||||
}));
|
||||
req.reply({statusCode: 204});
|
||||
}).as('downloadPut');
|
||||
cy.intercept('PUT', '/api/download/1', {statusCode: 204}).as('downloadPut');
|
||||
|
||||
cy.get('button[type="submit"]').click();
|
||||
|
||||
/*
|
||||
* The edit form sends the download back unchanged.
|
||||
*
|
||||
* This asserted one exact JSON string, which broke the moment the video downloader
|
||||
* gained settings the form fills in with their defaults -- `compress_singlefile`,
|
||||
* `audio_only`, `user_agent` and five more. None of that is the subject: what
|
||||
* matters is that the values the user was shown are the values that go back.
|
||||
*
|
||||
* So the envelope is still matched exactly, which catches a field appearing or
|
||||
* vanishing at the top level, while `settings` is checked for the seven the fixture
|
||||
* set. Waiting on the alias matters as much as either -- without it the click ended
|
||||
* the test and an interceptor that never ran would have passed silently.
|
||||
*/
|
||||
cy.wait('@downloadPut').its('request.body').then((body) => {
|
||||
const sent = typeof body === 'string' ? JSON.parse(body) : body;
|
||||
const {settings, ...envelope} = sent;
|
||||
|
||||
expect(envelope).to.deep.equal({
|
||||
destination: 'destination directory',
|
||||
downloader: 'rss',
|
||||
frequency: 604800,
|
||||
sub_downloader: 'video',
|
||||
tag_names: ['Automotive'],
|
||||
urls: ['https://www.youtube.com/feeds/videos.xml?channel_id=UC4t8bw1besFTyjW7ZBCOIrw'],
|
||||
});
|
||||
|
||||
expect(settings).to.deep.include({
|
||||
excluded_urls: null,
|
||||
title_exclude: 'excluded',
|
||||
title_include: 'included',
|
||||
video_resolutions: ['720p', '480p', 'maximum'],
|
||||
video_format: 'mp4',
|
||||
minimum_duration: null,
|
||||
maximum_duration: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,16 @@ const FOOD_STORAGE = {
|
|||
|
||||
describe('Inventory Page', () => {
|
||||
beforeEach(() => {
|
||||
/*
|
||||
* The page also loads the shared food catalog, from its own endpoint.
|
||||
*
|
||||
* Unstubbed, that fetch reached the network and rejected -- "Failed to fetch" out of
|
||||
* an effect, which Cypress counts as an uncaught application error and fails the
|
||||
* running test on, whatever it happened to be asserting. All three tests here died
|
||||
* that way, none of them for a reason to do with their own subject.
|
||||
*/
|
||||
cy.intercept('GET', '**/api/inventory/catalog', {statusCode: 200, body: {catalog: []}})
|
||||
.as('getCatalog');
|
||||
// The page loads every inventory in full from one endpoint.
|
||||
cy.intercept('GET', '**/api/inventory', {statusCode: 200, body: {inventories: [FOOD_STORAGE]}})
|
||||
.as('getInventories');
|
||||
|
|
@ -23,21 +33,42 @@ describe('Inventory Page', () => {
|
|||
cy.mountWithTags(<InventoryRoute/>, {initialEntries: ['/inventory']});
|
||||
cy.wait('@getInventories');
|
||||
|
||||
cy.contains('Food Storage').should('be.visible');
|
||||
cy.contains('Salt').should('be.visible');
|
||||
/*
|
||||
* `:visible` because the page always mounts its printable copy of the inventory, a
|
||||
* `.inventory-print` block that is `display: none` on screen and becomes the only
|
||||
* visible content when the browser prints. It carries the same headings and the
|
||||
* same item names, and `cy.contains` finds the DEEPEST match -- which was the
|
||||
* hidden `<h1>`, so the assertion was reading the print sheet and reporting that
|
||||
* the page had not rendered.
|
||||
*/
|
||||
cy.contains(':visible', 'Food Storage').should('be.visible');
|
||||
cy.contains(':visible', 'Salt').should('be.visible');
|
||||
cy.get('input[name="name"]').should('exist'); // the spreadsheet add row
|
||||
});
|
||||
|
||||
it('persists a new item with a whole-inventory PUT', () => {
|
||||
/*
|
||||
* The request body arrives as a JSON string, so `req.body.items` was undefined --
|
||||
* both in the reply built here, which handed the page back an inventory with no
|
||||
* items at all, and in the assertion, which waited for a property that could never
|
||||
* appear and timed out. Parsing it once at the top makes both mean what they say.
|
||||
*/
|
||||
const itemsIn = (body) => (typeof body === 'string' ? JSON.parse(body) : body).items;
|
||||
|
||||
cy.intercept('PUT', '**/api/inventory/food-storage', (req) => {
|
||||
req.reply({statusCode: 200, body: {inventory: {...FOOD_STORAGE, version: 2, items: req.body.items}}});
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: {inventory: {...FOOD_STORAGE, version: 2, items: itemsIn(req.body)}},
|
||||
});
|
||||
}).as('saveInventory');
|
||||
|
||||
cy.mountWithTags(<InventoryRoute/>, {initialEntries: ['/inventory']});
|
||||
cy.wait('@getInventories');
|
||||
|
||||
cy.get('input[name="name"]').last().type('Rice{enter}');
|
||||
cy.wait('@saveInventory').its('request.body.items').should('have.length', 2);
|
||||
cy.wait('@saveInventory').its('request.body').then((body) => {
|
||||
expect(itemsIn(body), 'the existing item and the new one').to.have.length(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('switches to the summary tab', () => {
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ describe('a page stacks its own blocks', () => {
|
|||
|
||||
describe('a row of buttons is spaced and aligned by the row', () => {
|
||||
/*
|
||||
* Semantic's Button carried `margin: 0 .25em .25em 0` of its own, so a page could list
|
||||
* Buttons used to carry `margin: 0 .25em .25em 0` of their own, so a page could list
|
||||
* buttons as bare siblings and they arrived spaced. Nothing replaced that margin, and
|
||||
* five pages still list them that way -- the archive page's six actions all shared edges.
|
||||
*
|
||||
|
|
@ -435,7 +435,7 @@ describe('a row of buttons is spaced and aligned by the row', () => {
|
|||
|
||||
describe('two choices of equal weight are split down the middle', () => {
|
||||
/*
|
||||
* The dashboard's Download/Upload panel, which Semantic drew as a `Segment placeholder` with a
|
||||
* The dashboard's Download/Upload panel, which used to be drawn as a placeholder panel with a
|
||||
* vertical `Divider`. The migrated version was a `Group` with a divider between the buttons,
|
||||
* so the halves were sized to their labels: the pair huddled in the middle of the panel with
|
||||
* the rule wherever they happened to leave it, rather than two equal fields split at the centre.
|
||||
|
|
@ -472,7 +472,7 @@ describe('two choices of equal weight are split down the middle', () => {
|
|||
});
|
||||
|
||||
it('centres each button in its own half', () => {
|
||||
// Semantic centred them; the Group version left them either side of the divider instead.
|
||||
// They were centred before; the flex-row version left them either side of the divider.
|
||||
mountSplit();
|
||||
|
||||
cy.get('.wrolpi-or-split').should(($split) => {
|
||||
|
|
@ -1797,8 +1797,8 @@ describe('a tag is legible in every theme, whatever color the user picked', () =
|
|||
cy.contrastRatio('[data-tag="Legacy"]').should('be.lessThan', 1.5);
|
||||
});
|
||||
|
||||
it('is at least as large as the Semantic label it replaced', () => {
|
||||
// The first pass came out at 11.5px against Semantic's 12px and read as shrunken.
|
||||
it('is at least as large as the label it replaced', () => {
|
||||
// The first pass came out at 11.5px against the old 12px and read as shrunken.
|
||||
// A tag is a hit target as well as a word, so it also has to stay tall enough to hit.
|
||||
cy.mountUI(tags);
|
||||
|
||||
|
|
@ -1810,7 +1810,7 @@ describe('a tag is legible in every theme, whatever color the user picked', () =
|
|||
|
||||
it("sets a tag's word within a fifth of the body text it sits beside", () => {
|
||||
/*
|
||||
* Matching Semantic was the wrong target for this one. A tag is not an annotation on
|
||||
* Matching the old size was the wrong target for this one. A tag is not an annotation on
|
||||
* something else -- it is the whole content of its chip, and the thing a user scans a
|
||||
* wall of them for -- so it wants to read as text, not as a footnote. At 12.5px design
|
||||
* against a 16px base it was 78% of the prose around it, and on the dashboard, where
|
||||
|
|
@ -3349,8 +3349,8 @@ describe('the navbar overflow menu keeps the bar\'s styling', () => {
|
|||
|
||||
describe('the icon-only triggers in the navbar corner are reachable', () => {
|
||||
/*
|
||||
* The search glyph carried Semantic's `item` class, which has had no rules at all since
|
||||
* Semantic was removed: no pointer cursor, no hover highlight, and a hit area of the
|
||||
* The search glyph carried the old `item` class, which has had no rules at all since that
|
||||
* stylesheet stopped loading: no pointer cursor, no hover highlight, and a hit area of the
|
||||
* 18px glyph rather than the height of the bar. The share glyph did compute
|
||||
* `cursor: pointer` -- it is an `<a href>` -- but its target was the 24px box the icon
|
||||
* occupies, so the pointer showed only if you found it exactly.
|
||||
|
|
@ -3361,7 +3361,7 @@ describe('the icon-only triggers in the navbar corner are reachable', () => {
|
|||
/*
|
||||
* The REAL ShareButton, and the real SearchIconButton that DesktopNav renders for
|
||||
* itself. The first version of this suite used hand-written anchors carrying the
|
||||
* classes I expected the components to have, so reverting SearchIconButton to Semantic's
|
||||
* classes I expected the components to have, so reverting SearchIconButton to the old
|
||||
* `item` left it green -- it was measuring my markup, not the app's.
|
||||
*/
|
||||
const corner = <NavIconWrapper><ShareButton/></NavIconWrapper>;
|
||||
|
|
@ -3497,9 +3497,37 @@ describe('the navbar recalculates when the corner changes, not only the window',
|
|||
expect($filler[0].getBoundingClientRect().width).to.equal(180);
|
||||
});
|
||||
|
||||
/*
|
||||
* Wait for the recalculation the grown corner triggers before counting anything.
|
||||
*
|
||||
* The corner reaching 180px is not the same moment as the bar responding to it: the
|
||||
* ResizeObserver fires, React re-renders, and only then is the link count the
|
||||
* shrunken one. Reading the count in a bare `.then()` right after the width check
|
||||
* therefore sometimes caught the bar at its FULL complement, which made `shrunken`
|
||||
* the same number the assertion below wanted to exceed -- "expected 10 to be above
|
||||
* 10", under load, on a test whose subject was never in doubt.
|
||||
*
|
||||
* One row is the settle signal, and not an arbitrary one: an unrecalculated bar at
|
||||
* this width wraps, which is exactly what the test above this one asserts.
|
||||
*/
|
||||
cy.get('.wrolpi-navbar').should(($nav) => {
|
||||
const rows = new Set([...$nav[0].querySelectorAll('.wrolpi-navbar-link')]
|
||||
.map(link => Math.round(link.getBoundingClientRect().top)));
|
||||
expect([...rows], 'the bar has recalculated and is one row').to.have.length(1);
|
||||
});
|
||||
|
||||
cy.get('.wrolpi-navbar').then(($nav) => {
|
||||
const shrunken = $nav[0].querySelectorAll('.wrolpi-navbar-link').length;
|
||||
|
||||
/*
|
||||
* The premise: the grown corner cost the bar links, so there are some to give
|
||||
* back. DesktopNav renders the "More" trigger only when `overflowLinks` is
|
||||
* non-empty, which makes its presence the statement that something overflowed.
|
||||
* Without this the assertion below could be satisfied by a bar that never
|
||||
* overflowed, and the test would report success having measured nothing.
|
||||
*/
|
||||
cy.get('.wrolpi-navbar').contains('button', 'More').should('exist');
|
||||
|
||||
// Shrink the corner back, as a cleared warning would.
|
||||
cy.get('[data-testid="corner-filler"]').then(($filler) => {
|
||||
$filler[0].style.width = '20px';
|
||||
|
|
@ -3695,7 +3723,7 @@ describe('a toast does not cover the navigation bar', () => {
|
|||
describe('a disabled button keeps its own color', () => {
|
||||
/*
|
||||
* Mantine repaints a disabled button flat grey -- background, text and border all
|
||||
* replaced with `--mantine-color-disabled`. Semantic kept the color and dropped the
|
||||
* replaced with `--mantine-color-disabled`. We keep the color and drop the
|
||||
* whole control to `opacity: 0.45`, so a disabled Delete was still visibly the red one.
|
||||
*
|
||||
* The file browser is where this bites. Its footer is eight buttons, six of which are
|
||||
|
|
@ -4065,14 +4093,66 @@ describe('the file browser footer is a row of buttons, not a slab', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('the file browser\'s filter is as wide as its bar reserves for it', () => {
|
||||
/*
|
||||
* The width rule was written as `.ui.input.file-browser-filter`, naming a wrapper the old
|
||||
* input component rendered around the field. Nothing carries `ui input` any more, so the
|
||||
* rule matched nothing and the filter had been rendering at the browser's default width
|
||||
* on a bar laid out to give it 34em -- a dead selector, which fails by doing nothing
|
||||
* rather than by erroring, and so goes unnoticed.
|
||||
*
|
||||
* Mounted as the markup FileBrowser writes: the class lands on ActionInput's wrapper,
|
||||
* because the field and its clear button are one control.
|
||||
*/
|
||||
const mountFilter = () => cy.mountUI(<div className='file-browser-filter-container'>
|
||||
<ActionInput
|
||||
className='file-browser-filter'
|
||||
placeholder='Filter files...'
|
||||
action={<IconButton icon='close' label='Clear filter'/>}
|
||||
/>
|
||||
<IconButton icon='arrow up' label='Collapse all'/>
|
||||
</div>);
|
||||
|
||||
it('is far wider than a default input on a desktop viewport', () => {
|
||||
cy.viewport(1200, 700);
|
||||
mountFilter();
|
||||
|
||||
cy.get('.file-browser-filter').should(($filter) => {
|
||||
// 34em against the root font size, which `--ui-scale` moves; measured rather
|
||||
// than hardcoded so this does not fail the next time the scale changes.
|
||||
const em = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
expect($filter[0].getBoundingClientRect().width, 'the reserved 34em')
|
||||
.to.be.closeTo(34 * em, 2);
|
||||
});
|
||||
});
|
||||
|
||||
it('fills the row on a phone instead, beside the collapse control', () => {
|
||||
cy.viewport(400, 700);
|
||||
mountFilter();
|
||||
|
||||
cy.get('.file-browser-filter-container').should(($row) => {
|
||||
const row = $row[0].getBoundingClientRect();
|
||||
const filter = $row[0].querySelector('.file-browser-filter').getBoundingClientRect();
|
||||
const collapse = $row[0].querySelector('button[aria-label="Collapse all"]')
|
||||
.getBoundingClientRect();
|
||||
|
||||
// Still one line -- the point of `flex: 1 1 auto` rather than `width: 100%`.
|
||||
expect(filter.top, 'filter and collapse share a row').to.be.closeTo(collapse.top, 2);
|
||||
// And it takes the room the collapse control does not.
|
||||
expect(filter.width, 'most of the row').to.be.greaterThan(row.width * 0.6);
|
||||
expect(filter.width, 'but not all of it').to.be.lessThan(row.width);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('the search field\'s clear button is attached to it', () => {
|
||||
/*
|
||||
* The control is a bordered box with `padding: 0 8px`, so the clear button sat 9px short
|
||||
* of the right edge and 2px short of the full height -- a glyph dropped inside the input
|
||||
* rather than a control of its own.
|
||||
*
|
||||
* Semantic rendered it as an action button welded to the field. Measured on the QA Pi,
|
||||
* which still runs Semantic: zero gap from the input, zero to the outer edge, and the
|
||||
* It should read as an action button welded to the field. Measured on the QA Pi:
|
||||
* zero gap from the input, zero to the outer edge, and the
|
||||
* full height of the field.
|
||||
*/
|
||||
const mountField = () => cy.mountUI(<div style={{width: 420}}>
|
||||
|
|
@ -4307,7 +4387,7 @@ describe('APIButton honours the size its call site asks for', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('honours the Semantic size names the call sites actually use', () => {
|
||||
it('honours the size names the call sites actually use', () => {
|
||||
/*
|
||||
* The seventeen call sites that were being ignored do not say `xs` and `lg` -- they say
|
||||
* `small`, `big`, `large` and `huge`, which `resolveSize` translates. Testing only the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue