android-midscene-automation 0.1.12 → 0.1.13

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,8 +1,16 @@
1
1
  # 更新记录
2
2
 
3
+ ## v0.1.13
4
+
5
+ - Appium 模块:修复加载历史脚本并新增节点后被误判为新脚本、无法覆盖保存的问题。
6
+ - Appium 模块:脚本末尾的连接节点改为当前脚本完成后的串联阶段,避免判断分支未命中时漏掉后一个脚本。
7
+
3
8
  ## v0.1.12
4
9
 
5
10
  - 修复部分浏览器打开更新记录时中文乱码的问题,将 README 链接切换到明确返回 UTF-8 编码的 jsDelivr。
11
+ - Appium 模块:删除结束节点、前置操作和后置操作;“启动 APP”改为从开始节点直接插入为流程第一步,并对齐开始节点与插入按钮。
12
+ - Appium 模块:每个录制脚本只允许一个“启动 APP”节点;添加后操作菜单自动禁用该选项,删除节点后恢复。
13
+ - Appium 模块:连接脚本回放时自动跳过子脚本的“启动 APP”节点,子脚本独立回放时仍正常启动 App。
6
14
 
7
15
  ## v0.1.11
8
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "android-midscene-automation",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "android-midscene-automation": "./bin/android-midscene-automation.js"
@@ -557,6 +557,10 @@ async function appendStepDiagnostics(lines: string[], deviceId: string, step: Ap
557
557
  }
558
558
  }
559
559
 
560
+ type ReplayStepOptions = {
561
+ skipLaunchApp?: boolean;
562
+ };
563
+
560
564
  async function replayLinkedScript(
561
565
  sessionId: string,
562
566
  deviceId: string,
@@ -580,7 +584,9 @@ async function replayLinkedScript(
580
584
 
581
585
  lines.push(`连接脚本开始:${linkedScript.name}`);
582
586
  const nextStack = [...stack, linkedScript.id];
583
- await replayScriptSteps(sessionId, deviceId, linkedScript.steps, lines, nextStack);
587
+ await replayScriptSteps(sessionId, deviceId, linkedScript.steps, lines, nextStack, {
588
+ skipLaunchApp: true,
589
+ });
584
590
  lines.push(`连接脚本完成:${linkedScript.name}`);
585
591
  }
586
592
 
@@ -590,8 +596,13 @@ async function replayLinearSteps(
590
596
  steps: AppiumRecordedStepRecord[],
591
597
  lines: string[],
592
598
  stack: string[] = [],
599
+ options: ReplayStepOptions = {},
593
600
  ) {
594
601
  for (const [index, step] of steps.entries()) {
602
+ if (options.skipLaunchApp && step.type === 'launchApp') {
603
+ lines.push(`[步骤 ${index + 1}] 跳过:${step.label}(连接脚本不重复启动 App)`);
604
+ continue;
605
+ }
595
606
  lines.push(`[步骤 ${index + 1}] 开始:${step.label}`);
596
607
  try {
597
608
  if (step.type === 'runScript') {
@@ -619,6 +630,7 @@ async function replayFlowSteps(
619
630
  steps: AppiumRecordedStepRecord[],
620
631
  lines: string[],
621
632
  stack: string[] = [],
633
+ options: ReplayStepOptions = {},
622
634
  ) {
623
635
  const idToIndex = new Map(steps.map((step, index) => [step.id, index]));
624
636
  const visitedPath: string[] = [];
@@ -631,6 +643,12 @@ async function replayFlowSteps(
631
643
  if (guard > maxVisits) throw new Error('流程图可能存在循环,已终止回放');
632
644
 
633
645
  const step = steps[index];
646
+ if (options.skipLaunchApp && step.type === 'launchApp') {
647
+ lines.push(`[节点 ${index + 1}] 跳过:${step.label}(连接脚本不重复启动 App)`);
648
+ const targetId = step.flow?.successTargetId;
649
+ index = targetId ? idToIndex.get(targetId) : index + 1;
650
+ continue;
651
+ }
634
652
  visitedPath.push(step.label);
635
653
  const nodeKind = step.flow?.nodeKind || (step.type === 'assertExists' || step.type === 'assertText' ? 'assertion' : 'action');
636
654
  lines.push(`[节点 ${index + 1}] 开始:${step.label}`);
@@ -679,12 +697,13 @@ async function replayStepGroup(
679
697
  steps: AppiumRecordedStepRecord[],
680
698
  lines: string[],
681
699
  stack: string[],
700
+ options: ReplayStepOptions = {},
682
701
  ) {
683
702
  if (!steps.length) return;
684
703
  if (hasFlowSteps(steps)) {
685
- await replayFlowSteps(sessionId, deviceId, steps, lines, stack);
704
+ await replayFlowSteps(sessionId, deviceId, steps, lines, stack, options);
686
705
  } else {
687
- await replayLinearSteps(sessionId, deviceId, steps, lines, stack);
706
+ await replayLinearSteps(sessionId, deviceId, steps, lines, stack, options);
688
707
  }
689
708
  }
690
709
 
@@ -694,36 +713,21 @@ async function replayScriptSteps(
694
713
  steps: AppiumRecordedStepRecord[],
695
714
  lines: string[],
696
715
  stack: string[],
716
+ options: ReplayStepOptions = {},
697
717
  ) {
698
- const hasScopedSteps = steps.some((step) => step.flow?.scope === 'pre' || step.flow?.scope === 'post');
699
- if (!hasScopedSteps) {
700
- await replayStepGroup(sessionId, deviceId, steps, lines, stack);
701
- return;
718
+ let trailingLinkIndex = steps.length;
719
+ while (trailingLinkIndex > 0 && steps[trailingLinkIndex - 1]?.type === 'runScript') {
720
+ trailingLinkIndex -= 1;
702
721
  }
703
722
 
704
- const preSteps = steps.filter((step) => step.flow?.scope === 'pre');
705
- const mainSteps = steps.filter((step) => !step.flow?.scope || step.flow.scope === 'main');
706
- const postSteps = steps.filter((step) => step.flow?.scope === 'post');
707
- let primaryError: unknown;
708
-
709
- try {
710
- if (preSteps.length) lines.push('执行前置操作...');
711
- await replayStepGroup(sessionId, deviceId, preSteps, lines, stack);
712
- if (mainSteps.length) lines.push('执行主流程...');
713
- await replayStepGroup(sessionId, deviceId, mainSteps, lines, stack);
714
- } catch (error) {
715
- primaryError = error;
716
- }
723
+ const mainSteps = steps.slice(0, trailingLinkIndex);
724
+ const trailingLinkedSteps = steps.slice(trailingLinkIndex);
725
+ await replayStepGroup(sessionId, deviceId, mainSteps, lines, stack, options);
717
726
 
718
- try {
719
- if (postSteps.length) lines.push('执行后置操作...');
720
- await replayStepGroup(sessionId, deviceId, postSteps, lines, stack);
721
- } catch (error) {
722
- if (!primaryError) throw error;
723
- lines.push(`后置操作失败:${errorDetail(error)}`);
727
+ for (const step of trailingLinkedSteps) {
728
+ lines.push(`当前脚本步骤完成,开始执行:${step.label}`);
729
+ await replayLinkedScript(sessionId, deviceId, step, lines, stack);
724
730
  }
725
-
726
- if (primaryError) throw primaryError;
727
731
  }
728
732
 
729
733
  export async function replayAppiumScript(script: AppiumRecordedScriptRecord, deviceId: string) {
@@ -73,7 +73,6 @@ export type AppiumRecordedStepRecord = {
73
73
  keyCode?: number;
74
74
  timeoutMs?: number;
75
75
  flow?: {
76
- scope?: 'pre' | 'main' | 'post';
77
76
  nodeKind?: 'action' | 'condition' | 'assertion';
78
77
  yesTargetId?: string;
79
78
  noTargetId?: string;
@@ -249,17 +248,35 @@ export function saveAppiumRecordedScript(input: {
249
248
  if (!name) throw new Error('脚本名称不能为空');
250
249
  if (!appPackage) throw new Error('App 包名不能为空');
251
250
 
252
- if (input.id && getAppiumRecordedScript(input.id)) {
251
+ if (input.id) {
253
252
  runSql(`
254
- UPDATE appium_recorded_scripts
255
- SET
256
- name = ${sqlString(name)},
257
- app_package = ${sqlString(appPackage)},
258
- app_activity = ${sqlNullableString(input.appActivity || '')},
259
- device_id = ${sqlNullableString(input.deviceId || '')},
260
- flow_json = ${sqlJson(input.steps || [])},
261
- updated_at = ${sqlString(now)}
262
- WHERE id = ${sqlString(input.id)};
253
+ INSERT INTO appium_recorded_scripts (
254
+ id,
255
+ name,
256
+ app_package,
257
+ app_activity,
258
+ device_id,
259
+ flow_json,
260
+ created_at,
261
+ updated_at
262
+ )
263
+ VALUES (
264
+ ${sqlString(input.id)},
265
+ ${sqlString(name)},
266
+ ${sqlString(appPackage)},
267
+ ${sqlNullableString(input.appActivity || '')},
268
+ ${sqlNullableString(input.deviceId || '')},
269
+ ${sqlJson(input.steps || [])},
270
+ ${sqlString(now)},
271
+ ${sqlString(now)}
272
+ )
273
+ ON CONFLICT(id) DO UPDATE SET
274
+ name = excluded.name,
275
+ app_package = excluded.app_package,
276
+ app_activity = excluded.app_activity,
277
+ device_id = excluded.device_id,
278
+ flow_json = excluded.flow_json,
279
+ updated_at = excluded.updated_at;
263
280
  `);
264
281
  return getAppiumRecordedScript(input.id);
265
282
  }
@@ -20,7 +20,7 @@ import type { AppiumNode, AppiumRecordedScript, AppiumRecordedStep } from './typ
20
20
 
21
21
  type NodeStepType = 'tap' | 'input' | 'assertExists' | 'waitFor';
22
22
  type BranchName = 'yes' | 'no';
23
- type StepScope = 'pre' | 'main' | 'post';
23
+ type LegacyFlow = NonNullable<AppiumRecordedStep['flow']> & { scope?: string };
24
24
  type BranchTarget = {
25
25
  stepId: string;
26
26
  branch: BranchName;
@@ -138,44 +138,47 @@ const selectedBounds = computed(() => {
138
138
  return bounds && selectedNode.value ? { id: selectedNode.value.id, ...bounds } : undefined;
139
139
  });
140
140
 
141
- function stepScope(step?: AppiumRecordedStep): StepScope {
142
- return step?.flow?.scope || 'main';
143
- }
144
-
145
- function insertionScope(index?: number): StepScope {
146
- if (index === -2) return 'pre';
147
- if (index === -3) return 'post';
148
- return typeof index === 'number' && index >= 0 ? stepScope(steps.value[index]) : 'main';
149
- }
150
-
151
- function withStepScope(step: AppiumRecordedStep, scope: StepScope): AppiumRecordedStep {
152
- if (scope === 'main') return step;
153
- return {
154
- ...step,
155
- flow: {
156
- ...(step.flow || {}),
157
- scope,
158
- },
159
- };
160
- }
161
-
162
141
  function insertStep(step: AppiumRecordedStep, index?: number) {
163
142
  const nextSteps = [...steps.value];
164
- if (index === -2) {
165
- const firstMainIndex = nextSteps.findIndex((item) => stepScope(item) !== 'pre');
166
- const insertAt = firstMainIndex < 0 ? nextSteps.length : firstMainIndex;
167
- nextSteps.splice(insertAt, 0, withStepScope(step, 'pre'));
168
- } else if (index === -3) {
169
- nextSteps.push(withStepScope(step, 'post'));
170
- } else if (index === -1) {
171
- const firstMainIndex = nextSteps.findIndex((item) => stepScope(item) !== 'pre');
172
- nextSteps.splice(firstMainIndex < 0 ? nextSteps.length : firstMainIndex, 0, step);
143
+ let insertedStep = step;
144
+ if (index === -1) {
145
+ nextSteps.unshift(step);
146
+ } else if (typeof index === 'number') {
147
+ const previousStep = nextSteps[index];
148
+ const previousTargetId = previousStep?.flow?.successTargetId;
149
+ if (previousStep && previousTargetId) {
150
+ insertedStep = {
151
+ ...step,
152
+ flow: {
153
+ ...(step.flow || {}),
154
+ successTargetId: previousTargetId,
155
+ },
156
+ };
157
+ nextSteps[index] = {
158
+ ...previousStep,
159
+ flow: {
160
+ ...(previousStep.flow || {}),
161
+ successTargetId: insertedStep.id,
162
+ },
163
+ };
164
+ }
165
+ nextSteps.splice(index + 1, 0, insertedStep);
173
166
  } else {
174
- const scope = typeof index === 'number' ? stepScope(nextSteps[index]) : 'main';
175
- nextSteps.splice(typeof index === 'number' ? index + 1 : nextSteps.length, 0, withStepScope(step, scope));
167
+ nextSteps.push(step);
176
168
  }
177
169
  steps.value = nextSteps;
178
- return step;
170
+ return insertedStep;
171
+ }
172
+
173
+ function normalizeLegacyFlowScope(step: AppiumRecordedStep): AppiumRecordedStep {
174
+ const flow = step.flow as LegacyFlow | undefined;
175
+ if (!flow?.scope) return step;
176
+ const nextFlow = { ...flow };
177
+ delete nextFlow.scope;
178
+ return {
179
+ ...step,
180
+ flow: Object.keys(nextFlow).length ? nextFlow : undefined,
181
+ };
179
182
  }
180
183
 
181
184
  function updateBranchTarget(stepId: string, branch: BranchName, targetId: string) {
@@ -632,13 +635,12 @@ async function addAction(
632
635
  if (recordingBusy.value) return;
633
636
  if (scriptActivityMismatch.value && !['keyBack', 'runScript', 'launchApp', 'delay'].includes(action)) return;
634
637
  if (!ensureAppPackageSelected()) return;
635
- const scope = insertionScope(index);
636
- if (scope !== 'main' && action !== 'launchApp' && action !== 'delay') {
637
- ElMessage.warning('前置和后置操作仅支持启动 APP 和添加延时');
638
+ if (action === 'launchApp' && steps.value.some((step) => step.type === 'launchApp')) {
639
+ ElMessage.warning('启动 APP 节点只能添加一个');
638
640
  return;
639
641
  }
640
- if (scope === 'main' && action === 'launchApp') {
641
- ElMessage.warning('启动 APP 只能添加到前置或后置操作');
642
+ if (action === 'launchApp' && index !== -1) {
643
+ ElMessage.warning('启动 APP 只能从开始节点添加');
642
644
  return;
643
645
  }
644
646
  if (action === 'runScript') {
@@ -805,7 +807,7 @@ async function saveOnActivityChange() {
805
807
  const scriptName = form.name.trim() || `${activityName}-${Date.now().toString(36).slice(-4)}`;
806
808
  try {
807
809
  const payload = await saveAppiumScript({
808
- id: selectedScript.value?.id,
810
+ id: selectedScriptId.value || undefined,
809
811
  name: scriptName,
810
812
  appPackage: form.appPackage,
811
813
  appActivity: pending.beforeActivity || form.appActivity,
@@ -887,7 +889,7 @@ function loadScript(script: AppiumRecordedScript) {
887
889
  if (script.deviceId && script.deviceId !== selectedDeviceId.value) {
888
890
  void switchDevice(script.deviceId);
889
891
  }
890
- steps.value = script.steps || [];
892
+ steps.value = (script.steps || []).map(normalizeLegacyFlowScope);
891
893
  activeWorkbenchTab.value = 'recording';
892
894
  }
893
895
 
@@ -946,7 +948,7 @@ async function saveScript() {
946
948
  saving.value = true;
947
949
  try {
948
950
  const payload = await saveAppiumScript({
949
- id: selectedScript.value?.id,
951
+ id: selectedScriptId.value || undefined,
950
952
  name: form.name,
951
953
  appPackage: form.appPackage,
952
954
  appActivity: form.appActivity,
@@ -5,7 +5,6 @@ import type { AppiumRecordedStep, AppiumSelector } from '../types';
5
5
 
6
6
  type FlowKind = 'action' | 'condition' | 'assertion';
7
7
  type BranchName = 'yes' | 'no';
8
- type StepScope = 'pre' | 'main' | 'post';
9
8
  type StepItem = { step: AppiumRecordedStep; index: number };
10
9
  type InsertAction =
11
10
  | 'delay'
@@ -77,6 +76,8 @@ const stepItems = computed<StepItem[]>(() => props.steps
77
76
 
78
77
  const visibleStepItems = computed(() => stepItems.value
79
78
  .filter(({ step }) => !branchTargetIds.value.has(step.id)));
79
+ const hasFlowSteps = computed(() => visibleStepItems.value.length > 0);
80
+ const hasLaunchAppStep = computed(() => props.steps.some((step) => step.type === 'launchApp'));
80
81
 
81
82
  const insertActionGroups: Array<{ title: string; actions: Array<{ type: InsertAction; label: string }> }> = [
82
83
  {
@@ -130,28 +131,10 @@ const mainActionGroups = insertActionGroups.map((group) => ({
130
131
  actions: group.actions.filter((action) => action.type !== 'launchApp'),
131
132
  }));
132
133
 
133
- const boundaryActionGroups = [{
134
- title: '前后置操作',
135
- actions: [
136
- { type: 'launchApp' as const, label: '启动 APP' },
137
- { type: 'delay' as const, label: '添加延时' },
138
- ],
139
- }];
134
+ const startActionGroups = insertActionGroups;
140
135
 
141
- function stepScope(step: AppiumRecordedStep): StepScope {
142
- return step.flow?.scope || 'main';
143
- }
144
-
145
- function scopedStepOptions(source: AppiumRecordedStep) {
146
- const scope = stepScope(source);
147
- return stepOptions.value.filter((option) => {
148
- const target = props.steps.find((step) => step.id === option.value);
149
- return target && stepScope(target) === scope;
150
- });
151
- }
152
-
153
- function actionGroupsForStep(step: AppiumRecordedStep) {
154
- return stepScope(step) === 'main' ? mainActionGroups : boundaryActionGroups;
136
+ function scopedStepOptions() {
137
+ return stepOptions.value;
155
138
  }
156
139
 
157
140
  function stepMeta(step: AppiumRecordedStep) {
@@ -273,6 +256,10 @@ function insertAction(index: number, command: string | number | object) {
273
256
  emit('insertAction', index, command as InsertAction);
274
257
  }
275
258
 
259
+ function insertStartAction(command: string | number | object) {
260
+ emit('insertAction', -1, command as InsertAction);
261
+ }
262
+
276
263
  function insertBranchAction(index: number, branch: BranchName, command: string | number | object) {
277
264
  emit('insertBranchAction', index, branch, command as InsertAction);
278
265
  }
@@ -281,6 +268,10 @@ function isInsertActionDisabled(action: InsertAction) {
281
268
  return Boolean(props.disabled && !props.allowedLockedActions?.includes(action));
282
269
  }
283
270
 
271
+ function isStartActionDisabled(action: InsertAction) {
272
+ return (action === 'launchApp' && hasLaunchAppStep.value) || isInsertActionDisabled(action);
273
+ }
274
+
284
275
  function canOpenInsertMenu() {
285
276
  return !props.disabled || Boolean(props.allowedLockedActions?.length);
286
277
  }
@@ -381,48 +372,14 @@ watch(() => props.steps.length, () => {
381
372
  class="appium-flow-content"
382
373
  :style="{ transform: `translate(${flowPan.x}px, ${flowPan.y}px) scale(${flowScale})` }"
383
374
  >
384
- <div class="appium-flow-boundary appium-flow-boundary--pre">
385
- <span class="appium-flow-boundary__label">前置操作</span>
386
- <el-dropdown
387
- trigger="click"
388
- :disabled="!canOpenInsertMenu()"
389
- max-height="320px"
390
- popper-class="appium-action-dropdown"
391
- @command="insertAction(-2, $event)"
392
- >
393
- <button
394
- type="button"
395
- class="appium-flow-insert"
396
- :disabled="!canOpenInsertMenu()"
397
- >
398
- <el-icon><Timer /></el-icon>
399
- <span>添加前置操作</span>
400
- </button>
401
- <template #dropdown>
402
- <el-dropdown-menu>
403
- <template v-for="group in boundaryActionGroups" :key="group.title">
404
- <div class="appium-action-dropdown__group">{{ group.title }}</div>
405
- <el-dropdown-item
406
- v-for="action in group.actions"
407
- :key="action.type"
408
- :command="action.type"
409
- :disabled="isInsertActionDisabled(action.type)"
410
- >
411
- {{ action.label }}
412
- </el-dropdown-item>
413
- </template>
414
- </el-dropdown-menu>
415
- </template>
416
- </el-dropdown>
417
- </div>
418
- <div class="appium-flow-start">
375
+ <div class="appium-flow-start" :class="{ 'appium-flow-start--has-next': hasFlowSteps }">
419
376
  <div class="appium-flow-start__node">开始</div>
420
377
  <el-dropdown
421
378
  trigger="click"
422
379
  :disabled="!canOpenInsertMenu()"
423
380
  max-height="320px"
424
381
  popper-class="appium-action-dropdown"
425
- @command="insertAction(-1, $event)"
382
+ @command="insertStartAction($event)"
426
383
  >
427
384
  <button
428
385
  type="button"
@@ -434,13 +391,13 @@ watch(() => props.steps.length, () => {
434
391
  </button>
435
392
  <template #dropdown>
436
393
  <el-dropdown-menu>
437
- <template v-for="group in mainActionGroups" :key="group.title">
394
+ <template v-for="group in startActionGroups" :key="group.title">
438
395
  <div class="appium-action-dropdown__group">{{ group.title }}</div>
439
396
  <el-dropdown-item
440
397
  v-for="action in group.actions"
441
398
  :key="action.type"
442
399
  :command="action.type"
443
- :disabled="isInsertActionDisabled(action.type)"
400
+ :disabled="isStartActionDisabled(action.type)"
444
401
  >
445
402
  {{ action.label }}
446
403
  </el-dropdown-item>
@@ -453,10 +410,7 @@ watch(() => props.steps.length, () => {
453
410
  v-for="{ step, index } in visibleStepItems"
454
411
  :key="step.id"
455
412
  class="appium-flow-item"
456
- :class="[
457
- `appium-flow-item--${stepScope(step)}`,
458
- { 'appium-flow-item--condition': defaultKind(step) === 'condition' },
459
- ]"
413
+ :class="{ 'appium-flow-item--condition': defaultKind(step) === 'condition' }"
460
414
  >
461
415
  <div
462
416
  class="appium-flow-node"
@@ -739,7 +693,7 @@ watch(() => props.steps.length, () => {
739
693
  placeholder="默认下一步"
740
694
  @update:model-value="patchFlow(index, { yesTargetId: String($event || '') })"
741
695
  >
742
- <el-option v-for="option in scopedStepOptions(step)" :key="option.value" :label="option.label" :value="option.value" />
696
+ <el-option v-for="option in scopedStepOptions()" :key="option.value" :label="option.label" :value="option.value" />
743
697
  </el-select>
744
698
  </el-form-item>
745
699
  <el-form-item label="否,进入">
@@ -750,7 +704,7 @@ watch(() => props.steps.length, () => {
750
704
  placeholder="默认结束"
751
705
  @update:model-value="patchFlow(index, { noTargetId: String($event || '') })"
752
706
  >
753
- <el-option v-for="option in scopedStepOptions(step)" :key="option.value" :label="option.label" :value="option.value" />
707
+ <el-option v-for="option in scopedStepOptions()" :key="option.value" :label="option.label" :value="option.value" />
754
708
  </el-select>
755
709
  </el-form-item>
756
710
  </div>
@@ -760,7 +714,7 @@ watch(() => props.steps.length, () => {
760
714
  插入延时
761
715
  </el-button>
762
716
  <el-button
763
- v-if="defaultKind(step) !== 'condition' && stepScope(step) === 'main'"
717
+ v-if="defaultKind(step) !== 'condition'"
764
718
  :disabled="disabled"
765
719
  @click="patchFlow(index, { nodeKind: 'condition', yesTargetId: '', noTargetId: '' })"
766
720
  >
@@ -788,7 +742,7 @@ watch(() => props.steps.length, () => {
788
742
  </button>
789
743
  <template #dropdown>
790
744
  <el-dropdown-menu>
791
- <template v-for="group in actionGroupsForStep(step)" :key="group.title">
745
+ <template v-for="group in mainActionGroups" :key="group.title">
792
746
  <div class="appium-action-dropdown__group">{{ group.title }}</div>
793
747
  <el-dropdown-item
794
748
  v-for="action in group.actions"
@@ -803,43 +757,6 @@ watch(() => props.steps.length, () => {
803
757
  </template>
804
758
  </el-dropdown>
805
759
  </div>
806
- <div class="appium-flow-end">
807
- <div class="appium-flow-end__node">结束</div>
808
- </div>
809
- <div class="appium-flow-boundary appium-flow-boundary--post">
810
- <span class="appium-flow-boundary__label">后置操作</span>
811
- <el-dropdown
812
- trigger="click"
813
- :disabled="!canOpenInsertMenu()"
814
- max-height="320px"
815
- popper-class="appium-action-dropdown"
816
- @command="insertAction(-3, $event)"
817
- >
818
- <button
819
- type="button"
820
- class="appium-flow-insert"
821
- :disabled="!canOpenInsertMenu()"
822
- >
823
- <el-icon><Timer /></el-icon>
824
- <span>添加后置操作</span>
825
- </button>
826
- <template #dropdown>
827
- <el-dropdown-menu>
828
- <template v-for="group in boundaryActionGroups" :key="group.title">
829
- <div class="appium-action-dropdown__group">{{ group.title }}</div>
830
- <el-dropdown-item
831
- v-for="action in group.actions"
832
- :key="action.type"
833
- :command="action.type"
834
- :disabled="isInsertActionDisabled(action.type)"
835
- >
836
- {{ action.label }}
837
- </el-dropdown-item>
838
- </template>
839
- </el-dropdown-menu>
840
- </template>
841
- </el-dropdown>
842
- </div>
843
760
  </div>
844
761
  </div>
845
762
  <el-dialog
@@ -851,20 +768,17 @@ watch(() => props.steps.length, () => {
851
768
  >
852
769
  <div class="appium-flow-dialog-canvas">
853
770
  <div class="appium-flow-content appium-flow-content--overview">
854
- <div class="appium-flow-boundary appium-flow-boundary--pre">
855
- <span class="appium-flow-boundary__label">前置操作</span>
856
- </div>
857
- <div class="appium-flow-start appium-flow-start--overview">
771
+ <div
772
+ class="appium-flow-start appium-flow-start--overview"
773
+ :class="{ 'appium-flow-start--has-next': hasFlowSteps }"
774
+ >
858
775
  <div class="appium-flow-start__node">开始</div>
859
776
  </div>
860
777
  <div
861
778
  v-for="{ step, index } in visibleStepItems"
862
779
  :key="step.id"
863
780
  class="appium-flow-item"
864
- :class="[
865
- `appium-flow-item--${stepScope(step)}`,
866
- { 'appium-flow-item--condition': defaultKind(step) === 'condition' },
867
- ]"
781
+ :class="{ 'appium-flow-item--condition': defaultKind(step) === 'condition' }"
868
782
  >
869
783
  <div
870
784
  class="appium-flow-node"
@@ -928,12 +842,6 @@ watch(() => props.steps.length, () => {
928
842
  </div>
929
843
  </div>
930
844
  </div>
931
- <div class="appium-flow-end">
932
- <div class="appium-flow-end__node">结束</div>
933
- </div>
934
- <div class="appium-flow-boundary appium-flow-boundary--post">
935
- <span class="appium-flow-boundary__label">后置操作</span>
936
- </div>
937
845
  </div>
938
846
  </div>
939
847
  </el-dialog>
@@ -75,7 +75,6 @@ export type AppiumRecordedStep = {
75
75
  keyCode?: number;
76
76
  timeoutMs?: number;
77
77
  flow?: {
78
- scope?: 'pre' | 'main' | 'post';
79
78
  nodeKind?: 'action' | 'condition' | 'assertion';
80
79
  yesTargetId?: string;
81
80
  noTargetId?: string;
package/src/style.css CHANGED
@@ -728,70 +728,7 @@ select {
728
728
  gap: 6px;
729
729
  }
730
730
 
731
- .appium-flow-boundary {
732
- position: relative;
733
- z-index: 1;
734
- display: flex;
735
- align-items: center;
736
- gap: 8px;
737
- width: min(440px, 100%);
738
- }
739
-
740
- .appium-flow-boundary__label {
741
- min-width: 62px;
742
- color: #606266;
743
- font-size: 12px;
744
- font-weight: 700;
745
- }
746
-
747
- .appium-flow-boundary--pre {
748
- order: 0;
749
- }
750
-
751
- .appium-flow-item--pre {
752
- order: 1;
753
- }
754
-
755
- .appium-flow-start {
756
- order: 2;
757
- }
758
-
759
- .appium-flow-item--main {
760
- order: 3;
761
- }
762
-
763
- .appium-flow-end {
764
- position: relative;
765
- z-index: 1;
766
- order: 4;
767
- display: grid;
768
- justify-items: start;
769
- }
770
-
771
- .appium-flow-end__node {
772
- display: grid;
773
- place-items: center;
774
- box-sizing: border-box;
775
- width: 38px;
776
- height: 30px;
777
- padding: 0 6px;
778
- border: 1px solid #909399;
779
- border-radius: 8px;
780
- background: #f4f4f5;
781
- color: #606266;
782
- font-size: 12px;
783
- font-weight: 700;
784
- }
785
-
786
- .appium-flow-boundary--post {
787
- order: 5;
788
- }
789
-
790
- .appium-flow-item--post {
791
- order: 6;
792
- }
793
-
794
- .appium-flow-start::after {
731
+ .appium-flow-start--has-next::after {
795
732
  content: "";
796
733
  position: absolute;
797
734
  z-index: 0;
@@ -1198,6 +1135,10 @@ select {
1198
1135
  opacity: 0.45;
1199
1136
  }
1200
1137
 
1138
+ .appium-flow-start .appium-flow-insert {
1139
+ margin-left: 0;
1140
+ }
1141
+
1201
1142
  .appium-linked-script-select {
1202
1143
  width: 100%;
1203
1144
  }