Files

44 lines
1.6 KiB
TypeScript
Raw Permalink Normal View History

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([]);
},
);
});