// scripts/check-dark-text.mjs が本当に効くことを守るテスト (#1388)。 // // 守る対象は「いま何件あるか」ではなく、**暗い外観で二度反転する書き方を新しく // 足したら赤くなる経路が残っていること**。控え(baseline)方式は放っておくと // 「全部控えに入れて常に緑」になりやすいので、増えたら落ちる・減っても落ちない、 // の両方を固定する。 import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); const script = join(repoRoot, 'scripts', 'check-dark-text.mjs'); // 見張りは content の並びから走査対象を決めるので、fixture にも同じ形の設定を置く。 const FIXTURE_CONFIG = `export default { content: ['./index.html', './src/**/*.{ts,tsx}'], darkMode: ['selector', '[data-theme="dark"]'], theme: { extend: { colors: { slate: { 200: 'var(--slate-200)' } } } }, }; `; // テスト自身のソースに丸ごとの class 名を書くと、このリポジトリ本体の控えに // 数えられてしまう。組み立てて使う。 const DARK = 'dark:'; const T = (n: number, ...variants: string[]) => `${DARK}${variants.join('')}text-slate-${n}`; let dir: string; const ui = () => join(dir, 'ui'); const baseline = () => join(dir, 'baseline.json'); function write(rel: string, body: string) { const full = join(dir, rel); mkdirSync(dirname(full), { recursive: true }); writeFileSync(full, body); } function run(...extra: string[]) { return spawnSync(process.execPath, [script, '--ui', ui(), '--baseline', baseline(), ...extra], { cwd: repoRoot, encoding: 'utf8', }); } beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'dark-text-')); write('ui/package.json', '{ "type": "module" }\n'); write('ui/tailwind.config.js', FIXTURE_CONFIG); // index.html も content に載っているので、こちらの 1 件も拾われる。 write('ui/index.html', `
\n`); write( 'ui/src/Panel.tsx', [ 'export const Panel = () => (', `
`, ` `, '
', ');', ].join('\n'), ); // .ts も content に載っている(ui/src/lib/workspaceDirs.ts がこの形)。 write('ui/src/roles.ts', `export const badge = 'bg-slate-100 text-slate-600 ${T(300)}';\n`); }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); describe('check-dark-text', () => { it('dark: 付きの text-slate だけを拾う(素の指定・別の色・別の性質は拾わない)', () => { expect(run('--update').status).toBe(0); const found = JSON.parse(readFileSync(baseline(), 'utf8')) as { total: number; files: number; entries: { file: string; class: string; count: number }[]; }; const classes = found.entries.map((e) => e.class).sort(); // 変化形は前にも後ろにも付いたまま数える。素の text-slate-*、dark:bg-slate-*、 // dark:text-emerald-* は 1 件も混じらない。 expect(classes).toEqual( [T(200), `group-hover:${T(400)}`, T(300, 'hover:'), T(300)].sort(), ); expect(found.total).toBe(4); expect(found.entries.some((e) => e.file.endsWith('index.html'))).toBe(true); expect(found.entries.some((e) => e.file.endsWith('roles.ts'))).toBe(true); }); it('控えと同じなら通る', () => { expect(run('--update').status).toBe(0); const check = run(); expect(check.status).toBe(0); expect(check.stdout).toContain('no new ones'); }); it('新しいファイルに 1 つ足すと落ちる', () => { expect(run('--update').status).toBe(0); write('ui/src/New.tsx', `export const New = () =>
;\n`); const check = run(); expect(check.status).toBe(1); expect(check.stderr).toContain(T(200)); expect(check.stderr).toContain('New.tsx'); // 落ちたときのメッセージが、なぜ駄目かと直し方を持っていること。 expect(check.stderr).toContain('INVERTS'); expect(check.stderr).toContain('-> text-slate-700'); }); it('同じファイルで件数が増えても落ちる', () => { expect(run('--update').status).toBe(0); write('ui/index.html', `
\n
\n`); const check = run(); expect(check.status).toBe(1); expect(check.stderr).toContain('baseline allows 1, found 2'); }); it('控えにある分を直したら、通したうえで控えの更新を促す', () => { expect(run('--update').status).toBe(0); write('ui/src/roles.ts', "export const badge = 'bg-slate-100 text-slate-600';\n"); const check = run(); expect(check.status).toBe(0); expect(check.stdout).toContain('-- --update'); }); it('控えが無ければ落ちる(黙って通さない)', () => { const check = run(); expect(check.status).toBe(1); expect(check.stderr).toContain('--update'); }); it('このリポジトリ自身も控えと一致している', () => { if (!existsSync(join(repoRoot, 'ui', 'node_modules'))) return; // 依存未導入なら見送る const check = spawnSync(process.execPath, [script], { cwd: repoRoot, encoding: 'utf8' }); expect(check.stderr + check.stdout).toContain('OK'); expect(check.status).toBe(0); }); });