feat: add testing setup with Vitest and Testing Library
- Updated package.json to include Vitest and Testing Library dependencies and scripts for testing. - Created a test suite for the ProvinceProvider context to validate cityId and province detection based on subdomains. - Implemented unit tests for utility functions in helper/index.js, including phone number formatting and validation. - Added tests for state information retrieval in lib/getStateInfo.js, ensuring correct city and state matching based on subdomains. - Developed tests for appointment slot adaptation and availability checks in lib/appointmentSlots.js. - Created tests for token storage functionality in lib/tokenStore.js. - Implemented sanitization and JSON parsing tests in lib/sanitize.js. - Added CASL ability tests in lib/ability.js to verify user access rights. - Created tests for cookie management in lib/refreshCookie.js. - Developed tests for patient user representation in lib/representationAdapters.js. - Implemented client-side state information retrieval tests in lib/getStateInfoClient.js. - Created tests for canonical URL generation in lib/getCanonicalUrl.js. - Developed tests for clinic API service functions in services/clinicApi.js. - Added request wrapper tests in services/response.js to ensure correct API interaction. - Set up Vitest configuration in vitest.config.mjs for JSX support and alias resolution. - Created setup and utility files for testing environment in test/setup.js and test/utils.jsx.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { hostRef } = vi.hoisted(() => ({ hostRef: { value: '' } }));
|
||||
|
||||
vi.mock('next/headers', () => ({
|
||||
headers: async () => ({ get: (k) => (k === 'host' ? hostRef.value : null) }),
|
||||
}));
|
||||
|
||||
import { getStateInfo } from '@/lib/getStateInfo';
|
||||
|
||||
beforeEach(() => {
|
||||
hostRef.value = '';
|
||||
});
|
||||
|
||||
describe('getStateInfo (server)', () => {
|
||||
it('subdomain شناختهشده → شهر و استان درست', async () => {
|
||||
hostRef.value = 'arak-nobat.ir';
|
||||
const { matchedCity, matchedState } = await getStateInfo();
|
||||
expect(matchedCity?.name).toBe('اراک');
|
||||
expect(matchedState?.name).toBe('مرکزی');
|
||||
});
|
||||
|
||||
it('host ناشناخته → بدون تطبیق', async () => {
|
||||
hostRef.value = 'unknown-city.ir';
|
||||
const { matchedCity, matchedState } = await getStateInfo();
|
||||
expect(matchedCity).toBeUndefined();
|
||||
expect(matchedState).toBeFalsy();
|
||||
});
|
||||
|
||||
it('host خالی → بدون تطبیق', async () => {
|
||||
hostRef.value = '';
|
||||
const { matchedCity } = await getStateInfo();
|
||||
expect(matchedCity).toBeUndefined();
|
||||
});
|
||||
});
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { adaptSlots, hasAvailable } from '@/lib/appointmentSlots';
|
||||
import { getAccessToken, setAccessToken, clearAccessToken } from '@/lib/tokenStore';
|
||||
import { sanitizeHtml, safeJsonParse } from '@/lib/sanitize';
|
||||
import { defineAbilitiesFor } from '@/lib/ability';
|
||||
import { setRefreshCookie, clearRefreshCookie, COOKIE_NAME } from '@/lib/refreshCookie';
|
||||
import { buildPatientUser } from '@/lib/representationAdapters';
|
||||
|
||||
describe('appointmentSlots', () => {
|
||||
const resp = {
|
||||
sessions: [
|
||||
{ slots: [
|
||||
{ start_time: '09:00', is_available: true },
|
||||
{ start_time: '11:30', is_available: false },
|
||||
{ start_time: '14:00', is_available: true },
|
||||
] },
|
||||
],
|
||||
};
|
||||
it('adaptSlots اسلاتها را بر اساس 12:00 به صبح/عصر تقسیم میکند', () => {
|
||||
const { morning, evening } = adaptSlots(resp);
|
||||
expect(morning).toHaveLength(2);
|
||||
expect(evening).toHaveLength(1);
|
||||
expect(evening[0].start_time).toBe('14:00');
|
||||
});
|
||||
it('ساختار data.sessions را هم میپذیرد', () => {
|
||||
const { morning } = adaptSlots({ data: resp });
|
||||
expect(morning).toHaveLength(2);
|
||||
});
|
||||
it('ورودی خالی → آرایههای خالی', () => {
|
||||
expect(adaptSlots(null)).toEqual({ morning: [], evening: [] });
|
||||
});
|
||||
it('hasAvailable', () => {
|
||||
expect(hasAvailable([{ is_available: false }, { is_available: true }])).toBe(true);
|
||||
expect(hasAvailable([{ is_available: false }])).toBe(false);
|
||||
expect(hasAvailable([])).toBe(false);
|
||||
expect(hasAvailable(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tokenStore', () => {
|
||||
beforeEach(() => clearAccessToken());
|
||||
it('set/get/clear', () => {
|
||||
expect(getAccessToken()).toBeNull();
|
||||
setAccessToken('tok');
|
||||
expect(getAccessToken()).toBe('tok');
|
||||
clearAccessToken();
|
||||
expect(getAccessToken()).toBeNull();
|
||||
});
|
||||
it('set با مقدار falsy → null', () => {
|
||||
setAccessToken('');
|
||||
expect(getAccessToken()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitize', () => {
|
||||
it('غیررشته → رشته خالی', () => {
|
||||
expect(sanitizeHtml(null)).toBe('');
|
||||
expect(sanitizeHtml(123)).toBe('');
|
||||
});
|
||||
it('تگ مجاز حفظ، اسکریپت حذف', () => {
|
||||
const out = sanitizeHtml('<p>سلام</p><script>alert(1)</script>');
|
||||
expect(out).toContain('<p>سلام</p>');
|
||||
expect(out).not.toContain('<script>');
|
||||
});
|
||||
it('safeJsonParse معتبر → object', () => {
|
||||
expect(safeJsonParse('{"a":1}')).toEqual({ a: 1 });
|
||||
});
|
||||
it('safeJsonParse نامعتبر → fallback', () => {
|
||||
expect(safeJsonParse('{bad', { x: 1 })).toEqual({ x: 1 });
|
||||
expect(safeJsonParse('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ability (CASL)', () => {
|
||||
it('کاربر لاگین → دسترسی Dashboard، نه Login', () => {
|
||||
const a = defineAbilitiesFor({ id: 1 });
|
||||
expect(a.can('access', 'Dashboard')).toBe(true);
|
||||
expect(a.can('access', 'Login')).toBe(false);
|
||||
});
|
||||
it('بدون کاربر → دسترسی Login، نه Dashboard', () => {
|
||||
const a = defineAbilitiesFor(null);
|
||||
expect(a.can('access', 'Login')).toBe(true);
|
||||
expect(a.can('access', 'Dashboard')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshCookie (cookieDomain از طریق set)', () => {
|
||||
let response;
|
||||
beforeEach(() => {
|
||||
response = { cookies: { set: vi.fn() } };
|
||||
});
|
||||
it('دامنهی واقعی → "." + دو لیبل آخر', () => {
|
||||
setRefreshCookie(response, 'rt', 'arak-nobat.ir');
|
||||
const opts = response.cookies.set.mock.calls[0][2];
|
||||
expect(response.cookies.set.mock.calls[0][0]).toBe(COOKIE_NAME);
|
||||
expect(opts.domain).toBe('.arak-nobat.ir');
|
||||
expect(opts.httpOnly).toBe(true);
|
||||
});
|
||||
it('host سهلیبلی → فقط دو لیبل آخر', () => {
|
||||
setRefreshCookie(response, 'rt', 'www.arak-nobat.ir');
|
||||
expect(response.cookies.set.mock.calls[0][2].domain).toBe('.arak-nobat.ir');
|
||||
});
|
||||
it('localhost و IP → domain undefined', () => {
|
||||
setRefreshCookie(response, 'rt', 'localhost:3000');
|
||||
expect(response.cookies.set.mock.calls[0][2].domain).toBeUndefined();
|
||||
response.cookies.set.mockClear();
|
||||
setRefreshCookie(response, 'rt', '127.0.0.1');
|
||||
expect(response.cookies.set.mock.calls[0][2].domain).toBeUndefined();
|
||||
});
|
||||
it('clearRefreshCookie با maxAge=0', () => {
|
||||
clearRefreshCookie(response, 'arak-nobat.ir');
|
||||
expect(response.cookies.set.mock.calls[0][2].maxAge).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('representationAdapters.buildPatientUser', () => {
|
||||
it('ساختار double-nested را flatten میکند', () => {
|
||||
const out = buildPatientUser({
|
||||
data: { data: { label: 'علی', family: 'رضایی', phone: '09120000000', national_code: '0013542419' } },
|
||||
});
|
||||
expect(out.name).toBe('علی رضایی');
|
||||
expect(out.phone).toBe('09120000000');
|
||||
expect(out.national_code).toBe('0013542419');
|
||||
});
|
||||
it('ورودی خالی → مقادیر پیشفرض', () => {
|
||||
const out = buildPatientUser(null);
|
||||
expect(out.name).toBe('');
|
||||
expect(out.profile).toBe('/assets/images/profile-user.png');
|
||||
expect(out.turns).toEqual({ current: [], done: [] });
|
||||
});
|
||||
it('extras بهعنوان fallback استفاده میشود', () => {
|
||||
const out = buildPatientUser({}, { realName: 'مهمان', mobile: '0935' });
|
||||
expect(out.name).toBe('مهمان');
|
||||
expect(out.phone).toBe('0935');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { getStateInfoClient } from '@/lib/getStateInfoClient';
|
||||
import { getCanonicalUrlClient } from '@/lib/getCanonicalUrl';
|
||||
|
||||
function setLocation(loc) {
|
||||
Object.defineProperty(window, 'location', { value: loc, writable: true, configurable: true });
|
||||
}
|
||||
|
||||
describe('getStateInfoClient (window)', () => {
|
||||
it('subdomain شناختهشده → شهر و استان', () => {
|
||||
setLocation({ host: 'arak-nobat.ir', href: 'https://arak-nobat.ir/' });
|
||||
const { matchedCity, matchedState, host } = getStateInfoClient();
|
||||
expect(matchedCity?.name).toBe('اراک');
|
||||
expect(matchedState?.name).toBe('مرکزی');
|
||||
expect(host).toBe('arak-nobat.ir');
|
||||
});
|
||||
|
||||
it('host ناشناخته → بدون تطبیق', () => {
|
||||
setLocation({ host: 'zzz.ir', href: 'https://zzz.ir/' });
|
||||
expect(getStateInfoClient().matchedCity).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCanonicalUrlClient (window)', () => {
|
||||
beforeEach(() => {
|
||||
setLocation({ host: 'arak-nobat.ir', pathname: '/doctors' });
|
||||
});
|
||||
|
||||
it('subdomain → canonical به دامنهی اصلی', () => {
|
||||
expect(getCanonicalUrlClient()).toBe('https://nobat724.com/doctors');
|
||||
});
|
||||
|
||||
it('دامنهی اصلی → null', () => {
|
||||
setLocation({ host: 'nobat724.com', pathname: '/doctors' });
|
||||
expect(getCanonicalUrlClient()).toBeNull();
|
||||
});
|
||||
|
||||
it('www و حروف بزرگ نرمال میشوند (دامنهی اصلی) → null', () => {
|
||||
setLocation({ host: 'WWW.Nobat724.com', pathname: '/x' });
|
||||
expect(getCanonicalUrlClient()).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user