// 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);
});
});