diff --git a/test/design-lint-assets.test.ts b/test/design-lint-assets.test.ts
index c9dc50cd6..a73456593 100644
--- a/test/design-lint-assets.test.ts
+++ b/test/design-lint-assets.test.ts
@@ -137,3 +137,25 @@ describe('design-lint assets: fixtures', () => {
expect(only(slopped.findings, 'unlabeled-decoration').map((f) => [f.grade, f.line])).toEqual([['blocks', 114]]);
});
});
+
+describe('design-lint assets: third review cycle', () => {
+ test('large text passes at 3:1 and the same colour on body text asks; body text at the 4.5:1 edge is clean', () => {
+ const big = page('.big { font-size: 32px; color: #959595; background: #fff; }', '
');
+ expect(only(run(small), 'low-contrast').map((f) => [f.grade, f.evidence])).toEqual([['ask', '3.00:1 under the 4.5:1 threshold on .small']]);
+ expect(only(run(page('.body { color: #767676; background: #fff; }', '
Readable
')), 'low-contrast')).toEqual([]);
+ });
+ test('two-icon-libraries reads class tokens and asset references, not prose: feather-light and bi-weekly are words', () => {
+ const prose = page('', '
A feather-light app with our bi-weekly newsletter.
');
+ expect(only(run(prose), 'two-icon-libraries')).toEqual([]);
+ const mixed = page('', '');
+ expect(only(run(mixed), 'two-icon-libraries').map((f) => f.evidence)).toEqual(['icon libraries mixed on one page: font-awesome and bootstrap-icons']);
+ const linked = page('', '');
+ expect(only(run(linked), 'two-icon-libraries').map((f) => f.evidence)).toEqual(['icon libraries mixed on one page: font-awesome and feather']);
+ });
+ test('an svg inside a labelled list item whose end tag is left out is still labelled', () => {
+ const f = run(page('', '
']]);
});
});
+
+describe('design-lint chrome: third review cycle', () => {
+ test('repeated-fact counts a percentage printed in four sections', () => {
+ const band = (i: number) => '
Save 30% off this month.
';
+ const hits = only(run(page('', '\n' + [1, 2, 3, 4].map(band).join('\n') + '\n')), 'repeated-fact');
+ expect(hits.map((f) => [f.grade, f.evidence])).toEqual([['ask', 'the same fact printed in 4 sections: 30% (4)']]);
+ });
+ test('a stock line in the hero blocks; the same line in body copy below the hero is suspected, so it sits at ask and a waiver can answer it', () => {
+ const inHero = page('', '\n
'));
+ expect(hits(nested, 'nested-cards').map(([g]) => g)).toEqual(['blocks']);
+ });
+ test('one selector sticky at two breakpoints is not a double sticky; two selectors are', async () => {
+ none(await lintPage(page('.site-header { position: sticky; top: 0; }\n@media (min-width: 900px) { .site-header { position: sticky; top: 0; padding: 16px; } }', 'H')), 'double-sticky');
+ const two = await lintPage(page('.site-header { position: sticky; top: 0; }\n.toolbar { position: sticky; top: 0; }', 'H
T
'));
+ expect(hits(two, 'double-sticky').map(([g]) => g)).toEqual(['blocks']);
+ });
+ test('a light grey side border on a blockquote is not the accent stripe; a dark ink stripe and a coloured one are', async () => {
+ none(await lintPage(page('blockquote { border-left: 4px solid #dddddd; }', '
')), 'template-cards').map(([g]) => g)).toEqual(['ask']);
+ });
+ test('many grid rules with a bare fr track do not rescan the document per rule', async () => {
+ let css = '';
+ let body = '';
+ for (let i = 0; i < 1500; i++) {
+ css += `.c${i} { grid-template-columns: 1fr 2fr; }\n`;
+ body += `
Heading ${i}
Text.
`;
+ }
+ const t = performance.now();
+ const f = await lintPage(page(css, body));
+ expect(performance.now() - t).toBeLessThan(8000);
+ expect(hits(f, 'bare-fr-track').length).toBe(1500);
+ });
+});
diff --git a/test/design-lint-tailwind.test.ts b/test/design-lint-tailwind.test.ts
index 9ace15b91..467460ab6 100644
--- a/test/design-lint-tailwind.test.ts
+++ b/test/design-lint-tailwind.test.ts
@@ -360,3 +360,12 @@ describe('design-lint tailwind: fixtures', () => {
}
});
});
+
+describe('design-lint tailwind: one gradient classifier for both tiers (third review cycle)', () => {
+ test('arbitrary indigo-to-violet stops report the same purple-blue-gradient the stylesheet form does', async () => {
+ const tw = await lintFiles({ 'A.tsx': 'export const A = () =>
hello there
;' });
+ expect(only(tw, 'purple-blue-gradient').map((f) => [f.grade, f.evidence])).toEqual([['blocks', expect.stringContaining('indigo-to-violet')]]);
+ const css = await lintFiles({ 'a.css': '.g { background: linear-gradient(90deg, #6366f1, #8b5cf6); }' });
+ expect(only(css, 'purple-blue-gradient').map((f) => [f.grade, f.evidence])).toEqual([['blocks', expect.stringContaining('indigo-to-violet')]]);
+ });
+});
diff --git a/test/design-lint-type.test.ts b/test/design-lint-type.test.ts
index f0424e7ce..b506db442 100644
--- a/test/design-lint-type.test.ts
+++ b/test/design-lint-type.test.ts
@@ -190,3 +190,24 @@ describe('design-lint type: fixtures', () => {
expect(byRule(findings, 'monospace-costume').map((f) => [f.grade, f.line])).toEqual([['ask', lineOf(html, '.a__eyebrow {')]]);
});
});
+
+describe('design-lint type: third review cycle', () => {
+ test('banned-font: a banned face as the first family on a heading or in a --font-* token asks once; a fallback position, a var() and a near name are clean', () => {
+ const { html, findings } = lintPage(page('h1 { font-family: "Comic Sans MS", cursive; }\nh2 { font-family: Impact, sans-serif; }\n.quote { font-family: var(--font-body); }\n.note { font-family: "Papyrus Variable Text", serif; }\n.last { font-family: Georgia, Papyrus, serif; }', '
Ledger
Notes
Q
N
L
', TOKENS.replace('--font-body: "Public Sans", sans-serif;', '--font-body: Papyrus, serif;')));
+ const hits = byRule(findings, 'banned-font').map((f) => [f.grade, f.line, f.evidence]);
+ expect(hits).toEqual([
+ ['ask', lineOf(html, '--font-body: Papyrus'), '"papyrus" (--font-body)'],
+ ['ask', lineOf(html, '"Comic Sans MS"'), '"comic sans ms" (h1)'],
+ ['ask', lineOf(html, 'Impact'), '"impact" (h2)'],
+ ['ask', lineOf(html, '"Papyrus Variable Text"'), '"papyrus variable text" (.note)'],
+ ]);
+ });
+ test('an empty icon glyph in a heading, and an that is the whole heading, are not an italic word', () => {
+ const icon = lintPage(page('', '
'));
+ expect(lines(byRule(whole.findings, 'italic-word-in-heading'))).toEqual([]);
+ const word = lintPage(page('', '
Fast and safe
'));
+ expect(byRule(word.findings, 'italic-word-in-heading').map((f) => f.grade)).toEqual(['blocks']);
+ });
+});
diff --git a/test/design-lint.test.ts b/test/design-lint.test.ts
index 5b530d0b3..71abcbb79 100644
--- a/test/design-lint.test.ts
+++ b/test/design-lint.test.ts
@@ -8,12 +8,12 @@ import { afterAll, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
-import { buildTokens, parseCss, pxOf, resolveVars } from '../lib/design-lint/css';
+import { buildStyleIndex, buildTokens, parseCss, pxOf, resolveVars, stylesFor, type StyleIndex } from '../lib/design-lint/css';
import { contrastRatio, parseColor, srgbToOklch } from '../lib/design-lint/color';
import { ancestorsOf, directChildren, getAttr, innerOf, jsxPseudo, parseDoc, plainText, treeOf } from '../lib/design-lint/html';
import { lint, formatResult } from '../lib/design-lint/index';
import { Reporter, exitCodeOf, finalize, formatFinding, formatSummary, summarize } from '../lib/design-lint/report';
-import { collectFiles, contextWaivers, lintStatic } from '../lib/design-lint/static';
+import { collectFiles, contextWaivers, lintStatic, MAX_FILE_BYTES } from '../lib/design-lint/static';
import type { LintContext } from '../lib/design-lint/context';
import { browserAvailable } from './helpers/browser-available';
import { RULES } from '../lib/design-lint/rules/ids';
@@ -338,6 +338,74 @@ describe('design-lint: waivers', () => {
});
});
+describe('design-lint: third review cycle', () => {
+ test('two waiver comments on one line are both read, and a stylesheet claim answers only for that stylesheet', () => {
+ const claims = findWaivers('a.html', ' \n/* impeccable-disable overused-font: the masthead face is the paper */ .x{}');
+ expect(claims.map((c) => [c.rule, c.reason])).toEqual([['section-gap', 'the ledger separates rows'], ['line-length', 'the manifesto sets long lines'], ['overused-font', 'the masthead face is the paper']]);
+ const dir = tmpTree({
+ 'index.html': 'Ledger