import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import { TauriBarChart, TauriLineChart, type ChartPoint } from './TauriCharts'; const zero: ChartPoint[] = Array.from({ length: 7 }, (_, i) => ({ label: `روز ${i}`, value: 0 })); const real: ChartPoint[] = [ { label: '۷ خرداد', value: 15 }, { label: '۸ خرداد', value: 40 }, { label: '۹ خرداد', value: 48 }, ]; const EMPTY = 'داده‌ای برای نمایش نیست'; describe('TauriBarChart', () => { it('shows the empty state when every value is zero', () => { render(); expect(screen.getByText(EMPTY)).toBeInTheDocument(); }); it('shows the empty state when there is no data', () => { render(); expect(screen.getByText(EMPTY)).toBeInTheDocument(); }); it('renders the chart (x labels, no empty state) with real data', () => { render(); expect(screen.queryByText(EMPTY)).not.toBeInTheDocument(); expect(screen.getByText('۸ خرداد')).toBeInTheDocument(); }); it('renders one bar per day of a full Jalali month', () => { const month: ChartPoint[] = Array.from({ length: 31 }, (_, i) => ({ label: String(i + 1), value: i % 5 })); const { container } = render(); expect(container.querySelectorAll('.bg-\\[\\#5559CE\\]')).toHaveLength(31); }); /** * A fixed-width bar makes its column's intrinsic min-width 24px, so a 31-day * month sums past the plot width and the row overflows out of the card * (bars land on the neighbouring chart). Columns must be shrinkable and the * bar fluid-but-capped. */ it('keeps a dense month inside the card: shrinkable columns, fluid bars', () => { const month: ChartPoint[] = Array.from({ length: 31 }, (_, i) => ({ label: String(i + 1), value: i % 5 })); const { container } = render(); const bar = container.querySelector('.bg-\\[\\#5559CE\\]') as HTMLElement; // کسری از ستون، نه تمام آن — وگرنه میله‌ها بدون فاصله به هم می‌چسبند expect(bar.className).toMatch(/\bw-\[\d+%\]/); expect(bar.className.split(/\s+/)).not.toContain('w-full'); expect(bar.className).toContain('max-w-[24px]'); // فقط سقف عرض، نه عرض ثابت (max-w-[24px] خودش شامل رشته‌ی w-[24px] است) expect(bar.className.split(/\s+/)).not.toContain('w-[24px]'); const column = bar.parentElement as HTMLElement; expect(column.className).toContain('min-w-0'); const plot = container.querySelector('.relative.flex-1') as HTMLElement; expect(plot.className).toContain('overflow-hidden'); }); /** * styles.css ships an unlayered legacy `.flex { align-items: center }` that * beats Tailwind's `items-*` utilities and collapses the plot column to zero * height. The frame must pin alignment inline, or the chart renders blank. */ it('pins flex alignment inline so the legacy global .flex cannot collapse it', () => { const { container } = render(); const frame = container.querySelector('.h-\\[300px\\]') as HTMLElement; expect(frame.style.alignItems).toBe('stretch'); expect((frame.firstElementChild as HTMLElement).style.alignItems).toBe('stretch'); }); }); describe('TauriLineChart', () => { it('shows the empty state when every value is zero', () => { render(); expect(screen.getByText(EMPTY)).toBeInTheDocument(); }); it('shows the empty state with fewer than two points', () => { render(); expect(screen.getByText(EMPTY)).toBeInTheDocument(); }); it('renders the chart with real data', () => { const { container } = render(); expect(screen.queryByText(EMPTY)).not.toBeInTheDocument(); expect(container.querySelector('svg path')).toBeTruthy(); }); /** سبک «gradient line»: خط از گرادیان افقی رنگ می‌گیرد، نه یک رنگ ثابت. */ it('paints the line with the horizontal gradient and a glow', () => { const { container } = render(); const line = container.querySelectorAll('svg path')[1] as SVGPathElement; expect(line.getAttribute('stroke')).toBe('url(#tdIncomeStroke)'); expect(line.getAttribute('filter')).toBe('url(#tdIncomeGlow)'); expect(container.querySelector('#tdIncomeStroke')).toBeTruthy(); expect(container.querySelector('#tdIncomeGlow feDropShadow')).toBeTruthy(); }); /** نقطه و تولتیپ برای هر داده ساخته می‌شود و با هاور نمایان می‌شود. */ it('renders a hover marker and value tooltip per point', () => { const { container } = render(); expect(container.querySelectorAll('.td-hit')).toHaveLength(real.length); expect(container.querySelectorAll('.td-dot')).toHaveLength(real.length); // مقدارها با ارقام فارسی در تولتیپ expect(screen.getByText('۴۸')).toBeInTheDocument(); }); }); /** * ماه‌های نیامده صفر برمی‌گردند؛ رسم‌کردنشان یک خط صاف تا انتهای سال می‌ساخت. * از آخرین دادهٔ واقعی به بعد باید روند نقطه‌چین ادامه پیدا کند. */ describe('TauriLineChart — ادامهٔ پیش‌بینی', () => { /** ۴ ماه واقعیِ صعودی + ۸ ماه نیامده (صفر) */ const year: ChartPoint[] = Array.from({ length: 12 }, (_, i) => ({ label: `ماه ${i + 1}`, value: i < 4 ? (i + 1) * 10 : 0, })); it('بدون actualCount خط پیش‌بینی ندارد (رفتار قبلی حفظ می‌شود)', () => { const { container } = render(); expect(container.querySelector('.td-forecast')).toBeNull(); expect(screen.queryByText('نقطه‌چین: پیش‌بینی')).not.toBeInTheDocument(); }); it('با actualCount ماه‌های باقی‌مانده را نقطه‌چین ادامه می‌دهد', () => { const { container } = render(); const forecast = container.querySelector('.td-forecast'); expect(forecast).toBeTruthy(); expect(forecast!.getAttribute('stroke')).toBe('var(--accent)'); expect(screen.getByText('نقطه‌چین: پیش‌بینی')).toBeInTheDocument(); }); it('روند صعودی را ادامه می‌دهد و صفرها را رسم نمی‌کند', () => { render(); // شیب ۱۰ در ماه، آخرین واقعی ۴۰ ⇒ ماه پنجم ۵۰ expect(screen.getByText('پیش‌بینی: ۵۰')).toBeInTheDocument(); // آخرین ماه: ۴۰ + ۸×۱۰ = ۱۲۰ expect(screen.getByText('پیش‌بینی: ۱۲۰')).toBeInTheDocument(); }); it('پیش‌بینی هرگز منفی نمی‌شود', () => { const falling: ChartPoint[] = Array.from({ length: 12 }, (_, i) => ({ label: `ماه ${i + 1}`, value: i < 3 ? 100 - i * 45 : 0, })); const { container } = render(); const tips = Array.from(container.querySelectorAll('.td-tip')) .map((el) => el.textContent ?? '') .filter((t) => t.startsWith('پیش‌بینی')); expect(tips.length).toBeGreaterThan(0); expect(tips.some((t) => t.includes('-') || t.includes('−'))).toBe(false); }); it('سال کامل (actualCount برابر طول داده) پیش‌بینی ندارد', () => { const full: ChartPoint[] = Array.from({ length: 12 }, (_, i) => ({ label: `ماه ${i + 1}`, value: 10 + i })); const { container } = render(); expect(container.querySelector('.td-forecast')).toBeNull(); }); });