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:
parent
70532dda3a
commit
0db2be28d8
@ -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('@umijs/max');
|
||||||
vi.mock('@/services/api');
|
vi.mock('@/services/api');
|
||||||
|
|
||||||
import { smokeTest } from '../../../../test/smoke';
|
import { smokeTest } from '../../../../test/smoke';
|
||||||
import Page from './index';
|
import Page from './index';
|
||||||
|
import { createProduct, getProductSummary } from '@/services/api';
|
||||||
|
|
||||||
describe('Inventory / Products page', () => {
|
describe('Inventory / Products page', () => {
|
||||||
smokeTest(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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user