|
@@ -1,6 +1,7 @@
|
|
|
import React from 'react';
|
|
import React from 'react';
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
|
|
|
+import userEvent from '@testing-library/user-event';
|
|
|
import { toast } from 'sonner';
|
|
import { toast } from 'sonner';
|
|
|
|
|
|
|
|
import { BatchSpecCreatorInline } from '../../src/components/BatchSpecCreatorInline';
|
|
import { BatchSpecCreatorInline } from '../../src/components/BatchSpecCreatorInline';
|
|
@@ -55,18 +56,19 @@ describe('BatchSpecCreatorInline', () => {
|
|
|
expect(screen.getByText('2')).toBeInTheDocument(); // 规格数量
|
|
expect(screen.getByText('2')).toBeInTheDocument(); // 规格数量
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('应该添加新规格', () => {
|
|
|
|
|
|
|
+ it('应该添加新规格', async () => {
|
|
|
|
|
+ const user = userEvent.setup();
|
|
|
const onSpecsChange = vi.fn();
|
|
const onSpecsChange = vi.fn();
|
|
|
renderComponent({ onSpecsChange });
|
|
renderComponent({ onSpecsChange });
|
|
|
|
|
|
|
|
- // 填写规格信息
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '测试规格' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('价格'), { target: { value: '150' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('成本价'), { target: { value: '120' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('库存'), { target: { value: '25' } });
|
|
|
|
|
|
|
+ // 填写规格信息 - 使用userEvent更接近真实用户交互
|
|
|
|
|
+ await user.type(screen.getByLabelText('规格名称 *'), '测试规格');
|
|
|
|
|
+ await user.type(screen.getByLabelText('价格'), '150');
|
|
|
|
|
+ await user.type(screen.getByLabelText('成本价'), '120');
|
|
|
|
|
+ await user.type(screen.getByLabelText('库存'), '25');
|
|
|
|
|
|
|
|
// 点击添加按钮
|
|
// 点击添加按钮
|
|
|
- fireEvent.click(screen.getByText('添加'));
|
|
|
|
|
|
|
+ await user.click(screen.getByText('添加'));
|
|
|
|
|
|
|
|
// 验证toast被调用
|
|
// 验证toast被调用
|
|
|
expect(toast.success).toHaveBeenCalledWith('规格已添加');
|
|
expect(toast.success).toHaveBeenCalledWith('规格已添加');
|
|
@@ -102,55 +104,78 @@ describe('BatchSpecCreatorInline', () => {
|
|
|
expect(addButton).toBeDisabled();
|
|
expect(addButton).toBeDisabled();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('应该验证价格不能为负数', () => {
|
|
|
|
|
|
|
+ it('应该验证价格不能为负数', async () => {
|
|
|
|
|
+ const user = userEvent.setup();
|
|
|
renderComponent();
|
|
renderComponent();
|
|
|
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '测试规格' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('价格'), { target: { value: '-10' } });
|
|
|
|
|
|
|
+ await user.type(screen.getByLabelText('规格名称 *'), '测试规格');
|
|
|
|
|
+ await user.type(screen.getByLabelText('价格'), '-10');
|
|
|
|
|
+
|
|
|
|
|
+ // 等待按钮启用
|
|
|
|
|
+ const addButton = screen.getByText('添加');
|
|
|
|
|
+ await waitFor(() => expect(addButton).not.toBeDisabled());
|
|
|
|
|
|
|
|
- fireEvent.click(screen.getByText('添加'));
|
|
|
|
|
|
|
+ await user.click(addButton);
|
|
|
|
|
|
|
|
- expect(toast.error).toHaveBeenCalledWith('价格不能为负数');
|
|
|
|
|
|
|
+ // 等待toast被调用,因为表单验证可能是异步的
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(toast.error).toHaveBeenCalledWith('价格不能为负数');
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('应该验证成本价不能为负数', () => {
|
|
|
|
|
|
|
+ it('应该验证成本价不能为负数', async () => {
|
|
|
|
|
+ const user = userEvent.setup();
|
|
|
renderComponent();
|
|
renderComponent();
|
|
|
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '测试规格' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('成本价'), { target: { value: '-5' } });
|
|
|
|
|
|
|
+ await user.type(screen.getByLabelText('规格名称 *'), '测试规格');
|
|
|
|
|
+ // 清除成本价字段(可能包含默认值0),然后输入负值
|
|
|
|
|
+ await user.clear(screen.getByLabelText('成本价'));
|
|
|
|
|
+ await user.type(screen.getByLabelText('成本价'), '-5');
|
|
|
|
|
|
|
|
- fireEvent.click(screen.getByText('添加'));
|
|
|
|
|
|
|
+ await user.click(screen.getByText('添加'));
|
|
|
|
|
|
|
|
- expect(toast.error).toHaveBeenCalledWith('成本价不能为负数');
|
|
|
|
|
|
|
+ // 等待toast被调用,因为表单验证可能是异步的
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(toast.error).toHaveBeenCalledWith('成本价不能为负数');
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('应该验证库存不能为负数', () => {
|
|
|
|
|
|
|
+ it('应该验证库存不能为负数', async () => {
|
|
|
|
|
+ const user = userEvent.setup();
|
|
|
renderComponent();
|
|
renderComponent();
|
|
|
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '测试规格' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('库存'), { target: { value: '-1' } });
|
|
|
|
|
|
|
+ await user.type(screen.getByLabelText('规格名称 *'), '测试规格');
|
|
|
|
|
+ // 清除库存字段(可能包含默认值0),然后输入负值
|
|
|
|
|
+ await user.clear(screen.getByLabelText('库存'));
|
|
|
|
|
+ await user.type(screen.getByLabelText('库存'), '-1');
|
|
|
|
|
|
|
|
- fireEvent.click(screen.getByText('添加'));
|
|
|
|
|
|
|
+ await user.click(screen.getByText('添加'));
|
|
|
|
|
|
|
|
- expect(toast.error).toHaveBeenCalledWith('库存不能为负数');
|
|
|
|
|
|
|
+ // 等待toast被调用,因为表单验证可能是异步的
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(toast.error).toHaveBeenCalledWith('库存不能为负数');
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('应该验证规格名称不能重复(添加时)', () => {
|
|
|
|
|
|
|
+ it('应该验证规格名称不能重复(添加时)', async () => {
|
|
|
|
|
+ const user = userEvent.setup();
|
|
|
const initialSpecs = [
|
|
const initialSpecs = [
|
|
|
{ name: '红色', price: 100, costPrice: 80, stock: 50, sort: 1 }
|
|
{ name: '红色', price: 100, costPrice: 80, stock: 50, sort: 1 }
|
|
|
];
|
|
];
|
|
|
renderComponent({ initialSpecs });
|
|
renderComponent({ initialSpecs });
|
|
|
|
|
|
|
|
// 尝试添加重复的规格名称(不区分大小写)
|
|
// 尝试添加重复的规格名称(不区分大小写)
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '红色' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('价格'), { target: { value: '120' } });
|
|
|
|
|
- fireEvent.click(screen.getByText('添加'));
|
|
|
|
|
|
|
+ await user.type(screen.getByLabelText('规格名称 *'), '红色');
|
|
|
|
|
+ await user.type(screen.getByLabelText('价格'), '120');
|
|
|
|
|
+ await user.click(screen.getByText('添加'));
|
|
|
|
|
|
|
|
expect(toast.error).toHaveBeenCalledWith('规格名称 "红色" 已存在,请使用不同的名称');
|
|
expect(toast.error).toHaveBeenCalledWith('规格名称 "红色" 已存在,请使用不同的名称');
|
|
|
|
|
|
|
|
// 尝试添加不同大小写的重复名称
|
|
// 尝试添加不同大小写的重复名称
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '红 色' } }); // 有空格
|
|
|
|
|
- fireEvent.click(screen.getByText('添加'));
|
|
|
|
|
|
|
+ // 首先清除输入字段
|
|
|
|
|
+ await user.clear(screen.getByLabelText('规格名称 *'));
|
|
|
|
|
+ await user.type(screen.getByLabelText('规格名称 *'), '红 色'); // 有空格
|
|
|
|
|
+ await user.click(screen.getByText('添加'));
|
|
|
|
|
|
|
|
// 应该通过,因为"红 色"(有空格)与"红色"(无空格)不同
|
|
// 应该通过,因为"红 色"(有空格)与"红色"(无空格)不同
|
|
|
expect(toast.success).toHaveBeenCalledWith('规格已添加');
|
|
expect(toast.success).toHaveBeenCalledWith('规格已添加');
|
|
@@ -189,27 +214,34 @@ describe('BatchSpecCreatorInline', () => {
|
|
|
expect(onSpecsChange).not.toHaveBeenCalled();
|
|
expect(onSpecsChange).not.toHaveBeenCalled();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('应该验证多个错误字段', () => {
|
|
|
|
|
|
|
+ it('应该验证多个错误字段', async () => {
|
|
|
|
|
+ const user = userEvent.setup();
|
|
|
renderComponent();
|
|
renderComponent();
|
|
|
|
|
|
|
|
// 设置所有字段为无效值
|
|
// 设置所有字段为无效值
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('价格'), { target: { value: '-10' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('成本价'), { target: { value: '-5' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('库存'), { target: { value: '-1' } });
|
|
|
|
|
|
|
+ await user.clear(screen.getByLabelText('规格名称 *'));
|
|
|
|
|
+ await user.clear(screen.getByLabelText('价格'));
|
|
|
|
|
+ await user.type(screen.getByLabelText('价格'), '-10');
|
|
|
|
|
+ await user.clear(screen.getByLabelText('成本价'));
|
|
|
|
|
+ await user.type(screen.getByLabelText('成本价'), '-5');
|
|
|
|
|
+ await user.clear(screen.getByLabelText('库存'));
|
|
|
|
|
+ await user.type(screen.getByLabelText('库存'), '-1');
|
|
|
|
|
|
|
|
// 按钮应该被禁用(因为名称为空)
|
|
// 按钮应该被禁用(因为名称为空)
|
|
|
const addButton = screen.getByText('添加');
|
|
const addButton = screen.getByText('添加');
|
|
|
expect(addButton).toBeDisabled();
|
|
expect(addButton).toBeDisabled();
|
|
|
|
|
|
|
|
// 填写名称后,点击按钮应该显示第一个错误
|
|
// 填写名称后,点击按钮应该显示第一个错误
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '测试规格' } });
|
|
|
|
|
|
|
+ await user.type(screen.getByLabelText('规格名称 *'), '测试规格');
|
|
|
expect(addButton).not.toBeDisabled();
|
|
expect(addButton).not.toBeDisabled();
|
|
|
|
|
|
|
|
- fireEvent.click(addButton);
|
|
|
|
|
|
|
+ await user.click(addButton);
|
|
|
|
|
|
|
|
// 应该显示价格不能为负数的错误(第一个验证错误)
|
|
// 应该显示价格不能为负数的错误(第一个验证错误)
|
|
|
- expect(toast.error).toHaveBeenCalledWith('价格不能为负数');
|
|
|
|
|
|
|
+ // 等待toast被调用,因为表单验证可能是异步的
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(toast.error).toHaveBeenCalledWith('价格不能为负数');
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
it('应该更新规格', () => {
|
|
it('应该更新规格', () => {
|
|
@@ -360,18 +392,19 @@ describe('BatchSpecCreatorInline', () => {
|
|
|
expect(screen.getByText('添加规格后,将在创建商品时批量生成子商品')).toBeInTheDocument();
|
|
expect(screen.getByText('添加规格后,将在创建商品时批量生成子商品')).toBeInTheDocument();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('应该测试完整的用户交互流程:添加多个规格并保存模板', () => {
|
|
|
|
|
|
|
+ it('应该测试完整的用户交互流程:添加多个规格并保存模板', async () => {
|
|
|
|
|
+ const user = userEvent.setup();
|
|
|
const onSpecsChange = vi.fn();
|
|
const onSpecsChange = vi.fn();
|
|
|
const onSaveTemplate = vi.fn();
|
|
const onSaveTemplate = vi.fn();
|
|
|
|
|
|
|
|
renderComponent({ onSpecsChange, onSaveTemplate });
|
|
renderComponent({ onSpecsChange, onSaveTemplate });
|
|
|
|
|
|
|
|
// 第一步:添加第一个规格
|
|
// 第一步:添加第一个规格
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '红色' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('价格'), { target: { value: '100' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('成本价'), { target: { value: '80' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('库存'), { target: { value: '50' } });
|
|
|
|
|
- fireEvent.click(screen.getByText('添加'));
|
|
|
|
|
|
|
+ await user.type(screen.getByLabelText('规格名称 *'), '红色');
|
|
|
|
|
+ await user.type(screen.getByLabelText('价格'), '100');
|
|
|
|
|
+ await user.type(screen.getByLabelText('成本价'), '80');
|
|
|
|
|
+ await user.type(screen.getByLabelText('库存'), '50');
|
|
|
|
|
+ await user.click(screen.getByText('添加'));
|
|
|
|
|
|
|
|
expect(toast.success).toHaveBeenCalledWith('规格已添加');
|
|
expect(toast.success).toHaveBeenCalledWith('规格已添加');
|
|
|
expect(onSpecsChange).toHaveBeenCalledWith([
|
|
expect(onSpecsChange).toHaveBeenCalledWith([
|
|
@@ -379,11 +412,13 @@ describe('BatchSpecCreatorInline', () => {
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
// 第二步:添加第二个规格
|
|
// 第二步:添加第二个规格
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '蓝色' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('价格'), { target: { value: '110' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('成本价'), { target: { value: '85' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('库存'), { target: { value: '30' } });
|
|
|
|
|
- fireEvent.click(screen.getByText('添加'));
|
|
|
|
|
|
|
+ // 清除第一个字段(规格名称),其他字段会被重置
|
|
|
|
|
+ await user.clear(screen.getByLabelText('规格名称 *'));
|
|
|
|
|
+ await user.type(screen.getByLabelText('规格名称 *'), '蓝色');
|
|
|
|
|
+ await user.type(screen.getByLabelText('价格'), '110');
|
|
|
|
|
+ await user.type(screen.getByLabelText('成本价'), '85');
|
|
|
|
|
+ await user.type(screen.getByLabelText('库存'), '30');
|
|
|
|
|
+ await user.click(screen.getByText('添加'));
|
|
|
|
|
|
|
|
expect(toast.success).toHaveBeenCalledWith('规格已添加');
|
|
expect(toast.success).toHaveBeenCalledWith('规格已添加');
|
|
|
// 验证回调被调用,但不验证具体的sort值,因为sort逻辑可能复杂
|
|
// 验证回调被调用,但不验证具体的sort值,因为sort逻辑可能复杂
|
|
@@ -400,18 +435,19 @@ describe('BatchSpecCreatorInline', () => {
|
|
|
|
|
|
|
|
// 第三步:更新第一个规格
|
|
// 第三步:更新第一个规格
|
|
|
const nameInputs = screen.getAllByDisplayValue('红色');
|
|
const nameInputs = screen.getAllByDisplayValue('红色');
|
|
|
- fireEvent.change(nameInputs[0], { target: { value: '深红色' } });
|
|
|
|
|
|
|
+ await user.clear(nameInputs[0]);
|
|
|
|
|
+ await user.type(nameInputs[0], '深红色');
|
|
|
|
|
|
|
|
// 更新规格后,回调应该被调用
|
|
// 更新规格后,回调应该被调用
|
|
|
// 我们只验证回调被调用,不验证具体参数,因为可能有多次调用
|
|
// 我们只验证回调被调用,不验证具体参数,因为可能有多次调用
|
|
|
expect(onSpecsChange).toHaveBeenCalled();
|
|
expect(onSpecsChange).toHaveBeenCalled();
|
|
|
|
|
|
|
|
// 第四步:保存模板
|
|
// 第四步:保存模板
|
|
|
- fireEvent.click(screen.getByText('保存为模板'));
|
|
|
|
|
|
|
+ await user.click(screen.getByText('保存为模板'));
|
|
|
|
|
|
|
|
const templateInput = screen.getByPlaceholderText('输入模板名称');
|
|
const templateInput = screen.getByPlaceholderText('输入模板名称');
|
|
|
- fireEvent.change(templateInput, { target: { value: '颜色规格' } });
|
|
|
|
|
- fireEvent.click(screen.getByText('保存'));
|
|
|
|
|
|
|
+ await user.type(templateInput, '颜色规格');
|
|
|
|
|
+ await user.click(screen.getByText('保存'));
|
|
|
|
|
|
|
|
expect(onSaveTemplate).toHaveBeenCalledWith('颜色规格', [
|
|
expect(onSaveTemplate).toHaveBeenCalledWith('颜色规格', [
|
|
|
expect.objectContaining({ name: '深红色', price: 100, costPrice: 80, stock: 50 }),
|
|
expect.objectContaining({ name: '深红色', price: 100, costPrice: 80, stock: 50 }),
|
|
@@ -426,28 +462,29 @@ describe('BatchSpecCreatorInline', () => {
|
|
|
expect(screen.getByText('80')).toBeInTheDocument(); // 50 + 30
|
|
expect(screen.getByText('80')).toBeInTheDocument(); // 50 + 30
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- it('应该测试错误场景:保存空模板', () => {
|
|
|
|
|
|
|
+ it('应该测试错误场景:保存空模板', async () => {
|
|
|
|
|
+ const user = userEvent.setup();
|
|
|
const onSaveTemplate = vi.fn();
|
|
const onSaveTemplate = vi.fn();
|
|
|
const onSpecsChange = vi.fn();
|
|
const onSpecsChange = vi.fn();
|
|
|
renderComponent({ onSaveTemplate, onSpecsChange });
|
|
renderComponent({ onSaveTemplate, onSpecsChange });
|
|
|
|
|
|
|
|
// 先添加一个规格,这样"保存为模板"按钮才会显示
|
|
// 先添加一个规格,这样"保存为模板"按钮才会显示
|
|
|
- fireEvent.change(screen.getByLabelText('规格名称 *'), { target: { value: '测试规格' } });
|
|
|
|
|
- fireEvent.change(screen.getByLabelText('价格'), { target: { value: '100' } });
|
|
|
|
|
- fireEvent.click(screen.getByText('添加'));
|
|
|
|
|
|
|
+ await user.type(screen.getByLabelText('规格名称 *'), '测试规格');
|
|
|
|
|
+ await user.type(screen.getByLabelText('价格'), '100');
|
|
|
|
|
+ await user.click(screen.getByText('添加'));
|
|
|
|
|
|
|
|
// 尝试保存空模板
|
|
// 尝试保存空模板
|
|
|
- fireEvent.click(screen.getByText('保存为模板'));
|
|
|
|
|
|
|
+ await user.click(screen.getByText('保存为模板'));
|
|
|
|
|
|
|
|
const templateInput = screen.getByPlaceholderText('输入模板名称');
|
|
const templateInput = screen.getByPlaceholderText('输入模板名称');
|
|
|
- fireEvent.change(templateInput, { target: { value: '' } });
|
|
|
|
|
|
|
+ await user.clear(templateInput);
|
|
|
|
|
|
|
|
// 保存按钮应该被禁用
|
|
// 保存按钮应该被禁用
|
|
|
const saveButton = screen.getByText('保存');
|
|
const saveButton = screen.getByText('保存');
|
|
|
expect(saveButton).toBeDisabled();
|
|
expect(saveButton).toBeDisabled();
|
|
|
|
|
|
|
|
// 即使点击也不会触发保存
|
|
// 即使点击也不会触发保存
|
|
|
- fireEvent.click(saveButton);
|
|
|
|
|
|
|
+ await user.click(saveButton);
|
|
|
|
|
|
|
|
// 验证toast.error没有被调用(因为按钮被禁用)
|
|
// 验证toast.error没有被调用(因为按钮被禁用)
|
|
|
expect(toast.error).not.toHaveBeenCalledWith('请输入模板名称');
|
|
expect(toast.error).not.toHaveBeenCalledWith('请输入模板名称');
|