Remove .DS_Store from version control and update .gitignore. Enhance BatchCaseDetailDrawer and BatchRunCompletedView components to support error handling and improved UI for displaying test case statuses. Add functionality for editing and rerunning test cases. Update TestCasesPage to handle initial case selection. Refactor related components for better maintainability.
This commit is contained in:
@@ -56,25 +56,19 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
cloneOverallExpectation,
|
||||
cloneOverallCriteria,
|
||||
cloneTurns,
|
||||
cloneUserSimulation,
|
||||
cloneVoiceSettings,
|
||||
createDefaultUserSimulation,
|
||||
createDefaultVoiceSettings,
|
||||
createEmptyFixedInputTurn,
|
||||
createTestCase,
|
||||
createTestSuite,
|
||||
DEFAULT_INPUT_MODE,
|
||||
duplicateTestCase,
|
||||
duplicateTestSuite,
|
||||
getTestCaseValidationMessage,
|
||||
getTestSuite,
|
||||
isFixedScriptMode,
|
||||
isVoiceMode,
|
||||
kindFromInputMode,
|
||||
listTestCases,
|
||||
listTestSuites,
|
||||
normalizeOverallExpectation,
|
||||
normalizeOverallCriteria,
|
||||
removeTestCase,
|
||||
removeTestCases,
|
||||
removeTestSuite,
|
||||
@@ -97,12 +91,18 @@ import { assistantsApi, type Assistant } from "@/lib/api";
|
||||
export type TestCasesPageProps =
|
||||
| { mode: "list" }
|
||||
| { mode: "create" }
|
||||
| { mode: "detail"; suiteId: string };
|
||||
| { mode: "detail"; suiteId: string; initialCaseId?: string };
|
||||
|
||||
export function TestCasesPage(props: TestCasesPageProps) {
|
||||
if (props.mode === "list") return <SuiteListView />;
|
||||
if (props.mode === "create") return <SuiteCreateView />;
|
||||
return <SuiteDetailView key={props.suiteId} suiteId={props.suiteId} />;
|
||||
return (
|
||||
<SuiteDetailView
|
||||
key={`${props.suiteId}:${props.initialCaseId ?? ""}`}
|
||||
suiteId={props.suiteId}
|
||||
initialCaseId={props.initialCaseId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Suite 列表 ──────────────────────────────────────────────────────────────
|
||||
@@ -441,12 +441,9 @@ function caseToDraft(item: TestCase): CaseEditorDraft {
|
||||
return {
|
||||
name: item.name,
|
||||
inputMode: item.inputMode,
|
||||
kind: item.kind,
|
||||
contextTurns: item.contextTurns.map((turn) => ({ ...turn })),
|
||||
turns: cloneTurns(item.turns),
|
||||
voiceSettings: cloneVoiceSettings(item.voiceSettings),
|
||||
userSimulation: cloneUserSimulation(item.userSimulation),
|
||||
overallExpectation: cloneOverallExpectation(item.overallExpectation),
|
||||
overallCriteria: cloneOverallCriteria(item.overallCriteria),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -458,12 +455,9 @@ function createEmptyCaseDraft(): CaseEditorDraft {
|
||||
return {
|
||||
name: "未命名用例",
|
||||
inputMode: DEFAULT_INPUT_MODE,
|
||||
kind: kindFromInputMode(DEFAULT_INPUT_MODE),
|
||||
contextTurns: [],
|
||||
turns: [createEmptyFixedInputTurn()],
|
||||
voiceSettings: null,
|
||||
userSimulation: null,
|
||||
overallExpectation: null,
|
||||
overallCriteria: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -471,25 +465,23 @@ function applyInputMode(
|
||||
draft: CaseEditorDraft,
|
||||
mode: TestCaseInputMode,
|
||||
): CaseEditorDraft {
|
||||
const next: CaseEditorDraft = {
|
||||
...draft,
|
||||
inputMode: mode,
|
||||
kind: kindFromInputMode(mode),
|
||||
};
|
||||
if (isVoiceMode(mode) && !next.voiceSettings) {
|
||||
next.voiceSettings = createDefaultVoiceSettings();
|
||||
}
|
||||
if (!isFixedScriptMode(mode) && !next.userSimulation) {
|
||||
next.userSimulation = createDefaultUserSimulation();
|
||||
}
|
||||
return next;
|
||||
return { ...draft, inputMode: mode };
|
||||
}
|
||||
|
||||
function SuiteDetailView({ suiteId }: { suiteId: string }) {
|
||||
function SuiteDetailView({
|
||||
suiteId,
|
||||
initialCaseId,
|
||||
}: {
|
||||
suiteId: string;
|
||||
initialCaseId?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [initial] = useState(() => {
|
||||
const initialCases = listTestCases(suiteId);
|
||||
const initialCase = initialCases[0] ?? null;
|
||||
const initialCase =
|
||||
initialCases.find((item) => item.id === initialCaseId) ??
|
||||
initialCases[0] ??
|
||||
null;
|
||||
const initialDraft = initialCase ? caseToDraft(initialCase) : null;
|
||||
return {
|
||||
suite: getTestSuite(suiteId),
|
||||
@@ -562,6 +554,60 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
|
||||
savedSnapshot === null ||
|
||||
!draftsEqual(draft, savedSnapshot));
|
||||
|
||||
const validationMessage = draft
|
||||
? getTestCaseValidationMessage(draft)
|
||||
: "请先选择测试用例";
|
||||
const canSave =
|
||||
Boolean(draft?.name.trim()) && dirty && validationMessage === null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!dirty) return;
|
||||
|
||||
function handleBeforeUnload(event: BeforeUnloadEvent) {
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
}
|
||||
|
||||
function handleInternalLink(event: MouseEvent) {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
event.button !== 0 ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
const link = target.closest<HTMLAnchorElement>("a[href]");
|
||||
if (!link || link.target === "_blank" || link.hasAttribute("download")) {
|
||||
return;
|
||||
}
|
||||
const nextUrl = new URL(link.href, window.location.href);
|
||||
if (nextUrl.origin !== window.location.origin) return;
|
||||
if (
|
||||
nextUrl.pathname === window.location.pathname &&
|
||||
nextUrl.search === window.location.search
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (window.confirm("当前用例有未保存修改,离开将丢弃。继续?")) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
document.addEventListener("click", handleInternalLink, true);
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
document.removeEventListener("click", handleInternalLink, true);
|
||||
};
|
||||
}, [dirty]);
|
||||
|
||||
function selectCase(item: TestCase) {
|
||||
if (dirty && !window.confirm("当前用例有未保存修改,切换将丢弃。继续?")) {
|
||||
return;
|
||||
@@ -584,18 +630,13 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (!draft) return;
|
||||
if (!draft || validationMessage) return;
|
||||
const patch = {
|
||||
name: draft.name.trim() || "未命名用例",
|
||||
inputMode: draft.inputMode,
|
||||
kind: kindFromInputMode(draft.inputMode),
|
||||
contextTurns: draft.contextTurns,
|
||||
turns: draft.turns,
|
||||
voiceSettings: draft.voiceSettings,
|
||||
userSimulation: draft.userSimulation,
|
||||
overallExpectation: normalizeOverallExpectation(
|
||||
draft.overallExpectation?.criteria,
|
||||
),
|
||||
overallCriteria: normalizeOverallCriteria(draft.overallCriteria),
|
||||
};
|
||||
|
||||
let saved: TestCase | null;
|
||||
@@ -807,7 +848,15 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
|
||||
/>
|
||||
|
||||
<div className="ml-auto flex shrink-0 items-center gap-2">
|
||||
{dirty ? (
|
||||
{dirty && validationMessage ? (
|
||||
<span
|
||||
className="max-w-56 truncate text-xs text-destructive"
|
||||
title={validationMessage}
|
||||
role="status"
|
||||
>
|
||||
{validationMessage}
|
||||
</span>
|
||||
) : dirty ? (
|
||||
<span className="text-xs text-amber-600">未保存</span>
|
||||
) : statusMessage ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
@@ -817,7 +866,8 @@ function SuiteDetailView({ suiteId }: { suiteId: string }) {
|
||||
<Button
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
disabled={!dirty || !draft.name.trim()}
|
||||
disabled={!canSave}
|
||||
title={validationMessage ?? undefined}
|
||||
onClick={handleSave}
|
||||
>
|
||||
<Save size={14} />
|
||||
|
||||
Reference in New Issue
Block a user