Files
maestro/scripts/check-tailwind-opacity.test.ts
T
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

152 lines
5.5 KiB
TypeScript

// scripts/check-tailwind-opacity.mjs が本当に効くことを守るテスト (#1377)。
//
// 守る対象は「いま何件あるか」ではなく、**効かない組み合わせを書いたら赤くなる
// 経路が残っていること**。控え(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-tailwind-opacity.mjs');
/** 不透明度の置き場がある色(黒・白・<alpha-value> 付き)と、無い色(var())。 */
const FIXTURE_CONFIG = `export default {
content: ['./index.html', './src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
accent: 'var(--brand-primary, #18181b)',
'accent-soft': 'var(--brand-primary-soft, #f4f4f5)',
themed: 'rgb(var(--themed) / <alpha-value>)',
plain: '#123456',
slate: { 900: 'var(--slate-900)' },
},
},
},
};
`;
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(), 'tw-opacity-'));
// 見つけ先の tailwind.config.js を ESM として読ませる。
write('ui/package.json', '{ "type": "module" }\n');
write('ui/tailwind.config.js', FIXTURE_CONFIG);
// index.html も content に載っているので、こちらの 1 件も拾われる。
write('ui/index.html', '<div class="bg-plain/50 bg-accent/5"></div>\n');
write(
'ui/src/Panel.tsx',
[
'export const Panel = () => (',
' <div className="bg-slate-900/50 dark:bg-accent-soft/80 border-accent/30">',
' <span className="bg-black/40 bg-white/50 bg-themed/60 bg-plain/25 bg-[rgb(15_23_42_/_0.5)]" />',
' </div>',
');',
].join('\n'),
);
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
describe('check-tailwind-opacity', () => {
it('不透明度の置き場が無い色だけを拾う(黒・白・<alpha-value>・具体値は拾わない)', () => {
const written = run('--update');
expect(written.status).toBe(0);
const found = JSON.parse(readFileSync(baseline(), 'utf8')) as {
total: number;
entries: { file: string; class: string; count: number }[];
};
const classes = found.entries.map((e) => e.class).sort();
// 変化形(dark: 等)は付いたまま数える。安全な色(黒・白・<alpha-value>・
// 具体値・すでに直した arbitrary value)は 1 件も混じらない。
expect(classes).toEqual(
['bg-accent/5', 'dark:bg-accent-soft/80', 'bg-slate-900/50', 'border-accent/30'].sort(),
);
expect(found.total).toBe(4);
expect(found.entries.some((e) => e.file.endsWith('index.html'))).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 = () => <div className="bg-slate-900/25" />;\n');
const check = run();
expect(check.status).toBe(1);
expect(check.stderr).toContain('bg-slate-900/25');
expect(check.stderr).toContain('New.tsx');
});
it('同じファイルで件数が増えても落ちる', () => {
expect(run('--update').status).toBe(0);
write(
'ui/src/Panel.tsx',
[
'export const Panel = () => (',
' <div className="bg-slate-900/50 dark:bg-accent-soft/80 border-accent/30">',
' <span className="bg-slate-900/50" />',
' </div>',
');',
].join('\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/Panel.tsx',
'export const Panel = () => <div className="bg-[rgb(15_23_42_/_0.5)]" />;\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);
});
});