mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
* fix(ui): one focus ring colour everywhere via the shared mixin #1866 gave the app a keyboard focus ring token (`--app-focus-ring`, at least 3:1 on every surface), but 59 component rings still drew their own colour: the selection blue under several names (`--app-selection-color`, `--mat-sys-primary`, `--app-selection-border`, the EPG's `$accent-blue`, `--embedded-mpv-accent`, `--apd-accent`, the hero's `--accent-color`), the heading colour, or the overlay's literal text colour. The selection blue falls to 2.6:1 on the stronger selection tint. - Every one now includes `focus-ring.focus-ring-declarations`, keeping its offset; the projects that newly import `libs/ui/styles` declare `ui-styles`. - The mixin reads the token alone: it is declared on `html` in both themes, and the fallback chain cost bytes in every ring. - `tools/nx/check-focus-ring-colour.mjs` joins `styles:focus-visible: validate`: an outline in a focus rule must use the token or, over video, the player's `--pc-*` palette. Three deliberate exceptions are listed with their reasons, and a stale one is reported. On master it reports these 59 rings. - The keyboard-focus E2E asserts each ring's colour equals the token where it is drawn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): search fields and shadow rings take the focus colour too; guard reads them Local review (Greptile P2): the ring-colour guard read only `outline`, so a focus indicator drawn as a shadow or a border kept any colour. Search fields showed focus as a `--mat-sys-primary`/selection border with a 12-16% halo, the EPG guide's keyboard cell as an inset `$accent-blue` shadow, and the downloads cards tinted their artwork border with the M3 primary. - Every one now uses `--app-focus-ring` (the halos and tints keep their strength through `color-mix()`); over video the panel's search border mixes the overlay's own ring colour. - The guard reads a focus rule's outline, its unblurred ring or line shadows and its border colours. Neutral boundaries (separator and widget-border tokens, transparent, currentColor) and blurred lift shadows are not indicators. On the previous commit it reports these 21 declarations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): check every focus colour on its own; count exceptions only where they excuse a ring Greptile on #1882 (2×P2): - The guard joined a declaration's colours and accepted the lot when the token appeared anywhere, so `border-color: var(--app-focus-ring) red` or a `color-mix()` of the token and red passed. It now splits each declaration into plain colours, every `color-mix()` argument included, and checks each one: the token, the player palette or a neutral boundary. An outline or shadow ring without a colour is drawn in the text colour, which only a border may keep. - An exception counted as used when its value appeared in any declaration (the diagnostic's amber is also a text colour), so a stale one was never reported. It now counts only where it excuses an off-token focus indicator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): the row a search result reveals takes the app focus ring Codex on #1882 (P2): choosing a settings search result with Enter focuses its row by script, and the keypress keeps `:focus-visible`, so the row's 60%-transparent selection outline was a real keyboard focus ring under 3:1, not the passive marker its guard exception described. The row now includes the shared mixin (keeping its 12px radius), the exception is gone, and the keyboard-focus E2E reveals a row with Enter and asserts the app ring on it; with the old rule it fails on the 60% colour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): accept focus colour tokens by exact name Greptile (local, P2): the guard matched `--app-focus-ring` and the neutral boundary tokens by prefix, so `var(--app-focus-ring-other, red)` or `var(--app-separator-strong)` passed. It now reads the property a `var()` names and accepts exactly `--app-focus-ring`, a `--pc-*` palette token or one of the four neutral boundary tokens; their fallbacks are never drawn, since the tokens are always declared, so they are not checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): check the body of a focus mixin, the shared ring included Greptile (local, P2): a mixin body has no selector and its includes are not expanded, so the shared `focus-ring-declarations` itself escaped the colour check; turning it red would change every ring and pass. The walker now names the mixin a declaration sits in, and the colour guard treats the body of a mixin whose name mentions focus as a focus rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): read logical border sides in the focus colour guard Greptile on #1882 (P2): only `border`, `border-color` and the physical sides were read, so `border-inline-start: 2px solid red` in a focus rule passed. The guard now reads every colour-carrying border property: the shorthand and the physical and logical sides, with or without `-color`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): an exception excuses its own colour, not the declaration Greptile (local, P2): an exception matched the whole declaration, so in the player stylesheet `box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px red`, or the white mixed with red, passed. Exceptions now apply to each plain colour, by exact value, like every other check; an exception counts as used only where it excuses one of a focus indicator's colours. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): tell a var() or calc() width from the ring colour Greptile (local, P2): `outline: var(--ring-width) solid var(--app-focus- ring)` failed the guard, since any non-length token counted as a colour. Math functions now count as lengths, and a shorthand with one colour slot takes its literal colour, else an accepted token (the other `var()`s are widths), else reports every candidate it cannot prove; `border-color` still checks a colour per side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): read a shadow ring whose spread is computed Codex (local, P2): with `box-shadow: 0 0 0 calc(1px + 1px) red` or a `var()` spread, only three literal zeros were left as lengths, so the shadow looked flat and was skipped. A shadow is now skipped only when it is provably not a ring: a literal non-zero blur in the third place, or every length a literal zero with no `var()` that could be a spread. The test with a variable-width token ring now asserts it is read, not skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): read shadow lengths by type, with the colour first or no blur Greptile (local, P2): the guard took a shadow's first three tokens as its lengths, so `0 2px` (a line in the text colour, no blur) was skipped as blurred and a colour-first lift shadow was read as a ring. Lengths are now read by type: a colour sits before or after them, never between, and a missing blur is zero. A `var()` counts as a possible length, so a shadow is skipped only when the first three possible lengths prove a blur, or every length and the first four possible ones are zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): a negated focus condition does not make a focus rule Greptile (local, P2): the colour guard read `:focus-visible` inside `:not()` as a focus rule, so a hover style such as `.button:hover:not(:focus-visible) { border-color: red; }` failed. The selector is now tested without its `:not()` arguments; a focus condition elsewhere, `:has()` included, still makes a focus rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): read a width in any CSS length unit Greptile (local, P2): only px, em and rem were lengths, so `outline: 1pt solid var(--app-focus-ring)` took `1pt` for the colour and failed. Every CSS length unit (absolute, font-relative, viewport and container) now counts as a length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): the open-in-playlist chip rings in the app focus colour Codex on #1882 (P2): the chip's inline component styles drew its focus ring in `--app-selection-color`, out of the colour guard's reach (it reads stylesheets). It was the only inline-style focus ring in the repository; it now uses `--app-focus-ring` like every other ring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ui): move the open-in-playlist chip styles into a stylesheet Codex on #1882 (P2) suggested this over scanning TS inline styles: the chip was the only component with a focus ring in inline `styles`, which the colour guard does not read. Its styles now live in `open-in-playlist-chip.component.scss` unchanged, the ring through the shared `focus-ring-declarations` mixin, so the guard covers it (a drifted colour there is reported). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
407 lines
15 KiB
JavaScript
407 lines
15 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { test } from 'node:test';
|
|
|
|
import {
|
|
RING_EXCEPTIONS,
|
|
checkFocusRingColour,
|
|
colourAtoms,
|
|
findOffTokenRings,
|
|
findUnusedExceptions,
|
|
indicatorColours,
|
|
listScannedFiles,
|
|
} from './check-focus-ring-colour.mjs';
|
|
|
|
const rootDir = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'../..'
|
|
);
|
|
|
|
test('reports focus rings drawn in their own colour, nested or in a mixin', () => {
|
|
const source = [
|
|
'.chip {',
|
|
' &:focus-visible {',
|
|
' outline: 2px solid var(--app-selection-color);',
|
|
' outline-offset: 2px;',
|
|
' }',
|
|
'}',
|
|
'.row:focus-within { outline-color: #2f7bff; }',
|
|
'.cell.is-focused { outline: 2px solid $accent-blue; }',
|
|
'@mixin ring { &:focus-visible { outline: 1px dashed red; } }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('a.scss', source).map(
|
|
({ line, selector, declaration }) =>
|
|
`${line} ${selector} | ${declaration}`
|
|
),
|
|
[
|
|
'3 .chip:focus-visible | outline: 2px solid var(--app-selection-color)',
|
|
'7 .row:focus-within | outline-color: #2f7bff',
|
|
'8 .cell.is-focused | outline: 2px solid $accent-blue',
|
|
'9 &:focus-visible | outline: 1px dashed red',
|
|
]
|
|
);
|
|
});
|
|
|
|
test('accepts the app ring, the player palette, removals and decorative outlines', () => {
|
|
const source = [
|
|
'.a:focus-visible { @include focus-ring.focus-ring-declarations; }',
|
|
'.b:focus-visible { outline: 2px solid var(--app-focus-ring); }',
|
|
// A stylesheet a spec loads as raw CSS uses the token directly.
|
|
'.c:focus-visible { outline: 2px solid var(--app-focus-ring, #1d63e0); }',
|
|
// Over video the overlay keeps its own palette.
|
|
':host :is(button:focus-visible) { outline: 2px solid var(--pc-text); }',
|
|
// Hiding the outline is the other guard's business.
|
|
'.d:focus { outline: none; }',
|
|
'.e:focus-visible { outline: 0 solid transparent; }',
|
|
// Not a focus rule: a decorative outline.
|
|
'.panel { outline: 1px solid rgba(255, 255, 255, 0.04); }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(findOffTokenRings('b.scss', source), []);
|
|
});
|
|
|
|
test('reads ring-shaped shadows and border colours as focus indicators', () => {
|
|
const source = [
|
|
'.search:focus-within {',
|
|
' border-color: var(--mat-sys-primary);',
|
|
' box-shadow: 0 0 0 3px',
|
|
' color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);',
|
|
'}',
|
|
'.cell.is-focused { box-shadow: inset 0 0 0 2px $accent-blue; }',
|
|
'.field:focus-within { border: 1px solid #2f7bff; }',
|
|
'.tab:focus-visible { box-shadow: inset 0 -2px 0 var(--app-selection-color); }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('c.scss', source).map(({ line }) => line),
|
|
[2, 3, 6, 7, 8]
|
|
);
|
|
});
|
|
|
|
test('leaves neutral boundaries, lift shadows and token colours alone', () => {
|
|
const source = [
|
|
'.row:focus-within { border-color: var(--app-separator); }',
|
|
'.row:focus-within { border-color: transparent; border-radius: 8px; }',
|
|
'.card:focus-within { box-shadow: 0 12px 28px rgba(0, 0, 0, 0.14); }',
|
|
'.search:focus-within {',
|
|
' border-color: var(--app-focus-ring);',
|
|
' box-shadow: 0 0 0 3px',
|
|
' color-mix(in srgb, var(--app-focus-ring) 12%, transparent);',
|
|
'}',
|
|
':host(:focus-visible) {',
|
|
' box-shadow: 0 0 0 2px var(--pc-text), 0 12px 32px rgba(0, 0, 0, 0.45);',
|
|
'}',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(findOffTokenRings('d.scss', source), []);
|
|
// The blurred shadow in a mixed list is not an indicator.
|
|
assert.deepEqual(
|
|
indicatorColours(
|
|
'box-shadow: 0 0 0 2px var(--pc-text), 0 12px 32px rgba(0, 0, 0, 0.45)'
|
|
),
|
|
['var(--pc-text)']
|
|
);
|
|
});
|
|
|
|
test('checks every colour on its own, inside color-mix() too', () => {
|
|
assert.deepEqual(
|
|
colourAtoms(
|
|
'var(--app-focus-ring) color-mix(in srgb, red 40%, var(--app-separator))'
|
|
),
|
|
['var(--app-focus-ring)', 'red', 'var(--app-separator)']
|
|
);
|
|
|
|
const source = [
|
|
// Two sides in the token, two in red.
|
|
'.a:focus-visible { border-color: var(--app-focus-ring) red; }',
|
|
'.b:focus-within {',
|
|
' box-shadow: 0 0 0 3px',
|
|
' color-mix(in srgb, var(--app-focus-ring) 40%, red);',
|
|
'}',
|
|
// No colour: the ring is drawn in the text colour.
|
|
'.c:focus-visible { outline: 2px solid; }',
|
|
'.d:focus-visible { box-shadow: 0 0 0 2px; }',
|
|
// The token beside a neutral boundary, or mixed with one.
|
|
'.e:focus-within { border-color: var(--app-focus-ring) var(--app-separator); }',
|
|
'.f:focus-within { border-color: color-mix(in srgb, var(--app-focus-ring) 38%, var(--app-separator)); }',
|
|
// A border may keep the text colour.
|
|
'.g:focus-within { border-color: currentColor; }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('e.scss', source).map(({ line }) => line),
|
|
[1, 3, 6, 7]
|
|
);
|
|
});
|
|
|
|
test('counts an exception as used only where it excuses a focus indicator', () => {
|
|
const diagnostic = RING_EXCEPTIONS.find(({ value }) => value === '#ffb24c');
|
|
const others = RING_EXCEPTIONS.filter(
|
|
(exception) => exception !== diagnostic
|
|
).map(({ file, value }) => ({
|
|
file,
|
|
source: `.x:focus-visible { outline: 2px solid ${value}; }`,
|
|
}));
|
|
// The amber stays as a text colour after its ring moved to the token.
|
|
const textOnly = {
|
|
file: diagnostic.file,
|
|
source: [
|
|
'.label { color: #ffb24c; }',
|
|
'.button:focus-visible { outline: 2px solid var(--app-focus-ring); }',
|
|
].join('\n'),
|
|
};
|
|
|
|
assert.deepEqual(findUnusedExceptions([...others, textOnly]), [diagnostic]);
|
|
assert.deepEqual(
|
|
findUnusedExceptions([
|
|
...others,
|
|
{
|
|
file: diagnostic.file,
|
|
source: '.button:focus-visible { outline: 2px solid #ffb24c; }',
|
|
},
|
|
]),
|
|
[]
|
|
);
|
|
});
|
|
|
|
test('accepts a token only by its exact name', () => {
|
|
const source = [
|
|
'.a:focus-visible { outline: 2px solid var(--app-focus-ring-other, red); }',
|
|
'.b:focus-within { border-color: var(--app-separator-strong); }',
|
|
'.c:focus-visible { outline: 2px solid var(--app-focus-ring); }',
|
|
'.d:focus-visible { outline: 2px solid var( --app-focus-ring , #1d63e0); }',
|
|
'.e:focus-visible { outline: 2px solid var(--pc-text, #e7ecf3); }',
|
|
'.f:focus-within { border-color: var(--app-separator, var(--mat-sys-outline-variant)); }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('f.scss', source).map(({ line }) => line),
|
|
[1, 2]
|
|
);
|
|
});
|
|
|
|
test('checks the body of a focus mixin, the shared ring included', () => {
|
|
const source = [
|
|
'@mixin focus-ring-declarations($offset: 2px) {',
|
|
' outline: 2px solid red;',
|
|
' outline-offset: $offset;',
|
|
'}',
|
|
// Not a focus mixin: a decorative outline.
|
|
'@mixin panel-frame { outline: 1px solid rgba(255, 255, 255, 0.04); }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('libs/ui/styles/_focus-ring.scss', source).map(
|
|
({ line, selector }) => `${line} ${selector}`
|
|
),
|
|
['2 @mixin focus-ring-declarations']
|
|
);
|
|
});
|
|
|
|
test('reads logical border sides as well as physical ones', () => {
|
|
const source = [
|
|
'.a:focus-visible { border-inline-start: 2px solid red; }',
|
|
'.b:focus-visible { border-block-color: #2f7bff; }',
|
|
'.c:focus-visible { border-inline-end-color: var(--mat-sys-primary); }',
|
|
'.d:focus-visible { border-block-start: 2px solid var(--app-focus-ring); }',
|
|
// Not a colour: the radius of a logical corner.
|
|
'.e:focus-visible { border-start-start-radius: 4px; }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('g.scss', source).map(({ line }) => line),
|
|
[1, 2, 3]
|
|
);
|
|
});
|
|
|
|
test('tells a width given through var() or calc() from the colour', () => {
|
|
const source = [
|
|
'.a:focus-visible { outline: var(--ring-width) solid var(--app-focus-ring); }',
|
|
'.b:focus-visible { outline: calc(1px + 1px) solid var(--app-focus-ring); }',
|
|
'.c:focus-within { border: var(--ring-width) solid var(--app-focus-ring); }',
|
|
'.d:focus-visible { box-shadow: 0 0 0 var(--ring-width) var(--app-focus-ring); }',
|
|
// The colour is still checked when the width is a variable.
|
|
'.e:focus-visible { outline: var(--ring-width) solid red; }',
|
|
'.f:focus-visible { outline: var(--ring-width) solid var(--mat-sys-primary); }',
|
|
'.g:focus-visible { outline: max(2px, 0.1em) solid #2f7bff; }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('h.scss', source).map(({ line }) => line),
|
|
[5, 6, 7]
|
|
);
|
|
});
|
|
|
|
test('reads a shadow ring whose spread is computed', () => {
|
|
const source = [
|
|
'.a:focus-visible { box-shadow: 0 0 0 calc(1px + 1px) red; }',
|
|
'.b:focus-visible { box-shadow: 0 0 0 var(--ring-width) red; }',
|
|
'.c:focus-visible { box-shadow: 0 0 0 var(--ring-width) var(--mat-sys-primary); }',
|
|
// Checked and accepted, not skipped as flat.
|
|
'.d:focus-visible { box-shadow: 0 0 0 var(--ring-width) var(--app-focus-ring); }',
|
|
// Provably not a ring: flat, or a lift shadow with a literal blur.
|
|
'.e:focus-visible { box-shadow: 0 0 0 0 red; }',
|
|
'.f:focus-within { box-shadow: 0 12px 28px red; }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('i.scss', source).map(({ line }) => line),
|
|
[1, 2, 3]
|
|
);
|
|
assert.deepEqual(
|
|
indicatorColours(
|
|
'box-shadow: 0 0 0 var(--ring-width) var(--app-focus-ring)'
|
|
),
|
|
['var(--app-focus-ring)']
|
|
);
|
|
});
|
|
|
|
test('reads shadow lengths by type, with the colour first or the blur left out', () => {
|
|
const source = [
|
|
// No blur and no colour: a line in the text colour.
|
|
'.a:focus-visible { box-shadow: 0 2px; }',
|
|
// The colour first, then a ring.
|
|
'.b:focus-visible { box-shadow: red 0 0 0 2px; }',
|
|
// The colour first, then a lift shadow with a literal blur.
|
|
'.c:focus-within { box-shadow: rgba(0, 0, 0, 0.5) 0 12px 28px; }',
|
|
'.d:focus-visible { box-shadow: var(--app-focus-ring) 0 0 0 2px; }',
|
|
// A lift shadow whose colour is a variable.
|
|
'.e:focus-within { box-shadow: 0 12px 28px var(--shadow-colour); }',
|
|
// Four literal zeros: the variable can only be the colour.
|
|
'.f:focus-visible { box-shadow: 0 0 0 0 var(--mat-sys-primary); }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('j.scss', source).map(({ line }) => line),
|
|
[1, 2]
|
|
);
|
|
});
|
|
|
|
test('ignores a focus condition that a rule negates', () => {
|
|
const source = [
|
|
// A hover style that excludes keyboard focus.
|
|
'.a:hover:not(:focus-visible) { border-color: red; }',
|
|
// A negation elsewhere does not hide a real focus condition.
|
|
'.b:not(.disabled):focus-visible { border-color: red; }',
|
|
// A focus condition inside :has() still makes a focus rule.
|
|
'.c:has(> button:focus-visible) { outline: 2px solid red; }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('k.scss', source).map(({ line }) => line),
|
|
[2, 3]
|
|
);
|
|
});
|
|
|
|
test('reads a width in any CSS length unit', () => {
|
|
const source = [
|
|
'.a:focus-visible { outline: 1pt solid var(--app-focus-ring); }',
|
|
'.b:focus-visible { outline: 0.1ch solid var(--app-focus-ring); }',
|
|
'.c:focus-within { border: 0.5mm solid var(--app-focus-ring); }',
|
|
'.d:focus-visible { box-shadow: 0 0 0 0.2vmin var(--app-focus-ring); }',
|
|
'.e:focus-visible { outline: 2pt solid red; }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings('l.scss', source).map(({ line }) => line),
|
|
[5]
|
|
);
|
|
});
|
|
|
|
test('lets a listed exception through only in its own file', () => {
|
|
const [exception] = RING_EXCEPTIONS;
|
|
const source = `.x:focus-visible { outline: 2px solid ${exception.value}; }`;
|
|
|
|
assert.deepEqual(findOffTokenRings(exception.file, source), []);
|
|
assert.equal(findOffTokenRings('elsewhere.scss', source).length, 1);
|
|
});
|
|
|
|
test('excuses only the excepted colour, not the rest of the declaration', () => {
|
|
const player = RING_EXCEPTIONS.find(({ value }) => value === '#ffffff');
|
|
const source = [
|
|
'.a:focus-visible { outline: 2px solid #ffffff; }',
|
|
'.b:focus-visible { outline: 2px solid #FFFFFF; }',
|
|
// A second ring in another colour beside the excepted one.
|
|
'.c:focus-visible { box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px red; }',
|
|
// The excepted colour mixed with another one.
|
|
'.d:focus-visible { outline: 2px solid color-mix(in srgb, #ffffff 50%, red); }',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
findOffTokenRings(player.file, source).map(({ line }) => line),
|
|
[3, 4]
|
|
);
|
|
});
|
|
|
|
test('reports an exception that no longer matches anything', () => {
|
|
const sources = RING_EXCEPTIONS.slice(1).map(({ file, value }) => ({
|
|
file,
|
|
source: `.x:focus-visible { outline: 2px solid ${value}; }`,
|
|
}));
|
|
|
|
assert.deepEqual(
|
|
findUnusedExceptions(sources).map(({ file }) => file),
|
|
[RING_EXCEPTIONS[0].file]
|
|
);
|
|
assert.match(
|
|
checkFocusRingColour(sources).at(-1),
|
|
/matches nothing; remove it from RING_EXCEPTIONS$/
|
|
);
|
|
});
|
|
|
|
test('selects every tracked app and library stylesheet', async () => {
|
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'focus-ring-colour-'));
|
|
const git = (...args) =>
|
|
execFileSync('git', args, { cwd: tempDir, stdio: 'pipe' });
|
|
try {
|
|
git('init', '-q');
|
|
const files = [
|
|
'apps/web/src/styles.scss',
|
|
'apps/web/src/app/settings/settings.component.scss',
|
|
'apps/website/src/styles/global.scss',
|
|
'libs/ui/components/src/lib/deep/row.component.scss',
|
|
'libs/ui/components/src/lib/deep/row.component.ts',
|
|
];
|
|
for (const file of files) {
|
|
await mkdir(path.dirname(path.join(tempDir, file)), {
|
|
recursive: true,
|
|
});
|
|
await writeFile(path.join(tempDir, file), '');
|
|
}
|
|
git('add', '.');
|
|
|
|
assert.deepEqual(listScannedFiles(tempDir).sort(), [
|
|
'apps/web/src/app/settings/settings.component.scss',
|
|
'apps/web/src/styles.scss',
|
|
'libs/ui/components/src/lib/deep/row.component.scss',
|
|
]);
|
|
} finally {
|
|
await rm(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('the repository stylesheets pass, and one drifted ring would not', async () => {
|
|
const sources = await Promise.all(
|
|
listScannedFiles(rootDir).map(async (file) => ({
|
|
file,
|
|
source: await readFile(path.join(rootDir, file), 'utf8'),
|
|
}))
|
|
);
|
|
assert.deepEqual(checkFocusRingColour(sources), []);
|
|
|
|
const drifted = [
|
|
...sources,
|
|
{
|
|
file: 'libs/ui/components/src/lib/new/new.component.scss',
|
|
source: '.new:focus-visible { outline: 2px solid var(--app-selection-color); }',
|
|
},
|
|
];
|
|
assert.equal(checkFocusRingColour(drifted).length, 1);
|
|
});
|