mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
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.
This commit is contained in:
parent
b5f278eff4
commit
9bfa1eaab2
2 changed files with 92 additions and 1 deletions
|
|
@ -19,12 +19,16 @@ import '../../src/index.css';
|
|||
import '../../src/themes/fonts.css';
|
||||
import '../../src/themes/tokens.css';
|
||||
import '@mantine/core/styles.css';
|
||||
// App.js loads this too. Without it a Notification renders unstyled here, so a toast had no
|
||||
// surface and no title colour of Mantine's -- and its contrast test could not fail.
|
||||
import '@mantine/notifications/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 {isDarkTheme} from "../../src/themes/names";
|
||||
import {MediaFilterDefs} from "../../src/themes/MediaFilterDefs";
|
||||
import React from "react";
|
||||
|
||||
|
|
@ -75,6 +79,13 @@ Cypress.Commands.add('mountWithRouter', (component, options) => {
|
|||
* `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.
|
||||
*
|
||||
* `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.
|
||||
*/
|
||||
Cypress.Commands.add('mountUI', (component, options = {}) => {
|
||||
const {theme = 'light', mediaFilter, ...mountOptions} = options;
|
||||
|
|
@ -85,7 +96,11 @@ Cypress.Commands.add('mountUI', (component, options = {}) => {
|
|||
else delete html.dataset.mediaFilter;
|
||||
|
||||
return cy.mount(
|
||||
<MantineProvider theme={mantineTheme} cssVariablesResolver={cssVariablesResolver}>
|
||||
<MantineProvider
|
||||
theme={mantineTheme}
|
||||
cssVariablesResolver={cssVariablesResolver}
|
||||
forceColorScheme={isDarkTheme(theme) ? 'dark' : 'light'}
|
||||
>
|
||||
{mediaFilter ? <MediaFilterDefs/> : null}
|
||||
{component}
|
||||
</MantineProvider>,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,17 @@ import {
|
|||
ActionInput, Button, Header, PathInput, Statistic, StatisticGroup, TabBar, tabClassName, TextInput,
|
||||
} from './index';
|
||||
import {contrastingColor} from '../Common';
|
||||
import {Notifications} from '@mantine/notifications';
|
||||
import {toast} from './toast';
|
||||
import {themeNames} from '../../themes/names';
|
||||
|
||||
/* `--panel` is authored as a hex; computed backgrounds come back as rgb(). */
|
||||
const hexToRgb = (hex) => {
|
||||
const value = hex.replace('#', '');
|
||||
const [r, g, b] = [0, 2, 4].map(i => parseInt(value.slice(i, i + 2), 16));
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
};
|
||||
|
||||
/*
|
||||
* Layout and theming assertions for the component library, run in a real browser.
|
||||
*
|
||||
|
|
@ -543,6 +552,73 @@ describe('tabs', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('a toast is readable in every theme', () => {
|
||||
/*
|
||||
* Mantine paints a dark-scheme notification title with `--mantine-color-white`, and the
|
||||
* bridge aliases that to `--btn-text` -- the text drawn on a filled button, which is
|
||||
* near-black in dark, night and amber. The title measured about 1.1:1 against the toast's
|
||||
* own surface: present, and invisible.
|
||||
*
|
||||
* Only a browser can catch that. jsdom resolves no `var()`, so the title's colour there is
|
||||
* the literal string `var(--mantine-color-white)` and every contrast check passes.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Mounts once, with the theme, and raises a toast. Mounting a second time to raise it --
|
||||
* which is how this was first written -- resets the theme to the default, so all four
|
||||
* per-theme cases silently ran in light and none of them could fail.
|
||||
*/
|
||||
const showToast = (theme) => {
|
||||
cy.mountUI(<>
|
||||
<Notifications position='top-right' limit={5}/>
|
||||
<button type='button' onClick={() => toast({
|
||||
type: 'error', title: 'Download failed',
|
||||
description: 'HTTP 403 from the remote server after 3 attempts.',
|
||||
})}>Raise one</button>
|
||||
</>, {theme});
|
||||
cy.contains('button', 'Raise one').click();
|
||||
cy.get('[class*="mantine-Notification-root"]').should('exist');
|
||||
};
|
||||
|
||||
themeNames.forEach((theme) => {
|
||||
it(`reads its title and message in ${theme}`, () => {
|
||||
showToast(theme);
|
||||
|
||||
// WCAG AA for both. The description is the message itself, so it is held to the
|
||||
// same bar as the title rather than treated as decoration.
|
||||
cy.contrastRatio('[class*="mantine-Notification-title"]').should('be.at.least', 4.5);
|
||||
cy.contrastRatio('[class*="mantine-Notification-description"]').should('be.at.least', 4.5);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the title more prominent than the message', () => {
|
||||
/*
|
||||
* Night and amber drop the description's muting, because `--muted` on a panel is 2.0:1
|
||||
* in night. Weight has to carry the hierarchy once colour cannot.
|
||||
*/
|
||||
showToast('night');
|
||||
|
||||
cy.get('[class*="mantine-Notification-title"]').then(($title) => {
|
||||
cy.get('[class*="mantine-Notification-description"]').should(($description) => {
|
||||
const weight = (el) => parseInt(getComputedStyle(el).fontWeight, 10);
|
||||
expect(weight($title[0]), 'title weight')
|
||||
.to.be.greaterThan(weight($description[0]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('takes its surface from the theme, not from Mantine', () => {
|
||||
// A toast is a panel. Left to Mantine it would be `--mantine-color-body`, which is not
|
||||
// one of our tokens and does not follow night or amber.
|
||||
showToast('night');
|
||||
|
||||
cy.get('[class*="mantine-Notification-root"]').should(($toast) => {
|
||||
const panel = getComputedStyle(document.documentElement).getPropertyValue('--panel').trim();
|
||||
expect(getComputedStyle($toast[0]).backgroundColor).to.equal(hexToRgb(panel));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('theme tokens actually resolve', () => {
|
||||
themeNames.forEach((theme) => {
|
||||
it(`paints text and borders from ${theme}'s table`, () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue