android-midscene-automation 0.1.26 → 0.1.27
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 -0
- package/FAQ.md +14 -0
- package/README.md +31 -3
- package/bin/android-midscene-automation.js +71 -1
- package/package.json +4 -1
- package/server/appium-recorder/appium-runner.ts +273 -28
- package/server/appium-recorder/report.ts +186 -3
- package/server/appium-recorder/repository.ts +24 -3
- package/server/appium-recorder/visual-change.ts +184 -0
- package/server/http-api.ts +3 -4
- package/src/appium-recorder/AppiumPage.vue +465 -139
- package/src/appium-recorder/api.ts +1 -0
- package/src/appium-recorder/components/FlowCanvas.vue +14 -1
- package/src/appium-recorder/components/FlowStepEditor.vue +121 -2
- package/src/appium-recorder/components/NestedConditionBranches.vue +26 -4
- package/src/appium-recorder/components/RecordedSteps.vue +13 -1
- package/src/appium-recorder/components/VisualChangeDialog.vue +151 -0
- package/src/appium-recorder/flow-copy.ts +47 -1
- package/src/appium-recorder/flow-graph.ts +101 -6
- package/src/appium-recorder/flow-labels.ts +6 -1
- package/src/appium-recorder/flow-normalize.ts +67 -0
- package/src/appium-recorder/flow-remove.ts +67 -0
- package/src/appium-recorder/types.ts +25 -1
- package/src/appium-recorder/visual-change-pairs.ts +147 -0
- package/src/appium-recorder/visual-change.ts +75 -0
- package/src/components/device/DevicePreviewPanel.vue +148 -4
- package/src/style.css +39 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, h, onMounted, onUnmounted, reactive, shallowRef, watch } from 'vue';
|
|
3
3
|
import { ElForm, ElFormItem, ElInputNumber, ElMessage, ElMessageBox, ElOption, ElSelect } from 'element-plus';
|
|
4
|
-
import { Check, CircleClose, CopyDocument, Delete, Document, Download, Refresh, Upload, VideoPlay, View } from '@element-plus/icons-vue';
|
|
4
|
+
import { Check, CircleClose, CopyDocument, Delete, Document, Download, Edit, Refresh, Upload, VideoPlay, View } from '@element-plus/icons-vue';
|
|
5
5
|
import type { AndroidDevice, AppPreset, DeviceAction } from '../types';
|
|
6
6
|
import DevicePreviewPanel from '../components/device/DevicePreviewPanel.vue';
|
|
7
7
|
import {
|
|
@@ -21,15 +21,34 @@ import ComponentTree from './components/ComponentTree.vue';
|
|
|
21
21
|
import FlowCanvas from './components/FlowCanvas.vue';
|
|
22
22
|
import NodeDetail from './components/NodeDetail.vue';
|
|
23
23
|
import RecordedSteps from './components/RecordedSteps.vue';
|
|
24
|
+
import VisualChangeDialog from './components/VisualChangeDialog.vue';
|
|
24
25
|
import {
|
|
25
26
|
createFlowClipboard,
|
|
26
27
|
pasteFlowClipboard,
|
|
27
28
|
type FlowClipboard,
|
|
28
29
|
} from './flow-copy';
|
|
29
30
|
import { labelFlowStep } from './flow-labels';
|
|
31
|
+
import { normalizeLegacyNestedConditionBranches } from './flow-normalize';
|
|
32
|
+
import { removeFlowStep } from './flow-remove';
|
|
30
33
|
import type { FlowActionGroup, InsertAction } from './flow-graph';
|
|
31
34
|
import { findSmallestNodeAtPoint, flattenNodes, parseWindowHierarchy } from './tree';
|
|
32
|
-
import
|
|
35
|
+
import {
|
|
36
|
+
boundsToVisualRegion,
|
|
37
|
+
createStepId,
|
|
38
|
+
normalizeVisualChangeConfig,
|
|
39
|
+
visualRegionToBounds,
|
|
40
|
+
} from './visual-change';
|
|
41
|
+
import {
|
|
42
|
+
findOpenVisualChangeStart,
|
|
43
|
+
nextVisualChangePairLabel,
|
|
44
|
+
} from './visual-change-pairs';
|
|
45
|
+
import type {
|
|
46
|
+
AppiumNode,
|
|
47
|
+
AppiumRecordedScript,
|
|
48
|
+
AppiumRecordedStep,
|
|
49
|
+
AppiumVisualChangeConfig,
|
|
50
|
+
AppiumVisualChangeRegion,
|
|
51
|
+
} from './types';
|
|
33
52
|
|
|
34
53
|
type NodeStepType = 'tap' | 'input' | 'assertExists' | 'waitFor';
|
|
35
54
|
type BranchName = 'yes' | 'no';
|
|
@@ -43,29 +62,7 @@ type BranchTarget = {
|
|
|
43
62
|
entryTargetId?: string;
|
|
44
63
|
nextTargetId?: string;
|
|
45
64
|
};
|
|
46
|
-
type RecorderAction =
|
|
47
|
-
| 'delay'
|
|
48
|
-
| NodeStepType
|
|
49
|
-
| 'tapIfExists'
|
|
50
|
-
| 'inputIfExists'
|
|
51
|
-
| 'clearIfExists'
|
|
52
|
-
| 'backIfExists'
|
|
53
|
-
| 'popupCondition'
|
|
54
|
-
| 'runScript'
|
|
55
|
-
| 'keyBack'
|
|
56
|
-
| 'keyHome'
|
|
57
|
-
| 'keyRecent'
|
|
58
|
-
| 'keyPower'
|
|
59
|
-
| 'waitActivity'
|
|
60
|
-
| 'swipe'
|
|
61
|
-
| 'clearInput'
|
|
62
|
-
| 'coordinateTap'
|
|
63
|
-
| 'launchApp'
|
|
64
|
-
| 'clearAppData'
|
|
65
|
-
| 'waitDisappear'
|
|
66
|
-
| 'assertText'
|
|
67
|
-
| 'longPress'
|
|
68
|
-
| 'pinch';
|
|
65
|
+
type RecorderAction = InsertAction;
|
|
69
66
|
|
|
70
67
|
const WORKBENCH_TAB_STORAGE_KEY = 'android-midscene-automation:appium-workbench-tab';
|
|
71
68
|
const AUTO_TREE_REFRESH_INTERVAL_MS = 1800;
|
|
@@ -96,6 +93,9 @@ const insertableRecorderActions: RecorderAction[] = [
|
|
|
96
93
|
'assertText',
|
|
97
94
|
'longPress',
|
|
98
95
|
'pinch',
|
|
96
|
+
'noop',
|
|
97
|
+
'visualChangeStart',
|
|
98
|
+
'visualChangeEnd',
|
|
99
99
|
];
|
|
100
100
|
const readonlyFlowActionGroups: FlowActionGroup[] = [];
|
|
101
101
|
const readonlyFlowSelectedIndexes: number[] = [];
|
|
@@ -145,11 +145,18 @@ const linkedScriptExpectedActivity = shallowRef('');
|
|
|
145
145
|
const linkedScriptPreviewVisible = shallowRef(false);
|
|
146
146
|
const linkedScriptPreviewScriptId = shallowRef('');
|
|
147
147
|
const linkedScriptPreviewResetToken = shallowRef(0);
|
|
148
|
+
const visualChangeDialogVisible = shallowRef(false);
|
|
149
|
+
const visualChangePicking = shallowRef(false);
|
|
150
|
+
const visualChangeInsertIndex = shallowRef<number | undefined>();
|
|
151
|
+
const visualChangeBranchTarget = shallowRef<BranchTarget | null>(null);
|
|
152
|
+
const visualChangeConfig = shallowRef<AppiumVisualChangeConfig>(normalizeVisualChangeConfig());
|
|
148
153
|
const loadingTree = shallowRef(false);
|
|
149
154
|
const saving = shallowRef(false);
|
|
150
155
|
const importingScript = shallowRef(false);
|
|
151
156
|
const scriptImportInput = shallowRef<HTMLInputElement | null>(null);
|
|
152
157
|
const deletingScriptId = shallowRef('');
|
|
158
|
+
const duplicatingScriptId = shallowRef('');
|
|
159
|
+
const renamingScriptId = shallowRef('');
|
|
153
160
|
const replaying = shallowRef(false);
|
|
154
161
|
const stoppingReplay = shallowRef(false);
|
|
155
162
|
const activeReplayDeviceId = shallowRef('');
|
|
@@ -229,16 +236,38 @@ const selectedBounds = computed(() => {
|
|
|
229
236
|
const bounds = selectedNode.value?.bounds;
|
|
230
237
|
return bounds && selectedNode.value ? { id: selectedNode.value.id, ...bounds } : undefined;
|
|
231
238
|
});
|
|
239
|
+
const visualChangeSelectedRegion = computed(() => {
|
|
240
|
+
if (!visualChangeDialogVisible.value || visualChangeConfig.value.mode !== 'region') return undefined;
|
|
241
|
+
return visualRegionToBounds('visual-change-region', visualChangeConfig.value.region);
|
|
242
|
+
});
|
|
243
|
+
const visualChangeRegionSelectionEnabled = computed(() => (
|
|
244
|
+
visualChangeDialogVisible.value && visualChangeConfig.value.mode === 'region'
|
|
245
|
+
));
|
|
232
246
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
247
|
+
function stepWithBranchTarget(step: AppiumRecordedStep, branchTarget: BranchTarget) {
|
|
248
|
+
return {
|
|
249
|
+
...step,
|
|
250
|
+
flow: {
|
|
251
|
+
...(step.flow || {}),
|
|
252
|
+
parentConditionId: branchTarget.stepId,
|
|
253
|
+
parentBranch: branchTarget.branch,
|
|
254
|
+
...(branchTarget.nextTargetId ? { successTargetId: branchTarget.nextTargetId } : {}),
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function insertStep(step: AppiumRecordedStep, index?: number, branchTarget?: BranchTarget) {
|
|
260
|
+
let nextSteps = [...steps.value];
|
|
261
|
+
let insertedStep = branchTarget ? stepWithBranchTarget(step, branchTarget) : step;
|
|
236
262
|
if (index === -1) {
|
|
237
263
|
nextSteps.unshift(insertedStep);
|
|
238
264
|
} else if (typeof index === 'number') {
|
|
239
265
|
const previousStep = nextSteps[index];
|
|
240
266
|
const previousTargetId = previousStep?.flow?.successTargetId;
|
|
241
|
-
|
|
267
|
+
const shouldRewireSuccess = previousStep
|
|
268
|
+
&& previousTargetId
|
|
269
|
+
&& !(branchTarget && previousStep.id === branchTarget.stepId);
|
|
270
|
+
if (shouldRewireSuccess) {
|
|
242
271
|
insertedStep = {
|
|
243
272
|
...insertedStep,
|
|
244
273
|
flow: {
|
|
@@ -256,7 +285,22 @@ function insertStep(step: AppiumRecordedStep, index?: number) {
|
|
|
256
285
|
}
|
|
257
286
|
nextSteps.splice(index + 1, 0, insertedStep);
|
|
258
287
|
} else {
|
|
259
|
-
nextSteps.push(
|
|
288
|
+
nextSteps.push(insertedStep);
|
|
289
|
+
}
|
|
290
|
+
if (branchTarget?.updateTarget) {
|
|
291
|
+
const targetKey = branchTarget.branch === 'yes' ? 'yesTargetId' : 'noTargetId';
|
|
292
|
+
nextSteps = nextSteps.map((item) => (
|
|
293
|
+
item.id === branchTarget.stepId
|
|
294
|
+
? {
|
|
295
|
+
...item,
|
|
296
|
+
flow: {
|
|
297
|
+
...(item.flow || {}),
|
|
298
|
+
nodeKind: 'condition',
|
|
299
|
+
[targetKey]: branchTarget.entryTargetId || insertedStep.id,
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
: item
|
|
303
|
+
));
|
|
260
304
|
}
|
|
261
305
|
steps.value = nextSteps;
|
|
262
306
|
return insertedStep;
|
|
@@ -334,17 +378,38 @@ function attachStepToBranch(stepId: string, conditionId: string, branch: BranchN
|
|
|
334
378
|
));
|
|
335
379
|
}
|
|
336
380
|
|
|
381
|
+
function applyBranchTargetToInsertedStep(inserted: AppiumRecordedStep, branchTarget: BranchTarget) {
|
|
382
|
+
attachStepToBranch(inserted.id, branchTarget.stepId, branchTarget.branch);
|
|
383
|
+
if (branchTarget.nextTargetId) {
|
|
384
|
+
steps.value = steps.value.map((step) => (
|
|
385
|
+
step.id === inserted.id
|
|
386
|
+
? {
|
|
387
|
+
...step,
|
|
388
|
+
flow: { ...(step.flow || {}), successTargetId: branchTarget.nextTargetId },
|
|
389
|
+
}
|
|
390
|
+
: step
|
|
391
|
+
));
|
|
392
|
+
}
|
|
393
|
+
if (branchTarget.updateTarget) {
|
|
394
|
+
updateBranchTarget(
|
|
395
|
+
branchTarget.stepId,
|
|
396
|
+
branchTarget.branch,
|
|
397
|
+
branchTarget.entryTargetId || inserted.id,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
337
402
|
function branchContextForInsert(index: number, branch: BranchName) {
|
|
338
403
|
const anchor = steps.value[index];
|
|
339
404
|
if (!anchor) return undefined;
|
|
405
|
+
if (anchor.flow?.nodeKind === 'condition') {
|
|
406
|
+
return { condition: anchor, conditionIndex: index };
|
|
407
|
+
}
|
|
340
408
|
if (anchor.flow?.parentConditionId && anchor.flow.parentBranch === branch) {
|
|
341
409
|
const conditionIndex = steps.value.findIndex((step) => step.id === anchor.flow?.parentConditionId);
|
|
342
410
|
const condition = conditionIndex >= 0 ? steps.value[conditionIndex] : undefined;
|
|
343
411
|
return condition ? { condition, conditionIndex } : undefined;
|
|
344
412
|
}
|
|
345
|
-
if (anchor.flow?.nodeKind === 'condition') {
|
|
346
|
-
return { condition: anchor, conditionIndex: index };
|
|
347
|
-
}
|
|
348
413
|
return undefined;
|
|
349
414
|
}
|
|
350
415
|
|
|
@@ -489,6 +554,87 @@ function createRunScriptStep(script: AppiumRecordedScript): AppiumRecordedStep {
|
|
|
489
554
|
};
|
|
490
555
|
}
|
|
491
556
|
|
|
557
|
+
function createNoopStep(): AppiumRecordedStep {
|
|
558
|
+
return {
|
|
559
|
+
id: createStepId(),
|
|
560
|
+
type: 'noop',
|
|
561
|
+
label: '空节点',
|
|
562
|
+
flow: { nodeKind: 'action' },
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function defaultVisualChangeRegion(): AppiumVisualChangeRegion {
|
|
567
|
+
const width = props.deviceWidth || 1080;
|
|
568
|
+
const height = props.deviceHeight || 1920;
|
|
569
|
+
const regionWidth = Math.max(1, Math.round(width * 0.5));
|
|
570
|
+
const regionHeight = Math.max(1, Math.round(height * 0.25));
|
|
571
|
+
return {
|
|
572
|
+
x: Math.max(0, Math.round((width - regionWidth) / 2)),
|
|
573
|
+
y: Math.max(0, Math.round((height - regionHeight) / 2)),
|
|
574
|
+
width: regionWidth,
|
|
575
|
+
height: regionHeight,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function currentVisualChangeConfig() {
|
|
580
|
+
const bounds = selectedNode.value?.bounds;
|
|
581
|
+
return normalizeVisualChangeConfig({
|
|
582
|
+
mode: bounds ? 'selectedElement' : 'region',
|
|
583
|
+
region: bounds ? boundsToVisualRegion(bounds) : defaultVisualChangeRegion(),
|
|
584
|
+
} as AppiumVisualChangeConfig);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function createVisualChangeStartStep(config: AppiumVisualChangeConfig): AppiumRecordedStep {
|
|
588
|
+
const normalizedConfig = normalizeVisualChangeConfig(config);
|
|
589
|
+
const pairId = createStepId();
|
|
590
|
+
const pairLabel = nextVisualChangePairLabel(steps.value);
|
|
591
|
+
const visualChange = {
|
|
592
|
+
...normalizedConfig,
|
|
593
|
+
role: 'start' as const,
|
|
594
|
+
pairId,
|
|
595
|
+
pairLabel,
|
|
596
|
+
startStepId: '',
|
|
597
|
+
endStepId: '',
|
|
598
|
+
};
|
|
599
|
+
const node = normalizedConfig.mode === 'selectedElement' ? selectedNode.value : null;
|
|
600
|
+
if (node) {
|
|
601
|
+
const step = createNodeActionStep('visualChange', `${pairLabel}-开始节点`, node);
|
|
602
|
+
return {
|
|
603
|
+
...step,
|
|
604
|
+
label: `${pairLabel}-开始节点`,
|
|
605
|
+
visualChange,
|
|
606
|
+
flow: { ...(step.flow || {}), nodeKind: 'assertion' },
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
id: createStepId(),
|
|
611
|
+
type: 'visualChange',
|
|
612
|
+
label: `${pairLabel}-开始节点`,
|
|
613
|
+
visualChange,
|
|
614
|
+
flow: { nodeKind: 'assertion' },
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function createVisualChangeEndStep(startStep: AppiumRecordedStep): AppiumRecordedStep {
|
|
619
|
+
const config = normalizeVisualChangeConfig(startStep.visualChange);
|
|
620
|
+
const id = createStepId();
|
|
621
|
+
const pairLabel = config.pairLabel || '检测画面变化';
|
|
622
|
+
const pairId = config.pairId || startStep.id;
|
|
623
|
+
return {
|
|
624
|
+
id,
|
|
625
|
+
type: 'visualChange',
|
|
626
|
+
label: `${pairLabel}-结束节点`,
|
|
627
|
+
visualChange: {
|
|
628
|
+
...config,
|
|
629
|
+
role: 'end',
|
|
630
|
+
pairId,
|
|
631
|
+
startStepId: startStep.id,
|
|
632
|
+
endStepId: id,
|
|
633
|
+
},
|
|
634
|
+
flow: { nodeKind: 'assertion' },
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
492
638
|
function linkedScriptExitActivity(script: AppiumRecordedScript) {
|
|
493
639
|
for (let index = script.steps.length - 1; index >= 0; index -= 1) {
|
|
494
640
|
const activity = script.steps[index]?.pageAfter?.activity;
|
|
@@ -763,7 +909,7 @@ async function swipePreview(gesture: {
|
|
|
763
909
|
}
|
|
764
910
|
}
|
|
765
911
|
|
|
766
|
-
async function recordTapStep(index?: number) {
|
|
912
|
+
async function recordTapStep(index?: number, branchTarget?: BranchTarget) {
|
|
767
913
|
if (!selectedNode.value || recordingTap.value) return;
|
|
768
914
|
if (!ensureAppPackageSelected()) return;
|
|
769
915
|
if (!selectedDeviceId.value) {
|
|
@@ -778,7 +924,7 @@ async function recordTapStep(index?: number) {
|
|
|
778
924
|
}
|
|
779
925
|
const beforeActivity = currentActivity.value;
|
|
780
926
|
const step = createStep('tap', node);
|
|
781
|
-
insertStep(step, index);
|
|
927
|
+
const inserted = insertStep(step, index, branchTarget);
|
|
782
928
|
recordingTap.value = true;
|
|
783
929
|
try {
|
|
784
930
|
await tapAppiumDevice({ deviceId: selectedDeviceId.value, x: bounds.centerX, y: bounds.centerY });
|
|
@@ -805,12 +951,12 @@ async function recordTapStep(index?: number) {
|
|
|
805
951
|
}
|
|
806
952
|
}
|
|
807
953
|
|
|
808
|
-
async function addStep(type: NodeStepType, index?: number) {
|
|
954
|
+
async function addStep(type: NodeStepType, index?: number, branchTarget?: BranchTarget) {
|
|
809
955
|
if (recordingBusy.value) return;
|
|
810
956
|
if (!selectedNode.value) return;
|
|
811
957
|
if (!ensureAppPackageSelected()) return;
|
|
812
958
|
if (type === 'tap') {
|
|
813
|
-
return recordTapStep(index);
|
|
959
|
+
return recordTapStep(index, branchTarget);
|
|
814
960
|
}
|
|
815
961
|
const step = createStep(type, selectedNode.value);
|
|
816
962
|
if (type === 'input') {
|
|
@@ -823,10 +969,10 @@ async function addStep(type: NodeStepType, index?: number) {
|
|
|
823
969
|
if (!input) return;
|
|
824
970
|
step.value = input.value;
|
|
825
971
|
}
|
|
826
|
-
return insertStep(step, index);
|
|
972
|
+
return insertStep(step, index, branchTarget);
|
|
827
973
|
}
|
|
828
974
|
|
|
829
|
-
async function addDelayStep(index?: number) {
|
|
975
|
+
async function addDelayStep(index?: number, branchTarget?: BranchTarget) {
|
|
830
976
|
if (recordingBusy.value) return;
|
|
831
977
|
const input = await ElMessageBox.prompt('请输入延时时间,单位毫秒', '添加延时', {
|
|
832
978
|
inputValue: '1000',
|
|
@@ -838,7 +984,7 @@ async function addDelayStep(index?: number) {
|
|
|
838
984
|
if (!input) return;
|
|
839
985
|
const timeoutMs = Number(input.value);
|
|
840
986
|
const step = createDelayStep(timeoutMs);
|
|
841
|
-
return insertStep(step, index);
|
|
987
|
+
return insertStep(step, index, branchTarget);
|
|
842
988
|
}
|
|
843
989
|
|
|
844
990
|
function toSwipeNumber(value: unknown, fallback: number) {
|
|
@@ -884,7 +1030,7 @@ function renderSwipeForm(form: SwipeForm) {
|
|
|
884
1030
|
]);
|
|
885
1031
|
}
|
|
886
1032
|
|
|
887
|
-
async function addSwipeStep(index?: number) {
|
|
1033
|
+
async function addSwipeStep(index?: number, branchTarget?: BranchTarget) {
|
|
888
1034
|
const preset = swipePreset('up');
|
|
889
1035
|
const form = reactive<SwipeForm>({
|
|
890
1036
|
direction: preset.direction,
|
|
@@ -909,7 +1055,7 @@ async function addSwipeStep(index?: number) {
|
|
|
909
1055
|
endY: toSwipeNumber(form.endY, finalPreset.swipe.endY),
|
|
910
1056
|
duration: Math.max(80, toSwipeNumber(form.duration, finalPreset.swipe.duration)),
|
|
911
1057
|
},
|
|
912
|
-
}), index);
|
|
1058
|
+
}), index, branchTarget);
|
|
913
1059
|
}
|
|
914
1060
|
|
|
915
1061
|
function openLinkedScriptDialog(index?: number, branchTarget?: BranchTarget) {
|
|
@@ -958,30 +1104,11 @@ function addLinkedScriptStep() {
|
|
|
958
1104
|
);
|
|
959
1105
|
return;
|
|
960
1106
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
item.id === step.id
|
|
967
|
-
? {
|
|
968
|
-
...item,
|
|
969
|
-
flow: {
|
|
970
|
-
...(item.flow || {}),
|
|
971
|
-
successTargetId: linkedScriptBranchTarget.value?.nextTargetId,
|
|
972
|
-
},
|
|
973
|
-
}
|
|
974
|
-
: item
|
|
975
|
-
));
|
|
976
|
-
}
|
|
977
|
-
if (linkedScriptBranchTarget.value.updateTarget) {
|
|
978
|
-
updateBranchTarget(
|
|
979
|
-
linkedScriptBranchTarget.value.stepId,
|
|
980
|
-
linkedScriptBranchTarget.value.branch,
|
|
981
|
-
linkedScriptBranchTarget.value.entryTargetId || step.id,
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
1107
|
+
insertStep(
|
|
1108
|
+
createRunScriptStep(script),
|
|
1109
|
+
linkedScriptInsertIndex.value,
|
|
1110
|
+
linkedScriptBranchTarget.value || undefined,
|
|
1111
|
+
);
|
|
985
1112
|
linkedScriptInsertIndex.value = undefined;
|
|
986
1113
|
linkedScriptBranchTarget.value = null;
|
|
987
1114
|
linkedScriptExpectedActivity.value = '';
|
|
@@ -1018,6 +1145,64 @@ function loadPreviewScriptForEditing() {
|
|
|
1018
1145
|
loadScript(linkedScriptPreviewScript.value);
|
|
1019
1146
|
}
|
|
1020
1147
|
|
|
1148
|
+
function openVisualChangeDialog(index?: number, branchTarget?: BranchTarget) {
|
|
1149
|
+
visualChangeInsertIndex.value = index;
|
|
1150
|
+
visualChangeBranchTarget.value = branchTarget || null;
|
|
1151
|
+
visualChangeConfig.value = currentVisualChangeConfig();
|
|
1152
|
+
visualChangePicking.value = false;
|
|
1153
|
+
visualChangeDialogVisible.value = true;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function closeVisualChangeDialog() {
|
|
1157
|
+
visualChangeDialogVisible.value = false;
|
|
1158
|
+
visualChangePicking.value = false;
|
|
1159
|
+
visualChangeInsertIndex.value = undefined;
|
|
1160
|
+
visualChangeBranchTarget.value = null;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function updateVisualChangeConfig(config: AppiumVisualChangeConfig) {
|
|
1164
|
+
visualChangeConfig.value = normalizeVisualChangeConfig(config);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function pickVisualChangeRegion() {
|
|
1168
|
+
if (!selectedDeviceId.value) {
|
|
1169
|
+
ElMessage.warning('请选择设备');
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
visualChangeConfig.value = normalizeVisualChangeConfig({
|
|
1173
|
+
...visualChangeConfig.value,
|
|
1174
|
+
mode: 'region',
|
|
1175
|
+
});
|
|
1176
|
+
visualChangePicking.value = true;
|
|
1177
|
+
ElMessage.info('请在设备预览中拖拽框选检测区域');
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function applyVisualChangeRegion(region: AppiumVisualChangeRegion) {
|
|
1181
|
+
visualChangeConfig.value = normalizeVisualChangeConfig({
|
|
1182
|
+
...visualChangeConfig.value,
|
|
1183
|
+
mode: 'region',
|
|
1184
|
+
region,
|
|
1185
|
+
});
|
|
1186
|
+
visualChangePicking.value = false;
|
|
1187
|
+
ElMessage.success('已选择检测区域');
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function confirmVisualChangeStep() {
|
|
1191
|
+
const config = normalizeVisualChangeConfig(visualChangeConfig.value);
|
|
1192
|
+
if (config.mode === 'selectedElement' && !selectedNode.value?.bounds) {
|
|
1193
|
+
ElMessage.warning('当前没有可用的选中元素,请先选择组件或改为手动框选区域');
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const step = createVisualChangeStartStep(config);
|
|
1197
|
+
insertStep(
|
|
1198
|
+
step,
|
|
1199
|
+
visualChangeInsertIndex.value,
|
|
1200
|
+
visualChangeBranchTarget.value || undefined,
|
|
1201
|
+
);
|
|
1202
|
+
closeVisualChangeDialog();
|
|
1203
|
+
ElMessage.success('已添加检测画面变化开始节点');
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1021
1206
|
function isReadonlyFlowActionDisabled(_action: InsertAction) {
|
|
1022
1207
|
return true;
|
|
1023
1208
|
}
|
|
@@ -1037,6 +1222,7 @@ async function addAction(
|
|
|
1037
1222
|
) {
|
|
1038
1223
|
if (recordingBusy.value) return;
|
|
1039
1224
|
if (!ensureAppPackageSelected()) return;
|
|
1225
|
+
const previousStep = typeof index === 'number' && index >= 0 ? steps.value[index] : undefined;
|
|
1040
1226
|
if (action === 'launchApp' && steps.value.some((step) => step.type === 'launchApp')) {
|
|
1041
1227
|
ElMessage.warning('启动 APP 节点只能添加一个');
|
|
1042
1228
|
return;
|
|
@@ -1045,8 +1231,12 @@ async function addAction(
|
|
|
1045
1231
|
ElMessage.warning('清理 App 缓存节点只能添加一个');
|
|
1046
1232
|
return;
|
|
1047
1233
|
}
|
|
1048
|
-
if (
|
|
1049
|
-
ElMessage.warning(
|
|
1234
|
+
if (action === 'clearAppData' && index !== -1) {
|
|
1235
|
+
ElMessage.warning('清理 App 缓存只能从开始节点添加');
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (action === 'launchApp' && index !== -1 && previousStep?.type !== 'clearAppData') {
|
|
1239
|
+
ElMessage.warning('启动 APP 只能从开始节点或清理 App 缓存后添加');
|
|
1050
1240
|
return;
|
|
1051
1241
|
}
|
|
1052
1242
|
if (action === 'runScript') {
|
|
@@ -1054,22 +1244,22 @@ async function addAction(
|
|
|
1054
1244
|
return;
|
|
1055
1245
|
}
|
|
1056
1246
|
if (action === 'delay') {
|
|
1057
|
-
return addDelayStep(index);
|
|
1247
|
+
return addDelayStep(index, branchTarget);
|
|
1058
1248
|
}
|
|
1059
1249
|
if (action === 'tap' || action === 'input' || action === 'assertExists' || action === 'waitFor') {
|
|
1060
|
-
return addStep(action, index);
|
|
1250
|
+
return addStep(action, index, branchTarget);
|
|
1061
1251
|
}
|
|
1062
1252
|
if (action === 'keyBack') {
|
|
1063
|
-
return insertStep(createKeyStep(4, '系统返回'), index);
|
|
1253
|
+
return insertStep(createKeyStep(4, '系统返回'), index, branchTarget);
|
|
1064
1254
|
}
|
|
1065
1255
|
if (action === 'keyHome') {
|
|
1066
|
-
return insertStep(createKeyStep(3, 'Home 键'), index);
|
|
1256
|
+
return insertStep(createKeyStep(3, 'Home 键'), index, branchTarget);
|
|
1067
1257
|
}
|
|
1068
1258
|
if (action === 'keyRecent') {
|
|
1069
|
-
return insertStep(createKeyStep(187, '最近任务'), index);
|
|
1259
|
+
return insertStep(createKeyStep(187, '最近任务'), index, branchTarget);
|
|
1070
1260
|
}
|
|
1071
1261
|
if (action === 'keyPower') {
|
|
1072
|
-
return insertStep(createKeyStep(26, '电源键'), index);
|
|
1262
|
+
return insertStep(createKeyStep(26, '电源键'), index, branchTarget);
|
|
1073
1263
|
}
|
|
1074
1264
|
if (action === 'waitActivity') {
|
|
1075
1265
|
const input = await ElMessageBox.prompt('请输入目标 Activity', '等待 Activity', {
|
|
@@ -1077,11 +1267,25 @@ async function addAction(
|
|
|
1077
1267
|
confirmButtonText: '添加',
|
|
1078
1268
|
cancelButtonText: '取消',
|
|
1079
1269
|
}).catch(() => null);
|
|
1080
|
-
if (input?.value) return insertStep(createWaitActivityStep(input.value), index);
|
|
1270
|
+
if (input?.value) return insertStep(createWaitActivityStep(input.value), index, branchTarget);
|
|
1081
1271
|
return;
|
|
1082
1272
|
}
|
|
1083
1273
|
if (action === 'swipe') {
|
|
1084
|
-
return addSwipeStep(index);
|
|
1274
|
+
return addSwipeStep(index, branchTarget);
|
|
1275
|
+
}
|
|
1276
|
+
if (action === 'visualChange' || action === 'visualChangeStart') {
|
|
1277
|
+
openVisualChangeDialog(index, branchTarget);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
if (action === 'visualChangeEnd') {
|
|
1281
|
+
const startStep = findOpenVisualChangeStart(steps.value, index, branchTarget);
|
|
1282
|
+
if (!startStep) {
|
|
1283
|
+
ElMessage.warning('当前分支没有可对应的检测画面变化开始节点');
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const inserted = insertStep(createVisualChangeEndStep(startStep), index, branchTarget);
|
|
1287
|
+
ElMessage.success(`已添加 ${inserted.label}`);
|
|
1288
|
+
return inserted;
|
|
1085
1289
|
}
|
|
1086
1290
|
if (action === 'pinch') {
|
|
1087
1291
|
const input = await ElMessageBox.prompt('请输入缩放方向:放大 / 缩小', '添加双指缩放', {
|
|
@@ -1103,16 +1307,20 @@ async function addAction(
|
|
|
1103
1307
|
centerY: Math.round((props.deviceHeight || 1920) / 2),
|
|
1104
1308
|
percent: 0.5,
|
|
1105
1309
|
},
|
|
1106
|
-
}, index);
|
|
1310
|
+
}, index, branchTarget);
|
|
1311
|
+
}
|
|
1312
|
+
if (action === 'noop') {
|
|
1313
|
+
return insertStep(createNoopStep(), index, branchTarget);
|
|
1107
1314
|
}
|
|
1108
1315
|
if (action === 'launchApp') {
|
|
1109
1316
|
const clearStepIndex = steps.value.findIndex((step) => step.type === 'clearAppData');
|
|
1317
|
+
const insertIndex = previousStep?.type === 'clearAppData' ? index : clearStepIndex >= 0 ? clearStepIndex : index;
|
|
1110
1318
|
return insertStep({
|
|
1111
1319
|
id: `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1112
1320
|
type: 'launchApp',
|
|
1113
1321
|
label: `启动 APP ${form.appPackage}`,
|
|
1114
1322
|
value: form.appPackage,
|
|
1115
|
-
},
|
|
1323
|
+
}, insertIndex);
|
|
1116
1324
|
}
|
|
1117
1325
|
if (action === 'clearAppData') {
|
|
1118
1326
|
return insertStep({
|
|
@@ -1126,12 +1334,12 @@ async function addAction(
|
|
|
1126
1334
|
if (!node) return;
|
|
1127
1335
|
if (action === 'popupCondition') {
|
|
1128
1336
|
const step = createNodeActionStep('assertExists', '判断存在', node, { timeoutMs: 2000 });
|
|
1129
|
-
const inserted = insertStep({ ...step, flow: { nodeKind: 'condition' } }, index);
|
|
1337
|
+
const inserted = insertStep({ ...step, flow: { nodeKind: 'condition' } }, index, branchTarget);
|
|
1130
1338
|
ElMessage.success('已添加判断节点,请在节点面板配置是/否分支');
|
|
1131
1339
|
return inserted;
|
|
1132
1340
|
}
|
|
1133
1341
|
if (action === 'tapIfExists') {
|
|
1134
|
-
return insertStep(createNodeActionStep('tapIfExists', '存在则点击', node, { timeoutMs: 2000 }), index);
|
|
1342
|
+
return insertStep(createNodeActionStep('tapIfExists', '存在则点击', node, { timeoutMs: 2000 }), index, branchTarget);
|
|
1135
1343
|
}
|
|
1136
1344
|
if (action === 'inputIfExists') {
|
|
1137
1345
|
const input = await ElMessageBox.prompt('', '存在则输入', {
|
|
@@ -1141,16 +1349,20 @@ async function addAction(
|
|
|
1141
1349
|
center: true,
|
|
1142
1350
|
}).catch(() => null);
|
|
1143
1351
|
if (!input) return;
|
|
1144
|
-
return insertStep(
|
|
1352
|
+
return insertStep(
|
|
1353
|
+
createNodeActionStep('inputIfExists', '存在则输入', node, { timeoutMs: 2000, value: input.value }),
|
|
1354
|
+
index,
|
|
1355
|
+
branchTarget,
|
|
1356
|
+
);
|
|
1145
1357
|
}
|
|
1146
1358
|
if (action === 'clearIfExists') {
|
|
1147
|
-
return insertStep(createNodeActionStep('clearIfExists', '存在则清空', node, { timeoutMs: 2000 }), index);
|
|
1359
|
+
return insertStep(createNodeActionStep('clearIfExists', '存在则清空', node, { timeoutMs: 2000 }), index, branchTarget);
|
|
1148
1360
|
}
|
|
1149
1361
|
if (action === 'backIfExists') {
|
|
1150
|
-
return insertStep(createNodeActionStep('backIfExists', '存在则返回', node, { timeoutMs: 2000 }), index);
|
|
1362
|
+
return insertStep(createNodeActionStep('backIfExists', '存在则返回', node, { timeoutMs: 2000 }), index, branchTarget);
|
|
1151
1363
|
}
|
|
1152
1364
|
if (action === 'clearInput') {
|
|
1153
|
-
return insertStep(createNodeActionStep('clearInput', '清空输入', node), index);
|
|
1365
|
+
return insertStep(createNodeActionStep('clearInput', '清空输入', node), index, branchTarget);
|
|
1154
1366
|
}
|
|
1155
1367
|
if (action === 'coordinateTap') {
|
|
1156
1368
|
if (!node.bounds) {
|
|
@@ -1162,7 +1374,7 @@ async function addAction(
|
|
|
1162
1374
|
type: 'coordinateTap',
|
|
1163
1375
|
label: `点击坐标 ${node.bounds.centerX},${node.bounds.centerY}`,
|
|
1164
1376
|
fallback: { strategy: 'bounds', centerX: node.bounds.centerX, centerY: node.bounds.centerY },
|
|
1165
|
-
}, index);
|
|
1377
|
+
}, index, branchTarget);
|
|
1166
1378
|
}
|
|
1167
1379
|
if (action === 'longPress') {
|
|
1168
1380
|
if (!node.bounds) {
|
|
@@ -1184,10 +1396,10 @@ async function addAction(
|
|
|
1184
1396
|
label: `长按 ${node.label}`,
|
|
1185
1397
|
fallback: { strategy: 'bounds', centerX: node.bounds.centerX, centerY: node.bounds.centerY },
|
|
1186
1398
|
timeoutMs: duration,
|
|
1187
|
-
}, index);
|
|
1399
|
+
}, index, branchTarget);
|
|
1188
1400
|
}
|
|
1189
1401
|
if (action === 'waitDisappear') {
|
|
1190
|
-
return insertStep(createNodeActionStep('waitDisappear', '等待元素消失', node, { timeoutMs: 10000 }), index);
|
|
1402
|
+
return insertStep(createNodeActionStep('waitDisappear', '等待元素消失', node, { timeoutMs: 10000 }), index, branchTarget);
|
|
1191
1403
|
}
|
|
1192
1404
|
if (action === 'assertText') {
|
|
1193
1405
|
const input = await ElMessageBox.prompt('请输入要断言的文本', '断言文本', {
|
|
@@ -1195,7 +1407,7 @@ async function addAction(
|
|
|
1195
1407
|
confirmButtonText: '添加',
|
|
1196
1408
|
cancelButtonText: '取消',
|
|
1197
1409
|
}).catch(() => null);
|
|
1198
|
-
if (input?.value) return insertStep(createNodeActionStep('assertText', '断言文本', node, { value: input.value }), index);
|
|
1410
|
+
if (input?.value) return insertStep(createNodeActionStep('assertText', '断言文本', node, { value: input.value }), index, branchTarget);
|
|
1199
1411
|
}
|
|
1200
1412
|
}
|
|
1201
1413
|
|
|
@@ -1215,28 +1427,18 @@ async function addBranchAction(index: number, branch: BranchName, action: Record
|
|
|
1215
1427
|
const nextTargetId = insertAtBranchEntry
|
|
1216
1428
|
? nextBranchStepAfter(condition.id, branch, index)?.id || (!targetIsBranchStep ? currentTargetId : undefined)
|
|
1217
1429
|
: undefined;
|
|
1218
|
-
const
|
|
1430
|
+
const branchTarget: BranchTarget = {
|
|
1219
1431
|
stepId: condition.id,
|
|
1220
1432
|
branch,
|
|
1221
1433
|
updateTarget: insertAtBranchEntry || !currentTargetId || !targetIsBranchStep,
|
|
1222
1434
|
entryTargetId: insertAtBranchEntry ? undefined : existingBranchSteps[0]?.id,
|
|
1223
1435
|
nextTargetId,
|
|
1224
|
-
}
|
|
1436
|
+
};
|
|
1437
|
+
const inserted = await addAction(action, insertIndex, branchTarget);
|
|
1225
1438
|
if (!inserted) return;
|
|
1226
|
-
|
|
1227
|
-
if (
|
|
1228
|
-
|
|
1229
|
-
step.id === inserted.id
|
|
1230
|
-
? {
|
|
1231
|
-
...step,
|
|
1232
|
-
flow: { ...(step.flow || {}), successTargetId: nextTargetId },
|
|
1233
|
-
}
|
|
1234
|
-
: step
|
|
1235
|
-
));
|
|
1236
|
-
}
|
|
1237
|
-
if (insertAtBranchEntry || !currentTargetId || !targetIsBranchStep) {
|
|
1238
|
-
updateBranchTarget(condition.id, branch, insertAtBranchEntry ? inserted.id : existingBranchSteps[0]?.id || inserted.id);
|
|
1239
|
-
}
|
|
1439
|
+
const currentInserted = steps.value.find((step) => step.id === inserted.id);
|
|
1440
|
+
if (currentInserted?.flow?.parentConditionId === condition.id && currentInserted.flow.parentBranch === branch) return;
|
|
1441
|
+
applyBranchTargetToInsertedStep(inserted, branchTarget);
|
|
1240
1442
|
}
|
|
1241
1443
|
|
|
1242
1444
|
async function saveOnActivityChange() {
|
|
@@ -1270,41 +1472,42 @@ async function saveOnActivityChange() {
|
|
|
1270
1472
|
|
|
1271
1473
|
function removeStep(index: number) {
|
|
1272
1474
|
if (recordingBusy.value) return;
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
if (flow.parentConditionId === removed.id) {
|
|
1296
|
-
delete flow.parentConditionId;
|
|
1297
|
-
delete flow.parentBranch;
|
|
1298
|
-
changed = true;
|
|
1299
|
-
}
|
|
1300
|
-
return changed ? { ...step, flow } : step;
|
|
1301
|
-
});
|
|
1475
|
+
steps.value = removeFlowStep(steps.value, index);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function syncVisualChangePairConfig(
|
|
1479
|
+
candidate: AppiumRecordedStep,
|
|
1480
|
+
sourceConfig: NonNullable<AppiumRecordedStep['visualChange']>,
|
|
1481
|
+
) {
|
|
1482
|
+
if (candidate.visualChange?.role !== 'end') return candidate;
|
|
1483
|
+
if (!sourceConfig.pairId || candidate.visualChange.pairId !== sourceConfig.pairId) return candidate;
|
|
1484
|
+
return {
|
|
1485
|
+
...candidate,
|
|
1486
|
+
visualChange: {
|
|
1487
|
+
...candidate.visualChange,
|
|
1488
|
+
mode: sourceConfig.mode,
|
|
1489
|
+
region: sourceConfig.region,
|
|
1490
|
+
pairLabel: sourceConfig.pairLabel,
|
|
1491
|
+
durationMs: sourceConfig.durationMs,
|
|
1492
|
+
intervalMs: sourceConfig.intervalMs,
|
|
1493
|
+
changeRatioThreshold: sourceConfig.changeRatioThreshold,
|
|
1494
|
+
pixelmatchThreshold: sourceConfig.pixelmatchThreshold,
|
|
1495
|
+
},
|
|
1496
|
+
};
|
|
1302
1497
|
}
|
|
1303
1498
|
|
|
1304
1499
|
function updateStep(index: number, step: AppiumRecordedStep) {
|
|
1305
1500
|
if (recordingLocked.value) return;
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1501
|
+
const visualChange = step.visualChange?.role === 'start'
|
|
1502
|
+
? normalizeVisualChangeConfig(step.visualChange)
|
|
1503
|
+
: undefined;
|
|
1504
|
+
const nextSteps = steps.value.map((candidate, stepIndex) => (
|
|
1505
|
+
stepIndex === index
|
|
1506
|
+
? step
|
|
1507
|
+
: visualChange
|
|
1508
|
+
? syncVisualChangePairConfig(candidate, visualChange)
|
|
1509
|
+
: candidate
|
|
1510
|
+
));
|
|
1308
1511
|
steps.value = nextSteps;
|
|
1309
1512
|
}
|
|
1310
1513
|
|
|
@@ -1339,7 +1542,7 @@ function loadScript(script: AppiumRecordedScript) {
|
|
|
1339
1542
|
if (script.deviceId && script.deviceId !== selectedDeviceId.value) {
|
|
1340
1543
|
void switchDevice(script.deviceId);
|
|
1341
1544
|
}
|
|
1342
|
-
steps.value = (script.steps || []).map(normalizeLegacyFlowScope);
|
|
1545
|
+
steps.value = normalizeLegacyNestedConditionBranches((script.steps || []).map(normalizeLegacyFlowScope));
|
|
1343
1546
|
markDraftSaved();
|
|
1344
1547
|
}
|
|
1345
1548
|
|
|
@@ -1367,6 +1570,91 @@ function formatScriptTime(value: string) {
|
|
|
1367
1570
|
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString();
|
|
1368
1571
|
}
|
|
1369
1572
|
|
|
1573
|
+
function cloneScriptSteps(stepList: AppiumRecordedStep[]) {
|
|
1574
|
+
return JSON.parse(JSON.stringify(stepList || [])) as AppiumRecordedStep[];
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function isScriptNameTaken(name: string, exceptId = '') {
|
|
1578
|
+
return scripts.value.some((script) => script.id !== exceptId && script.name === name);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function createDuplicateScriptName(sourceName: string) {
|
|
1582
|
+
const baseName = `${sourceName} Copy`;
|
|
1583
|
+
if (!isScriptNameTaken(baseName)) return baseName;
|
|
1584
|
+
|
|
1585
|
+
let suffix = 2;
|
|
1586
|
+
let nextName = `${baseName} ${suffix}`;
|
|
1587
|
+
while (isScriptNameTaken(nextName)) {
|
|
1588
|
+
suffix += 1;
|
|
1589
|
+
nextName = `${baseName} ${suffix}`;
|
|
1590
|
+
}
|
|
1591
|
+
return nextName;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
async function duplicateScript(script: AppiumRecordedScript) {
|
|
1595
|
+
if (duplicatingScriptId.value) return;
|
|
1596
|
+
duplicatingScriptId.value = script.id;
|
|
1597
|
+
try {
|
|
1598
|
+
const payload = await saveAppiumScript({
|
|
1599
|
+
name: createDuplicateScriptName(script.name),
|
|
1600
|
+
appPackage: script.appPackage,
|
|
1601
|
+
appActivity: script.appActivity,
|
|
1602
|
+
deviceId: script.deviceId,
|
|
1603
|
+
steps: cloneScriptSteps(script.steps),
|
|
1604
|
+
});
|
|
1605
|
+
await loadScripts();
|
|
1606
|
+
ElMessage.success(`已复制为「${payload.script.name}」`);
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
ElMessage.error(error instanceof Error ? error.message : '复制脚本失败');
|
|
1609
|
+
} finally {
|
|
1610
|
+
duplicatingScriptId.value = '';
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
async function renameScript(script: AppiumRecordedScript) {
|
|
1615
|
+
if (renamingScriptId.value) return;
|
|
1616
|
+
const input = await ElMessageBox.prompt('请输入新的脚本名称', '修改脚本名称', {
|
|
1617
|
+
inputValue: script.name,
|
|
1618
|
+
confirmButtonText: '保存',
|
|
1619
|
+
cancelButtonText: '取消',
|
|
1620
|
+
center: true,
|
|
1621
|
+
inputValidator(value) {
|
|
1622
|
+
const nextName = String(value || '').trim();
|
|
1623
|
+
if (!nextName) return '脚本名称不能为空';
|
|
1624
|
+
if (isScriptNameTaken(nextName, script.id)) return '脚本名称已存在';
|
|
1625
|
+
return true;
|
|
1626
|
+
},
|
|
1627
|
+
}).catch(() => null);
|
|
1628
|
+
const nextName = input ? String(input.value || '').trim() : '';
|
|
1629
|
+
if (!nextName || nextName === script.name) return;
|
|
1630
|
+
|
|
1631
|
+
const wasCurrentScript = selectedScriptId.value === script.id;
|
|
1632
|
+
const hadUnsavedChanges = hasUnsavedChanges.value;
|
|
1633
|
+
renamingScriptId.value = script.id;
|
|
1634
|
+
try {
|
|
1635
|
+
const payload = await saveAppiumScript({
|
|
1636
|
+
id: script.id,
|
|
1637
|
+
name: nextName,
|
|
1638
|
+
appPackage: script.appPackage,
|
|
1639
|
+
appActivity: script.appActivity,
|
|
1640
|
+
deviceId: script.deviceId,
|
|
1641
|
+
steps: cloneScriptSteps(script.steps),
|
|
1642
|
+
});
|
|
1643
|
+
await loadScripts();
|
|
1644
|
+
if (wasCurrentScript) {
|
|
1645
|
+
form.name = payload.script.name;
|
|
1646
|
+
if (!hadUnsavedChanges) {
|
|
1647
|
+
markDraftSaved();
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
ElMessage.success('脚本名称已修改');
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
ElMessage.error(error instanceof Error ? error.message : '修改脚本名称失败');
|
|
1653
|
+
} finally {
|
|
1654
|
+
renamingScriptId.value = '';
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1370
1658
|
async function removeScript(script: AppiumRecordedScript) {
|
|
1371
1659
|
try {
|
|
1372
1660
|
await ElMessageBox.confirm(`确定删除脚本「${script.name}」吗?`, '删除脚本', {
|
|
@@ -1428,7 +1716,7 @@ async function importScriptFile(event: Event) {
|
|
|
1428
1716
|
}
|
|
1429
1717
|
|
|
1430
1718
|
async function saveScript() {
|
|
1431
|
-
if (!ensureAppPackageSelected()) return;
|
|
1719
|
+
if (!ensureAppPackageSelected()) return false;
|
|
1432
1720
|
saving.value = true;
|
|
1433
1721
|
try {
|
|
1434
1722
|
const payload = await saveAppiumScript({
|
|
@@ -1443,8 +1731,10 @@ async function saveScript() {
|
|
|
1443
1731
|
selectedScriptId.value = payload.script.id;
|
|
1444
1732
|
markDraftSaved();
|
|
1445
1733
|
ElMessage.success('Appium 脚本已保存');
|
|
1734
|
+
return true;
|
|
1446
1735
|
} catch (error) {
|
|
1447
1736
|
ElMessage.error(error instanceof Error ? error.message : '保存失败');
|
|
1737
|
+
return false;
|
|
1448
1738
|
} finally {
|
|
1449
1739
|
saving.value = false;
|
|
1450
1740
|
}
|
|
@@ -1455,6 +1745,10 @@ async function replayScript() {
|
|
|
1455
1745
|
ElMessage.warning('请选择已保存脚本');
|
|
1456
1746
|
return;
|
|
1457
1747
|
}
|
|
1748
|
+
if (hasUnsavedChanges.value) {
|
|
1749
|
+
const saved = await saveScript();
|
|
1750
|
+
if (!saved || !selectedScript.value) return;
|
|
1751
|
+
}
|
|
1458
1752
|
replaying.value = true;
|
|
1459
1753
|
stoppingReplay.value = false;
|
|
1460
1754
|
activeReplayDeviceId.value = selectedDeviceId.value;
|
|
@@ -1470,6 +1764,8 @@ async function replayScript() {
|
|
|
1470
1764
|
replayOutput.value = result.output || '';
|
|
1471
1765
|
if (result.stopped) {
|
|
1472
1766
|
ElMessage.info('回放已终止,报告、日志和截图回放已生成');
|
|
1767
|
+
} else if (result.softFailureCount) {
|
|
1768
|
+
ElMessage.warning(`回放完成,但存在 ${result.softFailureCount} 个视觉变化未达预期节点`);
|
|
1473
1769
|
} else if (result.success) {
|
|
1474
1770
|
ElMessage.success('回放完成');
|
|
1475
1771
|
} else {
|
|
@@ -1581,12 +1877,16 @@ watch(
|
|
|
1581
1877
|
:actions="deviceActions"
|
|
1582
1878
|
:overlay-bounds="overlayBounds"
|
|
1583
1879
|
:selected-bounds="selectedBounds"
|
|
1880
|
+
:selected-region="visualChangeSelectedRegion"
|
|
1881
|
+
:region-selection="visualChangeRegionSelectionEnabled"
|
|
1882
|
+
:region-draw-mode="visualChangePicking"
|
|
1584
1883
|
:device-width="deviceWidth"
|
|
1585
1884
|
:device-height="deviceHeight"
|
|
1586
1885
|
@switch-device="switchDevice"
|
|
1587
1886
|
@trigger-key="triggerDeviceKey"
|
|
1588
1887
|
@refresh-preview="refreshDevicePreview"
|
|
1589
1888
|
@tap="selectNodeFromPoint"
|
|
1889
|
+
@region-select="applyVisualChangeRegion"
|
|
1590
1890
|
@swipe="swipePreview"
|
|
1591
1891
|
/>
|
|
1592
1892
|
|
|
@@ -1715,18 +2015,33 @@ watch(
|
|
|
1715
2015
|
</div>
|
|
1716
2016
|
<div class="appium-script-list__actions">
|
|
1717
2017
|
<el-button size="small" @click.stop="loadScript(script)">加载</el-button>
|
|
2018
|
+
<el-button
|
|
2019
|
+
size="small"
|
|
2020
|
+
:icon="Edit"
|
|
2021
|
+
:loading="renamingScriptId === script.id"
|
|
2022
|
+
title="修改脚本名称"
|
|
2023
|
+
@click.stop="renameScript(script)"
|
|
2024
|
+
/>
|
|
2025
|
+
<el-button
|
|
2026
|
+
size="small"
|
|
2027
|
+
:icon="CopyDocument"
|
|
2028
|
+
:loading="duplicatingScriptId === script.id"
|
|
2029
|
+
title="复制脚本"
|
|
2030
|
+
@click.stop="duplicateScript(script)"
|
|
2031
|
+
/>
|
|
1718
2032
|
<el-button
|
|
1719
2033
|
size="small"
|
|
1720
2034
|
:icon="Download"
|
|
1721
2035
|
title="下载脚本"
|
|
1722
|
-
@click="downloadScript(script)"
|
|
2036
|
+
@click.stop="downloadScript(script)"
|
|
1723
2037
|
/>
|
|
1724
2038
|
<el-button
|
|
1725
2039
|
size="small"
|
|
1726
2040
|
type="danger"
|
|
1727
2041
|
:icon="Delete"
|
|
1728
2042
|
:loading="deletingScriptId === script.id"
|
|
1729
|
-
|
|
2043
|
+
title="删除脚本"
|
|
2044
|
+
@click.stop="removeScript(script)"
|
|
1730
2045
|
/>
|
|
1731
2046
|
</div>
|
|
1732
2047
|
</article>
|
|
@@ -1737,6 +2052,17 @@ watch(
|
|
|
1737
2052
|
</el-card>
|
|
1738
2053
|
</div>
|
|
1739
2054
|
|
|
2055
|
+
<VisualChangeDialog
|
|
2056
|
+
:model-value="visualChangeDialogVisible"
|
|
2057
|
+
:config="visualChangeConfig"
|
|
2058
|
+
:has-selected-element="Boolean(selectedNode?.bounds)"
|
|
2059
|
+
:picking-region="visualChangePicking"
|
|
2060
|
+
@update:model-value="$event ? (visualChangeDialogVisible = true) : closeVisualChangeDialog()"
|
|
2061
|
+
@update:config="updateVisualChangeConfig"
|
|
2062
|
+
@pick-region="pickVisualChangeRegion"
|
|
2063
|
+
@confirm="confirmVisualChangeStep"
|
|
2064
|
+
/>
|
|
2065
|
+
|
|
1740
2066
|
<el-dialog
|
|
1741
2067
|
:model-value="Boolean(pendingNavigation)"
|
|
1742
2068
|
title="检测到 Activity 跳转"
|