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:
@@ -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());
|
||||
|
||||
@@ -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 میکند.
|
||||
|
||||
Reference in New Issue
Block a user