- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化) - 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转 - 设置→外观 主题顺序调整为 深色/浅色/跟随系统 - release 二进制重编并验证(浏览器实测关于页链接可开文档页) - 附带工作区中未提交的扁平化重构现状(另一会话进行中) - homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
138 lines
4.2 KiB
TypeScript
138 lines
4.2 KiB
TypeScript
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<TestRun | null> {
|
|
return executeOnce(caseId, () => executeTestCase(caseId, trigger));
|
|
}
|
|
|
|
async function executeTestCase(caseId: string, trigger: TestRunTrigger): Promise<TestRun | null> {
|
|
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;
|
|
}
|