android-midscene-automation 0.1.26 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/FAQ.md +14 -0
- package/README.md +31 -3
- package/bin/android-midscene-automation.js +71 -1
- package/package.json +4 -1
- package/server/appium-recorder/appium-runner.ts +273 -28
- package/server/appium-recorder/report.ts +186 -3
- package/server/appium-recorder/repository.ts +24 -3
- package/server/appium-recorder/visual-change.ts +184 -0
- package/server/http-api.ts +3 -4
- package/src/appium-recorder/AppiumPage.vue +465 -139
- package/src/appium-recorder/api.ts +1 -0
- package/src/appium-recorder/components/FlowCanvas.vue +14 -1
- package/src/appium-recorder/components/FlowStepEditor.vue +121 -2
- package/src/appium-recorder/components/NestedConditionBranches.vue +26 -4
- package/src/appium-recorder/components/RecordedSteps.vue +13 -1
- package/src/appium-recorder/components/VisualChangeDialog.vue +151 -0
- package/src/appium-recorder/flow-copy.ts +47 -1
- package/src/appium-recorder/flow-graph.ts +101 -6
- package/src/appium-recorder/flow-labels.ts +6 -1
- package/src/appium-recorder/flow-normalize.ts +67 -0
- package/src/appium-recorder/flow-remove.ts +67 -0
- package/src/appium-recorder/types.ts +25 -1
- package/src/appium-recorder/visual-change-pairs.ts +147 -0
- package/src/appium-recorder/visual-change.ts +75 -0
- package/src/components/device/DevicePreviewPanel.vue +148 -4
- package/src/style.css +39 -0
|
@@ -118,7 +118,19 @@ function handlePaneReady() {
|
|
|
118
118
|
void nextTick(() => scheduleResetView());
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
function flowStructureKey() {
|
|
122
|
+
return props.steps.map((step) => [
|
|
123
|
+
step.id,
|
|
124
|
+
step.flow?.nodeKind || '',
|
|
125
|
+
step.flow?.parentConditionId || '',
|
|
126
|
+
step.flow?.parentBranch || '',
|
|
127
|
+
step.flow?.yesTargetId || '',
|
|
128
|
+
step.flow?.noTargetId || '',
|
|
129
|
+
step.flow?.successTargetId || '',
|
|
130
|
+
].join(':')).join('|');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
watch(flowStructureKey, () => {
|
|
122
134
|
measuredNodeHeights.value = {};
|
|
123
135
|
void nextTick(scheduleNodeInternalsUpdate);
|
|
124
136
|
});
|
|
@@ -168,6 +180,7 @@ function handleInsert(payload: {
|
|
|
168
180
|
:nodes-draggable="false"
|
|
169
181
|
:nodes-connectable="false"
|
|
170
182
|
:elements-selectable="false"
|
|
183
|
+
:only-render-visible-elements="false"
|
|
171
184
|
:pan-on-drag="true"
|
|
172
185
|
:zoom-on-scroll="true"
|
|
173
186
|
zoom-activation-key-code="Control"
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
2
3
|
import type { AppiumRecordedStep, AppiumSelector } from '../types';
|
|
4
|
+
import { normalizeVisualChangeConfig } from '../visual-change';
|
|
3
5
|
|
|
4
6
|
type FlowKind = 'action' | 'condition' | 'assertion';
|
|
5
7
|
type SwipeGesture = NonNullable<AppiumRecordedStep['swipe']>;
|
|
8
|
+
type VisualChangeConfig = NonNullable<AppiumRecordedStep['visualChange']>;
|
|
6
9
|
|
|
7
10
|
const props = defineProps<{
|
|
8
11
|
step: AppiumRecordedStep;
|
|
@@ -14,9 +17,21 @@ const emit = defineEmits<{
|
|
|
14
17
|
update: [payload: { index: number; step: AppiumRecordedStep }];
|
|
15
18
|
}>();
|
|
16
19
|
|
|
20
|
+
const visualChangeConfig = computed(() => normalizeVisualChangeConfig(props.step.visualChange));
|
|
21
|
+
const visualChangeRoleLabel = computed(() => (
|
|
22
|
+
visualChangeConfig.value.role === 'start'
|
|
23
|
+
? `${visualChangeConfig.value.pairLabel || '检测画面变化'} · 开始节点`
|
|
24
|
+
: visualChangeConfig.value.role === 'end'
|
|
25
|
+
? `${visualChangeConfig.value.pairLabel || '检测画面变化'} · 结束节点`
|
|
26
|
+
: '起止节点截图对比'
|
|
27
|
+
));
|
|
28
|
+
const visualChangeConfigDisabled = computed(() => Boolean(
|
|
29
|
+
props.disabled || visualChangeConfig.value.role === 'end',
|
|
30
|
+
));
|
|
31
|
+
|
|
17
32
|
function defaultKind(): FlowKind {
|
|
18
33
|
if (props.step.flow?.nodeKind) return props.step.flow.nodeKind;
|
|
19
|
-
if (props.step.type === 'assertExists' || props.step.type === 'assertText') return 'assertion';
|
|
34
|
+
if (props.step.type === 'assertExists' || props.step.type === 'assertText' || props.step.type === 'visualChange') return 'assertion';
|
|
20
35
|
return 'action';
|
|
21
36
|
}
|
|
22
37
|
|
|
@@ -55,6 +70,25 @@ function patchSwipe(patch: Partial<SwipeGesture>) {
|
|
|
55
70
|
});
|
|
56
71
|
}
|
|
57
72
|
|
|
73
|
+
function patchVisualChange(patch: Partial<VisualChangeConfig>) {
|
|
74
|
+
patchStep({
|
|
75
|
+
visualChange: normalizeVisualChangeConfig({
|
|
76
|
+
...normalizeVisualChangeConfig(props.step.visualChange),
|
|
77
|
+
...patch,
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function patchVisualRegion(key: keyof VisualChangeConfig['region'], value: unknown) {
|
|
83
|
+
const current = normalizeVisualChangeConfig(props.step.visualChange);
|
|
84
|
+
patchVisualChange({
|
|
85
|
+
region: {
|
|
86
|
+
...current.region,
|
|
87
|
+
[key]: Number(value),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
58
92
|
function timeoutLabel() {
|
|
59
93
|
return props.step.type === 'longPress' ? '长按时间 ms' : '超时时间 ms';
|
|
60
94
|
}
|
|
@@ -121,6 +155,91 @@ function patchTimeout(value: unknown) {
|
|
|
121
155
|
@update:model-value="patchStep({ value: String($event) })"
|
|
122
156
|
/>
|
|
123
157
|
</el-form-item>
|
|
158
|
+
<template v-if="step.type === 'visualChange'">
|
|
159
|
+
<div class="appium-flow-editor__grid">
|
|
160
|
+
<el-form-item label="检测标记">
|
|
161
|
+
<el-input :model-value="visualChangeRoleLabel" disabled />
|
|
162
|
+
</el-form-item>
|
|
163
|
+
<el-form-item label="检测目标">
|
|
164
|
+
<el-select
|
|
165
|
+
:model-value="visualChangeConfig.mode"
|
|
166
|
+
:disabled="visualChangeConfigDisabled"
|
|
167
|
+
@update:model-value="patchVisualChange({ mode: $event as VisualChangeConfig['mode'] })"
|
|
168
|
+
>
|
|
169
|
+
<el-option label="当前选中元素" value="selectedElement" />
|
|
170
|
+
<el-option label="手动区域" value="region" />
|
|
171
|
+
</el-select>
|
|
172
|
+
</el-form-item>
|
|
173
|
+
<el-form-item label="变化阈值 %">
|
|
174
|
+
<el-input-number
|
|
175
|
+
:model-value="visualChangeConfig.changeRatioThreshold"
|
|
176
|
+
:disabled="visualChangeConfigDisabled"
|
|
177
|
+
:min="0.01"
|
|
178
|
+
:max="100"
|
|
179
|
+
:step="0.1"
|
|
180
|
+
controls-position="right"
|
|
181
|
+
@update:model-value="patchVisualChange({ changeRatioThreshold: Number($event) })"
|
|
182
|
+
/>
|
|
183
|
+
</el-form-item>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="appium-flow-editor__grid appium-flow-editor__grid--visual">
|
|
186
|
+
<el-form-item label="X">
|
|
187
|
+
<el-input-number
|
|
188
|
+
:model-value="visualChangeConfig.region.x"
|
|
189
|
+
:disabled="visualChangeConfigDisabled"
|
|
190
|
+
:min="0"
|
|
191
|
+
:max="99999"
|
|
192
|
+
:precision="0"
|
|
193
|
+
controls-position="right"
|
|
194
|
+
@update:model-value="patchVisualRegion('x', $event)"
|
|
195
|
+
/>
|
|
196
|
+
</el-form-item>
|
|
197
|
+
<el-form-item label="Y">
|
|
198
|
+
<el-input-number
|
|
199
|
+
:model-value="visualChangeConfig.region.y"
|
|
200
|
+
:disabled="visualChangeConfigDisabled"
|
|
201
|
+
:min="0"
|
|
202
|
+
:max="99999"
|
|
203
|
+
:precision="0"
|
|
204
|
+
controls-position="right"
|
|
205
|
+
@update:model-value="patchVisualRegion('y', $event)"
|
|
206
|
+
/>
|
|
207
|
+
</el-form-item>
|
|
208
|
+
<el-form-item label="宽度">
|
|
209
|
+
<el-input-number
|
|
210
|
+
:model-value="visualChangeConfig.region.width"
|
|
211
|
+
:disabled="visualChangeConfigDisabled"
|
|
212
|
+
:min="1"
|
|
213
|
+
:max="99999"
|
|
214
|
+
:precision="0"
|
|
215
|
+
controls-position="right"
|
|
216
|
+
@update:model-value="patchVisualRegion('width', $event)"
|
|
217
|
+
/>
|
|
218
|
+
</el-form-item>
|
|
219
|
+
<el-form-item label="高度">
|
|
220
|
+
<el-input-number
|
|
221
|
+
:model-value="visualChangeConfig.region.height"
|
|
222
|
+
:disabled="visualChangeConfigDisabled"
|
|
223
|
+
:min="1"
|
|
224
|
+
:max="99999"
|
|
225
|
+
:precision="0"
|
|
226
|
+
controls-position="right"
|
|
227
|
+
@update:model-value="patchVisualRegion('height', $event)"
|
|
228
|
+
/>
|
|
229
|
+
</el-form-item>
|
|
230
|
+
<el-form-item label="像素容差">
|
|
231
|
+
<el-input-number
|
|
232
|
+
:model-value="visualChangeConfig.pixelmatchThreshold"
|
|
233
|
+
:disabled="visualChangeConfigDisabled"
|
|
234
|
+
:min="0"
|
|
235
|
+
:max="1"
|
|
236
|
+
:step="0.01"
|
|
237
|
+
controls-position="right"
|
|
238
|
+
@update:model-value="patchVisualChange({ pixelmatchThreshold: Number($event) })"
|
|
239
|
+
/>
|
|
240
|
+
</el-form-item>
|
|
241
|
+
</div>
|
|
242
|
+
</template>
|
|
124
243
|
<div v-if="step.type === 'swipe'" class="appium-flow-editor__grid appium-flow-editor__grid--swipe">
|
|
125
244
|
<el-form-item label="起点 X">
|
|
126
245
|
<el-input-number
|
|
@@ -222,7 +341,7 @@ function patchTimeout(value: unknown) {
|
|
|
222
341
|
/>
|
|
223
342
|
</el-form-item>
|
|
224
343
|
</div>
|
|
225
|
-
<el-form-item label="可选步骤">
|
|
344
|
+
<el-form-item v-if="step.type !== 'visualChange'" label="可选步骤">
|
|
226
345
|
<el-switch
|
|
227
346
|
:model-value="Boolean(step.optional)"
|
|
228
347
|
:disabled="disabled"
|
|
@@ -32,7 +32,11 @@ type InsertAction =
|
|
|
32
32
|
| 'assertText'
|
|
33
33
|
| 'waitDisappear'
|
|
34
34
|
| 'waitActivity'
|
|
35
|
-
| 'runScript'
|
|
35
|
+
| 'runScript'
|
|
36
|
+
| 'noop'
|
|
37
|
+
| 'visualChangeStart'
|
|
38
|
+
| 'visualChangeEnd'
|
|
39
|
+
| 'visualChange';
|
|
36
40
|
|
|
37
41
|
const PASTE_COMMAND = '__paste_flow_nodes__';
|
|
38
42
|
|
|
@@ -87,6 +91,7 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
87
91
|
{ type: 'keyPower', label: '电源键' },
|
|
88
92
|
{ type: 'swipe', label: '滑动' },
|
|
89
93
|
{ type: 'pinch', label: '双指缩放' },
|
|
94
|
+
{ type: 'noop', label: '空节点' },
|
|
90
95
|
],
|
|
91
96
|
},
|
|
92
97
|
{
|
|
@@ -103,6 +108,8 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
103
108
|
{ type: 'assertText', label: '断言文本' },
|
|
104
109
|
{ type: 'waitDisappear', label: '等待元素消失' },
|
|
105
110
|
{ type: 'waitActivity', label: '等待 Activity' },
|
|
111
|
+
{ type: 'visualChangeStart', label: '检测画面变化开始节点' },
|
|
112
|
+
{ type: 'visualChangeEnd', label: '检测画面变化结束节点' },
|
|
106
113
|
],
|
|
107
114
|
},
|
|
108
115
|
{ title: '流程控制', actions: [{ type: 'runScript', label: '连接脚本' }] },
|
|
@@ -110,7 +117,7 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
110
117
|
|
|
111
118
|
function defaultKind(step: AppiumRecordedStep): FlowKind {
|
|
112
119
|
if (step.flow?.nodeKind) return step.flow.nodeKind;
|
|
113
|
-
if (step.type === 'assertExists' || step.type === 'assertText') return 'assertion';
|
|
120
|
+
if (step.type === 'assertExists' || step.type === 'assertText' || step.type === 'visualChange') return 'assertion';
|
|
114
121
|
return 'action';
|
|
115
122
|
}
|
|
116
123
|
|
|
@@ -125,7 +132,8 @@ function typeLabel(step: AppiumRecordedStep) {
|
|
|
125
132
|
waitFor: '等待出现', waitDisappear: '等待消失', assertExists: '断言存在',
|
|
126
133
|
assertText: '断言文本', key: '按键', waitActivity: '等待 Activity', delay: '延时',
|
|
127
134
|
coordinateTap: '坐标点击', swipe: '滑动', launchApp: '启动 APP', clearAppData: '清理 APP 缓存', longPress: '长按',
|
|
128
|
-
pinch: '双指缩放', runScript: '连接脚本', screenshot: '截图',
|
|
135
|
+
pinch: '双指缩放', runScript: '连接脚本', screenshot: '截图', visualChange: '检测画面变化',
|
|
136
|
+
noop: '空节点',
|
|
129
137
|
};
|
|
130
138
|
return labels[step.type] || step.type;
|
|
131
139
|
}
|
|
@@ -139,6 +147,14 @@ function stepMeta(step: AppiumRecordedStep) {
|
|
|
139
147
|
const swipe = step.swipe;
|
|
140
148
|
return swipe ? `[${swipe.startX},${swipe.startY}] -> [${swipe.endX},${swipe.endY}]` : '';
|
|
141
149
|
}
|
|
150
|
+
if (step.type === 'visualChange') {
|
|
151
|
+
const config = step.visualChange;
|
|
152
|
+
if (!config) return '';
|
|
153
|
+
if (config.role === 'start') return `${config.pairLabel || '检测画面变化'} · 开始 · 阈值 ${config.changeRatioThreshold}%`;
|
|
154
|
+
if (config.role === 'end') return `${config.pairLabel || '检测画面变化'} · 结束`;
|
|
155
|
+
return `${config.startStepId && config.endStepId ? '起止节点截图对比' : '请选择起止节点'} · 阈值 ${config.changeRatioThreshold}%`;
|
|
156
|
+
}
|
|
157
|
+
if (step.type === 'noop') return '';
|
|
142
158
|
if (defaultKind(step) === 'condition' && step.value) {
|
|
143
159
|
return `${step.flow?.textMatch === 'exact' ? '精准匹配' : '模糊匹配'}:${step.value}`;
|
|
144
160
|
}
|
|
@@ -199,6 +215,12 @@ function isActionDisabled(action: InsertAction) {
|
|
|
199
215
|
return Boolean(props.disabled && !props.allowedLockedActions?.includes(action));
|
|
200
216
|
}
|
|
201
217
|
|
|
218
|
+
function canCopyStep(step: AppiumRecordedStep) {
|
|
219
|
+
return step.type !== 'launchApp'
|
|
220
|
+
&& step.type !== 'clearAppData'
|
|
221
|
+
&& step.visualChange?.role !== 'end';
|
|
222
|
+
}
|
|
223
|
+
|
|
202
224
|
function insert(branch: BranchName, command: string | number | object) {
|
|
203
225
|
if (command === PASTE_COMMAND) emit('paste', props.conditionIndex, branch);
|
|
204
226
|
else emit('insertBranchAction', props.conditionIndex, branch, command as InsertAction);
|
|
@@ -275,7 +297,7 @@ function branchSplitPath() {
|
|
|
275
297
|
</button>
|
|
276
298
|
<span class="appium-flow-node__actions" @click.stop>
|
|
277
299
|
<el-button
|
|
278
|
-
v-if="!copyMode"
|
|
300
|
+
v-if="!copyMode && canCopyStep(item.step)"
|
|
279
301
|
text
|
|
280
302
|
size="small"
|
|
281
303
|
:icon="CopyDocument"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, shallowRef } from 'vue';
|
|
2
|
+
import { computed, nextTick, shallowRef, watch } from 'vue';
|
|
3
3
|
import { ElMessage } from 'element-plus';
|
|
4
4
|
import { Aim, CopyDocument, FullScreen } from '@element-plus/icons-vue';
|
|
5
5
|
import type { AppiumRecordedStep } from '../types';
|
|
@@ -35,6 +35,7 @@ const copyMode = shallowRef(false);
|
|
|
35
35
|
const selectedCopyIndexes = shallowRef<number[]>([]);
|
|
36
36
|
const flowDialogVisible = shallowRef(false);
|
|
37
37
|
const mainResetViewToken = shallowRef(0);
|
|
38
|
+
const dialogResetViewToken = shallowRef(0);
|
|
38
39
|
|
|
39
40
|
const insertActionGroups: FlowActionGroup[] = [
|
|
40
41
|
{
|
|
@@ -56,6 +57,7 @@ const insertActionGroups: FlowActionGroup[] = [
|
|
|
56
57
|
{ type: 'keyPower', label: '电源键' },
|
|
57
58
|
{ type: 'swipe', label: '滑动' },
|
|
58
59
|
{ type: 'pinch', label: '双指缩放' },
|
|
60
|
+
{ type: 'noop', label: '空节点' },
|
|
59
61
|
{ type: 'clearAppData', label: '清理 App 缓存' },
|
|
60
62
|
{ type: 'launchApp', label: '启动 App' },
|
|
61
63
|
],
|
|
@@ -74,6 +76,8 @@ const insertActionGroups: FlowActionGroup[] = [
|
|
|
74
76
|
{ type: 'assertText', label: '断言文本' },
|
|
75
77
|
{ type: 'waitDisappear', label: '等待元素消失' },
|
|
76
78
|
{ type: 'waitActivity', label: '等待 Activity' },
|
|
79
|
+
{ type: 'visualChangeStart', label: '检测画面变化开始节点' },
|
|
80
|
+
{ type: 'visualChangeEnd', label: '检测画面变化结束节点' },
|
|
77
81
|
],
|
|
78
82
|
},
|
|
79
83
|
{
|
|
@@ -169,6 +173,13 @@ function resetMainFlowPosition() {
|
|
|
169
173
|
mainResetViewToken.value += 1;
|
|
170
174
|
}
|
|
171
175
|
|
|
176
|
+
watch(flowDialogVisible, (visible) => {
|
|
177
|
+
if (!visible) return;
|
|
178
|
+
void nextTick(() => {
|
|
179
|
+
dialogResetViewToken.value += 1;
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
172
183
|
function canOpenInsertMenu() {
|
|
173
184
|
return !props.disabled || Boolean(props.allowedLockedActions?.length);
|
|
174
185
|
}
|
|
@@ -308,6 +319,7 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
308
319
|
:remove-disabled="removeDisabled"
|
|
309
320
|
:launching-step-id="launchingStepId"
|
|
310
321
|
:clipboard-count="clipboardCount"
|
|
322
|
+
:reset-view-token="dialogResetViewToken"
|
|
311
323
|
:start-action-groups="startActionGroups"
|
|
312
324
|
:main-action-groups="mainActionGroups"
|
|
313
325
|
:can-open-insert-menu="canOpenInsertMenu()"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { Aim } from '@element-plus/icons-vue';
|
|
4
|
+
import type { AppiumVisualChangeConfig } from '../types';
|
|
5
|
+
import { normalizeVisualChangeConfig } from '../visual-change';
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
modelValue: boolean;
|
|
9
|
+
config: AppiumVisualChangeConfig;
|
|
10
|
+
hasSelectedElement: boolean;
|
|
11
|
+
pickingRegion?: boolean;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
'update:modelValue': [value: boolean];
|
|
16
|
+
'update:config': [value: AppiumVisualChangeConfig];
|
|
17
|
+
pickRegion: [];
|
|
18
|
+
confirm: [];
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
const normalizedConfig = computed(() => normalizeVisualChangeConfig(props.config));
|
|
22
|
+
|
|
23
|
+
function patchConfig(patch: Partial<AppiumVisualChangeConfig>) {
|
|
24
|
+
emit('update:config', normalizeVisualChangeConfig({
|
|
25
|
+
...normalizedConfig.value,
|
|
26
|
+
...patch,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function patchRegion(key: keyof AppiumVisualChangeConfig['region'], value: unknown) {
|
|
31
|
+
patchConfig({
|
|
32
|
+
region: {
|
|
33
|
+
...normalizedConfig.value.region,
|
|
34
|
+
[key]: Number(value),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<el-dialog
|
|
42
|
+
:model-value="modelValue"
|
|
43
|
+
title="检测画面变化"
|
|
44
|
+
width="480px"
|
|
45
|
+
top="9vh"
|
|
46
|
+
class="appium-visual-change-dialog"
|
|
47
|
+
append-to-body
|
|
48
|
+
draggable
|
|
49
|
+
:modal="false"
|
|
50
|
+
modal-penetrable
|
|
51
|
+
:close-on-click-modal="false"
|
|
52
|
+
:close-on-press-escape="false"
|
|
53
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
54
|
+
>
|
|
55
|
+
<el-form label-position="top" size="small" class="appium-visual-change-form">
|
|
56
|
+
<el-form-item label="检测目标">
|
|
57
|
+
<el-radio-group
|
|
58
|
+
:model-value="normalizedConfig.mode"
|
|
59
|
+
@update:model-value="patchConfig({ mode: $event as AppiumVisualChangeConfig['mode'] })"
|
|
60
|
+
>
|
|
61
|
+
<el-radio-button label="selectedElement" :disabled="!hasSelectedElement">
|
|
62
|
+
当前选中元素
|
|
63
|
+
</el-radio-button>
|
|
64
|
+
<el-radio-button label="region">手动框选区域</el-radio-button>
|
|
65
|
+
</el-radio-group>
|
|
66
|
+
</el-form-item>
|
|
67
|
+
|
|
68
|
+
<el-alert
|
|
69
|
+
v-if="normalizedConfig.mode === 'selectedElement' && !hasSelectedElement"
|
|
70
|
+
type="warning"
|
|
71
|
+
:closable="false"
|
|
72
|
+
show-icon
|
|
73
|
+
title="当前没有可用的选中元素,请先选择组件或改为手动框选区域"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<el-form-item v-if="normalizedConfig.mode === 'region'" label="框选区域">
|
|
77
|
+
<el-button :icon="Aim" :loading="pickingRegion" @click="emit('pickRegion')">
|
|
78
|
+
{{ pickingRegion ? '请在设备预览拖拽' : '框选/拖动区域' }}
|
|
79
|
+
</el-button>
|
|
80
|
+
</el-form-item>
|
|
81
|
+
|
|
82
|
+
<div class="appium-visual-change-form__grid">
|
|
83
|
+
<el-form-item label="X">
|
|
84
|
+
<el-input-number
|
|
85
|
+
:model-value="normalizedConfig.region.x"
|
|
86
|
+
:min="0"
|
|
87
|
+
:max="99999"
|
|
88
|
+
:precision="0"
|
|
89
|
+
controls-position="right"
|
|
90
|
+
@update:model-value="patchRegion('x', $event)"
|
|
91
|
+
/>
|
|
92
|
+
</el-form-item>
|
|
93
|
+
<el-form-item label="Y">
|
|
94
|
+
<el-input-number
|
|
95
|
+
:model-value="normalizedConfig.region.y"
|
|
96
|
+
:min="0"
|
|
97
|
+
:max="99999"
|
|
98
|
+
:precision="0"
|
|
99
|
+
controls-position="right"
|
|
100
|
+
@update:model-value="patchRegion('y', $event)"
|
|
101
|
+
/>
|
|
102
|
+
</el-form-item>
|
|
103
|
+
<el-form-item label="宽度">
|
|
104
|
+
<el-input-number
|
|
105
|
+
:model-value="normalizedConfig.region.width"
|
|
106
|
+
:min="1"
|
|
107
|
+
:max="99999"
|
|
108
|
+
:precision="0"
|
|
109
|
+
controls-position="right"
|
|
110
|
+
@update:model-value="patchRegion('width', $event)"
|
|
111
|
+
/>
|
|
112
|
+
</el-form-item>
|
|
113
|
+
<el-form-item label="高度">
|
|
114
|
+
<el-input-number
|
|
115
|
+
:model-value="normalizedConfig.region.height"
|
|
116
|
+
:min="1"
|
|
117
|
+
:max="99999"
|
|
118
|
+
:precision="0"
|
|
119
|
+
controls-position="right"
|
|
120
|
+
@update:model-value="patchRegion('height', $event)"
|
|
121
|
+
/>
|
|
122
|
+
</el-form-item>
|
|
123
|
+
<el-form-item label="变化阈值 %">
|
|
124
|
+
<el-input-number
|
|
125
|
+
:model-value="normalizedConfig.changeRatioThreshold"
|
|
126
|
+
:min="0.01"
|
|
127
|
+
:max="100"
|
|
128
|
+
:step="0.1"
|
|
129
|
+
controls-position="right"
|
|
130
|
+
@update:model-value="patchConfig({ changeRatioThreshold: Number($event) })"
|
|
131
|
+
/>
|
|
132
|
+
</el-form-item>
|
|
133
|
+
<el-form-item label="像素容差">
|
|
134
|
+
<el-input-number
|
|
135
|
+
:model-value="normalizedConfig.pixelmatchThreshold"
|
|
136
|
+
:min="0"
|
|
137
|
+
:max="1"
|
|
138
|
+
:step="0.01"
|
|
139
|
+
controls-position="right"
|
|
140
|
+
@update:model-value="patchConfig({ pixelmatchThreshold: Number($event) })"
|
|
141
|
+
/>
|
|
142
|
+
</el-form-item>
|
|
143
|
+
</div>
|
|
144
|
+
</el-form>
|
|
145
|
+
|
|
146
|
+
<template #footer>
|
|
147
|
+
<el-button @click="emit('update:modelValue', false)">取消</el-button>
|
|
148
|
+
<el-button type="primary" @click="emit('confirm')">添加开始节点</el-button>
|
|
149
|
+
</template>
|
|
150
|
+
</el-dialog>
|
|
151
|
+
</template>
|
|
@@ -58,6 +58,33 @@ function wireContinuation(step: AppiumRecordedStep, targetId: string) {
|
|
|
58
58
|
if (!step.flow.noTargetId) setFlowTarget(step, 'noTargetId', targetId);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function isVisualChangeStart(step: AppiumRecordedStep) {
|
|
62
|
+
return step.visualChange?.role === 'start';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isVisualChangeEnd(step: AppiumRecordedStep) {
|
|
66
|
+
return step.visualChange?.role === 'end';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function visualChangePairKeys(step: AppiumRecordedStep) {
|
|
70
|
+
const config = step.visualChange;
|
|
71
|
+
if (!config) return [];
|
|
72
|
+
return [
|
|
73
|
+
config.pairId ? `pair:${config.pairId}` : '',
|
|
74
|
+
step.id ? `start:${step.id}` : '',
|
|
75
|
+
config.startStepId ? `start:${config.startStepId}` : '',
|
|
76
|
+
config.pairLabel ? `label:${config.pairLabel}` : '',
|
|
77
|
+
].filter(Boolean);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hasCopiedVisualChangeStartForEnd(copiedSteps: AppiumRecordedStep[], endStep: AppiumRecordedStep) {
|
|
81
|
+
const endKeys = new Set(visualChangePairKeys(endStep));
|
|
82
|
+
return copiedSteps.some((step) => (
|
|
83
|
+
isVisualChangeStart(step)
|
|
84
|
+
&& visualChangePairKeys(step).some((key) => endKeys.has(key))
|
|
85
|
+
));
|
|
86
|
+
}
|
|
87
|
+
|
|
61
88
|
function branchConditionForTarget(
|
|
62
89
|
steps: AppiumRecordedStep[],
|
|
63
90
|
target: PasteTarget,
|
|
@@ -65,10 +92,10 @@ function branchConditionForTarget(
|
|
|
65
92
|
if (!target.branch) return undefined;
|
|
66
93
|
const anchor = steps[target.afterIndex];
|
|
67
94
|
if (!anchor) return undefined;
|
|
95
|
+
if (anchor.flow?.nodeKind === 'condition') return anchor;
|
|
68
96
|
if (anchor.flow?.parentConditionId && anchor.flow.parentBranch === target.branch) {
|
|
69
97
|
return steps.find((step) => step.id === anchor.flow?.parentConditionId);
|
|
70
98
|
}
|
|
71
|
-
if (anchor.flow?.nodeKind === 'condition') return anchor;
|
|
72
99
|
return undefined;
|
|
73
100
|
}
|
|
74
101
|
|
|
@@ -123,6 +150,9 @@ export function createFlowClipboard(steps: AppiumRecordedStep[], indexes: number
|
|
|
123
150
|
if (copiedSteps.some((step) => step.type === 'launchApp')) {
|
|
124
151
|
throw new Error('启动 APP 节点不能复制');
|
|
125
152
|
}
|
|
153
|
+
if (copiedSteps.some((step) => isVisualChangeEnd(step) && !hasCopiedVisualChangeStartForEnd(copiedSteps, step))) {
|
|
154
|
+
throw new Error('检测画面变化结束节点不能单独复制');
|
|
155
|
+
}
|
|
126
156
|
return { steps: clone(copiedSteps), rootIds };
|
|
127
157
|
}
|
|
128
158
|
|
|
@@ -141,6 +171,11 @@ export function pasteFlowClipboard(
|
|
|
141
171
|
throw new Error('目标分支不存在');
|
|
142
172
|
}
|
|
143
173
|
|
|
174
|
+
const pairMap = new Map<string, string>();
|
|
175
|
+
clipboard.steps.filter(isVisualChangeStart).forEach((step) => {
|
|
176
|
+
const pairId = createId();
|
|
177
|
+
visualChangePairKeys(step).forEach((key) => pairMap.set(key, pairId));
|
|
178
|
+
});
|
|
144
179
|
const copies = clipboard.steps.map((source) => {
|
|
145
180
|
const step = clone(source);
|
|
146
181
|
step.id = idMap.get(source.id) || createId();
|
|
@@ -168,6 +203,17 @@ export function pasteFlowClipboard(
|
|
|
168
203
|
}
|
|
169
204
|
}
|
|
170
205
|
step.flow = Object.keys(flow).length ? flow : undefined;
|
|
206
|
+
if (step.visualChange) {
|
|
207
|
+
const nextPairId = visualChangePairKeys(source)
|
|
208
|
+
.map((key) => pairMap.get(key))
|
|
209
|
+
.find(Boolean);
|
|
210
|
+
step.visualChange = {
|
|
211
|
+
...step.visualChange,
|
|
212
|
+
pairId: nextPairId || step.visualChange.pairId,
|
|
213
|
+
startStepId: idMap.get(step.visualChange.startStepId || '') || step.visualChange.startStepId,
|
|
214
|
+
endStepId: idMap.get(step.visualChange.endStepId || '') || step.visualChange.endStepId,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
171
217
|
return step;
|
|
172
218
|
});
|
|
173
219
|
|