android-midscene-automation 0.2.3 → 0.2.4

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 CHANGED
@@ -1,4 +1,8 @@
1
1
  # 更新记录
2
+ ## v0.2.4
3
+ - Midscene Android、Android Playground、Core、Playground、Shared 和 Web 模块统一升级至 1.14.0。
4
+ - 修复 Midscene 测试脚本生成过程中切换“AI 生成 / 自定义步骤编排”会清空生成状态和结果的问题;生成任务继续在后台执行,模式切换后保留已生成步骤与代码。
5
+
2
6
  ## v0.2.3
3
7
  - 修复长时间执行 ADB 操作、延时或 AI 识别后,Appium 因默认 60 秒命令空闲超时销毁会话,导致后续 Home 键、点击等节点报“invalid session id”的问题;托管服务关闭会话空闲计时,外部服务通过心跳保活并保留 60 秒空闲回收,避免创建请求取消或清理失败后遗留永久会话。
4
8
  - 为启动 App、清理数据、坐标点击与滑动补齐 ADB 超时和取消控制;终止前台检查后不再继续启动 App,异常结束仍清理会话。
@@ -6,7 +10,6 @@
6
10
  - 修复远程设备无法手动终止回放的问题,通过独立心跳传递取消信号,兼容任务派发前取消,并阻止同一设备重复提交回放。
7
11
 
8
12
  ## v0.2.2
9
- - 更新浅色后台界面:蓝色主色、白色卡片、浅灰背景、可折叠侧边栏与顶部页签。
10
13
  - 新增“概览 → 分析页”,根据已保存脚本和真实执行记录分别汇总 Midscene、Appium 测试数据。
11
14
  - 修复配置加载失败后出现空白可保存表单的问题,增加独立错误提示和重试;后端断线后自动尝试重连并支持手动重连,保留页面编辑内容。
12
15
  - Appium 配置自动保存失败后保留最新待保存内容并提供“重试保存”;修复切换或关闭 Appium 页签后,离开页面时未保存草稿保护失效的问题。
package/README.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  一个面向 Android App 的自动化测试工具,同时支持 Midscene AI 测试和 Appium 组件树录制回放。
4
4
 
5
+ ## 界面预览
6
+
7
+ ![](./screen/c1.jpg)
8
+
9
+ ### Midscene
10
+
11
+ ![](./screen/m1.jpg)
12
+
13
+ ![](./screen/m2.jpg)
14
+
15
+ ![](./screen/m3.jpg)
16
+
17
+ ### Appium
18
+
19
+ ![](./screen/a1.jpg)
20
+
21
+ ![](./screen/a2.jpg)
22
+
23
+ ![](./screen/a3.jpg)
24
+
25
+ ![](./screen/a4.jpg)
26
+
27
+ ![](./screen/a5.jpg)
28
+
5
29
  ## 支持的测试方式
6
30
 
7
31
  | 测试方式 | 页面入口 | 工作方式 | 模型要求 | 适用场景 |
@@ -1358,6 +1382,10 @@ Linux: ~/.local/share/android-midscene-automation
1358
1382
 
1359
1383
  # 项目更新记录
1360
1384
 
1385
+ ## v0.2.4
1386
+ - Midscene Android、Android Playground、Core、Playground、Shared 和 Web 模块统一升级至 1.14.0。
1387
+ - 修复 Midscene 测试脚本生成过程中切换“AI 生成 / 自定义步骤编排”会清空生成状态和结果的问题;生成任务继续在后台执行,模式切换后保留已生成步骤与代码。
1388
+
1361
1389
  ## v0.2.3
1362
1390
  - 修复长时间执行 ADB 操作、延时或 AI 识别后,Appium 因默认 60 秒命令空闲超时销毁会话,导致后续 Home 键、点击等节点报“invalid session id”的问题;托管服务关闭会话空闲计时,外部服务通过心跳保活并保留 60 秒空闲回收,避免创建请求取消或清理失败后遗留永久会话。
1363
1391
  - 为启动 App、清理数据、坐标点击与滑动补齐 ADB 超时和取消控制;终止前台检查后不再继续启动 App,异常结束仍清理会话。
@@ -1365,7 +1393,6 @@ Linux: ~/.local/share/android-midscene-automation
1365
1393
  - 修复远程设备无法手动终止回放的问题,通过独立心跳传递取消信号,兼容任务派发前取消,并阻止同一设备重复提交回放。
1366
1394
 
1367
1395
  ## v0.2.2
1368
- - 更新浅色后台界面:蓝色主色、白色卡片、浅灰背景、可折叠侧边栏与顶部页签。
1369
1396
  - 新增“概览 → 分析页”,根据已保存脚本和真实执行记录分别汇总 Midscene、Appium 测试数据。
1370
1397
  - 修复配置加载失败后出现空白可保存表单的问题,增加独立错误提示和重试;后端断线后自动尝试重连并支持手动重连,保留页面编辑内容。
1371
1398
  - Appium 配置自动保存失败后保留最新待保存内容并提供“重试保存”;修复切换或关闭 Appium 页签后,离开页面时未保存草稿保护失效的问题。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "android-midscene-automation",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oooooooko/android-midscene-automation.git"
@@ -45,12 +45,12 @@
45
45
  "mediabunny": "1.58.0",
46
46
  "@dagrejs/dagre": "1.1.5",
47
47
  "@element-plus/icons-vue": "2.3.2",
48
- "@midscene/android": "https://registry.npmjs.org/@midscene/android/-/android-1.12.7.tgz",
49
- "@midscene/android-playground": "https://registry.npmjs.org/@midscene/android-playground/-/android-playground-1.12.7.tgz",
50
- "@midscene/core": "https://registry.npmjs.org/@midscene/core/-/core-1.12.7.tgz",
51
- "@midscene/playground": "https://registry.npmjs.org/@midscene/playground/-/playground-1.12.7.tgz",
52
- "@midscene/shared": "https://registry.npmjs.org/@midscene/shared/-/shared-1.12.7.tgz",
53
- "@midscene/web": "https://registry.npmjs.org/@midscene/web/-/web-1.12.7.tgz",
48
+ "@midscene/android": "https://registry.npmjs.org/@midscene/android/-/android-1.14.0.tgz",
49
+ "@midscene/android-playground": "https://registry.npmjs.org/@midscene/android-playground/-/android-playground-1.14.0.tgz",
50
+ "@midscene/core": "https://registry.npmjs.org/@midscene/core/-/core-1.14.0.tgz",
51
+ "@midscene/playground": "https://registry.npmjs.org/@midscene/playground/-/playground-1.14.0.tgz",
52
+ "@midscene/shared": "https://registry.npmjs.org/@midscene/shared/-/shared-1.14.0.tgz",
53
+ "@midscene/web": "https://registry.npmjs.org/@midscene/web/-/web-1.14.0.tgz",
54
54
  "@techstark/opencv-js": "5.0.0-release.1",
55
55
  "@vitejs/plugin-vue": "6.0.7",
56
56
  "@vue-flow/core": "1.48.2",
@@ -79,7 +79,7 @@ function delay(ms: number) {
79
79
  });
80
80
  }
81
81
 
82
- // Work around @midscene/android-playground v1.12.x ESM __dirname usage on Node 24.
82
+ // Keep the Android Playground launcher compatible with Node 24 ESM execution.
83
83
  const START_ANDROID_PLAYGROUND_WITHOUT_BROWSER = [
84
84
  "import { createRequire } from 'node:module';",
85
85
  'const require = createRequire(import.meta.url);',
package/src/App.vue CHANGED
@@ -110,8 +110,9 @@ const { isRunningScript, isStoppingScript, executionLog, executionProcess, lastR
110
110
  const visitedMenus = reactive(new Set<MenuKey>([activeMenu.value]));
111
111
  const openMenus = ref<MenuKey[]>([...menuKeys]);
112
112
  const visibleMenuItems = computed(() => openMenus.value.map(key => menuItems.find(item => item.key === key)!));
113
- function closeMenu(key: MenuKey) {
113
+ async function closeMenu(key: MenuKey) {
114
114
  if (openMenus.value.length === 1) return;
115
+ if (key === 'generator' && !(await confirmUnsavedGeneratorNavigation())) return;
115
116
  const index = openMenus.value.indexOf(key);
116
117
  openMenus.value.splice(index, 1);
117
118
  if (activeMenu.value === key) activeMenu.value = openMenus.value[Math.max(0, index - 1)];
@@ -144,6 +145,23 @@ const appPromptPrefix = computed(() => {
144
145
  const effectiveSourcePrompt = computed(() => `${appPromptPrefix.value}${sourcePrompt.value}`.trim());
145
146
  const generatedCodeFromSteps = computed(() => buildScript(form, steps.value));
146
147
  const generatedCode = computed(() => generatedCodeOverride.value ?? generatedCodeFromSteps.value);
148
+ const generatorContentSnapshot = computed(() => JSON.stringify({
149
+ form: {
150
+ promptTitle: form.promptTitle,
151
+ testName: form.testName,
152
+ appPresetId: form.appPresetId,
153
+ },
154
+ sourcePrompt: sourcePrompt.value,
155
+ steps: steps.value,
156
+ generatedCodeOverride: generatedCodeOverride.value,
157
+ generatedCodeDraft: generatedCodeEditing.value ? generatedCodeDraft.value : '',
158
+ }));
159
+ const savedGeneratorSnapshot = shallowRef('');
160
+ const generatorHasUnsavedContent = computed(() => (
161
+ showGeneratedCode.value
162
+ && Boolean(generatedCode.value.trim())
163
+ && generatorContentSnapshot.value !== savedGeneratorSnapshot.value
164
+ ));
147
165
  const selectedScript = computed(() => savedScripts.value.find((item) => item.id === selectedScriptId.value));
148
166
  const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
149
167
  const deviceActions = [
@@ -155,8 +173,11 @@ const deviceActions = [
155
173
  { key: 'tasks', label: '任务', icon: tasksIcon, keyCode: 187 },
156
174
  ] as const;
157
175
 
158
- const selectMenu = (key: string) => {
159
- activeMenu.value = key as MenuKey;
176
+ const selectMenu = async (key: string) => {
177
+ const nextMenu = key as MenuKey;
178
+ if (nextMenu === activeMenu.value) return;
179
+ if (activeMenu.value === 'generator' && !(await confirmUnsavedGeneratorNavigation())) return;
180
+ activeMenu.value = nextMenu;
160
181
  };
161
182
 
162
183
  const resetGeneratedCodeEditor = () => {
@@ -166,11 +187,6 @@ const resetGeneratedCodeEditor = () => {
166
187
  generatedCodeEditSaving.value = false;
167
188
  };
168
189
 
169
- watch(activeGeneratorMode, () => {
170
- showGeneratedCode.value = false;
171
- resetGeneratedCodeEditor();
172
- });
173
-
174
190
  function formatScriptTime(value: string) {
175
191
  if (!value) return '';
176
192
  const date = new Date(value);
@@ -455,16 +471,16 @@ const buildSourcePromptForSave = () => {
455
471
 
456
472
  const saveCurrentScript = async () => {
457
473
  const validInput = validateCurrentGeneratorInputs();
458
- if (!validInput) return;
474
+ if (!validInput) return false;
459
475
 
460
476
  if (generatedCodeEditing.value) {
461
477
  ElMessage.error('请先保存或撤回代码修改');
462
- return;
478
+ return false;
463
479
  }
464
480
 
465
481
  if (!showGeneratedCode.value || !generatedCode.value.trim()) {
466
482
  ElMessage.error('请先点击 AI 生成,生成代码后再保存脚本');
467
- return;
483
+ return false;
468
484
  }
469
485
 
470
486
  const { scriptName, promptTitle } = validInput;
@@ -478,7 +494,7 @@ const saveCurrentScript = async () => {
478
494
  cancelButtonText: '取消',
479
495
  });
480
496
  } catch {
481
- return;
497
+ return false;
482
498
  }
483
499
  }
484
500
 
@@ -495,10 +511,34 @@ const saveCurrentScript = async () => {
495
511
  });
496
512
 
497
513
  await loadSavedScripts(payload.script?.id || '');
514
+ savedGeneratorSnapshot.value = generatorContentSnapshot.value;
498
515
  await sleep(Math.max(0, 1200 - (Date.now() - startedAt)));
499
516
  closeActionDialog();
517
+ return true;
500
518
  } catch (error) {
501
519
  failActionDialog(error instanceof Error ? error.message : '保存脚本失败');
520
+ return false;
521
+ }
522
+ };
523
+
524
+ const confirmUnsavedGeneratorNavigation = async () => {
525
+ if (!generatorHasUnsavedContent.value) return true;
526
+
527
+ try {
528
+ await ElMessageBox.confirm(
529
+ '当前生成内容尚未保存。可以先保存脚本,或暂不保存并继续切换页面。',
530
+ '未保存的测试脚本',
531
+ {
532
+ type: 'warning',
533
+ confirmButtonText: '保存脚本',
534
+ cancelButtonText: '暂不保存',
535
+ distinguishCancelAndClose: true,
536
+ closeOnClickModal: false,
537
+ },
538
+ );
539
+ return await saveCurrentScript();
540
+ } catch (action) {
541
+ return action === 'cancel';
502
542
  }
503
543
  };
504
544
 
@@ -554,7 +594,7 @@ const generateWithModel = async () => {
554
594
  errorMessage.value = '';
555
595
  const controller = new AbortController();
556
596
  aiGenerateAbortController = controller;
557
- openActionDialog('AI生成', {
597
+ openActionDialog('AI 生成', {
558
598
  canRunInBackground: true,
559
599
  canCancel: true,
560
600
  cancelText: '终止',
@@ -674,7 +714,7 @@ watch(activeMenu, menu => {
674
714
  if (!openMenus.value.includes(menu)) openMenus.value.push(menu);
675
715
  }, { flush: 'sync' });
676
716
  const warnBeforeUnload = (event: BeforeUnloadEvent) => {
677
- if (!appiumAutoSave.dirty.value) return;
717
+ if (!appiumAutoSave.dirty.value && !generatorHasUnsavedContent.value) return;
678
718
  event.preventDefault();
679
719
  event.returnValue = '';
680
720
  };
@@ -695,7 +735,9 @@ onUnmounted(() => {
695
735
  <el-dialog
696
736
  v-model="actionDialog.visible"
697
737
  :title="actionDialog.title"
698
- width="360px"
738
+ width="min(440px, calc(100vw - 32px))"
739
+ align-center
740
+ append-to-body
699
741
  :close-on-click-modal="false"
700
742
  :close-on-press-escape="!actionDialog.loading"
701
743
  :show-close="!actionDialog.loading"
@@ -712,7 +754,7 @@ onUnmounted(() => {
712
754
  <div v-if="actionDialog.loading" class="action-dialog__footer-actions">
713
755
  <el-button
714
756
  v-if="actionDialog.canCancel"
715
- class="action-dialog__footer-button action-dialog__footer-button--secondary"
757
+ class="action-dialog__footer-button"
716
758
  :icon="VideoPause"
717
759
  :loading="actionDialog.canceling"
718
760
  @click="stopGenerateWithModel"
@@ -721,7 +763,8 @@ onUnmounted(() => {
721
763
  </el-button>
722
764
  <el-button
723
765
  v-if="actionDialog.canRunInBackground"
724
- class="action-dialog__footer-button action-dialog__footer-button--primary"
766
+ type="primary"
767
+ class="action-dialog__footer-button"
725
768
  @click="sendActionDialogToBackground"
726
769
  >
727
770
  后台处理
@@ -729,7 +772,8 @@ onUnmounted(() => {
729
772
  </div>
730
773
  <el-button
731
774
  v-else
732
- class="action-dialog__footer-button action-dialog__footer-button--primary"
775
+ type="primary"
776
+ class="action-dialog__footer-button"
733
777
  @click="closeActionDialog"
734
778
  >
735
779
  关闭
package/src/style.css CHANGED
@@ -68,38 +68,33 @@ select {
68
68
  display: flex;
69
69
  align-items: center;
70
70
  justify-content: center;
71
- gap: 8px;
72
- min-height: 56px;
73
- padding: 4px 0;
74
- color: #303133;
71
+ gap: 10px;
72
+ min-height: 72px;
73
+ padding: 8px 0;
74
+ color: var(--ui-text);
75
75
  font-size: 14px;
76
76
  }
77
77
 
78
78
  .action-dialog__spinner {
79
- font-size: 16px;
79
+ color: var(--ui-primary);
80
+ font-size: 18px;
80
81
  animation: action-dialog-spin 1s linear infinite;
81
82
  }
82
83
 
83
84
  .action-dialog__error {
84
- min-height: 0;
85
- padding: 4px 0 2px;
86
- color: #f56c6c;
85
+ min-height: 72px;
86
+ padding: 14px 16px;
87
+ border: 1px solid #ffd6d6;
88
+ border-radius: var(--ui-radius-control);
89
+ background: #fff5f5;
90
+ color: var(--ui-danger);
87
91
  font-size: 13px;
88
92
  line-height: 1.6;
89
93
  white-space: pre-wrap;
90
94
  }
91
95
 
92
- .action-dialog .el-dialog__header {
93
- margin-right: 0;
94
- padding: 14px 16px 6px;
95
- }
96
-
97
96
  .action-dialog .el-dialog__body {
98
- padding: 8px 16px 6px;
99
- }
100
-
101
- .action-dialog .el-dialog__footer {
102
- padding: 6px 16px 14px;
97
+ padding-bottom: 14px;
103
98
  }
104
99
 
105
100
  .action-dialog__footer-actions {
@@ -110,44 +105,13 @@ select {
110
105
  }
111
106
 
112
107
  .action-dialog__footer-button.el-button {
113
- min-width: 88px;
114
- height: 34px;
115
- padding: 0 16px;
116
- border-radius: 7px;
117
- font-size: 14px;
118
- font-weight: 700;
108
+ min-width: 104px;
119
109
  }
120
110
 
121
111
  .action-dialog__footer-button.el-button + .action-dialog__footer-button.el-button {
122
112
  margin-left: 0;
123
113
  }
124
114
 
125
- .action-dialog__footer-button--secondary.el-button {
126
- border-color: #e5e7eb;
127
- background: #fff;
128
- color: #8b95a5;
129
- }
130
-
131
- .action-dialog__footer-button--secondary.el-button:hover,
132
- .action-dialog__footer-button--secondary.el-button:focus {
133
- border-color: #d7dce5;
134
- background: #f8fafc;
135
- color: #6b7280;
136
- }
137
-
138
- .action-dialog__footer-button--primary.el-button {
139
- border-color: #ffd436;
140
- background: #ffd436;
141
- color: #111827;
142
- }
143
-
144
- .action-dialog__footer-button--primary.el-button:hover,
145
- .action-dialog__footer-button--primary.el-button:focus {
146
- border-color: #ffca1f;
147
- background: #ffca1f;
148
- color: #111827;
149
- }
150
-
151
115
  @keyframes action-dialog-spin {
152
116
  from {
153
117
  transform: rotate(0deg);
@@ -1298,7 +1262,7 @@ select {
1298
1262
 
1299
1263
  .appium-replay-dialog.el-dialog {
1300
1264
  display: flex;
1301
- padding: 16px;
1265
+ padding: 0;
1302
1266
  height: min(620px, calc(100vh - 64px));
1303
1267
  min-width: 420px;
1304
1268
  min-height: 280px;
@@ -1318,16 +1282,16 @@ select {
1318
1282
  flex: 1 1 auto;
1319
1283
  min-height: 0;
1320
1284
  overflow: hidden;
1321
- padding: 8px 0 0;
1285
+ padding: 16px 20px 0;
1322
1286
  }
1323
1287
 
1324
1288
  .appium-replay-dialog > .el-dialog__header {
1325
- padding: 0 0 16px;
1326
- border-bottom: 0;
1289
+ padding: 18px 20px 14px;
1290
+ border-bottom: 1px solid var(--ui-border);
1327
1291
  }
1328
1292
 
1329
1293
  .appium-replay-dialog > .el-dialog__footer {
1330
- padding: 16px 0 0;
1294
+ padding: 12px 20px 18px;
1331
1295
  }
1332
1296
 
1333
1297
  .appium-replay-dialog .appium-replay-output {
@@ -3592,22 +3556,22 @@ body {
3592
3556
  flex-direction: column;
3593
3557
  height: 86dvh;
3594
3558
  max-height: calc(100dvh - 32px);
3595
- overflow: visible;
3596
- padding: 16px;
3597
- border: 0;
3598
- border-radius: 4px;
3599
- box-shadow: var(--el-box-shadow);
3559
+ overflow: hidden;
3560
+ padding: 0;
3561
+ border: 1px solid var(--ui-border);
3562
+ border-radius: var(--ui-radius);
3563
+ box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
3600
3564
  }
3601
3565
 
3602
3566
  .appium-flow-dialog .el-dialog__header {
3603
3567
  flex: none;
3604
- padding: 0 0 16px;
3605
- border-bottom: 0;
3568
+ padding: 18px 20px 14px;
3569
+ border-bottom: 1px solid var(--ui-border);
3606
3570
  }
3607
3571
 
3608
3572
  .appium-flow-dialog .el-dialog__title {
3609
- font-size: var(--el-dialog-title-font-size);
3610
- font-weight: 400;
3573
+ font-size: 16px;
3574
+ font-weight: 650;
3611
3575
  }
3612
3576
 
3613
3577
  .appium-flow-dialog .el-dialog__body {
@@ -3615,7 +3579,7 @@ body {
3615
3579
  flex-direction: column;
3616
3580
  flex: 1;
3617
3581
  min-height: 0;
3618
- padding: 8px 0 0;
3582
+ padding: 16px 20px 20px;
3619
3583
  }
3620
3584
 
3621
3585
  .appium-flow-dialog__toolbar { flex: none; }