import { executeOnce } from "./features/testing/execution"; /** * 用例执行器:串行执行步骤 → 断言 → 变量提取 → 生成报告。 * 与手动发送共用 buildHttpSpec(含环境变量与 Mock)。 */ import type { CollectionNode, HttpSpec, RequestNode, TestRun, TestRunTrigger, TestStep, TestStepResult, } from "./core/index"; import { evaluateAssertion, extractVariables } from "./core/index"; import { buildHttpSpec } from "./send"; import { activeEnvBaseUrl, activeEnvMap, useWorkspace } from "./workspaceStore"; async function getTransportFn() { const { detectTransport } = await import("./transport/index"); const { transport } = detectTransport({ ginBaseUrl: "" }); return transport.http.bind(transport); } function stepLabel(step: TestStep, node: RequestNode | undefined): string { return node?.name ?? "(接口已被删除)"; } /** 执行单个用例并写入运行记录;返回本次报告 */ export function runTestCase(caseId: string, trigger: TestRunTrigger): Promise { return executeOnce(caseId, () => executeTestCase(caseId, trigger)); } async function executeTestCase(caseId: string, trigger: TestRunTrigger): Promise { const tc = useWorkspace.getState().testCases.find((c) => c.id === caseId); if (!tc) return null; const http = await getTransportFn(); const started = performance.now(); const stepResults: TestStepResult[] = []; for (const step of tc.steps) { if (!step.enabled) continue; const node = findRequestNode(step.nodeId); if (!node || node.kind !== "http") { stepResults.push({ stepId: step.id, name: stepLabel(step, node), method: "—", url: "—", ok: false, httpStatus: null, durationMs: null, error: node ? "仅支持 HTTP 步骤" : "接口不存在或已被删除", assertionResults: [], }); if (!tc.continueOnFailure) break; continue; } const env = activeEnvMap(); const { timeoutMs, mock } = useWorkspace.getState().settings; const spec: HttpSpec = buildHttpSpec(node.data, env, timeoutMs, mock, activeEnvBaseUrl()); const t0 = performance.now(); try { const res = await http(spec); const durationMs = Math.round(performance.now() - t0); const stepRes: TestStepResult = { stepId: step.id, name: node.name, method: spec.method, url: spec.url, ok: true, httpStatus: res.status, durationMs, error: null, assertionResults: step.assertions .filter((a) => a.enabled) .map((a) => evaluateAssertion(res, a)), }; stepRes.ok = stepRes.assertionResults.every((a) => a.passed); // 变量提取 → 写入当前环境 const extracted = extractVariables(res, step.extracts); if (Object.keys(extracted).length) { useWorkspace.getState().setEnvValues(extracted); } stepResults.push(stepRes); if (!stepRes.ok && !tc.continueOnFailure) break; } catch (e) { stepResults.push({ stepId: step.id, name: node.name, method: spec.method, url: spec.url, ok: false, httpStatus: null, durationMs: Math.round(performance.now() - t0), error: e instanceof Error ? e.message : String(e), assertionResults: [], }); if (!tc.continueOnFailure) break; } } const run: TestRun = { id: crypto.randomUUID(), caseId: tc.id, caseName: tc.name, at: Date.now(), trigger, totalMs: Math.round(performance.now() - started), passed: stepResults.length > 0 && stepResults.every((s) => s.ok), stepResults, }; useWorkspace.getState().addTestRun(run); return run; } function findRequestNode(id: string): RequestNode | undefined { const { collections } = useWorkspace.getState(); const walk = (nodes: CollectionNode[]): RequestNode | undefined => { for (const n of nodes) { if (n.type === "request" && n.id === id) return n; if (n.type === "folder") { const hit = walk(n.children); if (hit) return hit; } } return undefined; }; for (const c of collections) { const hit = walk(c.children); if (hit) return hit; } return undefined; }