280 lines
24 KiB
JavaScript
280 lines
24 KiB
JavaScript
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
export const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||
export const bundleRoot = path.join(projectRoot, "resources/test-flows/app-system");
|
||
const ref = (expression) => `\${${expression}}`;
|
||
const literal = (value) => JSON.stringify(value);
|
||
const epoch = "2026-10-30T00:00:00.000Z";
|
||
|
||
// 只生成现有引擎支持的节点,不注册测试专用执行器。
|
||
export class TestFlow {
|
||
constructor(number, title, components, manual = false) {
|
||
this.number = String(number).padStart(2, "0");
|
||
this.flow = {
|
||
schemaVersion: 1, id: `test_appsystem_${this.number}`,
|
||
name: `组件测试 ${this.number} · ${title}${manual ? "【手动】" : ""}`,
|
||
description: manual ? "手动测试:先阅读测试说明。默认不开启执行开关,不能把组件返回成功当作功能通过。" : "查看运行日志中的用例结果;有失败时任务最终显示失败,其他用例仍继续。",
|
||
enabled: true, trigger: { type: "manual", label: "手动运行" }, variables: [], nodes: [], edges: [],
|
||
meta: { createdAt: epoch, updatedAt: epoch, appVersion: "0.1.0" }
|
||
};
|
||
this.entry = { id: this.flow.id, file: `${this.flow.id}.json`, name: this.flow.name, manual, components, cases: [] };
|
||
this.seq = 0;
|
||
this.top = [];
|
||
this.add("Trigger.Manual", { entryName: "开始组件测试" }, undefined, "人工发起");
|
||
this.set("failures", 0, "初始化失败计数");
|
||
this.set("pending", 0, "初始化待确认计数");
|
||
this.set("blocked", 0, "初始化阻塞计数");
|
||
if (manual) this.set("manualEnabled", false, "手动执行开关:确认环境后才可改为 true");
|
||
}
|
||
|
||
add(componentId, values = {}, outputVar, label, parent) {
|
||
const id = `t${this.number}_${++this.seq}`;
|
||
const node = { id, componentId, label: label || componentId, position: { x: 220, y: 80 + this.top.length * 180 }, values,
|
||
options: { timeoutMs: 15000, retryCount: 0, logException: false, ...(outputVar ? { outputVar } : {}) } };
|
||
if (parent) { node.parentId = parent.id; node.slot = parent.slot; }
|
||
else {
|
||
if (this.top.length) this.flow.edges.push({ id: `e_${id}`, source: this.top.at(-1), target: id });
|
||
this.top.push(id);
|
||
}
|
||
this.flow.nodes.push(node);
|
||
return node;
|
||
}
|
||
|
||
set(name, value, label, parent) { return this.add("Basic_SetVariable", { value }, name, label, parent); }
|
||
log(message, parent) { return this.add("Log_Info", { msg: message }, undefined, "输出测试结果", parent); }
|
||
command(script, outputVar, label, parent) { return this.add("System_ExecuteCommand", { action: script }, outputVar, label, parent); }
|
||
|
||
case(componentId, values, expected, predicate, options = {}) {
|
||
const index = this.entry.cases.length + 1;
|
||
const caseId = `${this.number}-${String(index).padStart(2, "0")}`;
|
||
const out = options.output || `actual_${index}`;
|
||
const result = `case_${index}`;
|
||
const container = this.add("logic_control_try_catch", {}, undefined, `${caseId} · ${options.title || componentId}`);
|
||
const run = { id: container.id, slot: "try_fn" };
|
||
const caught = { id: container.id, slot: "except_fn" };
|
||
let work = run;
|
||
if (this.entry.manual) {
|
||
const gate = this.add("logic_control_if_else", { condition: "${manualEnabled === true}" }, undefined, "手动执行保护", run);
|
||
work = { id: gate.id, slot: "statement_if" };
|
||
this.add("ExceptionUtil_Throws", { msg: "待人工确认:执行开关未开启" }, undefined, "默认不执行系统状态操作", { id: gate.id, slot: "statement_else" });
|
||
}
|
||
for (const step of options.before || []) this.add(step.componentId, step.values, step.output, step.label, work);
|
||
const target = this.add(componentId, values, out, options.title, work);
|
||
if (options.timeoutMs) target.options.timeoutMs = options.timeoutMs;
|
||
for (const step of options.after || []) this.add(step.componentId, step.values, step.output, step.label, work);
|
||
const actual = options.actual || out;
|
||
const manualResult = options.observe === true;
|
||
const status = options.blocked ? '"BLOCKED"' : manualResult ? '"MANUAL"' : `(${predicate}) ? "PASS" : "FAIL"`;
|
||
// 数组是表达式引擎原生支持的结构;对象交给 json/parseJson 处理。
|
||
const prefix = [caseId, componentId];
|
||
const input = options.input || JSON.stringify(values);
|
||
this.set(result, ref(`[${literal(prefix[0])}, ${literal(prefix[1])}, ${status}, ${literal(input)}, ${literal(expected)}, ${actual}]`), "记录实际值与判定", work);
|
||
this.set("failures", ref(`failures + (${result}[2] === "FAIL" ? 1 : 0)`), "累计失败数", work);
|
||
this.set("pending", ref(`pending + (${result}[2] === "MANUAL" ? 1 : 0)`), "累计待人工确认数", work);
|
||
if (options.blocked) this.set("blocked", ref("blocked + 1"), options.blocked, work);
|
||
const errorStatus = this.entry.manual ? 'manualEnabled === true ? "FAIL" : "MANUAL"' : '"FAIL"';
|
||
this.set(result, ref(`[${literal(caseId)}, ${literal(componentId)}, ${errorStatus}, ${literal(input)}, ${literal(expected)}, error.reason]`), "记录异常结果", caught);
|
||
this.set("failures", ref(`failures + (${result}[2] === "FAIL" ? 1 : 0)`), "异常累计失败数", caught);
|
||
this.set("pending", ref(`pending + (${result}[2] === "MANUAL" ? 1 : 0)`), "异常累计待确认数", caught);
|
||
this.log(`用例结果:\${json(${result})}`);
|
||
this.entry.cases.push({ id: caseId, componentId, targetNodeId: target.id, resultVariable: result, input, expected, observe: manualResult, ...(options.blocked ? { blockedReason: options.blocked } : {}) });
|
||
return out;
|
||
}
|
||
|
||
finish() {
|
||
this.log("本组汇总:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。");
|
||
const gate = this.add("logic_control_if_else", { condition: "${failures > 0 || pending > 0 || blocked > 0}" }, undefined, "汇总判定");
|
||
this.add("ExceptionUtil_Throws", { msg: "组件测试未全部通过:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。请查看日志。" }, undefined, "如实标记未通过", { id: gate.id, slot: "statement_if" });
|
||
this.log("本组全部通过。", { id: gate.id, slot: "statement_else" });
|
||
let y = 80;
|
||
for (const id of this.top) {
|
||
const node = this.flow.nodes.find(item => item.id === id);
|
||
node.position.y = y;
|
||
const children = this.flow.nodes.filter(item => item.parentId === id);
|
||
y += children.length ? 320 + children.length * 46 : 142;
|
||
}
|
||
return { flow: this.flow, entry: this.entry };
|
||
}
|
||
}
|
||
|
||
export function buildSuite() {
|
||
const groups = [];
|
||
groups.push(processFlow());
|
||
const conversion = new TestFlow(2, "数据转换", ["Basic_StringToNum", "Basic_ConvertNumber", "Basic_ConvertInt", "Basic_ConvertBool", "Basic_ConvertString", "Basic_DataTypeConversion", "Basic_Dodecryption"]);
|
||
conversion.case("Basic_StringToNum", { val: "123.5" }, "数字 123.5", "actual_1 === 123.5");
|
||
conversion.case("Basic_ConvertNumber", { val: "-42.25" }, "数字 -42.25", "actual_2 === -42.25");
|
||
conversion.case("Basic_ConvertInt", { val: -3.9 }, "整数 -3(向零截断)", "actual_3 === -3");
|
||
conversion.case("Basic_ConvertBool", { val: "true" }, "布尔 true", "actual_4 === true");
|
||
conversion.case("Basic_ConvertBool", { val: "false" }, "布尔 false", "actual_5 === false");
|
||
conversion.case("Basic_ConvertString", { val: 42 }, '文本 "42"', 'actual_6 === "42"');
|
||
conversion.case("Basic_DataTypeConversion", { val: "42", into_type: "number" }, "数值 42,严格区分文本和数值", "actual_7 === 42");
|
||
conversion.case("Basic_DataTypeConversion", { val: "-3.9", into_type: "int" }, "整数 -3", "actual_8 === -3");
|
||
conversion.case("Basic_DataTypeConversion", { val: "false", into_type: "bool" }, "布尔 false", "actual_9 === false");
|
||
conversion.case("Basic_DataTypeConversion", { val: 42, into_type: "string" }, '文本 "42"', 'actual_10 === "42"');
|
||
conversion.case("Basic_Dodecryption", { words: "UlBBLURvZGVjcnlwdGlvbg==" }, 'Base64 解码 "RPA-Dodecryption"', 'actual_11 === "RPA-Dodecryption"');
|
||
groups.push(conversion.finish());
|
||
const judge = new TestFlow(3, "数据判断", ["Basic_IsDictionary", "Basic_IsNumber", "Basic_IsNull", "Basic_IsArray", "Basic_Type", "Basic_JudgeDataType"]);
|
||
for (const [id, val, expected] of [
|
||
["Basic_IsDictionary", { name: "测试" }, true], ["Basic_IsDictionary", [], false],
|
||
["Basic_IsNumber", "12.5", true], ["Basic_IsNumber", "中文", false],
|
||
["Basic_IsNull", null, true], ["Basic_IsNull", "", true], ["Basic_IsNull", 0, false],
|
||
["Basic_IsArray", [], true], ["Basic_IsArray", "[]", false],
|
||
["Basic_Type", [1, 2], "list"], ["Basic_Type", null, "null"]
|
||
]) {
|
||
const out = `actual_${judge.entry.cases.length + 1}`;
|
||
judge.case(id, { val }, JSON.stringify(expected), `${out} === ${literal(expected)}`);
|
||
}
|
||
judge.case("Basic_JudgeDataType", { val: 42, into_type: "number" }, "数字 42 属于数值类型,返回布尔 true", "actual_12 === true");
|
||
groups.push(judge.finish());
|
||
|
||
const vars = new TestFlow(4, "变量与配置", ["Basic_SetVariable", "Basic_SetVariable_V1", "Basic_Clone", "Basic_GetProcessInfo", "Basic_DebugOutput", "CustomConfig_GetConfig", "Basic_BusinessOrderCount"]);
|
||
vars.case("Basic_SetVariable", { value: "中文变量往返" }, "原值及输出变量均保持中文文本", 'actual_1 === "中文变量往返"');
|
||
vars.case("Basic_SetVariable_V1", { variableType: "rpa_int", value: "42" }, "数字 42", "actual_2 === 42");
|
||
vars.case("Basic_SetVariable_V1", { variableType: "rpa_list", value: "[1,2]" }, "数组 [1,2]", 'json(actual_3) === "[1,2]"');
|
||
vars.set("original", { title: "中文", nested: { n: 1 } }, "初始化复制源");
|
||
vars.case("Basic_Clone", { val: "${original}" }, "深复制内容相同,外层和嵌套对象均不共用引用", "json(actual_4) === json(original) && actual_4 !== original && actual_4.nested !== original.nested");
|
||
vars.case("Basic_GetProcessInfo", {}, "当前流程 ID、名称、运行 ID 和节点数一致", `actual_5.id === ${literal(vars.flow.id)} && actual_5.name === ${literal(vars.flow.name)} && len(actual_5.runId) > 0 && actual_5.nodeCount > 0`);
|
||
vars.case("Basic_DebugOutput", { msg: "中文日志测试:${actual_2}" }, '日志及返回值为“中文日志测试:42”', 'actual_6 === "中文日志测试:42"');
|
||
vars.case("CustomConfig_GetConfig", { jsonlist: '[{"name":"timeout","value":30}]', configName: "timeout" }, "数字 30", "actual_7 === 30");
|
||
vars.case("CustomConfig_GetConfig", { jsonlist: "[]", configName: "missing" }, "未命中返回 null", "actual_8 === null");
|
||
vars.case("Basic_BusinessOrderCount", { start_count: 10, count: 2 }, "首次计数 12", "actual_9 === 12");
|
||
vars.case("Basic_BusinessOrderCount", { start_count: 10, count: 2 }, "同一流程再次计数 14", "actual_10 === 14");
|
||
groups.push(vars.finish());
|
||
|
||
groups.push(timingFlow());
|
||
const system = new TestFlow(6, "系统路径与环境变量", ["System_GetTempPath", "System_GetHomePath", "System_GetSystemPath", "System_GetEnv", "System_SetEnv"]);
|
||
system.command("@{temp=[IO.Path]::GetTempPath().TrimEnd('\\'); home=[Environment]::GetFolderPath('UserProfile'); desktop=[Environment]::GetFolderPath('Desktop')} | ConvertTo-Json -Compress", "pathOracle", "独立读取 Windows 系统路径");
|
||
system.set("paths", "${parseJson(pathOracle)}", "解析独立预期路径");
|
||
system.case("System_GetTempPath", {}, "与系统临时目录一致(忽略尾部分隔符)", 'actual_1 === paths.temp || actual_1 === paths.temp + "\\\\"');
|
||
system.case("System_GetHomePath", {}, "与 Windows 用户文件夹一致", "actual_2 === paths.home");
|
||
system.case("System_GetSystemPath", { path_name: "desktop" }, "与 Windows 实际桌面目录一致(包含重定向)", "actual_3 === paths.desktop");
|
||
system.case("System_GetEnv", { name: "RPA_APP_SYSTEM_TEST_VALUE" }, "读取测试专用变量的原始值,作为恢复依据", "true", { output: "originalEnv" });
|
||
system.case("System_SetEnv", { name: "RPA_APP_SYSTEM_TEST_VALUE", value: "中文-测试-42" }, "当前应用进程内设置成功", 'actual_5 === "中文-测试-42"');
|
||
system.case("System_GetEnv", { name: "RPA_APP_SYSTEM_TEST_VALUE" }, "能读回中文-测试-42", 'actual_6 === "中文-测试-42"');
|
||
system.case("System_SetEnv", { name: "RPA_APP_SYSTEM_TEST_VALUE", value: "${originalEnv}" }, "恢复测试前读取值", "actual_7 === originalEnv");
|
||
groups.push(system.finish());
|
||
groups.push(commandFlow());
|
||
groups.push(...manualFlows());
|
||
return groups;
|
||
}
|
||
|
||
function processFlow() {
|
||
const f = new TestFlow(1, "应用与 PID", ["App_Run", "App_GetPid", "App_GetPids", "App_Kill", "App_KillV1"]);
|
||
const csharp = "using System; using System.Threading; public static class RpaFixture { public static void Main() { Thread.Sleep(90000); } }";
|
||
const setup = `$ErrorActionPreference='Stop'; $tag=[Guid]::NewGuid().ToString('N'); $dir=Join-Path ([IO.Path]::GetTempPath()) ('RPA-AppSystem-Tests\\'+$tag); [IO.Directory]::CreateDirectory($dir) | Out-Null; $name='rpatest_'+$tag.Substring(0,12); $exe=Join-Path $dir ($name+'.exe'); Add-Type -TypeDefinition '${csharp}' -OutputAssembly $exe -OutputType ConsoleApplication; @{folder=$dir;exe=$exe;name=$name} | ConvertTo-Json -Compress`;
|
||
f.command(setup, "fixtureJson", "准备唯一测试程序(90 秒自动退出)");
|
||
f.set("fixture", "${parseJson(fixtureJson)}", "读取夹具路径与专用进程名");
|
||
f.case("App_Run", { executeable: "${fixture.exe}" }, "第一个测试进程 PID 大于 0", "pid1 > 0", { output: "pid1" });
|
||
f.case("App_Run", { executeable: "${fixture.exe}" }, "第二个 PID 大于 0,且与第一个不同", "pid2 > 0 && pid2 !== pid1", { output: "pid2" });
|
||
f.case("App_GetPid", { pattern: "${fixture.name}.exe" }, "返回本次创建的任一 PID", "actual_3 === pid1 || actual_3 === pid2");
|
||
f.case("App_GetPids", { pattern: "${fixture.name}.exe" }, "列表恰含本次创建的两个 PID", "len(actual_4) === 2 && ((actual_4[0] === pid1 && actual_4[1] === pid2) || (actual_4[0] === pid2 && actual_4[1] === pid1))");
|
||
f.case("App_Kill", { pid: "${pid1}" }, "结束第一个夹具 PID,第二个仍在运行", "parseJson(alive1) === false && parseJson(alive2) === true", {
|
||
after: [processProbe("pid1", "alive1"), processProbe("pid2", "alive2")], actual: "[actual_5, parseJson(alive1), parseJson(alive2)]"
|
||
});
|
||
f.case("App_KillV1", { close_type: "id", pid: "${pid2}", kill_all: "0" }, "只填写 PID 即可结束第二个夹具进程", "parseJson(aliveAfterV1) === false", {
|
||
after: [processProbe("pid2", "aliveAfterV1")], actual: "[actual_6, parseJson(aliveAfterV1)]"
|
||
});
|
||
f.case("App_GetPids", { pattern: "${fixture.name}.exe" }, "所有夹具结束后应返回空数组 []", "len(actual_7) === 0");
|
||
f.case("App_GetPid", { pattern: "${fixture.name}.exe" }, "无匹配时应返回 null", "actual_8 === null");
|
||
const cleanup = "$ErrorActionPreference='Stop'; $fixture='${fixture.exe}'; if ((Split-Path $fixture -Leaf) -notmatch '^rpatest_[0-9a-f]{12}\\.exe$') { throw '拒绝清理非测试进程' }; Get-Process -Name '${fixture.name}' -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $fixture } | Stop-Process -Force; '测试进程清理完成'";
|
||
f.command(cleanup, "cleanupResult", "收尾:仅清理本次专用测试进程");
|
||
return f.finish();
|
||
}
|
||
|
||
function processProbe(pid, output) {
|
||
return { componentId: "System_ExecuteCommand", values: { action: `if (Get-Process -Id \${${pid}} -ErrorAction SilentlyContinue) { 'true' } else { 'false' }` }, output, label: "独立核对 PID 是否存活" };
|
||
}
|
||
|
||
function timingFlow() {
|
||
const f = new TestFlow(5, "等待与随机数", ["Basic_Delay_V2", "Basic_Delay_V1", "Basic_Delay", "Basic_RandomInt", "Basic_Rnd"]);
|
||
for (const [id, values, low, high] of [
|
||
["Basic_Delay_V2", { is_random: "0", second: "0.15", min_second: "0.1", max_second: "0.2" }, 120, 1800],
|
||
["Basic_Delay_V2", { is_random: "1", second: "0.15", min_second: "0.1", max_second: "0.2" }, 80, 1800],
|
||
["Basic_Delay_V1", { second: "0.15" }, 120, 1800],
|
||
["Basic_Delay", { mileSecond: "3000" }, 2800, 6500]
|
||
]) {
|
||
const step = (output) => ({ componentId: "Time_GetDatetimeObj", values: { date_input: "${now()}" }, output, label: "独立采集毫秒时间" });
|
||
f.case(id, values, `实际等待耗时 ${low}~${high} ms`, `endTime.timestamp - startTime.timestamp >= ${low} && endTime.timestamp - startTime.timestamp <= ${high}`, { before: [step("startTime")], after: [step("endTime")], actual: "endTime.timestamp - startTime.timestamp" });
|
||
}
|
||
f.case("Basic_RandomInt", { min: 7, max: 7 }, "最小值等于最大值时返回整数 7", "actual_5 === 7");
|
||
f.case("Basic_RandomInt", { min: -2, max: 2 }, "返回 -2~2 的整数", "actual_6 >= -2 && actual_6 <= 2 && actual_6 % 1 === 0");
|
||
f.case("Basic_Rnd", {}, "随机小数满足 0 ≤ x < 1", "actual_7 >= 0 && actual_7 < 1");
|
||
return f.finish();
|
||
}
|
||
|
||
function commandFlow() {
|
||
const f = new TestFlow(7, "命令行", ["System_ExecuteCommand", "System_ExecuteCommandV1"]);
|
||
f.case("System_ExecuteCommand", { action: "Write-Output 'RPA-PS-中文-OK'" }, "RPA-PS-中文-OK", 'actual_1 === "RPA-PS-中文-OK"');
|
||
f.case("System_ExecuteCommandV1", { command_type: "1", dos_command: "echo RPA-DOS-OK", get_ret: "1" }, "RPA-DOS-OK", 'actual_2 === "RPA-DOS-OK"');
|
||
f.case("System_ExecuteCommandV1", { command_type: "3", ps_command: "Write-Output 'RPA-PS-V1-OK'", get_ret: "1" }, "RPA-PS-V1-OK", 'actual_3 === "RPA-PS-V1-OK"');
|
||
f.command("$dir=Join-Path ([IO.Path]::GetTempPath()) ('RPA-AppSystem-Tests\\'+[Guid]::NewGuid().ToString('N')); [IO.Directory]::CreateDirectory($dir) | Out-Null; $bat=Join-Path $dir 'echo-test.bat'; [IO.File]::WriteAllText($bat, '@echo RPA-BAT-OK', [Text.Encoding]::ASCII); $bat", "batPath", "创建仅输出标记的 BAT 夹具");
|
||
f.case("System_ExecuteCommandV1", { command_type: "2", bat_file: "${batPath}", get_ret: "1" }, "RPA-BAT-OK", 'actual_4 === "RPA-BAT-OK"');
|
||
f.case("System_ExecuteCommand", { action: "Write-Output 'RPA-ERROR-PROBE'; exit 9" }, "非零退出码应抛出异常", "false", { title: "非零返回码检查" });
|
||
// 最后一例是预期异常:由正常 Catch 记录 PASS,仍保留实际异常证据。
|
||
const last = f.entry.cases.at(-1);
|
||
const caught = f.flow.nodes.find(node => node.options.outputVar === last.resultVariable && node.slot === "except_fn");
|
||
caught.values.value = ref(`[${literal(last.id)}, ${literal(last.componentId)}, "PASS", ${literal(last.input)}, ${literal(last.expected)}, error.reason]`);
|
||
return f.finish();
|
||
}
|
||
|
||
function manualFlows() {
|
||
const result = [];
|
||
const credential = new TestFlow(8, "Windows 凭据", ["System_AddCredential", "System_ReadCredential", "System_DeleteCredential"], true);
|
||
credential.set("credentialTarget", "RPA-AppSystem-Tests-${now()}", "只允许使用新的测试凭据目标");
|
||
credential.case("System_AddCredential", { application: "${credentialTarget}", name: "", password: "" }, "安全输入机制准备好后,在专用目标添加测试凭据", "false", { observe: true });
|
||
credential.case("System_ReadCredential", { application: "${credentialTarget}" }, "仅验证测试凭据存在,不读取或打印密码", "false", { observe: true });
|
||
credential.case("System_DeleteCredential", { application: "${credentialTarget}" }, "删除刚创建的测试目标,之后人工确认不存在", "false", { observe: true });
|
||
result.push(credential.finish());
|
||
|
||
const ime = new TestFlow(9, "输入法", ["System_GetInputMethodState", "System_SetInputMethodState"], true);
|
||
ime.case("System_GetInputMethodState", {}, "读取前台窗口的真实输入法状态,人工对照任务栏", "false", { observe: true, output: "originalIme" });
|
||
ime.case("System_SetInputMethodState", { mode: "en-US" }, "前台编辑器应切换到英文模式;仅子进程返回 set:true 不能算通过", "false", { observe: true });
|
||
ime.case("System_SetInputMethodState", { mode: "${originalIme.culture}" }, "恢复原输入法;人工确认前台窗口的实际状态", "false", { observe: true });
|
||
result.push(ime.finish());
|
||
|
||
const sound = new TestFlow(10, "音频", ["System_PlaySound"], true);
|
||
const wav = fs.readFileSync(path.join(bundleRoot, "fixtures/tone.wav")).toString("base64");
|
||
sound.command(`$dir=Join-Path ([IO.Path]::GetTempPath()) ('RPA-AppSystem-Tests\\'+[Guid]::NewGuid().ToString('N')); [IO.Directory]::CreateDirectory($dir) | Out-Null; $wav=Join-Path $dir 'tone.wav'; [IO.File]::WriteAllBytes($wav,[Convert]::FromBase64String('${wav}')); $wav`, "audioPath", "准备 0.3 秒低音量测试音");
|
||
sound.case("System_PlaySound", { audio: "${audioPath}" }, "听到一次短提示音;没有声音不能算通过", "false", { observe: true });
|
||
result.push(sound.finish());
|
||
|
||
const saver = new TestFlow(11, "屏保", ["System_ScreenSaverShow", "System_ScreenSaverShowV1", "System_ScreenSaverStop"], true);
|
||
saver.case("System_ScreenSaverShow", { text: "RPA 测试", status: "1" }, "屏幕实际进入屏保;无配置时记录阻塞", "false", { observe: true });
|
||
saver.case("System_ScreenSaverStop", {}, "屏保实际关闭,可正常操作", "false", { observe: true });
|
||
saver.case("System_ScreenSaverShowV1", {}, "开启 Windows 已配置的屏保,不要求输入密码;人工确认屏幕实际变化", "false", { observe: true });
|
||
saver.case("System_ScreenSaverStop", {}, "第二种屏保实际关闭", "false", { observe: true });
|
||
result.push(saver.finish());
|
||
|
||
const lock = new TestFlow(12, "锁屏与解锁", ["ScreenControl_Screenlock", "ScreenControl_ScreenUnlock"], true);
|
||
lock.case("ScreenControl_Screenlock", { username: "", pwd: "", width: "", height: "", scale_factor: "100" }, "实际锁屏;需要人在机器旁用 Windows 登录凭据恢复,不改变账户密码", "false", { observe: true });
|
||
lock.case("ScreenControl_ScreenUnlock", {}, "仅尝试唤醒屏幕,loginUnlockSupported 必须为 false;Windows 登录仍需人工完成", "false", { observe: true });
|
||
result.push(lock.finish());
|
||
|
||
const remote = new TestFlow(13, "远程桌面", ["System_ConnectRemoteDesktop", "System_CloseRemoteDesktop"], true);
|
||
remote.set("remoteAddress", "", "填写用户自己的测试远程地址");
|
||
remote.set("allowDisconnect", false, "断连单独开关:可能断开当前 Windows 会话,必须另行确认");
|
||
remote.case("System_ConnectRemoteDesktop", { ip: "${remoteAddress}", port: "3389" }, "人工完成登录,并确认已连接到正确测试机器;启动 mstsc 不等于连接成功", "false", { observe: true });
|
||
const disconnect = remote.case("System_CloseRemoteDesktop", {}, "确认目标会话及恢复方式后才能执行;当前实现会调用 tsdiscon", "false", { observe: true });
|
||
const target = remote.flow.nodes.find(node => node.options.outputVar === disconnect);
|
||
const parent = { id: target.parentId, slot: target.slot };
|
||
const guard = remote.add("logic_control_if_else", { condition: "${allowDisconnect === true}" }, undefined, "断连操作单独保护", parent);
|
||
// 子节点按数组顺序执行,保护节点必须在目标前。
|
||
remote.flow.nodes.splice(remote.flow.nodes.indexOf(guard), 1);
|
||
remote.flow.nodes.splice(remote.flow.nodes.indexOf(target), 0, guard);
|
||
target.parentId = guard.id; target.slot = "statement_if";
|
||
remote.add("ExceptionUtil_Throws", { msg: "阻塞:断连开关未开启;执行可能中断当前会话。" }, undefined, "阻止未授权断连", { id: guard.id, slot: "statement_else" });
|
||
result.push(remote.finish());
|
||
return result;
|
||
}
|
||
|
||
export function writeSuite() {
|
||
const groups = buildSuite();
|
||
fs.mkdirSync(bundleRoot, { recursive: true });
|
||
for (const { flow, entry } of groups) fs.writeFileSync(path.join(bundleRoot, entry.file), JSON.stringify(flow, null, 2) + "\n");
|
||
const manifest = { schemaVersion: 1, generatedAt: epoch, groups: groups.map(item => item.entry) };
|
||
fs.writeFileSync(path.join(bundleRoot, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
||
return manifest;
|
||
}
|