From fd0fe59fee1b57e3f1a1905fe0e4d5a8f9ffb34d Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 28 Sep 2026 13:27:15 +0200 Subject: [PATCH] test(frontend): lock the style invariants a QA pass keeps re-finding Two defects in this QA pass were pure CSS drift with no test surface at all: 100vw full-bleed sections panning the document by a scrollbar's width, and controls sitting on three different corner radii. Assert both against the sources, so the next person who hard-codes a radius or drops an overflow-x guard fails CI instead of failing a screenshot review. Tests now read files off disk, so they live in their own tsc project (tsconfig.test.json, node types) excluded from the browser build. --- frontend/.gitignore | 1 + .../src/features/landing/tests/style.test.ts | 33 +++++++++++++++++++ frontend/tsconfig.app.json | 4 ++- frontend/tsconfig.json | 1 + frontend/tsconfig.test.json | 27 +++++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 frontend/src/features/landing/tests/style.test.ts create mode 100644 frontend/tsconfig.test.json diff --git a/frontend/.gitignore b/frontend/.gitignore index 40cd782..6821953 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -2,3 +2,4 @@ node_modules dist dist-ssr *.local +*.tsbuildinfo diff --git a/frontend/src/features/landing/tests/style.test.ts b/frontend/src/features/landing/tests/style.test.ts new file mode 100644 index 0000000..6c00020 --- /dev/null +++ b/frontend/src/features/landing/tests/style.test.ts @@ -0,0 +1,33 @@ +import { readFileSync } from 'node:fs' +import { expect, test } from 'vitest' + +/** + * Style invariants. These are what a visual QA pass keeps re-finding, because nothing + * else stops them from regressing silently: a full-bleed section turning into a + * horizontal scrollbar, and controls drifting onto a corner radius different from the + * button sitting next to them. Reads the sources, so it is a rule about the CSS we ship, + * not about one browser's computed output. + */ +// vitest runs from the package root, the same place `bun run build` does. +const read = (rel: string) => readFileSync(rel, 'utf8') + +const indexCss = read('src/index.css') +const landingCss = read('src/features/landing/styles/landing.css') +const button = read('src/shared/ui/Button.tsx') +const textField = read('src/shared/ui/text-field/TextField.tsx') + +test('full-bleed 100vw sections cannot produce a horizontal scrollbar', () => { + expect(landingCss).toMatch(/\.lv-bleed\s*{[^}]*width:\s*100vw/) + // Clipping body alone is not enough: still pans by the width the 100vw + // sections overshoot the client width by. + expect(indexCss).toMatch(/html\s*{[^}]*overflow-x:\s*clip/) + expect(indexCss).toMatch(/body\s*{[^}]*overflow-x:\s*clip/) +}) + +test('every control shares one corner radius', () => { + expect(indexCss).toMatch(/--radius-ctl:\s*10px/) + expect(button).toContain('rounded-ctl') + expect(textField).toContain('rounded-ctl') + // A hard-coded radius here is how the CTAs ended up at 9.6px while buttons sat at 6px. + expect(landingCss).not.toMatch(/\.lv-cta[^{]*\{[^}]*border-radius:\s*(?!var\(--radius-ctl)\d/) +}) diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 6830b6f..af5db10 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -22,5 +22,7 @@ "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"] + "include": ["src"], + // Tests are a separate project (tsconfig.test.json): they need Node types. + "exclude": ["src/**/tests/*", "src/test"] } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1ffef60..4a3e749 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -2,6 +2,7 @@ "files": [], "references": [ { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.test.json" }, { "path": "./tsconfig.node.json" } ] } diff --git a/frontend/tsconfig.test.json b/frontend/tsconfig.test.json new file mode 100644 index 0000000..c94ffb2 --- /dev/null +++ b/frontend/tsconfig.test.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + // Tests read source files off disk (the style-invariant test), so they need + // the Node types the browser build deliberately excludes. + "types": ["node", "vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/tests/*", "src/test"] +}