From bd2a86fdf7a3fd68d439821412cc8d35101c999d Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 26 Oct 2026 09:19:33 +0000 Subject: [PATCH] design-lint: fourth review cycle tests and docs test/design-lint-fourth-cycle.test.ts pins commented-out markup dead for every rule, unclosed
  • Tailwind frames and wireframe slots, linear-time unclosed runs and component scripts, cyclic var() and nested @media, leading-dot lengths, a symlinked lint root, oversize files reported, a whitespace-named stylesheet's waiver, the overused-font stems, width: min() caps, box-shadow rings, Meet headings below the hero, wrong-shaped render answers and the never-waivable refusals by class; the settle golden no longer repeats the kept width and expects the bad grade as tampering. docs/design-lint.md describes the caps, implicit closes, skipped-file findings and the matching rules. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- docs/design-lint.md | 32 +++- test/design-lint-fourth-cycle.test.ts | 260 ++++++++++++++++++++++++++ test/design-lint-settle.test.ts | 6 +- test/design-lint.test.ts | 4 +- 4 files changed, 293 insertions(+), 9 deletions(-) create mode 100644 test/design-lint-fourth-cycle.test.ts diff --git a/docs/design-lint.md b/docs/design-lint.md index 754f55da8..f2dc53ad4 100644 --- a/docs/design-lint.md +++ b/docs/design-lint.md @@ -26,8 +26,20 @@ gstack-design-lint [--render] [--url ] [--scope pa - Reads `.html`, `.css`, `.jsx`, `.tsx`, `.vue`, `.svelte` and `.astro`. Tailwind class strings in components are read too, including `cn()`, `clsx()`, `cva()` and their kin. `node_modules`, dot folders and build - output below the top level are skipped; a symlink is followed once; a file + output below the top level are skipped; a symlink is followed once and lint + roots and the working directory are read through their real paths; a file over 2 MB is a bundle, not a page, and is skipped with a warning on stderr. + A page or stylesheet under a lint root that the lint skipped (over the cap, + unreadable, unparseable) is also one `internal-error` finding at ask, so a + page the lint could not read is not a pass; a linked stylesheet over the cap + is only warned about, because the page that links it is still read. +- Markup inside an HTML comment is dead for every rule, a commented-out + `\n\n\n' + body + '\n\n\n'; +} + +function tree(files: Record): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-cycle4-')); + tmpDirs.push(dir); + for (const [name, text] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(dir, name)), { recursive: true }); + fs.writeFileSync(path.join(dir, name), text); + } + return dir; +} + +const quiet = { warn: () => {} }; +const run = (html: string, files: Record = {}): Finding[] => { + const dir = tree({ 'index.html': html, ...files }); + return lintStatic([dir], { cwd: dir, ...quiet }).findings; +}; +const only = (f: Finding[], rule: string) => f.filter((x) => x.rule === rule); +const rules = (f: Finding[]) => [...new Set(f.map((x) => x.rule))].sort(); + +describe('design-lint fourth cycle: commented-out markup is dead for every rule', () => { + test('tells inside an HTML comment report nothing: inline literal, italic word, broken image, fake avatar, emoji, craft copy', () => { + const live = page('', '

    The harbour office

    Rows close in order.

    '); + const dead = page('', '

    The harbour office

    Rows close in order.

    \n'); + const before = rules(run(live)); + const after = run(dead); + expect(rules(after)).toEqual(before); + expect(only(after, 'literal-outside-tokens').concat(only(after, 'italic-word-in-heading'), only(after, 'broken-image'), only(after, 'fake-avatar'), only(after, 'generic-hero-copy'))).toEqual([]); + }); + + test('a commented-out module script and stylesheet link do not make the page a build-only app', () => { + const live = run(page('', '

    Harbour

    Rows.

    \n')); + expect(only(live, 'unbuilt-source')).toEqual([]); + }); +}); + +describe('design-lint fourth cycle: one pairing for every tree reader', () => { + test('an unclosed
  • list before an eyebrow does not hide kicker-above-heading from the Tailwind frames', () => { + const body = (li: string) => '

    Hi there

    1. one' + li + '
    2. two' + li + '

    Our depots

    Bergen and Stavanger

    '; + const closed = run(page('', body('
  • '))); + const open = run(page('', body(''))); + expect(only(closed, 'kicker-above-heading').length).toBeGreaterThan(0); + expect(only(open, 'kicker-above-heading').map((f) => f.line)).toEqual(only(closed, 'kicker-above-heading').map((f) => f.line)); + }); + + test('wireframe-slots groups a row of unclosed
  • items under their list', () => { + const items = (close: string) => Array.from({ length: 6 }, (_, i) => '
  • x' + close).join('\n'); + const f = run(page('', '

    Hi there

      ' + items('') + '
    ')); + const closed = run(page('', '

    Hi there

      ' + items('') + '
    ')); + expect(only(f, 'wireframe-slots').map((x) => x.evidence)).toEqual(only(closed, 'wireframe-slots').map((x) => x.evidence)); + expect(only(f, 'wireframe-slots').some((x) => /in one row or grid/.test(x.evidence))).toBe(true); + }); + + test('a heading start tag closes an open heading and a block start closes an open

    , so an unclosed run costs its length', () => { + const doc = parseDoc('x.html', '' + '

    Title x\n'.repeat(3000) + '

    a

    b
    ' + ''); + const heads = doc.tags.filter((t) => !t.close && t.name === 'h2'); + expect(heads.length).toBe(3000); + for (const frag of ['

    x ', '