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); }); });