Files
clinicpro/assets/admin/lib/utils.test.ts
T
hamed b423a0ae4d refactor: normalize date handling to Tehran timezone
- Updated date handling in BlogSeoFields and ScheduleSection to use Tehran timezone utilities for consistency.
- Introduced `toTehranClockTime`, `tehranWallClockToUnix`, and `todayIso` functions for accurate date representation.
- Modified various components to utilize these new utilities, ensuring that date strings are correctly formatted and timestamps are accurately converted.
- Enhanced API documentation to clarify the handling of date fields, emphasizing the importance of server-local midnight.
- Added tests to verify that date overrides and holidays maintain the correct day without shifting due to timezone discrepancies.
2026-07-27 19:37:44 +03:30

327 lines
12 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 { z } from 'zod';
import {
digitsOnly,
formatYear,
parseUserNumber,
parseUserNumberClamped,
persianSafeNumber,
iranNationalCodeSchema,
iranNationalCodeOptionalSchema,
formatRial,
rialToToman,
tomanToRial,
formatNumber,
toDate,
formatDate,
formatDateTime,
toGregorianDate,
toTehranClockTime,
tehranWallClockToUnix,
unixToIso,
todayIso,
maskMobile,
cn,
toEnglishDigits,
sanitizeMobileInput,
isValidIranMobile,
IRAN_MOBILE_RE,
iranMobileSchema,
iranMobileOptionalSchema,
} from '@/lib/utils';
const PERSIAN_DIGITS = /[۰-۹]/;
describe('formatRial', () => {
it('مقدار ریال را ÷۱۰ به تومان تبدیل و با پسوند تومان برمی‌گرداند', () => {
const out = formatRial(150000); // ۱۵۰٬۰۰۰ ریال = ۱۵٬۰۰۰ تومان
expect(out).toContain('تومان');
expect(out).toMatch(PERSIAN_DIGITS);
expect(out).toContain(new Intl.NumberFormat('fa-IR').format(15000));
});
it('صفر را هم فرمت می‌کند', () => {
expect(formatRial(0)).toContain('تومان');
});
});
describe('rialToToman / tomanToRial', () => {
it('ریال↔تومان با ضریب ۱۰', () => {
expect(rialToToman(150000)).toBe(15000);
expect(tomanToRial(15000)).toBe(150000);
expect(tomanToRial(rialToToman(500))).toBe(500);
});
});
describe('formatNumber', () => {
it('رقم فارسی برمی‌گرداند', () => {
expect(formatNumber(1234)).toMatch(PERSIAN_DIGITS);
});
});
describe('toDate', () => {
it('null و رشته خالی → null', () => {
expect(toDate(null)).toBeNull();
expect(toDate(undefined)).toBeNull();
expect(toDate('')).toBeNull();
});
it('عدد را به‌عنوان ثانیه (×۱۰۰۰) تفسیر می‌کند', () => {
const d = toDate(1_700_000_000);
expect(d).toBeInstanceOf(Date);
expect(d!.getTime()).toBe(1_700_000_000 * 1000);
});
it('Y-m-d را ظهر محلی می‌گیرد (نه نیمه‌شب UTC)', () => {
const d = toDate('2024-03-21');
expect(d!.getHours()).toBe(12);
expect(d!.getFullYear()).toBe(2024);
});
it('رشته ISO کامل را پارس می‌کند', () => {
const d = toDate('2024-03-21T08:30:00Z');
expect(d).toBeInstanceOf(Date);
expect(isNaN(d!.getTime())).toBe(false);
});
});
describe('formatDate', () => {
it('ورودی نامعتبر → "—"', () => {
expect(formatDate(null)).toBe('—');
expect(formatDate('not-a-date')).toBe('—');
});
it('timestamp ثانیه‌ای معتبر → رشته شمسی با رقم فارسی', () => {
const out = formatDate(1_700_000_000);
expect(out).not.toBe('—');
expect(out).toMatch(PERSIAN_DIGITS);
});
});
describe('formatDateTime', () => {
it('ورودی نامعتبر → "—"', () => {
expect(formatDateTime(undefined)).toBe('—');
});
it('معتبر → شامل ساعت و رقم فارسی', () => {
const out = formatDateTime(1_700_000_000);
expect(out).not.toBe('—');
expect(out).toMatch(PERSIAN_DIGITS);
});
});
describe('toGregorianDate', () => {
it('YYYY-MM-DD با صفر پیش‌رو', () => {
expect(toGregorianDate(new Date(2024, 0, 5))).toBe('2024-01-05');
expect(toGregorianDate(new Date(2024, 11, 31))).toBe('2024-12-31');
});
});
describe('unixToIso / todayIso / toTehranClockTime', () => {
// نیمه‌شبِ ۲۹ ژوئیهٔ ۲۰۲۶ به وقت تهران = ۲۸ ژوئیه ۲۰:۳۰ UTC. تبدیل با
// toISOString همین‌جا یک روز عقب می‌رفت — رگرسیونِ «تاریخ خاص یک روز قبل».
const tehranMidnight = Math.floor(Date.UTC(2026, 6, 28, 20, 30, 0) / 1000);
it('timestamp نیمه‌شبِ تهران همان روز را می‌دهد، نه روز قبل', () => {
expect(unixToIso(tehranMidnight)).toBe('2026-07-29');
});
it('لحظهٔ آخر شب به وقت تهران هنوز همان روز است', () => {
const lateNight = Math.floor(Date.UTC(2026, 6, 29, 20, 0, 0) / 1000); // ۲۳:۳۰ تهران
expect(unixToIso(lateNight)).toBe('2026-07-29');
});
it('مقدار خالی رشتهٔ تهی می‌دهد', () => {
expect(unixToIso(null)).toBe('');
expect(unixToIso(undefined)).toBe('');
});
it('todayIso همان تاریخ تهرانِ لحظهٔ جاری است', () => {
expect(todayIso()).toBe(toGregorianDate(new Date()));
expect(todayIso()).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
it('ساعت دیواری تهران با رقم لاتین و ۲۴ساعته برمی‌گردد', () => {
expect(toTehranClockTime(new Date(tehranMidnight * 1000))).toBe('00:00');
expect(toTehranClockTime(new Date(Date.UTC(2026, 6, 29, 10, 0, 0)))).toBe('13:30');
});
it('رفت‌وبرگشتِ tehranWallClockToUnix ↔ unixToIso روز را جابه‌جا نمی‌کند', () => {
const ts = tehranWallClockToUnix('2026-07-29', '00:00');
expect(ts).toBe(tehranMidnight);
expect(unixToIso(ts)).toBe('2026-07-29');
});
});
describe('maskMobile', () => {
it('شماره ۱۱ رقمی را ماسک می‌کند', () => {
expect(maskMobile('09123456789')).toBe('0912***789');
});
it('شماره کوتاه‌تر از ۷ رقم بدون تغییر', () => {
expect(maskMobile('123')).toBe('123');
});
});
describe('cn', () => {
it('مقادیر falsy را حذف و بقیه را با فاصله می‌چسباند', () => {
expect(cn('a', false, null, undefined, 'b')).toBe('a b');
expect(cn()).toBe('');
});
});
describe('toEnglishDigits', () => {
it('رقم فارسی به انگلیسی', () => {
expect(toEnglishDigits('۰۹۱۲۳۴۵۶۷۸۹')).toBe('09123456789');
});
it('رقم عربی به انگلیسی', () => {
expect(toEnglishDigits('٠٩١٢')).toBe('0912');
});
it('ورودی خالی → رشته خالی', () => {
expect(toEnglishDigits('')).toBe('');
});
it('کاراکترهای غیررقمی را دست‌نخورده نگه می‌دارد', () => {
expect(toEnglishDigits('شماره ۰۹')).toBe('شماره 09');
});
});
describe('digitsOnly', () => {
it('رقم فارسی و عربی مخلوط را نرمال و غیررقم را حذف می‌کند', () => {
expect(digitsOnly('۱۲٣٤-56 ب')).toBe('123456');
});
it('با maxLen برش می‌زند', () => {
expect(digitsOnly('۱۲۳۴۵۶۷۸۹۰۱۲', 10)).toBe('1234567890');
});
it('ورودی خالی → رشته خالی', () => {
expect(digitsOnly('')).toBe('');
});
it('فقط حروف → خالی', () => {
expect(digitsOnly('کد ملی')).toBe('');
});
});
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());
expect(schema.parse('۱۲۳')).toBe(123);
});
it('عدد را دست‌نخورده رد می‌کند', () => {
const schema = persianSafeNumber(z.coerce.number());
expect(schema.parse(42)).toBe(42);
});
});
describe('iranNationalCodeSchema', () => {
it('کد ملی فارسی را می‌پذیرد و لاتین برمی‌گرداند', () => {
expect(iranNationalCodeSchema.parse('۰۰۱۲۳۴۵۶۷۸')).toBe('0012345678');
});
it('کمتر از ۱۰ رقم رد می‌شود', () => {
expect(() => iranNationalCodeSchema.parse('12345')).toThrow();
});
it('نسخه‌ی اختیاری خالی را می‌پذیرد', () => {
expect(iranNationalCodeOptionalSchema.parse('')).toBe('');
expect(() => iranNationalCodeOptionalSchema.parse('123')).toThrow();
});
});
describe('sanitizeMobileInput', () => {
it('غیررقم حذف، رقم فارسی نرمال، حداکثر ۱۱ رقم', () => {
expect(sanitizeMobileInput('۰۹۱۲-۳۴۵ ۶۷۸۹۰۱۲')).toBe('09123456789');
});
it('فقط حروف → خالی', () => {
expect(sanitizeMobileInput('abc')).toBe('');
});
});
describe('isValidIranMobile / IRAN_MOBILE_RE', () => {
it.each([
['09123456789', true],
['۰۹۱۲۳۴۵۶۷۸۹', true],
['9123456789', false],
['08123456789', false],
['0912345678', false],
['091234567890', false],
])('%s → %s', (input, expected) => {
expect(isValidIranMobile(input as string)).toBe(expected);
});
it('regex مستقیم روی ارقام انگلیسی', () => {
expect(IRAN_MOBILE_RE.test('09120000000')).toBe(true);
});
});
describe('iranMobileSchema', () => {
it('رقم فارسی را پذیرفته و نرمال می‌کند', () => {
expect(iranMobileSchema.parse('۰۹۱۲۳۴۵۶۷۸۹')).toBe('09123456789');
});
it('جداکننده‌ها را حذف می‌کند', () => {
expect(iranMobileSchema.parse('0912-345-6789')).toBe('09123456789');
});
it('شماره نامعتبر → ناموفق', () => {
const r = iranMobileSchema.safeParse('12345');
expect(r.success).toBe(false);
});
});
describe('iranMobileOptionalSchema', () => {
it('رشته خالی مجاز است', () => {
expect(iranMobileOptionalSchema.parse('')).toBe('');
});
it('شماره معتبر نرمال می‌شود', () => {
expect(iranMobileOptionalSchema.parse('۰۹۱۲۳۴۵۶۷۸۹')).toBe('09123456789');
});
it('شماره نیمه‌کاره → ناموفق', () => {
expect(iranMobileOptionalSchema.safeParse('0912').success).toBe(false);
});
});