feat(测试用例): 状态流转接入单条工作流
This commit is contained in:
@@ -1,25 +1,24 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import type { TestCase, TestCaseStatus } from '@/lib/test-case';
|
||||
import { canTcTransition, generateCaseNo } from '@/lib/test-case';
|
||||
import { generateCaseNo, normalizeTestCases } from '@/lib/test-case';
|
||||
import { applyTestCaseTransition, normalizeTestCaseOnCreate } from '@/lib/test-case-workflow';
|
||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||
|
||||
const STORAGE_KEY = 'ftb_test_cases_v1';
|
||||
|
||||
function saveLocal(items: TestCase[]) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); } catch {}
|
||||
function saveStored(items: TestCase[]) {
|
||||
saveServerData('test-cases', items).catch(() => {});
|
||||
}
|
||||
|
||||
function loadLocal(): TestCase[] | null {
|
||||
async function loadStored(): Promise<TestCase[] | null> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
return await loadServerData<TestCase[]>('test-cases');
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface TestCaseState {
|
||||
testCases: TestCase[];
|
||||
fetchTestCases: () => void;
|
||||
fetchTestCases: () => Promise<void>;
|
||||
createTestCase: (data: Omit<TestCase, 'id' | 'caseNo' | 'createdAt' | 'updatedAt' | 'status'>) => TestCase;
|
||||
updateTestCase: (id: string, data: Partial<TestCase>) => void;
|
||||
deleteTestCase: (id: string) => void;
|
||||
@@ -32,60 +31,53 @@ interface TestCaseState {
|
||||
export const useTestCaseStore = create<TestCaseState>((set, get) => ({
|
||||
testCases: [],
|
||||
|
||||
fetchTestCases: () => {
|
||||
const cached = loadLocal();
|
||||
if (cached) set({ testCases: cached });
|
||||
fetchTestCases: async () => {
|
||||
const cached = await loadStored();
|
||||
if (cached) set({ testCases: normalizeTestCases(cached) });
|
||||
},
|
||||
|
||||
createTestCase: (data) => {
|
||||
const list = get().testCases;
|
||||
const now = new Date().toISOString();
|
||||
const tc: TestCase = {
|
||||
const tc: TestCase = normalizeTestCaseOnCreate({
|
||||
...data,
|
||||
id: `tc-${Date.now()}`,
|
||||
caseNo: generateCaseNo(list),
|
||||
status: 'pending',
|
||||
estimateHours: data.estimateHours ?? 0.5,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
} as TestCase);
|
||||
const updated = [...list, tc];
|
||||
set({ testCases: updated });
|
||||
saveLocal(updated);
|
||||
saveStored(updated);
|
||||
return tc;
|
||||
},
|
||||
|
||||
updateTestCase: (id, data) => {
|
||||
const updated = get().testCases.map((c) =>
|
||||
c.id === id ? { ...c, ...data, updatedAt: new Date().toISOString() } : c,
|
||||
c.id === id ? { ...c, ...data, aiDraft: false, updatedAt: new Date().toISOString() } : c,
|
||||
);
|
||||
set({ testCases: updated });
|
||||
saveLocal(updated);
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
deleteTestCase: (id) => {
|
||||
const updated = get().testCases.filter((c) => c.id !== id);
|
||||
set({ testCases: updated });
|
||||
saveLocal(updated);
|
||||
saveStored(updated);
|
||||
},
|
||||
|
||||
changeStatus: (id, to, extra) => {
|
||||
const tc = get().testCases.find((c) => c.id === id);
|
||||
if (!tc) return { ok: false, message: '用例不存在' };
|
||||
if (!canTcTransition(tc.status, to)) {
|
||||
return { ok: false, message: `不允许从「${tc.status}」流转到「${to}」` };
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
const patch: Partial<TestCase> = { status: to };
|
||||
if (to === 'running' && !tc.startedAt) patch.startedAt = now;
|
||||
if (to === 'passed' || to === 'failed' || to === 'blocked') patch.completedAt = now;
|
||||
if (to === 'running' || to === 'passed' || to === 'failed' || to === 'blocked') {
|
||||
patch.executedAt = now;
|
||||
}
|
||||
// 回退到执行中时清除完成时间
|
||||
if (to === 'running') { patch.failReason = undefined; patch.blockReason = undefined; patch.completedAt = undefined; }
|
||||
if (to === 'failed' && extra?.failReason) patch.failReason = extra.failReason;
|
||||
if (to === 'blocked' && extra?.blockReason) patch.blockReason = extra.blockReason;
|
||||
get().updateTestCase(id, patch);
|
||||
const result = applyTestCaseTransition(tc, to, {
|
||||
now: new Date(),
|
||||
failReason: extra?.failReason,
|
||||
blockReason: extra?.blockReason,
|
||||
});
|
||||
if (!result.ok || !result.patch) return { ok: false, message: result.message };
|
||||
get().updateTestCase(id, result.patch);
|
||||
return { ok: true };
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user