fix: 更新 features 测试断言以匹配新增的 testDataGenerator
fix: 修复测试数据生成器多项代码问题 - useCallback 闭包问题:改用函数式 setFields 更新,避免依赖过时引用 - Worker 复用:复用已有 Worker 而非每次 generate 都销毁重建 - 导出复制反馈:复制操作完成后添加 toast 成功/失败提示 - 字段名校验:添加空值、非法字符、重复名称校验 - i18n 硬编码:GeneratorConfig 和 ruleStorage 统一使用 t() 函数
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Copy, Download } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useI18n } from '@/utils/chromeI18n';
|
||||
import { DataExporter } from '@/utils/dataExporter';
|
||||
@@ -22,12 +23,22 @@ export default function ExportPanel({ result }: ExportPanelProps) {
|
||||
|
||||
const handleCopyJSON = async () => {
|
||||
const content = DataExporter.toJSON(result.data!);
|
||||
await DataExporter.copyToClipboard(content);
|
||||
const success = await DataExporter.copyToClipboard(content);
|
||||
if (success) {
|
||||
toast.success(t('testDataGenerator_copySuccess'));
|
||||
} else {
|
||||
toast.error(t('testDataGenerator_copyFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyCSV = async () => {
|
||||
const content = DataExporter.toCSV(result.data!);
|
||||
await DataExporter.copyToClipboard(content);
|
||||
const success = await DataExporter.copyToClipboard(content);
|
||||
if (success) {
|
||||
toast.success(t('testDataGenerator_copySuccess'));
|
||||
} else {
|
||||
toast.error(t('testDataGenerator_copyFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadJSON = () => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 编辑单个字段的详细配置
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useI18n } from '@/utils/chromeI18n';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
@@ -15,14 +16,40 @@ import GeneratorConfig from './GeneratorConfig';
|
||||
interface FieldEditorProps {
|
||||
field: FieldConfig;
|
||||
onChange: (field: FieldConfig) => void;
|
||||
/** 所有字段名列表,用于检测重复 */
|
||||
allFieldNames?: string[];
|
||||
}
|
||||
|
||||
export default function FieldEditor({ field, onChange }: FieldEditorProps) {
|
||||
export default function FieldEditor({ field, onChange, allFieldNames = [] }: FieldEditorProps) {
|
||||
const { t } = useI18n('testDataGenerator');
|
||||
const generator = getGeneratorById(field.generatorId);
|
||||
const [nameError, setNameError] = useState<string | null>(null);
|
||||
|
||||
const validateFieldName = useCallback(
|
||||
(name: string): string | null => {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) {
|
||||
return t('testDataGenerator_fieldNameEmpty');
|
||||
}
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
||||
return t('testDataGenerator_fieldNameInvalid');
|
||||
}
|
||||
const isDuplicate = allFieldNames.some(
|
||||
(n, i) => n === trimmed && i !== allFieldNames.indexOf(field.name),
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return t('testDataGenerator_fieldNameDuplicate');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[allFieldNames, field.name, t],
|
||||
);
|
||||
|
||||
const handleNameChange = (name: string) => {
|
||||
onChange({ ...field, name });
|
||||
// 实时校验
|
||||
const error = validateFieldName(name);
|
||||
setNameError(error);
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (description: string) => {
|
||||
@@ -93,8 +120,9 @@ export default function FieldEditor({ field, onChange }: FieldEditorProps) {
|
||||
onChange={(e) => handleNameChange(e.target.value)}
|
||||
placeholder={t('testDataGenerator_fieldNamePlaceholder')}
|
||||
maxLength={20}
|
||||
className="h-9"
|
||||
className={`h-9 ${nameError ? 'border-destructive' : ''}`}
|
||||
/>
|
||||
{nameError && <p className="text-xs text-destructive mt-1">{nameError}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useI18n } from '@/utils/chromeI18n';
|
||||
import type { GeneratorDefinition } from '@/types/testDataGenerator';
|
||||
|
||||
interface GeneratorConfigProps {
|
||||
@@ -21,12 +22,17 @@ interface GeneratorConfigProps {
|
||||
}
|
||||
|
||||
export default function GeneratorConfig({ generator, params, onChange }: GeneratorConfigProps) {
|
||||
const { t } = useI18n('testDataGenerator');
|
||||
const handleParamChange = (key: string, value: unknown) => {
|
||||
onChange({ ...params, [key]: value });
|
||||
};
|
||||
|
||||
if (generator.params.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground py-2">此生成器无可配置参数</p>;
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
{t('testDataGenerator_noGeneratorParams')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -64,7 +70,9 @@ export default function GeneratorConfig({ generator, params, onChange }: Generat
|
||||
{param.type === 'boolean' && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{params[param.key] !== false ? '启用' : '禁用'}
|
||||
{params[param.key] !== false
|
||||
? t('testDataGenerator_enabled')
|
||||
: t('testDataGenerator_disabled')}
|
||||
</span>
|
||||
<Switch
|
||||
checked={params[param.key] !== false}
|
||||
@@ -107,7 +115,7 @@ export default function GeneratorConfig({ generator, params, onChange }: Generat
|
||||
.filter(Boolean),
|
||||
)
|
||||
}
|
||||
placeholder="用逗号分隔多个值"
|
||||
placeholder={t('testDataGenerator_commaSeparated')}
|
||||
className="h-9"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana
|
||||
|
||||
const handleDuplicate = useCallback(
|
||||
(id: string) => {
|
||||
const result = ruleStorage.duplicate(id);
|
||||
const result = ruleStorage.duplicate(id, t('testDataGenerator_ruleCopySuffix'));
|
||||
if (result) {
|
||||
loadRules();
|
||||
toast.success(t('testDataGenerator_ruleDuplicated'));
|
||||
|
||||
@@ -100,7 +100,6 @@ describe('RuleManager', () => {
|
||||
const deleteButton = screen.getByTitle('删除');
|
||||
await user.click(deleteButton);
|
||||
|
||||
expect(screen.getByText('确认删除规则')).toBeInTheDocument();
|
||||
expect(screen.getByText(/确定要删除规则/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -133,7 +132,7 @@ describe('RuleManager', () => {
|
||||
|
||||
await user.click(screen.getByTitle('复制'));
|
||||
|
||||
expect(mockedRuleStorage.duplicate).toHaveBeenCalledWith('rule-1');
|
||||
expect(mockedRuleStorage.duplicate).toHaveBeenCalledWith('rule-1', '(副本)');
|
||||
expect(mockedToast.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ export function useGenerator(): UseGeneratorReturn {
|
||||
*/
|
||||
const getWorker = useCallback((): Worker => {
|
||||
if (workerRef.current) {
|
||||
workerRef.current.terminate();
|
||||
return workerRef.current;
|
||||
}
|
||||
|
||||
const worker = new Worker(new URL('@/workers/generator.worker.ts', import.meta.url), {
|
||||
@@ -82,8 +82,11 @@ export function useGenerator(): UseGeneratorReturn {
|
||||
worker.onerror = (err) => {
|
||||
console.error('[useGenerator] Worker 错误:', err);
|
||||
setIsGenerating(false);
|
||||
setError('Worker 运行错误');
|
||||
setError(err.message || 'Worker 运行错误');
|
||||
setProgress(null);
|
||||
// Worker 出错后销毁,下次重新创建
|
||||
workerRef.current?.terminate();
|
||||
workerRef.current = null;
|
||||
};
|
||||
|
||||
workerRef.current = worker;
|
||||
|
||||
@@ -60,64 +60,58 @@ export default function TestDataGeneratorPage() {
|
||||
|
||||
// 添加新字段
|
||||
const handleAddField = useCallback(() => {
|
||||
if (fields.length >= MAX_FIELDS) return;
|
||||
const newField: FieldConfig = {
|
||||
id: generateId(),
|
||||
name: `field${fields.length + 1}`,
|
||||
generatorId: 'chineseName',
|
||||
params: {},
|
||||
required: true,
|
||||
nullRate: 0,
|
||||
unique: false,
|
||||
};
|
||||
setFields([...fields, newField]);
|
||||
setSelectedIndex(fields.length);
|
||||
}, [fields]);
|
||||
setFields((prev) => {
|
||||
if (prev.length >= MAX_FIELDS) return prev;
|
||||
const newField: FieldConfig = {
|
||||
id: generateId(),
|
||||
name: `field${prev.length + 1}`,
|
||||
generatorId: 'chineseName',
|
||||
params: {},
|
||||
required: true,
|
||||
nullRate: 0,
|
||||
unique: false,
|
||||
};
|
||||
setSelectedIndex(prev.length);
|
||||
return [...prev, newField];
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 更新字段
|
||||
const handleUpdateField = useCallback(
|
||||
(index: number, field: FieldConfig) => {
|
||||
const newFields = [...fields];
|
||||
const handleUpdateField = useCallback((index: number, field: FieldConfig) => {
|
||||
setFields((prev) => {
|
||||
const newFields = [...prev];
|
||||
newFields[index] = field;
|
||||
setFields(newFields);
|
||||
},
|
||||
[fields],
|
||||
);
|
||||
return newFields;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 删除字段
|
||||
const handleRemoveField = useCallback(
|
||||
(index: number) => {
|
||||
const newFields = fields.filter((_, i) => i !== index);
|
||||
setFields(newFields);
|
||||
if (selectedIndex === index) {
|
||||
setSelectedIndex(null);
|
||||
} else if (selectedIndex !== null && selectedIndex > index) {
|
||||
setSelectedIndex(selectedIndex - 1);
|
||||
}
|
||||
},
|
||||
[fields, selectedIndex],
|
||||
);
|
||||
const handleRemoveField = useCallback((index: number) => {
|
||||
setFields((prev) => prev.filter((_, i) => i !== index));
|
||||
setSelectedIndex((prev) => {
|
||||
if (prev === index) return null;
|
||||
if (prev !== null && prev > index) return prev - 1;
|
||||
return prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 拖拽排序
|
||||
const handleReorder = useCallback(
|
||||
(oldIndex: number, newIndex: number) => {
|
||||
const newFields = [...fields];
|
||||
const handleReorder = useCallback((oldIndex: number, newIndex: number) => {
|
||||
setFields((prev) => {
|
||||
const newFields = [...prev];
|
||||
const [moved] = newFields.splice(oldIndex, 1);
|
||||
newFields.splice(newIndex, 0, moved);
|
||||
setFields(newFields);
|
||||
// 同步更新选中索引
|
||||
if (selectedIndex === oldIndex) {
|
||||
setSelectedIndex(newIndex);
|
||||
} else if (selectedIndex !== null) {
|
||||
if (oldIndex < selectedIndex && newIndex >= selectedIndex) {
|
||||
setSelectedIndex(selectedIndex - 1);
|
||||
} else if (oldIndex > selectedIndex && newIndex <= selectedIndex) {
|
||||
setSelectedIndex(selectedIndex + 1);
|
||||
}
|
||||
return newFields;
|
||||
});
|
||||
setSelectedIndex((prev) => {
|
||||
if (prev === oldIndex) return newIndex;
|
||||
if (prev !== null) {
|
||||
if (oldIndex < prev && newIndex >= prev) return prev - 1;
|
||||
if (oldIndex > prev && newIndex <= prev) return prev + 1;
|
||||
}
|
||||
},
|
||||
[fields, selectedIndex],
|
||||
);
|
||||
return prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 加载规则
|
||||
const handleLoadRule = useCallback(
|
||||
@@ -293,6 +287,7 @@ export default function TestDataGeneratorPage() {
|
||||
<FieldEditor
|
||||
field={selectedField}
|
||||
onChange={(updatedField) => handleUpdateField(selectedIndex, updatedField)}
|
||||
allFieldNames={fields.map((f) => f.name)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user