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();
+ });
+ });
});