Add tests and implementation for ServiceDetailPage and PriceInput components

- Implement PriceInput component tests to validate Persian and Arabic numeral handling, input formatting, and controlled behavior.
- Create ServiceDetailPage component with detailed service information, including pricing, insurance coverage, and editing capabilities.
- Add API tests for service item detail retrieval and coverage synchronization with insurance contracts.
- Ensure proper error handling and user feedback for service item retrieval and coverage management.
This commit is contained in:
hamed
2026-07-18 12:10:49 +03:30
parent f1d58e1604
commit 42d9ad26c5
27 changed files with 1758 additions and 284 deletions
+61
View File
@@ -2,6 +2,9 @@ import { describe, it, expect } from 'vitest';
import { z } from 'zod';
import {
digitsOnly,
formatYear,
parseUserNumber,
parseUserNumberClamped,
persianSafeNumber,
iranNationalCodeSchema,
iranNationalCodeOptionalSchema,
@@ -150,6 +153,64 @@ describe('digitsOnly', () => {
});
});
describe('formatYear', () => {
it('سال شمسی را بدون جداکننده‌ی هزارگان می‌دهد', () => {
expect(formatYear(1404)).toBe('۱۴۰۴');
});
it('برخلاف formatNumber جداکننده نمی‌گذارد', () => {
expect(formatNumber(1404)).not.toBe(formatYear(1404));
});
});
describe('parseUserNumber', () => {
it('رقم فارسی را به عدد تبدیل می‌کند', () => {
expect(parseUserNumber('۲۵')).toBe(25);
});
it('رقم عربی را به عدد تبدیل می‌کند', () => {
expect(parseUserNumber('٢٥')).toBe(25);
});
it('جداکننده هزارگان و فاصله را نادیده می‌گیرد', () => {
expect(parseUserNumber('1,200')).toBe(1200);
expect(parseUserNumber('۸۵٬۰۰۰')).toBe(85000);
expect(parseUserNumber(' ۹۰ ')).toBe(90);
});
it('اعشار فارسی را می‌پذیرد', () => {
expect(parseUserNumber('۱۲.۵')).toBe(12.5);
});
it('عدد منفی را می‌پذیرد', () => {
expect(parseUserNumber('-۳')).toBe(-3);
});
it('عدد را دست‌نخورده رد می‌کند', () => {
expect(parseUserNumber(42)).toBe(42);
});
it('خالی و null → null', () => {
expect(parseUserNumber('')).toBeNull();
expect(parseUserNumber(null)).toBeNull();
expect(parseUserNumber(undefined)).toBeNull();
});
it('ورودی نامعتبر → null و هرگز NaN', () => {
expect(parseUserNumber('abc')).toBeNull();
expect(parseUserNumber('۱۲ب۳')).toBeNull();
expect(parseUserNumber('.')).toBeNull();
});
});
describe('parseUserNumberClamped', () => {
it('مقدار داخل بازه را دست‌نخورده برمی‌گرداند', () => {
expect(parseUserNumberClamped('۲۵', 0, 100)).toBe(25);
});
it('بیشتر از سقف را به سقف می‌برد', () => {
expect(parseUserNumberClamped('۱۵۰', 0, 100)).toBe(100);
});
it('کمتر از کف را به کف می‌برد', () => {
expect(parseUserNumberClamped('-۵', 0, 100)).toBe(0);
});
it('ورودی نامعتبر → null (نه کف)', () => {
expect(parseUserNumberClamped('abc', 0, 100)).toBeNull();
expect(parseUserNumberClamped('', 0, 100)).toBeNull();
});
});
describe('persianSafeNumber', () => {
it('رشته‌ی فارسی را قبل از عدد شدن نرمال می‌کند', () => {
const schema = persianSafeNumber(z.coerce.number());
+27
View File
@@ -14,6 +14,11 @@ export function formatNumber(n: number): string {
return new Intl.NumberFormat('fa-IR').format(n);
}
// سال شمسی: رقم فارسی بدون جداکننده‌ی هزارگان («۱۴۰۴» نه «۱٬۴۰۴»).
export function formatYear(year: number): string {
return new Intl.NumberFormat('fa-IR', { useGrouping: false }).format(year);
}
// تایم‌زون رسمی سراسری برنامه = ایران. همهٔ نمایش/تبدیل تاریخ باید با این tz باشد،
// مستقل از تایم‌زون مرورگرِ کاربر (اجباری).
export const APP_TZ = 'Asia/Tehran';
@@ -122,6 +127,28 @@ export function sanitizeMobileInput(input: string): string {
return digitsOnly(input, 11);
}
/**
* رشته‌ی ورودی کاربر (ارقام فارسی/عربی، جداکننده‌ی هزارگان، فاصله) را به عدد امن تبدیل می‌کند.
* هرگز NaN برنمی‌گرداند؛ ورودی نامعتبر یا خالی ⇒ null.
*/
export function parseUserNumber(raw: string | number | null | undefined): number | null {
if (raw == null || raw === '') return null;
const s = toEnglishDigits(String(raw)).replace(/[,\s٫٬]/g, '');
if (!/^-?(\d+\.?\d*|\.\d+)$/.test(s)) return null;
const n = Number(s);
return Number.isFinite(n) ? n : null;
}
/** همان parseUserNumber با محدودکردن بازه — برای درصد (۰..۱۰۰) و مقادیر غیرمنفی. */
export function parseUserNumberClamped(
raw: string | number | null | undefined,
min: number,
max: number,
): number | null {
const n = parseUserNumber(raw);
return n == null ? null : Math.min(max, Math.max(min, n));
}
// z.coerce.number() روی رشته‌ی فارسی NaN می‌دهد. فیلدهای عددی پنل در مبدأ (numericField
// در lib/forms.ts) نرمال می‌شوند، پس این wrapper فقط برای مصرف‌کننده‌های خارج از آن مسیر است.
// روی resolverهای React Hook Form استفاده نکن — z.preprocess تایپ ورودی را unknown می‌کند.