test(products): add regression tests for GROUP_CONCAT NULL bug and reload path

- 'renders summary table without crash when product has empty colors and locations':
  mocks getProductSummary returning a product with colors/locations='', verifying the
  page does not crash and calls the API (reproduces the previously-broken scenario).

- 'closes dialog after successful product creation (reload branch reached)':
  verifies that a successful form submission calls createProduct and the ModalForm
  closes, confirming onFinish returned true — the same code branch that triggers
  summaryRef.reload().
This commit is contained in:
kae 2026-06-16 03:30:47 +09:00
parent 70532dda3a
commit 0db2be28d8

View File

@ -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(<Page />);
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(<Page />);
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(<Page />);
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(<Page />);
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(<Page />);
// 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(<Page />);
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();
});
});
});