This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user