PRA-only-demo/scripts/app-system-tests.test.mjs
yu caa8e19332 阶段1-rpa-core建包: engine+31个纯Node能力迁入packages/rpa-core, 领域类型单一源化, createEngine工厂收拢模块级状态
- engine/(registry/types/runtime/expression)物理迁入包, 解耦shared/types(域类型归包, 宿主type-only re-export)
- 31个纯Node能力+27个测试迁入包; 宿主留守Electron/浏览器/AI依赖能力, index.ts经@rpa/core/capabilities拉起
- 新增createEngine()/defaultEngine: excel缓存/Excel句柄/db连接/FTP客户端/OCR实例/拟真设置挂引擎实例
- remaining-core子流程依赖改为host-bridge注入, 包内不再import宿主flows
- 宿主main/index.ts启动时注入子流程源; electron-vite/vitest/tsconfig配@rpa/core源码别名
- 验证: typecheck绿, 快照909个id diff为零, 332测试过(2个桌面冒烟因会话无交互SetForegroundWindow被拒, 环境阻塞)
2026-11-04 12:36:03 +08:00

180 lines
9.6 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);
}
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);
});
});