diff --git a/src/pages/Inventory/Products/index.test.tsx b/src/pages/Inventory/Products/index.test.tsx index 39b6dda..7faf4c3 100644 --- a/src/pages/Inventory/Products/index.test.tsx +++ b/src/pages/Inventory/Products/index.test.tsx @@ -1,11 +1,151 @@ -import { describe, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor, act, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; vi.mock('@umijs/max'); vi.mock('@/services/api'); import { smokeTest } from '../../../../test/smoke'; import Page from './index'; +import { createProduct, getProductSummary } from '@/services/api'; describe('Inventory / Products page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + smokeTest(Page); + + it('renders 新增产品 toolbar button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /新增产品/ })).toBeInTheDocument(); + }); + + it('opens create dialog when 新增产品 is clicked', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增产品/ })); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + expect(within(screen.getByRole('dialog')).getByText('新增产品')).toBeInTheDocument(); + }); + + it('calls createProduct after filling form and submitting', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增产品/ })); + await waitFor(() => screen.getByRole('dialog')); + + const dialog = screen.getByRole('dialog'); + const nameInput = within(dialog).getByRole('textbox', { name: /产品名称/ }); + await user.type(nameInput, '测试产品'); + + // Click the submit/OK button – find the non-close button in the dialog footer + const buttons = within(dialog).getAllByRole('button'); + const submitBtn = buttons.find((b) => { + const text = b.textContent?.trim() ?? ''; + return text && !text.includes('×') && !text.includes('✕') && !/取/.test(text) && !/重/.test(text) && !/Cancel/i.test(text); + }); + if (submitBtn) await user.click(submitBtn); + + await waitFor(() => { + expect(vi.mocked(createProduct)).toHaveBeenCalled(); + }); + }); + + it('renders Excel export button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /Excel导出/ })).toBeInTheDocument(); + }); + + // Regression: when a product has no pans the backend returns empty strings for + // colors/locations (via COALESCE). Previously GROUP_CONCAT returned NULL which + // crashed the Go scanner and made the entire summary return an error, leaving the + // table empty. This test ensures the frontend handles the fixed response correctly. + it('renders summary table without crash when product has empty colors and locations', async () => { + vi.mocked(getProductSummary).mockResolvedValueOnce({ + code: 0, + msg: 'ok', + data: { + list: [ + { + productName: '无盘测试布料', + spec: '100cm', + colorCount: 1, + totalPanCount: 0, + totalBoltCount: 0, + totalLengthM: '0.00', + colors: '', + locations: '', + }, + ], + }, + } as any); + + render(); + + // ProTable header text appears in multiple nodes — presence check is enough + await waitFor(() => { + expect(screen.getAllByText('产品总览').length).toBeGreaterThan(0); + }); + // getProductSummary was invoked (not short-circuited by an error) + await waitFor(() => { + expect(vi.mocked(getProductSummary)).toHaveBeenCalled(); + }); + }); + + // Verifies the success path: createProduct returns code 0 → ModalForm closes. + // The dialog closing is the proxy for summaryRef.reload() being reached — + // ModalForm only calls onOpenChange(false) when onFinish returns true, + // which is the same branch that triggers the summary reload. + it('closes dialog after successful product creation (reload branch reached)', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增产品/ })); + await waitFor(() => screen.getByRole('dialog')); + + const dialog = screen.getByRole('dialog'); + const nameInput = within(dialog).getByRole('textbox', { name: /产品名称/ }); + await user.type(nameInput, '新布料'); + + const buttons = within(dialog).getAllByRole('button'); + const submitBtn = buttons.find((b) => { + const text = b.textContent?.trim() ?? ''; + return text && !text.includes('×') && !text.includes('✕') && !/取/.test(text) && !/重/.test(text) && !/Cancel/i.test(text); + }); + if (submitBtn) await user.click(submitBtn); + + await waitFor(() => { + expect(vi.mocked(createProduct)).toHaveBeenCalled(); + }); + // ModalForm closes only when onFinish returns true — the same branch that calls reload() + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); });