android-midscene-automation 0.1.26 → 0.1.28
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 +18 -0
- package/FAQ.md +14 -0
- package/README.md +39 -3
- package/bin/android-midscene-automation.js +71 -1
- package/package.json +7 -2
- package/remote-agent/index.ts +2 -11
- package/server/appium-recorder/ai-recognition.ts +67 -0
- package/server/appium-recorder/appium-runner.ts +335 -52
- package/server/appium-recorder/report.ts +297 -16
- package/server/appium-recorder/repository.ts +31 -4
- package/server/appium-recorder/routes.ts +31 -32
- package/server/appium-recorder/screenshot.ts +21 -0
- package/server/appium-recorder/script-export.ts +67 -0
- package/server/appium-recorder/tree-dump.ts +22 -0
- package/server/appium-recorder/visual-change.ts +184 -0
- package/server/config.ts +10 -0
- package/server/http-api.ts +4 -5
- package/src/App.vue +14 -7
- package/src/api.ts +1 -1
- package/src/appium-recorder/AppiumPage.vue +658 -162
- package/src/appium-recorder/ai-recognition.ts +16 -0
- package/src/appium-recorder/api.ts +20 -3
- package/src/appium-recorder/components/AiRecognitionTestDialog.vue +71 -0
- package/src/appium-recorder/components/FlowActionMenu.vue +206 -0
- package/src/appium-recorder/components/FlowCanvas.vue +16 -1
- package/src/appium-recorder/components/FlowNodeCard.vue +23 -37
- package/src/appium-recorder/components/FlowStepEditor.vue +152 -25
- package/src/appium-recorder/components/LongPressSettings.vue +58 -0
- package/src/appium-recorder/components/NestedConditionBranches.vue +55 -35
- package/src/appium-recorder/components/NewScriptDialog.vue +27 -0
- package/src/appium-recorder/components/NodeDetail.vue +2 -0
- package/src/appium-recorder/components/RecordedSteps.vue +24 -4
- package/src/appium-recorder/components/StageLogSettings.vue +17 -0
- package/src/appium-recorder/components/TextClickSettings.vue +19 -0
- 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 +147 -64
- package/src/appium-recorder/flow-labels.ts +35 -6
- package/src/appium-recorder/flow-normalize.ts +76 -0
- package/src/appium-recorder/flow-remove.ts +69 -0
- package/src/appium-recorder/long-press.ts +27 -0
- package/src/appium-recorder/native-control-state.ts +26 -0
- package/src/appium-recorder/stage-log.ts +21 -0
- package/src/appium-recorder/text-click.ts +9 -0
- package/src/appium-recorder/tree.ts +3 -0
- package/src/appium-recorder/types.ts +34 -2
- package/src/appium-recorder/visual-change-pairs.ts +147 -0
- package/src/appium-recorder/visual-change.ts +75 -0
- package/src/components/config/AppiumConfigPanel.vue +48 -0
- package/src/components/config/BaseConfigPanel.vue +90 -0
- package/src/components/config/MidsceneConfigPanel.vue +204 -0
- package/src/components/device/DevicePreviewPanel.vue +148 -4
- package/src/pages/ConfigPage.vue +107 -265
- package/src/style.css +43 -4
- package/src/types.ts +2 -0
- package/src/ui-refresh.css +5 -0
|
@@ -1,13 +1,13 @@
|
|
|
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, Plus, 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 {
|
|
8
|
-
appiumScriptDownloadUrl,
|
|
9
8
|
clearAppiumDeviceAppData,
|
|
10
9
|
deleteAppiumScript,
|
|
10
|
+
downloadAppiumScript,
|
|
11
11
|
getAppiumScripts,
|
|
12
12
|
getAppiumTree,
|
|
13
13
|
importAppiumScript,
|
|
@@ -20,16 +20,45 @@ import {
|
|
|
20
20
|
import ComponentTree from './components/ComponentTree.vue';
|
|
21
21
|
import FlowCanvas from './components/FlowCanvas.vue';
|
|
22
22
|
import NodeDetail from './components/NodeDetail.vue';
|
|
23
|
+
import LongPressSettings from './components/LongPressSettings.vue';
|
|
24
|
+
import StageLogSettings from './components/StageLogSettings.vue';
|
|
25
|
+
import TextClickSettings from './components/TextClickSettings.vue';
|
|
26
|
+
import { textClickSelector } from './text-click';
|
|
27
|
+
import { DEFAULT_LOG_PREFIX, validateStageLog } from './stage-log';
|
|
23
28
|
import RecordedSteps from './components/RecordedSteps.vue';
|
|
29
|
+
import NewScriptDialog from './components/NewScriptDialog.vue';
|
|
30
|
+
import VisualChangeDialog from './components/VisualChangeDialog.vue';
|
|
31
|
+
import AiRecognitionTestDialog from './components/AiRecognitionTestDialog.vue';
|
|
32
|
+
import { validateAiRecognitionPrompt } from './ai-recognition';
|
|
24
33
|
import {
|
|
25
34
|
createFlowClipboard,
|
|
26
35
|
pasteFlowClipboard,
|
|
27
36
|
type FlowClipboard,
|
|
28
37
|
} from './flow-copy';
|
|
29
38
|
import { labelFlowStep } from './flow-labels';
|
|
39
|
+
import { validateLongPress } from './long-press';
|
|
40
|
+
import { nativeControlName, nativeControlType } from './native-control-state';
|
|
41
|
+
import { normalizeLegacyNestedConditionBranches } from './flow-normalize';
|
|
42
|
+
import { removeFlowStep } from './flow-remove';
|
|
30
43
|
import type { FlowActionGroup, InsertAction } from './flow-graph';
|
|
31
44
|
import { findSmallestNodeAtPoint, flattenNodes, parseWindowHierarchy } from './tree';
|
|
32
|
-
import
|
|
45
|
+
import {
|
|
46
|
+
boundsToVisualRegion,
|
|
47
|
+
createStepId,
|
|
48
|
+
normalizeVisualChangeConfig,
|
|
49
|
+
visualRegionToBounds,
|
|
50
|
+
} from './visual-change';
|
|
51
|
+
import {
|
|
52
|
+
findOpenVisualChangeStart,
|
|
53
|
+
nextVisualChangePairLabel,
|
|
54
|
+
} from './visual-change-pairs';
|
|
55
|
+
import type {
|
|
56
|
+
AppiumNode,
|
|
57
|
+
AppiumRecordedScript,
|
|
58
|
+
AppiumRecordedStep,
|
|
59
|
+
AppiumVisualChangeConfig,
|
|
60
|
+
AppiumVisualChangeRegion,
|
|
61
|
+
} from './types';
|
|
33
62
|
|
|
34
63
|
type NodeStepType = 'tap' | 'input' | 'assertExists' | 'waitFor';
|
|
35
64
|
type BranchName = 'yes' | 'no';
|
|
@@ -43,29 +72,7 @@ type BranchTarget = {
|
|
|
43
72
|
entryTargetId?: string;
|
|
44
73
|
nextTargetId?: string;
|
|
45
74
|
};
|
|
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';
|
|
75
|
+
type RecorderAction = InsertAction;
|
|
69
76
|
|
|
70
77
|
const WORKBENCH_TAB_STORAGE_KEY = 'android-midscene-automation:appium-workbench-tab';
|
|
71
78
|
const AUTO_TREE_REFRESH_INTERVAL_MS = 1800;
|
|
@@ -81,6 +88,10 @@ const insertableRecorderActions: RecorderAction[] = [
|
|
|
81
88
|
'clearIfExists',
|
|
82
89
|
'backIfExists',
|
|
83
90
|
'popupCondition',
|
|
91
|
+
'checkboxState',
|
|
92
|
+
'radioButtonState',
|
|
93
|
+
'aiRecognition',
|
|
94
|
+
'textClick',
|
|
84
95
|
'runScript',
|
|
85
96
|
'keyBack',
|
|
86
97
|
'keyHome',
|
|
@@ -96,11 +107,16 @@ const insertableRecorderActions: RecorderAction[] = [
|
|
|
96
107
|
'assertText',
|
|
97
108
|
'longPress',
|
|
98
109
|
'pinch',
|
|
110
|
+
'noop',
|
|
111
|
+
'log',
|
|
112
|
+
'visualChangeStart',
|
|
113
|
+
'visualChangeEnd',
|
|
99
114
|
];
|
|
100
115
|
const readonlyFlowActionGroups: FlowActionGroup[] = [];
|
|
101
116
|
const readonlyFlowSelectedIndexes: number[] = [];
|
|
102
117
|
|
|
103
118
|
const props = defineProps<{
|
|
119
|
+
aiRecognitionModelConfigured?: boolean;
|
|
104
120
|
active: boolean;
|
|
105
121
|
appPresets: AppPreset[];
|
|
106
122
|
deviceActions: readonly DeviceAction[];
|
|
@@ -129,9 +145,12 @@ const tree = shallowRef<AppiumNode | null>(null);
|
|
|
129
145
|
const selectedNode = shallowRef<AppiumNode | null>(null);
|
|
130
146
|
const scripts = shallowRef<AppiumRecordedScript[]>([]);
|
|
131
147
|
const selectedScriptId = shallowRef('');
|
|
148
|
+
const newScriptDialogVisible = shallowRef(false);
|
|
149
|
+
const newScriptRevision = shallowRef(0);
|
|
132
150
|
const storedWorkbenchTab = window.localStorage.getItem(WORKBENCH_TAB_STORAGE_KEY);
|
|
133
151
|
const activeWorkbenchTab = shallowRef<'recording' | 'scripts'>(storedWorkbenchTab === 'scripts' ? 'scripts' : 'recording');
|
|
134
152
|
const steps = shallowRef<AppiumRecordedStep[]>([]);
|
|
153
|
+
const aiRecognitionTestStep = shallowRef<AppiumRecordedStep | null>(null);
|
|
135
154
|
const flowClipboard = shallowRef<FlowClipboard | null>(null);
|
|
136
155
|
const rawXml = shallowRef('');
|
|
137
156
|
const currentActivity = shallowRef('');
|
|
@@ -145,11 +164,19 @@ const linkedScriptExpectedActivity = shallowRef('');
|
|
|
145
164
|
const linkedScriptPreviewVisible = shallowRef(false);
|
|
146
165
|
const linkedScriptPreviewScriptId = shallowRef('');
|
|
147
166
|
const linkedScriptPreviewResetToken = shallowRef(0);
|
|
167
|
+
const visualChangeDialogVisible = shallowRef(false);
|
|
168
|
+
const visualChangePicking = shallowRef(false);
|
|
169
|
+
const visualChangeInsertIndex = shallowRef<number | undefined>();
|
|
170
|
+
const visualChangeBranchTarget = shallowRef<BranchTarget | null>(null);
|
|
171
|
+
const visualChangeConfig = shallowRef<AppiumVisualChangeConfig>(normalizeVisualChangeConfig());
|
|
148
172
|
const loadingTree = shallowRef(false);
|
|
149
173
|
const saving = shallowRef(false);
|
|
150
174
|
const importingScript = shallowRef(false);
|
|
151
175
|
const scriptImportInput = shallowRef<HTMLInputElement | null>(null);
|
|
152
176
|
const deletingScriptId = shallowRef('');
|
|
177
|
+
const duplicatingScriptId = shallowRef('');
|
|
178
|
+
const renamingScriptId = shallowRef('');
|
|
179
|
+
const downloadingScriptId = shallowRef('');
|
|
153
180
|
const replaying = shallowRef(false);
|
|
154
181
|
const stoppingReplay = shallowRef(false);
|
|
155
182
|
const activeReplayDeviceId = shallowRef('');
|
|
@@ -222,6 +249,7 @@ const recordingBusy = computed(() => (
|
|
|
222
249
|
|| Boolean(pendingNavigation.value)
|
|
223
250
|
));
|
|
224
251
|
const recordingLocked = computed(() => recordingBusy.value || scriptActivityMismatch.value);
|
|
252
|
+
const newScriptDisabled = computed(() => saving.value || replaying.value || recordingBusy.value || launchingApp.value || importingScript.value);
|
|
225
253
|
const overlayBounds = computed(() => flattenNodes(tree.value).flatMap((node) => (
|
|
226
254
|
node.bounds ? [{ id: node.id, ...node.bounds }] : []
|
|
227
255
|
)));
|
|
@@ -229,16 +257,38 @@ const selectedBounds = computed(() => {
|
|
|
229
257
|
const bounds = selectedNode.value?.bounds;
|
|
230
258
|
return bounds && selectedNode.value ? { id: selectedNode.value.id, ...bounds } : undefined;
|
|
231
259
|
});
|
|
260
|
+
const visualChangeSelectedRegion = computed(() => {
|
|
261
|
+
if (!visualChangeDialogVisible.value || visualChangeConfig.value.mode !== 'region') return undefined;
|
|
262
|
+
return visualRegionToBounds('visual-change-region', visualChangeConfig.value.region);
|
|
263
|
+
});
|
|
264
|
+
const visualChangeRegionSelectionEnabled = computed(() => (
|
|
265
|
+
visualChangeDialogVisible.value && visualChangeConfig.value.mode === 'region'
|
|
266
|
+
));
|
|
232
267
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
268
|
+
function stepWithBranchTarget(step: AppiumRecordedStep, branchTarget: BranchTarget) {
|
|
269
|
+
return {
|
|
270
|
+
...step,
|
|
271
|
+
flow: {
|
|
272
|
+
...(step.flow || {}),
|
|
273
|
+
parentConditionId: branchTarget.stepId,
|
|
274
|
+
parentBranch: branchTarget.branch,
|
|
275
|
+
...(branchTarget.nextTargetId ? { successTargetId: branchTarget.nextTargetId } : {}),
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function insertStep(step: AppiumRecordedStep, index?: number, branchTarget?: BranchTarget) {
|
|
281
|
+
let nextSteps = [...steps.value];
|
|
282
|
+
let insertedStep = branchTarget ? stepWithBranchTarget(step, branchTarget) : step;
|
|
236
283
|
if (index === -1) {
|
|
237
284
|
nextSteps.unshift(insertedStep);
|
|
238
285
|
} else if (typeof index === 'number') {
|
|
239
286
|
const previousStep = nextSteps[index];
|
|
240
287
|
const previousTargetId = previousStep?.flow?.successTargetId;
|
|
241
|
-
|
|
288
|
+
const shouldRewireSuccess = previousStep
|
|
289
|
+
&& previousTargetId
|
|
290
|
+
&& !(branchTarget && previousStep.id === branchTarget.stepId);
|
|
291
|
+
if (shouldRewireSuccess) {
|
|
242
292
|
insertedStep = {
|
|
243
293
|
...insertedStep,
|
|
244
294
|
flow: {
|
|
@@ -256,7 +306,22 @@ function insertStep(step: AppiumRecordedStep, index?: number) {
|
|
|
256
306
|
}
|
|
257
307
|
nextSteps.splice(index + 1, 0, insertedStep);
|
|
258
308
|
} else {
|
|
259
|
-
nextSteps.push(
|
|
309
|
+
nextSteps.push(insertedStep);
|
|
310
|
+
}
|
|
311
|
+
if (branchTarget?.updateTarget) {
|
|
312
|
+
const targetKey = branchTarget.branch === 'yes' ? 'yesTargetId' : 'noTargetId';
|
|
313
|
+
nextSteps = nextSteps.map((item) => (
|
|
314
|
+
item.id === branchTarget.stepId
|
|
315
|
+
? {
|
|
316
|
+
...item,
|
|
317
|
+
flow: {
|
|
318
|
+
...(item.flow || {}),
|
|
319
|
+
nodeKind: 'condition',
|
|
320
|
+
[targetKey]: branchTarget.entryTargetId || insertedStep.id,
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
: item
|
|
324
|
+
));
|
|
260
325
|
}
|
|
261
326
|
steps.value = nextSteps;
|
|
262
327
|
return insertedStep;
|
|
@@ -287,6 +352,7 @@ function pasteFlowNodes(index: number, branch?: BranchName) {
|
|
|
287
352
|
});
|
|
288
353
|
steps.value = nextSteps;
|
|
289
354
|
ElMessage.success(`已粘贴 ${flowClipboardCount.value} 个节点${branch ? `到${branch === 'yes' ? '是' : '否'}分支` : ''}`);
|
|
355
|
+
flowClipboard.value = null;
|
|
290
356
|
} catch (error) {
|
|
291
357
|
ElMessage.warning(error instanceof Error ? error.message : '粘贴节点失败');
|
|
292
358
|
}
|
|
@@ -334,17 +400,38 @@ function attachStepToBranch(stepId: string, conditionId: string, branch: BranchN
|
|
|
334
400
|
));
|
|
335
401
|
}
|
|
336
402
|
|
|
403
|
+
function applyBranchTargetToInsertedStep(inserted: AppiumRecordedStep, branchTarget: BranchTarget) {
|
|
404
|
+
attachStepToBranch(inserted.id, branchTarget.stepId, branchTarget.branch);
|
|
405
|
+
if (branchTarget.nextTargetId) {
|
|
406
|
+
steps.value = steps.value.map((step) => (
|
|
407
|
+
step.id === inserted.id
|
|
408
|
+
? {
|
|
409
|
+
...step,
|
|
410
|
+
flow: { ...(step.flow || {}), successTargetId: branchTarget.nextTargetId },
|
|
411
|
+
}
|
|
412
|
+
: step
|
|
413
|
+
));
|
|
414
|
+
}
|
|
415
|
+
if (branchTarget.updateTarget) {
|
|
416
|
+
updateBranchTarget(
|
|
417
|
+
branchTarget.stepId,
|
|
418
|
+
branchTarget.branch,
|
|
419
|
+
branchTarget.entryTargetId || inserted.id,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
337
424
|
function branchContextForInsert(index: number, branch: BranchName) {
|
|
338
425
|
const anchor = steps.value[index];
|
|
339
426
|
if (!anchor) return undefined;
|
|
427
|
+
if (anchor.flow?.nodeKind === 'condition') {
|
|
428
|
+
return { condition: anchor, conditionIndex: index };
|
|
429
|
+
}
|
|
340
430
|
if (anchor.flow?.parentConditionId && anchor.flow.parentBranch === branch) {
|
|
341
431
|
const conditionIndex = steps.value.findIndex((step) => step.id === anchor.flow?.parentConditionId);
|
|
342
432
|
const condition = conditionIndex >= 0 ? steps.value[conditionIndex] : undefined;
|
|
343
433
|
return condition ? { condition, conditionIndex } : undefined;
|
|
344
434
|
}
|
|
345
|
-
if (anchor.flow?.nodeKind === 'condition') {
|
|
346
|
-
return { condition: anchor, conditionIndex: index };
|
|
347
|
-
}
|
|
348
435
|
return undefined;
|
|
349
436
|
}
|
|
350
437
|
|
|
@@ -489,6 +576,87 @@ function createRunScriptStep(script: AppiumRecordedScript): AppiumRecordedStep {
|
|
|
489
576
|
};
|
|
490
577
|
}
|
|
491
578
|
|
|
579
|
+
function createNoopStep(): AppiumRecordedStep {
|
|
580
|
+
return {
|
|
581
|
+
id: createStepId(),
|
|
582
|
+
type: 'noop',
|
|
583
|
+
label: '空节点',
|
|
584
|
+
flow: { nodeKind: 'action' },
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function defaultVisualChangeRegion(): AppiumVisualChangeRegion {
|
|
589
|
+
const width = props.deviceWidth || 1080;
|
|
590
|
+
const height = props.deviceHeight || 1920;
|
|
591
|
+
const regionWidth = Math.max(1, Math.round(width * 0.5));
|
|
592
|
+
const regionHeight = Math.max(1, Math.round(height * 0.25));
|
|
593
|
+
return {
|
|
594
|
+
x: Math.max(0, Math.round((width - regionWidth) / 2)),
|
|
595
|
+
y: Math.max(0, Math.round((height - regionHeight) / 2)),
|
|
596
|
+
width: regionWidth,
|
|
597
|
+
height: regionHeight,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function currentVisualChangeConfig() {
|
|
602
|
+
const bounds = selectedNode.value?.bounds;
|
|
603
|
+
return normalizeVisualChangeConfig({
|
|
604
|
+
mode: bounds ? 'selectedElement' : 'region',
|
|
605
|
+
region: bounds ? boundsToVisualRegion(bounds) : defaultVisualChangeRegion(),
|
|
606
|
+
} as AppiumVisualChangeConfig);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function createVisualChangeStartStep(config: AppiumVisualChangeConfig): AppiumRecordedStep {
|
|
610
|
+
const normalizedConfig = normalizeVisualChangeConfig(config);
|
|
611
|
+
const pairId = createStepId();
|
|
612
|
+
const pairLabel = nextVisualChangePairLabel(steps.value);
|
|
613
|
+
const visualChange = {
|
|
614
|
+
...normalizedConfig,
|
|
615
|
+
role: 'start' as const,
|
|
616
|
+
pairId,
|
|
617
|
+
pairLabel,
|
|
618
|
+
startStepId: '',
|
|
619
|
+
endStepId: '',
|
|
620
|
+
};
|
|
621
|
+
const node = normalizedConfig.mode === 'selectedElement' ? selectedNode.value : null;
|
|
622
|
+
if (node) {
|
|
623
|
+
const step = createNodeActionStep('visualChange', `${pairLabel}-开始节点`, node);
|
|
624
|
+
return {
|
|
625
|
+
...step,
|
|
626
|
+
label: `${pairLabel}-开始节点`,
|
|
627
|
+
visualChange,
|
|
628
|
+
flow: { ...(step.flow || {}), nodeKind: 'assertion' },
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
id: createStepId(),
|
|
633
|
+
type: 'visualChange',
|
|
634
|
+
label: `${pairLabel}-开始节点`,
|
|
635
|
+
visualChange,
|
|
636
|
+
flow: { nodeKind: 'assertion' },
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function createVisualChangeEndStep(startStep: AppiumRecordedStep): AppiumRecordedStep {
|
|
641
|
+
const config = normalizeVisualChangeConfig(startStep.visualChange);
|
|
642
|
+
const id = createStepId();
|
|
643
|
+
const pairLabel = config.pairLabel || '检测画面变化';
|
|
644
|
+
const pairId = config.pairId || startStep.id;
|
|
645
|
+
return {
|
|
646
|
+
id,
|
|
647
|
+
type: 'visualChange',
|
|
648
|
+
label: `${pairLabel}-结束节点`,
|
|
649
|
+
visualChange: {
|
|
650
|
+
...config,
|
|
651
|
+
role: 'end',
|
|
652
|
+
pairId,
|
|
653
|
+
startStepId: startStep.id,
|
|
654
|
+
endStepId: id,
|
|
655
|
+
},
|
|
656
|
+
flow: { nodeKind: 'assertion' },
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
492
660
|
function linkedScriptExitActivity(script: AppiumRecordedScript) {
|
|
493
661
|
for (let index = script.steps.length - 1; index >= 0; index -= 1) {
|
|
494
662
|
const activity = script.steps[index]?.pageAfter?.activity;
|
|
@@ -679,6 +847,11 @@ async function refreshTree() {
|
|
|
679
847
|
|
|
680
848
|
async function executeFlowStep(index: number) {
|
|
681
849
|
const step = steps.value[index];
|
|
850
|
+
if (step?.type === 'aiRecognition') {
|
|
851
|
+
if (replaying.value || recordingBusy.value) { ElMessage.warning('设备正在执行操作,请稍后测试'); return; }
|
|
852
|
+
aiRecognitionTestStep.value = { ...step };
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
682
855
|
if (!step || (step.type !== 'launchApp' && step.type !== 'clearAppData')) return;
|
|
683
856
|
if (launchingApp.value) return;
|
|
684
857
|
if (!selectedDeviceId.value) {
|
|
@@ -763,7 +936,7 @@ async function swipePreview(gesture: {
|
|
|
763
936
|
}
|
|
764
937
|
}
|
|
765
938
|
|
|
766
|
-
async function recordTapStep(index?: number) {
|
|
939
|
+
async function recordTapStep(index?: number, branchTarget?: BranchTarget) {
|
|
767
940
|
if (!selectedNode.value || recordingTap.value) return;
|
|
768
941
|
if (!ensureAppPackageSelected()) return;
|
|
769
942
|
if (!selectedDeviceId.value) {
|
|
@@ -778,7 +951,7 @@ async function recordTapStep(index?: number) {
|
|
|
778
951
|
}
|
|
779
952
|
const beforeActivity = currentActivity.value;
|
|
780
953
|
const step = createStep('tap', node);
|
|
781
|
-
insertStep(step, index);
|
|
954
|
+
const inserted = insertStep(step, index, branchTarget);
|
|
782
955
|
recordingTap.value = true;
|
|
783
956
|
try {
|
|
784
957
|
await tapAppiumDevice({ deviceId: selectedDeviceId.value, x: bounds.centerX, y: bounds.centerY });
|
|
@@ -805,12 +978,12 @@ async function recordTapStep(index?: number) {
|
|
|
805
978
|
}
|
|
806
979
|
}
|
|
807
980
|
|
|
808
|
-
async function addStep(type: NodeStepType, index?: number) {
|
|
981
|
+
async function addStep(type: NodeStepType, index?: number, branchTarget?: BranchTarget) {
|
|
809
982
|
if (recordingBusy.value) return;
|
|
810
983
|
if (!selectedNode.value) return;
|
|
811
984
|
if (!ensureAppPackageSelected()) return;
|
|
812
985
|
if (type === 'tap') {
|
|
813
|
-
return recordTapStep(index);
|
|
986
|
+
return recordTapStep(index, branchTarget);
|
|
814
987
|
}
|
|
815
988
|
const step = createStep(type, selectedNode.value);
|
|
816
989
|
if (type === 'input') {
|
|
@@ -823,10 +996,10 @@ async function addStep(type: NodeStepType, index?: number) {
|
|
|
823
996
|
if (!input) return;
|
|
824
997
|
step.value = input.value;
|
|
825
998
|
}
|
|
826
|
-
return insertStep(step, index);
|
|
999
|
+
return insertStep(step, index, branchTarget);
|
|
827
1000
|
}
|
|
828
1001
|
|
|
829
|
-
async function addDelayStep(index?: number) {
|
|
1002
|
+
async function addDelayStep(index?: number, branchTarget?: BranchTarget) {
|
|
830
1003
|
if (recordingBusy.value) return;
|
|
831
1004
|
const input = await ElMessageBox.prompt('请输入延时时间,单位毫秒', '添加延时', {
|
|
832
1005
|
inputValue: '1000',
|
|
@@ -838,7 +1011,7 @@ async function addDelayStep(index?: number) {
|
|
|
838
1011
|
if (!input) return;
|
|
839
1012
|
const timeoutMs = Number(input.value);
|
|
840
1013
|
const step = createDelayStep(timeoutMs);
|
|
841
|
-
return insertStep(step, index);
|
|
1014
|
+
return insertStep(step, index, branchTarget);
|
|
842
1015
|
}
|
|
843
1016
|
|
|
844
1017
|
function toSwipeNumber(value: unknown, fallback: number) {
|
|
@@ -884,7 +1057,7 @@ function renderSwipeForm(form: SwipeForm) {
|
|
|
884
1057
|
]);
|
|
885
1058
|
}
|
|
886
1059
|
|
|
887
|
-
async function addSwipeStep(index?: number) {
|
|
1060
|
+
async function addSwipeStep(index?: number, branchTarget?: BranchTarget) {
|
|
888
1061
|
const preset = swipePreset('up');
|
|
889
1062
|
const form = reactive<SwipeForm>({
|
|
890
1063
|
direction: preset.direction,
|
|
@@ -909,7 +1082,7 @@ async function addSwipeStep(index?: number) {
|
|
|
909
1082
|
endY: toSwipeNumber(form.endY, finalPreset.swipe.endY),
|
|
910
1083
|
duration: Math.max(80, toSwipeNumber(form.duration, finalPreset.swipe.duration)),
|
|
911
1084
|
},
|
|
912
|
-
}), index);
|
|
1085
|
+
}), index, branchTarget);
|
|
913
1086
|
}
|
|
914
1087
|
|
|
915
1088
|
function openLinkedScriptDialog(index?: number, branchTarget?: BranchTarget) {
|
|
@@ -958,30 +1131,11 @@ function addLinkedScriptStep() {
|
|
|
958
1131
|
);
|
|
959
1132
|
return;
|
|
960
1133
|
}
|
|
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
|
-
}
|
|
1134
|
+
insertStep(
|
|
1135
|
+
createRunScriptStep(script),
|
|
1136
|
+
linkedScriptInsertIndex.value,
|
|
1137
|
+
linkedScriptBranchTarget.value || undefined,
|
|
1138
|
+
);
|
|
985
1139
|
linkedScriptInsertIndex.value = undefined;
|
|
986
1140
|
linkedScriptBranchTarget.value = null;
|
|
987
1141
|
linkedScriptExpectedActivity.value = '';
|
|
@@ -1018,6 +1172,64 @@ function loadPreviewScriptForEditing() {
|
|
|
1018
1172
|
loadScript(linkedScriptPreviewScript.value);
|
|
1019
1173
|
}
|
|
1020
1174
|
|
|
1175
|
+
function openVisualChangeDialog(index?: number, branchTarget?: BranchTarget) {
|
|
1176
|
+
visualChangeInsertIndex.value = index;
|
|
1177
|
+
visualChangeBranchTarget.value = branchTarget || null;
|
|
1178
|
+
visualChangeConfig.value = currentVisualChangeConfig();
|
|
1179
|
+
visualChangePicking.value = false;
|
|
1180
|
+
visualChangeDialogVisible.value = true;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function closeVisualChangeDialog() {
|
|
1184
|
+
visualChangeDialogVisible.value = false;
|
|
1185
|
+
visualChangePicking.value = false;
|
|
1186
|
+
visualChangeInsertIndex.value = undefined;
|
|
1187
|
+
visualChangeBranchTarget.value = null;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function updateVisualChangeConfig(config: AppiumVisualChangeConfig) {
|
|
1191
|
+
visualChangeConfig.value = normalizeVisualChangeConfig(config);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function pickVisualChangeRegion() {
|
|
1195
|
+
if (!selectedDeviceId.value) {
|
|
1196
|
+
ElMessage.warning('请选择设备');
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
visualChangeConfig.value = normalizeVisualChangeConfig({
|
|
1200
|
+
...visualChangeConfig.value,
|
|
1201
|
+
mode: 'region',
|
|
1202
|
+
});
|
|
1203
|
+
visualChangePicking.value = true;
|
|
1204
|
+
ElMessage.info('请在设备预览中拖拽框选检测区域');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function applyVisualChangeRegion(region: AppiumVisualChangeRegion) {
|
|
1208
|
+
visualChangeConfig.value = normalizeVisualChangeConfig({
|
|
1209
|
+
...visualChangeConfig.value,
|
|
1210
|
+
mode: 'region',
|
|
1211
|
+
region,
|
|
1212
|
+
});
|
|
1213
|
+
visualChangePicking.value = false;
|
|
1214
|
+
ElMessage.success('已选择检测区域');
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function confirmVisualChangeStep() {
|
|
1218
|
+
const config = normalizeVisualChangeConfig(visualChangeConfig.value);
|
|
1219
|
+
if (config.mode === 'selectedElement' && !selectedNode.value?.bounds) {
|
|
1220
|
+
ElMessage.warning('当前没有可用的选中元素,请先选择组件或改为手动框选区域');
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const step = createVisualChangeStartStep(config);
|
|
1224
|
+
insertStep(
|
|
1225
|
+
step,
|
|
1226
|
+
visualChangeInsertIndex.value,
|
|
1227
|
+
visualChangeBranchTarget.value || undefined,
|
|
1228
|
+
);
|
|
1229
|
+
closeVisualChangeDialog();
|
|
1230
|
+
ElMessage.success('已添加检测画面变化开始节点');
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1021
1233
|
function isReadonlyFlowActionDisabled(_action: InsertAction) {
|
|
1022
1234
|
return true;
|
|
1023
1235
|
}
|
|
@@ -1037,6 +1249,7 @@ async function addAction(
|
|
|
1037
1249
|
) {
|
|
1038
1250
|
if (recordingBusy.value) return;
|
|
1039
1251
|
if (!ensureAppPackageSelected()) return;
|
|
1252
|
+
const previousStep = typeof index === 'number' && index >= 0 ? steps.value[index] : undefined;
|
|
1040
1253
|
if (action === 'launchApp' && steps.value.some((step) => step.type === 'launchApp')) {
|
|
1041
1254
|
ElMessage.warning('启动 APP 节点只能添加一个');
|
|
1042
1255
|
return;
|
|
@@ -1045,8 +1258,12 @@ async function addAction(
|
|
|
1045
1258
|
ElMessage.warning('清理 App 缓存节点只能添加一个');
|
|
1046
1259
|
return;
|
|
1047
1260
|
}
|
|
1048
|
-
if (
|
|
1049
|
-
ElMessage.warning(
|
|
1261
|
+
if (action === 'clearAppData' && index !== -1) {
|
|
1262
|
+
ElMessage.warning('清理 App 缓存只能从开始节点添加');
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (action === 'launchApp' && index !== -1 && previousStep?.type !== 'clearAppData') {
|
|
1266
|
+
ElMessage.warning('启动 APP 只能从开始节点或清理 App 缓存后添加');
|
|
1050
1267
|
return;
|
|
1051
1268
|
}
|
|
1052
1269
|
if (action === 'runScript') {
|
|
@@ -1054,22 +1271,60 @@ async function addAction(
|
|
|
1054
1271
|
return;
|
|
1055
1272
|
}
|
|
1056
1273
|
if (action === 'delay') {
|
|
1057
|
-
return addDelayStep(index);
|
|
1274
|
+
return addDelayStep(index, branchTarget);
|
|
1275
|
+
}
|
|
1276
|
+
if (action === 'aiRecognition') {
|
|
1277
|
+
const input = await ElMessageBox.prompt('识别内容', '添加 AI 识别', {
|
|
1278
|
+
inputType: 'textarea',
|
|
1279
|
+
inputPlaceholder: '例如:检查当前画面有没有显示黑屏',
|
|
1280
|
+
inputValidator: (value) => {
|
|
1281
|
+
try { validateAiRecognitionPrompt(value); return true; }
|
|
1282
|
+
catch (error) { return error instanceof Error ? error.message : '识别内容无效'; }
|
|
1283
|
+
},
|
|
1284
|
+
confirmButtonText: '添加', cancelButtonText: '取消',
|
|
1285
|
+
}).catch(() => null);
|
|
1286
|
+
if (!input) return;
|
|
1287
|
+
return insertStep({
|
|
1288
|
+
id: createStepId(), type: 'aiRecognition', label: 'AI 识别',
|
|
1289
|
+
value: validateAiRecognitionPrompt(input.value), timeoutMs: 60000,
|
|
1290
|
+
flow: { nodeKind: 'condition' },
|
|
1291
|
+
}, index, branchTarget);
|
|
1292
|
+
}
|
|
1293
|
+
if (action === 'textClick') {
|
|
1294
|
+
const step = reactive<AppiumRecordedStep>({
|
|
1295
|
+
id: createStepId(), type: 'textClick', label: '文字点击', value: '', timeoutMs: 10000,
|
|
1296
|
+
flow: { nodeKind: 'condition', textMatch: 'exact' },
|
|
1297
|
+
});
|
|
1298
|
+
const result = await ElMessageBox({
|
|
1299
|
+
title: '添加文字点击',
|
|
1300
|
+
message: h(ElForm, { labelPosition: 'top', style: { width: 'min(380px, calc(100vw - 64px))' } },
|
|
1301
|
+
() => h(TextClickSettings, { step, onUpdate: (patch) => Object.assign(step, patch) })),
|
|
1302
|
+
showCancelButton: true, confirmButtonText: '添加', cancelButtonText: '取消',
|
|
1303
|
+
beforeClose: (action, _instance, done) => {
|
|
1304
|
+
if (action === 'confirm') {
|
|
1305
|
+
try { textClickSelector(step); }
|
|
1306
|
+
catch (error) { ElMessage.warning((error as Error).message); return; }
|
|
1307
|
+
}
|
|
1308
|
+
done();
|
|
1309
|
+
},
|
|
1310
|
+
}).catch(() => null);
|
|
1311
|
+
if (result) return insertStep({ ...step }, index, branchTarget);
|
|
1312
|
+
return;
|
|
1058
1313
|
}
|
|
1059
1314
|
if (action === 'tap' || action === 'input' || action === 'assertExists' || action === 'waitFor') {
|
|
1060
|
-
return addStep(action, index);
|
|
1315
|
+
return addStep(action, index, branchTarget);
|
|
1061
1316
|
}
|
|
1062
1317
|
if (action === 'keyBack') {
|
|
1063
|
-
return insertStep(createKeyStep(4, '系统返回'), index);
|
|
1318
|
+
return insertStep(createKeyStep(4, '系统返回'), index, branchTarget);
|
|
1064
1319
|
}
|
|
1065
1320
|
if (action === 'keyHome') {
|
|
1066
|
-
return insertStep(createKeyStep(3, 'Home 键'), index);
|
|
1321
|
+
return insertStep(createKeyStep(3, 'Home 键'), index, branchTarget);
|
|
1067
1322
|
}
|
|
1068
1323
|
if (action === 'keyRecent') {
|
|
1069
|
-
return insertStep(createKeyStep(187, '最近任务'), index);
|
|
1324
|
+
return insertStep(createKeyStep(187, '最近任务'), index, branchTarget);
|
|
1070
1325
|
}
|
|
1071
1326
|
if (action === 'keyPower') {
|
|
1072
|
-
return insertStep(createKeyStep(26, '电源键'), index);
|
|
1327
|
+
return insertStep(createKeyStep(26, '电源键'), index, branchTarget);
|
|
1073
1328
|
}
|
|
1074
1329
|
if (action === 'waitActivity') {
|
|
1075
1330
|
const input = await ElMessageBox.prompt('请输入目标 Activity', '等待 Activity', {
|
|
@@ -1077,11 +1332,25 @@ async function addAction(
|
|
|
1077
1332
|
confirmButtonText: '添加',
|
|
1078
1333
|
cancelButtonText: '取消',
|
|
1079
1334
|
}).catch(() => null);
|
|
1080
|
-
if (input?.value) return insertStep(createWaitActivityStep(input.value), index);
|
|
1335
|
+
if (input?.value) return insertStep(createWaitActivityStep(input.value), index, branchTarget);
|
|
1081
1336
|
return;
|
|
1082
1337
|
}
|
|
1083
1338
|
if (action === 'swipe') {
|
|
1084
|
-
return addSwipeStep(index);
|
|
1339
|
+
return addSwipeStep(index, branchTarget);
|
|
1340
|
+
}
|
|
1341
|
+
if (action === 'visualChange' || action === 'visualChangeStart') {
|
|
1342
|
+
openVisualChangeDialog(index, branchTarget);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (action === 'visualChangeEnd') {
|
|
1346
|
+
const startStep = findOpenVisualChangeStart(steps.value, index, branchTarget);
|
|
1347
|
+
if (!startStep) {
|
|
1348
|
+
ElMessage.warning('当前分支没有可对应的检测画面变化开始节点');
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
const inserted = insertStep(createVisualChangeEndStep(startStep), index, branchTarget);
|
|
1352
|
+
ElMessage.success(`已添加 ${inserted.label}`);
|
|
1353
|
+
return inserted;
|
|
1085
1354
|
}
|
|
1086
1355
|
if (action === 'pinch') {
|
|
1087
1356
|
const input = await ElMessageBox.prompt('请输入缩放方向:放大 / 缩小', '添加双指缩放', {
|
|
@@ -1103,16 +1372,39 @@ async function addAction(
|
|
|
1103
1372
|
centerY: Math.round((props.deviceHeight || 1920) / 2),
|
|
1104
1373
|
percent: 0.5,
|
|
1105
1374
|
},
|
|
1106
|
-
}, index);
|
|
1375
|
+
}, index, branchTarget);
|
|
1376
|
+
}
|
|
1377
|
+
if (action === 'noop') {
|
|
1378
|
+
return insertStep(createNoopStep(), index, branchTarget);
|
|
1379
|
+
}
|
|
1380
|
+
if (action === 'log') {
|
|
1381
|
+
const step = reactive<AppiumRecordedStep>({
|
|
1382
|
+
id: createStepId(), type: 'log', label: '输出日志', logPrefix: DEFAULT_LOG_PREFIX, value: '',
|
|
1383
|
+
});
|
|
1384
|
+
const result = await ElMessageBox({
|
|
1385
|
+
title: '添加输出日志',
|
|
1386
|
+
message: h(ElForm, { labelPosition: 'top', style: { width: 'min(380px, calc(100vw - 64px))' } }, () => h(StageLogSettings, {
|
|
1387
|
+
step, onUpdate: (patch) => Object.assign(step, patch),
|
|
1388
|
+
})),
|
|
1389
|
+
showCancelButton: true, confirmButtonText: '添加', cancelButtonText: '取消',
|
|
1390
|
+
beforeClose: (action, _instance, done) => {
|
|
1391
|
+
const error = action === 'confirm' ? validateStageLog(step) : '';
|
|
1392
|
+
if (error) { ElMessage.warning(error); return; }
|
|
1393
|
+
done();
|
|
1394
|
+
},
|
|
1395
|
+
}).catch(() => null);
|
|
1396
|
+
if (result) return insertStep({ ...step }, index, branchTarget);
|
|
1397
|
+
return;
|
|
1107
1398
|
}
|
|
1108
1399
|
if (action === 'launchApp') {
|
|
1109
1400
|
const clearStepIndex = steps.value.findIndex((step) => step.type === 'clearAppData');
|
|
1401
|
+
const insertIndex = previousStep?.type === 'clearAppData' ? index : clearStepIndex >= 0 ? clearStepIndex : index;
|
|
1110
1402
|
return insertStep({
|
|
1111
1403
|
id: `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1112
1404
|
type: 'launchApp',
|
|
1113
1405
|
label: `启动 APP ${form.appPackage}`,
|
|
1114
1406
|
value: form.appPackage,
|
|
1115
|
-
},
|
|
1407
|
+
}, insertIndex);
|
|
1116
1408
|
}
|
|
1117
1409
|
if (action === 'clearAppData') {
|
|
1118
1410
|
return insertStep({
|
|
@@ -1124,14 +1416,35 @@ async function addAction(
|
|
|
1124
1416
|
}
|
|
1125
1417
|
const node = getSelectedNode();
|
|
1126
1418
|
if (!node) return;
|
|
1419
|
+
if (action === 'checkboxState' || action === 'radioButtonState') {
|
|
1420
|
+
const controlName = nativeControlName(action);
|
|
1421
|
+
if (nativeControlType(node.className) !== action || node.checkable !== true) {
|
|
1422
|
+
ElMessage.warning(`请选择原生 ${controlName} 元素,当前选中:${node.className || '未知类型'}`);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
const step = createNodeActionStep(action, `判断 ${controlName} 状态`, node, { timeoutMs: 2000 });
|
|
1426
|
+
if ((!step.selector?.value || step.selector.strategy === 'bounds') && node.xpath) {
|
|
1427
|
+
step.selector = { strategy: 'xpath', value: node.xpath };
|
|
1428
|
+
step.contextSelector = undefined;
|
|
1429
|
+
step.selectorChain = undefined;
|
|
1430
|
+
}
|
|
1431
|
+
if (!step.selector?.value || step.selector.strategy === 'bounds') {
|
|
1432
|
+
ElMessage.warning(`当前 ${controlName} 没有可用的元素定位器`);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
// 只保存定位信息,回放时读取实时状态;坐标不能用于判断元素属性。
|
|
1436
|
+
step.fallback = undefined;
|
|
1437
|
+
step.flow = { nodeKind: 'condition' };
|
|
1438
|
+
return insertStep(step, index, branchTarget);
|
|
1439
|
+
}
|
|
1127
1440
|
if (action === 'popupCondition') {
|
|
1128
1441
|
const step = createNodeActionStep('assertExists', '判断存在', node, { timeoutMs: 2000 });
|
|
1129
|
-
const inserted = insertStep({ ...step, flow: { nodeKind: 'condition' } }, index);
|
|
1442
|
+
const inserted = insertStep({ ...step, flow: { nodeKind: 'condition' } }, index, branchTarget);
|
|
1130
1443
|
ElMessage.success('已添加判断节点,请在节点面板配置是/否分支');
|
|
1131
1444
|
return inserted;
|
|
1132
1445
|
}
|
|
1133
1446
|
if (action === 'tapIfExists') {
|
|
1134
|
-
return insertStep(createNodeActionStep('tapIfExists', '存在则点击', node, { timeoutMs: 2000 }), index);
|
|
1447
|
+
return insertStep(createNodeActionStep('tapIfExists', '存在则点击', node, { timeoutMs: 2000 }), index, branchTarget);
|
|
1135
1448
|
}
|
|
1136
1449
|
if (action === 'inputIfExists') {
|
|
1137
1450
|
const input = await ElMessageBox.prompt('', '存在则输入', {
|
|
@@ -1141,16 +1454,20 @@ async function addAction(
|
|
|
1141
1454
|
center: true,
|
|
1142
1455
|
}).catch(() => null);
|
|
1143
1456
|
if (!input) return;
|
|
1144
|
-
return insertStep(
|
|
1457
|
+
return insertStep(
|
|
1458
|
+
createNodeActionStep('inputIfExists', '存在则输入', node, { timeoutMs: 2000, value: input.value }),
|
|
1459
|
+
index,
|
|
1460
|
+
branchTarget,
|
|
1461
|
+
);
|
|
1145
1462
|
}
|
|
1146
1463
|
if (action === 'clearIfExists') {
|
|
1147
|
-
return insertStep(createNodeActionStep('clearIfExists', '存在则清空', node, { timeoutMs: 2000 }), index);
|
|
1464
|
+
return insertStep(createNodeActionStep('clearIfExists', '存在则清空', node, { timeoutMs: 2000 }), index, branchTarget);
|
|
1148
1465
|
}
|
|
1149
1466
|
if (action === 'backIfExists') {
|
|
1150
|
-
return insertStep(createNodeActionStep('backIfExists', '存在则返回', node, { timeoutMs: 2000 }), index);
|
|
1467
|
+
return insertStep(createNodeActionStep('backIfExists', '存在则返回', node, { timeoutMs: 2000 }), index, branchTarget);
|
|
1151
1468
|
}
|
|
1152
1469
|
if (action === 'clearInput') {
|
|
1153
|
-
return insertStep(createNodeActionStep('clearInput', '清空输入', node), index);
|
|
1470
|
+
return insertStep(createNodeActionStep('clearInput', '清空输入', node), index, branchTarget);
|
|
1154
1471
|
}
|
|
1155
1472
|
if (action === 'coordinateTap') {
|
|
1156
1473
|
if (!node.bounds) {
|
|
@@ -1162,32 +1479,39 @@ async function addAction(
|
|
|
1162
1479
|
type: 'coordinateTap',
|
|
1163
1480
|
label: `点击坐标 ${node.bounds.centerX},${node.bounds.centerY}`,
|
|
1164
1481
|
fallback: { strategy: 'bounds', centerX: node.bounds.centerX, centerY: node.bounds.centerY },
|
|
1165
|
-
}, index);
|
|
1482
|
+
}, index, branchTarget);
|
|
1166
1483
|
}
|
|
1167
1484
|
if (action === 'longPress') {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1485
|
+
const step = reactive<AppiumRecordedStep>({
|
|
1486
|
+
...createNodeActionStep('longPress', '长按', node, { timeoutMs: 800 }),
|
|
1487
|
+
longPressMode: 'element',
|
|
1488
|
+
});
|
|
1489
|
+
// 无 id/text 的组件仍可用组件树中的绝对 XPath 定位,不依赖录制坐标。
|
|
1490
|
+
if ((!step.selector?.value || step.selector.strategy === 'bounds') && node.xpath) {
|
|
1491
|
+
step.selector = { strategy: 'xpath', value: node.xpath };
|
|
1492
|
+
step.contextSelector = undefined;
|
|
1493
|
+
step.selectorChain = undefined;
|
|
1171
1494
|
}
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1495
|
+
const result = await ElMessageBox({
|
|
1496
|
+
title: '添加长按',
|
|
1497
|
+
message: h(ElForm, { labelPosition: 'top', size: 'small' }, () => [
|
|
1498
|
+
h(ElFormItem, { label: '当前元素' }, () => h('div', { style: { overflowWrap: 'anywhere' } }, node.label)),
|
|
1499
|
+
h(LongPressSettings, { step, onUpdate: (patch) => Object.assign(step, patch) }),
|
|
1500
|
+
]),
|
|
1501
|
+
showCancelButton: true,
|
|
1176
1502
|
confirmButtonText: '添加',
|
|
1177
1503
|
cancelButtonText: '取消',
|
|
1504
|
+
beforeClose: (action, _instance, done) => {
|
|
1505
|
+
const error = action === 'confirm' ? validateLongPress(step) : '';
|
|
1506
|
+
if (error) { ElMessage.warning(error); return; }
|
|
1507
|
+
done();
|
|
1508
|
+
},
|
|
1178
1509
|
}).catch(() => null);
|
|
1179
|
-
if (!
|
|
1180
|
-
|
|
1181
|
-
return insertStep({
|
|
1182
|
-
id: `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1183
|
-
type: 'longPress',
|
|
1184
|
-
label: `长按 ${node.label}`,
|
|
1185
|
-
fallback: { strategy: 'bounds', centerX: node.bounds.centerX, centerY: node.bounds.centerY },
|
|
1186
|
-
timeoutMs: duration,
|
|
1187
|
-
}, index);
|
|
1510
|
+
if (!result) return;
|
|
1511
|
+
return insertStep({ ...step }, index, branchTarget);
|
|
1188
1512
|
}
|
|
1189
1513
|
if (action === 'waitDisappear') {
|
|
1190
|
-
return insertStep(createNodeActionStep('waitDisappear', '等待元素消失', node, { timeoutMs: 10000 }), index);
|
|
1514
|
+
return insertStep(createNodeActionStep('waitDisappear', '等待元素消失', node, { timeoutMs: 10000 }), index, branchTarget);
|
|
1191
1515
|
}
|
|
1192
1516
|
if (action === 'assertText') {
|
|
1193
1517
|
const input = await ElMessageBox.prompt('请输入要断言的文本', '断言文本', {
|
|
@@ -1195,7 +1519,7 @@ async function addAction(
|
|
|
1195
1519
|
confirmButtonText: '添加',
|
|
1196
1520
|
cancelButtonText: '取消',
|
|
1197
1521
|
}).catch(() => null);
|
|
1198
|
-
if (input?.value) return insertStep(createNodeActionStep('assertText', '断言文本', node, { value: input.value }), index);
|
|
1522
|
+
if (input?.value) return insertStep(createNodeActionStep('assertText', '断言文本', node, { value: input.value }), index, branchTarget);
|
|
1199
1523
|
}
|
|
1200
1524
|
}
|
|
1201
1525
|
|
|
@@ -1215,28 +1539,18 @@ async function addBranchAction(index: number, branch: BranchName, action: Record
|
|
|
1215
1539
|
const nextTargetId = insertAtBranchEntry
|
|
1216
1540
|
? nextBranchStepAfter(condition.id, branch, index)?.id || (!targetIsBranchStep ? currentTargetId : undefined)
|
|
1217
1541
|
: undefined;
|
|
1218
|
-
const
|
|
1542
|
+
const branchTarget: BranchTarget = {
|
|
1219
1543
|
stepId: condition.id,
|
|
1220
1544
|
branch,
|
|
1221
1545
|
updateTarget: insertAtBranchEntry || !currentTargetId || !targetIsBranchStep,
|
|
1222
1546
|
entryTargetId: insertAtBranchEntry ? undefined : existingBranchSteps[0]?.id,
|
|
1223
1547
|
nextTargetId,
|
|
1224
|
-
}
|
|
1548
|
+
};
|
|
1549
|
+
const inserted = await addAction(action, insertIndex, branchTarget);
|
|
1225
1550
|
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
|
-
}
|
|
1551
|
+
const currentInserted = steps.value.find((step) => step.id === inserted.id);
|
|
1552
|
+
if (currentInserted?.flow?.parentConditionId === condition.id && currentInserted.flow.parentBranch === branch) return;
|
|
1553
|
+
applyBranchTargetToInsertedStep(inserted, branchTarget);
|
|
1240
1554
|
}
|
|
1241
1555
|
|
|
1242
1556
|
async function saveOnActivityChange() {
|
|
@@ -1270,41 +1584,42 @@ async function saveOnActivityChange() {
|
|
|
1270
1584
|
|
|
1271
1585
|
function removeStep(index: number) {
|
|
1272
1586
|
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
|
-
});
|
|
1587
|
+
steps.value = removeFlowStep(steps.value, index);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function syncVisualChangePairConfig(
|
|
1591
|
+
candidate: AppiumRecordedStep,
|
|
1592
|
+
sourceConfig: NonNullable<AppiumRecordedStep['visualChange']>,
|
|
1593
|
+
) {
|
|
1594
|
+
if (candidate.visualChange?.role !== 'end') return candidate;
|
|
1595
|
+
if (!sourceConfig.pairId || candidate.visualChange.pairId !== sourceConfig.pairId) return candidate;
|
|
1596
|
+
return {
|
|
1597
|
+
...candidate,
|
|
1598
|
+
visualChange: {
|
|
1599
|
+
...candidate.visualChange,
|
|
1600
|
+
mode: sourceConfig.mode,
|
|
1601
|
+
region: sourceConfig.region,
|
|
1602
|
+
pairLabel: sourceConfig.pairLabel,
|
|
1603
|
+
durationMs: sourceConfig.durationMs,
|
|
1604
|
+
intervalMs: sourceConfig.intervalMs,
|
|
1605
|
+
changeRatioThreshold: sourceConfig.changeRatioThreshold,
|
|
1606
|
+
pixelmatchThreshold: sourceConfig.pixelmatchThreshold,
|
|
1607
|
+
},
|
|
1608
|
+
};
|
|
1302
1609
|
}
|
|
1303
1610
|
|
|
1304
1611
|
function updateStep(index: number, step: AppiumRecordedStep) {
|
|
1305
1612
|
if (recordingLocked.value) return;
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1613
|
+
const visualChange = step.visualChange?.role === 'start'
|
|
1614
|
+
? normalizeVisualChangeConfig(step.visualChange)
|
|
1615
|
+
: undefined;
|
|
1616
|
+
const nextSteps = steps.value.map((candidate, stepIndex) => (
|
|
1617
|
+
stepIndex === index
|
|
1618
|
+
? step
|
|
1619
|
+
: visualChange
|
|
1620
|
+
? syncVisualChangePairConfig(candidate, visualChange)
|
|
1621
|
+
: candidate
|
|
1622
|
+
));
|
|
1308
1623
|
steps.value = nextSteps;
|
|
1309
1624
|
}
|
|
1310
1625
|
|
|
@@ -1339,7 +1654,7 @@ function loadScript(script: AppiumRecordedScript) {
|
|
|
1339
1654
|
if (script.deviceId && script.deviceId !== selectedDeviceId.value) {
|
|
1340
1655
|
void switchDevice(script.deviceId);
|
|
1341
1656
|
}
|
|
1342
|
-
steps.value = (script.steps || []).map(normalizeLegacyFlowScope);
|
|
1657
|
+
steps.value = normalizeLegacyNestedConditionBranches((script.steps || []).map(normalizeLegacyFlowScope));
|
|
1343
1658
|
markDraftSaved();
|
|
1344
1659
|
}
|
|
1345
1660
|
|
|
@@ -1361,12 +1676,121 @@ function resetCurrentScript() {
|
|
|
1361
1676
|
markDraftSaved();
|
|
1362
1677
|
}
|
|
1363
1678
|
|
|
1679
|
+
function requestNewScript() {
|
|
1680
|
+
if (newScriptDisabled.value) return;
|
|
1681
|
+
if (selectedScriptId.value || form.name.trim() || steps.value.length) {
|
|
1682
|
+
newScriptDialogVisible.value = true;
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
startNewScript();
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function startNewScript() {
|
|
1689
|
+
if (newScriptDisabled.value) return;
|
|
1690
|
+
resetCurrentScript();
|
|
1691
|
+
// 新脚本不沿用旧画布的展开、批量选择和缩放状态。
|
|
1692
|
+
newScriptRevision.value += 1;
|
|
1693
|
+
newScriptDialogVisible.value = false;
|
|
1694
|
+
setActiveWorkbenchTab('recording');
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
async function saveAndNewScript() {
|
|
1698
|
+
if (newScriptDisabled.value) return;
|
|
1699
|
+
// 保存成功后才重置,校验错误或网络异常均保留当前草稿。
|
|
1700
|
+
if (await saveScript()) startNewScript();
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1364
1703
|
function formatScriptTime(value: string) {
|
|
1365
1704
|
if (!value) return '-';
|
|
1366
1705
|
const date = new Date(value);
|
|
1367
1706
|
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString();
|
|
1368
1707
|
}
|
|
1369
1708
|
|
|
1709
|
+
function cloneScriptSteps(stepList: AppiumRecordedStep[]) {
|
|
1710
|
+
return JSON.parse(JSON.stringify(stepList || [])) as AppiumRecordedStep[];
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function isScriptNameTaken(name: string, exceptId = '') {
|
|
1714
|
+
return scripts.value.some((script) => script.id !== exceptId && script.name === name);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function createDuplicateScriptName(sourceName: string) {
|
|
1718
|
+
const baseName = `${sourceName} Copy`;
|
|
1719
|
+
if (!isScriptNameTaken(baseName)) return baseName;
|
|
1720
|
+
|
|
1721
|
+
let suffix = 2;
|
|
1722
|
+
let nextName = `${baseName} ${suffix}`;
|
|
1723
|
+
while (isScriptNameTaken(nextName)) {
|
|
1724
|
+
suffix += 1;
|
|
1725
|
+
nextName = `${baseName} ${suffix}`;
|
|
1726
|
+
}
|
|
1727
|
+
return nextName;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
async function duplicateScript(script: AppiumRecordedScript) {
|
|
1731
|
+
if (duplicatingScriptId.value) return;
|
|
1732
|
+
duplicatingScriptId.value = script.id;
|
|
1733
|
+
try {
|
|
1734
|
+
const payload = await saveAppiumScript({
|
|
1735
|
+
name: createDuplicateScriptName(script.name),
|
|
1736
|
+
appPackage: script.appPackage,
|
|
1737
|
+
appActivity: script.appActivity,
|
|
1738
|
+
deviceId: script.deviceId,
|
|
1739
|
+
steps: cloneScriptSteps(script.steps),
|
|
1740
|
+
});
|
|
1741
|
+
await loadScripts();
|
|
1742
|
+
ElMessage.success(`已复制为「${payload.script.name}」`);
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
ElMessage.error(error instanceof Error ? error.message : '复制脚本失败');
|
|
1745
|
+
} finally {
|
|
1746
|
+
duplicatingScriptId.value = '';
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
async function renameScript(script: AppiumRecordedScript) {
|
|
1751
|
+
if (renamingScriptId.value) return;
|
|
1752
|
+
const input = await ElMessageBox.prompt('请输入新的脚本名称', '修改脚本名称', {
|
|
1753
|
+
inputValue: script.name,
|
|
1754
|
+
confirmButtonText: '保存',
|
|
1755
|
+
cancelButtonText: '取消',
|
|
1756
|
+
center: true,
|
|
1757
|
+
inputValidator(value) {
|
|
1758
|
+
const nextName = String(value || '').trim();
|
|
1759
|
+
if (!nextName) return '脚本名称不能为空';
|
|
1760
|
+
if (isScriptNameTaken(nextName, script.id)) return '脚本名称已存在';
|
|
1761
|
+
return true;
|
|
1762
|
+
},
|
|
1763
|
+
}).catch(() => null);
|
|
1764
|
+
const nextName = input ? String(input.value || '').trim() : '';
|
|
1765
|
+
if (!nextName || nextName === script.name) return;
|
|
1766
|
+
|
|
1767
|
+
const wasCurrentScript = selectedScriptId.value === script.id;
|
|
1768
|
+
const hadUnsavedChanges = hasUnsavedChanges.value;
|
|
1769
|
+
renamingScriptId.value = script.id;
|
|
1770
|
+
try {
|
|
1771
|
+
const payload = await saveAppiumScript({
|
|
1772
|
+
id: script.id,
|
|
1773
|
+
name: nextName,
|
|
1774
|
+
appPackage: script.appPackage,
|
|
1775
|
+
appActivity: script.appActivity,
|
|
1776
|
+
deviceId: script.deviceId,
|
|
1777
|
+
steps: cloneScriptSteps(script.steps),
|
|
1778
|
+
});
|
|
1779
|
+
await loadScripts();
|
|
1780
|
+
if (wasCurrentScript) {
|
|
1781
|
+
form.name = payload.script.name;
|
|
1782
|
+
if (!hadUnsavedChanges) {
|
|
1783
|
+
markDraftSaved();
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
ElMessage.success('脚本名称已修改');
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
ElMessage.error(error instanceof Error ? error.message : '修改脚本名称失败');
|
|
1789
|
+
} finally {
|
|
1790
|
+
renamingScriptId.value = '';
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1370
1794
|
async function removeScript(script: AppiumRecordedScript) {
|
|
1371
1795
|
try {
|
|
1372
1796
|
await ElMessageBox.confirm(`确定删除脚本「${script.name}」吗?`, '删除脚本', {
|
|
@@ -1395,13 +1819,24 @@ async function removeScript(script: AppiumRecordedScript) {
|
|
|
1395
1819
|
}
|
|
1396
1820
|
}
|
|
1397
1821
|
|
|
1398
|
-
function downloadScript(script: AppiumRecordedScript) {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1822
|
+
async function downloadScript(script: AppiumRecordedScript) {
|
|
1823
|
+
if (downloadingScriptId.value) return;
|
|
1824
|
+
downloadingScriptId.value = script.id;
|
|
1825
|
+
try {
|
|
1826
|
+
const { blob, fileName } = await downloadAppiumScript(script.id);
|
|
1827
|
+
const url = URL.createObjectURL(blob);
|
|
1828
|
+
const link = document.createElement('a');
|
|
1829
|
+
link.href = url;
|
|
1830
|
+
link.download = fileName;
|
|
1831
|
+
document.body.appendChild(link);
|
|
1832
|
+
link.click();
|
|
1833
|
+
link.remove();
|
|
1834
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
ElMessage.error(error instanceof Error ? error.message : '导出脚本失败');
|
|
1837
|
+
} finally {
|
|
1838
|
+
downloadingScriptId.value = '';
|
|
1839
|
+
}
|
|
1405
1840
|
}
|
|
1406
1841
|
|
|
1407
1842
|
function chooseScriptImportFile() {
|
|
@@ -1428,7 +1863,7 @@ async function importScriptFile(event: Event) {
|
|
|
1428
1863
|
}
|
|
1429
1864
|
|
|
1430
1865
|
async function saveScript() {
|
|
1431
|
-
if (!ensureAppPackageSelected()) return;
|
|
1866
|
+
if (!ensureAppPackageSelected()) return false;
|
|
1432
1867
|
saving.value = true;
|
|
1433
1868
|
try {
|
|
1434
1869
|
const payload = await saveAppiumScript({
|
|
@@ -1443,8 +1878,10 @@ async function saveScript() {
|
|
|
1443
1878
|
selectedScriptId.value = payload.script.id;
|
|
1444
1879
|
markDraftSaved();
|
|
1445
1880
|
ElMessage.success('Appium 脚本已保存');
|
|
1881
|
+
return true;
|
|
1446
1882
|
} catch (error) {
|
|
1447
1883
|
ElMessage.error(error instanceof Error ? error.message : '保存失败');
|
|
1884
|
+
return false;
|
|
1448
1885
|
} finally {
|
|
1449
1886
|
saving.value = false;
|
|
1450
1887
|
}
|
|
@@ -1455,6 +1892,10 @@ async function replayScript() {
|
|
|
1455
1892
|
ElMessage.warning('请选择已保存脚本');
|
|
1456
1893
|
return;
|
|
1457
1894
|
}
|
|
1895
|
+
if (hasUnsavedChanges.value) {
|
|
1896
|
+
const saved = await saveScript();
|
|
1897
|
+
if (!saved || !selectedScript.value) return;
|
|
1898
|
+
}
|
|
1458
1899
|
replaying.value = true;
|
|
1459
1900
|
stoppingReplay.value = false;
|
|
1460
1901
|
activeReplayDeviceId.value = selectedDeviceId.value;
|
|
@@ -1470,6 +1911,8 @@ async function replayScript() {
|
|
|
1470
1911
|
replayOutput.value = result.output || '';
|
|
1471
1912
|
if (result.stopped) {
|
|
1472
1913
|
ElMessage.info('回放已终止,报告、日志和截图回放已生成');
|
|
1914
|
+
} else if (result.softFailureCount) {
|
|
1915
|
+
ElMessage.warning(`回放完成,但存在 ${result.softFailureCount} 个视觉变化未达预期节点`);
|
|
1473
1916
|
} else if (result.success) {
|
|
1474
1917
|
ElMessage.success('回放完成');
|
|
1475
1918
|
} else {
|
|
@@ -1549,6 +1992,7 @@ watch(
|
|
|
1549
1992
|
>
|
|
1550
1993
|
<el-option v-for="script in scripts" :key="script.id" :label="script.name" :value="script.id" />
|
|
1551
1994
|
</el-select>
|
|
1995
|
+
<el-button :icon="Plus" :disabled="newScriptDisabled || newScriptDialogVisible" @click="requestNewScript">新建</el-button>
|
|
1552
1996
|
<el-button :icon="Check" :loading="saving" @click="saveScript">保存</el-button>
|
|
1553
1997
|
<el-button
|
|
1554
1998
|
type="primary"
|
|
@@ -1581,12 +2025,16 @@ watch(
|
|
|
1581
2025
|
:actions="deviceActions"
|
|
1582
2026
|
:overlay-bounds="overlayBounds"
|
|
1583
2027
|
:selected-bounds="selectedBounds"
|
|
2028
|
+
:selected-region="visualChangeSelectedRegion"
|
|
2029
|
+
:region-selection="visualChangeRegionSelectionEnabled"
|
|
2030
|
+
:region-draw-mode="visualChangePicking"
|
|
1584
2031
|
:device-width="deviceWidth"
|
|
1585
2032
|
:device-height="deviceHeight"
|
|
1586
2033
|
@switch-device="switchDevice"
|
|
1587
2034
|
@trigger-key="triggerDeviceKey"
|
|
1588
2035
|
@refresh-preview="refreshDevicePreview"
|
|
1589
2036
|
@tap="selectNodeFromPoint"
|
|
2037
|
+
@region-select="applyVisualChangeRegion"
|
|
1590
2038
|
@swipe="swipePreview"
|
|
1591
2039
|
/>
|
|
1592
2040
|
|
|
@@ -1664,6 +2112,8 @@ watch(
|
|
|
1664
2112
|
<section class="appium-workbench__section">
|
|
1665
2113
|
<h3>录制步骤</h3>
|
|
1666
2114
|
<RecordedSteps
|
|
2115
|
+
:key="newScriptRevision"
|
|
2116
|
+
:ai-recognition-model-configured="aiRecognitionModelConfigured"
|
|
1667
2117
|
:steps="steps"
|
|
1668
2118
|
:clipboard-count="flowClipboardCount"
|
|
1669
2119
|
:disabled="recordingLocked"
|
|
@@ -1715,18 +2165,35 @@ watch(
|
|
|
1715
2165
|
</div>
|
|
1716
2166
|
<div class="appium-script-list__actions">
|
|
1717
2167
|
<el-button size="small" @click.stop="loadScript(script)">加载</el-button>
|
|
2168
|
+
<el-button
|
|
2169
|
+
size="small"
|
|
2170
|
+
:icon="Edit"
|
|
2171
|
+
:loading="renamingScriptId === script.id"
|
|
2172
|
+
title="修改脚本名称"
|
|
2173
|
+
@click.stop="renameScript(script)"
|
|
2174
|
+
/>
|
|
2175
|
+
<el-button
|
|
2176
|
+
size="small"
|
|
2177
|
+
:icon="CopyDocument"
|
|
2178
|
+
:loading="duplicatingScriptId === script.id"
|
|
2179
|
+
title="复制脚本"
|
|
2180
|
+
@click.stop="duplicateScript(script)"
|
|
2181
|
+
/>
|
|
1718
2182
|
<el-button
|
|
1719
2183
|
size="small"
|
|
1720
2184
|
:icon="Download"
|
|
2185
|
+
:loading="downloadingScriptId === script.id"
|
|
2186
|
+
:disabled="Boolean(downloadingScriptId) && downloadingScriptId !== script.id"
|
|
1721
2187
|
title="下载脚本"
|
|
1722
|
-
@click="downloadScript(script)"
|
|
2188
|
+
@click.stop="downloadScript(script)"
|
|
1723
2189
|
/>
|
|
1724
2190
|
<el-button
|
|
1725
2191
|
size="small"
|
|
1726
2192
|
type="danger"
|
|
1727
2193
|
:icon="Delete"
|
|
1728
2194
|
:loading="deletingScriptId === script.id"
|
|
1729
|
-
|
|
2195
|
+
title="删除脚本"
|
|
2196
|
+
@click.stop="removeScript(script)"
|
|
1730
2197
|
/>
|
|
1731
2198
|
</div>
|
|
1732
2199
|
</article>
|
|
@@ -1737,6 +2204,34 @@ watch(
|
|
|
1737
2204
|
</el-card>
|
|
1738
2205
|
</div>
|
|
1739
2206
|
|
|
2207
|
+
<NewScriptDialog
|
|
2208
|
+
v-if="newScriptDialogVisible"
|
|
2209
|
+
:script-name="form.name"
|
|
2210
|
+
:saving="saving"
|
|
2211
|
+
@save="saveAndNewScript"
|
|
2212
|
+
@discard="startNewScript"
|
|
2213
|
+
@cancel="newScriptDialogVisible = false"
|
|
2214
|
+
/>
|
|
2215
|
+
|
|
2216
|
+
<AiRecognitionTestDialog
|
|
2217
|
+
v-if="aiRecognitionTestStep"
|
|
2218
|
+
:step="aiRecognitionTestStep"
|
|
2219
|
+
:device-id="selectedDeviceId"
|
|
2220
|
+
:model-configured="Boolean(aiRecognitionModelConfigured)"
|
|
2221
|
+
@close="aiRecognitionTestStep = null"
|
|
2222
|
+
/>
|
|
2223
|
+
|
|
2224
|
+
<VisualChangeDialog
|
|
2225
|
+
:model-value="visualChangeDialogVisible"
|
|
2226
|
+
:config="visualChangeConfig"
|
|
2227
|
+
:has-selected-element="Boolean(selectedNode?.bounds)"
|
|
2228
|
+
:picking-region="visualChangePicking"
|
|
2229
|
+
@update:model-value="$event ? (visualChangeDialogVisible = true) : closeVisualChangeDialog()"
|
|
2230
|
+
@update:config="updateVisualChangeConfig"
|
|
2231
|
+
@pick-region="pickVisualChangeRegion"
|
|
2232
|
+
@confirm="confirmVisualChangeStep"
|
|
2233
|
+
/>
|
|
2234
|
+
|
|
1740
2235
|
<el-dialog
|
|
1741
2236
|
:model-value="Boolean(pendingNavigation)"
|
|
1742
2237
|
title="检测到 Activity 跳转"
|
|
@@ -1827,6 +2322,7 @@ watch(
|
|
|
1827
2322
|
</div>
|
|
1828
2323
|
<FlowCanvas
|
|
1829
2324
|
id="appium-linked-script-preview"
|
|
2325
|
+
:ai-recognition-model-configured="aiRecognitionModelConfigured"
|
|
1830
2326
|
class="appium-linked-script-preview-canvas"
|
|
1831
2327
|
readonly
|
|
1832
2328
|
disabled
|