From ff3c66531c531148b7eba5b6d89c86078bfad3e1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 29 Jun 2026 11:05:02 +0000 Subject: [PATCH] =?UTF-8?q?fix(=E6=B5=8B=E8=AF=95=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8):=20=E4=BF=AE=E5=A4=8D=E8=A7=84?= =?UTF-8?q?=E5=88=99=E8=A6=86=E7=9B=96=E4=BF=9D=E5=AD=98=E8=AF=AF=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E9=87=8D=E5=A4=8D=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖同名规则时 handleSave(true) 未传入已有规则 id,save() 走新建分支 导致旧规则保留且产生同名重复项,getByName 仍返回旧数据。 --- .../components/FieldList.tsx | 35 ++++--- .../components/__tests__/FieldList.test.tsx | 92 +++++++++++++++++++ src/utils/__tests__/ruleStorage.test.ts | 21 +++++ 3 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 src/pages/TestDataGenerator/components/__tests__/FieldList.test.tsx diff --git a/src/pages/TestDataGenerator/components/FieldList.tsx b/src/pages/TestDataGenerator/components/FieldList.tsx index 1c4870a..da069a1 100644 --- a/src/pages/TestDataGenerator/components/FieldList.tsx +++ b/src/pages/TestDataGenerator/components/FieldList.tsx @@ -234,29 +234,38 @@ export default function FieldList({ if (!ruleName.trim()) return; const trimmedName = ruleName.trim(); + const existingRule = ruleStorage.getByName(trimmedName); // 检查名称是否重复 - if (!overwrite) { - const existingRule = ruleStorage.getByName(trimmedName); - if (existingRule) { - setShowConfirmOverwrite(true); - return; - } + if (!overwrite && existingRule) { + setShowConfirmOverwrite(true); + return; } - const newRule = ruleStorage.save({ - name: trimmedName, - description: ruleDescription.trim(), - fields: fields, - }); + const savedRule = ruleStorage.save( + overwrite && existingRule + ? { + id: existingRule.id, + name: trimmedName, + description: ruleDescription.trim(), + fields: fields, + } + : { + name: trimmedName, + description: ruleDescription.trim(), + fields: fields, + }, + ); - if (newRule) { + if (savedRule) { setShowSaveDialog(false); setShowConfirmOverwrite(false); setRuleName(''); setRuleDescription(''); - toast.success('规则已保存'); + toast.success(overwrite ? '规则已覆盖' : '规则已保存'); onRuleSaved?.(); + } else { + toast.error('规则保存失败'); } }, [ruleName, ruleDescription, fields, onRuleSaved], diff --git a/src/pages/TestDataGenerator/components/__tests__/FieldList.test.tsx b/src/pages/TestDataGenerator/components/__tests__/FieldList.test.tsx new file mode 100644 index 0000000..8d462af --- /dev/null +++ b/src/pages/TestDataGenerator/components/__tests__/FieldList.test.tsx @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import type { FieldConfig } from '@/types/testDataGenerator'; + +vi.mock('@/utils/ruleStorage', () => ({ + getByName: vi.fn(), + save: vi.fn(), + update: vi.fn(), +})); + +vi.mock('sonner', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +import FieldList from '../FieldList'; +import * as ruleStorage from '@/utils/ruleStorage'; +import { toast } from 'sonner'; + +const mockedRuleStorage = vi.mocked(ruleStorage); +const mockedToast = vi.mocked(toast); + +const mockFields: FieldConfig[] = [ + { + id: 'field-1', + name: 'username', + generatorId: 'string', + params: {}, + required: true, + nullRate: 0, + unique: false, + }, +]; + +const defaultProps = { + fields: mockFields, + onUpdate: vi.fn(), + onRemove: vi.fn(), + onAdd: vi.fn(), + onEdit: vi.fn(), + onReorder: vi.fn(), +}; + +describe('FieldList 规则保存', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockedRuleStorage.getByName.mockReturnValue(undefined); + mockedRuleStorage.save.mockReturnValue({ + id: 'rule-1', + name: 'My Rule', + fields: mockFields, + createdAt: Date.now(), + updatedAt: Date.now(), + useCount: 0, + }); + }); + + it('覆盖同名规则时应更新已有规则而非新建', async () => { + const user = userEvent.setup(); + const existingRule = { + id: 'existing-rule-id', + name: 'My Rule', + fields: mockFields, + createdAt: Date.now(), + updatedAt: Date.now(), + useCount: 0, + }; + + mockedRuleStorage.getByName.mockReturnValue(existingRule); + + render(); + + await user.click(screen.getByRole('button', { name: /保存规则/ })); + await user.type(screen.getByPlaceholderText('规则名称'), 'My Rule'); + await user.click(screen.getByRole('button', { name: '确认' })); + + expect(screen.getByText('已存在同名规则,是否覆盖保存?')).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: '覆盖' })); + + expect(mockedRuleStorage.save).toHaveBeenCalledWith({ + id: 'existing-rule-id', + name: 'My Rule', + description: '', + fields: mockFields, + }); + expect(mockedToast.success).toHaveBeenCalledWith('规则已覆盖'); + }); +}); diff --git a/src/utils/__tests__/ruleStorage.test.ts b/src/utils/__tests__/ruleStorage.test.ts index 27330d7..a7b6434 100644 --- a/src/utils/__tests__/ruleStorage.test.ts +++ b/src/utils/__tests__/ruleStorage.test.ts @@ -48,6 +48,27 @@ describe('ruleStorage', () => { expect(updated).toBeNull(); }); + it('save 带 id 时应更新已有规则而非新建', () => { + const first = ruleStorage.save({ + name: 'Test Rule', + fields: [mockField], + }); + expect(first).not.toBeNull(); + + const updatedField = { ...mockField, name: 'email' }; + const updated = ruleStorage.save({ + id: first!.id, + name: 'Test Rule', + description: 'Updated', + fields: [updatedField], + }); + + expect(updated).not.toBeNull(); + expect(ruleStorage.getCount()).toBe(1); + expect(ruleStorage.getById(first!.id)?.fields[0].name).toBe('email'); + expect(ruleStorage.getById(first!.id)?.description).toBe('Updated'); + }); + it('deleteRule 在 localStorage 写入失败时应返回 false', () => { const saved = ruleStorage.save({ name: 'Test Rule',