merge: bring liuwenxin app-system fixes into main
This commit is contained in:
commit
ee60435e2b
@ -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)
|
||||
|
||||
68
resources/catalog-overrides.json
Normal file
68
resources/catalog-overrides.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
@ -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": "",
|
||||
|
||||
106
resources/test-flows/app-system/README.md
Normal file
106
resources/test-flows/app-system/README.md
Normal 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)。
|
||||
153
resources/test-flows/app-system/REPORT-before-fix.md
Normal file
153
resources/test-flows/app-system/REPORT-before-fix.md
Normal 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
|
||||
159
resources/test-flows/app-system/REPORT.md
Normal file
159
resources/test-flows/app-system/REPORT.md
Normal 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
|
||||
BIN
resources/test-flows/app-system/fixtures/tone.wav
Normal file
BIN
resources/test-flows/app-system/fixtures/tone.wav
Normal file
Binary file not shown.
861
resources/test-flows/app-system/manifest.json
Normal file
861
resources/test-flows/app-system/manifest.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1776
resources/test-flows/app-system/test_appsystem_01.json
Normal file
1776
resources/test-flows/app-system/test_appsystem_01.json
Normal file
File diff suppressed because it is too large
Load Diff
2195
resources/test-flows/app-system/test_appsystem_02.json
Normal file
2195
resources/test-flows/app-system/test_appsystem_02.json
Normal file
File diff suppressed because it is too large
Load Diff
2379
resources/test-flows/app-system/test_appsystem_03.json
Normal file
2379
resources/test-flows/app-system/test_appsystem_03.json
Normal file
File diff suppressed because it is too large
Load Diff
2041
resources/test-flows/app-system/test_appsystem_04.json
Normal file
2041
resources/test-flows/app-system/test_appsystem_04.json
Normal file
File diff suppressed because it is too large
Load Diff
1629
resources/test-flows/app-system/test_appsystem_05.json
Normal file
1629
resources/test-flows/app-system/test_appsystem_05.json
Normal file
File diff suppressed because it is too large
Load Diff
1507
resources/test-flows/app-system/test_appsystem_06.json
Normal file
1507
resources/test-flows/app-system/test_appsystem_06.json
Normal file
File diff suppressed because it is too large
Load Diff
1128
resources/test-flows/app-system/test_appsystem_07.json
Normal file
1128
resources/test-flows/app-system/test_appsystem_07.json
Normal file
File diff suppressed because it is too large
Load Diff
897
resources/test-flows/app-system/test_appsystem_08.json
Normal file
897
resources/test-flows/app-system/test_appsystem_08.json
Normal 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"
|
||||
}
|
||||
}
|
||||
870
resources/test-flows/app-system/test_appsystem_09.json
Normal file
870
resources/test-flows/app-system/test_appsystem_09.json
Normal 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"
|
||||
}
|
||||
}
|
||||
455
resources/test-flows/app-system/test_appsystem_10.json
Normal file
455
resources/test-flows/app-system/test_appsystem_10.json
Normal file
File diff suppressed because one or more lines are too long
1087
resources/test-flows/app-system/test_appsystem_11.json
Normal file
1087
resources/test-flows/app-system/test_appsystem_11.json
Normal file
File diff suppressed because it is too large
Load Diff
654
resources/test-flows/app-system/test_appsystem_12.json
Normal file
654
resources/test-flows/app-system/test_appsystem_12.json
Normal 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"
|
||||
}
|
||||
}
|
||||
735
resources/test-flows/app-system/test_appsystem_13.json
Normal file
735
resources/test-flows/app-system/test_appsystem_13.json
Normal 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"
|
||||
}
|
||||
}
|
||||
279
scripts/app-system-test-flows.mjs
Normal file
279
scripts/app-system-test-flows.mjs
Normal 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;
|
||||
}
|
||||
390
scripts/app-system-tests.mjs
Normal file
390
scripts/app-system-tests.mjs
Normal 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; });
|
||||
}
|
||||
179
scripts/app-system-tests.test.mjs
Normal file
179
scripts/app-system-tests.test.mjs
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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(",")}`);
|
||||
|
||||
17
scripts/catalog-overrides.mjs
Normal file
17
scripts/catalog-overrides.mjs
Normal 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;
|
||||
}
|
||||
85
src/main/capabilities/app-system-regression.test.ts
Normal file
85
src/main/capabilities/app-system-regression.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@ -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 () => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
|
||||
@ -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 };
|
||||
});
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user