android-midscene-automation 0.1.6 → 0.1.9
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/README.md +8 -10
- package/package.json +3 -3
- package/server/appium-recorder/appium-runner.ts +362 -26
- package/server/appium-recorder/repository.ts +89 -12
- package/src/appium-recorder/AppiumPage.vue +504 -124
- package/src/appium-recorder/components/ComponentTree.vue +14 -1
- package/src/appium-recorder/components/NodeDetail.vue +24 -118
- package/src/appium-recorder/components/RecordedSteps.vue +898 -36
- package/src/appium-recorder/tree.ts +55 -9
- package/src/appium-recorder/types.ts +33 -1
- package/src/style.css +659 -11
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, reactive, shallowRef, watch } from 'vue';
|
|
3
3
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
4
|
-
import { Check, Delete, Refresh, VideoPlay } from '@element-plus/icons-vue';
|
|
4
|
+
import { Check, CopyDocument, Delete, FullScreen, Refresh, VideoPlay } 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
8
|
deleteAppiumScript,
|
|
9
9
|
getAppiumScripts,
|
|
10
10
|
getAppiumTree,
|
|
11
|
-
pressAppiumDeviceKey,
|
|
12
11
|
replayAppiumScript,
|
|
13
12
|
saveAppiumScript,
|
|
14
13
|
tapAppiumDevice,
|
|
@@ -19,10 +18,23 @@ import RecordedSteps from './components/RecordedSteps.vue';
|
|
|
19
18
|
import { findSmallestNodeAtPoint, flattenNodes, parseWindowHierarchy } from './tree';
|
|
20
19
|
import type { AppiumNode, AppiumRecordedScript, AppiumRecordedStep } from './types';
|
|
21
20
|
|
|
22
|
-
type NodeStepType = 'tap' | 'input' | 'assertExists';
|
|
21
|
+
type NodeStepType = 'tap' | 'input' | 'assertExists' | 'waitFor';
|
|
22
|
+
type BranchName = 'yes' | 'no';
|
|
23
|
+
type StepScope = 'pre' | 'main' | 'post';
|
|
24
|
+
type BranchTarget = {
|
|
25
|
+
stepId: string;
|
|
26
|
+
branch: BranchName;
|
|
27
|
+
updateTarget?: boolean;
|
|
28
|
+
};
|
|
23
29
|
type RecorderAction =
|
|
24
30
|
| 'delay'
|
|
25
31
|
| NodeStepType
|
|
32
|
+
| 'tapIfExists'
|
|
33
|
+
| 'inputIfExists'
|
|
34
|
+
| 'clearIfExists'
|
|
35
|
+
| 'backIfExists'
|
|
36
|
+
| 'popupCondition'
|
|
37
|
+
| 'runScript'
|
|
26
38
|
| 'keyBack'
|
|
27
39
|
| 'keyHome'
|
|
28
40
|
| 'keyRecent'
|
|
@@ -71,18 +83,21 @@ const steps = shallowRef<AppiumRecordedStep[]>([]);
|
|
|
71
83
|
const rawXml = shallowRef('');
|
|
72
84
|
const currentActivity = shallowRef('');
|
|
73
85
|
const replayOutput = shallowRef('');
|
|
86
|
+
const replayOutputDialogVisible = shallowRef(false);
|
|
87
|
+
const linkedScriptDialogVisible = shallowRef(false);
|
|
88
|
+
const linkedScriptTargetId = shallowRef('');
|
|
89
|
+
const linkedScriptInsertIndex = shallowRef<number | undefined>();
|
|
90
|
+
const linkedScriptBranchTarget = shallowRef<BranchTarget | null>(null);
|
|
91
|
+
const linkedScriptExpectedActivity = shallowRef('');
|
|
74
92
|
const loadingTree = shallowRef(false);
|
|
75
93
|
const saving = shallowRef(false);
|
|
76
94
|
const deletingScriptId = shallowRef('');
|
|
77
95
|
const replaying = shallowRef(false);
|
|
78
96
|
const recordingTap = shallowRef(false);
|
|
79
97
|
const resolvingNavigation = shallowRef(false);
|
|
80
|
-
const maxReplayOutputLength = 20_000;
|
|
81
98
|
const pendingNavigation = shallowRef<{
|
|
82
99
|
beforeActivity: string;
|
|
83
100
|
afterActivity: string;
|
|
84
|
-
beforeSignature: string;
|
|
85
|
-
afterSignature: string;
|
|
86
101
|
} | null>(null);
|
|
87
102
|
|
|
88
103
|
const form = reactive({
|
|
@@ -92,9 +107,29 @@ const form = reactive({
|
|
|
92
107
|
});
|
|
93
108
|
|
|
94
109
|
const selectedScript = computed(() => scripts.value.find((script) => script.id === selectedScriptId.value) || null);
|
|
95
|
-
const
|
|
110
|
+
const linkableScripts = computed(() => scripts.value.filter((script) => script.id !== selectedScriptId.value));
|
|
111
|
+
const compatibleLinkableScripts = computed(() => linkableScripts.value.filter((script) => (
|
|
112
|
+
Boolean(linkedScriptExpectedActivity.value)
|
|
113
|
+
&& script.appActivity === linkedScriptExpectedActivity.value
|
|
114
|
+
)));
|
|
115
|
+
const visibleReplayOutput = computed(() => {
|
|
116
|
+
const lines = replayOutput.value.split(/\r?\n/);
|
|
117
|
+
const maxLines = 80;
|
|
118
|
+
if (lines.length <= maxLines) return replayOutput.value;
|
|
119
|
+
return `... 仅显示最近 ${maxLines} 行,点击展开查看完整日志\n${lines.slice(-maxLines).join('\n')}`;
|
|
120
|
+
});
|
|
96
121
|
const hasSelectedAppPackage = computed(() => props.appPresets.some((app) => app.packageName === form.appPackage));
|
|
97
|
-
const
|
|
122
|
+
const scriptActivityMismatch = computed(() => {
|
|
123
|
+
const expectedActivity = selectedScript.value?.appActivity?.trim();
|
|
124
|
+
if (!expectedActivity) return false;
|
|
125
|
+
return currentActivity.value !== expectedActivity;
|
|
126
|
+
});
|
|
127
|
+
const recordingBusy = computed(() => (
|
|
128
|
+
recordingTap.value
|
|
129
|
+
|| resolvingNavigation.value
|
|
130
|
+
|| Boolean(pendingNavigation.value)
|
|
131
|
+
));
|
|
132
|
+
const recordingLocked = computed(() => recordingBusy.value || scriptActivityMismatch.value);
|
|
98
133
|
const overlayBounds = computed(() => flattenNodes(tree.value).flatMap((node) => (
|
|
99
134
|
node.bounds ? [{ id: node.id, ...node.bounds }] : []
|
|
100
135
|
)));
|
|
@@ -103,19 +138,103 @@ const selectedBounds = computed(() => {
|
|
|
103
138
|
return bounds && selectedNode.value ? { id: selectedNode.value.id, ...bounds } : undefined;
|
|
104
139
|
});
|
|
105
140
|
|
|
141
|
+
function stepScope(step?: AppiumRecordedStep): StepScope {
|
|
142
|
+
return step?.flow?.scope || 'main';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function insertionScope(index?: number): StepScope {
|
|
146
|
+
if (index === -2) return 'pre';
|
|
147
|
+
if (index === -3) return 'post';
|
|
148
|
+
return typeof index === 'number' && index >= 0 ? stepScope(steps.value[index]) : 'main';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function withStepScope(step: AppiumRecordedStep, scope: StepScope): AppiumRecordedStep {
|
|
152
|
+
if (scope === 'main') return step;
|
|
153
|
+
return {
|
|
154
|
+
...step,
|
|
155
|
+
flow: {
|
|
156
|
+
...(step.flow || {}),
|
|
157
|
+
scope,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function insertStep(step: AppiumRecordedStep, index?: number) {
|
|
163
|
+
const nextSteps = [...steps.value];
|
|
164
|
+
if (index === -2) {
|
|
165
|
+
const firstMainIndex = nextSteps.findIndex((item) => stepScope(item) !== 'pre');
|
|
166
|
+
const insertAt = firstMainIndex < 0 ? nextSteps.length : firstMainIndex;
|
|
167
|
+
nextSteps.splice(insertAt, 0, withStepScope(step, 'pre'));
|
|
168
|
+
} else if (index === -3) {
|
|
169
|
+
nextSteps.push(withStepScope(step, 'post'));
|
|
170
|
+
} else if (index === -1) {
|
|
171
|
+
const firstMainIndex = nextSteps.findIndex((item) => stepScope(item) !== 'pre');
|
|
172
|
+
nextSteps.splice(firstMainIndex < 0 ? nextSteps.length : firstMainIndex, 0, step);
|
|
173
|
+
} else {
|
|
174
|
+
const scope = typeof index === 'number' ? stepScope(nextSteps[index]) : 'main';
|
|
175
|
+
nextSteps.splice(typeof index === 'number' ? index + 1 : nextSteps.length, 0, withStepScope(step, scope));
|
|
176
|
+
}
|
|
177
|
+
steps.value = nextSteps;
|
|
178
|
+
return step;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function updateBranchTarget(stepId: string, branch: BranchName, targetId: string) {
|
|
182
|
+
const targetKey = branch === 'yes' ? 'yesTargetId' : 'noTargetId';
|
|
183
|
+
steps.value = steps.value.map((step) => (
|
|
184
|
+
step.id === stepId
|
|
185
|
+
? {
|
|
186
|
+
...step,
|
|
187
|
+
flow: {
|
|
188
|
+
...(step.flow || {}),
|
|
189
|
+
nodeKind: 'condition',
|
|
190
|
+
[targetKey]: targetId,
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
: step
|
|
194
|
+
));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function attachStepToBranch(stepId: string, conditionId: string, branch: BranchName) {
|
|
198
|
+
steps.value = steps.value.map((step) => (
|
|
199
|
+
step.id === stepId
|
|
200
|
+
? {
|
|
201
|
+
...step,
|
|
202
|
+
flow: {
|
|
203
|
+
...(step.flow || {}),
|
|
204
|
+
parentConditionId: conditionId,
|
|
205
|
+
parentBranch: branch,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
: step
|
|
209
|
+
));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function branchInsertIndex(conditionId: string, branch: BranchName, fallbackIndex: number) {
|
|
213
|
+
const ownedIndexes = steps.value.flatMap((step, index) => (
|
|
214
|
+
step.flow?.parentConditionId === conditionId && step.flow?.parentBranch === branch ? [index] : []
|
|
215
|
+
));
|
|
216
|
+
return ownedIndexes.length ? ownedIndexes[ownedIndexes.length - 1] : fallbackIndex;
|
|
217
|
+
}
|
|
218
|
+
|
|
106
219
|
function createStep(type: NodeStepType, node: AppiumNode): AppiumRecordedStep {
|
|
107
220
|
const labelMap = {
|
|
108
221
|
tap: '点击',
|
|
109
222
|
input: '输入',
|
|
110
223
|
assertExists: '断言存在',
|
|
224
|
+
waitFor: '等待出现',
|
|
111
225
|
};
|
|
112
226
|
return {
|
|
113
227
|
id: `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
114
228
|
type,
|
|
115
229
|
label: `${labelMap[type]} ${node.label}`,
|
|
116
230
|
selector: node.selector,
|
|
231
|
+
contextSelector: node.contextSelector,
|
|
232
|
+
selectorChain: node.xpath && node.selector.value !== node.xpath
|
|
233
|
+
? [{ strategy: 'xpath', value: node.xpath, unique: true, matchCount: 1 }]
|
|
234
|
+
: undefined,
|
|
117
235
|
fallback: node.bounds ? { strategy: 'bounds', centerX: node.bounds.centerX, centerY: node.bounds.centerY } : undefined,
|
|
118
|
-
timeoutMs: type === 'assertExists' ? 10000 : undefined,
|
|
236
|
+
timeoutMs: type === 'assertExists' || type === 'waitFor' ? 10000 : undefined,
|
|
237
|
+
pageBefore: currentPageSnapshot(node),
|
|
119
238
|
snapshot: {
|
|
120
239
|
text: node.text,
|
|
121
240
|
resourceId: node.resourceId,
|
|
@@ -125,9 +244,15 @@ function createStep(type: NodeStepType, node: AppiumNode): AppiumRecordedStep {
|
|
|
125
244
|
};
|
|
126
245
|
}
|
|
127
246
|
|
|
128
|
-
function
|
|
129
|
-
if (
|
|
130
|
-
|
|
247
|
+
async function copyReplayOutput() {
|
|
248
|
+
if (!replayOutput.value) return;
|
|
249
|
+
await navigator.clipboard.writeText(replayOutput.value);
|
|
250
|
+
ElMessage.success('回放输出已复制');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function clearReplayOutput() {
|
|
254
|
+
replayOutput.value = '';
|
|
255
|
+
replayOutputDialogVisible.value = false;
|
|
131
256
|
}
|
|
132
257
|
|
|
133
258
|
function createDelayStep(timeoutMs: number): AppiumRecordedStep {
|
|
@@ -150,8 +275,13 @@ function createNodeActionStep(
|
|
|
150
275
|
type,
|
|
151
276
|
label: `${label} ${node.label}`,
|
|
152
277
|
selector: node.selector,
|
|
278
|
+
contextSelector: node.contextSelector,
|
|
279
|
+
selectorChain: node.xpath && node.selector.value !== node.xpath
|
|
280
|
+
? [{ strategy: 'xpath', value: node.xpath, unique: true, matchCount: 1 }]
|
|
281
|
+
: undefined,
|
|
153
282
|
fallback: node.bounds ? { strategy: 'bounds', centerX: node.bounds.centerX, centerY: node.bounds.centerY } : undefined,
|
|
154
283
|
...options,
|
|
284
|
+
pageBefore: currentPageSnapshot(node),
|
|
155
285
|
snapshot: {
|
|
156
286
|
text: node.text,
|
|
157
287
|
resourceId: node.resourceId,
|
|
@@ -203,6 +333,37 @@ function createWaitActivityStep(activity: string): AppiumRecordedStep {
|
|
|
203
333
|
};
|
|
204
334
|
}
|
|
205
335
|
|
|
336
|
+
function createRunScriptStep(script: AppiumRecordedScript): AppiumRecordedStep {
|
|
337
|
+
return {
|
|
338
|
+
id: `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
339
|
+
type: 'runScript',
|
|
340
|
+
label: `连接脚本 ${script.name}`,
|
|
341
|
+
value: script.id,
|
|
342
|
+
flow: { nodeKind: 'action' },
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function linkedScriptExitActivity(script: AppiumRecordedScript) {
|
|
347
|
+
for (let index = script.steps.length - 1; index >= 0; index -= 1) {
|
|
348
|
+
const activity = script.steps[index]?.pageAfter?.activity;
|
|
349
|
+
if (activity) return activity;
|
|
350
|
+
}
|
|
351
|
+
return script.appActivity;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function expectedActivityAfterStep(index?: number) {
|
|
355
|
+
const scriptActivity = selectedScript.value?.appActivity || form.appActivity || currentActivity.value;
|
|
356
|
+
if (typeof index !== 'number' || index < 0) return scriptActivity;
|
|
357
|
+
|
|
358
|
+
const previousStep = steps.value[index];
|
|
359
|
+
if (!previousStep) return scriptActivity;
|
|
360
|
+
if (previousStep.type === 'runScript') {
|
|
361
|
+
const script = scripts.value.find((item) => item.id === previousStep.value);
|
|
362
|
+
if (script) return linkedScriptExitActivity(script);
|
|
363
|
+
}
|
|
364
|
+
return previousStep.pageAfter?.activity || previousStep.pageBefore?.activity || scriptActivity;
|
|
365
|
+
}
|
|
366
|
+
|
|
206
367
|
function wait(ms: number) {
|
|
207
368
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
208
369
|
}
|
|
@@ -220,6 +381,15 @@ function treeSignature(root: AppiumNode | null) {
|
|
|
220
381
|
.join('\n');
|
|
221
382
|
}
|
|
222
383
|
|
|
384
|
+
function currentPageSnapshot(node = selectedNode.value) {
|
|
385
|
+
return {
|
|
386
|
+
activity: currentActivity.value,
|
|
387
|
+
packageName: node?.packageName || '',
|
|
388
|
+
treeSignature: treeSignature(tree.value),
|
|
389
|
+
selectedNodePath: node?.xpath || '',
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
223
393
|
function ensureAppPackageSelected() {
|
|
224
394
|
if (hasSelectedAppPackage.value) return true;
|
|
225
395
|
ElMessage.warning('请先从预设 App 参数中选择 App 包名');
|
|
@@ -252,9 +422,6 @@ async function loadScripts() {
|
|
|
252
422
|
if (selectedScriptId.value && !scripts.value.some((script) => script.id === selectedScriptId.value)) {
|
|
253
423
|
selectedScriptId.value = '';
|
|
254
424
|
}
|
|
255
|
-
if (!selectedScriptId.value) {
|
|
256
|
-
selectedScriptId.value = scripts.value[0]?.id || '';
|
|
257
|
-
}
|
|
258
425
|
}
|
|
259
426
|
|
|
260
427
|
async function switchDevice(deviceId: string) {
|
|
@@ -320,7 +487,7 @@ async function swipePreview(gesture: {
|
|
|
320
487
|
}
|
|
321
488
|
}
|
|
322
489
|
|
|
323
|
-
async function recordTapStep() {
|
|
490
|
+
async function recordTapStep(index?: number) {
|
|
324
491
|
if (!selectedNode.value || recordingTap.value) return;
|
|
325
492
|
if (!ensureAppPackageSelected()) return;
|
|
326
493
|
if (!selectedDeviceId.value) {
|
|
@@ -334,40 +501,40 @@ async function recordTapStep() {
|
|
|
334
501
|
return;
|
|
335
502
|
}
|
|
336
503
|
const beforeActivity = currentActivity.value;
|
|
337
|
-
const beforeSignature = treeSignature(tree.value);
|
|
338
504
|
const step = createStep('tap', node);
|
|
339
|
-
|
|
505
|
+
insertStep(step, index);
|
|
340
506
|
recordingTap.value = true;
|
|
341
507
|
try {
|
|
342
508
|
await tapAppiumDevice({ deviceId: selectedDeviceId.value, x: bounds.centerX, y: bounds.centerY });
|
|
343
509
|
await wait(1200);
|
|
344
510
|
const after = await loadTreeSnapshot();
|
|
511
|
+
steps.value = steps.value.map((item) => (
|
|
512
|
+
item.id === step.id ? { ...item, pageAfter: currentPageSnapshot() } : item
|
|
513
|
+
));
|
|
345
514
|
const activityChanged = Boolean(beforeActivity && after.activity && beforeActivity !== after.activity);
|
|
346
|
-
|
|
347
|
-
if (activityChanged || treeChanged) {
|
|
515
|
+
if (activityChanged) {
|
|
348
516
|
pendingNavigation.value = {
|
|
349
517
|
beforeActivity,
|
|
350
518
|
afterActivity: after.activity,
|
|
351
|
-
beforeSignature,
|
|
352
|
-
afterSignature: after.signature,
|
|
353
519
|
};
|
|
354
|
-
return;
|
|
520
|
+
return step;
|
|
355
521
|
}
|
|
356
522
|
ElMessage.success('已录制点击');
|
|
523
|
+
return step;
|
|
357
524
|
} catch (error) {
|
|
358
525
|
ElMessage.error(error instanceof Error ? error.message : '录制点击失败');
|
|
526
|
+
return undefined;
|
|
359
527
|
} finally {
|
|
360
528
|
recordingTap.value = false;
|
|
361
529
|
}
|
|
362
530
|
}
|
|
363
531
|
|
|
364
|
-
async function addStep(type: NodeStepType) {
|
|
532
|
+
async function addStep(type: NodeStepType, index?: number) {
|
|
365
533
|
if (recordingLocked.value) return;
|
|
366
534
|
if (!selectedNode.value) return;
|
|
367
535
|
if (!ensureAppPackageSelected()) return;
|
|
368
536
|
if (type === 'tap') {
|
|
369
|
-
|
|
370
|
-
return;
|
|
537
|
+
return recordTapStep(index);
|
|
371
538
|
}
|
|
372
539
|
const step = createStep(type, selectedNode.value);
|
|
373
540
|
if (type === 'input') {
|
|
@@ -380,7 +547,7 @@ async function addStep(type: NodeStepType) {
|
|
|
380
547
|
if (!input) return;
|
|
381
548
|
step.value = input.value;
|
|
382
549
|
}
|
|
383
|
-
|
|
550
|
+
return insertStep(step, index);
|
|
384
551
|
}
|
|
385
552
|
|
|
386
553
|
async function addDelayStep(index?: number) {
|
|
@@ -395,37 +562,106 @@ async function addDelayStep(index?: number) {
|
|
|
395
562
|
if (!input) return;
|
|
396
563
|
const timeoutMs = Number(input.value);
|
|
397
564
|
const step = createDelayStep(timeoutMs);
|
|
398
|
-
|
|
399
|
-
nextSteps.splice(typeof index === 'number' ? index + 1 : nextSteps.length, 0, step);
|
|
400
|
-
steps.value = nextSteps;
|
|
565
|
+
return insertStep(step, index);
|
|
401
566
|
}
|
|
402
567
|
|
|
403
|
-
|
|
404
|
-
if (
|
|
568
|
+
function openLinkedScriptDialog(index?: number, branchTarget?: BranchTarget) {
|
|
569
|
+
if (!linkableScripts.value.length) {
|
|
570
|
+
ElMessage.warning('暂无可连接的已保存脚本');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
linkedScriptExpectedActivity.value = expectedActivityAfterStep(index);
|
|
574
|
+
if (!linkedScriptExpectedActivity.value) {
|
|
575
|
+
ElMessage.warning('无法确定当前插入点的 Activity,请先刷新组件树');
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (!compatibleLinkableScripts.value.length) {
|
|
579
|
+
ElMessage.warning(`没有入口 Activity 为 ${linkedScriptExpectedActivity.value} 的可连接脚本`);
|
|
580
|
+
linkedScriptExpectedActivity.value = '';
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
linkedScriptTargetId.value = compatibleLinkableScripts.value[0]?.id || '';
|
|
584
|
+
linkedScriptInsertIndex.value = index;
|
|
585
|
+
linkedScriptBranchTarget.value = branchTarget || null;
|
|
586
|
+
linkedScriptDialogVisible.value = true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function addLinkedScriptStep() {
|
|
590
|
+
const script = scripts.value.find((item) => item.id === linkedScriptTargetId.value);
|
|
591
|
+
if (!script) {
|
|
592
|
+
ElMessage.warning('请选择要连接的脚本');
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (!linkedScriptExpectedActivity.value || script.appActivity !== linkedScriptExpectedActivity.value) {
|
|
596
|
+
ElMessage.error(
|
|
597
|
+
`无法连接:插入点 Activity 为 ${linkedScriptExpectedActivity.value || '-'},`
|
|
598
|
+
+ `目标脚本入口 Activity 为 ${script.appActivity || '-'}`,
|
|
599
|
+
);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const step = insertStep(createRunScriptStep(script), linkedScriptInsertIndex.value);
|
|
603
|
+
if (linkedScriptBranchTarget.value) {
|
|
604
|
+
attachStepToBranch(step.id, linkedScriptBranchTarget.value.stepId, linkedScriptBranchTarget.value.branch);
|
|
605
|
+
if (linkedScriptBranchTarget.value.updateTarget) {
|
|
606
|
+
updateBranchTarget(
|
|
607
|
+
linkedScriptBranchTarget.value.stepId,
|
|
608
|
+
linkedScriptBranchTarget.value.branch,
|
|
609
|
+
step.id,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
linkedScriptInsertIndex.value = undefined;
|
|
614
|
+
linkedScriptBranchTarget.value = null;
|
|
615
|
+
linkedScriptExpectedActivity.value = '';
|
|
616
|
+
linkedScriptDialogVisible.value = false;
|
|
617
|
+
ElMessage.success('已添加连接脚本步骤');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function closeLinkedScriptDialog() {
|
|
621
|
+
linkedScriptDialogVisible.value = false;
|
|
622
|
+
linkedScriptInsertIndex.value = undefined;
|
|
623
|
+
linkedScriptBranchTarget.value = null;
|
|
624
|
+
linkedScriptExpectedActivity.value = '';
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function addAction(
|
|
628
|
+
action: RecorderAction,
|
|
629
|
+
index?: number,
|
|
630
|
+
branchTarget?: BranchTarget,
|
|
631
|
+
) {
|
|
632
|
+
if (recordingBusy.value) return;
|
|
633
|
+
if (scriptActivityMismatch.value && !['keyBack', 'runScript', 'launchApp', 'delay'].includes(action)) return;
|
|
405
634
|
if (!ensureAppPackageSelected()) return;
|
|
406
|
-
|
|
407
|
-
|
|
635
|
+
const scope = insertionScope(index);
|
|
636
|
+
if (scope !== 'main' && action !== 'launchApp' && action !== 'delay') {
|
|
637
|
+
ElMessage.warning('前置和后置操作仅支持启动 APP 和添加延时');
|
|
408
638
|
return;
|
|
409
639
|
}
|
|
410
|
-
if (
|
|
411
|
-
|
|
640
|
+
if (scope === 'main' && action === 'launchApp') {
|
|
641
|
+
ElMessage.warning('启动 APP 只能添加到前置或后置操作');
|
|
412
642
|
return;
|
|
413
643
|
}
|
|
414
|
-
if (action === '
|
|
415
|
-
|
|
644
|
+
if (action === 'runScript') {
|
|
645
|
+
openLinkedScriptDialog(index, branchTarget);
|
|
416
646
|
return;
|
|
417
647
|
}
|
|
648
|
+
if (action === 'delay') {
|
|
649
|
+
return addDelayStep(index);
|
|
650
|
+
}
|
|
651
|
+
if (action === 'tap' || action === 'input' || action === 'assertExists' || action === 'waitFor') {
|
|
652
|
+
return addStep(action, index);
|
|
653
|
+
}
|
|
654
|
+
if (action === 'keyBack') {
|
|
655
|
+
return insertStep(createKeyStep(4, '系统返回'), index);
|
|
656
|
+
}
|
|
418
657
|
if (action === 'keyHome') {
|
|
419
|
-
|
|
420
|
-
return;
|
|
658
|
+
return insertStep(createKeyStep(3, 'Home 键'), index);
|
|
421
659
|
}
|
|
422
660
|
if (action === 'keyRecent') {
|
|
423
|
-
|
|
424
|
-
return;
|
|
661
|
+
return insertStep(createKeyStep(187, '最近任务'), index);
|
|
425
662
|
}
|
|
426
663
|
if (action === 'keyPower') {
|
|
427
|
-
|
|
428
|
-
return;
|
|
664
|
+
return insertStep(createKeyStep(26, '电源键'), index);
|
|
429
665
|
}
|
|
430
666
|
if (action === 'waitActivity') {
|
|
431
667
|
const input = await ElMessageBox.prompt('请输入目标 Activity', '等待 Activity', {
|
|
@@ -433,7 +669,7 @@ async function addAction(action: RecorderAction) {
|
|
|
433
669
|
confirmButtonText: '添加',
|
|
434
670
|
cancelButtonText: '取消',
|
|
435
671
|
}).catch(() => null);
|
|
436
|
-
if (input?.value)
|
|
672
|
+
if (input?.value) return insertStep(createWaitActivityStep(input.value), index);
|
|
437
673
|
return;
|
|
438
674
|
}
|
|
439
675
|
if (action === 'swipe') {
|
|
@@ -444,7 +680,7 @@ async function addAction(action: RecorderAction) {
|
|
|
444
680
|
confirmButtonText: '添加',
|
|
445
681
|
cancelButtonText: '取消',
|
|
446
682
|
}).catch(() => null);
|
|
447
|
-
if (input?.value)
|
|
683
|
+
if (input?.value) return insertStep(createSwipeStep(input.value), index);
|
|
448
684
|
return;
|
|
449
685
|
}
|
|
450
686
|
if (action === 'pinch') {
|
|
@@ -457,7 +693,7 @@ async function addAction(action: RecorderAction) {
|
|
|
457
693
|
}).catch(() => null);
|
|
458
694
|
if (!input?.value) return;
|
|
459
695
|
const direction = input.value === '缩小' || input.value === 'in' ? 'in' : 'out';
|
|
460
|
-
|
|
696
|
+
return insertStep({
|
|
461
697
|
id: `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
462
698
|
type: 'pinch',
|
|
463
699
|
label: direction === 'out' ? '双指放大' : '双指缩小',
|
|
@@ -467,54 +703,73 @@ async function addAction(action: RecorderAction) {
|
|
|
467
703
|
centerY: Math.round((props.deviceHeight || 1920) / 2),
|
|
468
704
|
percent: 0.5,
|
|
469
705
|
},
|
|
470
|
-
}
|
|
471
|
-
return;
|
|
706
|
+
}, index);
|
|
472
707
|
}
|
|
473
708
|
if (action === 'launchApp') {
|
|
474
|
-
|
|
709
|
+
return insertStep({
|
|
475
710
|
id: `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
476
711
|
type: 'launchApp',
|
|
477
|
-
label: `启动
|
|
712
|
+
label: `启动 APP ${form.appPackage}`,
|
|
478
713
|
value: form.appPackage,
|
|
479
|
-
}
|
|
480
|
-
return;
|
|
714
|
+
}, index);
|
|
481
715
|
}
|
|
482
716
|
const node = getSelectedNode();
|
|
483
717
|
if (!node) return;
|
|
718
|
+
if (action === 'popupCondition') {
|
|
719
|
+
const step = createNodeActionStep('assertExists', '判断存在', node, { timeoutMs: 2000 });
|
|
720
|
+
const inserted = insertStep({ ...step, flow: { nodeKind: 'condition' } }, index);
|
|
721
|
+
ElMessage.success('已添加判断节点,请在节点面板配置是/否分支');
|
|
722
|
+
return inserted;
|
|
723
|
+
}
|
|
724
|
+
if (action === 'tapIfExists') {
|
|
725
|
+
return insertStep(createNodeActionStep('tapIfExists', '存在则点击', node, { timeoutMs: 2000 }), index);
|
|
726
|
+
}
|
|
727
|
+
if (action === 'inputIfExists') {
|
|
728
|
+
const input = await ElMessageBox.prompt('', '存在则输入', {
|
|
729
|
+
inputValue: '',
|
|
730
|
+
confirmButtonText: '添加',
|
|
731
|
+
cancelButtonText: '取消',
|
|
732
|
+
center: true,
|
|
733
|
+
}).catch(() => null);
|
|
734
|
+
if (!input) return;
|
|
735
|
+
return insertStep(createNodeActionStep('inputIfExists', '存在则输入', node, { timeoutMs: 2000, value: input.value }), index);
|
|
736
|
+
}
|
|
737
|
+
if (action === 'clearIfExists') {
|
|
738
|
+
return insertStep(createNodeActionStep('clearIfExists', '存在则清空', node, { timeoutMs: 2000 }), index);
|
|
739
|
+
}
|
|
740
|
+
if (action === 'backIfExists') {
|
|
741
|
+
return insertStep(createNodeActionStep('backIfExists', '存在则返回', node, { timeoutMs: 2000 }), index);
|
|
742
|
+
}
|
|
484
743
|
if (action === 'clearInput') {
|
|
485
|
-
|
|
486
|
-
return;
|
|
744
|
+
return insertStep(createNodeActionStep('clearInput', '清空输入', node), index);
|
|
487
745
|
}
|
|
488
746
|
if (action === 'coordinateTap') {
|
|
489
747
|
if (!node.bounds) {
|
|
490
748
|
ElMessage.warning('当前组件没有可点击坐标');
|
|
491
749
|
return;
|
|
492
750
|
}
|
|
493
|
-
|
|
751
|
+
return insertStep({
|
|
494
752
|
id: `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
495
753
|
type: 'coordinateTap',
|
|
496
754
|
label: `点击坐标 ${node.bounds.centerX},${node.bounds.centerY}`,
|
|
497
755
|
fallback: { strategy: 'bounds', centerX: node.bounds.centerX, centerY: node.bounds.centerY },
|
|
498
|
-
}
|
|
499
|
-
return;
|
|
756
|
+
}, index);
|
|
500
757
|
}
|
|
501
758
|
if (action === 'longPress') {
|
|
502
759
|
if (!node.bounds) {
|
|
503
760
|
ElMessage.warning('当前组件没有可长按坐标');
|
|
504
761
|
return;
|
|
505
762
|
}
|
|
506
|
-
|
|
763
|
+
return insertStep({
|
|
507
764
|
id: `step_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
508
765
|
type: 'longPress',
|
|
509
766
|
label: `长按 ${node.label}`,
|
|
510
767
|
fallback: { strategy: 'bounds', centerX: node.bounds.centerX, centerY: node.bounds.centerY },
|
|
511
768
|
timeoutMs: 800,
|
|
512
|
-
}
|
|
513
|
-
return;
|
|
769
|
+
}, index);
|
|
514
770
|
}
|
|
515
771
|
if (action === 'waitDisappear') {
|
|
516
|
-
|
|
517
|
-
return;
|
|
772
|
+
return insertStep(createNodeActionStep('waitDisappear', '等待元素消失', node, { timeoutMs: 10000 }), index);
|
|
518
773
|
}
|
|
519
774
|
if (action === 'assertText') {
|
|
520
775
|
const input = await ElMessageBox.prompt('请输入要断言的文本', '断言文本', {
|
|
@@ -522,82 +777,96 @@ async function addAction(action: RecorderAction) {
|
|
|
522
777
|
confirmButtonText: '添加',
|
|
523
778
|
cancelButtonText: '取消',
|
|
524
779
|
}).catch(() => null);
|
|
525
|
-
if (input?.value)
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
async function returnToPreviousPage() {
|
|
530
|
-
const pending = pendingNavigation.value;
|
|
531
|
-
if (!pending || resolvingNavigation.value) return;
|
|
532
|
-
resolvingNavigation.value = true;
|
|
533
|
-
try {
|
|
534
|
-
await pressAppiumDeviceKey({ deviceId: selectedDeviceId.value, keyCode: 4 });
|
|
535
|
-
let returned = false;
|
|
536
|
-
for (let index = 0; index < 6; index += 1) {
|
|
537
|
-
await wait(700);
|
|
538
|
-
const snapshot = await loadTreeSnapshot();
|
|
539
|
-
const activityReturned = !pending.beforeActivity || snapshot.activity === pending.beforeActivity;
|
|
540
|
-
const leftDestination = !pending.afterSignature || snapshot.signature !== pending.afterSignature;
|
|
541
|
-
if (activityReturned && leftDestination) {
|
|
542
|
-
returned = true;
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
if (!returned) throw new Error('设备页面未返回,请确认当前页面支持系统返回键');
|
|
547
|
-
steps.value = [
|
|
548
|
-
...steps.value,
|
|
549
|
-
createKeyStep(4),
|
|
550
|
-
...(pending.beforeActivity ? [createWaitActivityStep(pending.beforeActivity)] : []),
|
|
551
|
-
];
|
|
552
|
-
pendingNavigation.value = null;
|
|
553
|
-
ElMessage.success('已返回原页面,可继续录制');
|
|
554
|
-
} catch (error) {
|
|
555
|
-
ElMessage.error(error instanceof Error ? error.message : '返回原页面失败');
|
|
556
|
-
} finally {
|
|
557
|
-
resolvingNavigation.value = false;
|
|
780
|
+
if (input?.value) return insertStep(createNodeActionStep('assertText', '断言文本', node, { value: input.value }), index);
|
|
558
781
|
}
|
|
559
782
|
}
|
|
560
783
|
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
784
|
+
async function addBranchAction(index: number, branch: BranchName, action: RecorderAction) {
|
|
785
|
+
const source = steps.value[index];
|
|
786
|
+
if (!source) return;
|
|
787
|
+
const currentTargetId = branch === 'yes' ? source.flow?.yesTargetId : source.flow?.noTargetId;
|
|
788
|
+
const insertIndex = branchInsertIndex(source.id, branch, index);
|
|
789
|
+
const inserted = await addAction(action, insertIndex, {
|
|
790
|
+
stepId: source.id,
|
|
791
|
+
branch,
|
|
792
|
+
updateTarget: !currentTargetId,
|
|
793
|
+
});
|
|
794
|
+
if (!inserted) return;
|
|
795
|
+
attachStepToBranch(inserted.id, source.id, branch);
|
|
796
|
+
if (!currentTargetId) updateBranchTarget(source.id, branch, inserted.id);
|
|
565
797
|
}
|
|
566
798
|
|
|
567
|
-
async function
|
|
799
|
+
async function saveOnActivityChange() {
|
|
568
800
|
const pending = pendingNavigation.value;
|
|
569
801
|
if (!pending || resolvingNavigation.value) return;
|
|
570
802
|
if (!ensureAppPackageSelected()) return;
|
|
571
803
|
resolvingNavigation.value = true;
|
|
572
|
-
const activityName = (pending.
|
|
573
|
-
const
|
|
804
|
+
const activityName = (pending.beforeActivity || 'Activity').split(/[/.]/).filter(Boolean).pop() || 'Activity';
|
|
805
|
+
const scriptName = form.name.trim() || `${activityName}-${Date.now().toString(36).slice(-4)}`;
|
|
574
806
|
try {
|
|
575
807
|
const payload = await saveAppiumScript({
|
|
576
|
-
|
|
808
|
+
id: selectedScript.value?.id,
|
|
809
|
+
name: scriptName,
|
|
577
810
|
appPackage: form.appPackage,
|
|
578
|
-
appActivity: form.appActivity,
|
|
811
|
+
appActivity: pending.beforeActivity || form.appActivity,
|
|
579
812
|
deviceId: selectedDeviceId.value,
|
|
580
813
|
steps: steps.value,
|
|
581
814
|
});
|
|
582
815
|
await loadScripts();
|
|
583
|
-
selectedScriptId.value = payload.script.id;
|
|
584
|
-
form.name = payload.script.name;
|
|
585
816
|
pendingNavigation.value = null;
|
|
586
|
-
|
|
817
|
+
resetCurrentScript();
|
|
818
|
+
form.appActivity = pending.afterActivity;
|
|
819
|
+
ElMessage.success(`脚本「${payload.script.name}」已保存,可开始录制新 Activity`);
|
|
587
820
|
} catch (error) {
|
|
588
|
-
ElMessage.error(error instanceof Error ? error.message : '
|
|
821
|
+
ElMessage.error(error instanceof Error ? error.message : '保存脚本失败');
|
|
589
822
|
} finally {
|
|
590
823
|
resolvingNavigation.value = false;
|
|
591
824
|
}
|
|
592
825
|
}
|
|
593
826
|
|
|
594
827
|
function removeStep(index: number) {
|
|
595
|
-
|
|
828
|
+
if (recordingLocked.value) return;
|
|
829
|
+
const removed = steps.value[index];
|
|
830
|
+
if (!removed) return;
|
|
831
|
+
const nextBranchStep = removed.flow?.parentConditionId && removed.flow.parentBranch
|
|
832
|
+
? steps.value.slice(index + 1).find((step) => (
|
|
833
|
+
step.flow?.parentConditionId === removed.flow?.parentConditionId
|
|
834
|
+
&& step.flow?.parentBranch === removed.flow?.parentBranch
|
|
835
|
+
))
|
|
836
|
+
: undefined;
|
|
837
|
+
const replacementId = removed.flow?.parentConditionId
|
|
838
|
+
? nextBranchStep?.id || ''
|
|
839
|
+
: steps.value[index + 1]?.id || '';
|
|
840
|
+
steps.value = steps.value
|
|
841
|
+
.filter((_step, stepIndex) => stepIndex !== index)
|
|
842
|
+
.map((step) => {
|
|
843
|
+
if (!step.flow) return step;
|
|
844
|
+
const flow = { ...step.flow };
|
|
845
|
+
let changed = false;
|
|
846
|
+
(['yesTargetId', 'noTargetId', 'successTargetId', 'failureTargetId'] as const).forEach((key) => {
|
|
847
|
+
if (flow[key] !== removed.id) return;
|
|
848
|
+
flow[key] = replacementId;
|
|
849
|
+
changed = true;
|
|
850
|
+
});
|
|
851
|
+
if (flow.parentConditionId === removed.id) {
|
|
852
|
+
delete flow.parentConditionId;
|
|
853
|
+
delete flow.parentBranch;
|
|
854
|
+
changed = true;
|
|
855
|
+
}
|
|
856
|
+
return changed ? { ...step, flow } : step;
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function updateStep(index: number, step: AppiumRecordedStep) {
|
|
861
|
+
if (recordingLocked.value) return;
|
|
862
|
+
const nextSteps = [...steps.value];
|
|
863
|
+
nextSteps[index] = step;
|
|
864
|
+
steps.value = nextSteps;
|
|
596
865
|
}
|
|
597
866
|
|
|
598
867
|
async function editInputStep(index: number) {
|
|
599
868
|
const step = steps.value[index];
|
|
600
|
-
if (!step || step.type !== 'input' || recordingLocked.value) return;
|
|
869
|
+
if (!step || (step.type !== 'input' && step.type !== 'inputIfExists') || recordingLocked.value) return;
|
|
601
870
|
const input = await ElMessageBox.prompt('请输入新的录制文本', '修改输入内容', {
|
|
602
871
|
inputValue: step.value || '',
|
|
603
872
|
confirmButtonText: '保存',
|
|
@@ -623,7 +892,11 @@ function loadScript(script: AppiumRecordedScript) {
|
|
|
623
892
|
}
|
|
624
893
|
|
|
625
894
|
function loadSelectedScript() {
|
|
626
|
-
if (selectedScript.value)
|
|
895
|
+
if (selectedScript.value) {
|
|
896
|
+
loadScript(selectedScript.value);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
resetCurrentScript();
|
|
627
900
|
}
|
|
628
901
|
|
|
629
902
|
function resetCurrentScript() {
|
|
@@ -699,10 +972,10 @@ async function replayScript() {
|
|
|
699
972
|
replayOutput.value = '';
|
|
700
973
|
try {
|
|
701
974
|
const result = await replayAppiumScript({ id: selectedScript.value.id, deviceId: selectedDeviceId.value });
|
|
702
|
-
replayOutput.value =
|
|
975
|
+
replayOutput.value = result.output || '';
|
|
703
976
|
if (result.success) ElMessage.success('回放完成');
|
|
704
977
|
} catch (error) {
|
|
705
|
-
replayOutput.value =
|
|
978
|
+
replayOutput.value = error instanceof Error ? error.message : '回放失败';
|
|
706
979
|
ElMessage.error('回放失败,详情见回放输出');
|
|
707
980
|
} finally {
|
|
708
981
|
replaying.value = false;
|
|
@@ -711,6 +984,15 @@ async function replayScript() {
|
|
|
711
984
|
|
|
712
985
|
onMounted(loadScripts);
|
|
713
986
|
|
|
987
|
+
watch(() => form.appPackage, (packageName) => {
|
|
988
|
+
if (!packageName) return;
|
|
989
|
+
steps.value = steps.value.map((step) => (
|
|
990
|
+
step.type === 'launchApp'
|
|
991
|
+
? { ...step, label: `启动 APP ${packageName}`, value: packageName }
|
|
992
|
+
: step
|
|
993
|
+
));
|
|
994
|
+
});
|
|
995
|
+
|
|
714
996
|
watch(
|
|
715
997
|
() => [props.active, props.playgroundDeviceId] as const,
|
|
716
998
|
([active, deviceId]) => {
|
|
@@ -725,6 +1007,7 @@ watch(
|
|
|
725
1007
|
<div v-if="active" class="appium-header-actions">
|
|
726
1008
|
<el-select
|
|
727
1009
|
v-model="selectedScriptId"
|
|
1010
|
+
clearable
|
|
728
1011
|
placeholder="选择脚本"
|
|
729
1012
|
class="appium-header-actions__script"
|
|
730
1013
|
@change="loadSelectedScript"
|
|
@@ -803,11 +1086,30 @@ watch(
|
|
|
803
1086
|
/>
|
|
804
1087
|
</el-select>
|
|
805
1088
|
</div>
|
|
1089
|
+
<el-alert
|
|
1090
|
+
v-if="scriptActivityMismatch"
|
|
1091
|
+
class="appium-activity-lock-alert"
|
|
1092
|
+
title="当前脚本已锁定"
|
|
1093
|
+
type="warning"
|
|
1094
|
+
show-icon
|
|
1095
|
+
:closable="false"
|
|
1096
|
+
>
|
|
1097
|
+
<div class="appium-activity-lock-alert__details">
|
|
1098
|
+
<div>
|
|
1099
|
+
<strong>脚本绑定</strong>
|
|
1100
|
+
<code>{{ selectedScript?.appActivity || '-' }}</code>
|
|
1101
|
+
</div>
|
|
1102
|
+
<div>
|
|
1103
|
+
<strong>当前 Activity</strong>
|
|
1104
|
+
<code>{{ currentActivity || '正在获取' }}</code>
|
|
1105
|
+
</div>
|
|
1106
|
+
<p>当前仅允许插入“系统返回”或连接与插入点 Activity 匹配的脚本,其他流程编辑已锁定。</p>
|
|
1107
|
+
</div>
|
|
1108
|
+
</el-alert>
|
|
806
1109
|
<NodeDetail
|
|
807
1110
|
:node="selectedNode"
|
|
808
|
-
:
|
|
1111
|
+
:current-activity="currentActivity"
|
|
809
1112
|
:loading="recordingTap"
|
|
810
|
-
@add-action="addAction"
|
|
811
1113
|
/>
|
|
812
1114
|
</section>
|
|
813
1115
|
|
|
@@ -816,14 +1118,46 @@ watch(
|
|
|
816
1118
|
<RecordedSteps
|
|
817
1119
|
:steps="steps"
|
|
818
1120
|
:disabled="recordingLocked"
|
|
1121
|
+
:allowed-locked-actions="scriptActivityMismatch && !recordingBusy ? ['keyBack', 'runScript', 'launchApp', 'delay'] : []"
|
|
819
1122
|
@remove="removeStep"
|
|
820
1123
|
@add-delay="addDelayStep"
|
|
1124
|
+
@insert-action="(index, action) => addAction(action, index)"
|
|
1125
|
+
@insert-branch-action="addBranchAction"
|
|
821
1126
|
@edit-input="editInputStep"
|
|
1127
|
+
@update-step="updateStep"
|
|
822
1128
|
/>
|
|
823
1129
|
</section>
|
|
824
1130
|
|
|
825
1131
|
<section class="appium-workbench__section">
|
|
826
|
-
<
|
|
1132
|
+
<div class="appium-section-title">
|
|
1133
|
+
<h3>回放输出</h3>
|
|
1134
|
+
<div class="appium-section-title__actions">
|
|
1135
|
+
<el-button
|
|
1136
|
+
size="small"
|
|
1137
|
+
:icon="CopyDocument"
|
|
1138
|
+
:disabled="!replayOutput"
|
|
1139
|
+
@click="copyReplayOutput"
|
|
1140
|
+
>
|
|
1141
|
+
复制
|
|
1142
|
+
</el-button>
|
|
1143
|
+
<el-button
|
|
1144
|
+
size="small"
|
|
1145
|
+
:icon="FullScreen"
|
|
1146
|
+
:disabled="!replayOutput"
|
|
1147
|
+
@click="replayOutputDialogVisible = true"
|
|
1148
|
+
>
|
|
1149
|
+
展开查看
|
|
1150
|
+
</el-button>
|
|
1151
|
+
<el-button
|
|
1152
|
+
size="small"
|
|
1153
|
+
:icon="Delete"
|
|
1154
|
+
:disabled="!replayOutput"
|
|
1155
|
+
@click="clearReplayOutput"
|
|
1156
|
+
>
|
|
1157
|
+
清除
|
|
1158
|
+
</el-button>
|
|
1159
|
+
</div>
|
|
1160
|
+
</div>
|
|
827
1161
|
<pre class="appium-replay-output">{{ visibleReplayOutput }}</pre>
|
|
828
1162
|
</section>
|
|
829
1163
|
</div>
|
|
@@ -864,13 +1198,14 @@ watch(
|
|
|
864
1198
|
|
|
865
1199
|
<el-dialog
|
|
866
1200
|
:model-value="Boolean(pendingNavigation)"
|
|
867
|
-
title="
|
|
868
|
-
width="
|
|
1201
|
+
title="检测到 Activity 跳转"
|
|
1202
|
+
width="560px"
|
|
869
1203
|
:close-on-click-modal="false"
|
|
870
|
-
|
|
1204
|
+
:close-on-press-escape="false"
|
|
1205
|
+
:show-close="false"
|
|
871
1206
|
>
|
|
872
1207
|
<div class="appium-navigation-dialog">
|
|
873
|
-
<p
|
|
1208
|
+
<p>当前录制仅支持单个 Activity。检测到 Activity 已跳转,请先保存当前脚本。</p>
|
|
874
1209
|
<dl>
|
|
875
1210
|
<div>
|
|
876
1211
|
<dt>点击前</dt>
|
|
@@ -884,11 +1219,56 @@ watch(
|
|
|
884
1219
|
</div>
|
|
885
1220
|
<template #footer>
|
|
886
1221
|
<div class="appium-navigation-footer">
|
|
887
|
-
<el-button :
|
|
888
|
-
<el-button :loading="resolvingNavigation" @click="saveAsBranchScript">保存为新用例</el-button>
|
|
889
|
-
<el-button type="primary" :loading="resolvingNavigation" @click="returnToPreviousPage">添加返回键继续录制</el-button>
|
|
1222
|
+
<el-button type="primary" :loading="resolvingNavigation" @click="saveOnActivityChange">保存脚本</el-button>
|
|
890
1223
|
</div>
|
|
891
1224
|
</template>
|
|
892
1225
|
</el-dialog>
|
|
1226
|
+
|
|
1227
|
+
<el-dialog
|
|
1228
|
+
v-model="linkedScriptDialogVisible"
|
|
1229
|
+
title="连接脚本"
|
|
1230
|
+
width="520px"
|
|
1231
|
+
align-center
|
|
1232
|
+
@closed="closeLinkedScriptDialog"
|
|
1233
|
+
>
|
|
1234
|
+
<el-form label-position="top">
|
|
1235
|
+
<el-form-item label="插入点 Activity">
|
|
1236
|
+
<code class="appium-linked-script-activity">{{ linkedScriptExpectedActivity || '-' }}</code>
|
|
1237
|
+
</el-form-item>
|
|
1238
|
+
<el-form-item label="选择入口 Activity 匹配的脚本">
|
|
1239
|
+
<el-select
|
|
1240
|
+
v-model="linkedScriptTargetId"
|
|
1241
|
+
filterable
|
|
1242
|
+
placeholder="选择要连接的脚本"
|
|
1243
|
+
class="appium-linked-script-select"
|
|
1244
|
+
>
|
|
1245
|
+
<el-option
|
|
1246
|
+
v-for="script in compatibleLinkableScripts"
|
|
1247
|
+
:key="script.id"
|
|
1248
|
+
:label="`${script.name} · ${script.steps.length} 步`"
|
|
1249
|
+
:value="script.id"
|
|
1250
|
+
/>
|
|
1251
|
+
</el-select>
|
|
1252
|
+
</el-form-item>
|
|
1253
|
+
</el-form>
|
|
1254
|
+
<template #footer>
|
|
1255
|
+
<el-button @click="closeLinkedScriptDialog">取消</el-button>
|
|
1256
|
+
<el-button type="primary" @click="addLinkedScriptStep">添加连接</el-button>
|
|
1257
|
+
</template>
|
|
1258
|
+
</el-dialog>
|
|
1259
|
+
|
|
1260
|
+
<el-dialog
|
|
1261
|
+
v-model="replayOutputDialogVisible"
|
|
1262
|
+
title="回放输出"
|
|
1263
|
+
width="82vw"
|
|
1264
|
+
top="6vh"
|
|
1265
|
+
>
|
|
1266
|
+
<pre class="appium-replay-output appium-replay-output--dialog">{{ replayOutput }}</pre>
|
|
1267
|
+
<template #footer>
|
|
1268
|
+
<el-button :icon="CopyDocument" :disabled="!replayOutput" @click="copyReplayOutput">复制</el-button>
|
|
1269
|
+
<el-button :icon="Delete" :disabled="!replayOutput" @click="clearReplayOutput">清除</el-button>
|
|
1270
|
+
<el-button type="primary" @click="replayOutputDialogVisible = false">关闭</el-button>
|
|
1271
|
+
</template>
|
|
1272
|
+
</el-dialog>
|
|
893
1273
|
</section>
|
|
894
1274
|
</template>
|