feat(dashboard): implement Jalali month start calculation and add related tests

This commit is contained in:
hamed
2026-08-04 20:31:08 +03:30
parent df8a43f8b2
commit 9d767a6c63
4 changed files with 240 additions and 24 deletions
@@ -11,6 +11,40 @@ const real: ChartPoint[] = [
const EMPTY = 'داده‌ای برای نمایش نیست';
/**
* صفحه RTL است، پس در یک ردیفِ flex «اولین فرزند» سمت راست رندر می‌شود. ستون مقادیر
* باید سمت چپ بنشیند، یعنی باید فرزند **آخر** باشد و فاصلهٔ ردیف برچسب‌ها هم از چپ
* گرفته شود. جابه‌جا شدن این ترتیب، اعداد را بی‌صدا به سمت راست برمی‌گرداند.
*/
describe('ChartFrame — جای محور مقادیر', () => {
const frameOf = (container: HTMLElement) => {
const root = container.firstElementChild as HTMLElement;
return {
plotRow: root.firstElementChild as HTMLElement,
labelRow: root.lastElementChild as HTMLElement,
};
};
it('ستون مقادیر آخرین فرزند ردیف است تا در RTL سمت چپ بیفتد', () => {
const { container } = render(<TauriLineChart data={real} />);
const { plotRow } = frameOf(container);
const ticks = plotRow.lastElementChild as HTMLElement;
expect(ticks.textContent).toContain('۰');
expect(ticks.className).toContain('flex-col');
// نمودار خودش فرزند اول است، پس سمت راست می‌ماند.
expect((plotRow.firstElementChild as HTMLElement).className).toContain('overflow-hidden');
});
it('ردیف برچسب‌ها به اندازهٔ ستون مقادیر از چپ فاصله می‌گیرد', () => {
const { container } = render(<TauriLineChart data={real} />);
const { labelRow } = frameOf(container);
expect(labelRow.style.paddingLeft).not.toBe('');
expect(labelRow.style.paddingRight).toBe('');
});
});
describe('TauriBarChart', () => {
it('shows the empty state when every value is zero', () => {
render(<TauriBarChart data={zero} />);
@@ -165,3 +199,59 @@ describe('TauriLineChart — ادامهٔ پیش‌بینی', () => {
expect(container.querySelector('.td-forecast')).toBeNull();
});
});
/**
* محور زمان راست‌به‌چپ است: برچسب‌ها در جریان RTL چیده می‌شوند، پس فروردین سمت راست
* می‌نشیند. SVG جریان RTL ندارد و اگر اندیس ۰ به `x=0` برود، خط نسبت به برچسب‌هایش
* آینه می‌شود — دقیقاً باگی که دادهٔ واقعیِ ابتدای سال را روی ماه‌های پایانی می‌انداخت.
*/
describe('TauriLineChart — جهت محور زمان', () => {
const year: ChartPoint[] = Array.from({ length: 12 }, (_, i) => ({
label: `ماه ${i + 1}`,
value: i < 4 ? (i + 1) * 10 : 0,
}));
/** ستونِ هاورِ هر نقطه دقیقاً حول همان نقطه می‌نشیند، پس `left`ش جای نقطه را می‌گوید. */
const hitCenters = (container: HTMLElement): number[] =>
Array.from(container.querySelectorAll<HTMLElement>('.td-hit')).map((el) => {
const left = parseFloat(el.style.left);
const width = parseFloat(el.style.width);
return left + width / 2;
});
it('اولین نقطه سمت راست و آخرین نقطه سمت چپ می‌نشیند', () => {
const { container } = render(<TauriLineChart data={year} actualCount={4} />);
const centers = hitCenters(container);
expect(centers[0]).toBeCloseTo(100, 5);
expect(centers[centers.length - 1]).toBeCloseTo(0, 5);
});
it('نقطه‌ها به ترتیب زمان از راست به چپ پیش می‌روند', () => {
const { container } = render(<TauriLineChart data={year} actualCount={4} />);
const centers = hitCenters(container);
for (let i = 1; i < centers.length; i++) {
expect(centers[i]).toBeLessThan(centers[i - 1]);
}
});
/** بخش واقعی سمت راست است و پیش‌بینی سمت چپ، نه برعکس. */
it('بخش واقعی راست‌تر از بخش پیش‌بینی است', () => {
const { container } = render(<TauriLineChart data={year} actualCount={4} />);
const centers = hitCenters(container);
const lastActual = centers[3];
const firstForecast = centers[4];
expect(lastActual).toBeGreaterThan(firstForecast);
});
/** نقطه باید روی خط بنشیند، نه داخل ستونِ خودش سُر بخورد. */
it('نقطه در مرکز ستون هاورِ خودش است', () => {
const { container } = render(<TauriLineChart data={year} actualCount={4} />);
for (const dot of Array.from(container.querySelectorAll<HTMLElement>('.td-dot'))) {
expect(dot.style.left).toBe('50%');
}
});
});