Files
clinicpro/assets/admin/pages/InventoryPage.test.tsx
T
hamedandClaude Opus 4.8 519bc8d7f6 feat: port inventory (انبارداری) page from tauri to admin dashboard
Add a per-tenant (doctor/clinic) Inventory domain and admin page, ported
from clinic-pro-tauri /inventory (which was static/mock) into a real feature.

Backend (src/Inventory/):
- Entities InventoryItem, InventoryPackage, InventoryPackageItem, scoped via
  entity_type/entity_id like TenantTag. Item status is derived, package total
  and availability derived at read time.
- InventoryService (stats, package assembly, availability), thin
  InventoryController with CRUD for items and packages + categories endpoint.
- Migration + docs/api/inventory.md + functional tests (10 tests, 42 assertions).

Frontend (assets/admin/):
- InventoryPage with two tabs (کالاهای مصرفی / پکیج), stat cards, items table
  (desktop + mobile cards), packages accordion, add/edit item and package
  modals, search + category filter — pixel-matched to the tauri source.
- useInventory hook (TanStack Query), route + sidebar link for doctor/clinic.
- Vitest coverage (real data, empty state, modal, packages tab).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:48:29 +03:30

78 lines
3.9 KiB
TypeScript

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<typeof vi.fn>;
const ITEM = {
uuid: 'i-1', name: 'دستکش جراحی', consumable: 'جراحی', unit: 'عدد',
price: 250000, stock: 150, alertThreshold: 20, status: 'in_stock',
};
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[] } = {}) {
get.mockImplementation((url: string = '') => {
if (url.includes('/inventory-packages')) return Promise.resolve({ success: true, data: opts.packages ?? [] });
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(<InventoryPage />, { 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(<InventoryPage />, { 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(<InventoryPage />, { 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();
});
it('switches to the packages tab and lists a package with its price', async () => {
mockApi({ packages: [PKG] });
renderWithProviders(<InventoryPage />, { 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());
});
});