import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, relative } from 'node:path'; import { describe, expect, it } from 'vitest'; /** * گاردِ تم: رنگ‌ها باید از توکن‌های `styles.css` بیایند تا دارک‌مود همه‌جا کار کند. * hex ثابت و کلاس‌های پالت Tailwind با تغییر تم عوض نمی‌شوند. */ const ROOT = join(__dirname, '..'); /** فاز ۳ — هنوز جفت slate/gray + dark: دارند؛ کار می‌کنند ولی از design-system منحرف‌اند. */ const DEFERRED = [ 'pages/DoctorDetailPage.tsx', 'pages/UserDetailPage.tsx', 'components/schedule/ScheduleSection.tsx', ]; /** رنگ‌های عمداً ثابت: دادهٔ کاربر یا پوششی که در هر دو تم یکسان است. */ const INTENTIONAL = [ 'pages/TagsSettingsPage.tsx', // TAG_COLORS — پالت انتخابی کاربر برای برچسب 'components/ImageCropModal.tsx', // bg-black/50 — اسکریم مودال، مثل .overlay 'stores/uiStore.ts', // BrandFixed — هیوی نارنجی عمداً hex ثابت است ]; const HEX = /#[0-9a-fA-F]{3,8}\b/; const PALETTE_UTIL = /(?:^|[\s"'`{])(?:dark:|hover:|focus:|active:|group-hover:|md:|lg:|sm:)*(?:bg|text|border|divide|ring|fill|stroke)-(?:white|black|gray|slate|zinc|neutral|stone|red|green|emerald|blue|sky|amber|yellow|orange|purple|indigo|violet|pink|rose|teal|cyan)(?:-\d{2,3})?\b/; function sourceFiles(): string[] { const out: string[] = []; (function walk(dir: string) { for (const entry of readdirSync(dir)) { const p = join(dir, entry); if (statSync(p).isDirectory()) { walk(p); continue; } if (!/\.tsx?$/.test(entry) || /\.test\.tsx?$/.test(entry)) continue; const rel = relative(ROOT, p); if (rel.startsWith('test/')) continue; out.push(rel); } })(ROOT); return out; } /** کامنت‌ها و fallbackهای `var(--x, #hex)` رنگِ رندرشده نیستند. */ function strip(src: string): string { return src .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/(^|[^:])\/\/.*$/gm, '$1') .replace(/var\(\s*--[a-z0-9-]+\s*,[^)]*\)/gi, 'var(--token)'); } describe('theme tokens', () => { const files = sourceFiles(); it('سورس ادمین را واقعاً پیدا می‌کند', () => { expect(files.length).toBeGreaterThan(100); }); it('هیچ کلاس dark: مرده‌ای کنار inline style رنگی نمانده است', () => { const offenders = files.filter((f) => { const src = readFileSync(join(ROOT, f), 'utf8'); return src.split('\n').some((line) => /style=\{\{[^}]*(?:color|background)/.test(line) && /className=[^>]*\bdark:(?:text|bg|border)-/.test(line)); }); expect(offenders).toEqual([]); }); it('فایل‌های پاک‌شده hex ثابت ندارند', () => { const offenders = files .filter((f) => ![...DEFERRED, ...INTENTIONAL].includes(f)) .filter((f) => HEX.test(strip(readFileSync(join(ROOT, f), 'utf8')))); expect(offenders).toEqual([]); }); it('فایل‌های پاک‌شده کلاس پالت Tailwind ندارند', () => { const offenders = files .filter((f) => ![...DEFERRED, ...INTENTIONAL].includes(f)) .filter((f) => PALETTE_UTIL.test(strip(readFileSync(join(ROOT, f), 'utf8')))); expect(offenders).toEqual([]); }); });