import { describe, it, expect, afterEach } from 'vitest'; import { anchorSelector, resolveSteps } from './resolveSteps'; import { getTour, TOURS } from './registry'; import type { TourStep } from './types'; const STEPS: TourStep[] = [ { anchor: 'one', title: 'یک', body: '…' }, { anchor: 'two', title: 'دو', body: '…' }, { anchor: 'three', title: 'سه', body: '…' }, ]; function mount(...anchors: string[]) { document.body.innerHTML = anchors.map((a) => `
`).join(''); } afterEach(() => { document.body.innerHTML = ''; }); describe('resolveSteps', () => { it('استپ‌های موجود را با حفظ ترتیب نگه می‌دارد', () => { mount('three', 'one'); expect(resolveSteps(STEPS).map((s) => s.anchor)).toEqual(['one', 'three']); }); it('وقتی هیچ المانی نیست، خروجی خالی است', () => { expect(resolveSteps(STEPS)).toEqual([]); }); it('آرایهٔ خالی خروجی خالی می‌دهد', () => { mount('one'); expect(resolveSteps([])).toEqual([]); }); it('سلکتور از روی anchor ساخته می‌شود', () => { expect(anchorSelector('appointments-stats')).toBe('[data-tour="appointments-stats"]'); }); }); describe('registry', () => { it('برای شناسهٔ ناشناس یا خالی null می‌دهد', () => { expect(getTour('does-not-exist')).toBeNull(); expect(getTour(undefined)).toBeNull(); }); it('تور نوبت‌ها ثبت شده است', () => { expect(getTour('appointments')?.id).toBe('appointments'); }); it('هر تور نسخهٔ معتبر و anchorهای بدون تکرار دارد', () => { for (const [id, tour] of Object.entries(TOURS)) { expect(tour.id).toBe(id); expect(tour.version).toBeGreaterThanOrEqual(1); expect(tour.steps.length).toBeGreaterThan(0); const anchors = tour.steps.map((s) => s.anchor); expect(new Set(anchors).size).toBe(anchors.length); } }); it('متن هر استپ فارسی و غیرخالی است', () => { for (const tour of Object.values(TOURS)) { for (const step of tour.steps) { expect(step.title.trim().length).toBeGreaterThan(2); expect(step.body.trim().length).toBeGreaterThan(10); // متن راهنما برای کاربر فارسی‌زبان است؛ استپِ انگلیسی‌مانده یعنی جا افتاده. expect(step.title).toMatch(/[؀-ۿ]/); expect(step.body).toMatch(/[؀-ۿ]/); } } }); }); /** * لنگرِ بی‌المان بی‌صدا حذف می‌شود، پس تورِ خراب هیچ خطایی نمی‌دهد و فقط استپش را * از دست می‌دهد. این تست همان سکوت را می‌شکند: هر anchor باید در کدِ صفحات وجود * داشته باشد. */ describe('انطباق لنگرها با کد صفحات', () => { it('هیچ استپی به data-tour ناموجود اشاره نمی‌کند', async () => { // خواندن مستقیم فایل‌ها، نه import.meta.glob: آن فقط زیر Vite کار می‌کند و // ts-loaderِ Encore همین فایل را هم type-check می‌کند و build را قرمز می‌کرد. const { readdirSync, readFileSync } = await import('node:fs'); const { join } = await import('node:path'); const walk = (dir: string): string[] => readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { const path = join(dir, entry.name); if (entry.isDirectory()) return walk(path); return entry.name.endsWith('.tsx') && !entry.name.includes('.test.') ? [path] : []; }); const sources = walk('assets/admin').map((path) => readFileSync(path, 'utf8')); const declared = new Set( sources.flatMap((src) => [...src.matchAll(/data-tour="([a-z0-9-]+)"/g)].map((m) => m[1])), ); expect(declared.size).toBeGreaterThan(0); const orphans = Object.values(TOURS).flatMap((tour) => tour.steps.filter((s) => !declared.has(s.anchor)).map((s) => `${tour.id}:${s.anchor}`), ); expect(orphans).toEqual([]); }); });