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.
This commit is contained in:
parent
1c94e0ebea
commit
fd0fe59fee
5 changed files with 65 additions and 1 deletions
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
|
|
@ -2,3 +2,4 @@ node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
|
||||||
33
frontend/src/features/landing/tests/style.test.ts
Normal file
33
frontend/src/features/landing/tests/style.test.ts
Normal file
|
|
@ -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: <html> 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/)
|
||||||
|
})
|
||||||
|
|
@ -22,5 +22,7 @@
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"],
|
||||||
|
// Tests are a separate project (tsconfig.test.json): they need Node types.
|
||||||
|
"exclude": ["src/**/tests/*", "src/test"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.test.json" },
|
||||||
{ "path": "./tsconfig.node.json" }
|
{ "path": "./tsconfig.node.json" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
frontend/tsconfig.test.json
Normal file
27
frontend/tsconfig.test.json
Normal file
|
|
@ -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"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue