Files
clinicpro/assets/admin/components/dashboard/TauriCharts.test.tsx
T
hamed 55ab2f5dfc Implement comprehensive dark/light mode overhaul for admin panel
- Refactor color palette in `ui-design-spec.md` to utilize CSS variables exclusively, eliminating fixed hex values and Tailwind utility classes.
- Complete dark mode implementation in `uiStore.ts`, ensuring proper theme application via `applyTheme()` and `applyBrand()`.
- Create `admin-theme-dark-light-audit.md` to document the transition process, outlining issues with inline styles and fixed colors.
- Introduce `theme-tokens.test.ts` to enforce rules against fixed hex colors and ensure compliance with the design system.
- Update various components and styles to replace inline styles and fixed colors with CSS variables, ensuring consistent theming across light and dark modes.
- Ensure all changes maintain visual integrity in both light and dark modes, with a focus on accessibility and contrast standards.
2026-07-27 16:41:52 +03:30

168 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(<TauriBarChart data={zero} />);
expect(screen.getByText(EMPTY)).toBeInTheDocument();
});
it('shows the empty state when there is no data', () => {
render(<TauriBarChart data={[]} />);
expect(screen.getByText(EMPTY)).toBeInTheDocument();
});
it('renders the chart (x labels, no empty state) with real data', () => {
render(<TauriBarChart data={real} />);
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(<TauriBarChart data={month} />);
expect(container.querySelectorAll('[class*="bg-[var(--primary)]"]')).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(<TauriBarChart data={month} />);
const bar = container.querySelector('[class*="bg-[var(--primary)]"]') 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(<TauriBarChart data={real} />);
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(<TauriLineChart data={zero} />);
expect(screen.getByText(EMPTY)).toBeInTheDocument();
});
it('shows the empty state with fewer than two points', () => {
render(<TauriLineChart data={[{ label: 'x', value: 5 }]} />);
expect(screen.getByText(EMPTY)).toBeInTheDocument();
});
it('renders the chart with real data', () => {
const { container } = render(<TauriLineChart data={real} />);
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(<TauriLineChart data={real} />);
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(<TauriLineChart data={real} />);
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(<TauriLineChart data={year} />);
expect(container.querySelector('.td-forecast')).toBeNull();
expect(screen.queryByText('نقطه‌چین: پیش‌بینی')).not.toBeInTheDocument();
});
it('با actualCount ماه‌های باقی‌مانده را نقطه‌چین ادامه می‌دهد', () => {
const { container } = render(<TauriLineChart data={year} actualCount={4} />);
const forecast = container.querySelector('.td-forecast');
expect(forecast).toBeTruthy();
expect(forecast!.getAttribute('stroke')).toBe('var(--accent)');
expect(screen.getByText('نقطه‌چین: پیش‌بینی')).toBeInTheDocument();
});
it('روند صعودی را ادامه می‌دهد و صفرها را رسم نمی‌کند', () => {
render(<TauriLineChart data={year} actualCount={4} />);
// شیب ۱۰ در ماه، آخرین واقعی ۴۰ ⇒ ماه پنجم ۵۰
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(<TauriLineChart data={falling} actualCount={3} />);
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(<TauriLineChart data={full} actualCount={12} />);
expect(container.querySelector('.td-forecast')).toBeNull();
});
});