merge: bring liuwenxin app-system fixes into main

This commit is contained in:
yu 2026-11-02 11:07:46 +08:00
commit ee60435e2b
38 changed files with 19887 additions and 147 deletions

View File

@ -3,14 +3,14 @@
画布式 RPA 桌面端(Electron + React Flow)。把 1016 项能力目录拖到画布上,连线、配置、运行,生成可执行的 RPA 流程。
> 当前状态:任务优先的编排工作台。默认进入“编排”任务列表,编辑任务后进入上下流向画布;组件库按分类树展示,设置为独立入口。
> 可执行能力持续接入中:**894 / 894(不接入项已分离备份)**(网页自动化、桌面自动化、数据、文件、办公、HTTP 等),其余灰标「待接入」,运行前检查会提前定位。
> 当前可编排能力:**894 / 894**(不接入项已分离备份;网页自动化、桌面自动化、数据、文件、办公、HTTP 等可直接编排)。
## 已实现
### 编排与运行
- Electron 31 + React 18 + TypeScript + React Flow 画布
- 1016 项组件目录:搜索 / 11 组筛选 / 可执行与待接入状态;任务列表实时刷新
- 894 项可编排组件目录:搜索 / 11 组筛选;122 项专用能力已分离备份;任务列表实时刷新
- 拖拽到画布、节点卡片、连线、缩放/平移/MiniMap、框选、撤销/重做
- IF / 计次循环 / 遍历 / 条件循环 / 异常捕获 容器:可把组件拖入分支或循环体
- 属性面板:按字段类型渲染输入、必填、变量、失败策略(超时/重试/间隔/跳过/延时)
@ -42,7 +42,8 @@
### 质量与测试
- 全量测试 192 项(能力注册冒烟、流程运行集成、Word/Excel/PDF/桌面桥真实设备测试)
- 全量测试 231 项(能力注册冒烟、流程运行集成、组件缺陷回归、测试流程包与安装保护、Word/Excel/PDF/桌面桥真实设备测试)
- 应用与系统 50 项组件验收:13 组任务、75 个用例,含真实执行与人工确认项
- 本地演示站(`127.0.0.1:17863`):网页订单页 + 网页元素演练场(表单/下拉/勾选/密码/禁用按钮/悬停/长页面)
- 内置 3 个网页自动化测试任务(浏览器导航、元素读写、滚动与 Cookie)+ 6 条固定示例任务
- 打包冒烟测试:跳过单实例锁隔离启动,验证产物可用性
@ -79,6 +80,7 @@ docs 现状分析、实施方案
## 文档
- [应用与系统 50 项组件测试任务](resources/test-flows/app-system/README.md):13 组可安装任务、使用步骤、覆盖清单及实际验收报告。
- [现状分析与可行性](docs/01-现状分析与可行性.md)
- [实施方案(三天)](docs/02-实施方案.md)
- [未接入能力名单](docs/04-未接入名单.md)

View File

@ -0,0 +1,68 @@
{
"Basic_JudgeDataType": {
"description": "按所选目标类型判断数据,返回布尔值 true 或 false。数值判断区分数字与数字文本。",
"fields": {
"into_type": {
"defaultValue": "number",
"description": "选择要判断的目标类型。历史默认值 1 兼容数值类型。",
"options": [
{ "label": "数值", "value": "number" },
{ "label": "整数", "value": "int" },
{ "label": "空值", "value": "null" },
{ "label": "字典", "value": "dict" },
{ "label": "数组", "value": "list" },
{ "label": "文本", "value": "string" },
{ "label": "布尔", "value": "bool" }
]
}
}
},
"Basic_DataTypeConversion": {
"description": "将数据转换为所选的数值、整数、布尔或文本类型;无法转换时给出参数错误。",
"fields": {
"into_type": {
"defaultValue": "number",
"description": "选择目标类型。历史默认值 1 兼容数值;其他数字别名 2=整数、3=布尔、4=文本。",
"options": [
{ "label": "数值", "value": "number" },
{ "label": "整数", "value": "int" },
{ "label": "布尔", "value": "bool" },
{ "label": "文本", "value": "string" }
]
}
}
},
"App_KillV1": {
"fields": {
"close_type": {
"options": [
{ "label": "按进程 PID", "value": "id" },
{ "label": "按进程名称", "value": "name" }
]
}
}
},
"System_ExecuteCommandV1": {
"fields": {
"command_type": {
"label": "命令类型",
"options": [
{ "label": "DOS 命令", "value": "1" },
{ "label": "BAT 文件", "value": "2" },
{ "label": "PowerShell 命令", "value": "3" }
]
}
}
},
"System_ScreenSaverShowV1": {
"description": "开启 Windows 已配置的系统屏保。当前组件不支持自定义屏保文字、透明度或解锁密码;登录保护遵循 Windows 系统设置。",
"messageTemplate": "开启 Windows 系统屏保",
"removeFields": ["text", "effect", "status", "set_password", "password"]
},
"ScreenControl_ScreenUnlock": {
"name": "唤醒屏幕(非登录解锁)",
"description": "仅尝试唤醒屏幕,不支持通过账号密码自动完成 Windows 登录解锁。锁屏后仍需本人手动登录。",
"messageTemplate": "尝试唤醒屏幕;Windows 登录锁屏需人工解锁",
"removeFields": ["username", "pwd"]
}
}

View File

@ -10,7 +10,7 @@
"vendor": 997,
"native": 16,
"triggers": 3,
"fields": 4300
"fields": 4293
},
"groups": [
{
@ -1633,7 +1633,17 @@
"placeholder": "",
"description": "通过进程PID、进程名称来终止进程",
"advanced": false,
"valueTypes": []
"valueTypes": [],
"options": [
{
"label": "按进程 PID",
"value": "id"
},
{
"label": "按进程名称",
"value": "name"
}
]
},
{
"key": "pid",
@ -3565,7 +3575,7 @@
"base_command"
],
"categorySignature": "judgment|base_command",
"description": "判断指定数据是否是数值、空值、字典、类型和数组,并返回true & false后保存。",
"description": "按所选目标类型判断数据,返回布尔值 true 或 false。数值判断区分数字与数字文本。",
"messageTemplate": "判断数据 $1 的类型, 然后把结果保存到 $0",
"baseType": "Basic_JudgeDataType",
"functionName": "Basic.JudgeDataType",
@ -3622,11 +3632,41 @@
"type": "string",
"displayType": "dropDownSelect",
"required": false,
"defaultValue": "1",
"defaultValue": "number",
"placeholder": "",
"description": "选择待判断目标数据的数据类型",
"description": "选择要判断的目标类型。历史默认值 1 兼容数值类型。",
"advanced": false,
"valueTypes": []
"valueTypes": [],
"options": [
{
"label": "数值",
"value": "number"
},
{
"label": "整数",
"value": "int"
},
{
"label": "空值",
"value": "null"
},
{
"label": "字典",
"value": "dict"
},
{
"label": "数组",
"value": "list"
},
{
"label": "文本",
"value": "string"
},
{
"label": "布尔",
"value": "bool"
}
]
}
],
"output": {
@ -3658,7 +3698,7 @@
"data_type_conversion"
],
"categorySignature": "data_type_conversion",
"description": "将指定的数据转为需要的类型(布尔、整数、数值、文字)后保存。",
"description": "将数据转换为所选的数值、整数、布尔或文本类型;无法转换时给出参数错误。",
"messageTemplate": "将待转换数据 $1 转换为 $2, 然后把结果保存到 $0",
"baseType": "Basic_DataTypeConversion",
"functionName": "Basic.DataTypeConversion",
@ -3714,11 +3754,29 @@
"type": "string",
"displayType": "dropDownSelect",
"required": false,
"defaultValue": "1",
"defaultValue": "number",
"placeholder": "",
"description": "Python 的基本数据类型选择。",
"description": "选择目标类型。历史默认值 1 兼容数值;其他数字别名 2=整数、3=布尔、4=文本。",
"advanced": false,
"valueTypes": []
"valueTypes": [],
"options": [
{
"label": "数值",
"value": "number"
},
{
"label": "整数",
"value": "int"
},
{
"label": "布尔",
"value": "bool"
},
{
"label": "文本",
"value": "string"
}
]
}
],
"output": {
@ -92484,15 +92542,15 @@
},
{
"id": "ScreenControl_ScreenUnlock",
"name": "屏幕解锁",
"name": "唤醒屏幕(非登录解锁)",
"kind": "vendor",
"group": "app",
"categories": [
"ScreenControl"
],
"categorySignature": "ScreenControl",
"description": "通过用户名,密码进行屏幕解锁。",
"messageTemplate": "通过用户名 $1 ,密码 $2进行屏幕解锁, 然后把结果保存到 $0",
"description": "仅尝试唤醒屏幕,不支持通过账号密码自动完成 Windows 登录解锁。锁屏后仍需本人手动登录。",
"messageTemplate": "尝试唤醒屏幕;Windows 登录锁屏需人工解锁",
"baseType": "ScreenControl_ScreenUnlock",
"functionName": "ScreenControl.ScreenUnlock",
"functionCalls": [
@ -92514,39 +92572,7 @@
"platformTag": null,
"icon": "app",
"risk": "high",
"fields": [
{
"key": "username",
"label": "用户名",
"widget": "text",
"type": "string",
"displayType": "text-expression",
"required": true,
"defaultValue": "",
"placeholder": "请输入屏幕解锁需要的用户名",
"description": "",
"advanced": false,
"valueTypes": [
"rpa_str"
]
},
{
"key": "pwd",
"label": "密码",
"widget": "secret",
"type": "string",
"displayType": "encryption",
"required": true,
"defaultValue": "",
"placeholder": "请输入屏幕解锁需要的密码",
"description": "",
"advanced": false,
"valueTypes": [
"rpa_password",
"rpa_str"
]
}
],
"fields": [],
"output": {
"label": "输出到变量",
"key": "var_name",
@ -96317,7 +96343,7 @@
"fields": [
{
"key": "command_type",
"label": "command_type",
"label": "命令类型",
"widget": "radio",
"type": "string",
"displayType": "buttonGroup",
@ -96326,7 +96352,21 @@
"placeholder": "",
"description": "",
"advanced": false,
"valueTypes": []
"valueTypes": [],
"options": [
{
"label": "DOS 命令",
"value": "1"
},
{
"label": "BAT 文件",
"value": "2"
},
{
"label": "PowerShell 命令",
"value": "3"
}
]
},
{
"key": "dos_command",
@ -96800,8 +96840,8 @@
"ScreenSaver"
],
"categorySignature": "ScreenSaver",
"description": "开启屏幕保护,防止隐私数据被偷窥。",
"messageTemplate": "屏保文字为 $0",
"description": "开启 Windows 已配置的系统屏保。当前组件不支持自定义屏保文字、透明度或解锁密码;登录保护遵循 Windows 系统设置。",
"messageTemplate": "开启 Windows 系统屏保",
"baseType": "System_ScreenSaverShow",
"functionName": "System.ScreenSaverShowV1",
"functionCalls": [
@ -96826,77 +96866,7 @@
"platformTag": null,
"icon": "app",
"risk": "high",
"fields": [
{
"key": "text",
"label": "屏保文字",
"widget": "text",
"type": "string",
"displayType": "text-expression",
"required": false,
"defaultValue": "",
"placeholder": "请输入屏保文字",
"description": "展示在屏保界面上的文字。",
"advanced": false,
"valueTypes": [
"rpa_str"
]
},
{
"key": "effect",
"label": "屏幕透明度",
"widget": "radio",
"type": "string",
"displayType": "radioGroup",
"required": false,
"defaultValue": "2",
"placeholder": "",
"description": "设置屏保的模糊、透明效果。",
"advanced": false,
"valueTypes": []
},
{
"key": "status",
"label": "流程暂停或结束时",
"widget": "radio",
"type": "string",
"displayType": "radioGroup",
"required": false,
"defaultValue": "1",
"placeholder": "",
"description": "运行结束时选择自动关闭屏保,还是手动输入密码关闭屏保。",
"advanced": false,
"valueTypes": []
},
{
"key": "set_password",
"label": "设置密码解锁",
"widget": "radio",
"type": "string",
"displayType": "radioGroup",
"required": false,
"defaultValue": "0",
"placeholder": "",
"description": "可设置解锁密码为系统密码或自定义密码,密码字符上限20",
"advanced": false,
"valueTypes": []
},
{
"key": "password",
"label": "password",
"widget": "text",
"type": "string",
"displayType": "text-expression",
"required": true,
"defaultValue": "",
"placeholder": "请输入自定义密码",
"description": "",
"advanced": false,
"valueTypes": [
"rpa_str"
]
}
],
"fields": [],
"output": {
"label": "",
"key": "",

View File

@ -0,0 +1,106 @@
# 应用与系统组件测试
本流程包覆盖“可执行 → 应用与系统”的 **50 个组件 ID**,分为 13 个任务、75 个用例。同名组件的不同版本分别统计。11 个待接入组件不计入验收。
测试发现的 PID 空结果、类型判断/转换、毫秒等待、按模式校验参数、命令行编码及 BAT 路径问题现已修复。绿色运行状态仅表示引擎没有抛出异常;本流程还会核对实际值、返回类型、进程状态和等待耗时。
## 在当前应用中使用
1. 返回左侧“编排”首页,点击任务列表右上角的刷新按钮。
2. 搜索“组件测试”,打开所需任务并点击“编辑流程”。
3. 先使用 01~07 组。运行后打开底部“运行日志”,搜索“用例结果”或查看末尾的“本组汇总”。
4. 每条用例结果依次为:`[用例编号, 组件 ID, 状态, 输入, 预期, 实际]`。
5. 状态 `PASS` 是通过,`FAIL` 是失败,`BLOCKED` 是缺少条件或无法校验,`MANUAL` 是待人工确认。有任一未通过项目,任务最终显示“失败”,详细原因以用例结果为准。
01~07 组现可自动验证;类型选项使用明确的语义值,07 组只填写当前命令模式需要的参数。实际结果见本包的最新报告,修复前的失败证据另行保留。
## 分组与预期
| 组 | 测试目标 | 关键输入与验收条件 |
|---|---|---|
| 01 · 应用与 PID | 5 个组件,8 个用例 | 生成唯一 `rpatest_` 程序,启动两个实例;返回 PID 与列表应匹配;两个关闭组件各关闭一个实例;无匹配时应返回 `null` / `[]`。 |
| 02 · 数据转换 | 6 个组件,10 个用例 | `"123.5" → 123.5`、`"-42.25" → -42.25`、`-3.9 → -3`、真假布尔值、`42 → "42"`;通用转换分别检查数值、整数、布尔和文本。 |
| 03 · 数据判断 | 6 个组件,12 个用例 | 对象与数组、数字与中文、`null` / 空文本 / `0`、真实数组与文本 `"[]"`;“判断数据类型”按目录契约应返回布尔值。 |
| 04 · 变量与配置 | 7 个组件,10 个用例 | 中文变量、整数和列表、深复制内容及引用独立、当前流程 ID、自定义配置命中与未命中、计数 `10 → 12 → 14`。 |
| 05 · 等待与随机数 | 5 个组件,7 个用例 | 三个等待组件按独立毫秒时钟测量;毫秒版本设置 `3000`,预期 `2800~6500 ms`;随机整数包含 `min=max=7` 与负数范围,小数应处于 `[0,1)`。 |
| 06 · 系统路径与环境变量 | 5 个组件,7 个用例 | 与 Windows 独立查询的临时、用户、桌面路径比较;只读写专用变量 `RPA_APP_SYSTEM_TEST_VALUE`,结束恢复原先读到的值。 |
| 07 · 命令行 | 2 个组件,5 个用例 | PowerShell 中文固定输出;DOS、BAT、PowerShell 三种 V1 模式;非零退出码应报错。只校验当前模式的参数,另有含中文和空格 BAT 路径的回归测试。 |
| 08 · Windows 凭据【手动】 | 3 个组件,3 个用例 | 仅操作唯一新建测试目标。缺少安全的秘密输入机制,默认阻塞,不能把真实密码填进流程。 |
| 09 · 输入法【手动】 | 2 个组件,3 个用例 | 读取、切换到 `en-US`、尝试恢复原状态;必须人工核对前台窗口与任务栏,子进程返回成功不算通过。 |
| 10 · 音频【手动】 | 1 个组件,1 个用例 | 使用自制 0.3 秒、440 Hz 低音量 WAV;人工确认确实听到声音。 |
| 11 · 屏保【手动】 | 3 个组件,4 个用例 | 两个开启版本分别配对关闭,需观察实际屏保;V1 按 Windows 已有屏保设置运行,已去掉未实现的自定义文字、效果和密码参数。 |
| 12 · 锁屏与解锁【手动】 | 2 个组件,2 个用例 | 锁屏仍要求账户等参数,该组保持阻塞。原“屏幕解锁”已明确标为“唤醒屏幕(非登录解锁)”,无需账号密码,不支持 Windows 登录解锁。 |
| 13 · 远程桌面【手动】 | 2 个组件,2 个用例 | 填写自己的测试地址,人工完成登录并验证目标;`tsdiscon` 可能断开当前会话,关闭操作有独立开关。 |
## 手动测试的执行开关
08~13 组不会由自动脚本执行。任务中名为“手动执行开关”的初始化节点,默认值是布尔 `false`。
只有确认该组的运行环境、影响和恢复方式后,才把该节点的“值”改为 `${true}`。恢复关闭使用 `${false}`。不要填写普通文本 `true` / `false`,保护条件只接受真正的布尔值。
- **08 组和 12 组的锁屏节点**:当前没有不会把秘密写入流程或 `run.json` 的输入途径,因此保持阻塞;不要填 Windows 登录密码,不创建、覆盖或删除已有业务凭据。“唤醒屏幕”本身已无需任何凭据,可在单独任务中测试。
- **09 组**:先准备一个没有未保存内容的文本编辑窗口,人工记录原输入法,执行后核对是否真的切换并恢复。异常时人工切回原状态。
- **10 组**:确认音量和输出设备后,只播放本包的短测试音。
- **11 组**:需人在机器旁,有办法手动退出屏保;只调用 Windows 已配置的屏保,不设置自定义解锁密码。旧节点若保留 `set_password=1`,会在桌面操作前给出明确错误,可删除该旧节点后重新添加。
- **12 组**:实际锁屏须先明确确认影响和恢复方式,人在机器旁并能正常用 Windows 登录;不能测试自动输入登录密码。
- **13 组**:先设置“填写用户自己的测试远程地址”。连接成功须人工确认,启动 `mstsc` 不算已经登录。断连还要求“断连单独开关”为 `${true}`,必须另行确认操作的是哪个会话及如何重新连接。确认前保持 `${false}`。
人工项目即使组件返回成功,也继续显示 `MANUAL`,本工具不会自动写成通过。人工证据请附实际观察、时间和必要截图,不能仅记录返回标志。
## 安装、校验与报告命令
在项目根目录运行:
```powershell
# 校验 13 组 / 49 项覆盖、连接关系及现有运行前校验
node scripts/app-system-tests.mjs verify
# 安装到当前用户:只新增;同 ID 已存在则跳过,保留用户修改
node scripts/app-system-tests.mjs install --user-data "$env:APPDATA\rpa-studio"
# 重建仓库里的流程 JSON;不会覆盖应用中已安装的任务
node scripts/app-system-tests.mjs generate
# 构建后,在全新的 Electron 用户目录运行非手动任务
npm run build
node scripts/app-system-tests.mjs run-safe
# 同时用临时可见的独立 Electron 窗口验证下拉选项、模式切换和唤醒提示
node scripts/app-system-tests.mjs run-safe --ui-check
# 从本次真实证据重新生成报告
node scripts/app-system-tests.mjs report
```
安装默认使用 `%APPDATA%\rpa-studio`;非标准用户目录需要显式传入 `--user-data`。重复安装不会覆盖已有测试任务,也不改变用户原有任务。
`run-safe` 退出码:`0` 表示报告没有失败或阻塞,`2` 表示存在失败或阻塞(包含手动任务缺少前置条件),`1` 表示工具自身异常。报告和原始事件保存在项目的 `test-output/app-system/`,该目录已由现有 `.gitignore` 忽略。
每次自动运行保留独立 Electron 用户目录及 `runs/<runId>/run.json`,测试不会占用当前日常实例,也不会把隔离验证结果伪装为用户任务列表中的运行历史。任务列表的新任务初始仍显示“尚未运行”。
## 边界与恢复
- 测试进程名称和可执行路径唯一,两个关闭组件只操作新建夹具 PID。常规异常后仍执行专用进程清理;中止流程可能跳过收尾,夹具会在 90 秒内自动退出。
- 临时文件仅写入系统临时目录下的新建 `RPA-AppSystem-Tests` 子目录,保留供排查,不批量删除文件。
- 环境变量组件只操作当前应用进程,不修改 Windows 用户/系统环境变量。本包恢复原读取值;原本不存在时只能恢复为空文本,当前组件不支持删除进程内变量,退出应用后消失。隔离运行预设原始值并验证恢复。
- 凭据、输入法、屏保、锁屏、远程会话均未在自动验收中改变。
- 缺少类型选项映射、真实桌面观察或测试远程主机时,状态保持阻塞或待人工确认。
## 修复结果与保留的限制
- PID:无匹配返回 `[]` / `null`,过滤空行、零和无效 PID。
- 类型判断:按目标类型返回布尔值;数值类型与数字文本分开判断。
- 类型转换:使用 `number`、`int`、`bool`、`string` 选项,输入无效时报错。历史默认值 `1` 兼容数值类型;其他数字别名为本项目明确规定的兼容值,前端统一使用语义值。
- 等待:毫秒组件读取 `mileSecond`,其他秒制等待组件保持秒制;支持取消,拒绝超出计时器范围的值。
- 关闭程序:PID/名称模式分别显示并校验参数;无效 PID 不会降级成按名称批量关闭。名称模式尊重“关闭所有会话”选项,并排除 RPA Studio 主进程。
- 命令行:DOS/BAT/PowerShell 分别校验,修复中文输出编码和含空格、中文 BAT 路径的引号处理。
- 屏保 V1:仅提供实际实现的系统屏保入口,不再要求自定义密码,也不展示未实现的自定义效果。
- 原“屏幕解锁”:名称改为“唤醒屏幕(非登录解锁)”,不收集账号密码,返回 `loginUnlockSupported:false`;本人仍需完成 Windows 登录。
目录修正保存在 `resources/catalog-overrides.json`,重新生成目录也会应用;不会恢复错误的下拉选项或密码字段。
安装仍只新增、不覆盖已有任务。需要升级本包旧任务时,使用 `upgrade --baseline <升级前的模板目录>`;仅升级仍匹配旧模板的任务,并备份原文件,用户修改过的任务会跳过。
输入法、音频、屏保、锁屏和远程桌面仍需要人工证据或专门环境,不自动执行高影响操作。输入法子进程实现和 `tsdiscon` 会话语义不在本轮 9 项修复范围内。
最新逐用例证据见 [REPORT.md](REPORT.md),修复前证据见 [REPORT-before-fix.md](REPORT-before-fix.md)。

View File

@ -0,0 +1,153 @@
# 应用与系统组件测试报告
记录时间:2026年10月30日 16:52:04(北京时间)。覆盖 50 项可执行组件,13 组任务。
按组件统计:通过 29,失败 4,阻塞 11,待人工确认 5,未运行 0。
状态依据是断言、前置校验和人工证据,节点执行成功不直接代表功能通过。未执行手动系统操作。
| 组 | 组件 ID | 名称 | 状态 |
|---|---|---|---|
| 01 | App_Run | 启动应用程序 | 通过 |
| 01 | App_GetPid | 获取一个匹配PID | 失败 |
| 01 | App_GetPids | 获取全部匹配PID | 失败 |
| 01 | App_Kill | 关闭应用程序 | 通过 |
| 01 | App_KillV1 | 关闭应用程序 | 通过 |
| 02 | Basic_StringToNum | 将文本转换为数字 | 通过 |
| 02 | Basic_ConvertNumber | 数据转为数值类型 | 通过 |
| 02 | Basic_ConvertInt | 数值转为整数类型 | 通过 |
| 02 | Basic_ConvertBool | 数据转为布尔类型 | 通过 |
| 02 | Basic_ConvertString | 将数字转换为文本 | 通过 |
| 02 | Basic_DataTypeConversion | 转换数据类型 | 阻塞 |
| 03 | Basic_IsDictionary | 判断数据是否为字典 | 通过 |
| 03 | Basic_IsNumber | 判断数据是否为数值 | 通过 |
| 03 | Basic_IsNull | 判断数据是否为空 | 通过 |
| 03 | Basic_IsArray | 判断数据是否为数组 | 通过 |
| 03 | Basic_Type | 获取数据类型 | 通过 |
| 03 | Basic_JudgeDataType | 判断数据类型 | 失败 |
| 04 | Basic_SetVariable | 设置变量 | 通过 |
| 04 | Basic_SetVariable_V1 | 设置变量 | 通过 |
| 04 | Basic_Clone | 复制数据 | 通过 |
| 04 | Basic_GetProcessInfo | 获取流程信息 | 通过 |
| 04 | Basic_DebugOutput | 调试输出 | 通过 |
| 04 | CustomConfig_GetConfig | 自定义配置 | 通过 |
| 04 | Basic_BusinessOrderCount | 业务办理计数 | 通过 |
| 05 | Basic_Delay_V2 | 等待 | 通过 |
| 05 | Basic_Delay_V1 | 等待 | 通过 |
| 05 | Basic_Delay | 流程延迟执行 | 失败 |
| 05 | Basic_RandomInt | 生成随机数 | 通过 |
| 05 | Basic_Rnd | 生成随机数 | 通过 |
| 06 | System_GetTempPath | 获取临时文件夹路径 | 通过 |
| 06 | System_GetHomePath | 获取用户文件夹路径 | 通过 |
| 06 | System_GetSystemPath | 获取系统文件夹路径 | 通过 |
| 06 | System_GetEnv | 获取环境变量 | 通过 |
| 06 | System_SetEnv | 设置环境变量 | 通过 |
| 07 | System_ExecuteCommand | 执行命令行 | 阻塞 |
| 07 | System_ExecuteCommandV1 | 执行命令行 | 阻塞 |
| 08 | System_AddCredential | 添加Windows凭据 | 阻塞 |
| 08 | System_ReadCredential | 获取Windows凭据 | 阻塞 |
| 08 | System_DeleteCredential | 删除Windows凭据 | 阻塞 |
| 09 | System_GetInputMethodState | 获取输入法状态 | 待人工确认 |
| 09 | System_SetInputMethodState | 设置输入法状态 | 待人工确认 |
| 10 | System_PlaySound | 播放音频 | 待人工确认 |
| 11 | System_ScreenSaverShow | 开启屏保 | 阻塞 |
| 11 | System_ScreenSaverShowV1 | 开启屏保 | 阻塞 |
| 11 | System_ScreenSaverStop | 关闭屏保 | 阻塞 |
| 12 | ScreenControl_Screenlock | 屏幕锁屏 | 阻塞 |
| 12 | ScreenControl_ScreenUnlock | 屏幕解锁 | 阻塞 |
| 13 | System_ConnectRemoteDesktop | 连接远程桌面 | 待人工确认 |
| 13 | System_CloseRemoteDesktop | 关闭远程连接 | 待人工确认 |
## 用例证据
| 用例 | 组件 | 状态 | 预期 | 实际/原因 |
|---|---|---|---|---|
| 01-01 | App_Run | 通过 | 第一个测试进程 PID 大于 0 | 37796 |
| 01-02 | App_Run | 通过 | 第二个 PID 大于 0,且与第一个不同 | 48484 |
| 01-03 | App_GetPid | 通过 | 返回本次创建的任一 PID | 37796 |
| 01-04 | App_GetPids | 通过 | 列表恰含本次创建的两个 PID | [37796,48484] |
| 01-05 | App_Kill | 通过 | 结束第一个夹具 PID,第二个仍在运行 | [37796,false,true] |
| 01-06 | App_KillV1 | 通过 | 结束第二个夹具 PID | [48484,false] |
| 01-07 | App_GetPids | 失败 | 所有夹具结束后应返回空数组 [] | [0] |
| 01-08 | App_GetPid | 失败 | 无匹配时应返回 null | 0 |
| 02-01 | Basic_StringToNum | 通过 | 数字 123.5 | 123.5 |
| 02-02 | Basic_ConvertNumber | 通过 | 数字 -42.25 | -42.25 |
| 02-03 | Basic_ConvertInt | 通过 | 整数 -3(向零截断) | -3 |
| 02-04 | Basic_ConvertBool | 通过 | 布尔 true | true |
| 02-05 | Basic_ConvertBool | 通过 | 布尔 false | false |
| 02-06 | Basic_ConvertString | 通过 | 文本 "42" | "42" |
| 02-07 | Basic_DataTypeConversion | 阻塞 | 按目标类型转换;先确认目录中目标类型编号映射 | "42";目录没有目标类型选项映射,不能猜测编号;当前实现也未读取 into_type。 |
| 03-01 | Basic_IsDictionary | 通过 | true | true |
| 03-02 | Basic_IsDictionary | 通过 | false | false |
| 03-03 | Basic_IsNumber | 通过 | true | true |
| 03-04 | Basic_IsNumber | 通过 | false | false |
| 03-05 | Basic_IsNull | 通过 | true | true |
| 03-06 | Basic_IsNull | 通过 | true | true |
| 03-07 | Basic_IsNull | 通过 | false | false |
| 03-08 | Basic_IsArray | 通过 | true | true |
| 03-09 | Basic_IsArray | 通过 | false | false |
| 03-10 | Basic_Type | 通过 | "list" | "list" |
| 03-11 | Basic_Type | 通过 | "null" | "null" |
| 03-12 | Basic_JudgeDataType | 失败 | 返回布尔判定结果(true 或 false),不是类型名称 | "number" |
| 04-01 | Basic_SetVariable | 通过 | 原值及输出变量均保持中文文本 | "中文变量往返" |
| 04-02 | Basic_SetVariable_V1 | 通过 | 数字 42 | 42 |
| 04-03 | Basic_SetVariable_V1 | 通过 | 数组 [1,2] | [1,2] |
| 04-04 | Basic_Clone | 通过 | 深复制内容相同,外层和嵌套对象均不共用引用 | {"title":"中文","nested":{"n":1}} |
| 04-05 | Basic_GetProcessInfo | 通过 | 当前流程 ID、名称、运行 ID 和节点数一致 | {"id":"test_appsystem_04","name":"组件测试 04 · 变量与配置","description":"查看运行日志中的用例结果;有失败时任务最终显示失败,其他用例仍继续。","enabled":true,"trigger":{"type":"manual","label":"手动运行"},"version":"0.1.0","createdAt":"2026-10-30T00:00:00.000Z","updatedAt":"2026-10-30T00:00:00.000Z","nodeCount":99,"runId":"run_mvuqh8oe_gmpo"} |
| 04-06 | Basic_DebugOutput | 通过 | 日志及返回值为“中文日志测试:42” | "中文日志测试:42" |
| 04-07 | CustomConfig_GetConfig | 通过 | 数字 30 | 30 |
| 04-08 | CustomConfig_GetConfig | 通过 | 未命中返回 null | null |
| 04-09 | Basic_BusinessOrderCount | 通过 | 首次计数 12 | 12 |
| 04-10 | Basic_BusinessOrderCount | 通过 | 同一流程再次计数 14 | 14 |
| 05-01 | Basic_Delay_V2 | 通过 | 实际等待耗时 120~1800 ms | 161 |
| 05-02 | Basic_Delay_V2 | 通过 | 实际等待耗时 80~1800 ms | 186 |
| 05-03 | Basic_Delay_V1 | 通过 | 实际等待耗时 120~1800 ms | 156 |
| 05-04 | Basic_Delay | 失败 | 实际等待耗时 2800~6500 ms | 1013 |
| 05-05 | Basic_RandomInt | 通过 | 最小值等于最大值时返回整数 7 | 7 |
| 05-06 | Basic_RandomInt | 通过 | 返回 -2~2 的整数 | -2 |
| 05-07 | Basic_Rnd | 通过 | 随机小数满足 0 ≤ x < 1 | 0.6374054093586852 |
| 06-01 | System_GetTempPath | 通过 | 与系统临时目录一致(忽略尾部分隔符) | "C:\\Users\\Administrator\\AppData\\Local\\Temp" |
| 06-02 | System_GetHomePath | 通过 | 与 Windows 用户文件夹一致 | "C:\\Users\\Administrator" |
| 06-03 | System_GetSystemPath | 通过 | 与 Windows 实际桌面目录一致(包含重定向) | "C:\\Users\\Administrator\\Desktop" |
| 06-04 | System_GetEnv | 通过 | 读取测试专用变量的原始值,作为恢复依据 | "隔离环境原始值" |
| 06-05 | System_SetEnv | 通过 | 当前应用进程内设置成功 | "中文-测试-42" |
| 06-06 | System_GetEnv | 通过 | 能读回中文-测试-42 | "中文-测试-42" |
| 06-07 | System_SetEnv | 通过 | 恢复测试前读取值 | "隔离环境原始值" |
| 07-01 | System_ExecuteCommand | 阻塞 | RPA-PS-中文-OK | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 07-02 | System_ExecuteCommandV1 | 阻塞 | RPA-DOS-OK | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 07-03 | System_ExecuteCommandV1 | 阻塞 | RPA-PS-V1-OK | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 07-04 | System_ExecuteCommandV1 | 阻塞 | RPA-BAT-OK | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 07-05 | System_ExecuteCommand | 阻塞 | 非零退出码应抛出异常 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 08-01 | System_AddCredential | 阻塞 | 安全输入机制准备好后,在专用目标添加测试凭据 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 08-02 | System_ReadCredential | 阻塞 | 仅验证测试凭据存在,不读取或打印密码 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 08-03 | System_DeleteCredential | 阻塞 | 删除刚创建的测试目标,之后人工确认不存在 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 09-01 | System_GetInputMethodState | 待人工确认 | 读取前台窗口的真实输入法状态,人工对照任务栏 | 手动任务不纳入自动执行 |
| 09-02 | System_SetInputMethodState | 待人工确认 | 前台编辑器应切换到英文模式;仅子进程返回 set:true 不能算通过 | 手动任务不纳入自动执行 |
| 09-03 | System_SetInputMethodState | 待人工确认 | 恢复原输入法;人工确认前台窗口的实际状态 | 手动任务不纳入自动执行 |
| 10-01 | System_PlaySound | 待人工确认 | 听到一次短提示音;没有声音不能算通过 | 手动任务不纳入自动执行 |
| 11-01 | System_ScreenSaverShow | 阻塞 | 屏幕实际进入屏保;无配置时记录阻塞 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 11-02 | System_ScreenSaverStop | 阻塞 | 屏保实际关闭,可正常操作 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 11-03 | System_ScreenSaverShowV1 | 阻塞 | 未启用密码保护时无需密码也应能运行 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 11-04 | System_ScreenSaverStop | 阻塞 | 第二种屏保实际关闭 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 12-01 | ScreenControl_Screenlock | 阻塞 | 实际锁屏;需要人在机器旁用 Windows 登录凭据恢复,不改变账户密码 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 12-02 | ScreenControl_ScreenUnlock | 阻塞 | 核对 Windows 登录解锁;当前实现仅尝试唤醒屏幕,不能标记自动解锁通过 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 13-01 | System_ConnectRemoteDesktop | 待人工确认 | 人工完成登录,并确认已连接到正确测试机器;启动 mstsc 不等于连接成功 | 手动任务不纳入自动执行 |
| 13-02 | System_CloseRemoteDesktop | 待人工确认 | 确认目标会话及恢复方式后才能执行;当前实现会调用 tsdiscon | 手动任务不纳入自动执行 |
## 运行前校验阻塞
- test_appsystem_07:「System_ExecuteCommandV1」缺少必填参数:BAT文件;「System_ExecuteCommandV1」缺少必填参数:PowerShell命令;「System_ExecuteCommandV1」缺少必填参数:DOS命令;「System_ExecuteCommandV1」缺少必填参数:BAT文件;「System_ExecuteCommandV1」缺少必填参数:DOS命令;「System_ExecuteCommandV1」缺少必填参数:PowerShell命令
- test_appsystem_08:「System_AddCredential」缺少必填参数:用户名
- test_appsystem_11:「System_ScreenSaverShowV1」缺少必填参数:password
- test_appsystem_12:「ScreenControl_Screenlock」缺少必填参数:用户名;「ScreenControl_Screenlock」缺少必填参数:密码;「ScreenControl_Screenlock」缺少必填参数:分辨率(宽);「ScreenControl_Screenlock」缺少必填参数:分辨率(高);「ScreenControl_ScreenUnlock」缺少必填参数:用户名;「ScreenControl_ScreenUnlock」缺少必填参数:密码
## 证据位置
隔离用户目录:D:\work\code\RPA-only-demo\test-output\app-system\isolated-I8utNv
- test_appsystem_01:D:\work\code\RPA-only-demo\test-output\app-system\isolated-I8utNv\runs\run_mvuqh1m8_mpns\run.json
- test_appsystem_02:D:\work\code\RPA-only-demo\test-output\app-system\isolated-I8utNv\runs\run_mvuqh8fe_enbw\run.json
- test_appsystem_03:D:\work\code\RPA-only-demo\test-output\app-system\isolated-I8utNv\runs\run_mvuqh8lw_3nsu\run.json
- test_appsystem_04:D:\work\code\RPA-only-demo\test-output\app-system\isolated-I8utNv\runs\run_mvuqh8oe_gmpo\run.json
- test_appsystem_05:D:\work\code\RPA-only-demo\test-output\app-system\isolated-I8utNv\runs\run_mvuqh8sp_54xy\run.json
- test_appsystem_06:D:\work\code\RPA-only-demo\test-output\app-system\isolated-I8utNv\runs\run_mvuqha0h_4bfu\run.json

View File

@ -0,0 +1,159 @@
# 应用与系统组件测试报告
记录时间:2026年10月30日 18:50:29(北京时间)。覆盖 50 项可执行组件,13 组任务。
按组件统计:通过 36,失败 0,阻塞 5,待人工确认 8,未运行 0。
状态依据是断言、前置校验和人工证据,节点执行成功不直接代表功能通过。未执行手动系统操作。
| 组 | 组件 ID | 名称 | 状态 |
|---|---|---|---|
| 01 | App_Run | 启动应用程序 | 通过 |
| 01 | App_GetPid | 获取一个匹配PID | 通过 |
| 01 | App_GetPids | 获取全部匹配PID | 通过 |
| 01 | App_Kill | 关闭应用程序 | 通过 |
| 01 | App_KillV1 | 关闭应用程序 | 通过 |
| 02 | Basic_StringToNum | 将文本转换为数字 | 通过 |
| 02 | Basic_ConvertNumber | 数据转为数值类型 | 通过 |
| 02 | Basic_ConvertInt | 数值转为整数类型 | 通过 |
| 02 | Basic_ConvertBool | 数据转为布尔类型 | 通过 |
| 02 | Basic_ConvertString | 将数字转换为文本 | 通过 |
| 02 | Basic_DataTypeConversion | 转换数据类型 | 通过 |
| 03 | Basic_IsDictionary | 判断数据是否为字典 | 通过 |
| 03 | Basic_IsNumber | 判断数据是否为数值 | 通过 |
| 03 | Basic_IsNull | 判断数据是否为空 | 通过 |
| 03 | Basic_IsArray | 判断数据是否为数组 | 通过 |
| 03 | Basic_Type | 获取数据类型 | 通过 |
| 03 | Basic_JudgeDataType | 判断数据类型 | 通过 |
| 04 | Basic_SetVariable | 设置变量 | 通过 |
| 04 | Basic_SetVariable_V1 | 设置变量 | 通过 |
| 04 | Basic_Clone | 复制数据 | 通过 |
| 04 | Basic_GetProcessInfo | 获取流程信息 | 通过 |
| 04 | Basic_DebugOutput | 调试输出 | 通过 |
| 04 | CustomConfig_GetConfig | 自定义配置 | 通过 |
| 04 | Basic_BusinessOrderCount | 业务办理计数 | 通过 |
| 05 | Basic_Delay_V2 | 等待 | 通过 |
| 05 | Basic_Delay_V1 | 等待 | 通过 |
| 05 | Basic_Delay | 流程延迟执行 | 通过 |
| 05 | Basic_RandomInt | 生成随机数 | 通过 |
| 05 | Basic_Rnd | 生成随机数 | 通过 |
| 06 | System_GetTempPath | 获取临时文件夹路径 | 通过 |
| 06 | System_GetHomePath | 获取用户文件夹路径 | 通过 |
| 06 | System_GetSystemPath | 获取系统文件夹路径 | 通过 |
| 06 | System_GetEnv | 获取环境变量 | 通过 |
| 06 | System_SetEnv | 设置环境变量 | 通过 |
| 07 | System_ExecuteCommand | 执行命令行 | 通过 |
| 07 | System_ExecuteCommandV1 | 执行命令行 | 通过 |
| 08 | System_AddCredential | 添加Windows凭据 | 阻塞 |
| 08 | System_ReadCredential | 获取Windows凭据 | 阻塞 |
| 08 | System_DeleteCredential | 删除Windows凭据 | 阻塞 |
| 09 | System_GetInputMethodState | 获取输入法状态 | 待人工确认 |
| 09 | System_SetInputMethodState | 设置输入法状态 | 待人工确认 |
| 10 | System_PlaySound | 播放音频 | 待人工确认 |
| 11 | System_ScreenSaverShow | 开启屏保 | 待人工确认 |
| 11 | System_ScreenSaverShowV1 | 开启屏保 | 待人工确认 |
| 11 | System_ScreenSaverStop | 关闭屏保 | 待人工确认 |
| 12 | ScreenControl_Screenlock | 屏幕锁屏 | 阻塞 |
| 12 | ScreenControl_ScreenUnlock | 唤醒屏幕(非登录解锁) | 阻塞 |
| 13 | System_ConnectRemoteDesktop | 连接远程桌面 | 待人工确认 |
| 13 | System_CloseRemoteDesktop | 关闭远程连接 | 待人工确认 |
## 真实界面验证
已通过独立 Electron 窗口检查:类型下拉选项、PID/名称参数切换、DOS/PowerShell 参数切换,以及唤醒组件不再索要账号密码。界面快照及 ui-check.json 保存在 test-output/app-system/。
## 用例证据
| 用例 | 组件 | 状态 | 预期 | 实际/原因 |
|---|---|---|---|---|
| 01-01 | App_Run | 通过 | 第一个测试进程 PID 大于 0 | 53688 |
| 01-02 | App_Run | 通过 | 第二个 PID 大于 0,且与第一个不同 | 47408 |
| 01-03 | App_GetPid | 通过 | 返回本次创建的任一 PID | 47408 |
| 01-04 | App_GetPids | 通过 | 列表恰含本次创建的两个 PID | [47408,53688] |
| 01-05 | App_Kill | 通过 | 结束第一个夹具 PID,第二个仍在运行 | [53688,false,true] |
| 01-06 | App_KillV1 | 通过 | 只填写 PID 即可结束第二个夹具进程 | [47408,false] |
| 01-07 | App_GetPids | 通过 | 所有夹具结束后应返回空数组 [] | [] |
| 01-08 | App_GetPid | 通过 | 无匹配时应返回 null | null |
| 02-01 | Basic_StringToNum | 通过 | 数字 123.5 | 123.5 |
| 02-02 | Basic_ConvertNumber | 通过 | 数字 -42.25 | -42.25 |
| 02-03 | Basic_ConvertInt | 通过 | 整数 -3(向零截断) | -3 |
| 02-04 | Basic_ConvertBool | 通过 | 布尔 true | true |
| 02-05 | Basic_ConvertBool | 通过 | 布尔 false | false |
| 02-06 | Basic_ConvertString | 通过 | 文本 "42" | "42" |
| 02-07 | Basic_DataTypeConversion | 通过 | 数值 42,严格区分文本和数值 | 42 |
| 02-08 | Basic_DataTypeConversion | 通过 | 整数 -3 | -3 |
| 02-09 | Basic_DataTypeConversion | 通过 | 布尔 false | false |
| 02-10 | Basic_DataTypeConversion | 通过 | 文本 "42" | "42" |
| 03-01 | Basic_IsDictionary | 通过 | true | true |
| 03-02 | Basic_IsDictionary | 通过 | false | false |
| 03-03 | Basic_IsNumber | 通过 | true | true |
| 03-04 | Basic_IsNumber | 通过 | false | false |
| 03-05 | Basic_IsNull | 通过 | true | true |
| 03-06 | Basic_IsNull | 通过 | true | true |
| 03-07 | Basic_IsNull | 通过 | false | false |
| 03-08 | Basic_IsArray | 通过 | true | true |
| 03-09 | Basic_IsArray | 通过 | false | false |
| 03-10 | Basic_Type | 通过 | "list" | "list" |
| 03-11 | Basic_Type | 通过 | "null" | "null" |
| 03-12 | Basic_JudgeDataType | 通过 | 数字 42 属于数值类型,返回布尔 true | true |
| 04-01 | Basic_SetVariable | 通过 | 原值及输出变量均保持中文文本 | "中文变量往返" |
| 04-02 | Basic_SetVariable_V1 | 通过 | 数字 42 | 42 |
| 04-03 | Basic_SetVariable_V1 | 通过 | 数组 [1,2] | [1,2] |
| 04-04 | Basic_Clone | 通过 | 深复制内容相同,外层和嵌套对象均不共用引用 | {"title":"中文","nested":{"n":1}} |
| 04-05 | Basic_GetProcessInfo | 通过 | 当前流程 ID、名称、运行 ID 和节点数一致 | {"id":"test_appsystem_04","name":"组件测试 04 · 变量与配置","description":"查看运行日志中的用例结果;有失败时任务最终显示失败,其他用例仍继续。","enabled":true,"trigger":{"type":"manual","label":"手动运行"},"version":"0.1.0","createdAt":"2026-10-30T00:00:00.000Z","updatedAt":"2026-10-30T00:00:00.000Z","nodeCount":99,"runId":"run_mvuupg9x_yifj"} |
| 04-06 | Basic_DebugOutput | 通过 | 日志及返回值为“中文日志测试:42” | "中文日志测试:42" |
| 04-07 | CustomConfig_GetConfig | 通过 | 数字 30 | 30 |
| 04-08 | CustomConfig_GetConfig | 通过 | 未命中返回 null | null |
| 04-09 | Basic_BusinessOrderCount | 通过 | 首次计数 12 | 12 |
| 04-10 | Basic_BusinessOrderCount | 通过 | 同一流程再次计数 14 | 14 |
| 05-01 | Basic_Delay_V2 | 通过 | 实际等待耗时 120~1800 ms | 165 |
| 05-02 | Basic_Delay_V2 | 通过 | 实际等待耗时 80~1800 ms | 122 |
| 05-03 | Basic_Delay_V1 | 通过 | 实际等待耗时 120~1800 ms | 154 |
| 05-04 | Basic_Delay | 通过 | 实际等待耗时 2800~6500 ms | 3011 |
| 05-05 | Basic_RandomInt | 通过 | 最小值等于最大值时返回整数 7 | 7 |
| 05-06 | Basic_RandomInt | 通过 | 返回 -2~2 的整数 | 2 |
| 05-07 | Basic_Rnd | 通过 | 随机小数满足 0 ≤ x < 1 | 0.18320588816502648 |
| 06-01 | System_GetTempPath | 通过 | 与系统临时目录一致(忽略尾部分隔符) | "C:\\Users\\Administrator\\AppData\\Local\\Temp" |
| 06-02 | System_GetHomePath | 通过 | 与 Windows 用户文件夹一致 | "C:\\Users\\Administrator" |
| 06-03 | System_GetSystemPath | 通过 | 与 Windows 实际桌面目录一致(包含重定向) | "C:\\Users\\Administrator\\Desktop" |
| 06-04 | System_GetEnv | 通过 | 读取测试专用变量的原始值,作为恢复依据 | "隔离环境原始值" |
| 06-05 | System_SetEnv | 通过 | 当前应用进程内设置成功 | "中文-测试-42" |
| 06-06 | System_GetEnv | 通过 | 能读回中文-测试-42 | "中文-测试-42" |
| 06-07 | System_SetEnv | 通过 | 恢复测试前读取值 | "隔离环境原始值" |
| 07-01 | System_ExecuteCommand | 通过 | RPA-PS-中文-OK | "RPA-PS-中文-OK" |
| 07-02 | System_ExecuteCommandV1 | 通过 | RPA-DOS-OK | "RPA-DOS-OK" |
| 07-03 | System_ExecuteCommandV1 | 通过 | RPA-PS-V1-OK | "RPA-PS-V1-OK" |
| 07-04 | System_ExecuteCommandV1 | 通过 | RPA-BAT-OK | "RPA-BAT-OK" |
| 07-05 | System_ExecuteCommand | 通过 | 非零退出码应抛出异常 | "桌面操作失败:Command failed: powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand JABQAHIAbwBnAHIAZQBzAHMAUAByAGUAZgBlAHIAZQBuAGMAZQA9ACcAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAnADsAIABbAEMAbwBuAHMAbwBsAGUAXQA6ADoATwB1AHQAcAB1AHQARQBuAGMAbwBkAGkAbgBnAD0AWwBTAHkAcwB0AGUAbQAuAFQAZQB4AHQALgBVAFQARgA4AEUAbgBjAG8AZABpAG4AZwBdADoAOgBuAGUAdwAoACQAZgBhAGwAcwBlACkAOwAgACQATwB1AHQAcAB1AHQARQBuAGMAbwBkAGkAbgBnAD0AWwBDAG8AbgBzAG8AbABlAF0AOgA6AE8AdQB0AHAAdQB0AEUAbgBjAG8AZABpAG4AZwA7ACAAVwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIAAnAFIAUABBAC0ARQBSAFIATwBSAC0AUABSAE8AQgBFACcAOwAgAGUAeABpAHQAIAA5AA==\n" |
| 08-01 | System_AddCredential | 阻塞 | 安全输入机制准备好后,在专用目标添加测试凭据 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 08-02 | System_ReadCredential | 阻塞 | 仅验证测试凭据存在,不读取或打印密码 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 08-03 | System_DeleteCredential | 阻塞 | 删除刚创建的测试目标,之后人工确认不存在 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 09-01 | System_GetInputMethodState | 待人工确认 | 读取前台窗口的真实输入法状态,人工对照任务栏 | 手动任务不纳入自动执行 |
| 09-02 | System_SetInputMethodState | 待人工确认 | 前台编辑器应切换到英文模式;仅子进程返回 set:true 不能算通过 | 手动任务不纳入自动执行 |
| 09-03 | System_SetInputMethodState | 待人工确认 | 恢复原输入法;人工确认前台窗口的实际状态 | 手动任务不纳入自动执行 |
| 10-01 | System_PlaySound | 待人工确认 | 听到一次短提示音;没有声音不能算通过 | 手动任务不纳入自动执行 |
| 11-01 | System_ScreenSaverShow | 待人工确认 | 屏幕实际进入屏保;无配置时记录阻塞 | 手动任务不纳入自动执行 |
| 11-02 | System_ScreenSaverStop | 待人工确认 | 屏保实际关闭,可正常操作 | 手动任务不纳入自动执行 |
| 11-03 | System_ScreenSaverShowV1 | 待人工确认 | 开启 Windows 已配置的屏保,不要求输入密码;人工确认屏幕实际变化 | 手动任务不纳入自动执行 |
| 11-04 | System_ScreenSaverStop | 待人工确认 | 第二种屏保实际关闭 | 手动任务不纳入自动执行 |
| 12-01 | ScreenControl_Screenlock | 阻塞 | 实际锁屏;需要人在机器旁用 Windows 登录凭据恢复,不改变账户密码 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 12-02 | ScreenControl_ScreenUnlock | 阻塞 | 仅尝试唤醒屏幕,loginUnlockSupported 必须为 false;Windows 登录仍需人工完成 | 运行前参数校验拦截,未绕过校验;详见校验问题 |
| 13-01 | System_ConnectRemoteDesktop | 待人工确认 | 人工完成登录,并确认已连接到正确测试机器;启动 mstsc 不等于连接成功 | 手动任务不纳入自动执行 |
| 13-02 | System_CloseRemoteDesktop | 待人工确认 | 确认目标会话及恢复方式后才能执行;当前实现会调用 tsdiscon | 手动任务不纳入自动执行 |
## 运行前校验阻塞
- test_appsystem_08:「System_AddCredential」缺少必填参数:用户名
- test_appsystem_12:「ScreenControl_Screenlock」缺少必填参数:用户名;「ScreenControl_Screenlock」缺少必填参数:密码;「ScreenControl_Screenlock」缺少必填参数:分辨率(宽);「ScreenControl_Screenlock」缺少必填参数:分辨率(高)
## 证据位置
隔离用户目录:D:\work\code\RPA-only-demo\test-output\app-system\isolated-PN2R7C
- test_appsystem_01:D:\work\code\RPA-only-demo\test-output\app-system\isolated-PN2R7C\runs\run_mvuuparb_ikwv\run.json
- test_appsystem_02:D:\work\code\RPA-only-demo\test-output\app-system\isolated-PN2R7C\runs\run_mvuupg4i_9qr9\run.json
- test_appsystem_03:D:\work\code\RPA-only-demo\test-output\app-system\isolated-PN2R7C\runs\run_mvuupg6q_6ij5\run.json
- test_appsystem_04:D:\work\code\RPA-only-demo\test-output\app-system\isolated-PN2R7C\runs\run_mvuupg9x_yifj\run.json
- test_appsystem_05:D:\work\code\RPA-only-demo\test-output\app-system\isolated-PN2R7C\runs\run_mvuupgcn_2wod\run.json
- test_appsystem_06:D:\work\code\RPA-only-demo\test-output\app-system\isolated-PN2R7C\runs\run_mvuupj2n_b65x\run.json
- test_appsystem_07:D:\work\code\RPA-only-demo\test-output\app-system\isolated-PN2R7C\runs\run_mvuupjiv_mh0i\run.json

Binary file not shown.

View File

@ -0,0 +1,861 @@
{
"schemaVersion": 1,
"generatedAt": "2026-10-30T00:00:00.000Z",
"groups": [
{
"id": "test_appsystem_01",
"file": "test_appsystem_01.json",
"name": "组件测试 01 · 应用与 PID",
"manual": false,
"components": [
"App_Run",
"App_GetPid",
"App_GetPids",
"App_Kill",
"App_KillV1"
],
"cases": [
{
"id": "01-01",
"componentId": "App_Run",
"targetNodeId": "t01_8",
"resultVariable": "case_1",
"input": "{\"executeable\":\"${fixture.exe}\"}",
"expected": "第一个测试进程 PID 大于 0",
"observe": false
},
{
"id": "01-02",
"componentId": "App_Run",
"targetNodeId": "t01_17",
"resultVariable": "case_2",
"input": "{\"executeable\":\"${fixture.exe}\"}",
"expected": "第二个 PID 大于 0,且与第一个不同",
"observe": false
},
{
"id": "01-03",
"componentId": "App_GetPid",
"targetNodeId": "t01_26",
"resultVariable": "case_3",
"input": "{\"pattern\":\"${fixture.name}.exe\"}",
"expected": "返回本次创建的任一 PID",
"observe": false
},
{
"id": "01-04",
"componentId": "App_GetPids",
"targetNodeId": "t01_35",
"resultVariable": "case_4",
"input": "{\"pattern\":\"${fixture.name}.exe\"}",
"expected": "列表恰含本次创建的两个 PID",
"observe": false
},
{
"id": "01-05",
"componentId": "App_Kill",
"targetNodeId": "t01_44",
"resultVariable": "case_5",
"input": "{\"pid\":\"${pid1}\"}",
"expected": "结束第一个夹具 PID,第二个仍在运行",
"observe": false
},
{
"id": "01-06",
"componentId": "App_KillV1",
"targetNodeId": "t01_55",
"resultVariable": "case_6",
"input": "{\"close_type\":\"id\",\"pid\":\"${pid2}\",\"kill_all\":\"0\"}",
"expected": "只填写 PID 即可结束第二个夹具进程",
"observe": false
},
{
"id": "01-07",
"componentId": "App_GetPids",
"targetNodeId": "t01_65",
"resultVariable": "case_7",
"input": "{\"pattern\":\"${fixture.name}.exe\"}",
"expected": "所有夹具结束后应返回空数组 []",
"observe": false
},
{
"id": "01-08",
"componentId": "App_GetPid",
"targetNodeId": "t01_74",
"resultVariable": "case_8",
"input": "{\"pattern\":\"${fixture.name}.exe\"}",
"expected": "无匹配时应返回 null",
"observe": false
}
]
},
{
"id": "test_appsystem_02",
"file": "test_appsystem_02.json",
"name": "组件测试 02 · 数据转换",
"manual": false,
"components": [
"Basic_StringToNum",
"Basic_ConvertNumber",
"Basic_ConvertInt",
"Basic_ConvertBool",
"Basic_ConvertString",
"Basic_DataTypeConversion",
"Basic_Dodecryption"
],
"cases": [
{
"id": "02-01",
"componentId": "Basic_StringToNum",
"targetNodeId": "t02_6",
"resultVariable": "case_1",
"input": "{\"val\":\"123.5\"}",
"expected": "数字 123.5",
"observe": false
},
{
"id": "02-02",
"componentId": "Basic_ConvertNumber",
"targetNodeId": "t02_15",
"resultVariable": "case_2",
"input": "{\"val\":\"-42.25\"}",
"expected": "数字 -42.25",
"observe": false
},
{
"id": "02-03",
"componentId": "Basic_ConvertInt",
"targetNodeId": "t02_24",
"resultVariable": "case_3",
"input": "{\"val\":-3.9}",
"expected": "整数 -3(向零截断)",
"observe": false
},
{
"id": "02-04",
"componentId": "Basic_ConvertBool",
"targetNodeId": "t02_33",
"resultVariable": "case_4",
"input": "{\"val\":\"true\"}",
"expected": "布尔 true",
"observe": false
},
{
"id": "02-05",
"componentId": "Basic_ConvertBool",
"targetNodeId": "t02_42",
"resultVariable": "case_5",
"input": "{\"val\":\"false\"}",
"expected": "布尔 false",
"observe": false
},
{
"id": "02-06",
"componentId": "Basic_ConvertString",
"targetNodeId": "t02_51",
"resultVariable": "case_6",
"input": "{\"val\":42}",
"expected": "文本 \"42\"",
"observe": false
},
{
"id": "02-07",
"componentId": "Basic_DataTypeConversion",
"targetNodeId": "t02_60",
"resultVariable": "case_7",
"input": "{\"val\":\"42\",\"into_type\":\"number\"}",
"expected": "数值 42,严格区分文本和数值",
"observe": false
},
{
"id": "02-08",
"componentId": "Basic_DataTypeConversion",
"targetNodeId": "t02_69",
"resultVariable": "case_8",
"input": "{\"val\":\"-3.9\",\"into_type\":\"int\"}",
"expected": "整数 -3",
"observe": false
},
{
"id": "02-09",
"componentId": "Basic_DataTypeConversion",
"targetNodeId": "t02_78",
"resultVariable": "case_9",
"input": "{\"val\":\"false\",\"into_type\":\"bool\"}",
"expected": "布尔 false",
"observe": false
},
{
"id": "02-10",
"componentId": "Basic_DataTypeConversion",
"targetNodeId": "t02_87",
"resultVariable": "case_10",
"input": "{\"val\":42,\"into_type\":\"string\"}",
"expected": "文本 \"42\"",
"observe": false
},
{
"id": "02-11",
"componentId": "Basic_Dodecryption",
"targetNodeId": "t02_96",
"resultVariable": "case_11",
"input": "{\"words\":\"UlBBLURvZGVjcnlwdGlvbg==\"}",
"expected": "Base64 解码 \"RPA-Dodecryption\"",
"observe": false
}
]
},
{
"id": "test_appsystem_03",
"file": "test_appsystem_03.json",
"name": "组件测试 03 · 数据判断",
"manual": false,
"components": [
"Basic_IsDictionary",
"Basic_IsNumber",
"Basic_IsNull",
"Basic_IsArray",
"Basic_Type",
"Basic_JudgeDataType"
],
"cases": [
{
"id": "03-01",
"componentId": "Basic_IsDictionary",
"targetNodeId": "t03_6",
"resultVariable": "case_1",
"input": "{\"val\":{\"name\":\"测试\"}}",
"expected": "true",
"observe": false
},
{
"id": "03-02",
"componentId": "Basic_IsDictionary",
"targetNodeId": "t03_15",
"resultVariable": "case_2",
"input": "{\"val\":[]}",
"expected": "false",
"observe": false
},
{
"id": "03-03",
"componentId": "Basic_IsNumber",
"targetNodeId": "t03_24",
"resultVariable": "case_3",
"input": "{\"val\":\"12.5\"}",
"expected": "true",
"observe": false
},
{
"id": "03-04",
"componentId": "Basic_IsNumber",
"targetNodeId": "t03_33",
"resultVariable": "case_4",
"input": "{\"val\":\"中文\"}",
"expected": "false",
"observe": false
},
{
"id": "03-05",
"componentId": "Basic_IsNull",
"targetNodeId": "t03_42",
"resultVariable": "case_5",
"input": "{\"val\":null}",
"expected": "true",
"observe": false
},
{
"id": "03-06",
"componentId": "Basic_IsNull",
"targetNodeId": "t03_51",
"resultVariable": "case_6",
"input": "{\"val\":\"\"}",
"expected": "true",
"observe": false
},
{
"id": "03-07",
"componentId": "Basic_IsNull",
"targetNodeId": "t03_60",
"resultVariable": "case_7",
"input": "{\"val\":0}",
"expected": "false",
"observe": false
},
{
"id": "03-08",
"componentId": "Basic_IsArray",
"targetNodeId": "t03_69",
"resultVariable": "case_8",
"input": "{\"val\":[]}",
"expected": "true",
"observe": false
},
{
"id": "03-09",
"componentId": "Basic_IsArray",
"targetNodeId": "t03_78",
"resultVariable": "case_9",
"input": "{\"val\":\"[]\"}",
"expected": "false",
"observe": false
},
{
"id": "03-10",
"componentId": "Basic_Type",
"targetNodeId": "t03_87",
"resultVariable": "case_10",
"input": "{\"val\":[1,2]}",
"expected": "\"list\"",
"observe": false
},
{
"id": "03-11",
"componentId": "Basic_Type",
"targetNodeId": "t03_96",
"resultVariable": "case_11",
"input": "{\"val\":null}",
"expected": "\"null\"",
"observe": false
},
{
"id": "03-12",
"componentId": "Basic_JudgeDataType",
"targetNodeId": "t03_105",
"resultVariable": "case_12",
"input": "{\"val\":42,\"into_type\":\"number\"}",
"expected": "数字 42 属于数值类型,返回布尔 true",
"observe": false
}
]
},
{
"id": "test_appsystem_04",
"file": "test_appsystem_04.json",
"name": "组件测试 04 · 变量与配置",
"manual": false,
"components": [
"Basic_SetVariable",
"Basic_SetVariable_V1",
"Basic_Clone",
"Basic_GetProcessInfo",
"Basic_DebugOutput",
"CustomConfig_GetConfig",
"Basic_BusinessOrderCount"
],
"cases": [
{
"id": "04-01",
"componentId": "Basic_SetVariable",
"targetNodeId": "t04_6",
"resultVariable": "case_1",
"input": "{\"value\":\"中文变量往返\"}",
"expected": "原值及输出变量均保持中文文本",
"observe": false
},
{
"id": "04-02",
"componentId": "Basic_SetVariable_V1",
"targetNodeId": "t04_15",
"resultVariable": "case_2",
"input": "{\"variableType\":\"rpa_int\",\"value\":\"42\"}",
"expected": "数字 42",
"observe": false
},
{
"id": "04-03",
"componentId": "Basic_SetVariable_V1",
"targetNodeId": "t04_24",
"resultVariable": "case_3",
"input": "{\"variableType\":\"rpa_list\",\"value\":\"[1,2]\"}",
"expected": "数组 [1,2]",
"observe": false
},
{
"id": "04-04",
"componentId": "Basic_Clone",
"targetNodeId": "t04_34",
"resultVariable": "case_4",
"input": "{\"val\":\"${original}\"}",
"expected": "深复制内容相同,外层和嵌套对象均不共用引用",
"observe": false
},
{
"id": "04-05",
"componentId": "Basic_GetProcessInfo",
"targetNodeId": "t04_43",
"resultVariable": "case_5",
"input": "{}",
"expected": "当前流程 ID、名称、运行 ID 和节点数一致",
"observe": false
},
{
"id": "04-06",
"componentId": "Basic_DebugOutput",
"targetNodeId": "t04_52",
"resultVariable": "case_6",
"input": "{\"msg\":\"中文日志测试:${actual_2}\"}",
"expected": "日志及返回值为“中文日志测试:42”",
"observe": false
},
{
"id": "04-07",
"componentId": "CustomConfig_GetConfig",
"targetNodeId": "t04_61",
"resultVariable": "case_7",
"input": "{\"jsonlist\":\"[{\\\"name\\\":\\\"timeout\\\",\\\"value\\\":30}]\",\"configName\":\"timeout\"}",
"expected": "数字 30",
"observe": false
},
{
"id": "04-08",
"componentId": "CustomConfig_GetConfig",
"targetNodeId": "t04_70",
"resultVariable": "case_8",
"input": "{\"jsonlist\":\"[]\",\"configName\":\"missing\"}",
"expected": "未命中返回 null",
"observe": false
},
{
"id": "04-09",
"componentId": "Basic_BusinessOrderCount",
"targetNodeId": "t04_79",
"resultVariable": "case_9",
"input": "{\"start_count\":10,\"count\":2}",
"expected": "首次计数 12",
"observe": false
},
{
"id": "04-10",
"componentId": "Basic_BusinessOrderCount",
"targetNodeId": "t04_88",
"resultVariable": "case_10",
"input": "{\"start_count\":10,\"count\":2}",
"expected": "同一流程再次计数 14",
"observe": false
}
]
},
{
"id": "test_appsystem_05",
"file": "test_appsystem_05.json",
"name": "组件测试 05 · 等待与随机数",
"manual": false,
"components": [
"Basic_Delay_V2",
"Basic_Delay_V1",
"Basic_Delay",
"Basic_RandomInt",
"Basic_Rnd"
],
"cases": [
{
"id": "05-01",
"componentId": "Basic_Delay_V2",
"targetNodeId": "t05_7",
"resultVariable": "case_1",
"input": "{\"is_random\":\"0\",\"second\":\"0.15\",\"min_second\":\"0.1\",\"max_second\":\"0.2\"}",
"expected": "实际等待耗时 120~1800 ms",
"observe": false
},
{
"id": "05-02",
"componentId": "Basic_Delay_V2",
"targetNodeId": "t05_18",
"resultVariable": "case_2",
"input": "{\"is_random\":\"1\",\"second\":\"0.15\",\"min_second\":\"0.1\",\"max_second\":\"0.2\"}",
"expected": "实际等待耗时 80~1800 ms",
"observe": false
},
{
"id": "05-03",
"componentId": "Basic_Delay_V1",
"targetNodeId": "t05_29",
"resultVariable": "case_3",
"input": "{\"second\":\"0.15\"}",
"expected": "实际等待耗时 120~1800 ms",
"observe": false
},
{
"id": "05-04",
"componentId": "Basic_Delay",
"targetNodeId": "t05_40",
"resultVariable": "case_4",
"input": "{\"mileSecond\":\"3000\"}",
"expected": "实际等待耗时 2800~6500 ms",
"observe": false
},
{
"id": "05-05",
"componentId": "Basic_RandomInt",
"targetNodeId": "t05_50",
"resultVariable": "case_5",
"input": "{\"min\":7,\"max\":7}",
"expected": "最小值等于最大值时返回整数 7",
"observe": false
},
{
"id": "05-06",
"componentId": "Basic_RandomInt",
"targetNodeId": "t05_59",
"resultVariable": "case_6",
"input": "{\"min\":-2,\"max\":2}",
"expected": "返回 -2~2 的整数",
"observe": false
},
{
"id": "05-07",
"componentId": "Basic_Rnd",
"targetNodeId": "t05_68",
"resultVariable": "case_7",
"input": "{}",
"expected": "随机小数满足 0 ≤ x < 1",
"observe": false
}
]
},
{
"id": "test_appsystem_06",
"file": "test_appsystem_06.json",
"name": "组件测试 06 · 系统路径与环境变量",
"manual": false,
"components": [
"System_GetTempPath",
"System_GetHomePath",
"System_GetSystemPath",
"System_GetEnv",
"System_SetEnv"
],
"cases": [
{
"id": "06-01",
"componentId": "System_GetTempPath",
"targetNodeId": "t06_8",
"resultVariable": "case_1",
"input": "{}",
"expected": "与系统临时目录一致(忽略尾部分隔符)",
"observe": false
},
{
"id": "06-02",
"componentId": "System_GetHomePath",
"targetNodeId": "t06_17",
"resultVariable": "case_2",
"input": "{}",
"expected": "与 Windows 用户文件夹一致",
"observe": false
},
{
"id": "06-03",
"componentId": "System_GetSystemPath",
"targetNodeId": "t06_26",
"resultVariable": "case_3",
"input": "{\"path_name\":\"desktop\"}",
"expected": "与 Windows 实际桌面目录一致(包含重定向)",
"observe": false
},
{
"id": "06-04",
"componentId": "System_GetEnv",
"targetNodeId": "t06_35",
"resultVariable": "case_4",
"input": "{\"name\":\"RPA_APP_SYSTEM_TEST_VALUE\"}",
"expected": "读取测试专用变量的原始值,作为恢复依据",
"observe": false
},
{
"id": "06-05",
"componentId": "System_SetEnv",
"targetNodeId": "t06_44",
"resultVariable": "case_5",
"input": "{\"name\":\"RPA_APP_SYSTEM_TEST_VALUE\",\"value\":\"中文-测试-42\"}",
"expected": "当前应用进程内设置成功",
"observe": false
},
{
"id": "06-06",
"componentId": "System_GetEnv",
"targetNodeId": "t06_53",
"resultVariable": "case_6",
"input": "{\"name\":\"RPA_APP_SYSTEM_TEST_VALUE\"}",
"expected": "能读回中文-测试-42",
"observe": false
},
{
"id": "06-07",
"componentId": "System_SetEnv",
"targetNodeId": "t06_62",
"resultVariable": "case_7",
"input": "{\"name\":\"RPA_APP_SYSTEM_TEST_VALUE\",\"value\":\"${originalEnv}\"}",
"expected": "恢复测试前读取值",
"observe": false
}
]
},
{
"id": "test_appsystem_07",
"file": "test_appsystem_07.json",
"name": "组件测试 07 · 命令行",
"manual": false,
"components": [
"System_ExecuteCommand",
"System_ExecuteCommandV1"
],
"cases": [
{
"id": "07-01",
"componentId": "System_ExecuteCommand",
"targetNodeId": "t07_6",
"resultVariable": "case_1",
"input": "{\"action\":\"Write-Output 'RPA-PS-中文-OK'\"}",
"expected": "RPA-PS-中文-OK",
"observe": false
},
{
"id": "07-02",
"componentId": "System_ExecuteCommandV1",
"targetNodeId": "t07_15",
"resultVariable": "case_2",
"input": "{\"command_type\":\"1\",\"dos_command\":\"echo RPA-DOS-OK\",\"get_ret\":\"1\"}",
"expected": "RPA-DOS-OK",
"observe": false
},
{
"id": "07-03",
"componentId": "System_ExecuteCommandV1",
"targetNodeId": "t07_24",
"resultVariable": "case_3",
"input": "{\"command_type\":\"3\",\"ps_command\":\"Write-Output 'RPA-PS-V1-OK'\",\"get_ret\":\"1\"}",
"expected": "RPA-PS-V1-OK",
"observe": false
},
{
"id": "07-04",
"componentId": "System_ExecuteCommandV1",
"targetNodeId": "t07_34",
"resultVariable": "case_4",
"input": "{\"command_type\":\"2\",\"bat_file\":\"${batPath}\",\"get_ret\":\"1\"}",
"expected": "RPA-BAT-OK",
"observe": false
},
{
"id": "07-05",
"componentId": "System_ExecuteCommand",
"targetNodeId": "t07_43",
"resultVariable": "case_5",
"input": "{\"action\":\"Write-Output 'RPA-ERROR-PROBE'; exit 9\"}",
"expected": "非零退出码应抛出异常",
"observe": false
}
]
},
{
"id": "test_appsystem_08",
"file": "test_appsystem_08.json",
"name": "组件测试 08 · Windows 凭据【手动】",
"manual": true,
"components": [
"System_AddCredential",
"System_ReadCredential",
"System_DeleteCredential"
],
"cases": [
{
"id": "08-01",
"componentId": "System_AddCredential",
"targetNodeId": "t08_10",
"resultVariable": "case_1",
"input": "{\"application\":\"${credentialTarget}\",\"name\":\"\",\"password\":\"\"}",
"expected": "安全输入机制准备好后,在专用目标添加测试凭据",
"observe": true
},
{
"id": "08-02",
"componentId": "System_ReadCredential",
"targetNodeId": "t08_21",
"resultVariable": "case_2",
"input": "{\"application\":\"${credentialTarget}\"}",
"expected": "仅验证测试凭据存在,不读取或打印密码",
"observe": true
},
{
"id": "08-03",
"componentId": "System_DeleteCredential",
"targetNodeId": "t08_32",
"resultVariable": "case_3",
"input": "{\"application\":\"${credentialTarget}\"}",
"expected": "删除刚创建的测试目标,之后人工确认不存在",
"observe": true
}
]
},
{
"id": "test_appsystem_09",
"file": "test_appsystem_09.json",
"name": "组件测试 09 · 输入法【手动】",
"manual": true,
"components": [
"System_GetInputMethodState",
"System_SetInputMethodState"
],
"cases": [
{
"id": "09-01",
"componentId": "System_GetInputMethodState",
"targetNodeId": "t09_9",
"resultVariable": "case_1",
"input": "{}",
"expected": "读取前台窗口的真实输入法状态,人工对照任务栏",
"observe": true
},
{
"id": "09-02",
"componentId": "System_SetInputMethodState",
"targetNodeId": "t09_20",
"resultVariable": "case_2",
"input": "{\"mode\":\"en-US\"}",
"expected": "前台编辑器应切换到英文模式;仅子进程返回 set:true 不能算通过",
"observe": true
},
{
"id": "09-03",
"componentId": "System_SetInputMethodState",
"targetNodeId": "t09_31",
"resultVariable": "case_3",
"input": "{\"mode\":\"${originalIme.culture}\"}",
"expected": "恢复原输入法;人工确认前台窗口的实际状态",
"observe": true
}
]
},
{
"id": "test_appsystem_10",
"file": "test_appsystem_10.json",
"name": "组件测试 10 · 音频【手动】",
"manual": true,
"components": [
"System_PlaySound"
],
"cases": [
{
"id": "10-01",
"componentId": "System_PlaySound",
"targetNodeId": "t10_10",
"resultVariable": "case_1",
"input": "{\"audio\":\"${audioPath}\"}",
"expected": "听到一次短提示音;没有声音不能算通过",
"observe": true
}
]
},
{
"id": "test_appsystem_11",
"file": "test_appsystem_11.json",
"name": "组件测试 11 · 屏保【手动】",
"manual": true,
"components": [
"System_ScreenSaverShow",
"System_ScreenSaverShowV1",
"System_ScreenSaverStop"
],
"cases": [
{
"id": "11-01",
"componentId": "System_ScreenSaverShow",
"targetNodeId": "t11_9",
"resultVariable": "case_1",
"input": "{\"text\":\"RPA 测试\",\"status\":\"1\"}",
"expected": "屏幕实际进入屏保;无配置时记录阻塞",
"observe": true
},
{
"id": "11-02",
"componentId": "System_ScreenSaverStop",
"targetNodeId": "t11_20",
"resultVariable": "case_2",
"input": "{}",
"expected": "屏保实际关闭,可正常操作",
"observe": true
},
{
"id": "11-03",
"componentId": "System_ScreenSaverShowV1",
"targetNodeId": "t11_31",
"resultVariable": "case_3",
"input": "{}",
"expected": "开启 Windows 已配置的屏保,不要求输入密码;人工确认屏幕实际变化",
"observe": true
},
{
"id": "11-04",
"componentId": "System_ScreenSaverStop",
"targetNodeId": "t11_42",
"resultVariable": "case_4",
"input": "{}",
"expected": "第二种屏保实际关闭",
"observe": true
}
]
},
{
"id": "test_appsystem_12",
"file": "test_appsystem_12.json",
"name": "组件测试 12 · 锁屏与解锁【手动】",
"manual": true,
"components": [
"ScreenControl_Screenlock",
"ScreenControl_ScreenUnlock"
],
"cases": [
{
"id": "12-01",
"componentId": "ScreenControl_Screenlock",
"targetNodeId": "t12_9",
"resultVariable": "case_1",
"input": "{\"username\":\"\",\"pwd\":\"\",\"width\":\"\",\"height\":\"\",\"scale_factor\":\"100\"}",
"expected": "实际锁屏;需要人在机器旁用 Windows 登录凭据恢复,不改变账户密码",
"observe": true
},
{
"id": "12-02",
"componentId": "ScreenControl_ScreenUnlock",
"targetNodeId": "t12_20",
"resultVariable": "case_2",
"input": "{}",
"expected": "仅尝试唤醒屏幕,loginUnlockSupported 必须为 false;Windows 登录仍需人工完成",
"observe": true
}
]
},
{
"id": "test_appsystem_13",
"file": "test_appsystem_13.json",
"name": "组件测试 13 · 远程桌面【手动】",
"manual": true,
"components": [
"System_ConnectRemoteDesktop",
"System_CloseRemoteDesktop"
],
"cases": [
{
"id": "13-01",
"componentId": "System_ConnectRemoteDesktop",
"targetNodeId": "t13_11",
"resultVariable": "case_1",
"input": "{\"ip\":\"${remoteAddress}\",\"port\":\"3389\"}",
"expected": "人工完成登录,并确认已连接到正确测试机器;启动 mstsc 不等于连接成功",
"observe": true
},
{
"id": "13-02",
"componentId": "System_CloseRemoteDesktop",
"targetNodeId": "t13_22",
"resultVariable": "case_2",
"input": "{}",
"expected": "确认目标会话及恢复方式后才能执行;当前实现会调用 tsdiscon",
"observe": true
}
]
}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,897 @@
{
"schemaVersion": 1,
"id": "test_appsystem_08",
"name": "组件测试 08 · Windows 凭据【手动】",
"description": "手动测试:先阅读测试说明。默认不开启执行开关,不能把组件返回成功当作功能通过。",
"enabled": true,
"trigger": {
"type": "manual",
"label": "手动运行"
},
"variables": [],
"nodes": [
{
"id": "t08_1",
"componentId": "Trigger.Manual",
"label": "人工发起",
"position": {
"x": 220,
"y": 80
},
"values": {
"entryName": "开始组件测试"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t08_2",
"componentId": "Basic_SetVariable",
"label": "初始化失败计数",
"position": {
"x": 220,
"y": 222
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
}
},
{
"id": "t08_3",
"componentId": "Basic_SetVariable",
"label": "初始化待确认计数",
"position": {
"x": 220,
"y": 364
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
}
},
{
"id": "t08_4",
"componentId": "Basic_SetVariable",
"label": "初始化阻塞计数",
"position": {
"x": 220,
"y": 506
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "blocked"
}
},
{
"id": "t08_5",
"componentId": "Basic_SetVariable",
"label": "手动执行开关:确认环境后才可改为 true",
"position": {
"x": 220,
"y": 648
},
"values": {
"value": false
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "manualEnabled"
}
},
{
"id": "t08_6",
"componentId": "Basic_SetVariable",
"label": "只允许使用新的测试凭据目标",
"position": {
"x": 220,
"y": 790
},
"values": {
"value": "RPA-AppSystem-Tests-${now()}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "credentialTarget"
}
},
{
"id": "t08_7",
"componentId": "logic_control_try_catch",
"label": "08-01 · System_AddCredential",
"position": {
"x": 220,
"y": 932
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t08_8",
"componentId": "logic_control_if_else",
"label": "手动执行保护",
"position": {
"x": 220,
"y": 1340
},
"values": {
"condition": "${manualEnabled === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t08_7",
"slot": "try_fn"
},
{
"id": "t08_9",
"componentId": "ExceptionUtil_Throws",
"label": "默认不执行系统状态操作",
"position": {
"x": 220,
"y": 1340
},
"values": {
"msg": "待人工确认:执行开关未开启"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t08_8",
"slot": "statement_else"
},
{
"id": "t08_10",
"componentId": "System_AddCredential",
"label": "System_AddCredential",
"position": {
"x": 220,
"y": 1340
},
"values": {
"application": "${credentialTarget}",
"name": "",
"password": ""
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "actual_1"
},
"parentId": "t08_8",
"slot": "statement_if"
},
{
"id": "t08_11",
"componentId": "Basic_SetVariable",
"label": "记录实际值与判定",
"position": {
"x": 220,
"y": 1340
},
"values": {
"value": "${[\"08-01\", \"System_AddCredential\", \"MANUAL\", \"{\\\"application\\\":\\\"${credentialTarget}\\\",\\\"name\\\":\\\"\\\",\\\"password\\\":\\\"\\\"}\", \"安全输入机制准备好后,在专用目标添加测试凭据\", actual_1]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_1"
},
"parentId": "t08_8",
"slot": "statement_if"
},
{
"id": "t08_12",
"componentId": "Basic_SetVariable",
"label": "累计失败数",
"position": {
"x": 220,
"y": 1340
},
"values": {
"value": "${failures + (case_1[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t08_8",
"slot": "statement_if"
},
{
"id": "t08_13",
"componentId": "Basic_SetVariable",
"label": "累计待人工确认数",
"position": {
"x": 220,
"y": 1340
},
"values": {
"value": "${pending + (case_1[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t08_8",
"slot": "statement_if"
},
{
"id": "t08_14",
"componentId": "Basic_SetVariable",
"label": "记录异常结果",
"position": {
"x": 220,
"y": 1340
},
"values": {
"value": "${[\"08-01\", \"System_AddCredential\", manualEnabled === true ? \"FAIL\" : \"MANUAL\", \"{\\\"application\\\":\\\"${credentialTarget}\\\",\\\"name\\\":\\\"\\\",\\\"password\\\":\\\"\\\"}\", \"安全输入机制准备好后,在专用目标添加测试凭据\", error.reason]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_1"
},
"parentId": "t08_7",
"slot": "except_fn"
},
{
"id": "t08_15",
"componentId": "Basic_SetVariable",
"label": "异常累计失败数",
"position": {
"x": 220,
"y": 1340
},
"values": {
"value": "${failures + (case_1[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t08_7",
"slot": "except_fn"
},
{
"id": "t08_16",
"componentId": "Basic_SetVariable",
"label": "异常累计待确认数",
"position": {
"x": 220,
"y": 1340
},
"values": {
"value": "${pending + (case_1[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t08_7",
"slot": "except_fn"
},
{
"id": "t08_17",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 1436
},
"values": {
"msg": "用例结果:${json(case_1)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t08_18",
"componentId": "logic_control_try_catch",
"label": "08-02 · System_ReadCredential",
"position": {
"x": 220,
"y": 1578
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t08_19",
"componentId": "logic_control_if_else",
"label": "手动执行保护",
"position": {
"x": 220,
"y": 1700
},
"values": {
"condition": "${manualEnabled === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t08_18",
"slot": "try_fn"
},
{
"id": "t08_20",
"componentId": "ExceptionUtil_Throws",
"label": "默认不执行系统状态操作",
"position": {
"x": 220,
"y": 1700
},
"values": {
"msg": "待人工确认:执行开关未开启"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t08_19",
"slot": "statement_else"
},
{
"id": "t08_21",
"componentId": "System_ReadCredential",
"label": "System_ReadCredential",
"position": {
"x": 220,
"y": 1700
},
"values": {
"application": "${credentialTarget}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "actual_2"
},
"parentId": "t08_19",
"slot": "statement_if"
},
{
"id": "t08_22",
"componentId": "Basic_SetVariable",
"label": "记录实际值与判定",
"position": {
"x": 220,
"y": 1700
},
"values": {
"value": "${[\"08-02\", \"System_ReadCredential\", \"MANUAL\", \"{\\\"application\\\":\\\"${credentialTarget}\\\"}\", \"仅验证测试凭据存在,不读取或打印密码\", actual_2]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_2"
},
"parentId": "t08_19",
"slot": "statement_if"
},
{
"id": "t08_23",
"componentId": "Basic_SetVariable",
"label": "累计失败数",
"position": {
"x": 220,
"y": 1700
},
"values": {
"value": "${failures + (case_2[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t08_19",
"slot": "statement_if"
},
{
"id": "t08_24",
"componentId": "Basic_SetVariable",
"label": "累计待人工确认数",
"position": {
"x": 220,
"y": 1700
},
"values": {
"value": "${pending + (case_2[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t08_19",
"slot": "statement_if"
},
{
"id": "t08_25",
"componentId": "Basic_SetVariable",
"label": "记录异常结果",
"position": {
"x": 220,
"y": 1700
},
"values": {
"value": "${[\"08-02\", \"System_ReadCredential\", manualEnabled === true ? \"FAIL\" : \"MANUAL\", \"{\\\"application\\\":\\\"${credentialTarget}\\\"}\", \"仅验证测试凭据存在,不读取或打印密码\", error.reason]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_2"
},
"parentId": "t08_18",
"slot": "except_fn"
},
{
"id": "t08_26",
"componentId": "Basic_SetVariable",
"label": "异常累计失败数",
"position": {
"x": 220,
"y": 1700
},
"values": {
"value": "${failures + (case_2[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t08_18",
"slot": "except_fn"
},
{
"id": "t08_27",
"componentId": "Basic_SetVariable",
"label": "异常累计待确认数",
"position": {
"x": 220,
"y": 1700
},
"values": {
"value": "${pending + (case_2[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t08_18",
"slot": "except_fn"
},
{
"id": "t08_28",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2082
},
"values": {
"msg": "用例结果:${json(case_2)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t08_29",
"componentId": "logic_control_try_catch",
"label": "08-03 · System_DeleteCredential",
"position": {
"x": 220,
"y": 2224
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t08_30",
"componentId": "logic_control_if_else",
"label": "手动执行保护",
"position": {
"x": 220,
"y": 2060
},
"values": {
"condition": "${manualEnabled === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t08_29",
"slot": "try_fn"
},
{
"id": "t08_31",
"componentId": "ExceptionUtil_Throws",
"label": "默认不执行系统状态操作",
"position": {
"x": 220,
"y": 2060
},
"values": {
"msg": "待人工确认:执行开关未开启"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t08_30",
"slot": "statement_else"
},
{
"id": "t08_32",
"componentId": "System_DeleteCredential",
"label": "System_DeleteCredential",
"position": {
"x": 220,
"y": 2060
},
"values": {
"application": "${credentialTarget}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "actual_3"
},
"parentId": "t08_30",
"slot": "statement_if"
},
{
"id": "t08_33",
"componentId": "Basic_SetVariable",
"label": "记录实际值与判定",
"position": {
"x": 220,
"y": 2060
},
"values": {
"value": "${[\"08-03\", \"System_DeleteCredential\", \"MANUAL\", \"{\\\"application\\\":\\\"${credentialTarget}\\\"}\", \"删除刚创建的测试目标,之后人工确认不存在\", actual_3]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_3"
},
"parentId": "t08_30",
"slot": "statement_if"
},
{
"id": "t08_34",
"componentId": "Basic_SetVariable",
"label": "累计失败数",
"position": {
"x": 220,
"y": 2060
},
"values": {
"value": "${failures + (case_3[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t08_30",
"slot": "statement_if"
},
{
"id": "t08_35",
"componentId": "Basic_SetVariable",
"label": "累计待人工确认数",
"position": {
"x": 220,
"y": 2060
},
"values": {
"value": "${pending + (case_3[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t08_30",
"slot": "statement_if"
},
{
"id": "t08_36",
"componentId": "Basic_SetVariable",
"label": "记录异常结果",
"position": {
"x": 220,
"y": 2060
},
"values": {
"value": "${[\"08-03\", \"System_DeleteCredential\", manualEnabled === true ? \"FAIL\" : \"MANUAL\", \"{\\\"application\\\":\\\"${credentialTarget}\\\"}\", \"删除刚创建的测试目标,之后人工确认不存在\", error.reason]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_3"
},
"parentId": "t08_29",
"slot": "except_fn"
},
{
"id": "t08_37",
"componentId": "Basic_SetVariable",
"label": "异常累计失败数",
"position": {
"x": 220,
"y": 2060
},
"values": {
"value": "${failures + (case_3[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t08_29",
"slot": "except_fn"
},
{
"id": "t08_38",
"componentId": "Basic_SetVariable",
"label": "异常累计待确认数",
"position": {
"x": 220,
"y": 2060
},
"values": {
"value": "${pending + (case_3[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t08_29",
"slot": "except_fn"
},
{
"id": "t08_39",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2728
},
"values": {
"msg": "用例结果:${json(case_3)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t08_40",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2870
},
"values": {
"msg": "本组汇总:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t08_41",
"componentId": "logic_control_if_else",
"label": "汇总判定",
"position": {
"x": 220,
"y": 3012
},
"values": {
"condition": "${failures > 0 || pending > 0 || blocked > 0}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t08_42",
"componentId": "ExceptionUtil_Throws",
"label": "如实标记未通过",
"position": {
"x": 220,
"y": 2600
},
"values": {
"msg": "组件测试未全部通过:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。请查看日志。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t08_41",
"slot": "statement_if"
},
{
"id": "t08_43",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2600
},
"values": {
"msg": "本组全部通过。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t08_41",
"slot": "statement_else"
}
],
"edges": [
{
"id": "e_t08_2",
"source": "t08_1",
"target": "t08_2"
},
{
"id": "e_t08_3",
"source": "t08_2",
"target": "t08_3"
},
{
"id": "e_t08_4",
"source": "t08_3",
"target": "t08_4"
},
{
"id": "e_t08_5",
"source": "t08_4",
"target": "t08_5"
},
{
"id": "e_t08_6",
"source": "t08_5",
"target": "t08_6"
},
{
"id": "e_t08_7",
"source": "t08_6",
"target": "t08_7"
},
{
"id": "e_t08_17",
"source": "t08_7",
"target": "t08_17"
},
{
"id": "e_t08_18",
"source": "t08_17",
"target": "t08_18"
},
{
"id": "e_t08_28",
"source": "t08_18",
"target": "t08_28"
},
{
"id": "e_t08_29",
"source": "t08_28",
"target": "t08_29"
},
{
"id": "e_t08_39",
"source": "t08_29",
"target": "t08_39"
},
{
"id": "e_t08_40",
"source": "t08_39",
"target": "t08_40"
},
{
"id": "e_t08_41",
"source": "t08_40",
"target": "t08_41"
}
],
"meta": {
"createdAt": "2026-10-30T00:00:00.000Z",
"updatedAt": "2026-10-30T00:00:00.000Z",
"appVersion": "0.1.0"
}
}

View File

@ -0,0 +1,870 @@
{
"schemaVersion": 1,
"id": "test_appsystem_09",
"name": "组件测试 09 · 输入法【手动】",
"description": "手动测试:先阅读测试说明。默认不开启执行开关,不能把组件返回成功当作功能通过。",
"enabled": true,
"trigger": {
"type": "manual",
"label": "手动运行"
},
"variables": [],
"nodes": [
{
"id": "t09_1",
"componentId": "Trigger.Manual",
"label": "人工发起",
"position": {
"x": 220,
"y": 80
},
"values": {
"entryName": "开始组件测试"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t09_2",
"componentId": "Basic_SetVariable",
"label": "初始化失败计数",
"position": {
"x": 220,
"y": 222
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
}
},
{
"id": "t09_3",
"componentId": "Basic_SetVariable",
"label": "初始化待确认计数",
"position": {
"x": 220,
"y": 364
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
}
},
{
"id": "t09_4",
"componentId": "Basic_SetVariable",
"label": "初始化阻塞计数",
"position": {
"x": 220,
"y": 506
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "blocked"
}
},
{
"id": "t09_5",
"componentId": "Basic_SetVariable",
"label": "手动执行开关:确认环境后才可改为 true",
"position": {
"x": 220,
"y": 648
},
"values": {
"value": false
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "manualEnabled"
}
},
{
"id": "t09_6",
"componentId": "logic_control_try_catch",
"label": "09-01 · System_GetInputMethodState",
"position": {
"x": 220,
"y": 790
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t09_7",
"componentId": "logic_control_if_else",
"label": "手动执行保护",
"position": {
"x": 220,
"y": 1160
},
"values": {
"condition": "${manualEnabled === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t09_6",
"slot": "try_fn"
},
{
"id": "t09_8",
"componentId": "ExceptionUtil_Throws",
"label": "默认不执行系统状态操作",
"position": {
"x": 220,
"y": 1160
},
"values": {
"msg": "待人工确认:执行开关未开启"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t09_7",
"slot": "statement_else"
},
{
"id": "t09_9",
"componentId": "System_GetInputMethodState",
"label": "System_GetInputMethodState",
"position": {
"x": 220,
"y": 1160
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "originalIme"
},
"parentId": "t09_7",
"slot": "statement_if"
},
{
"id": "t09_10",
"componentId": "Basic_SetVariable",
"label": "记录实际值与判定",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${[\"09-01\", \"System_GetInputMethodState\", \"MANUAL\", \"{}\", \"读取前台窗口的真实输入法状态,人工对照任务栏\", originalIme]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_1"
},
"parentId": "t09_7",
"slot": "statement_if"
},
{
"id": "t09_11",
"componentId": "Basic_SetVariable",
"label": "累计失败数",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${failures + (case_1[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t09_7",
"slot": "statement_if"
},
{
"id": "t09_12",
"componentId": "Basic_SetVariable",
"label": "累计待人工确认数",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${pending + (case_1[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t09_7",
"slot": "statement_if"
},
{
"id": "t09_13",
"componentId": "Basic_SetVariable",
"label": "记录异常结果",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${[\"09-01\", \"System_GetInputMethodState\", manualEnabled === true ? \"FAIL\" : \"MANUAL\", \"{}\", \"读取前台窗口的真实输入法状态,人工对照任务栏\", error.reason]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_1"
},
"parentId": "t09_6",
"slot": "except_fn"
},
{
"id": "t09_14",
"componentId": "Basic_SetVariable",
"label": "异常累计失败数",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${failures + (case_1[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t09_6",
"slot": "except_fn"
},
{
"id": "t09_15",
"componentId": "Basic_SetVariable",
"label": "异常累计待确认数",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${pending + (case_1[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t09_6",
"slot": "except_fn"
},
{
"id": "t09_16",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 1294
},
"values": {
"msg": "用例结果:${json(case_1)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t09_17",
"componentId": "logic_control_try_catch",
"label": "09-02 · System_SetInputMethodState",
"position": {
"x": 220,
"y": 1436
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t09_18",
"componentId": "logic_control_if_else",
"label": "手动执行保护",
"position": {
"x": 220,
"y": 1520
},
"values": {
"condition": "${manualEnabled === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t09_17",
"slot": "try_fn"
},
{
"id": "t09_19",
"componentId": "ExceptionUtil_Throws",
"label": "默认不执行系统状态操作",
"position": {
"x": 220,
"y": 1520
},
"values": {
"msg": "待人工确认:执行开关未开启"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t09_18",
"slot": "statement_else"
},
{
"id": "t09_20",
"componentId": "System_SetInputMethodState",
"label": "System_SetInputMethodState",
"position": {
"x": 220,
"y": 1520
},
"values": {
"mode": "en-US"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "actual_2"
},
"parentId": "t09_18",
"slot": "statement_if"
},
{
"id": "t09_21",
"componentId": "Basic_SetVariable",
"label": "记录实际值与判定",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${[\"09-02\", \"System_SetInputMethodState\", \"MANUAL\", \"{\\\"mode\\\":\\\"en-US\\\"}\", \"前台编辑器应切换到英文模式;仅子进程返回 set:true 不能算通过\", actual_2]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_2"
},
"parentId": "t09_18",
"slot": "statement_if"
},
{
"id": "t09_22",
"componentId": "Basic_SetVariable",
"label": "累计失败数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${failures + (case_2[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t09_18",
"slot": "statement_if"
},
{
"id": "t09_23",
"componentId": "Basic_SetVariable",
"label": "累计待人工确认数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${pending + (case_2[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t09_18",
"slot": "statement_if"
},
{
"id": "t09_24",
"componentId": "Basic_SetVariable",
"label": "记录异常结果",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${[\"09-02\", \"System_SetInputMethodState\", manualEnabled === true ? \"FAIL\" : \"MANUAL\", \"{\\\"mode\\\":\\\"en-US\\\"}\", \"前台编辑器应切换到英文模式;仅子进程返回 set:true 不能算通过\", error.reason]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_2"
},
"parentId": "t09_17",
"slot": "except_fn"
},
{
"id": "t09_25",
"componentId": "Basic_SetVariable",
"label": "异常累计失败数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${failures + (case_2[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t09_17",
"slot": "except_fn"
},
{
"id": "t09_26",
"componentId": "Basic_SetVariable",
"label": "异常累计待确认数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${pending + (case_2[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t09_17",
"slot": "except_fn"
},
{
"id": "t09_27",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 1940
},
"values": {
"msg": "用例结果:${json(case_2)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t09_28",
"componentId": "logic_control_try_catch",
"label": "09-03 · System_SetInputMethodState",
"position": {
"x": 220,
"y": 2082
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t09_29",
"componentId": "logic_control_if_else",
"label": "手动执行保护",
"position": {
"x": 220,
"y": 1880
},
"values": {
"condition": "${manualEnabled === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t09_28",
"slot": "try_fn"
},
{
"id": "t09_30",
"componentId": "ExceptionUtil_Throws",
"label": "默认不执行系统状态操作",
"position": {
"x": 220,
"y": 1880
},
"values": {
"msg": "待人工确认:执行开关未开启"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t09_29",
"slot": "statement_else"
},
{
"id": "t09_31",
"componentId": "System_SetInputMethodState",
"label": "System_SetInputMethodState",
"position": {
"x": 220,
"y": 1880
},
"values": {
"mode": "${originalIme.culture}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "actual_3"
},
"parentId": "t09_29",
"slot": "statement_if"
},
{
"id": "t09_32",
"componentId": "Basic_SetVariable",
"label": "记录实际值与判定",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${[\"09-03\", \"System_SetInputMethodState\", \"MANUAL\", \"{\\\"mode\\\":\\\"${originalIme.culture}\\\"}\", \"恢复原输入法;人工确认前台窗口的实际状态\", actual_3]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_3"
},
"parentId": "t09_29",
"slot": "statement_if"
},
{
"id": "t09_33",
"componentId": "Basic_SetVariable",
"label": "累计失败数",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${failures + (case_3[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t09_29",
"slot": "statement_if"
},
{
"id": "t09_34",
"componentId": "Basic_SetVariable",
"label": "累计待人工确认数",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${pending + (case_3[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t09_29",
"slot": "statement_if"
},
{
"id": "t09_35",
"componentId": "Basic_SetVariable",
"label": "记录异常结果",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${[\"09-03\", \"System_SetInputMethodState\", manualEnabled === true ? \"FAIL\" : \"MANUAL\", \"{\\\"mode\\\":\\\"${originalIme.culture}\\\"}\", \"恢复原输入法;人工确认前台窗口的实际状态\", error.reason]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_3"
},
"parentId": "t09_28",
"slot": "except_fn"
},
{
"id": "t09_36",
"componentId": "Basic_SetVariable",
"label": "异常累计失败数",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${failures + (case_3[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t09_28",
"slot": "except_fn"
},
{
"id": "t09_37",
"componentId": "Basic_SetVariable",
"label": "异常累计待确认数",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${pending + (case_3[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t09_28",
"slot": "except_fn"
},
{
"id": "t09_38",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2586
},
"values": {
"msg": "用例结果:${json(case_3)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t09_39",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2728
},
"values": {
"msg": "本组汇总:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t09_40",
"componentId": "logic_control_if_else",
"label": "汇总判定",
"position": {
"x": 220,
"y": 2870
},
"values": {
"condition": "${failures > 0 || pending > 0 || blocked > 0}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t09_41",
"componentId": "ExceptionUtil_Throws",
"label": "如实标记未通过",
"position": {
"x": 220,
"y": 2420
},
"values": {
"msg": "组件测试未全部通过:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。请查看日志。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t09_40",
"slot": "statement_if"
},
{
"id": "t09_42",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2420
},
"values": {
"msg": "本组全部通过。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t09_40",
"slot": "statement_else"
}
],
"edges": [
{
"id": "e_t09_2",
"source": "t09_1",
"target": "t09_2"
},
{
"id": "e_t09_3",
"source": "t09_2",
"target": "t09_3"
},
{
"id": "e_t09_4",
"source": "t09_3",
"target": "t09_4"
},
{
"id": "e_t09_5",
"source": "t09_4",
"target": "t09_5"
},
{
"id": "e_t09_6",
"source": "t09_5",
"target": "t09_6"
},
{
"id": "e_t09_16",
"source": "t09_6",
"target": "t09_16"
},
{
"id": "e_t09_17",
"source": "t09_16",
"target": "t09_17"
},
{
"id": "e_t09_27",
"source": "t09_17",
"target": "t09_27"
},
{
"id": "e_t09_28",
"source": "t09_27",
"target": "t09_28"
},
{
"id": "e_t09_38",
"source": "t09_28",
"target": "t09_38"
},
{
"id": "e_t09_39",
"source": "t09_38",
"target": "t09_39"
},
{
"id": "e_t09_40",
"source": "t09_39",
"target": "t09_40"
}
],
"meta": {
"createdAt": "2026-10-30T00:00:00.000Z",
"updatedAt": "2026-10-30T00:00:00.000Z",
"appVersion": "0.1.0"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,654 @@
{
"schemaVersion": 1,
"id": "test_appsystem_12",
"name": "组件测试 12 · 锁屏与解锁【手动】",
"description": "手动测试:先阅读测试说明。默认不开启执行开关,不能把组件返回成功当作功能通过。",
"enabled": true,
"trigger": {
"type": "manual",
"label": "手动运行"
},
"variables": [],
"nodes": [
{
"id": "t12_1",
"componentId": "Trigger.Manual",
"label": "人工发起",
"position": {
"x": 220,
"y": 80
},
"values": {
"entryName": "开始组件测试"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t12_2",
"componentId": "Basic_SetVariable",
"label": "初始化失败计数",
"position": {
"x": 220,
"y": 222
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
}
},
{
"id": "t12_3",
"componentId": "Basic_SetVariable",
"label": "初始化待确认计数",
"position": {
"x": 220,
"y": 364
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
}
},
{
"id": "t12_4",
"componentId": "Basic_SetVariable",
"label": "初始化阻塞计数",
"position": {
"x": 220,
"y": 506
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "blocked"
}
},
{
"id": "t12_5",
"componentId": "Basic_SetVariable",
"label": "手动执行开关:确认环境后才可改为 true",
"position": {
"x": 220,
"y": 648
},
"values": {
"value": false
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "manualEnabled"
}
},
{
"id": "t12_6",
"componentId": "logic_control_try_catch",
"label": "12-01 · ScreenControl_Screenlock",
"position": {
"x": 220,
"y": 790
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t12_7",
"componentId": "logic_control_if_else",
"label": "手动执行保护",
"position": {
"x": 220,
"y": 1160
},
"values": {
"condition": "${manualEnabled === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t12_6",
"slot": "try_fn"
},
{
"id": "t12_8",
"componentId": "ExceptionUtil_Throws",
"label": "默认不执行系统状态操作",
"position": {
"x": 220,
"y": 1160
},
"values": {
"msg": "待人工确认:执行开关未开启"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t12_7",
"slot": "statement_else"
},
{
"id": "t12_9",
"componentId": "ScreenControl_Screenlock",
"label": "ScreenControl_Screenlock",
"position": {
"x": 220,
"y": 1160
},
"values": {
"username": "",
"pwd": "",
"width": "",
"height": "",
"scale_factor": "100"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "actual_1"
},
"parentId": "t12_7",
"slot": "statement_if"
},
{
"id": "t12_10",
"componentId": "Basic_SetVariable",
"label": "记录实际值与判定",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${[\"12-01\", \"ScreenControl_Screenlock\", \"MANUAL\", \"{\\\"username\\\":\\\"\\\",\\\"pwd\\\":\\\"\\\",\\\"width\\\":\\\"\\\",\\\"height\\\":\\\"\\\",\\\"scale_factor\\\":\\\"100\\\"}\", \"实际锁屏;需要人在机器旁用 Windows 登录凭据恢复,不改变账户密码\", actual_1]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_1"
},
"parentId": "t12_7",
"slot": "statement_if"
},
{
"id": "t12_11",
"componentId": "Basic_SetVariable",
"label": "累计失败数",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${failures + (case_1[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t12_7",
"slot": "statement_if"
},
{
"id": "t12_12",
"componentId": "Basic_SetVariable",
"label": "累计待人工确认数",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${pending + (case_1[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t12_7",
"slot": "statement_if"
},
{
"id": "t12_13",
"componentId": "Basic_SetVariable",
"label": "记录异常结果",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${[\"12-01\", \"ScreenControl_Screenlock\", manualEnabled === true ? \"FAIL\" : \"MANUAL\", \"{\\\"username\\\":\\\"\\\",\\\"pwd\\\":\\\"\\\",\\\"width\\\":\\\"\\\",\\\"height\\\":\\\"\\\",\\\"scale_factor\\\":\\\"100\\\"}\", \"实际锁屏;需要人在机器旁用 Windows 登录凭据恢复,不改变账户密码\", error.reason]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_1"
},
"parentId": "t12_6",
"slot": "except_fn"
},
{
"id": "t12_14",
"componentId": "Basic_SetVariable",
"label": "异常累计失败数",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${failures + (case_1[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t12_6",
"slot": "except_fn"
},
{
"id": "t12_15",
"componentId": "Basic_SetVariable",
"label": "异常累计待确认数",
"position": {
"x": 220,
"y": 1160
},
"values": {
"value": "${pending + (case_1[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t12_6",
"slot": "except_fn"
},
{
"id": "t12_16",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 1294
},
"values": {
"msg": "用例结果:${json(case_1)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t12_17",
"componentId": "logic_control_try_catch",
"label": "12-02 · ScreenControl_ScreenUnlock",
"position": {
"x": 220,
"y": 1436
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t12_18",
"componentId": "logic_control_if_else",
"label": "手动执行保护",
"position": {
"x": 220,
"y": 1520
},
"values": {
"condition": "${manualEnabled === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t12_17",
"slot": "try_fn"
},
{
"id": "t12_19",
"componentId": "ExceptionUtil_Throws",
"label": "默认不执行系统状态操作",
"position": {
"x": 220,
"y": 1520
},
"values": {
"msg": "待人工确认:执行开关未开启"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t12_18",
"slot": "statement_else"
},
{
"id": "t12_20",
"componentId": "ScreenControl_ScreenUnlock",
"label": "ScreenControl_ScreenUnlock",
"position": {
"x": 220,
"y": 1520
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "actual_2"
},
"parentId": "t12_18",
"slot": "statement_if"
},
{
"id": "t12_21",
"componentId": "Basic_SetVariable",
"label": "记录实际值与判定",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${[\"12-02\", \"ScreenControl_ScreenUnlock\", \"MANUAL\", \"{}\", \"仅尝试唤醒屏幕,loginUnlockSupported 必须为 false;Windows 登录仍需人工完成\", actual_2]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_2"
},
"parentId": "t12_18",
"slot": "statement_if"
},
{
"id": "t12_22",
"componentId": "Basic_SetVariable",
"label": "累计失败数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${failures + (case_2[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t12_18",
"slot": "statement_if"
},
{
"id": "t12_23",
"componentId": "Basic_SetVariable",
"label": "累计待人工确认数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${pending + (case_2[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t12_18",
"slot": "statement_if"
},
{
"id": "t12_24",
"componentId": "Basic_SetVariable",
"label": "记录异常结果",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${[\"12-02\", \"ScreenControl_ScreenUnlock\", manualEnabled === true ? \"FAIL\" : \"MANUAL\", \"{}\", \"仅尝试唤醒屏幕,loginUnlockSupported 必须为 false;Windows 登录仍需人工完成\", error.reason]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_2"
},
"parentId": "t12_17",
"slot": "except_fn"
},
{
"id": "t12_25",
"componentId": "Basic_SetVariable",
"label": "异常累计失败数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${failures + (case_2[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t12_17",
"slot": "except_fn"
},
{
"id": "t12_26",
"componentId": "Basic_SetVariable",
"label": "异常累计待确认数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${pending + (case_2[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t12_17",
"slot": "except_fn"
},
{
"id": "t12_27",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 1940
},
"values": {
"msg": "用例结果:${json(case_2)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t12_28",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2082
},
"values": {
"msg": "本组汇总:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t12_29",
"componentId": "logic_control_if_else",
"label": "汇总判定",
"position": {
"x": 220,
"y": 2224
},
"values": {
"condition": "${failures > 0 || pending > 0 || blocked > 0}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t12_30",
"componentId": "ExceptionUtil_Throws",
"label": "如实标记未通过",
"position": {
"x": 220,
"y": 2060
},
"values": {
"msg": "组件测试未全部通过:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。请查看日志。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t12_29",
"slot": "statement_if"
},
{
"id": "t12_31",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2060
},
"values": {
"msg": "本组全部通过。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t12_29",
"slot": "statement_else"
}
],
"edges": [
{
"id": "e_t12_2",
"source": "t12_1",
"target": "t12_2"
},
{
"id": "e_t12_3",
"source": "t12_2",
"target": "t12_3"
},
{
"id": "e_t12_4",
"source": "t12_3",
"target": "t12_4"
},
{
"id": "e_t12_5",
"source": "t12_4",
"target": "t12_5"
},
{
"id": "e_t12_6",
"source": "t12_5",
"target": "t12_6"
},
{
"id": "e_t12_16",
"source": "t12_6",
"target": "t12_16"
},
{
"id": "e_t12_17",
"source": "t12_16",
"target": "t12_17"
},
{
"id": "e_t12_27",
"source": "t12_17",
"target": "t12_27"
},
{
"id": "e_t12_28",
"source": "t12_27",
"target": "t12_28"
},
{
"id": "e_t12_29",
"source": "t12_28",
"target": "t12_29"
}
],
"meta": {
"createdAt": "2026-10-30T00:00:00.000Z",
"updatedAt": "2026-10-30T00:00:00.000Z",
"appVersion": "0.1.0"
}
}

View File

@ -0,0 +1,735 @@
{
"schemaVersion": 1,
"id": "test_appsystem_13",
"name": "组件测试 13 · 远程桌面【手动】",
"description": "手动测试:先阅读测试说明。默认不开启执行开关,不能把组件返回成功当作功能通过。",
"enabled": true,
"trigger": {
"type": "manual",
"label": "手动运行"
},
"variables": [],
"nodes": [
{
"id": "t13_1",
"componentId": "Trigger.Manual",
"label": "人工发起",
"position": {
"x": 220,
"y": 80
},
"values": {
"entryName": "开始组件测试"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t13_2",
"componentId": "Basic_SetVariable",
"label": "初始化失败计数",
"position": {
"x": 220,
"y": 222
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
}
},
{
"id": "t13_3",
"componentId": "Basic_SetVariable",
"label": "初始化待确认计数",
"position": {
"x": 220,
"y": 364
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
}
},
{
"id": "t13_4",
"componentId": "Basic_SetVariable",
"label": "初始化阻塞计数",
"position": {
"x": 220,
"y": 506
},
"values": {
"value": 0
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "blocked"
}
},
{
"id": "t13_5",
"componentId": "Basic_SetVariable",
"label": "手动执行开关:确认环境后才可改为 true",
"position": {
"x": 220,
"y": 648
},
"values": {
"value": false
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "manualEnabled"
}
},
{
"id": "t13_6",
"componentId": "Basic_SetVariable",
"label": "填写用户自己的测试远程地址",
"position": {
"x": 220,
"y": 790
},
"values": {
"value": ""
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "remoteAddress"
}
},
{
"id": "t13_7",
"componentId": "Basic_SetVariable",
"label": "断连单独开关:可能断开当前 Windows 会话,必须另行确认",
"position": {
"x": 220,
"y": 932
},
"values": {
"value": false
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "allowDisconnect"
}
},
{
"id": "t13_8",
"componentId": "logic_control_try_catch",
"label": "13-01 · System_ConnectRemoteDesktop",
"position": {
"x": 220,
"y": 1074
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t13_9",
"componentId": "logic_control_if_else",
"label": "手动执行保护",
"position": {
"x": 220,
"y": 1520
},
"values": {
"condition": "${manualEnabled === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t13_8",
"slot": "try_fn"
},
{
"id": "t13_10",
"componentId": "ExceptionUtil_Throws",
"label": "默认不执行系统状态操作",
"position": {
"x": 220,
"y": 1520
},
"values": {
"msg": "待人工确认:执行开关未开启"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t13_9",
"slot": "statement_else"
},
{
"id": "t13_11",
"componentId": "System_ConnectRemoteDesktop",
"label": "System_ConnectRemoteDesktop",
"position": {
"x": 220,
"y": 1520
},
"values": {
"ip": "${remoteAddress}",
"port": "3389"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "actual_1"
},
"parentId": "t13_9",
"slot": "statement_if"
},
{
"id": "t13_12",
"componentId": "Basic_SetVariable",
"label": "记录实际值与判定",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${[\"13-01\", \"System_ConnectRemoteDesktop\", \"MANUAL\", \"{\\\"ip\\\":\\\"${remoteAddress}\\\",\\\"port\\\":\\\"3389\\\"}\", \"人工完成登录,并确认已连接到正确测试机器;启动 mstsc 不等于连接成功\", actual_1]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_1"
},
"parentId": "t13_9",
"slot": "statement_if"
},
{
"id": "t13_13",
"componentId": "Basic_SetVariable",
"label": "累计失败数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${failures + (case_1[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t13_9",
"slot": "statement_if"
},
{
"id": "t13_14",
"componentId": "Basic_SetVariable",
"label": "累计待人工确认数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${pending + (case_1[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t13_9",
"slot": "statement_if"
},
{
"id": "t13_15",
"componentId": "Basic_SetVariable",
"label": "记录异常结果",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${[\"13-01\", \"System_ConnectRemoteDesktop\", manualEnabled === true ? \"FAIL\" : \"MANUAL\", \"{\\\"ip\\\":\\\"${remoteAddress}\\\",\\\"port\\\":\\\"3389\\\"}\", \"人工完成登录,并确认已连接到正确测试机器;启动 mstsc 不等于连接成功\", error.reason]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_1"
},
"parentId": "t13_8",
"slot": "except_fn"
},
{
"id": "t13_16",
"componentId": "Basic_SetVariable",
"label": "异常累计失败数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${failures + (case_1[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t13_8",
"slot": "except_fn"
},
{
"id": "t13_17",
"componentId": "Basic_SetVariable",
"label": "异常累计待确认数",
"position": {
"x": 220,
"y": 1520
},
"values": {
"value": "${pending + (case_1[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t13_8",
"slot": "except_fn"
},
{
"id": "t13_18",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 1578
},
"values": {
"msg": "用例结果:${json(case_1)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t13_19",
"componentId": "logic_control_try_catch",
"label": "13-02 · System_CloseRemoteDesktop",
"position": {
"x": 220,
"y": 1720
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t13_20",
"componentId": "logic_control_if_else",
"label": "手动执行保护",
"position": {
"x": 220,
"y": 1880
},
"values": {
"condition": "${manualEnabled === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t13_19",
"slot": "try_fn"
},
{
"id": "t13_21",
"componentId": "ExceptionUtil_Throws",
"label": "默认不执行系统状态操作",
"position": {
"x": 220,
"y": 1880
},
"values": {
"msg": "待人工确认:执行开关未开启"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t13_20",
"slot": "statement_else"
},
{
"id": "t13_30",
"componentId": "logic_control_if_else",
"label": "断连操作单独保护",
"position": {
"x": 220,
"y": 2060
},
"values": {
"condition": "${allowDisconnect === true}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t13_20",
"slot": "statement_if"
},
{
"id": "t13_22",
"componentId": "System_CloseRemoteDesktop",
"label": "System_CloseRemoteDesktop",
"position": {
"x": 220,
"y": 1880
},
"values": {},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "actual_2"
},
"parentId": "t13_30",
"slot": "statement_if"
},
{
"id": "t13_23",
"componentId": "Basic_SetVariable",
"label": "记录实际值与判定",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${[\"13-02\", \"System_CloseRemoteDesktop\", \"MANUAL\", \"{}\", \"确认目标会话及恢复方式后才能执行;当前实现会调用 tsdiscon\", actual_2]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_2"
},
"parentId": "t13_20",
"slot": "statement_if"
},
{
"id": "t13_24",
"componentId": "Basic_SetVariable",
"label": "累计失败数",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${failures + (case_2[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t13_20",
"slot": "statement_if"
},
{
"id": "t13_25",
"componentId": "Basic_SetVariable",
"label": "累计待人工确认数",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${pending + (case_2[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t13_20",
"slot": "statement_if"
},
{
"id": "t13_26",
"componentId": "Basic_SetVariable",
"label": "记录异常结果",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${[\"13-02\", \"System_CloseRemoteDesktop\", manualEnabled === true ? \"FAIL\" : \"MANUAL\", \"{}\", \"确认目标会话及恢复方式后才能执行;当前实现会调用 tsdiscon\", error.reason]}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "case_2"
},
"parentId": "t13_19",
"slot": "except_fn"
},
{
"id": "t13_27",
"componentId": "Basic_SetVariable",
"label": "异常累计失败数",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${failures + (case_2[2] === \"FAIL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "failures"
},
"parentId": "t13_19",
"slot": "except_fn"
},
{
"id": "t13_28",
"componentId": "Basic_SetVariable",
"label": "异常累计待确认数",
"position": {
"x": 220,
"y": 1880
},
"values": {
"value": "${pending + (case_2[2] === \"MANUAL\" ? 1 : 0)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false,
"outputVar": "pending"
},
"parentId": "t13_19",
"slot": "except_fn"
},
{
"id": "t13_29",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2224
},
"values": {
"msg": "用例结果:${json(case_2)}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t13_31",
"componentId": "ExceptionUtil_Throws",
"label": "阻止未授权断连",
"position": {
"x": 220,
"y": 2060
},
"values": {
"msg": "阻塞:断连开关未开启;执行可能中断当前会话。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t13_30",
"slot": "statement_else"
},
{
"id": "t13_32",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2366
},
"values": {
"msg": "本组汇总:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t13_33",
"componentId": "logic_control_if_else",
"label": "汇总判定",
"position": {
"x": 220,
"y": 2508
},
"values": {
"condition": "${failures > 0 || pending > 0 || blocked > 0}"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
}
},
{
"id": "t13_34",
"componentId": "ExceptionUtil_Throws",
"label": "如实标记未通过",
"position": {
"x": 220,
"y": 2420
},
"values": {
"msg": "组件测试未全部通过:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。请查看日志。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t13_33",
"slot": "statement_if"
},
{
"id": "t13_35",
"componentId": "Log_Info",
"label": "输出测试结果",
"position": {
"x": 220,
"y": 2420
},
"values": {
"msg": "本组全部通过。"
},
"options": {
"timeoutMs": 15000,
"retryCount": 0,
"logException": false
},
"parentId": "t13_33",
"slot": "statement_else"
}
],
"edges": [
{
"id": "e_t13_2",
"source": "t13_1",
"target": "t13_2"
},
{
"id": "e_t13_3",
"source": "t13_2",
"target": "t13_3"
},
{
"id": "e_t13_4",
"source": "t13_3",
"target": "t13_4"
},
{
"id": "e_t13_5",
"source": "t13_4",
"target": "t13_5"
},
{
"id": "e_t13_6",
"source": "t13_5",
"target": "t13_6"
},
{
"id": "e_t13_7",
"source": "t13_6",
"target": "t13_7"
},
{
"id": "e_t13_8",
"source": "t13_7",
"target": "t13_8"
},
{
"id": "e_t13_18",
"source": "t13_8",
"target": "t13_18"
},
{
"id": "e_t13_19",
"source": "t13_18",
"target": "t13_19"
},
{
"id": "e_t13_29",
"source": "t13_19",
"target": "t13_29"
},
{
"id": "e_t13_32",
"source": "t13_29",
"target": "t13_32"
},
{
"id": "e_t13_33",
"source": "t13_32",
"target": "t13_33"
}
],
"meta": {
"createdAt": "2026-10-30T00:00:00.000Z",
"updatedAt": "2026-10-30T00:00:00.000Z",
"appVersion": "0.1.0"
}
}

View File

@ -0,0 +1,279 @@
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;
}

View File

@ -0,0 +1,390 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import net from "node:net";
import { spawn, execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import { projectRoot, bundleRoot, writeSuite } from "./app-system-test-flows.mjs";
const execFileAsync = promisify(execFile);
const pause = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const containers = new Set(["logic_control_if_else", "logic_control_count_for", "logic_control_array_for_v3", "logic_control_dictionary_for_v1", "logic_control_while", "infinite_loop", "logic_control_try_catch"]);
const statusNames = { PASS: "通过", FAIL: "失败", BLOCKED: "阻塞", MANUAL: "待人工确认", UNRUN: "未运行" };
export function readBundle() {
const manifest = JSON.parse(fs.readFileSync(path.join(bundleRoot, "manifest.json"), "utf8"));
return { manifest, groups: manifest.groups.map(entry => ({ entry, flow: JSON.parse(fs.readFileSync(path.join(bundleRoot, entry.file), "utf8")) })) };
}
// 静态清单供离线校验;真实运行前再与 Electron 返回的 implemented 清单逐项比较。
export function staticCatalog() {
const registered = new Set();
const directory = path.join(projectRoot, "src/main/capabilities");
for (const name of fs.readdirSync(directory).filter(name => name.endsWith(".ts") && !name.endsWith(".test.ts"))) {
const source = ts.createSourceFile(name, fs.readFileSync(path.join(directory, name), "utf8"), ts.ScriptTarget.Latest, true);
const visit = node => {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "register") {
const first = node.arguments[0];
if (first && ts.isStringLiteral(first)) registered.add(first.text);
if (first && ts.isArrayLiteralExpression(first)) for (const item of first.elements) if (ts.isStringLiteral(item)) registered.add(item.text);
}
ts.forEachChild(node, visit);
};
visit(source);
}
const catalog = JSON.parse(fs.readFileSync(path.join(projectRoot, "resources/catalog.json"), "utf8"));
for (const c of catalog.components) c.implemented = containers.has(c.id) || registered.has(c.id) || registered.has(c.functionName);
return catalog;
}
export async function validator() {
const source = fs.readFileSync(path.join(projectRoot, "src/renderer/src/validation/flowValidation.ts"), "utf8");
const { outputText } = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } });
return (await import(`data:text/javascript;base64,${Buffer.from(outputText).toString("base64")}`)).validateFlow;
}
export async function verifyBundle(catalog = staticCatalog()) {
const { manifest, groups } = readBundle();
const expected = catalog.components.filter(c => c.group === "app" && c.implemented).map(c => c.id).sort();
const actual = groups.flatMap(g => g.entry.components).sort();
if (groups.length !== 13 || actual.length !== 50 || new Set(actual).size !== 50 || JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error("流程包与当前 50 项可执行清单不一致,请重新生成并核对覆盖范围。");
const validateFlow = await validator();
const validation = {};
for (const { flow, entry } of groups) {
const nodes = new Map(flow.nodes.map(n => [n.id, n]));
if (nodes.size !== flow.nodes.length || flow.nodes[0].componentId !== "Trigger.Manual") throw new Error(`${flow.id} 节点 ID 重复或缺少入口`);
const covered = new Set(entry.cases.map(c => c.componentId));
if (entry.components.some(id => !covered.has(id)) || covered.size !== entry.components.length) throw new Error(`${flow.id} 用例覆盖不完整`);
for (const c of entry.cases) if (nodes.get(c.targetNodeId)?.componentId !== c.componentId) throw new Error(`${c.id} 目标节点不匹配`);
const reached = new Set([flow.nodes[0].id]);
for (let i = 0; i < nodes.size; i++) {
for (const edge of flow.edges) {
if (!nodes.has(edge.source) || !nodes.has(edge.target)) throw new Error(`${flow.id} 连线指向不存在的节点`);
if (reached.has(edge.source)) reached.add(edge.target);
}
for (const node of flow.nodes) if (reached.has(node.parentId)) reached.add(node.id);
}
if (reached.size !== nodes.size) throw new Error(`${flow.id} 存在断链或孤立节点`);
validation[entry.id] = validateFlow(flow, catalog);
const unexpected = validation[entry.id].filter(i => i.code !== "required");
if (unexpected.length) throw new Error(`${flow.id} 存在意外校验问题:${JSON.stringify(unexpected)}`);
}
return { manifest, groups, validation, executableCount: expected.length };
}
export function installBundle(userData) {
const { groups } = readBundle();
const directory = path.join(path.resolve(userData), "flows");
fs.mkdirSync(directory, { recursive: true });
const installed = [], skipped = [];
for (const { flow } of groups) {
const now = new Date().toISOString();
const target = path.join(directory, `${flow.id}.json`);
try {
fs.writeFileSync(target, JSON.stringify({ ...flow, meta: { ...flow.meta, createdAt: now, updatedAt: now } }, null, 2) + "\n", { flag: "wx" });
installed.push(flow.name);
} catch (error) {
if (error.code !== "EEXIST") throw error;
skipped.push(flow.name);
}
}
return { userData: path.resolve(userData), installed, skipped };
}
/** 仅升级仍与指定旧模板一致的测试任务;用户修改过的内容保持原样。 */
export function upgradeBundle(userData, baselineDirectory, backupDirectory) {
const { groups } = readBundle();
const directory = path.join(path.resolve(userData), "flows");
const normalize = flow => { const { meta, ...content } = flow; return JSON.stringify(content); };
const upgraded = [], skipped = [];
for (const { flow, entry } of groups) {
const target = path.join(directory, entry.file);
const baseline = path.join(path.resolve(baselineDirectory), entry.file);
if (!fs.existsSync(target) || !fs.existsSync(baseline)) { skipped.push({ id: entry.id, reason: "缺少现有任务或旧模板" }); continue; }
let current;
try { current = JSON.parse(fs.readFileSync(target, "utf8")); }
catch { skipped.push({ id: entry.id, reason: "任务文件无法解析,保留原文件" }); continue; }
if (normalize(current) === normalize(flow)) { skipped.push({ id: entry.id, reason: "已是当前版本" }); continue; }
if (normalize(current) !== normalize(JSON.parse(fs.readFileSync(baseline, "utf8")))) { skipped.push({ id: entry.id, reason: "用户已修改,保留现有版本" }); continue; }
fs.mkdirSync(backupDirectory, { recursive: true });
fs.copyFileSync(target, path.join(backupDirectory, entry.file), fs.constants.COPYFILE_EXCL);
const updated = { ...flow, meta: { ...flow.meta, createdAt: current.meta?.createdAt || flow.meta.createdAt, updatedAt: new Date().toISOString() } };
const temp = `${target}.tmp`;
fs.writeFileSync(temp, JSON.stringify(updated, null, 2) + "\n", { flag: "wx" });
fs.renameSync(temp, target);
upgraded.push(entry.id);
}
return { upgraded, skipped, backupDirectory: path.resolve(backupDirectory) };
}
async function freePort() {
const server = net.createServer();
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); });
const port = server.address().port;
await new Promise(resolve => server.close(resolve));
return port;
}
async function cdpCommand(page, method, params, timeout = 180000) {
const socket = new WebSocket(page.webSocketDebuggerUrl);
let timer;
try {
await new Promise((resolve, reject) => { timer = setTimeout(() => reject(new Error("CDP 连接超时")), 10000); socket.onopen = resolve; socket.onerror = reject; });
clearTimeout(timer);
return await new Promise((resolve, reject) => {
timer = setTimeout(() => reject(new Error(`隔离检查 ${method} 超时`)), timeout);
socket.onerror = reject;
socket.onclose = () => reject(new Error("隔离应用连接提前关闭"));
socket.onmessage = event => {
const response = JSON.parse(event.data);
if (response.id !== 1) return;
if (response.error || response.result?.exceptionDetails) reject(new Error(JSON.stringify(response.error || response.result.exceptionDetails)));
else resolve(response.result);
};
socket.send(JSON.stringify({ id: 1, method, params }));
});
} finally { clearTimeout(timer); socket.close(); }
}
async function evaluate(page, expression, timeout = 180000) {
const response = await cdpCommand(page, "Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }, timeout);
return response?.result?.value;
}
async function verifyRenderer(page, output) {
const specs = [
["conversion", "Basic_DataTypeConversion", { val: "42", into_type: "number" }],
["judgment", "Basic_JudgeDataType", { val: 42, into_type: "number" }],
["kill", "App_KillV1", { close_type: "id", pid: "", p_name: "" }],
["command", "System_ExecuteCommandV1", { command_type: "1", dos_command: "echo ui-check" }],
["wake", "ScreenControl_ScreenUnlock", {}]
];
const flow = { schemaVersion: 1, id: "test_appsystem_ui", name: "界面验证:组件参数", variables: [],
nodes: [{ id: "ui_start", componentId: "Trigger.Manual", values: {}, options: {}, position: { x: 220, y: 60 } }, ...specs.map(([id, componentId, values], index) => ({ id: `ui_${id}`, componentId, values, options: {}, position: { x: 220, y: 200 + index * 142 } }))],
edges: [], meta: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), appVersion: "test" } };
await evaluate(page, `window.rpa.flows.save(${JSON.stringify(flow)})`);
await evaluate(page, "document.querySelector('.refresh-btn').click()");
const waitFor = async expression => {
for (let n = 0; n < 100; n++) { if (await evaluate(page, expression)) return; await pause(50); }
throw new Error(`前端状态等待超时:${expression}`);
};
await waitFor(`Array.from(document.querySelectorAll('.task-row')).some(row => row.textContent.includes(${JSON.stringify(flow.name)}))`);
await evaluate(page, `Array.from(document.querySelectorAll('.task-row')).find(row => row.textContent.includes(${JSON.stringify(flow.name)})).querySelector('button[title="编辑流程"]').click()`);
const select = async (id, componentId) => {
await waitFor(`Boolean(document.querySelector('[data-id="ui_${id}"]'))`);
await evaluate(page, `document.querySelector('[data-id="ui_${id}"]').click()`);
await waitFor(`document.querySelector('.props-id')?.textContent === ${JSON.stringify(componentId)}`);
};
const labels = () => evaluate(page, "Array.from(document.querySelectorAll('.props .field-label')).map(el => el.textContent.trim())");
const clickOption = text => evaluate(page, `Array.from(document.querySelectorAll('.props .option-chip')).find(el => el.textContent === ${JSON.stringify(text)}).click()`);
const screenshot = async name => {
const result = await cdpCommand(page, "Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
fs.writeFileSync(path.join(output, name), Buffer.from(result.data, "base64"));
};
const observations = {};
await select("conversion", "Basic_DataTypeConversion");
observations.conversionOptions = await evaluate(page, "Array.from(document.querySelector('.props select').options).map(o => ({label:o.text,value:o.value}))");
if (!["number", "int", "bool", "string"].every(value => observations.conversionOptions.some(o => o.value === value))) throw new Error("转换类型下拉选项缺失");
await screenshot("ui-conversion.png");
await select("judgment", "Basic_JudgeDataType");
observations.judgmentOptions = await evaluate(page, "Array.from(document.querySelector('.props select').options).map(o => o.value)");
if (!["number", "int", "null", "dict", "list", "string", "bool"].every(value => observations.judgmentOptions.includes(value))) throw new Error("判断类型下拉选项缺失");
await select("kill", "App_KillV1");
observations.killPidFields = await labels();
if (!observations.killPidFields.some(s => s.startsWith("进程PID")) || observations.killPidFields.some(s => s.startsWith("进程名称"))) throw new Error("PID 模式显示了错误字段");
await clickOption("按进程名称"); await pause(80);
observations.killNameFields = await labels();
if (!observations.killNameFields.some(s => s.startsWith("进程名称")) || observations.killNameFields.some(s => s.startsWith("进程PID"))) throw new Error("名称模式显示了错误字段");
await screenshot("ui-kill-name.png");
await select("command", "System_ExecuteCommandV1");
observations.commandDosFields = await labels();
if (!observations.commandDosFields.some(s => s.startsWith("DOS命令")) || observations.commandDosFields.some(s => s.startsWith("BAT文件") || s.startsWith("PowerShell命令"))) throw new Error("DOS 模式显示了错误字段");
await clickOption("PowerShell 命令"); await pause(80);
observations.commandPsFields = await labels();
if (!observations.commandPsFields.some(s => s.startsWith("PowerShell命令")) || observations.commandPsFields.some(s => s.startsWith("DOS命令") || s.startsWith("BAT文件"))) throw new Error("PowerShell 模式显示了错误字段");
await screenshot("ui-command-ps.png");
await select("wake", "ScreenControl_ScreenUnlock");
observations.wakeTitle = await evaluate(page, "document.querySelector('.props-title').textContent");
observations.wakeFields = await labels();
if (!observations.wakeTitle.includes("非登录解锁") || observations.wakeFields.some(s => s === "密码" || s === "用户名")) throw new Error("唤醒组件仍在宣称登录解锁或索要凭据");
await screenshot("ui-wake.png");
fs.writeFileSync(path.join(output, "ui-check.json"), JSON.stringify({ passed: true, observations }, null, 2) + "\n");
return observations;
}
export async function runSafe(outputDirectory, checkUi = false) {
const output = path.resolve(outputDirectory);
fs.mkdirSync(output, { recursive: true });
const profile = fs.mkdtempSync(path.join(output, "isolated-"));
const debugPort = await freePort();
const candidates = [process.env.RPA_ELECTRON_DIST, "C:/electron-rpa/dist", path.join(projectRoot, "node_modules/electron/dist")].filter(Boolean);
const dist = candidates.find(dir => fs.existsSync(path.join(dir, "electron.exe")));
if (!dist) throw new Error("找不到 Electron,请先安装项目依赖。");
if (!fs.existsSync(path.join(projectRoot, "out/main/index.js"))) throw new Error("请先执行 npm run build。");
// Chromium 在隐藏窗口中可能停止后续绘制;界面截图检查使用独立可见窗口。
const env = { ...process.env, RPA_E2E: checkUi ? "0" : "1", RPA_SKIP_SINGLE_INSTANCE: "1", RPA_FIXTURE_PORT: "0", RPA_APP_SYSTEM_TEST_VALUE: "隔离环境原始值" };
delete env.ELECTRON_RUN_AS_NODE;
const child = spawn(path.join(dist, "electron.exe"), [projectRoot, `--user-data-dir=${profile}`, `--remote-debugging-port=${debugPort}`, "--remote-debugging-address=127.0.0.1", "--no-sandbox"], { cwd: projectRoot, env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
let diagnostics = "";
for (const stream of [child.stdout, child.stderr]) stream.on("data", chunk => { diagnostics = (diagnostics + chunk).slice(-16000); });
child.on("error", error => { diagnostics += error.message; });
const runs = [];
try {
let page;
const deadline = Date.now() + 45000;
while (Date.now() < deadline) {
if (child.exitCode !== null) throw new Error(`隔离应用提前退出:${diagnostics}`);
try {
const targets = await (await fetch(`http://127.0.0.1:${debugPort}/json/list`, { signal: AbortSignal.timeout(1500) })).json();
page = targets.find(p => p.type === "page" && p.title === "RPA Studio");
if (page && await evaluate(page, "Boolean(window.rpa)", 5000)) break;
} catch { /* Electron 和 preload 尚未就绪,继续有界等待。 */ }
page = undefined;
await pause(200);
}
if (!page) throw new Error(`等待隔离应用超时:${diagnostics}`);
const info = await evaluate(page, "window.rpa.app.info()");
if (path.resolve(info.userData).toLowerCase() !== profile.toLowerCase()) throw new Error("隔离用户目录不匹配,已拒绝执行测试。");
const catalog = await evaluate(page, "window.rpa.catalog.get()");
const checked = await verifyBundle(catalog);
for (const { flow, entry } of checked.groups) {
const issues = checked.validation[entry.id];
if (entry.manual || issues.some(i => i.severity === "error")) {
const status = issues.some(i => i.severity === "error") ? "BLOCKED" : "MANUAL";
runs.push({ flowId: flow.id, status, validation: issues, reason: status === "BLOCKED" ? "运行前参数校验拦截,未绕过校验;详见校验问题" : "手动任务不纳入自动执行" });
console.log(`${flow.name}:${statusNames[status]}`);
continue;
}
const record = await evaluate(page, `(async () => {
const flow = ${JSON.stringify(flow)};
await window.rpa.flows.save(flow);
const events = [];
const off = window.rpa.run.onEvent(event => events.push(event));
try {
const started = await window.rpa.run.start(flow);
const finished = await new Promise((resolve, reject) => {
const end = Date.now() + 150000;
const timer = setInterval(() => {
const item = events.find(e => e.runId === started.runId && e.type === 'run.finished');
if (item) { clearInterval(timer); resolve(item); }
else if (Date.now() > end) { clearInterval(timer); void window.rpa.run.stop(started.runId); reject(new Error('流程执行超时')); }
}, 50);
});
return { flowId: flow.id, runId: started.runId, status: finished.status, durationMs: finished.durationMs, events };
} finally { off(); }
})()`);
// run.finished 先通过 IPC 发出,主进程随后落盘,等待真正持久化再读取证据。
const persisted = path.join(profile, "runs", record.runId, "run.json");
for (let n = 0; n < 30 && !fs.existsSync(persisted); n++) await pause(100);
if (!fs.existsSync(persisted)) throw new Error(`${record.runId} 没有持久化运行证据`);
record.persistedFile = persisted;
record.result = JSON.parse(fs.readFileSync(persisted, "utf8"));
runs.push(record);
console.log(`${flow.name}:${record.status}(${record.durationMs} ms)`);
}
const report = { generatedAt: new Date().toISOString(), profile, executableCount: checked.executableCount, runs };
fs.writeFileSync(path.join(output, "latest.json"), JSON.stringify(report, null, 2) + "\n");
if (checkUi) {
try { report.ui = { passed: true, observations: await verifyRenderer(page, output) }; }
catch (error) { report.ui = { passed: false, reason: error.message }; throw error; }
finally { fs.writeFileSync(path.join(output, "latest.json"), JSON.stringify(report, null, 2) + "\n"); }
}
return report;
} finally {
if (child.pid && child.exitCode === null) {
try { await execFileAsync("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, timeout: 10000 }); }
catch (error) { if (child.exitCode === null) console.warn(`隔离进程退出检查:${error.message}`); }
}
// 保留隔离目录中的 run.json 作为可审阅证据,不清理用户或仓库目录。
}
}
export function caseResults(entry, record) {
return entry.cases.map(c => {
const value = record?.result?.variables?.[c.resultVariable];
return { ...c, status: value?.[2] || (record?.status === "BLOCKED" ? "BLOCKED" : entry.manual ? "MANUAL" : "UNRUN"), actual: value?.[5], reason: c.blockedReason || record?.reason };
});
}
export function makeReport(evidence) {
const { groups } = readBundle();
const records = new Map((evidence.runs || []).map(r => [r.flowId, r]));
const cases = groups.flatMap(g => caseResults(g.entry, records.get(g.entry.id)));
const priority = ["FAIL", "BLOCKED", "MANUAL", "UNRUN", "PASS"];
const catalog = staticCatalog();
const components = new Map(catalog.components.map(c => [c.id, c]));
const rows = groups.flatMap(({ entry }) => entry.components.map(id => {
const matches = cases.filter(c => c.componentId === id);
const status = priority.find(s => matches.some(c => c.status === s)) || "UNRUN";
return { id, name: components.get(id)?.name || id, group: entry.id.slice(-2), status };
}));
const counts = Object.fromEntries(priority.map(s => [s, rows.filter(r => r.status === s).length]));
const escape = value => String(value ?? "无执行记录").replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ").slice(0, 1200);
const recordedAt = evidence.generatedAt ? new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", dateStyle: "medium", timeStyle: "medium", hour12: false }).format(new Date(evidence.generatedAt)) + "(北京时间)" : "未执行";
const lines = ["# 应用与系统组件测试报告", "", `记录时间:${recordedAt}。覆盖 50 项可执行组件,13 组任务。`, "",
`按组件统计:通过 ${counts.PASS},失败 ${counts.FAIL},阻塞 ${counts.BLOCKED},待人工确认 ${counts.MANUAL},未运行 ${counts.UNRUN}。`, "",
"状态依据是断言、前置校验和人工证据,节点执行成功不直接代表功能通过。未执行手动系统操作。", "",
"| 组 | 组件 ID | 名称 | 状态 |", "|---|---|---|---|"];
for (const r of rows) lines.push(`| ${r.group} | ${r.id} | ${r.name} | ${statusNames[r.status]} |`);
if (evidence.ui) lines.push("", "## 真实界面验证", "", evidence.ui.passed ? "已通过独立 Electron 窗口检查:类型下拉选项、PID/名称参数切换、DOS/PowerShell 参数切换,以及唤醒组件不再索要账号密码。界面快照及 ui-check.json 保存在 test-output/app-system/。" : `界面检查未通过:${escape(evidence.ui.reason)}`);
lines.push("", "## 用例证据", "", "| 用例 | 组件 | 状态 | 预期 | 实际/原因 |", "|---|---|---|---|---|");
for (const c of cases) lines.push(`| ${c.id} | ${c.componentId} | ${statusNames[c.status]} | ${escape(c.expected)} | ${escape([c.actual === undefined ? undefined : JSON.stringify(c.actual), c.reason].filter(Boolean).join(";") || undefined)} |`);
lines.push("", "## 运行前校验阻塞", "");
for (const r of evidence.runs || []) {
if (!r.validation?.length) continue;
lines.push(`- ${r.flowId}:${r.validation.map(i => i.message).join(";")}`);
}
lines.push("", "## 证据位置", "", `隔离用户目录:${evidence.profile || "未执行"}`, "");
for (const r of evidence.runs || []) if (r.persistedFile) lines.push(`- ${r.flowId}:${r.persistedFile}`);
return { markdown: lines.join("\n") + "\n", counts, components: rows, cases };
}
function option(name, fallback) {
const index = process.argv.indexOf(name);
if (index < 0) return fallback;
if (!process.argv[index + 1] || process.argv[index + 1].startsWith("--")) throw new Error(`${name} 缺少参数`);
return process.argv[index + 1];
}
async function main() {
const action = process.argv[2];
const output = option("--output", path.join(projectRoot, "test-output/app-system"));
if (action === "generate") { const m = writeSuite(); console.log(`已生成 ${m.groups.length} 组任务。`); }
else if (action === "verify") {
const checked = await verifyBundle();
console.log(JSON.stringify({ groups: checked.groups.length, components: checked.executableCount, validation: checked.validation }, null, 2));
} else if (action === "install") {
await verifyBundle();
const userData = option("--user-data", path.join(process.env.APPDATA || path.join(os.homedir(), "AppData/Roaming"), "rpa-studio"));
console.log(JSON.stringify(installBundle(userData), null, 2));
} else if (action === "upgrade") {
await verifyBundle();
const baseline = option("--baseline", undefined);
if (!baseline) throw new Error("upgrade 需要 --baseline 指定升级前的旧流程模板目录。");
const userData = option("--user-data", path.join(process.env.APPDATA || path.join(os.homedir(), "AppData/Roaming"), "rpa-studio"));
const backup = path.join(output, `installed-backup-${Date.now()}`);
console.log(JSON.stringify(upgradeBundle(userData, baseline, backup), null, 2));
} else if (action === "run-safe") {
await verifyBundle();
const evidence = await runSafe(output, process.argv.includes("--ui-check"));
const report = makeReport(evidence);
fs.writeFileSync(path.join(output, "report.md"), report.markdown);
console.log(JSON.stringify(report.counts));
// 2 表示组件存在失败/阻塞;1 才表示测试工具自身无法完成。
if (report.counts.FAIL || report.counts.BLOCKED) process.exitCode = 2;
} else if (action === "report") {
const evidence = JSON.parse(fs.readFileSync(option("--evidence", path.join(output, "latest.json")), "utf8"));
const report = makeReport(evidence);
const destination = option("--file", path.join(output, "report.md"));
fs.writeFileSync(destination, report.markdown);
console.log(JSON.stringify({ file: destination, counts: report.counts }, null, 2));
} else {
console.log("用法:node scripts/app-system-tests.mjs generate|verify|install|upgrade|run-safe|report\n安装:install --user-data <应用用户目录>\n升级:upgrade --baseline <旧模板目录> --user-data <应用用户目录>\n运行:run-safe --output <证据目录>\n报告:report --evidence <latest.json> --file <报告.md>");
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch(error => { console.error(error.stack || error.message); process.exitCode = 1; });
}

View File

@ -0,0 +1,179 @@
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);
});
});

View File

@ -12,6 +12,7 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { applyCatalogOverrides } from "./catalog-overrides.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..");
@ -336,7 +337,7 @@ function main() {
};
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
fs.writeFileSync(OUT_FILE, JSON.stringify(catalog), "utf8");
fs.writeFileSync(OUT_FILE, JSON.stringify(applyCatalogOverrides(catalog)), "utf8");
const size = fs.statSync(OUT_FILE).size;
console.log(`catalog: ${counts.total} components (${counts.vendor} vendor + ${counts.native} native + ${counts.triggers} triggers)`);
console.log(`fields: ${counts.fields}; groups: ${catalog.groups.map((g) => `${g.id}:${g.count}`).join(",")}`);

View File

@ -0,0 +1,17 @@
import fs from "node:fs";
const overrides = JSON.parse(fs.readFileSync(new URL("../resources/catalog-overrides.json", import.meta.url), "utf8"));
/** 把本项目已实现的参数契约覆盖到派生目录,避免重新生成时恢复旧的错误元数据。 */
export function applyCatalogOverrides(catalog) {
for (const component of catalog.components) {
const override = overrides[component.id];
if (!override) continue;
const { fields = {}, removeFields = [], ...metadata } = override;
Object.assign(component, metadata);
component.fields = component.fields.filter(field => !removeFields.includes(field.key))
.map(field => ({ ...field, ...fields[field.key] }));
}
catalog.counts.fields = catalog.components.reduce((count, component) => count + component.fields.length, 0);
return catalog;
}

View File

@ -0,0 +1,85 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { CatalogComponent, FlowNode } from "../../shared/types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
const mocks = vi.hoisted(() => ({ powershell: vi.fn(), execFile: vi.fn() }));
vi.mock("./desktop", () => ({ powershell: mocks.powershell, psString: (value: unknown) => `'${String(value ?? "").replace(/'/g, "''")}'` }));
vi.mock("node:child_process", () => ({ execFile: mocks.execFile }));
import "./data";
import "./control";
import "./system-extended";
import "./windows-system";
function run(id: string, args: Record<string, unknown> = {}, signal = new AbortController().signal) {
const node: FlowNode = { id: "regression", componentId: id, values: args, options: {}, position: { x: 0, y: 0 } };
return getHandler(id)!({ signal, log: vi.fn(), vars: {} } as unknown as RunContext, node, args, {} as CatalogComponent);
}
afterEach(() => { vi.useRealTimers(); vi.resetAllMocks(); });
describe("应用与系统组件缺陷回归", () => {
it("没有匹配进程时返回 [] / null,忽略空行和非法 PID", async () => {
mocks.powershell.mockResolvedValue("");
await expect(run("App_GetPids", { pattern: "missing.exe" })).resolves.toEqual([]);
await expect(run("App_GetPid", { pattern: "missing.exe" })).resolves.toBeNull();
mocks.powershell.mockResolvedValue("\r\n0\r\n123\r\nabc\r\n456\r\n");
await expect(run("App_GetPids", { pattern: "fixture.exe" })).resolves.toEqual([123, 456]);
});
it("毫秒等待读取 mileSecond,计时和取消均正确", async () => {
vi.useFakeTimers();
const waiting = Promise.resolve(run("Basic_Delay", { mileSecond: "250" }));
let done = false;
void waiting.then(() => { done = true; });
await vi.advanceTimersByTimeAsync(249);
expect(done).toBe(false);
await vi.advanceTimersByTimeAsync(1);
expect(done).toBe(true);
const controller = new AbortController();
const cancelled = Promise.resolve(run("Basic_Delay", { mileSecond: "3000" }, controller.signal));
const rejection = expect(cancelled).rejects.toMatchObject({ code: "cancelled" });
controller.abort();
await rejection;
});
it.each([
[42, "number", true], ["42", "number", false], [42, "1", true],
[[], "list", true], [{ key: 1 }, "dict", true], [null, "null", true],
[false, "bool", true], ["中文", "string", true], [3.5, "int", false]
])("按目标类型判断 %j → %s", (val, into_type, expected) => {
expect(run("Basic_JudgeDataType", { val, into_type })).toBe(expected);
});
it.each([
["42.5", "number", 42.5], ["42", "1", 42], ["-3.9", "int", -3],
["false", "bool", false], ["true", "bool", true], [42, "string", "42"]
])("执行类型转换 %j → %s", (val, into_type, expected) => {
expect(run("Basic_DataTypeConversion", { val, into_type })).toBe(expected);
});
it("非法数值或未知目标类型给出定位错误,避免静默返回原值", () => {
expect(() => run("Basic_DataTypeConversion", { val: "abc", into_type: "int" })).toThrow();
expect(() => run("Basic_DataTypeConversion", { val: 1, into_type: "unknown" })).toThrow();
expect(() => run("Basic_JudgeDataType", { val: 1, into_type: "unknown" })).toThrow();
});
it("关闭组件按所选模式执行,非法 PID 不降级为按名称批量关闭", async () => {
mocks.powershell.mockResolvedValue("");
await expect(run("App_KillV1", { close_type: "id", pid: "invalid", p_name: "fixture.exe" })).rejects.toMatchObject({ code: "validation_error" });
expect(mocks.powershell).not.toHaveBeenCalled();
await run("App_KillV1", { close_type: "name", pid: 123, p_name: "fixture.exe", kill_all: "0" });
expect(mocks.powershell.mock.calls[0][0]).toContain("SessionId");
expect(mocks.powershell.mock.calls[0][0]).not.toContain("Stop-Process -Id 123");
});
it("唤醒不声称已通过 Windows 登录解锁", async () => {
mocks.execFile.mockImplementation((_file, _args, _opts, callback) => callback(null, { stdout: "", stderr: "" }));
await expect(run("ScreenControl_ScreenUnlock")).resolves.toMatchObject({ awakened: true, loginUnlockSupported: false });
});
it("屏保自定义密码不受支持时,在触发桌面操作前明确报错", async () => {
await expect(run("System_ScreenSaverShowV1", { set_password: "1", password: "" })).rejects.toMatchObject({ code: "validation_error" });
expect(mocks.execFile).not.toHaveBeenCalled();
});
});

View File

@ -81,6 +81,7 @@ describe("capability registration smoke", () => {
const execute = getHandler("System_ExecuteCommand");
expect(execute).toBeTruthy();
await expect(execute!(context, node("System_ExecuteCommand"), { action: "Write-Output smoke" }, component)).resolves.toContain("smoke");
await expect(execute!(context, node("System_ExecuteCommand"), { action: "Write-Output '中文输出测试'" }, component)).resolves.toBe("中文输出测试");
});
it("finds an exact PNG template inside a larger screenshot", async () => {

View File

@ -30,12 +30,21 @@ register("Basic_Delay_V2", async (ctx, node, args) => {
return seconds;
});
register(["Basic_Delay_V1", "Basic_Delay"], async (ctx, node, args) => {
register("Basic_Delay_V1", async (ctx, node, args) => {
const seconds = toNumber(args.second ?? args.time ?? 1, 1);
await sleep(Math.max(0, seconds) * 1000, ctx.signal);
return seconds;
});
register("Basic_Delay", async (ctx, node, args) => {
const milliseconds = Number(args.mileSecond ?? 1000);
if (!Number.isFinite(milliseconds) || milliseconds < 0 || milliseconds > 2_147_483_647) {
throw new RuntimeError("validation_error", "延时时间必须是 0~2147483647 范围内的毫秒数", { nodeId: node.id, componentId: node.componentId });
}
await sleep(milliseconds, ctx.signal);
return milliseconds / 1000;
});
register("Basic_DebugOutput", (ctx, node, args) => {
const message = String(args.msg ?? args.message ?? "");
ctx.log(message || "(空)", "info", node.id);

View File

@ -498,12 +498,30 @@ register("Basic_Type", (_ctx, _node, args) => {
if (Array.isArray(value)) return "list";
return typeof value;
});
register("Basic_JudgeDataType", (_ctx, _node, args) => {
// 原始目录缺少选项映射。新界面使用语义值;数字别名兼容此前默认的 "1"。
const JUDGE_TYPE_ALIASES: Record<string, string> = { "1": "number", "2": "null", "3": "dict", "4": "string", "5": "list", "6": "bool", "7": "int" };
const CONVERT_TYPE_ALIASES: Record<string, string> = { "1": "number", "2": "int", "3": "bool", "4": "string" };
function targetType(raw: unknown, aliases: Record<string, string>, supported: string[], nodeId: string, componentId: string): string {
const text = String(raw ?? "number").trim().toLowerCase();
const synonyms: Record<string, string> = { float: "number", boolean: "bool", str: "string", text: "string", array: "list", dictionary: "dict", object: "dict", empty: "null" };
const type = aliases[text] || synonyms[text] || text;
if (!supported.includes(type)) throw new RuntimeError("validation_error", "目标数据类型无效,请重新选择类型", { nodeId, componentId });
return type;
}
register("Basic_JudgeDataType", (_ctx, node, args) => {
const value = first(args, ["val"]);
if (value === null || value === undefined) return "null";
if (Array.isArray(value)) return "list";
if (typeof value === "object") return "dict";
return typeof value;
const type = targetType(args.into_type, JUDGE_TYPE_ALIASES, ["number", "null", "dict", "string", "list", "bool", "int"], node.id, node.componentId);
switch (type) {
case "number": return typeof value === "number" && Number.isFinite(value);
case "int": return typeof value === "number" && Number.isInteger(value);
case "null": return value === null || value === undefined || value === "";
case "dict": return Boolean(value) && typeof value === "object" && !Array.isArray(value);
case "string": return typeof value === "string";
case "list": return Array.isArray(value);
case "bool": return typeof value === "boolean";
}
});
register("Basic_Clone", (_ctx, _node, args) => structuredClone(first(args, ["val"])));
register("Basic_RandomInt", (_ctx, _node, args) => {
@ -512,4 +530,23 @@ register("Basic_RandomInt", (_ctx, _node, args) => {
return Math.floor(min + Math.random() * (max - min + 1));
});
register("Basic_Rnd", () => Math.random());
register("Basic_DataTypeConversion", (_ctx, _node, args) => first(args, ["val"]));
register("Basic_DataTypeConversion", (_ctx, node, args) => {
const value = first(args, ["val"]);
const type = targetType(args.into_type, CONVERT_TYPE_ALIASES, ["number", "int", "bool", "string"], node.id, node.componentId);
if (type === "string") return str(value);
if (type === "bool") {
if (value == null || value === "") return false;
if (typeof value === "boolean") return value;
if (typeof value === "number" && Number.isFinite(value)) return value !== 0;
const text = String(value).trim().toLowerCase();
if (["true", "1", "是", "开启"].includes(text)) return true;
if (["false", "0", "否", "关闭"].includes(text)) return false;
throw new RuntimeError("validation_error", "输入值无法转换为布尔类型", { nodeId: node.id, componentId: node.componentId });
}
if ((typeof value !== "number" && typeof value !== "string") || (typeof value === "string" && !value.trim()) || !Number.isFinite(Number(value))) {
throw new RuntimeError("validation_error", "输入值不是有效数字", { nodeId: node.id, componentId: node.componentId });
}
const result = type === "int" ? Math.trunc(Number(value)) : Number(value);
if (type === "int" && !Number.isSafeInteger(result)) throw new RuntimeError("validation_error", "转换后的整数超出安全范围", { nodeId: node.id, componentId: node.componentId });
return result;
});

View File

@ -80,7 +80,9 @@ function encoded(command: string): string {
export async function powershell(command: string, timeout = 30000): Promise<string> {
try {
const result = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded(command)], {
// Windows PowerShell 的默认输出编码依机器代码页变化;Node 按 UTF-8 解码管道。
const utf8 = "$ProgressPreference='SilentlyContinue'; [Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false); $OutputEncoding=[Console]::OutputEncoding; ";
const result = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded(utf8 + command)], {
windowsHide: true,
timeout,
maxBuffer: 4 * 1024 * 1024

View File

@ -75,21 +75,30 @@ register("System_GetSystemPath", (_ctx, _node, args) => {
const getPids: CapabilityHandler = async (_ctx, node, args) => {
const pattern = processPattern(required(args, ["pattern", "p_name", "name"], "进程名称", node.id, node.componentId));
const output = await powershell(`Get-Process | Where-Object { $_.ProcessName -like ${psString(pattern)} } | Select-Object -ExpandProperty Id`);
return output.split(/\r?\n/).map(Number).filter(Number.isFinite);
return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
.map(Number).filter((pid) => Number.isSafeInteger(pid) && pid > 0);
};
register("App_GetPids", getPids);
register("App_GetPid", async (ctx, node, args, component) => (await getPids(ctx, node, args, component) as number[])[0] ?? null);
register(["App_Kill", "App_KillV1"], async (_ctx, node, args) => {
const pid = Number(value(args, ["pid"]));
const name = value(args, ["p_name", "pattern"]);
if (Number.isFinite(pid) && pid > 0) {
const mode = node.componentId === "App_Kill" ? "id" : value(args, ["close_type"], "id");
if (mode === "id") {
const rawPid = args.pid;
const pid = typeof rawPid === "number" || typeof rawPid === "string" ? Number(rawPid) : NaN;
if (!Number.isSafeInteger(pid) || pid <= 0) {
throw new RuntimeError("validation_error", "进程 PID 必须是正整数", { nodeId: node.id, componentId: node.componentId });
}
if (pid === process.pid) throw new RuntimeError("validation_error", "拒绝关闭 RPA Studio 自身进程", { nodeId: node.id, componentId: node.componentId });
await powershell(`Stop-Process -Id ${pid} -Force -ErrorAction Stop`);
return pid;
}
const pattern = processPattern(required({ name }, ["name"], "进程名称", node.id, node.componentId));
await powershell(`Get-Process | Where-Object { $_.ProcessName -like ${psString(pattern)} } | Stop-Process -Force -ErrorAction Stop`);
if (mode !== "name") throw new RuntimeError("validation_error", "请选择按 PID 或按进程名称关闭", { nodeId: node.id, componentId: node.componentId });
const pattern = processPattern(required(args, ["p_name", "pattern"], "进程名称", node.id, node.componentId));
if (pattern === "*") throw new RuntimeError("validation_error", "关闭应用程序必须指定具体的进程名称", { nodeId: node.id, componentId: node.componentId });
const allSessions = [true, 1, "1", "true", "True"].includes(args.kill_all as never);
const session = allSessions ? "" : " -and $_.SessionId -eq [System.Diagnostics.Process]::GetCurrentProcess().SessionId";
await powershell(`Get-Process | Where-Object { $_.ProcessName -like ${psString(pattern)} -and $_.Id -ne ${process.pid}${session} } | Stop-Process -Force -ErrorAction Stop`);
return pattern;
});

View File

@ -79,6 +79,20 @@ describe("utility extended capabilities", () => {
await expect(run("System_ExecuteCommandV1", args)).resolves.toContain("utility-ok");
});
it("executes BAT files whose path contains spaces and Chinese characters", async () => {
if (process.platform !== "win32") return;
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-utility-中文 空格-"));
directories.push(directory);
const file = path.join(directory, "测试 脚本.bat");
await writeFile(file, "@echo RPA-BAT-PATH-OK\r\n", "ascii");
await expect(run("System_ExecuteCommandV1", { command_type: "2", bat_file: file, get_ret: "1" })).resolves.toBe("RPA-BAT-PATH-OK");
});
it("keeps PowerShell command output in UTF-8", async () => {
if (process.platform !== "win32") return;
await expect(run("System_ExecuteCommandV1", { command_type: "3", ps_command: "Write-Output '中文输出测试'", get_ret: "1" })).resolves.toBe("中文输出测试");
});
it("compares local text files with structured line changes", async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-utility-"));
directories.push(directory);

View File

@ -252,6 +252,7 @@ register("Basic_GetProcessInfo", (ctx) => ({
register("System_ExecuteCommandV1", async (_ctx, node, args) => {
const mode = String(args.command_type || "1");
if (!["1", "2", "3"].includes(mode)) throw new RuntimeError("validation_error", "请选择 DOS、BAT 或 PowerShell 命令类型", { nodeId: node.id, componentId: node.componentId });
const cwd = String(args.cwd || "").trim() || undefined;
const timeout = Math.max(1_000, Number(args.timeoutMs || 120_000));
let executable: string;
@ -264,7 +265,9 @@ register("System_ExecuteCommandV1", async (_ctx, node, args) => {
commandArgs = ["/d", "/s", "/c", `call "${file}"`];
} else if (mode === "3") {
executable = "powershell.exe";
commandArgs = ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", requiredText(args.ps_command, "PowerShell命令", node.id, node.componentId)];
const utf8 = "$ProgressPreference='SilentlyContinue'; [Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false); $OutputEncoding=[Console]::OutputEncoding; ";
const script = utf8 + requiredText(args.ps_command, "PowerShell命令", node.id, node.componentId);
commandArgs = ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")];
} else {
executable = process.env.ComSpec || "cmd.exe";
commandArgs = ["/d", "/s", "/c", requiredText(args.dos_command, "DOS命令", node.id, node.componentId)];
@ -276,7 +279,8 @@ register("System_ExecuteCommandV1", async (_ctx, node, args) => {
executable = "/bin/sh";
commandArgs = ["-lc", command];
}
const result = await execFileAsync(executable, commandArgs, { cwd, timeout, windowsHide: true, maxBuffer: 10 * 1024 * 1024 });
// cmd.exe 自己解析 /c 后的命令;不能让 Node 再把 BAT 路径引号转成反斜杠引号。
const result = await execFileAsync(executable, commandArgs, { cwd, timeout, windowsHide: true, windowsVerbatimArguments: process.platform === "win32" && mode !== "3", maxBuffer: 10 * 1024 * 1024 });
return bool(args.get_ret, true) ? String(result.stdout).trimEnd() : true;
});

View File

@ -22,13 +22,16 @@ register("ScreenControl_Screenlock", async () => {
register("ScreenControl_ScreenUnlock", async () => {
// 仅唤醒屏幕/解除屏保;系统锁屏需人工输入密码,无法通用自动解锁
await ps("Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint dx, uint dy, uint d, UIntPtr e);' -Name W32U -Namespace WinRpa | Out-Null; [WinRpa.W32U]::mouse_event(0x0001, 1, 1, 0, [UIntPtr]::Zero); Start-Sleep -Milliseconds 200; [WinRpa.W32U]::mouse_event(0x0001, 0xFFFFFFFF, 0, 0, [UIntPtr]::Zero)");
return { awakened: true, note: "已唤醒屏幕;锁屏需人工输入密码" };
return { awakened: true, loginUnlockSupported: false, note: "仅尝试唤醒屏幕;Windows 登录锁屏仍需本人手动解锁" };
});
const SC_SCREENSAVE = 0xF140;
const WM_SYSCOMMAND = 0x0112;
register(["System_ScreenSaverShow", "System_ScreenSaverShowV1"], async () => {
await ps("Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern IntPtr FindWindow(string cls, string win); [DllImport(\"user32.dll\")] public static extern IntPtr SendMessage(IntPtr h, uint m, IntPtr w, IntPtr l);' -Name W32S -Namespace WinRpa | Out-Null; [WinRpa.W32S]::SendMessage([IntPtr]0xFFFF, [UIntPtr]0x0112, [IntPtr]0xF140, [IntPtr]::Zero) | Out-Null");
register(["System_ScreenSaverShow", "System_ScreenSaverShowV1"], async (_ctx, node, args) => {
if ([true, 1, "1", "true", "True"].includes(args.set_password as never)) {
throw new RuntimeError("validation_error", "系统屏保不支持设置自定义解锁密码,请关闭该选项并使用 Windows 原有登录保护", { nodeId: node.id, componentId: node.componentId });
}
await ps("Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern IntPtr SendMessage(IntPtr h, uint m, IntPtr w, IntPtr l);' -Name W32S -Namespace WinRpa | Out-Null; [WinRpa.W32S]::SendMessage([IntPtr]0xFFFF, [uint32]0x0112, [IntPtr]0xF140, [IntPtr]::Zero) | Out-Null");
return { screensaver: true };
});

View File

@ -17,6 +17,7 @@ import type { Catalog, CatalogField, FlowNode, FlowVariable } from "@shared/type
import { useEditorStore } from "../store/editorStore";
import { groupIcon } from "../components/icons";
import { containerSlots } from "../canvas/ContainerNode";
import { fieldIsActive } from "../validation/flowValidation";
type Tab = "props" | "vars" | "run";
@ -247,8 +248,9 @@ function PropertyTab({
onToast: (message: string, kind?: "ok" | "error") => void;
}) {
const [advancedOpen, setAdvancedOpen] = useState(false);
const basic = component.fields.filter((field) => !field.advanced && field.widget !== "hidden");
const advanced = component.fields.filter((field) => field.advanced && field.widget !== "hidden");
const active = component.fields.filter((field) => field.widget !== "hidden" && fieldIsActive(component.id, field.key, node.values));
const basic = active.filter((field) => !field.advanced);
const advanced = active.filter((field) => field.advanced);
const slots = component.slots.length > 0 ? containerSlots(component) : [];
return (

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { Catalog, FlowDefinition } from "@shared/types";
import { validateFlow } from "./flowValidation";
import { fieldIsActive, validateFlow } from "./flowValidation";
const catalog = {
schemaVersion: 1, generatedAt: "", counts: { total: 2, vendor: 1, native: 0, triggers: 1 }, groups: [],
@ -20,6 +20,24 @@ function flow(): FlowDefinition {
}
describe("flow validation", () => {
it("关闭程序按模式仅显示并校验 PID 或进程名称", () => {
expect(fieldIsActive("App_KillV1", "pid", { close_type: "id" })).toBe(true);
expect(fieldIsActive("App_KillV1", "p_name", { close_type: "id" })).toBe(false);
expect(fieldIsActive("App_KillV1", "pid", { close_type: "name" })).toBe(false);
expect(fieldIsActive("App_KillV1", "p_name", { close_type: "name" })).toBe(true);
});
it.each(["1", "2", "3"])("命令模式 %s 仅显示并校验当前命令输入", (mode) => {
const fields = ["dos_command", "bat_file", "ps_command"];
const active = fields.filter(key => fieldIsActive("System_ExecuteCommandV1", key, { command_type: mode }));
expect(active).toEqual([fields[Number(mode) - 1]]);
});
it("旧屏保元数据不再要求未启用的密码", () => {
expect(fieldIsActive("System_ScreenSaverShowV1", "password", { set_password: "0" })).toBe(false);
expect(fieldIsActive("System_ScreenSaverShowV1", "password", { set_password: "1" })).toBe(true);
});
it("reports pending components, required fields, and disconnected nodes", () => {
const issues = validateFlow(flow(), catalog);
expect(issues).toEqual(expect.arrayContaining([

View File

@ -15,7 +15,25 @@ function truthy(value: unknown): boolean {
return [true, 1, "1", "true", "True", "是", "开启"].includes(value as never);
}
/** 属性显示与必填校验使用相同的模式条件,切换模式时保留未激活字段的旧值。 */
export function fieldIsActive(componentId: string, key: string, values: Record<string, unknown>): boolean {
if (componentId === "App_KillV1") {
const mode = String(values.close_type ?? "id");
if (key === "pid") return mode === "id";
if (key === "p_name" || key === "kill_all") return mode === "name";
}
if (componentId === "System_ExecuteCommandV1") {
const mode = String(values.command_type ?? "1");
if (["dos_command", "bat_file", "ps_command"].includes(key)) {
return key === ({ "1": "dos_command", "2": "bat_file", "3": "ps_command" } as Record<string, string>)[mode];
}
}
if (componentId === "System_ScreenSaverShowV1" && key === "password") return truthy(values.set_password);
return true;
}
function requiredInContext(componentId: string, key: string, values: Record<string, unknown>): boolean {
if (!fieldIsActive(componentId, key, values)) return false;
if (key === "designated_obj" && !truthy(values.designated_info)) return false;
if (key === "cvSelector" && String(values.choose ?? "0") !== "1") return false;
if (componentId === "Image_Screenshot" && ["image_path", "outImagePath", "outImageName"].includes(key)) return false;