import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, fireEvent, waitFor } from '@testing-library/react'; import { renderWithProviders } from '../test/utils'; vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); vi.mock('../lib/api', () => ({ api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() }, ApiError: class extends Error {}, })); import { api } from '../lib/api'; import InventoryPage from './InventoryPage'; const get = api.get as ReturnType; const ITEM = { uuid: 'i-1', name: 'دستکش جراحی', consumable: 'جراحی', category: 'لوازم مصرفی و تزریقات', unit: 'عدد', price: 250000, stock: 150, alertThreshold: 20, status: 'in_stock', }; const META = { units: ['عدد', 'بسته', 'ویال'], categories: ['دارو', 'لوازم مصرفی و تزریقات', 'سایر'] }; const STATS = { total: 1, low: 0, inStock: 1, outOfStock: 0 }; const PKG = { uuid: 'p-1', title: 'پکیج شماره یک', total: 2400000, available: true, items: [{ itemUuid: 'i-1', name: 'ژل', unit: 'سی‌سی', price: 1200000, amount: 2 }], }; function mockApi(opts: { items?: any[]; stats?: any; packages?: any[]; categories?: string[]; meta?: any } = {}) { get.mockImplementation((url: string = '') => { if (url.includes('/inventory-packages')) return Promise.resolve({ success: true, data: opts.packages ?? [] }); if (url.includes('/inventory-meta')) return Promise.resolve({ success: true, data: opts.meta ?? META }); if (url.includes('/inventory-categories')) return Promise.resolve({ success: true, data: opts.categories ?? [] }); if (url.includes('/inventory-items')) return Promise.resolve({ success: true, data: { items: opts.items ?? [], stats: opts.stats ?? { total: 0, low: 0, inStock: 0, outOfStock: 0 } } }); return Promise.resolve({ success: true, data: { items: [], stats: { total: 0, low: 0, inStock: 0, outOfStock: 0 } } }); }); } beforeEach(() => get.mockReset()); describe('InventoryPage', () => { it('renders the header, tabs and item row with real data', async () => { mockApi({ items: [ITEM], stats: STATS, categories: ['جراحی'] }); renderWithProviders(, { route: '/admin/inventory' }); expect(screen.getByText('انبارداری')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'کالاهای مصرفی' })).toBeInTheDocument(); // desktop table + mobile card both render in jsdom (no CSS media) → allow both expect((await screen.findAllByText('دستکش جراحی')).length).toBeGreaterThan(0); // stat card counter + status badge expect(screen.getByText('کالاهای موجود')).toBeInTheDocument(); expect(screen.getAllByText('موجود').length).toBeGreaterThan(0); }); it('shows the empty state when there are no items', async () => { mockApi({ items: [], stats: { total: 0, low: 0, inStock: 0, outOfStock: 0 } }); renderWithProviders(, { route: '/admin/inventory' }); expect(await screen.findByText('هنوز کالایی ثبت نشده است.')).toBeInTheDocument(); }); it('opens the add-item modal from the header button', async () => { mockApi({ items: [], stats: { total: 0, low: 0, inStock: 0, outOfStock: 0 } }); renderWithProviders(, { route: '/admin/inventory' }); fireEvent.click(await screen.findByRole('button', { name: /افزودن کالا/ })); expect(await screen.findByText('افزودن کالای جدید')).toBeInTheDocument(); // the source's distinctive "هشدار اتمام" field is present expect(screen.getByText('هشدار اتمام')).toBeInTheDocument(); // price input groups thousands as the user types const price = screen.getByPlaceholderText('قیمت') as HTMLInputElement; fireEvent.change(price, { target: { value: '1200000' } }); expect(price.value).toBe('1,200,000'); }); it('blocks submit when category is missing and unit is a picker, not free text', async () => { mockApi({ items: [], stats: { total: 0, low: 0, inStock: 0, outOfStock: 0 } }); renderWithProviders(, { route: '/admin/inventory' }); fireEvent.click(await screen.findByRole('button', { name: /افزودن کالا/ })); await screen.findByText('افزودن کالای جدید'); // unit is now a select — no free-text input with the old placeholder expect(screen.queryByPlaceholderText('عدد')).not.toBeInTheDocument(); fireEvent.change(screen.getByPlaceholderText('نام کالا'), { target: { value: 'ماسک' } }); fireEvent.click(screen.getByRole('button', { name: 'اضافه کردن کالا' })); expect(await screen.findByText('دسته‌بندی کالا الزامی است')).toBeInTheDocument(); }); it('switches to the packages tab and lists a package with its price', async () => { mockApi({ packages: [PKG] }); renderWithProviders(, { route: '/admin/inventory' }); fireEvent.click(screen.getByRole('button', { name: 'پکیج' })); expect(await screen.findByText('پکیج شماره یک')).toBeInTheDocument(); expect(screen.getByText('موجودی کافی')).toBeInTheDocument(); // 2,400,000 Rial → 240,000 Toman await waitFor(() => expect(screen.getByText(/۲۴۰٬۰۰۰ تومان/)).toBeInTheDocument()); }); });