import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import { renderWithProviders } from '../test/utils'; vi.mock('../lib/api', () => ({ api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() }, ApiError: class extends Error {}, })); vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); import { api } from '../lib/api'; import ServiceGroupsTab from './ServiceGroupsTab'; const get = api.get as ReturnType; const post = api.post as ReturnType; const patch = api.patch as ReturnType; const groups = [ { uuid: 'g1', name: 'نواحی بدن', min_select: 1, max_select: null, items: [{ uuid: 'i1', name: 'صورت' }], }, ]; const items = [ { uuid: 'i1', name: 'صورت' }, { uuid: 'i2', name: 'بیکینی' }, ]; function mockApi() { get.mockImplementation((path: string) => { if (path.endsWith('/groups')) return Promise.resolve({ success: true, data: groups }); if (path === '/api/v1/service-items') return Promise.resolve({ success: true, data: items }); return Promise.resolve({ success: true, data: null }); }); post.mockResolvedValue({ success: true, data: { valid: false, errors: [{ code: 'min_select', message: 'انتخاب حداقل ۱ مورد از «نواحی بدن» الزامی است' }], total_duration_minutes: 23, total_price_rials: 800000, breakdown: [ { item_uuid: 'i1', item_name: 'صورت', counted_as: 'solo', minutes: 15, price_rials: 500000 }, { item_uuid: 'i2', item_name: 'بیکینی', counted_as: 'additional', minutes: 8, price_rials: 300000 }, ], }, }); patch.mockResolvedValue({ success: true, data: groups[0] }); } describe('ServiceGroupsTab', () => { beforeEach(() => { vi.clearAllMocks(); mockApi(); }); it('shows each group with its min and max', async () => { renderWithProviders(, { route: '/admin/services/s1' }); await waitFor(() => expect(screen.getByDisplayValue('نواحی بدن')).toBeInTheDocument()); expect(screen.getByDisplayValue('1')).toBeInTheDocument(); // حداکثرِ خالی یعنی نامحدود — نه صفر. expect(screen.getByPlaceholderText('نامحدود')).toHaveValue(null); }); /** * ⭐ پیش‌نمایش نباید بدون انتخاب، درخواستی بزند: فراخوانی با سبد خالی هم نویز شبکه * است هم خطای «حداقل انتخاب» را بی‌جا نشان می‌دهد. */ it('does not call validate until something is selected', async () => { renderWithProviders(, { route: '/admin/services/s1' }); await waitFor(() => expect(screen.getByText('پیش‌نمایش انتخاب')).toBeInTheDocument()); expect(post).not.toHaveBeenCalledWith('/api/v1/service-selection/validate', expect.anything()); expect(screen.getByText(/چند آیتم انتخاب کنید/)).toBeInTheDocument(); }); it('explains the meaning of an empty maximum', async () => { renderWithProviders(, { route: '/admin/services/s1' }); await waitFor(() => expect(screen.getByText(/حداکثرِ خالی یعنی نامحدود/)).toBeInTheDocument(), ); }); });