|
|
@@ -5,16 +5,36 @@ import { vi } from 'vitest';
|
|
|
import { toast } from 'sonner';
|
|
|
import { BatchSpecCreator } from '../../src/components/BatchSpecCreator';
|
|
|
|
|
|
-// Mock useQuery to return data immediately
|
|
|
-vi.mock('@tanstack/react-query', async (importOriginal) => {
|
|
|
- const actual = await importOriginal() as any;
|
|
|
- return {
|
|
|
- ...actual,
|
|
|
- useQuery: vi.fn(({ queryKey, queryFn, onSuccess }: any) => {
|
|
|
- // 如果是获取父商品的查询
|
|
|
- if (queryKey[0] === 'parentGoods' && queryKey[1] === 1) {
|
|
|
- // 立即返回数据,模拟成功加载
|
|
|
- const data = {
|
|
|
+// 不模拟@tanstack/react-query,使用真实实现(符合组件模拟策略要求)
|
|
|
+// 通过模拟rpcClient来控制API响应
|
|
|
+
|
|
|
+// 完整的mock响应对象
|
|
|
+const createMockResponse = (status: number, data?: any) => ({
|
|
|
+ status,
|
|
|
+ ok: status >= 200 && status < 300,
|
|
|
+ body: null,
|
|
|
+ bodyUsed: false,
|
|
|
+ statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
|
|
|
+ headers: new Headers(),
|
|
|
+ url: '',
|
|
|
+ redirected: false,
|
|
|
+ type: 'basic' as ResponseType,
|
|
|
+ json: async () => data || {},
|
|
|
+ text: async () => '',
|
|
|
+ blob: async () => new Blob(),
|
|
|
+ arrayBuffer: async () => new ArrayBuffer(0),
|
|
|
+ formData: async () => new FormData(),
|
|
|
+ clone: function() { return this; }
|
|
|
+});
|
|
|
+
|
|
|
+// 统一模拟rpcClient函数(符合API模拟规范)
|
|
|
+const mockRpcClient = vi.hoisted(() => vi.fn(() => {
|
|
|
+ console.debug('mockRpcClient called');
|
|
|
+ const mockClient = {
|
|
|
+ ':id': {
|
|
|
+ $get: vi.fn((options) => {
|
|
|
+ console.debug('mock $get called with:', options);
|
|
|
+ return Promise.resolve(createMockResponse(200, {
|
|
|
id: 1,
|
|
|
name: '测试父商品',
|
|
|
categoryId1: 1,
|
|
|
@@ -26,47 +46,28 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
|
|
|
price: 100,
|
|
|
costPrice: 80,
|
|
|
stock: 100,
|
|
|
- state: 1
|
|
|
- };
|
|
|
-
|
|
|
- // 调用onSuccess回调(如果提供)
|
|
|
- if (onSuccess) {
|
|
|
- setTimeout(() => onSuccess(data), 0);
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- data,
|
|
|
- isLoading: false,
|
|
|
- isError: false,
|
|
|
- error: null,
|
|
|
- refetch: vi.fn()
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // 其他查询使用原始实现
|
|
|
- return actual.useQuery({ queryKey, queryFn });
|
|
|
- })
|
|
|
+ state: 1,
|
|
|
+ tenantId: 1
|
|
|
+ }));
|
|
|
+ })
|
|
|
+ },
|
|
|
+ index: {
|
|
|
+ $post: vi.fn((options) => {
|
|
|
+ console.debug('mock $post called with:', options);
|
|
|
+ return Promise.resolve(createMockResponse(201, { id: 100, name: '父商品 - 规格1' }));
|
|
|
+ })
|
|
|
+ }
|
|
|
};
|
|
|
-});
|
|
|
-
|
|
|
-// Mock the goodsClientManager for mutation tests
|
|
|
-const mockGoodsClient = {
|
|
|
- index: {
|
|
|
- $post: vi.fn(() => {
|
|
|
- return Promise.resolve({
|
|
|
- status: 201,
|
|
|
- json: () => Promise.resolve({ id: 100, name: '父商品 - 规格1' })
|
|
|
- });
|
|
|
- })
|
|
|
- }
|
|
|
-};
|
|
|
+ return mockClient;
|
|
|
+}));
|
|
|
|
|
|
-vi.mock('../../src/api/goodsClient', () => ({
|
|
|
- goodsClientManager: {
|
|
|
- get: vi.fn(() => mockGoodsClient)
|
|
|
- }
|
|
|
+vi.mock('@d8d/shared-ui-components/utils/hc', () => ({
|
|
|
+ rpcClient: mockRpcClient
|
|
|
}));
|
|
|
|
|
|
+// goodsClientManager不再需要模拟,因为它使用我们模拟的rpcClient
|
|
|
+// 保持真实实现,通过rpcClient模拟控制API响应
|
|
|
+
|
|
|
// Mock sonner toast
|
|
|
vi.mock('sonner', () => ({
|
|
|
toast: {
|
|
|
@@ -98,6 +99,19 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
|
</QueryClientProvider>
|
|
|
);
|
|
|
|
|
|
+// Helper function to wait for parent goods data to load
|
|
|
+const waitForParentGoodsLoaded = async () => {
|
|
|
+ // 等待加载提示消失
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(screen.queryByText('正在加载父商品信息...')).not.toBeInTheDocument();
|
|
|
+ }, { timeout: 5000 });
|
|
|
+
|
|
|
+ // 等待父商品ID显示
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(screen.getByDisplayValue('1')).toBeInTheDocument(); // 父商品ID
|
|
|
+ }, { timeout: 5000 });
|
|
|
+};
|
|
|
+
|
|
|
describe('BatchSpecCreator', () => {
|
|
|
const defaultProps = {
|
|
|
parentGoodsId: 1,
|
|
|
@@ -111,13 +125,16 @@ describe('BatchSpecCreator', () => {
|
|
|
vi.clearAllMocks();
|
|
|
});
|
|
|
|
|
|
- it('应该正确渲染组件', () => {
|
|
|
+ it('应该正确渲染组件', async () => {
|
|
|
render(
|
|
|
<Wrapper>
|
|
|
<BatchSpecCreator {...defaultProps} />
|
|
|
</Wrapper>
|
|
|
);
|
|
|
|
|
|
+ // 等待父商品数据加载完成
|
|
|
+ await waitForParentGoodsLoaded();
|
|
|
+
|
|
|
// 检查对话框标题
|
|
|
expect(screen.getByText('批量创建子商品规格')).toBeInTheDocument();
|
|
|
expect(screen.getByText('为父商品 "父商品" 批量创建多个子商品规格')).toBeInTheDocument();
|
|
|
@@ -176,13 +193,15 @@ describe('BatchSpecCreator', () => {
|
|
|
expect(nameInputs).toHaveLength(3);
|
|
|
});
|
|
|
|
|
|
- it('应该删除规格行', () => {
|
|
|
+ it('应该删除规格行', async () => {
|
|
|
render(
|
|
|
<Wrapper>
|
|
|
<BatchSpecCreator {...defaultProps} />
|
|
|
</Wrapper>
|
|
|
);
|
|
|
|
|
|
+ await waitForParentGoodsLoaded();
|
|
|
+
|
|
|
const deleteButtons = screen.getAllByRole('button', { name: '' });
|
|
|
fireEvent.click(deleteButtons[0]); // 删除第一个规格
|
|
|
|
|
|
@@ -190,13 +209,15 @@ describe('BatchSpecCreator', () => {
|
|
|
expect(nameInputs).toHaveLength(1);
|
|
|
});
|
|
|
|
|
|
- it('不能删除最后一个规格行', () => {
|
|
|
+ it('不能删除最后一个规格行', async () => {
|
|
|
render(
|
|
|
<Wrapper>
|
|
|
<BatchSpecCreator {...defaultProps} />
|
|
|
</Wrapper>
|
|
|
);
|
|
|
|
|
|
+ await waitForParentGoodsLoaded();
|
|
|
+
|
|
|
// 先删除一个
|
|
|
const deleteButtons = screen.getAllByRole('button', { name: '' });
|
|
|
fireEvent.click(deleteButtons[0]);
|
|
|
@@ -211,13 +232,15 @@ describe('BatchSpecCreator', () => {
|
|
|
expect(toast.error).toHaveBeenCalledWith('至少需要保留一个规格');
|
|
|
});
|
|
|
|
|
|
- it('应该更新规格字段', () => {
|
|
|
+ it('应该更新规格字段', async () => {
|
|
|
render(
|
|
|
<Wrapper>
|
|
|
<BatchSpecCreator {...defaultProps} />
|
|
|
</Wrapper>
|
|
|
);
|
|
|
|
|
|
+ await waitForParentGoodsLoaded();
|
|
|
+
|
|
|
const nameInput = screen.getAllByPlaceholderText('例如:红色、64GB、大号')[0];
|
|
|
fireEvent.change(nameInput, { target: { value: '红色' } });
|
|
|
expect(nameInput).toHaveValue('红色');
|
|
|
@@ -238,6 +261,8 @@ describe('BatchSpecCreator', () => {
|
|
|
</Wrapper>
|
|
|
);
|
|
|
|
|
|
+ await waitForParentGoodsLoaded();
|
|
|
+
|
|
|
const submitButton = screen.getByText('创建 2 个子商品');
|
|
|
fireEvent.click(submitButton);
|
|
|
|
|
|
@@ -253,6 +278,8 @@ describe('BatchSpecCreator', () => {
|
|
|
</Wrapper>
|
|
|
);
|
|
|
|
|
|
+ await waitForParentGoodsLoaded();
|
|
|
+
|
|
|
// 设置两个规格为相同的名称
|
|
|
const nameInputs = screen.getAllByPlaceholderText('例如:红色、64GB、大号');
|
|
|
fireEvent.change(nameInputs[0], { target: { value: '红色' } });
|
|
|
@@ -282,6 +309,8 @@ describe('BatchSpecCreator', () => {
|
|
|
</Wrapper>
|
|
|
);
|
|
|
|
|
|
+ await waitForParentGoodsLoaded();
|
|
|
+
|
|
|
// 设置规格名称
|
|
|
const nameInputs = screen.getAllByPlaceholderText('例如:红色、64GB、大号');
|
|
|
fireEvent.change(nameInputs[0], { target: { value: '红色' } });
|
|
|
@@ -305,6 +334,8 @@ describe('BatchSpecCreator', () => {
|
|
|
</Wrapper>
|
|
|
);
|
|
|
|
|
|
+ await waitForParentGoodsLoaded();
|
|
|
+
|
|
|
// 设置第一个规格
|
|
|
const nameInputs = screen.getAllByPlaceholderText('例如:红色、64GB、大号');
|
|
|
fireEvent.change(nameInputs[0], { target: { value: '红色' } });
|
|
|
@@ -329,26 +360,30 @@ describe('BatchSpecCreator', () => {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- it('应该处理取消操作', () => {
|
|
|
+ it('应该处理取消操作', async () => {
|
|
|
render(
|
|
|
<Wrapper>
|
|
|
<BatchSpecCreator {...defaultProps} />
|
|
|
</Wrapper>
|
|
|
);
|
|
|
|
|
|
+ await waitForParentGoodsLoaded();
|
|
|
+
|
|
|
const cancelButton = screen.getByText('取消');
|
|
|
fireEvent.click(cancelButton);
|
|
|
|
|
|
expect(defaultProps.onCancel).toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
- it('应该显示租户信息', () => {
|
|
|
+ it('应该显示租户信息', async () => {
|
|
|
render(
|
|
|
<Wrapper>
|
|
|
<BatchSpecCreator {...defaultProps} tenantId={123} />
|
|
|
</Wrapper>
|
|
|
);
|
|
|
|
|
|
+ await waitForParentGoodsLoaded();
|
|
|
+
|
|
|
expect(screen.getByText('• 所有子商品将自动关联到父商品(spuId = 1)')).toBeInTheDocument();
|
|
|
});
|
|
|
});
|