android-midscene-automation 0.1.29 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -25
- package/README.md +10 -25
- package/index.html +1 -1
- package/package.json +7 -7
- package/server/appium-recorder/appium-runner.ts +49 -29
- package/server/appium-recorder/bounded-loop.ts +118 -0
- package/server/appium-recorder/repository.ts +9 -0
- package/src/appium-recorder/AppiumPage.vue +101 -45
- package/src/appium-recorder/bounded-loop.ts +66 -0
- package/src/appium-recorder/components/BreakLoopSettings.vue +29 -0
- package/src/appium-recorder/components/ComponentInspector.vue +26 -0
- package/src/appium-recorder/components/ComponentTree.vue +24 -4
- package/src/appium-recorder/components/FlowCanvas.vue +21 -1
- package/src/appium-recorder/components/FlowNodeCard.vue +12 -4
- package/src/appium-recorder/components/FlowNodeSearch.vue +62 -0
- package/src/appium-recorder/components/FlowStepEditor.vue +9 -4
- package/src/appium-recorder/components/LoopSettings.vue +37 -0
- package/src/appium-recorder/components/NestedConditionBranches.vue +8 -1
- package/src/appium-recorder/components/NodeDetail.vue +50 -8
- package/src/appium-recorder/components/RecordedSteps.vue +108 -40
- package/src/appium-recorder/components/RecorderWorkspace.vue +132 -0
- package/src/appium-recorder/flow-copy.ts +1 -0
- package/src/appium-recorder/flow-graph.ts +6 -1
- package/src/appium-recorder/flow-labels.ts +7 -0
- package/src/appium-recorder/flow-merge.ts +3 -2
- package/src/appium-recorder/flow-normalize.ts +1 -0
- package/src/appium-recorder/flow-remove.ts +6 -0
- package/src/appium-recorder/flow-search.ts +29 -0
- package/src/appium-recorder/node-locators.ts +25 -0
- package/src/appium-recorder/tree.ts +2 -5
- package/src/appium-recorder/types.ts +7 -0
- package/src/components/device/DevicePreviewPanel.vue +62 -10
- package/src/style.css +24 -7
- package/src/ui-refresh.css +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
-
## v0.1.
|
|
3
|
+
## v0.1.31
|
|
4
|
+
- 组件详情展示 Resource ID、Accessibility ID、XPath 和 UiAutomator 四种定位内容,支持分别复制,方便配置循环退出条件;缺少对应属性时显示未提供。
|
|
5
|
+
- “流程控制”新增有界循环及退出循环:支持固定次数、元素出现或消失时退出,必须设置最大执行次数;循环体支持嵌套判断、合流和连接脚本。退出节点可选择当前或外层循环,退出外层时同时结束内部循环并直接执行目标循环结束路径,日志及 HTML 报告标注轮次和退出目标。
|
|
6
|
+
- 新增流程节点搜索,支持按名称、操作类型、定位内容和日志关键字筛选,展示所属分支并在多个结果间切换;普通画布与流程总览支持定位高亮,不改变节点布局及批量选中状态。
|
|
7
|
+
- Midscene 相关模块统一从 1.12.0 升级至 1.12.2,包含 Android、Android Playground、Core、Playground、Shared 和 Web。
|
|
8
|
+
- 录制工作区支持拖拽调整设备预览、组件树栏宽,自动保存布局并支持恢复默认。
|
|
9
|
+
|
|
10
|
+
## v0.1.30
|
|
11
|
+
- 新增节点批量删除功能,支持多选节点,选中节点及所属子节点以柔和红色背景和浅色文字标记,确认后统一删除;支持普通与放大视图同步操作。
|
|
4
12
|
|
|
13
|
+
## v0.1.29
|
|
5
14
|
- 判断节点新增“合并分支”,支持选择一侧公共流程起点,将两侧后续汇入同一段居中流程;另一侧可保留独有操作或确认删除重复后缀,支持嵌套判断及各类分支,公共节点只执行一次且不随单侧节点删除。
|
|
6
15
|
- 将 Checkbox、RadioButton 判断入口合并为“判断勾选”,新增 Switch 支持;添加和回放时自动校验组件类型及可勾选属性,读取实时 checked 状态进入 true / false 分支,不改变控件状态,兼容已有独立判断节点。
|
|
7
16
|
- “流程控制”新增终止流程节点,执行到该节点时主动结束本次回放,不再执行后续节点或连接脚本;子脚本触发时也结束整个流程,保留执行日志和报告,不作为执行失败。
|
|
@@ -9,7 +18,6 @@
|
|
|
9
18
|
- Appium 配置新增流程背景色设置,默认沿用浅绿色,支持预选颜色、颜色选择器、#RGB / #RRGGBB 输入校验和恢复默认;保存后持久化,普通画布、放大视图和连接脚本预览统一应用。
|
|
10
19
|
|
|
11
20
|
## v0.1.28
|
|
12
|
-
|
|
13
21
|
- Appium 配置中的流程背景色独立成区,与节点模型配置分开展示,保留预选颜色、自定义颜色及保存入口。
|
|
14
22
|
- “组件操作”新增文字点击分支节点,无需预选元素,支持精准匹配和模糊匹配原生组件文字;匹配并点击后进入“匹配到文字”,等待超时未找到则进入“未匹配到文字”。支持编辑文字和超时,多元素匹配及点击异常明确报错,日志和报告保留分支结果。
|
|
15
23
|
- “流程控制”新增输出日志节点,支持设置日志内容和自定义关键字,默认按 `stageLog:内容` 输出;支持添加后编辑,回放实时输出、日志文件及报告均保留日志内容,多行内容逐行添加关键字。
|
|
@@ -18,7 +26,6 @@
|
|
|
18
26
|
- 新增原生 Checkbox 和 RadioButton 状态判断节点,添加时校验选中元素类型,回放时读取 `checked` 属性,按 `true / false` 进入对应分支,不改变勾选状态;组件详情显示原生勾选属性,回放日志和报告记录判断结果。
|
|
19
27
|
|
|
20
28
|
## v0.1.27
|
|
21
|
-
|
|
22
29
|
- npx android-midscene-automation 启动时默认使用固定的系统用户数据目录保存脚本、配置、报告和运行态文件,并在启动时尝试迁移旧版启动目录下的数据,降低升级后脚本或配置“不见了”的风险。
|
|
23
30
|
- 新增脚本列表复制和重命名能力,可直接生成“原文件名 Copy”副本并修改脚本名称,便于复用已有录制流程。
|
|
24
31
|
- 新增检测画面变化能力,支持框选设备预览区域或使用当前选中元素作为检测区域,通过“检测画面变化N-开始节点 / 结束节点”对同一分支内的前后截图进行差异对比。
|
|
@@ -28,34 +35,28 @@
|
|
|
28
35
|
- 回放报告截图和截图节点改为使用 ADB 截图,避免 Appium /screenshot 超时后堵塞 UiAutomator2 命令队列;Appium 请求超时时也会显示更准确的超时提示。
|
|
29
36
|
|
|
30
37
|
## v0.1.26
|
|
31
|
-
|
|
32
38
|
- 修复流程节点复制与粘贴问题,避免 `structuredClone` 失败、粘贴后原节点消失、分支插入位置错误和复制后相对位置偏移。
|
|
33
39
|
- 修复判断分支已有节点时入口添加按钮缺失的问题,分支入口现在会保留可插入节点,并正确连接到首个分支节点。
|
|
34
40
|
- 滑动操作支持在添加弹窗和节点配置中修改起点、终点坐标及滑动时长。
|
|
35
41
|
- 长按操作支持设置长按时间,并在节点卡片中显示长按坐标和时长。
|
|
36
42
|
|
|
37
43
|
## v0.1.25
|
|
38
|
-
|
|
39
44
|
- Android 设备预览恢复使用 scrcpy 实时流,优化实时流参数,减少 H.264 丢帧导致的花屏;刷新画面时会重启底层 Playground 预览流并等待会话就绪。
|
|
40
45
|
- 测试脚本生成新增提示词模板,可在通用提示词、广告测试、表单填写、聊天对话、播放器测试、搜索列表、订单支付、权限弹窗、个人设置和 H5/WebView 等场景间切换,并支持编辑或上传当前原始 Prompt。
|
|
41
46
|
- 优化 AI 生成流程,支持后台处理和终止生成;生成中锁定原始 Prompt,再次点击“AI 生成”可恢复进度弹窗,终止后不再弹出额外提示。
|
|
42
47
|
- 优化脚本生成提示词策略,区分广告、播放器、弹窗等被测目标与无关遮挡内容,登录流程增加登录后遮挡弹窗处理和无遮挡首页成功判定。
|
|
43
48
|
|
|
44
49
|
## v0.1.24
|
|
45
|
-
|
|
46
50
|
- 参数配置的运行配置新增保存校验,Android SDK 路径必须包含 `platform-tools/adb`,回放报告目录必须填写已存在且可写入的绝对路径。
|
|
47
51
|
- Midscene 自定义提供方的 `Model Name` 和 `Model Family` 改为输入框,`Base URL` 匹配已知提供方时自动填充模型名称和模型系列,并在字段旁新增官方配置说明入口。
|
|
48
52
|
|
|
49
53
|
## v0.1.23
|
|
50
|
-
|
|
51
54
|
- 常见问题拆分为独立 `FAQ.md` 文档,并在 README 的“功能文档”中增加跳转入口。
|
|
52
55
|
|
|
53
56
|
## v0.1.22
|
|
54
|
-
|
|
55
57
|
- 升级 Midscene 相关依赖到 `v1.12.0`
|
|
56
58
|
|
|
57
59
|
## v0.1.21
|
|
58
|
-
|
|
59
60
|
- 录制步骤流程图升级为独立画布与节点卡片视图,优化节点内容排版、操作图标、选中态、批量复制和还原位置体验。
|
|
60
61
|
- 统一普通流程线、操作按钮连线和判断分支线条样式,按节点实际高度计算连接位置,修复线条颜色不一致、断裂、多余线条、节点重叠和分支错位问题。
|
|
61
62
|
- 优化判断分支布局,统一“是 / 否”分支的 T 形连接样式,避免分支节点堆叠、左右分支连接错乱及多出第三分支。
|
|
@@ -65,36 +66,29 @@
|
|
|
65
66
|
- 修复放大面板后点击操作按钮或节点配置会重置缩放的问题,保持当前画布大小和位置。
|
|
66
67
|
|
|
67
68
|
## v0.1.20
|
|
68
|
-
|
|
69
69
|
- Appium 流程支持单节点、连续节点及完整判断子流程的复制与粘贴。
|
|
70
70
|
- 优化判断分支布局和流程总览,支持在总览中编辑完整流程。
|
|
71
71
|
- Appium session 与“启动 APP”节点解耦,并避免将桌面、设置等系统 Activity 误识别为脚本入口。
|
|
72
72
|
- 修复复杂判断脚本加载失败及加载后无法进入“当前录制”的问题。
|
|
73
73
|
|
|
74
74
|
## v0.1.19
|
|
75
|
-
|
|
76
75
|
- 回放输出支持保留完整 Appium 服务日志,并可在执行过程中手动终止。
|
|
77
76
|
- 新增单文件 HTML 截图回放报告,提供时间轴、步骤导航和连续播放效果。
|
|
78
77
|
|
|
79
78
|
## v0.1.18
|
|
80
|
-
|
|
81
79
|
- 使用说明和更新记录自动嵌入 README,可直接在 npm 页面渲染查看。
|
|
82
80
|
|
|
83
81
|
## v0.1.17
|
|
84
|
-
|
|
85
82
|
- 新增在线文档查看入口,支持渲染使用说明和更新记录。
|
|
86
83
|
|
|
87
84
|
## v0.1.16
|
|
88
|
-
|
|
89
85
|
- 优化参数配置页面布局,将预设 App 参数归入运行配置区域。
|
|
90
86
|
|
|
91
87
|
## v0.1.15
|
|
92
|
-
|
|
93
88
|
- README 新增 Midscene 与 Appium 两种测试方式及环境要求说明。
|
|
94
89
|
- 明确 Midscene 需要配置模型,Appium 无需模型即可运行。
|
|
95
90
|
|
|
96
91
|
## v0.1.14
|
|
97
|
-
|
|
98
92
|
- 参数配置支持指定 Android SDK 和回放报告目录,并在回放前检查 Android SDK。
|
|
99
93
|
- 回放完成后生成包含节点配置、执行结果和日志的 Markdown 报告。
|
|
100
94
|
- 脚本列表支持 JSON 脚本导入和下载。
|
|
@@ -104,25 +98,20 @@
|
|
|
104
98
|
- 新增节点备注、未保存提醒和页面状态持久化。
|
|
105
99
|
|
|
106
100
|
## v0.1.13
|
|
107
|
-
|
|
108
101
|
- 修复历史脚本新增节点后无法覆盖保存的问题。
|
|
109
102
|
- 修复连接脚本未按顺序继续执行的问题。
|
|
110
103
|
|
|
111
104
|
## v0.1.12
|
|
112
|
-
|
|
113
105
|
- “启动 APP”统一为流程开始节点操作,每个脚本最多添加一次。
|
|
114
106
|
- 连接脚本回放时自动跳过子脚本的重复启动操作。
|
|
115
107
|
|
|
116
108
|
## v0.1.11
|
|
117
|
-
|
|
118
109
|
- 修复 npm 页面中的更新记录访问链接。
|
|
119
110
|
|
|
120
111
|
## v0.1.10
|
|
121
|
-
|
|
122
112
|
- npm 包新增项目更新记录,并清理未随包发布的内部文档链接。
|
|
123
113
|
|
|
124
114
|
## v0.1.9
|
|
125
|
-
|
|
126
115
|
- 录制步骤升级为可拖动、缩放和编辑的流程图,支持“是 / 否”判断分支。
|
|
127
116
|
- 支持在流程任意位置插入操作,并编辑节点、定位器、超时和输入内容。
|
|
128
117
|
- 支持 Activity 跳转检测和单 Activity 脚本录制保护。
|
|
@@ -130,23 +119,19 @@
|
|
|
130
119
|
- 新增脚本管理列表,可加载、删除和回放已录制脚本。
|
|
131
120
|
|
|
132
121
|
## v0.1.8
|
|
133
|
-
|
|
134
122
|
- 新增 selector 唯一性检测和父级上下文定位,解决多个组件共用同一 ID 的问题。
|
|
135
123
|
- 回放支持多级定位和坐标兜底,并记录页面检查点与完整失败诊断信息。
|
|
136
124
|
|
|
137
125
|
## v0.1.7
|
|
138
|
-
|
|
139
126
|
- 新增存在则点击、输入、清空、返回等可选操作。
|
|
140
127
|
- 新增等待出现、判断存在和等待消失等等待与判断能力。
|
|
141
128
|
|
|
142
129
|
## v0.1.6
|
|
143
|
-
|
|
144
130
|
- 新增可复用设备预览,支持点击画面选择组件树节点和刷新实时画面。
|
|
145
131
|
- 组件树支持滚动浏览并展示当前 Activity。
|
|
146
132
|
- 录制操作统一通过“添加操作”菜单选择。
|
|
147
133
|
|
|
148
134
|
## v0.1.5
|
|
149
|
-
|
|
150
135
|
- 首次提供 Appium 无模型录制与回放能力。
|
|
151
136
|
- 支持从组件树录制点击、输入、断言和延时,并保存为可回放脚本。
|
|
152
137
|
- App 包名与参数配置中的预设 App 统一管理。
|
package/README.md
CHANGED
|
@@ -1229,8 +1229,17 @@ Linux: ~/.local/share/android-midscene-automation
|
|
|
1229
1229
|
|
|
1230
1230
|
# 项目更新记录
|
|
1231
1231
|
|
|
1232
|
-
## v0.1.
|
|
1232
|
+
## v0.1.31
|
|
1233
|
+
- 组件详情展示 Resource ID、Accessibility ID、XPath 和 UiAutomator 四种定位内容,支持分别复制,方便配置循环退出条件;缺少对应属性时显示未提供。
|
|
1234
|
+
- “流程控制”新增有界循环及退出循环:支持固定次数、元素出现或消失时退出,必须设置最大执行次数;循环体支持嵌套判断、合流和连接脚本。退出节点可选择当前或外层循环,退出外层时同时结束内部循环并直接执行目标循环结束路径,日志及 HTML 报告标注轮次和退出目标。
|
|
1235
|
+
- 新增流程节点搜索,支持按名称、操作类型、定位内容和日志关键字筛选,展示所属分支并在多个结果间切换;普通画布与流程总览支持定位高亮,不改变节点布局及批量选中状态。
|
|
1236
|
+
- Midscene 相关模块统一从 1.12.0 升级至 1.12.2,包含 Android、Android Playground、Core、Playground、Shared 和 Web。
|
|
1237
|
+
- 录制工作区支持拖拽调整设备预览、组件树栏宽,自动保存布局并支持恢复默认。
|
|
1238
|
+
|
|
1239
|
+
## v0.1.30
|
|
1240
|
+
- 新增节点批量删除功能,支持多选节点,选中节点及所属子节点以柔和红色背景和浅色文字标记,确认后统一删除;支持普通与放大视图同步操作。
|
|
1233
1241
|
|
|
1242
|
+
## v0.1.29
|
|
1234
1243
|
- 判断节点新增“合并分支”,支持选择一侧公共流程起点,将两侧后续汇入同一段居中流程;另一侧可保留独有操作或确认删除重复后缀,支持嵌套判断及各类分支,公共节点只执行一次且不随单侧节点删除。
|
|
1235
1244
|
- 将 Checkbox、RadioButton 判断入口合并为“判断勾选”,新增 Switch 支持;添加和回放时自动校验组件类型及可勾选属性,读取实时 checked 状态进入 true / false 分支,不改变控件状态,兼容已有独立判断节点。
|
|
1236
1245
|
- “流程控制”新增终止流程节点,执行到该节点时主动结束本次回放,不再执行后续节点或连接脚本;子脚本触发时也结束整个流程,保留执行日志和报告,不作为执行失败。
|
|
@@ -1238,7 +1247,6 @@ Linux: ~/.local/share/android-midscene-automation
|
|
|
1238
1247
|
- Appium 配置新增流程背景色设置,默认沿用浅绿色,支持预选颜色、颜色选择器、#RGB / #RRGGBB 输入校验和恢复默认;保存后持久化,普通画布、放大视图和连接脚本预览统一应用。
|
|
1239
1248
|
|
|
1240
1249
|
## v0.1.28
|
|
1241
|
-
|
|
1242
1250
|
- Appium 配置中的流程背景色独立成区,与节点模型配置分开展示,保留预选颜色、自定义颜色及保存入口。
|
|
1243
1251
|
- “组件操作”新增文字点击分支节点,无需预选元素,支持精准匹配和模糊匹配原生组件文字;匹配并点击后进入“匹配到文字”,等待超时未找到则进入“未匹配到文字”。支持编辑文字和超时,多元素匹配及点击异常明确报错,日志和报告保留分支结果。
|
|
1244
1252
|
- “流程控制”新增输出日志节点,支持设置日志内容和自定义关键字,默认按 `stageLog:内容` 输出;支持添加后编辑,回放实时输出、日志文件及报告均保留日志内容,多行内容逐行添加关键字。
|
|
@@ -1247,7 +1255,6 @@ Linux: ~/.local/share/android-midscene-automation
|
|
|
1247
1255
|
- 新增原生 Checkbox 和 RadioButton 状态判断节点,添加时校验选中元素类型,回放时读取 `checked` 属性,按 `true / false` 进入对应分支,不改变勾选状态;组件详情显示原生勾选属性,回放日志和报告记录判断结果。
|
|
1248
1256
|
|
|
1249
1257
|
## v0.1.27
|
|
1250
|
-
|
|
1251
1258
|
- npx android-midscene-automation 启动时默认使用固定的系统用户数据目录保存脚本、配置、报告和运行态文件,并在启动时尝试迁移旧版启动目录下的数据,降低升级后脚本或配置“不见了”的风险。
|
|
1252
1259
|
- 新增脚本列表复制和重命名能力,可直接生成“原文件名 Copy”副本并修改脚本名称,便于复用已有录制流程。
|
|
1253
1260
|
- 新增检测画面变化能力,支持框选设备预览区域或使用当前选中元素作为检测区域,通过“检测画面变化N-开始节点 / 结束节点”对同一分支内的前后截图进行差异对比。
|
|
@@ -1257,34 +1264,28 @@ Linux: ~/.local/share/android-midscene-automation
|
|
|
1257
1264
|
- 回放报告截图和截图节点改为使用 ADB 截图,避免 Appium /screenshot 超时后堵塞 UiAutomator2 命令队列;Appium 请求超时时也会显示更准确的超时提示。
|
|
1258
1265
|
|
|
1259
1266
|
## v0.1.26
|
|
1260
|
-
|
|
1261
1267
|
- 修复流程节点复制与粘贴问题,避免 `structuredClone` 失败、粘贴后原节点消失、分支插入位置错误和复制后相对位置偏移。
|
|
1262
1268
|
- 修复判断分支已有节点时入口添加按钮缺失的问题,分支入口现在会保留可插入节点,并正确连接到首个分支节点。
|
|
1263
1269
|
- 滑动操作支持在添加弹窗和节点配置中修改起点、终点坐标及滑动时长。
|
|
1264
1270
|
- 长按操作支持设置长按时间,并在节点卡片中显示长按坐标和时长。
|
|
1265
1271
|
|
|
1266
1272
|
## v0.1.25
|
|
1267
|
-
|
|
1268
1273
|
- Android 设备预览恢复使用 scrcpy 实时流,优化实时流参数,减少 H.264 丢帧导致的花屏;刷新画面时会重启底层 Playground 预览流并等待会话就绪。
|
|
1269
1274
|
- 测试脚本生成新增提示词模板,可在通用提示词、广告测试、表单填写、聊天对话、播放器测试、搜索列表、订单支付、权限弹窗、个人设置和 H5/WebView 等场景间切换,并支持编辑或上传当前原始 Prompt。
|
|
1270
1275
|
- 优化 AI 生成流程,支持后台处理和终止生成;生成中锁定原始 Prompt,再次点击“AI 生成”可恢复进度弹窗,终止后不再弹出额外提示。
|
|
1271
1276
|
- 优化脚本生成提示词策略,区分广告、播放器、弹窗等被测目标与无关遮挡内容,登录流程增加登录后遮挡弹窗处理和无遮挡首页成功判定。
|
|
1272
1277
|
|
|
1273
1278
|
## v0.1.24
|
|
1274
|
-
|
|
1275
1279
|
- 参数配置的运行配置新增保存校验,Android SDK 路径必须包含 `platform-tools/adb`,回放报告目录必须填写已存在且可写入的绝对路径。
|
|
1276
1280
|
- Midscene 自定义提供方的 `Model Name` 和 `Model Family` 改为输入框,`Base URL` 匹配已知提供方时自动填充模型名称和模型系列,并在字段旁新增官方配置说明入口。
|
|
1277
1281
|
|
|
1278
1282
|
## v0.1.23
|
|
1279
|
-
|
|
1280
1283
|
- 常见问题拆分为独立 `FAQ.md` 文档,并在 README 的“功能文档”中增加跳转入口。
|
|
1281
1284
|
|
|
1282
1285
|
## v0.1.22
|
|
1283
|
-
|
|
1284
1286
|
- 升级 Midscene 相关依赖到 `v1.12.0`
|
|
1285
1287
|
|
|
1286
1288
|
## v0.1.21
|
|
1287
|
-
|
|
1288
1289
|
- 录制步骤流程图升级为独立画布与节点卡片视图,优化节点内容排版、操作图标、选中态、批量复制和还原位置体验。
|
|
1289
1290
|
- 统一普通流程线、操作按钮连线和判断分支线条样式,按节点实际高度计算连接位置,修复线条颜色不一致、断裂、多余线条、节点重叠和分支错位问题。
|
|
1290
1291
|
- 优化判断分支布局,统一“是 / 否”分支的 T 形连接样式,避免分支节点堆叠、左右分支连接错乱及多出第三分支。
|
|
@@ -1294,36 +1295,29 @@ Linux: ~/.local/share/android-midscene-automation
|
|
|
1294
1295
|
- 修复放大面板后点击操作按钮或节点配置会重置缩放的问题,保持当前画布大小和位置。
|
|
1295
1296
|
|
|
1296
1297
|
## v0.1.20
|
|
1297
|
-
|
|
1298
1298
|
- Appium 流程支持单节点、连续节点及完整判断子流程的复制与粘贴。
|
|
1299
1299
|
- 优化判断分支布局和流程总览,支持在总览中编辑完整流程。
|
|
1300
1300
|
- Appium session 与“启动 APP”节点解耦,并避免将桌面、设置等系统 Activity 误识别为脚本入口。
|
|
1301
1301
|
- 修复复杂判断脚本加载失败及加载后无法进入“当前录制”的问题。
|
|
1302
1302
|
|
|
1303
1303
|
## v0.1.19
|
|
1304
|
-
|
|
1305
1304
|
- 回放输出支持保留完整 Appium 服务日志,并可在执行过程中手动终止。
|
|
1306
1305
|
- 新增单文件 HTML 截图回放报告,提供时间轴、步骤导航和连续播放效果。
|
|
1307
1306
|
|
|
1308
1307
|
## v0.1.18
|
|
1309
|
-
|
|
1310
1308
|
- 使用说明和更新记录自动嵌入 README,可直接在 npm 页面渲染查看。
|
|
1311
1309
|
|
|
1312
1310
|
## v0.1.17
|
|
1313
|
-
|
|
1314
1311
|
- 新增在线文档查看入口,支持渲染使用说明和更新记录。
|
|
1315
1312
|
|
|
1316
1313
|
## v0.1.16
|
|
1317
|
-
|
|
1318
1314
|
- 优化参数配置页面布局,将预设 App 参数归入运行配置区域。
|
|
1319
1315
|
|
|
1320
1316
|
## v0.1.15
|
|
1321
|
-
|
|
1322
1317
|
- README 新增 Midscene 与 Appium 两种测试方式及环境要求说明。
|
|
1323
1318
|
- 明确 Midscene 需要配置模型,Appium 无需模型即可运行。
|
|
1324
1319
|
|
|
1325
1320
|
## v0.1.14
|
|
1326
|
-
|
|
1327
1321
|
- 参数配置支持指定 Android SDK 和回放报告目录,并在回放前检查 Android SDK。
|
|
1328
1322
|
- 回放完成后生成包含节点配置、执行结果和日志的 Markdown 报告。
|
|
1329
1323
|
- 脚本列表支持 JSON 脚本导入和下载。
|
|
@@ -1333,25 +1327,20 @@ Linux: ~/.local/share/android-midscene-automation
|
|
|
1333
1327
|
- 新增节点备注、未保存提醒和页面状态持久化。
|
|
1334
1328
|
|
|
1335
1329
|
## v0.1.13
|
|
1336
|
-
|
|
1337
1330
|
- 修复历史脚本新增节点后无法覆盖保存的问题。
|
|
1338
1331
|
- 修复连接脚本未按顺序继续执行的问题。
|
|
1339
1332
|
|
|
1340
1333
|
## v0.1.12
|
|
1341
|
-
|
|
1342
1334
|
- “启动 APP”统一为流程开始节点操作,每个脚本最多添加一次。
|
|
1343
1335
|
- 连接脚本回放时自动跳过子脚本的重复启动操作。
|
|
1344
1336
|
|
|
1345
1337
|
## v0.1.11
|
|
1346
|
-
|
|
1347
1338
|
- 修复 npm 页面中的更新记录访问链接。
|
|
1348
1339
|
|
|
1349
1340
|
## v0.1.10
|
|
1350
|
-
|
|
1351
1341
|
- npm 包新增项目更新记录,并清理未随包发布的内部文档链接。
|
|
1352
1342
|
|
|
1353
1343
|
## v0.1.9
|
|
1354
|
-
|
|
1355
1344
|
- 录制步骤升级为可拖动、缩放和编辑的流程图,支持“是 / 否”判断分支。
|
|
1356
1345
|
- 支持在流程任意位置插入操作,并编辑节点、定位器、超时和输入内容。
|
|
1357
1346
|
- 支持 Activity 跳转检测和单 Activity 脚本录制保护。
|
|
@@ -1359,23 +1348,19 @@ Linux: ~/.local/share/android-midscene-automation
|
|
|
1359
1348
|
- 新增脚本管理列表,可加载、删除和回放已录制脚本。
|
|
1360
1349
|
|
|
1361
1350
|
## v0.1.8
|
|
1362
|
-
|
|
1363
1351
|
- 新增 selector 唯一性检测和父级上下文定位,解决多个组件共用同一 ID 的问题。
|
|
1364
1352
|
- 回放支持多级定位和坐标兜底,并记录页面检查点与完整失败诊断信息。
|
|
1365
1353
|
|
|
1366
1354
|
## v0.1.7
|
|
1367
|
-
|
|
1368
1355
|
- 新增存在则点击、输入、清空、返回等可选操作。
|
|
1369
1356
|
- 新增等待出现、判断存在和等待消失等等待与判断能力。
|
|
1370
1357
|
|
|
1371
1358
|
## v0.1.6
|
|
1372
|
-
|
|
1373
1359
|
- 新增可复用设备预览,支持点击画面选择组件树节点和刷新实时画面。
|
|
1374
1360
|
- 组件树支持滚动浏览并展示当前 Activity。
|
|
1375
1361
|
- 录制操作统一通过“添加操作”菜单选择。
|
|
1376
1362
|
|
|
1377
1363
|
## v0.1.5
|
|
1378
|
-
|
|
1379
1364
|
- 首次提供 Appium 无模型录制与回放能力。
|
|
1380
1365
|
- 支持从组件树录制点击、输入、断言和延时,并保存为可回放脚本。
|
|
1381
1366
|
- App 包名与参数配置中的预设 App 统一管理。
|
package/index.html
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-midscene-automation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/oooooooko/android-midscene-automation.git"
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@dagrejs/dagre": "1.1.5",
|
|
40
40
|
"@element-plus/icons-vue": "2.3.2",
|
|
41
|
-
"@midscene/android": "https://registry.npmjs.org/@midscene/android/-/android-1.12.
|
|
42
|
-
"@midscene/android-playground": "1.12.
|
|
43
|
-
"@midscene/core": "https://registry.npmjs.org/@midscene/core/-/core-1.12.
|
|
44
|
-
"@midscene/playground": "https://registry.npmjs.org/@midscene/playground/-/playground-1.12.
|
|
45
|
-
"@midscene/shared": "https://registry.npmjs.org/@midscene/shared/-/shared-1.12.
|
|
46
|
-
"@midscene/web": "1.12.
|
|
41
|
+
"@midscene/android": "https://registry.npmjs.org/@midscene/android/-/android-1.12.2.tgz",
|
|
42
|
+
"@midscene/android-playground": "https://registry.npmjs.org/@midscene/android-playground/-/android-playground-1.12.2.tgz",
|
|
43
|
+
"@midscene/core": "https://registry.npmjs.org/@midscene/core/-/core-1.12.2.tgz",
|
|
44
|
+
"@midscene/playground": "https://registry.npmjs.org/@midscene/playground/-/playground-1.12.2.tgz",
|
|
45
|
+
"@midscene/shared": "https://registry.npmjs.org/@midscene/shared/-/shared-1.12.2.tgz",
|
|
46
|
+
"@midscene/web": "https://registry.npmjs.org/@midscene/web/-/web-1.12.2.tgz",
|
|
47
47
|
"@vitejs/plugin-vue": "6.0.7",
|
|
48
48
|
"@vue-flow/core": "1.48.2",
|
|
49
49
|
"archiver": "7.0.1",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
import { BoundedLoopTraversal } from './bounded-loop';
|
|
3
|
+
import { validateLoopSteps } from '../../src/appium-recorder/bounded-loop';
|
|
2
4
|
import { adbScreenshotBase64 } from './screenshot';
|
|
3
5
|
import { recognizeDeviceScreen } from './ai-recognition';
|
|
4
6
|
import { formatStageLog } from '../../src/appium-recorder/stage-log';
|
|
@@ -916,6 +918,15 @@ async function readNativeControlState(sessionId: string, step: AppiumRecordedSte
|
|
|
916
918
|
}
|
|
917
919
|
|
|
918
920
|
async function evaluateCondition(sessionId: string, deviceId: string, step: AppiumRecordedStepRecord) {
|
|
921
|
+
if (step.type === 'loop') {
|
|
922
|
+
if (step.loop?.exitWhen === 'never') return false;
|
|
923
|
+
const selector = toAppiumUsing(step.selector!);
|
|
924
|
+
const payload = await appiumRequest<AppiumValueResponse<Record<string, string>[]>>(`/session/${sessionId}/elements`, {
|
|
925
|
+
method: 'POST', body: JSON.stringify(selector),
|
|
926
|
+
});
|
|
927
|
+
if (!Array.isArray(payload.value)) throw new Error('循环退出条件查询返回了无效结果');
|
|
928
|
+
return step.loop?.exitWhen === 'exists' ? payload.value.length > 0 : payload.value.length === 0;
|
|
929
|
+
}
|
|
919
930
|
if (step.type === 'textClick') {
|
|
920
931
|
const selector = textClickSelector(step);
|
|
921
932
|
const deadline = Date.now() + Math.max(0, step.timeoutMs ?? 10000);
|
|
@@ -1121,35 +1132,18 @@ async function replayFlowSteps(
|
|
|
1121
1132
|
options: ReplayStepOptions = {},
|
|
1122
1133
|
) {
|
|
1123
1134
|
const idToIndex = new Map(steps.map((step, index) => [step.id, index]));
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
if (conditionIndex === undefined) return undefined;
|
|
1127
|
-
const explicitTargetId = steps[conditionIndex]?.flow?.successTargetId;
|
|
1128
|
-
return explicitTargetId ? idToIndex.get(explicitTargetId) : undefined;
|
|
1129
|
-
};
|
|
1130
|
-
const nextIndexAfterStep = (currentIndex: number, step: AppiumRecordedStepRecord) => {
|
|
1131
|
-
const explicitTargetId = step.flow?.successTargetId;
|
|
1132
|
-
if (explicitTargetId) return idToIndex.get(explicitTargetId);
|
|
1133
|
-
if (!step.flow?.parentConditionId || !step.flow.parentBranch) return currentIndex + 1;
|
|
1134
|
-
const nextStep = steps[currentIndex + 1];
|
|
1135
|
-
const staysInBranch = nextStep?.flow?.parentConditionId === step.flow.parentConditionId
|
|
1136
|
-
&& nextStep.flow.parentBranch === step.flow.parentBranch;
|
|
1137
|
-
return staysInBranch
|
|
1138
|
-
? currentIndex + 1
|
|
1139
|
-
: conditionContinuationIndex(step.flow.parentConditionId);
|
|
1140
|
-
};
|
|
1135
|
+
const traversal = new BoundedLoopTraversal(steps);
|
|
1136
|
+
const nextIndexAfterStep = (index: number, step: AppiumRecordedStepRecord) => traversal.next(step, index);
|
|
1141
1137
|
const visitedPath: string[] = [];
|
|
1142
1138
|
let index: number | undefined = 0;
|
|
1143
|
-
let guard = 0;
|
|
1144
|
-
const maxVisits = Math.max(steps.length * 4, 20);
|
|
1145
1139
|
|
|
1146
1140
|
while (typeof index === 'number' && index >= 0 && index < steps.length) {
|
|
1147
1141
|
if (replayContext.getStore()?.flowEnded) return;
|
|
1148
1142
|
throwIfReplayStopped();
|
|
1149
|
-
guard += 1;
|
|
1150
|
-
if (guard > maxVisits) throw new Error('流程图可能存在循环,已终止回放');
|
|
1151
|
-
|
|
1152
1143
|
const step = steps[index];
|
|
1144
|
+
traversal.visit(step);
|
|
1145
|
+
const loopContext = traversal.context(step);
|
|
1146
|
+
const frameStep = loopContext ? { ...step, label: `${step.label}(${loopContext})` } : step;
|
|
1153
1147
|
if (shouldSkipLinkedScriptInitStep(options, step)) {
|
|
1154
1148
|
lines.push(`[节点 ${index + 1}] 跳过:${step.label}(${linkedScriptInitSkipReason(step)})`);
|
|
1155
1149
|
index = nextIndexAfterStep(index, step);
|
|
@@ -1157,11 +1151,28 @@ async function replayFlowSteps(
|
|
|
1157
1151
|
}
|
|
1158
1152
|
visitedPath.push(step.label);
|
|
1159
1153
|
const nodeKind = defaultFlowKind(step);
|
|
1160
|
-
lines.push(`[节点 ${index + 1}] 开始:${
|
|
1161
|
-
await captureReplayFrame(sessionId,
|
|
1154
|
+
lines.push(`[节点 ${index + 1}] 开始:${frameStep.label}`);
|
|
1155
|
+
await captureReplayFrame(sessionId, frameStep, index + 1, options.scriptName || '', 'before', '执行前');
|
|
1162
1156
|
|
|
1163
1157
|
if (nodeKind === 'condition') {
|
|
1164
1158
|
try {
|
|
1159
|
+
if (step.type === 'loop') {
|
|
1160
|
+
const iterations = traversal.iteration(step);
|
|
1161
|
+
const atLimit = iterations >= step.loop!.maxIterations;
|
|
1162
|
+
const exitMatched = !atLimit && await evaluateCondition(sessionId, deviceId, step);
|
|
1163
|
+
if (atLimit || exitMatched) {
|
|
1164
|
+
const reason = atLimit ? '达到最大次数' : '满足退出条件';
|
|
1165
|
+
lines.push(`[循环 ${step.label}] 结束:${reason},已执行 ${iterations} 轮`);
|
|
1166
|
+
await captureReplayFrame(sessionId, frameStep, index + 1, options.scriptName || '', 'after', `${reason},已执行 ${iterations} 轮`);
|
|
1167
|
+
index = traversal.exit(step);
|
|
1168
|
+
} else {
|
|
1169
|
+
const entered = traversal.enter(step);
|
|
1170
|
+
lines.push(`[循环 ${step.label}] 第 ${entered.iteration}/${step.loop!.maxIterations} 轮开始`);
|
|
1171
|
+
await captureReplayFrame(sessionId, frameStep, index + 1, options.scriptName || '', 'after', `第 ${entered.iteration}/${step.loop!.maxIterations} 轮`);
|
|
1172
|
+
index = entered.next;
|
|
1173
|
+
}
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1165
1176
|
// AI 节点使用本次执行前截图,并将同一张图片和识别依据写入结果帧。
|
|
1166
1177
|
const beforeFrame = replayContext.getStore()?.frames.at(-1);
|
|
1167
1178
|
const recognition = step.type === 'aiRecognition' ? await recognizeDeviceScreen({
|
|
@@ -1172,19 +1183,20 @@ async function replayFlowSteps(
|
|
|
1172
1183
|
const matched = recognition ? recognition.result : await evaluateCondition(sessionId, deviceId, step);
|
|
1173
1184
|
if (recognition) lines.push(`[节点 ${index + 1}] AI 识别:${recognition.result},耗时 ${recognition.durationMs}ms${recognition.reason ? `,依据:${recognition.reason}` : ''}`);
|
|
1174
1185
|
const branchTargetId = matched ? step.flow?.yesTargetId : step.flow?.noTargetId;
|
|
1175
|
-
const
|
|
1186
|
+
const next = traversal.branch(step, matched);
|
|
1187
|
+
const targetId = branchTargetId || (next === undefined ? '' : steps[next]?.id) || '';
|
|
1176
1188
|
const branchLabel = flowBranchLabel(step, matched ? 'yes' : 'no');
|
|
1177
1189
|
lines.push(`[节点 ${index + 1}] 判断:${branchLabel}${isNativeStateCondition(step) ? `(checked=${matched})` : ''}${targetId ? `,进入 ${targetId}` : ',流程结束'}`);
|
|
1178
1190
|
await captureReplayFrame(
|
|
1179
1191
|
sessionId,
|
|
1180
|
-
|
|
1192
|
+
frameStep,
|
|
1181
1193
|
index + 1,
|
|
1182
1194
|
options.scriptName || '',
|
|
1183
1195
|
'after',
|
|
1184
1196
|
`判断:${branchLabel}${recognition?.reason ? `;${recognition.reason}` : ''}`,
|
|
1185
1197
|
recognition?.imageBase64,
|
|
1186
1198
|
);
|
|
1187
|
-
index =
|
|
1199
|
+
index = next;
|
|
1188
1200
|
} catch (error) {
|
|
1189
1201
|
if (isReplayStopped(error)) {
|
|
1190
1202
|
lines.push(`[节点 ${index + 1}] 已终止:${step.label}`);
|
|
@@ -1201,7 +1213,13 @@ async function replayFlowSteps(
|
|
|
1201
1213
|
|
|
1202
1214
|
try {
|
|
1203
1215
|
let softFailed = false;
|
|
1204
|
-
if (step.type === '
|
|
1216
|
+
if (step.type === 'breakLoop') {
|
|
1217
|
+
const exited = traversal.break(step);
|
|
1218
|
+
lines.push(`[循环 ${exited.loop.label}] 第 ${exited.iteration} 轮主动退出`);
|
|
1219
|
+
await captureReplayFrame(sessionId, frameStep, index + 1, options.scriptName || '', 'after', `已退出循环:${exited.loop.label}`);
|
|
1220
|
+
index = exited.next;
|
|
1221
|
+
continue;
|
|
1222
|
+
} else if (step.type === 'runScript') {
|
|
1205
1223
|
await replayLinkedScript(sessionId, deviceId, step, lines, stack);
|
|
1206
1224
|
} else {
|
|
1207
1225
|
const result = await runStep(sessionId, deviceId, step, {
|
|
@@ -1211,7 +1229,7 @@ async function replayFlowSteps(
|
|
|
1211
1229
|
softFailed = appendRunStepResult(lines, index + 1, result);
|
|
1212
1230
|
}
|
|
1213
1231
|
lines.push(`[节点 ${index + 1}] 完成:${step.label}${softFailed ? '(存在软失败,已继续)' : ''}`);
|
|
1214
|
-
await captureReplayFrame(sessionId,
|
|
1232
|
+
await captureReplayFrame(sessionId, frameStep, index + 1, options.scriptName || '', 'after', completedStepStatus(step, softFailed));
|
|
1215
1233
|
index = nextIndexAfterStep(index, step);
|
|
1216
1234
|
} catch (error) {
|
|
1217
1235
|
if (isReplayStopped(error)) {
|
|
@@ -1259,6 +1277,7 @@ async function replayScriptSteps(
|
|
|
1259
1277
|
stack: string[],
|
|
1260
1278
|
options: ReplayStepOptions = {},
|
|
1261
1279
|
) {
|
|
1280
|
+
validateLoopSteps(steps);
|
|
1262
1281
|
let trailingLinkIndex = steps.length;
|
|
1263
1282
|
while (
|
|
1264
1283
|
trailingLinkIndex > 0
|
|
@@ -1334,6 +1353,7 @@ export async function replayAppiumScript(
|
|
|
1334
1353
|
let stopped = false;
|
|
1335
1354
|
let managedAppium: Awaited<ReturnType<typeof startManagedAppiumServer>> | null = null;
|
|
1336
1355
|
try {
|
|
1356
|
+
validateLoopSteps(script.steps);
|
|
1337
1357
|
lines.push('正在检测 Android SDK...');
|
|
1338
1358
|
const androidSdk = ensureAndroidSdkAvailable();
|
|
1339
1359
|
lines.push(`Android SDK 已就绪:${androidSdk.root}`);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { AppiumRecordedStep } from '../../src/appium-recorder/types';
|
|
2
|
+
import { enclosingLoop, enclosingLoops, breakLoopTarget, validateLoopSteps } from '../../src/appium-recorder/bounded-loop';
|
|
3
|
+
import { defaultFlowKind } from '../../src/appium-recorder/flow-labels';
|
|
4
|
+
|
|
5
|
+
/** Virtual returns keep the saved flow graph acyclic and preserve branch layout. */
|
|
6
|
+
export class BoundedLoopTraversal {
|
|
7
|
+
private readonly byId: Map<string, number>;
|
|
8
|
+
private readonly active = new Map<string, { iteration: number; visited: Set<string> }>();
|
|
9
|
+
private readonly rootVisited = new Set<string>();
|
|
10
|
+
private readonly owners = new Map<string, AppiumRecordedStep | undefined>();
|
|
11
|
+
|
|
12
|
+
constructor(private readonly steps: AppiumRecordedStep[]) {
|
|
13
|
+
validateLoopSteps(steps);
|
|
14
|
+
this.byId = new Map(steps.map((step, index) => [step.id, index]));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private target(id?: string) { return id ? this.byId.get(id) : undefined; }
|
|
18
|
+
private owner(step: AppiumRecordedStep) {
|
|
19
|
+
if (!this.owners.has(step.id)) this.owners.set(step.id, enclosingLoop(this.steps, step));
|
|
20
|
+
return this.owners.get(step.id);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
visit(step: AppiumRecordedStep) {
|
|
24
|
+
if (step.type === 'loop' && this.active.has(step.id)) return;
|
|
25
|
+
const owner = this.owner(step);
|
|
26
|
+
const frame = owner ? this.active.get(owner.id) : undefined;
|
|
27
|
+
if (owner && !frame) throw new Error('不能从循环外直接进入循环体');
|
|
28
|
+
const visited = frame?.visited || this.rootVisited;
|
|
29
|
+
if (visited.has(step.id)) throw new Error('流程存在非有界循环回边,已终止回放');
|
|
30
|
+
visited.add(step.id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private continuation(step: AppiumRecordedStep, seen = new Set<string>()): number | undefined {
|
|
34
|
+
if (seen.has(step.id)) throw new Error('流程分支归属存在循环');
|
|
35
|
+
seen.add(step.id);
|
|
36
|
+
if (step.flow?.successTargetId) return this.target(step.flow.successTargetId);
|
|
37
|
+
if (defaultFlowKind(step) === 'condition') {
|
|
38
|
+
const start = this.byId.get(step.id)!;
|
|
39
|
+
const sibling = this.steps.findIndex((item, index) => index > start && item.flow?.parentConditionId === step.flow?.parentConditionId && item.flow?.parentBranch === step.flow?.parentBranch);
|
|
40
|
+
if (sibling >= 0) return sibling;
|
|
41
|
+
}
|
|
42
|
+
const parentIndex = this.target(step.flow?.parentConditionId);
|
|
43
|
+
if (parentIndex === undefined) return;
|
|
44
|
+
const parent = this.steps[parentIndex]!;
|
|
45
|
+
if (parent.type === 'loop' && step.flow?.parentBranch === 'yes') return parentIndex;
|
|
46
|
+
return this.continuation(parent, seen);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private returnAtBoundary(step: AppiumRecordedStep, next: number | undefined) {
|
|
50
|
+
const owner = this.owner(step);
|
|
51
|
+
if (!owner || !this.active.has(owner.id)) return next;
|
|
52
|
+
// The end of any nested branch returns to the loop, not to the other branch.
|
|
53
|
+
if (next === undefined || (this.steps[next]?.id !== owner.id && !this.isInBody(this.steps[next]!, owner.id))) {
|
|
54
|
+
return this.target(owner.id);
|
|
55
|
+
}
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private isInBody(step: AppiumRecordedStep, loopId: string) {
|
|
60
|
+
let owner = this.owner(step);
|
|
61
|
+
const seen = new Set<string>();
|
|
62
|
+
while (owner && !seen.has(owner.id)) {
|
|
63
|
+
if (owner.id === loopId) return true;
|
|
64
|
+
seen.add(owner.id); owner = this.owner(owner);
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
next(step: AppiumRecordedStep, index: number) {
|
|
70
|
+
let next = this.target(step.flow?.successTargetId);
|
|
71
|
+
if (!step.flow?.successTargetId) {
|
|
72
|
+
const sibling = this.steps[index + 1];
|
|
73
|
+
next = sibling && sibling.flow?.parentConditionId === step.flow?.parentConditionId && sibling.flow?.parentBranch === step.flow?.parentBranch
|
|
74
|
+
? index + 1 : this.continuation(step);
|
|
75
|
+
}
|
|
76
|
+
return this.returnAtBoundary(step, next);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
branch(step: AppiumRecordedStep, matched: boolean) {
|
|
80
|
+
const id = matched ? step.flow?.yesTargetId : step.flow?.noTargetId;
|
|
81
|
+
return this.returnAtBoundary(step, id ? this.target(id) : this.continuation(step));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
iteration(step: AppiumRecordedStep) { return this.active.get(step.id)?.iteration || 0; }
|
|
85
|
+
|
|
86
|
+
enter(step: AppiumRecordedStep) {
|
|
87
|
+
const iteration = this.iteration(step) + 1;
|
|
88
|
+
if (iteration > step.loop!.maxIterations) throw new Error('循环次数超过上限');
|
|
89
|
+
this.active.set(step.id, { iteration, visited: new Set() });
|
|
90
|
+
return { iteration, next: this.target(step.flow?.yesTargetId) ?? this.target(step.id) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
exit(step: AppiumRecordedStep) {
|
|
94
|
+
this.active.delete(step.id);
|
|
95
|
+
return this.returnAtBoundary(step, this.target(step.flow?.noTargetId) ?? this.continuation(step));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
break(step: AppiumRecordedStep) {
|
|
99
|
+
const owner = breakLoopTarget(this.steps, step);
|
|
100
|
+
if (!this.active.has(owner.id)) throw new Error('目标循环当前未执行');
|
|
101
|
+
// Unwind inner frames without running their completion branches.
|
|
102
|
+
for (const inner of enclosingLoops(this.steps, step)) {
|
|
103
|
+
if (inner.id === owner.id) break;
|
|
104
|
+
this.active.delete(inner.id);
|
|
105
|
+
}
|
|
106
|
+
return { loop: owner, iteration: this.iteration(owner), next: this.exit(owner) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
context(step: AppiumRecordedStep) {
|
|
110
|
+
const parts: string[] = [];
|
|
111
|
+
let owner = this.owner(step);
|
|
112
|
+
while (owner) {
|
|
113
|
+
parts.unshift(`${owner.label} 第 ${this.iteration(owner)}/${owner.loop!.maxIterations} 轮`);
|
|
114
|
+
owner = this.owner(owner);
|
|
115
|
+
}
|
|
116
|
+
return parts.join(' / ');
|
|
117
|
+
}
|
|
118
|
+
}
|