44 lines
1.6 KiB
TypeScript
44 lines
1.6 KiB
TypeScript
import fs from 'fs';
|
|||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Guards against "stranded" CSS: when markup moves between components, its
|
||
|
|
* rules must move with it. CSS Modules hash class names per file, so a rule
|
||
|
|
* left behind in another stylesheet silently stops applying — exactly how the
|
||
|
|
* header's mobile `.headerDetails { display: none }` (and the section-nav
|
||
|
|
* mobile swap) stopped working when the detail view was split into a client
|
||
|
|
* shell plus server sections.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const SCHOOL_DIR = path.join(__dirname, '..', '..', 'components', 'school');
|
||
|
|
|
||
|
|
function classesDefinedIn(css: string): Set<string> {
|
||
|
|
return new Set(Array.from(css.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g), (m) => m[1]));
|
||
|
|
}
|
||
|
|
|
||
|
|
function classesUsedIn(tsx: string): Set<string> {
|
||
|
|
return new Set(Array.from(tsx.matchAll(/\bstyles\.([_a-zA-Z][\w]*)/g), (m) => m[1]));
|
||
|
|
}
|
||
|
|
|
||
|
|
const components = fs
|
||
|
|
.readdirSync(SCHOOL_DIR)
|
||
|
|
.filter((f) => f.endsWith('.tsx'))
|
||
|
|
.map((file) => {
|
||
|
|
const source = fs.readFileSync(path.join(SCHOOL_DIR, file), 'utf8');
|
||
|
|
const importMatch = source.match(/import\s+styles\s+from\s+['"](.+?\.module\.css)['"]/);
|
||
|
|
return { file, source, stylesheet: importMatch?.[1] };
|
||
|
|
})
|
||
|
|
.filter((c) => c.stylesheet);
|
||
|
|
|
||
|
|
describe('school detail stylesheets', () => {
|
||
|
|
it.each(components.map((c) => [c.file, c.source, c.stylesheet as string]))(
|
||
|
|
'%s only uses classes its own stylesheet defines',
|
||
|
|
(_file, source, stylesheet) => {
|
||
|
|
const css = fs.readFileSync(path.join(SCHOOL_DIR, stylesheet), 'utf8');
|
||
|
|
const defined = classesDefinedIn(css);
|
||
|
|
const missing = Array.from(classesUsedIn(source)).filter((c) => !defined.has(c));
|
||
|
|
expect(missing).toEqual([]);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
});
|