180 lines
9.7 KiB
JavaScript
180 lines
9.7 KiB
JavaScript
import fs from "node:fs";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import { afterEach, describe, expect, it } from "vitest";
|
||
import { buildSuite, bundleRoot } from "./app-system-test-flows.mjs";
|
||
import { installBundle, upgradeBundle, makeReport, caseResults } from "./app-system-tests.mjs";
|
||
import { applyCatalogOverrides } from "./catalog-overrides.mjs";
|
||
import { hasCapability } from "../src/main/capabilities/index.ts";
|
||
import { runFlow } from "../src/main/engine/runtime.ts";
|
||
import { validateFlow } from "../src/renderer/src/validation/flowValidation.ts";
|
||
|
||
const containers = new Set(["logic_control_try_catch", "logic_control_if_else"]);
|
||
const catalog = JSON.parse(fs.readFileSync("resources/catalog.json", "utf8"));
|
||
for (const c of catalog.components) c.implemented = containers.has(c.id) || hasCapability(c.id) || hasCapability(c.functionName);
|
||
const componentMap = new Map(catalog.components.map(c => [c.id, c]));
|
||
const temporary = [];
|
||
afterEach(() => { for (const dir of temporary.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); });
|
||
|
||
async function execute(number, change) {
|
||
const group = buildSuite()[number - 1];
|
||
change?.(group.flow);
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rpa-appsystem-unit-")); temporary.push(dir);
|
||
const logs = [];
|
||
const result = await runFlow(group.flow, {
|
||
runId: `unit-${group.flow.id}`, evidenceDir: dir, signal: new AbortController().signal,
|
||
emit: () => {}, log: (message) => logs.push(message), componentOf: node => componentMap.get(node.componentId)
|
||
});
|
||
return { group, result, logs };
|
||
}
|
||
|
||
describe("应用与系统测试流程包", () => {
|
||
it("覆盖真实注册的全部 50 项,13 组无重复归属,不覆盖待接入组件", () => {
|
||
const suite = buildSuite();
|
||
expect(suite).toHaveLength(13);
|
||
const covered = suite.flatMap(group => group.entry.components).sort();
|
||
const executable = catalog.components.filter(c => c.group === "app" && c.implemented).map(c => c.id).sort();
|
||
expect(covered).toHaveLength(50);
|
||
expect(new Set(covered).size).toBe(50);
|
||
expect(covered).toEqual(executable);
|
||
for (const { flow, entry } of suite) {
|
||
expect(new Set(flow.nodes.map(node => node.id)).size).toBe(flow.nodes.length);
|
||
expect(new Set(entry.cases.map(c => c.componentId))).toEqual(new Set(entry.components));
|
||
const reached = new Set([flow.nodes[0].id]);
|
||
for (let n = 0; n < flow.nodes.length; n++) {
|
||
for (const edge of flow.edges) if (reached.has(edge.source)) reached.add(edge.target);
|
||
for (const node of flow.nodes) if (reached.has(node.parentId)) reached.add(node.id);
|
||
}
|
||
expect(reached.size, flow.name).toBe(flow.nodes.length);
|
||
expect(entry.cases.every(c => flow.nodes.some(n => n.id === c.targetNodeId && n.componentId === c.componentId))).toBe(true);
|
||
expect(JSON.parse(fs.readFileSync(path.join(bundleRoot, entry.file), "utf8"))).toEqual(flow);
|
||
}
|
||
});
|
||
|
||
it("目录修正可重复应用,前端类型选项都有实际判断与转换能力", async () => {
|
||
expect(applyCatalogOverrides(structuredClone(catalog))).toEqual(catalog);
|
||
for (const id of ["Basic_JudgeDataType", "Basic_DataTypeConversion"]) {
|
||
const component = componentMap.get(id);
|
||
const options = component.fields.find(field => field.key === "into_type").options;
|
||
expect(options.length).toBeGreaterThanOrEqual(4);
|
||
for (const option of options) {
|
||
const output = await import("../src/main/engine/registry.ts").then(({ getHandler }) => getHandler(id)({}, { id: "type-test", componentId: id }, { val: 42, into_type: option.value }, component));
|
||
if (id === "Basic_JudgeDataType") expect(typeof output).toBe("boolean");
|
||
}
|
||
}
|
||
const wake = componentMap.get("ScreenControl_ScreenUnlock");
|
||
expect(wake.name).toContain("非登录解锁");
|
||
expect(wake.fields).toHaveLength(0);
|
||
expect(componentMap.get("System_ScreenSaverShowV1").fields).toHaveLength(0);
|
||
});
|
||
|
||
it("只保留真实参数校验阻塞,不出现断链、未知变量或未接入节点", () => {
|
||
const blocked = [];
|
||
for (const { flow } of buildSuite()) {
|
||
const issues = validateFlow(flow, catalog);
|
||
expect(issues.every(issue => issue.code === "required"), flow.name).toBe(true);
|
||
if (issues.length) blocked.push(flow.id);
|
||
}
|
||
expect(blocked).toEqual(["test_appsystem_08", "test_appsystem_12"]);
|
||
const pidOnly = buildSuite()[0].flow;
|
||
const killV1 = pidOnly.nodes.find(n => n.componentId === "App_KillV1");
|
||
killV1.values.p_name = "";
|
||
expect(validateFlow(pidOnly, catalog).filter(issue => issue.nodeId === killV1.id)).toEqual([]);
|
||
});
|
||
|
||
it("真实执行转换流程,显式类型选项下完整核对全部用例", async () => {
|
||
const { group, result, logs } = await execute(2);
|
||
expect(result.status).toBe("succeeded");
|
||
expect(result.variables.failures).toBe(0);
|
||
expect(result.variables.blocked).toBe(0);
|
||
expect(group.entry.cases.map(c => result.variables[c.resultVariable][2])).toEqual(Array(11).fill("PASS"));
|
||
expect(logs.filter(log => log.startsWith("用例结果:"))).toHaveLength(11);
|
||
});
|
||
|
||
it("注入错误返回类型时断言会失败,且前面的用例继续留有证据", async () => {
|
||
// 只改变内存中的测试副本,不把现有组件缺陷固化成测试的预期行为。
|
||
const { result } = await execute(3, flow => {
|
||
const target = flow.nodes.find(n => n.componentId === "Basic_JudgeDataType");
|
||
target.componentId = "Basic_SetVariable";
|
||
target.values = { value: "number" };
|
||
});
|
||
expect(result.variables.failures).toBe(1);
|
||
expect(result.variables.case_12[2]).toBe("FAIL");
|
||
expect(result.variables.case_12[5]).toBe("number");
|
||
expect(result.variables.case_11[2]).toBe("PASS");
|
||
});
|
||
|
||
it("变量、深复制、配置、流程信息和计数能够真实通过", async () => {
|
||
const { result } = await execute(4);
|
||
expect(result.status).toBe("succeeded");
|
||
expect(result.variables.failures).toBe(0);
|
||
expect(result.variables.case_10[5]).toBe(14);
|
||
});
|
||
|
||
it("手动任务默认保护有效,不调用输入法、远程连接或断连操作", async () => {
|
||
for (const number of [9, 13]) {
|
||
const { group, result } = await execute(number);
|
||
expect(result.status).toBe("failed");
|
||
expect(result.variables.pending).toBe(group.entry.cases.length);
|
||
for (const c of group.entry.cases) {
|
||
expect(result.nodes.some(n => n.nodeId === c.targetNodeId)).toBe(false);
|
||
expect(result.variables[c.resultVariable][2]).toBe("MANUAL");
|
||
}
|
||
}
|
||
});
|
||
|
||
it("字符串 false 或 true 不会误开启手动保护", async () => {
|
||
for (const value of ["false", "true"]) {
|
||
const { group, result } = await execute(13, flow => {
|
||
flow.nodes.find(n => n.options.outputVar === "manualEnabled").values.value = value;
|
||
});
|
||
for (const c of group.entry.cases) expect(result.nodes.some(n => n.nodeId === c.targetNodeId)).toBe(false);
|
||
}
|
||
});
|
||
|
||
it("只新增 13 个任务,重复安装保留用户已有修改且不改无关文件", () => {
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rpa-appsystem-install-")); temporary.push(dir);
|
||
const first = installBundle(dir);
|
||
expect(first.installed).toHaveLength(13);
|
||
const file = path.join(dir, "flows/test_appsystem_01.json");
|
||
fs.writeFileSync(file, "用户自行编辑的内容");
|
||
fs.writeFileSync(path.join(dir, "flows/other.json"), "其他任务");
|
||
const second = installBundle(dir);
|
||
expect(second.installed).toHaveLength(0);
|
||
expect(second.skipped).toHaveLength(13);
|
||
expect(fs.readFileSync(file, "utf8")).toBe("用户自行编辑的内容");
|
||
expect(fs.readFileSync(path.join(dir, "flows/other.json"), "utf8")).toBe("其他任务");
|
||
});
|
||
|
||
it("升级仅覆盖匹配旧模板的测试任务,并保存原文件备份", () => {
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rpa-appsystem-upgrade-")); temporary.push(dir);
|
||
installBundle(dir);
|
||
const baseline = path.join(dir, "baseline"), backup = path.join(dir, "backup");
|
||
fs.mkdirSync(baseline);
|
||
for (const { entry, flow } of buildSuite()) {
|
||
const old = { ...flow, description: "旧测试模板" };
|
||
fs.writeFileSync(path.join(baseline, entry.file), JSON.stringify(old));
|
||
fs.writeFileSync(path.join(dir, "flows", entry.file), JSON.stringify(old));
|
||
}
|
||
const changed = path.join(dir, "flows/test_appsystem_01.json");
|
||
const userFlow = JSON.parse(fs.readFileSync(changed, "utf8")); userFlow.description = "用户已修改";
|
||
fs.writeFileSync(changed, JSON.stringify(userFlow));
|
||
const result = upgradeBundle(dir, baseline, backup);
|
||
expect(result.upgraded).toHaveLength(12);
|
||
expect(result.skipped).toContainEqual({ id: "test_appsystem_01", reason: "用户已修改,保留现有版本" });
|
||
expect(fs.readFileSync(changed, "utf8")).toBe(JSON.stringify(userFlow));
|
||
expect(JSON.parse(fs.readFileSync(path.join(backup, "test_appsystem_02.json"), "utf8")).description).toBe("旧测试模板");
|
||
});
|
||
|
||
it("报告按最差用例聚合,不把任务成功或缺失的记录冒充组件通过", () => {
|
||
const group = buildSuite()[0];
|
||
const variables = Object.fromEntries(group.entry.cases.map(c => [c.resultVariable, [c.id, c.componentId, "PASS", c.input, c.expected, 42]]));
|
||
variables.case_8[2] = "FAIL";
|
||
const report = makeReport({ runs: [{ flowId: group.flow.id, status: "succeeded", result: { variables } }] });
|
||
expect(report.components.find(c => c.id === "App_GetPid").status).toBe("FAIL");
|
||
expect(report.components.find(c => c.id === "Basic_ConvertInt").status).toBe("UNRUN");
|
||
expect(report.components.find(c => c.id === "System_PlaySound").status).toBe("MANUAL");
|
||
expect(caseResults(group.entry, { status: "BLOCKED" }).every(c => c.status === "BLOCKED")).toBe(true);
|
||
});
|
||
});
|