PRA-only-demo/scripts/app-system-tests.test.mjs
yu 5e8fc9686f fix(catalog): 修复重生成遗留的三类数据回归(迁移阶段1-T4)
- 昨日重生成用的对面旧版源数据缺 select 选项:从重生成前快照收割 782 个
  select/radio 字段的 options 进 overrides(含默认值落在选项内的校验)
- 移除实现不读的假字段:Excel_Cs_InsertColumnV3/InsertRowV3 的 direction
- 默认值修复:Time 系列 offset_unit_num 改数字 0(实现按 num 读取)、
  Mail_DoGetMailMessage_v2:attachment 恢复旧选项、Window_SizeWindow_v1:window_size
  恢复 text widget、Mobile_TouchSlideV1 无实现清空默认值
- app-system-tests 期望更新:12 号锁屏流程因假必填清理不再阻塞(有意变更)
- catalog-properties 13 项 + app-system 全过
2026-11-06 10:53:56 +08:00

182 lines
9.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, runFlow } from "@rpa/core";
import "../src/main/capabilities/index.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"));
const isRegistered = (component) => containers.has(component.id) || hasCapability(component.id) || hasCapability(component.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" && isRegistered(c)).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("@rpa/core").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);
}
// 12 号(锁屏/解锁)曾经因 Screenlock 的 6 个假必填而阻塞;
// 2026-11-05 参数审计确认实现不读这些字段,已改选填并清空,阻塞随之解除(有意变更)
expect(blocked).toEqual(["test_appsystem_08"]);
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);
});
});