桌面 GUI 外链修复 + 主题顺序调整(含结构扁平化进行中的工作区基线)
- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化) - 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转 - 设置→外观 主题顺序调整为 深色/浅色/跟随系统 - release 二进制重编并验证(浏览器实测关于页链接可开文档页) - 附带工作区中未提交的扁平化重构现状(另一会话进行中) - homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
This commit is contained in:
1 parent
6452026fd6
commit
2bb4ef7a95
299 files changed
+6059
-3322
No files matched your search
@@ -0,0 +1,196 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
describeAssertion,
|
||||
evaluateAssertion,
|
||||
exportRunJson,
|
||||
exportRunMarkdown,
|
||||
extractVariables,
|
||||
newTestCase,
|
||||
queryJsonPath,
|
||||
runStats,
|
||||
suggestAssertions,
|
||||
type StepHttpResponse,
|
||||
} from "./testing";
|
||||
import type { TestAssertion } from "./model";
|
||||
|
||||
const res: StepHttpResponse = {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json", "X-Trace-Id": "abc123" },
|
||||
body: JSON.stringify({ code: 0, data: { id: 42, name: "opsx", tags: ["a", "b"] }, msg: "ok" }),
|
||||
timeMs: 123,
|
||||
};
|
||||
|
||||
const as = (over: Partial<TestAssertion>): TestAssertion => ({
|
||||
id: "a1",
|
||||
enabled: true,
|
||||
target: "status",
|
||||
locator: "",
|
||||
op: "eq",
|
||||
expected: "",
|
||||
...over,
|
||||
});
|
||||
|
||||
describe("queryJsonPath", () => {
|
||||
it("基本路径", () => {
|
||||
const obj = JSON.parse(res.body);
|
||||
expect(queryJsonPath(obj, "$.code")).toBe(0);
|
||||
expect(queryJsonPath(obj, "$.data.id")).toBe(42);
|
||||
expect(queryJsonPath(obj, "$.data.tags[1]")).toBe("b");
|
||||
expect(queryJsonPath(obj, "$['data']['name']")).toBe("opsx");
|
||||
expect(queryJsonPath(obj, "$.data.missing")).toBeUndefined();
|
||||
expect(queryJsonPath(obj, "$")).toEqual(obj);
|
||||
});
|
||||
});
|
||||
|
||||
describe("断言评估", () => {
|
||||
it("状态码 / 耗时(数字比较)", () => {
|
||||
expect(evaluateAssertion(res, as({ target: "status", op: "eq", expected: "200" })).passed).toBe(true);
|
||||
expect(evaluateAssertion(res, as({ target: "status", op: "lt", expected: "300" })).passed).toBe(true);
|
||||
expect(evaluateAssertion(res, as({ target: "timeMs", op: "gt", expected: "100" })).passed).toBe(true);
|
||||
expect(evaluateAssertion(res, as({ target: "timeMs", op: "lt", expected: "50" })).passed).toBe(false);
|
||||
});
|
||||
|
||||
it("Header 大小写不敏感", () => {
|
||||
expect(evaluateAssertion(res, as({ target: "header", locator: "x-trace-id", op: "contains", expected: "abc" })).passed).toBe(true);
|
||||
expect(evaluateAssertion(res, as({ target: "header", locator: "X-Missing", op: "exists" })).passed).toBe(false);
|
||||
});
|
||||
|
||||
it("JSONPath 断言", () => {
|
||||
expect(evaluateAssertion(res, as({ target: "body", locator: "$.data.name", op: "jsonpath_eq", expected: "opsx" })).passed).toBe(true);
|
||||
expect(evaluateAssertion(res, as({ target: "body", locator: "$.data.id", op: "jsonpath_eq", expected: "43" })).passed).toBe(false);
|
||||
expect(evaluateAssertion(res, as({ target: "body", locator: "$.msg", op: "jsonpath_contains", expected: "o" })).passed).toBe(true);
|
||||
expect(evaluateAssertion(res, as({ target: "body", locator: "$.data.name", op: "exists" })).passed).toBe(true);
|
||||
expect(evaluateAssertion(res, as({ target: "body", locator: "$.none", op: "not_exists" })).passed).toBe(true);
|
||||
});
|
||||
|
||||
it("非 JSON 响应的 jsonpath 断言失败但不抛错", () => {
|
||||
const plain = { ...res, body: "hello world" };
|
||||
const r = evaluateAssertion(plain, as({ target: "body", locator: "$.a", op: "jsonpath_eq", expected: "1" }));
|
||||
expect(r.passed).toBe(false);
|
||||
expect(r.actual).toContain("非 JSON");
|
||||
});
|
||||
|
||||
it("断言文案", () => {
|
||||
expect(describeAssertion(as({ target: "status", op: "eq", expected: "200" }))).toContain("状态码");
|
||||
expect(describeAssertion(as({ target: "body", locator: "$.a", op: "jsonpath_contains", expected: "x" }))).toContain("$.a");
|
||||
});
|
||||
});
|
||||
|
||||
describe("变量提取", () => {
|
||||
it("提取到平面对象", () => {
|
||||
const out = extractVariables(res, [
|
||||
{ id: "e1", jsonpath: "$.data.id", varName: "petId" },
|
||||
{ id: "e2", jsonpath: "$.data.name", varName: "petName" },
|
||||
{ id: "e3", jsonpath: "$.none", varName: "missing" },
|
||||
{ id: "e4", jsonpath: "", varName: "bad" },
|
||||
]);
|
||||
expect(out).toEqual({ petId: "42", petName: "opsx" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("默认断言与统计", () => {
|
||||
it("suggestAssertions 至少含状态码与耗时", () => {
|
||||
const list = suggestAssertions({ method: "GET", url: "https://x" } as never);
|
||||
expect(list.length).toBeGreaterThanOrEqual(2);
|
||||
expect(list[0].target).toBe("status");
|
||||
});
|
||||
it("runStats", () => {
|
||||
expect(runStats({ stepResults: [{ ok: true }, { ok: false }, { ok: true }] })).toEqual({ ok: 2, fail: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("报告导出", () => {
|
||||
it("Markdown 结构", () => {
|
||||
const md = exportRunMarkdown({
|
||||
caseName: "下单流程",
|
||||
at: Date.now(),
|
||||
trigger: "manual",
|
||||
totalMs: 500,
|
||||
passed: false,
|
||||
stepResults: [
|
||||
{
|
||||
name: "创建订单",
|
||||
method: "POST",
|
||||
url: "https://api.test/orders",
|
||||
ok: true,
|
||||
httpStatus: 200,
|
||||
durationMs: 200,
|
||||
error: null,
|
||||
assertionResults: [{ label: "状态码 = 200", passed: true, actual: "200", expected: "200" }],
|
||||
},
|
||||
{
|
||||
name: "查询订单",
|
||||
method: "GET",
|
||||
url: "https://api.test/orders/1",
|
||||
ok: false,
|
||||
httpStatus: 404,
|
||||
durationMs: 300,
|
||||
error: null,
|
||||
assertionResults: [{ label: "状态码 = 200", passed: false, actual: "404", expected: "200" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(md).toContain("# 测试报告 · 下单流程");
|
||||
expect(md).toContain("1/2 步骤通过");
|
||||
expect(md).toContain("POST https://api.test/orders");
|
||||
expect(md).toContain("| 状态码 = 200 | 200 | 404 | ❌ |");
|
||||
});
|
||||
});
|
||||
|
||||
describe("用例构造", () => {
|
||||
it("newTestCase 默认值", () => {
|
||||
const tc = newTestCase("冒烟");
|
||||
expect(tc.steps).toEqual([]);
|
||||
expect(tc.continueOnFailure).toBe(false);
|
||||
expect(tc.id).toMatch(/^tc-/);
|
||||
});
|
||||
});
|
||||
|
||||
import { cronMatches, isValidCron, scheduleIsDue } from "./cron";
|
||||
|
||||
describe("exportRunJson", () => {
|
||||
it("emits opsxapi schema", () => {
|
||||
const json = JSON.parse(
|
||||
exportRunJson({
|
||||
caseName: "demo",
|
||||
at: 1,
|
||||
trigger: "manual",
|
||||
totalMs: 10,
|
||||
passed: true,
|
||||
stepResults: [],
|
||||
}),
|
||||
);
|
||||
expect(json.schema).toBe("opsxapi.test-run.v1");
|
||||
expect(json.caseName).toBe("demo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cron", () => {
|
||||
it("validates 5-field expressions", () => {
|
||||
expect(isValidCron("*/5 * * * *")).toBe(true);
|
||||
expect(isValidCron("* * *")).toBe(false);
|
||||
expect(isValidCron("*/0 * * * *")).toBe(false);
|
||||
expect(isValidCron("61 * * * *")).toBe(false);
|
||||
expect(isValidCron("10-2 * * * *")).toBe(false);
|
||||
expect(cronMatches("*/0 * * * *", new Date(2026, 0, 1, 12, 10))).toBe(false);
|
||||
});
|
||||
|
||||
it("matches */5 minutes", () => {
|
||||
const d = new Date(2026, 0, 1, 12, 10, 0); // min=10
|
||||
expect(cronMatches("*/5 * * * *", d)).toBe(true);
|
||||
expect(cronMatches("*/5 * * * *", new Date(2026, 0, 1, 12, 11, 0))).toBe(false);
|
||||
});
|
||||
|
||||
it("scheduleIsDue prefers cron over interval", () => {
|
||||
const d = new Date(2026, 0, 1, 12, 0, 0);
|
||||
expect(
|
||||
scheduleIsDue({ intervalMinutes: 999, cron: "0 * * * *", lastRunAt: null }, d),
|
||||
).toBe(true);
|
||||
expect(
|
||||
scheduleIsDue(
|
||||
{ intervalMinutes: 1, cron: "0 * * * *", lastRunAt: d.getTime() },
|
||||
d,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user