feat(frontend): implement batch testing views and status components
Add components for batch testing, including BatchRunCompletedView and BatchRunRunningView, to display test results and progress. Introduce BatchCaseStatusBadge and BatchPhasePill for visual status representation. Implement mock data handling for batch run snapshots and case statuses, enhancing the user experience for managing batch tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 批量测试运行 — 前端 mock。
|
||||
* 真实执行引擎接入前,用本地状态模拟进度与结果。
|
||||
*/
|
||||
|
||||
import type { TestCase } from "@/data/test-suites";
|
||||
|
||||
export type BatchCaseStatus =
|
||||
| "waiting"
|
||||
| "running"
|
||||
| "pass"
|
||||
| "fail"
|
||||
| "skipped";
|
||||
|
||||
export type BatchRunCase = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: BatchCaseStatus;
|
||||
expected: string;
|
||||
actual: string;
|
||||
failReason: string;
|
||||
};
|
||||
|
||||
export type BatchRunPhase = "config" | "running" | "completed";
|
||||
|
||||
export type BatchRunSnapshot = {
|
||||
title: string;
|
||||
assistantName: string;
|
||||
cases: BatchRunCase[];
|
||||
startedAt: string;
|
||||
finishedAt: string | null;
|
||||
stopped: boolean;
|
||||
};
|
||||
|
||||
/** 从测试用例生成期望文案(展示用) */
|
||||
export function buildExpectedResult(item: TestCase): string {
|
||||
if (item.assertionType === "llm") {
|
||||
return item.llmCriteria.trim() || "回复应符合 LLM 判断标准。";
|
||||
}
|
||||
if (item.keywords.length === 0) {
|
||||
return "回复应包含预期关键词。";
|
||||
}
|
||||
const mode =
|
||||
item.keywordMatchMode === "all" ? "同时包含" : "至少包含其一";
|
||||
return `回复应${mode}:${item.keywords.join("、")}。`;
|
||||
}
|
||||
|
||||
/** 预定部分用例失败,方便演示失败展开态 */
|
||||
function shouldFail(index: number, item: TestCase): boolean {
|
||||
if (item.lastResult === "fail") return true;
|
||||
return index % 4 === 2;
|
||||
}
|
||||
|
||||
function mockFailActual(): string {
|
||||
return "已为您转接人工处理。";
|
||||
}
|
||||
|
||||
function mockFailReason(item: TestCase): string {
|
||||
if (item.assertionType === "llm") {
|
||||
return "未按照预期确认关键信息,且错误转接人工。";
|
||||
}
|
||||
return `回复未命中预期关键词(${item.keywords.slice(0, 3).join("、") || "无"})。`;
|
||||
}
|
||||
|
||||
function mockPassActual(item: TestCase): string {
|
||||
if (item.assertionType === "keyword" && item.keywords[0]) {
|
||||
return `好的,请继续描述事故经过,并确认是否有人受伤。`;
|
||||
}
|
||||
return "好的,我已记录,我们继续处理。";
|
||||
}
|
||||
|
||||
export function createBatchRunSnapshot(input: {
|
||||
title: string;
|
||||
assistantName: string;
|
||||
cases: TestCase[];
|
||||
}): BatchRunSnapshot {
|
||||
return {
|
||||
title: input.title,
|
||||
assistantName: input.assistantName,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: null,
|
||||
stopped: false,
|
||||
cases: input.cases.map((item, index) => {
|
||||
const fail = shouldFail(index, item);
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
status: "waiting" as const,
|
||||
expected: buildExpectedResult(item),
|
||||
actual: fail ? mockFailActual() : mockPassActual(item),
|
||||
failReason: fail ? mockFailReason(item) : "",
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function countByStatus(cases: BatchRunCase[]) {
|
||||
const counts = {
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
running: 0,
|
||||
waiting: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
for (const item of cases) {
|
||||
counts[item.status] += 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function formatRunTime(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
}
|
||||
@@ -300,6 +300,38 @@ export function removeTestSuite(id: string): boolean {
|
||||
return suites.length < before;
|
||||
}
|
||||
|
||||
/** 复制测试集及其全部用例;名称加「(副本)」 */
|
||||
export function duplicateTestSuite(id: string): TestSuite | null {
|
||||
const source = getTestSuite(id);
|
||||
if (!source) return null;
|
||||
|
||||
const copied = createTestSuite({
|
||||
name: `${source.name}(副本)`,
|
||||
description: source.description,
|
||||
assistantName: source.assistantName,
|
||||
});
|
||||
|
||||
const sourceCases = listTestCases(id);
|
||||
const clonedCases: TestCase[] = sourceCases.map((item, index) => ({
|
||||
id: nextCaseId(),
|
||||
suiteId: copied.id,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
kind: item.kind,
|
||||
lastResult: "not_run",
|
||||
contextTurns: item.contextTurns.map((turn) => ({ ...turn })),
|
||||
userInput: item.userInput,
|
||||
assertionType: item.assertionType,
|
||||
keywords: [...item.keywords],
|
||||
keywordMatchMode: item.keywordMatchMode,
|
||||
llmCriteria: item.llmCriteria,
|
||||
sortOrder: index,
|
||||
updatedAt: nowIso(),
|
||||
}));
|
||||
cases = [...cases, ...clonedCases];
|
||||
return copied;
|
||||
}
|
||||
|
||||
export function createTestCase(input: {
|
||||
suiteId: string;
|
||||
name?: string;
|
||||
@@ -330,6 +362,36 @@ export function createTestCase(input: {
|
||||
return item;
|
||||
}
|
||||
|
||||
/** 复制单条用例;名称加「(副本)」,排在同套件末尾 */
|
||||
export function duplicateTestCase(id: string): TestCase | null {
|
||||
const source = getTestCase(id);
|
||||
if (!source || !getTestSuite(source.suiteId)) return null;
|
||||
|
||||
const maxOrder = cases
|
||||
.filter((item) => item.suiteId === source.suiteId)
|
||||
.reduce((max, item) => Math.max(max, item.sortOrder), -1);
|
||||
|
||||
const copied: TestCase = {
|
||||
id: nextCaseId(),
|
||||
suiteId: source.suiteId,
|
||||
name: `${source.name}(副本)`,
|
||||
description: source.description,
|
||||
kind: source.kind,
|
||||
lastResult: "not_run",
|
||||
contextTurns: source.contextTurns.map((turn) => ({ ...turn })),
|
||||
userInput: source.userInput,
|
||||
assertionType: source.assertionType,
|
||||
keywords: [...source.keywords],
|
||||
keywordMatchMode: source.keywordMatchMode,
|
||||
llmCriteria: source.llmCriteria,
|
||||
sortOrder: maxOrder + 1,
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
cases = [...cases, copied];
|
||||
updateTestSuite(source.suiteId, {});
|
||||
return copied;
|
||||
}
|
||||
|
||||
export type TestCasePatch = Partial<
|
||||
Pick<
|
||||
TestCase,
|
||||
|
||||
Reference in New Issue
Block a user