152 lines
5.5 KiB
TypeScript
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);
|
|
});
|
|
});
|