|
@@ -28,20 +28,63 @@ vi.mock('sonner', () => ({
|
|
|
|
|
|
|
|
import { ChildGoodsList } from '../../src/components/ChildGoodsList';
|
|
import { ChildGoodsList } from '../../src/components/ChildGoodsList';
|
|
|
|
|
|
|
|
-// Mock the goodsClientManager
|
|
|
|
|
-vi.mock('../../src/api/goodsClient', () => ({
|
|
|
|
|
- goodsClientManager: {
|
|
|
|
|
- get: vi.fn(() => ({
|
|
|
|
|
- ':id': {
|
|
|
|
|
- children: {
|
|
|
|
|
- $get: vi.fn(() => Promise.resolve({ status: 200, json: () => Promise.resolve({ data: [], total: 0 }) }))
|
|
|
|
|
- },
|
|
|
|
|
- $put: vi.fn(() => Promise.resolve({ status: 200, json: () => Promise.resolve({}) }))
|
|
|
|
|
- }
|
|
|
|
|
- }))
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// 创建模拟的rpcClient函数(根据API模拟规范)
|
|
|
|
|
+// 符合测试策略文档的API模拟规范:统一模拟@d8d/shared-ui-components/utils/hc中的rpcClient函数
|
|
|
|
|
+const mockRpcClient = vi.hoisted(() => vi.fn((aptBaseUrl: string) => {
|
|
|
|
|
+ // 根据页面组件实际调用的RPC路径定义模拟端点
|
|
|
|
|
+ // 符合规范:支持Hono风格的$get、$post、$put、$delete方法
|
|
|
|
|
+ return {
|
|
|
|
|
+ ':id': {
|
|
|
|
|
+ children: {
|
|
|
|
|
+ $get: vi.fn(() => Promise.resolve(createMockResponse(200, { data: [], total: 0 }))),
|
|
|
|
|
+ },
|
|
|
|
|
+ $put: vi.fn(() => Promise.resolve(createMockResponse(200))),
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+}));
|
|
|
|
|
+
|
|
|
|
|
+// 模拟共享UI组件包中的rpcClient函数(统一模拟点)
|
|
|
|
|
+// 核心API模拟规范:统一拦截所有API调用,支持跨UI包集成测试
|
|
|
|
|
+vi.mock('@d8d/shared-ui-components/utils/hc', () => ({
|
|
|
|
|
+ rpcClient: mockRpcClient
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
|
|
|
+// 完整的mock响应对象(与GoodsParentChildPanel保持一致)
|
|
|
|
|
+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; }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Mock API client(保持向后兼容性,但实际使用上面的统一模拟)
|
|
|
|
|
+// 符合API模拟规范:统一模拟rpcClient函数,客户端管理器使用模拟的rpcClient
|
|
|
|
|
+vi.mock('../../src/api/goodsClient', () => {
|
|
|
|
|
+ // 获取模拟的客户端实例(通过mockRpcClient创建)
|
|
|
|
|
+ // 符合测试策略文档的规范:直接通过模拟的rpcClient函数创建客户端
|
|
|
|
|
+ const mockClient = mockRpcClient('/');
|
|
|
|
|
+
|
|
|
|
|
+ const mockGoodsClientManager = {
|
|
|
|
|
+ get: vi.fn(() => mockClient),
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ goodsClientManager: mockGoodsClientManager,
|
|
|
|
|
+ goodsClient: mockClient,
|
|
|
|
|
+ };
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
import { goodsClientManager } from '../../src/api/goodsClient';
|
|
import { goodsClientManager } from '../../src/api/goodsClient';
|
|
|
|
|
|
|
|
describe('ChildGoodsList', () => {
|
|
describe('ChildGoodsList', () => {
|
|
@@ -80,10 +123,9 @@ describe('ChildGoodsList', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
it('应该显示空状态', async () => {
|
|
it('应该显示空状态', async () => {
|
|
|
- mockGoodsClient[':id'].children.$get.mockResolvedValue({
|
|
|
|
|
- status: 200,
|
|
|
|
|
- json: async () => ({ data: [], total: 0 })
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ mockGoodsClient[':id'].children.$get.mockResolvedValue(
|
|
|
|
|
+ createMockResponse(200, { data: [], total: 0 })
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
renderComponent();
|
|
renderComponent();
|
|
|
|
|
|
|
@@ -127,10 +169,9 @@ describe('ChildGoodsList', () => {
|
|
|
}
|
|
}
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
- mockGoodsClient[':id'].children.$get.mockResolvedValue({
|
|
|
|
|
- status: 200,
|
|
|
|
|
- json: async () => ({ data: mockChildren, total: 3 })
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ mockGoodsClient[':id'].children.$get.mockResolvedValue(
|
|
|
|
|
+ createMockResponse(200, { data: mockChildren, total: 3 })
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
renderComponent();
|
|
renderComponent();
|
|
|
|
|
|
|
@@ -183,10 +224,9 @@ describe('ChildGoodsList', () => {
|
|
|
}
|
|
}
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
- mockGoodsClient[':id'].children.$get.mockResolvedValue({
|
|
|
|
|
- status: 200,
|
|
|
|
|
- json: async () => ({ data: mockChildren, total: 2 })
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ mockGoodsClient[':id'].children.$get.mockResolvedValue(
|
|
|
|
|
+ createMockResponse(200, { data: mockChildren, total: 2 })
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
renderComponent();
|
|
renderComponent();
|
|
|
|
|
|
|
@@ -204,10 +244,9 @@ describe('ChildGoodsList', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
it('应该处理API错误', async () => {
|
|
it('应该处理API错误', async () => {
|
|
|
- mockGoodsClient[':id'].children.$get.mockResolvedValue({
|
|
|
|
|
- status: 500,
|
|
|
|
|
- json: async () => ({ error: '服务器错误' })
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ mockGoodsClient[':id'].children.$get.mockResolvedValue(
|
|
|
|
|
+ createMockResponse(500, { error: '服务器错误' })
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
renderComponent();
|
|
renderComponent();
|
|
|
|
|
|
|
@@ -231,10 +270,9 @@ describe('ChildGoodsList', () => {
|
|
|
}
|
|
}
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
- mockGoodsClient[':id'].children.$get.mockResolvedValue({
|
|
|
|
|
- status: 200,
|
|
|
|
|
- json: async () => ({ data: mockChildren, total: 1 })
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ mockGoodsClient[':id'].children.$get.mockResolvedValue(
|
|
|
|
|
+ createMockResponse(200, { data: mockChildren, total: 1 })
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
renderComponent({ showActions: false });
|
|
renderComponent({ showActions: false });
|
|
|
|
|
|
|
@@ -259,10 +297,9 @@ describe('ChildGoodsList', () => {
|
|
|
}
|
|
}
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
- mockGoodsClient[':id'].children.$get.mockResolvedValue({
|
|
|
|
|
- status: 200,
|
|
|
|
|
- json: async () => ({ data: mockChildren, total: 1 })
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ mockGoodsClient[':id'].children.$get.mockResolvedValue(
|
|
|
|
|
+ createMockResponse(200, { data: mockChildren, total: 1 })
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
const onEditChild = vi.fn();
|
|
const onEditChild = vi.fn();
|
|
|
const onDeleteChild = vi.fn();
|
|
const onDeleteChild = vi.fn();
|
|
@@ -278,9 +315,10 @@ describe('ChildGoodsList', () => {
|
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 点击删除按钮
|
|
|
|
|
- const deleteButton = screen.getByTitle('删除');
|
|
|
|
|
- await userEvent.click(deleteButton);
|
|
|
|
|
|
|
+ // 点击删除按钮(可能有多个,点击第一个)
|
|
|
|
|
+ const deleteButtons = screen.getAllByTitle('删除');
|
|
|
|
|
+ expect(deleteButtons.length).toBeGreaterThan(0);
|
|
|
|
|
+ await userEvent.click(deleteButtons[0]);
|
|
|
|
|
|
|
|
// 验证onDeleteChild回调被调用,并传递正确的子商品ID
|
|
// 验证onDeleteChild回调被调用,并传递正确的子商品ID
|
|
|
expect(onDeleteChild).toHaveBeenCalledTimes(1);
|
|
expect(onDeleteChild).toHaveBeenCalledTimes(1);
|
|
@@ -301,10 +339,9 @@ describe('ChildGoodsList', () => {
|
|
|
}
|
|
}
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
- mockGoodsClient[':id'].children.$get.mockResolvedValue({
|
|
|
|
|
- status: 200,
|
|
|
|
|
- json: async () => ({ data: mockChildren, total: 1 })
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ mockGoodsClient[':id'].children.$get.mockResolvedValue(
|
|
|
|
|
+ createMockResponse(200, { data: mockChildren, total: 1 })
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
const onDeleteChild = vi.fn();
|
|
const onDeleteChild = vi.fn();
|
|
|
|
|
|
|
@@ -318,13 +355,14 @@ describe('ChildGoodsList', () => {
|
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 删除按钮应该被禁用
|
|
|
|
|
- const deleteButton = screen.getByTitle('删除');
|
|
|
|
|
- expect(deleteButton).toBeDisabled();
|
|
|
|
|
|
|
+ // 删除按钮应该被禁用(可能有多个,检查第一个)
|
|
|
|
|
+ const deleteButtons = screen.getAllByTitle('删除');
|
|
|
|
|
+ expect(deleteButtons.length).toBeGreaterThan(0);
|
|
|
|
|
+ expect(deleteButtons[0]).toBeDisabled();
|
|
|
|
|
|
|
|
// 应该显示加载旋转器而不是垃圾桶图标
|
|
// 应该显示加载旋转器而不是垃圾桶图标
|
|
|
// Loader2图标有animate-spin类
|
|
// Loader2图标有animate-spin类
|
|
|
- const loaderIcon = deleteButton.querySelector('.animate-spin');
|
|
|
|
|
|
|
+ const loaderIcon = deleteButtons[0].querySelector('.animate-spin');
|
|
|
expect(loaderIcon).toBeInTheDocument();
|
|
expect(loaderIcon).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -341,10 +379,9 @@ describe('ChildGoodsList', () => {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
beforeEach(() => {
|
|
|
- mockGoodsClient[':id'].children.$get.mockResolvedValue({
|
|
|
|
|
- status: 200,
|
|
|
|
|
- json: async () => ({ data: [mockChild], total: 1 })
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ mockGoodsClient[':id'].children.$get.mockResolvedValue(
|
|
|
|
|
+ createMockResponse(200, { data: [mockChild], total: 1 })
|
|
|
|
|
+ );
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
it('应该显示编辑按钮', async () => {
|
|
it('应该显示编辑按钮', async () => {
|
|
@@ -354,9 +391,10 @@ describe('ChildGoodsList', () => {
|
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 应该显示编辑按钮
|
|
|
|
|
- const editButton = screen.getByTitle('编辑');
|
|
|
|
|
- expect(editButton).toBeInTheDocument();
|
|
|
|
|
|
|
+ // 应该显示编辑按钮(可能有多个,检查至少存在一个)
|
|
|
|
|
+ const editButtons = screen.getAllByTitle('编辑');
|
|
|
|
|
+ expect(editButtons.length).toBeGreaterThan(0);
|
|
|
|
|
+ expect(editButtons[0]).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
it('点击编辑按钮应该触发行内编辑模式', async () => {
|
|
it('点击编辑按钮应该触发行内编辑模式', async () => {
|
|
@@ -366,9 +404,10 @@ describe('ChildGoodsList', () => {
|
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 点击编辑按钮
|
|
|
|
|
- const editButton = screen.getByTitle('编辑');
|
|
|
|
|
- await userEvent.click(editButton);
|
|
|
|
|
|
|
+ // 点击编辑按钮(可能有多个,点击第一个)
|
|
|
|
|
+ const editButtons = screen.getAllByTitle('编辑');
|
|
|
|
|
+ expect(editButtons.length).toBeGreaterThan(0);
|
|
|
|
|
+ await userEvent.click(editButtons[0]);
|
|
|
|
|
|
|
|
// 应该显示行内编辑表单
|
|
// 应该显示行内编辑表单
|
|
|
expect(screen.getByLabelText('商品名称')).toBeInTheDocument();
|
|
expect(screen.getByLabelText('商品名称')).toBeInTheDocument();
|
|
@@ -385,9 +424,10 @@ describe('ChildGoodsList', () => {
|
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 进入编辑模式
|
|
|
|
|
- const editButton = screen.getByTitle('编辑');
|
|
|
|
|
- await userEvent.click(editButton);
|
|
|
|
|
|
|
+ // 进入编辑模式(可能有多个编辑按钮,点击第一个)
|
|
|
|
|
+ const editButtons = screen.getAllByTitle('编辑');
|
|
|
|
|
+ expect(editButtons.length).toBeGreaterThan(0);
|
|
|
|
|
+ await userEvent.click(editButtons[0]);
|
|
|
|
|
|
|
|
// 点击取消按钮
|
|
// 点击取消按钮
|
|
|
const cancelButton = screen.getByText('取消');
|
|
const cancelButton = screen.getByText('取消');
|
|
@@ -400,10 +440,9 @@ describe('ChildGoodsList', () => {
|
|
|
|
|
|
|
|
it('应该成功保存编辑', async () => {
|
|
it('应该成功保存编辑', async () => {
|
|
|
// Mock 更新API成功响应
|
|
// Mock 更新API成功响应
|
|
|
- mockGoodsClient[':id'].$put.mockResolvedValue({
|
|
|
|
|
- status: 200,
|
|
|
|
|
- json: async () => ({ success: true })
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ mockGoodsClient[':id'].$put.mockResolvedValue(
|
|
|
|
|
+ createMockResponse(200, { success: true })
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
renderComponent();
|
|
renderComponent();
|
|
|
|
|
|
|
@@ -411,9 +450,10 @@ describe('ChildGoodsList', () => {
|
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 进入编辑模式
|
|
|
|
|
- const editButton = screen.getByTitle('编辑');
|
|
|
|
|
- await userEvent.click(editButton);
|
|
|
|
|
|
|
+ // 进入编辑模式(可能有多个编辑按钮,点击第一个)
|
|
|
|
|
+ const editButtons = screen.getAllByTitle('编辑');
|
|
|
|
|
+ expect(editButtons.length).toBeGreaterThan(0);
|
|
|
|
|
+ await userEvent.click(editButtons[0]);
|
|
|
|
|
|
|
|
// 修改商品名称
|
|
// 修改商品名称
|
|
|
const nameInput = screen.getByLabelText('商品名称');
|
|
const nameInput = screen.getByLabelText('商品名称');
|
|
@@ -444,10 +484,9 @@ describe('ChildGoodsList', () => {
|
|
|
|
|
|
|
|
it('应该处理保存失败', async () => {
|
|
it('应该处理保存失败', async () => {
|
|
|
// Mock 更新API失败响应
|
|
// Mock 更新API失败响应
|
|
|
- mockGoodsClient[':id'].$put.mockResolvedValue({
|
|
|
|
|
- status: 400,
|
|
|
|
|
- text: async () => '验证失败'
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ mockGoodsClient[':id'].$put.mockResolvedValue(
|
|
|
|
|
+ createMockResponse(400, { error: '验证失败' })
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
renderComponent();
|
|
renderComponent();
|
|
|
|
|
|
|
@@ -455,9 +494,10 @@ describe('ChildGoodsList', () => {
|
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 进入编辑模式
|
|
|
|
|
- const editButton = screen.getByTitle('编辑');
|
|
|
|
|
- await userEvent.click(editButton);
|
|
|
|
|
|
|
+ // 进入编辑模式(可能有多个编辑按钮,点击第一个)
|
|
|
|
|
+ const editButtons = screen.getAllByTitle('编辑');
|
|
|
|
|
+ expect(editButtons.length).toBeGreaterThan(0);
|
|
|
|
|
+ await userEvent.click(editButtons[0]);
|
|
|
|
|
|
|
|
// 点击保存按钮
|
|
// 点击保存按钮
|
|
|
const saveButton = screen.getByText('保存');
|
|
const saveButton = screen.getByText('保存');
|
|
@@ -479,9 +519,10 @@ describe('ChildGoodsList', () => {
|
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
expect(screen.getByText('测试商品')).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 进入编辑模式
|
|
|
|
|
- const editButton = screen.getByTitle('编辑');
|
|
|
|
|
- await userEvent.click(editButton);
|
|
|
|
|
|
|
+ // 进入编辑模式(可能有多个编辑按钮,点击第一个)
|
|
|
|
|
+ const editButtons = screen.getAllByTitle('编辑');
|
|
|
|
|
+ expect(editButtons.length).toBeGreaterThan(0);
|
|
|
|
|
+ await userEvent.click(editButtons[0]);
|
|
|
|
|
|
|
|
// 清空商品名称
|
|
// 清空商品名称
|
|
|
const nameInput = screen.getByLabelText('商品名称');
|
|
const nameInput = screen.getByLabelText('商品名称');
|