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,8 +1,17 @@
|
|
|
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';
|
|
5
|
+
import { longPressMode } from '../long-press';
|
|
6
|
+
import { defaultFlowKind, isBooleanCondition } from '../flow-labels';
|
|
7
|
+
import TextClickSettings from './TextClickSettings.vue';
|
|
8
|
+
import { isNativeStateCondition } from '../native-control-state';
|
|
9
|
+
import LongPressSettings from './LongPressSettings.vue';
|
|
10
|
+
import StageLogSettings from './StageLogSettings.vue';
|
|
3
11
|
|
|
4
12
|
type FlowKind = 'action' | 'condition' | 'assertion';
|
|
5
13
|
type SwipeGesture = NonNullable<AppiumRecordedStep['swipe']>;
|
|
14
|
+
type VisualChangeConfig = NonNullable<AppiumRecordedStep['visualChange']>;
|
|
6
15
|
|
|
7
16
|
const props = defineProps<{
|
|
8
17
|
step: AppiumRecordedStep;
|
|
@@ -14,10 +23,20 @@ const emit = defineEmits<{
|
|
|
14
23
|
update: [payload: { index: number; step: AppiumRecordedStep }];
|
|
15
24
|
}>();
|
|
16
25
|
|
|
26
|
+
const visualChangeConfig = computed(() => normalizeVisualChangeConfig(props.step.visualChange));
|
|
27
|
+
const visualChangeRoleLabel = computed(() => (
|
|
28
|
+
visualChangeConfig.value.role === 'start'
|
|
29
|
+
? `${visualChangeConfig.value.pairLabel || '检测画面变化'} · 开始节点`
|
|
30
|
+
: visualChangeConfig.value.role === 'end'
|
|
31
|
+
? `${visualChangeConfig.value.pairLabel || '检测画面变化'} · 结束节点`
|
|
32
|
+
: '起止节点截图对比'
|
|
33
|
+
));
|
|
34
|
+
const visualChangeConfigDisabled = computed(() => Boolean(
|
|
35
|
+
props.disabled || visualChangeConfig.value.role === 'end',
|
|
36
|
+
));
|
|
37
|
+
|
|
17
38
|
function defaultKind(): FlowKind {
|
|
18
|
-
|
|
19
|
-
if (props.step.type === 'assertExists' || props.step.type === 'assertText') return 'assertion';
|
|
20
|
-
return 'action';
|
|
39
|
+
return defaultFlowKind(props.step);
|
|
21
40
|
}
|
|
22
41
|
|
|
23
42
|
function patchStep(patch: Partial<AppiumRecordedStep>) {
|
|
@@ -55,18 +74,41 @@ function patchSwipe(patch: Partial<SwipeGesture>) {
|
|
|
55
74
|
});
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
function
|
|
59
|
-
|
|
77
|
+
function patchVisualChange(patch: Partial<VisualChangeConfig>) {
|
|
78
|
+
patchStep({
|
|
79
|
+
visualChange: normalizeVisualChangeConfig({
|
|
80
|
+
...normalizeVisualChangeConfig(props.step.visualChange),
|
|
81
|
+
...patch,
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
60
84
|
}
|
|
61
85
|
|
|
62
|
-
function
|
|
63
|
-
|
|
86
|
+
function patchVisualRegion(key: keyof VisualChangeConfig['region'], value: unknown) {
|
|
87
|
+
const current = normalizeVisualChangeConfig(props.step.visualChange);
|
|
88
|
+
patchVisualChange({
|
|
89
|
+
region: {
|
|
90
|
+
...current.region,
|
|
91
|
+
[key]: Number(value),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
function patchTimeout(value: unknown) {
|
|
67
|
-
const timeout = Math.max(
|
|
97
|
+
const timeout = Math.max(0, toInteger(value, props.step.timeoutMs || 0));
|
|
68
98
|
patchStep({ timeoutMs: timeout || undefined });
|
|
69
99
|
}
|
|
100
|
+
|
|
101
|
+
const showSelector = computed(() => props.step.selector && (
|
|
102
|
+
props.step.type !== 'longPress' || longPressMode(props.step) === 'element'
|
|
103
|
+
));
|
|
104
|
+
|
|
105
|
+
function patchSelector(patch: Partial<AppiumSelector>) {
|
|
106
|
+
patchStep({
|
|
107
|
+
selector: { ...props.step.selector!, ...patch },
|
|
108
|
+
// 修改元素目标后,不能继续优先使用录制时的备用 XPath。
|
|
109
|
+
...(props.step.type === 'longPress' || isNativeStateCondition(props.step) ? { selectorChain: undefined } : {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
70
112
|
</script>
|
|
71
113
|
|
|
72
114
|
<template>
|
|
@@ -92,7 +134,7 @@ function patchTimeout(value: unknown) {
|
|
|
92
134
|
<el-form-item label="节点类型">
|
|
93
135
|
<el-select
|
|
94
136
|
:model-value="defaultKind()"
|
|
95
|
-
:disabled="disabled"
|
|
137
|
+
:disabled="disabled || isBooleanCondition(step) || step.type === 'log'"
|
|
96
138
|
@update:model-value="patchFlow({ nodeKind: $event as FlowKind })"
|
|
97
139
|
>
|
|
98
140
|
<el-option label="操作" value="action" />
|
|
@@ -100,17 +142,26 @@ function patchTimeout(value: unknown) {
|
|
|
100
142
|
<el-option label="校验" value="assertion" />
|
|
101
143
|
</el-select>
|
|
102
144
|
</el-form-item>
|
|
103
|
-
<el-form-item
|
|
145
|
+
<el-form-item v-if="step.type !== 'longPress' && step.type !== 'log'" label="超时时间 ms">
|
|
104
146
|
<el-input-number
|
|
105
147
|
:model-value="step.timeoutMs || undefined"
|
|
106
148
|
:disabled="disabled"
|
|
107
|
-
:min="
|
|
149
|
+
:min="0"
|
|
108
150
|
:max="999999"
|
|
109
151
|
controls-position="right"
|
|
110
152
|
@update:model-value="patchTimeout($event)"
|
|
111
153
|
/>
|
|
112
154
|
</el-form-item>
|
|
113
155
|
</div>
|
|
156
|
+
<LongPressSettings v-if="step.type === 'longPress'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
157
|
+
<StageLogSettings v-if="step.type === 'log'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
158
|
+
<TextClickSettings v-if="step.type === 'textClick'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
159
|
+
<el-form-item v-if="step.type === 'aiRecognition'" label="识别内容">
|
|
160
|
+
<el-input
|
|
161
|
+
:model-value="step.value || ''" type="textarea" :rows="3" maxlength="4000"
|
|
162
|
+
:disabled="disabled" @update:model-value="patchStep({ value: String($event) })"
|
|
163
|
+
/>
|
|
164
|
+
</el-form-item>
|
|
114
165
|
<el-form-item
|
|
115
166
|
v-if="step.type === 'input' || step.type === 'inputIfExists' || step.type === 'assertText'"
|
|
116
167
|
label="文本内容"
|
|
@@ -121,6 +172,91 @@ function patchTimeout(value: unknown) {
|
|
|
121
172
|
@update:model-value="patchStep({ value: String($event) })"
|
|
122
173
|
/>
|
|
123
174
|
</el-form-item>
|
|
175
|
+
<template v-if="step.type === 'visualChange'">
|
|
176
|
+
<div class="appium-flow-editor__grid">
|
|
177
|
+
<el-form-item label="检测标记">
|
|
178
|
+
<el-input :model-value="visualChangeRoleLabel" disabled />
|
|
179
|
+
</el-form-item>
|
|
180
|
+
<el-form-item label="检测目标">
|
|
181
|
+
<el-select
|
|
182
|
+
:model-value="visualChangeConfig.mode"
|
|
183
|
+
:disabled="visualChangeConfigDisabled"
|
|
184
|
+
@update:model-value="patchVisualChange({ mode: $event as VisualChangeConfig['mode'] })"
|
|
185
|
+
>
|
|
186
|
+
<el-option label="当前选中元素" value="selectedElement" />
|
|
187
|
+
<el-option label="手动区域" value="region" />
|
|
188
|
+
</el-select>
|
|
189
|
+
</el-form-item>
|
|
190
|
+
<el-form-item label="变化阈值 %">
|
|
191
|
+
<el-input-number
|
|
192
|
+
:model-value="visualChangeConfig.changeRatioThreshold"
|
|
193
|
+
:disabled="visualChangeConfigDisabled"
|
|
194
|
+
:min="0.01"
|
|
195
|
+
:max="100"
|
|
196
|
+
:step="0.1"
|
|
197
|
+
controls-position="right"
|
|
198
|
+
@update:model-value="patchVisualChange({ changeRatioThreshold: Number($event) })"
|
|
199
|
+
/>
|
|
200
|
+
</el-form-item>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="appium-flow-editor__grid appium-flow-editor__grid--visual">
|
|
203
|
+
<el-form-item label="X">
|
|
204
|
+
<el-input-number
|
|
205
|
+
:model-value="visualChangeConfig.region.x"
|
|
206
|
+
:disabled="visualChangeConfigDisabled"
|
|
207
|
+
:min="0"
|
|
208
|
+
:max="99999"
|
|
209
|
+
:precision="0"
|
|
210
|
+
controls-position="right"
|
|
211
|
+
@update:model-value="patchVisualRegion('x', $event)"
|
|
212
|
+
/>
|
|
213
|
+
</el-form-item>
|
|
214
|
+
<el-form-item label="Y">
|
|
215
|
+
<el-input-number
|
|
216
|
+
:model-value="visualChangeConfig.region.y"
|
|
217
|
+
:disabled="visualChangeConfigDisabled"
|
|
218
|
+
:min="0"
|
|
219
|
+
:max="99999"
|
|
220
|
+
:precision="0"
|
|
221
|
+
controls-position="right"
|
|
222
|
+
@update:model-value="patchVisualRegion('y', $event)"
|
|
223
|
+
/>
|
|
224
|
+
</el-form-item>
|
|
225
|
+
<el-form-item label="宽度">
|
|
226
|
+
<el-input-number
|
|
227
|
+
:model-value="visualChangeConfig.region.width"
|
|
228
|
+
:disabled="visualChangeConfigDisabled"
|
|
229
|
+
:min="1"
|
|
230
|
+
:max="99999"
|
|
231
|
+
:precision="0"
|
|
232
|
+
controls-position="right"
|
|
233
|
+
@update:model-value="patchVisualRegion('width', $event)"
|
|
234
|
+
/>
|
|
235
|
+
</el-form-item>
|
|
236
|
+
<el-form-item label="高度">
|
|
237
|
+
<el-input-number
|
|
238
|
+
:model-value="visualChangeConfig.region.height"
|
|
239
|
+
:disabled="visualChangeConfigDisabled"
|
|
240
|
+
:min="1"
|
|
241
|
+
:max="99999"
|
|
242
|
+
:precision="0"
|
|
243
|
+
controls-position="right"
|
|
244
|
+
@update:model-value="patchVisualRegion('height', $event)"
|
|
245
|
+
/>
|
|
246
|
+
</el-form-item>
|
|
247
|
+
<el-form-item label="像素容差">
|
|
248
|
+
<el-input-number
|
|
249
|
+
:model-value="visualChangeConfig.pixelmatchThreshold"
|
|
250
|
+
:disabled="visualChangeConfigDisabled"
|
|
251
|
+
:min="0"
|
|
252
|
+
:max="1"
|
|
253
|
+
:step="0.01"
|
|
254
|
+
controls-position="right"
|
|
255
|
+
@update:model-value="patchVisualChange({ pixelmatchThreshold: Number($event) })"
|
|
256
|
+
/>
|
|
257
|
+
</el-form-item>
|
|
258
|
+
</div>
|
|
259
|
+
</template>
|
|
124
260
|
<div v-if="step.type === 'swipe'" class="appium-flow-editor__grid appium-flow-editor__grid--swipe">
|
|
125
261
|
<el-form-item label="起点 X">
|
|
126
262
|
<el-input-number
|
|
@@ -178,12 +314,12 @@ function patchTimeout(value: unknown) {
|
|
|
178
314
|
/>
|
|
179
315
|
</el-form-item>
|
|
180
316
|
</div>
|
|
181
|
-
<div v-if="step.selector" class="appium-flow-editor__grid">
|
|
317
|
+
<div v-if="showSelector && step.selector" class="appium-flow-editor__grid">
|
|
182
318
|
<el-form-item label="Selector 类型">
|
|
183
319
|
<el-select
|
|
184
320
|
:model-value="step.selector.strategy"
|
|
185
321
|
:disabled="disabled"
|
|
186
|
-
@update:model-value="
|
|
322
|
+
@update:model-value="patchSelector({ strategy: $event as AppiumSelector['strategy'] })"
|
|
187
323
|
>
|
|
188
324
|
<el-option label="accessibility id" value="accessibilityId" />
|
|
189
325
|
<el-option label="id" value="id" />
|
|
@@ -195,11 +331,11 @@ function patchTimeout(value: unknown) {
|
|
|
195
331
|
<el-input
|
|
196
332
|
:model-value="step.selector.value || ''"
|
|
197
333
|
:disabled="disabled"
|
|
198
|
-
@update:model-value="
|
|
334
|
+
@update:model-value="patchSelector({ value: String($event) })"
|
|
199
335
|
/>
|
|
200
336
|
</el-form-item>
|
|
201
337
|
</div>
|
|
202
|
-
<div v-if="
|
|
338
|
+
<div v-if="showSelector" class="appium-flow-editor__grid">
|
|
203
339
|
<el-form-item label="父级上下文类型">
|
|
204
340
|
<el-select
|
|
205
341
|
:model-value="step.contextSelector?.strategy || ''"
|
|
@@ -222,16 +358,7 @@ function patchTimeout(value: unknown) {
|
|
|
222
358
|
/>
|
|
223
359
|
</el-form-item>
|
|
224
360
|
</div>
|
|
225
|
-
<
|
|
226
|
-
<el-switch
|
|
227
|
-
:model-value="Boolean(step.optional)"
|
|
228
|
-
:disabled="disabled"
|
|
229
|
-
active-text="找不到时跳过"
|
|
230
|
-
inactive-text="找不到时失败"
|
|
231
|
-
@update:model-value="patchStep({ optional: Boolean($event) })"
|
|
232
|
-
/>
|
|
233
|
-
</el-form-item>
|
|
234
|
-
<div v-if="defaultKind() === 'condition'" class="appium-flow-editor__grid">
|
|
361
|
+
<div v-if="defaultKind() === 'condition' && !isBooleanCondition(step)" class="appium-flow-editor__grid">
|
|
235
362
|
<el-form-item label="指定文本(可选)">
|
|
236
363
|
<el-input
|
|
237
364
|
:model-value="step.value || ''"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import type { AppiumRecordedStep } from '../types';
|
|
4
|
+
import { longPressMode } from '../long-press';
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{ step: AppiumRecordedStep; disabled?: boolean }>();
|
|
7
|
+
const emit = defineEmits<{ update: [patch: Partial<AppiumRecordedStep>] }>();
|
|
8
|
+
const mode = computed(() => longPressMode(props.step));
|
|
9
|
+
|
|
10
|
+
function changeMode(value: AppiumRecordedStep['longPressMode']) {
|
|
11
|
+
emit('update', {
|
|
12
|
+
longPressMode: value,
|
|
13
|
+
// 旧坐标节点切换到元素模式后,开放定位器输入。
|
|
14
|
+
...(value === 'element' && (!props.step.selector || props.step.selector.strategy === 'bounds')
|
|
15
|
+
? { selector: { strategy: 'id' as const, value: '' }, selectorChain: undefined, contextSelector: undefined }
|
|
16
|
+
: {}),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function changeCoordinate(key: 'centerX' | 'centerY', value: number | undefined) {
|
|
21
|
+
emit('update', { fallback: { ...props.step.fallback, strategy: 'bounds', [key]: value } });
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<div>
|
|
27
|
+
<el-form-item label="长按方式">
|
|
28
|
+
<el-radio-group :model-value="mode" :disabled="disabled" @update:model-value="changeMode($event as AppiumRecordedStep['longPressMode'])">
|
|
29
|
+
<el-radio-button value="element">长按元素</el-radio-button>
|
|
30
|
+
<el-radio-button value="coordinates">长按坐标</el-radio-button>
|
|
31
|
+
</el-radio-group>
|
|
32
|
+
</el-form-item>
|
|
33
|
+
<el-form-item label="长按时间 ms">
|
|
34
|
+
<el-input-number
|
|
35
|
+
:model-value="step.timeoutMs ?? 800"
|
|
36
|
+
:disabled="disabled"
|
|
37
|
+
:min="80"
|
|
38
|
+
:max="999999"
|
|
39
|
+
:precision="0"
|
|
40
|
+
controls-position="right"
|
|
41
|
+
@update:model-value="emit('update', { timeoutMs: $event ?? 800 })"
|
|
42
|
+
/>
|
|
43
|
+
</el-form-item>
|
|
44
|
+
<div v-if="mode === 'coordinates'" class="appium-flow-editor__grid">
|
|
45
|
+
<el-form-item v-for="axis in (['X', 'Y'] as const)" :key="axis" :label="axis">
|
|
46
|
+
<el-input-number
|
|
47
|
+
:model-value="axis === 'X' ? step.fallback?.centerX : step.fallback?.centerY"
|
|
48
|
+
:disabled="disabled"
|
|
49
|
+
:min="0"
|
|
50
|
+
:max="99999"
|
|
51
|
+
:precision="0"
|
|
52
|
+
controls-position="right"
|
|
53
|
+
@update:model-value="changeCoordinate(axis === 'X' ? 'centerX' : 'centerY', $event)"
|
|
54
|
+
/>
|
|
55
|
+
</el-form-item>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { defaultFlowKind as defaultKind, flowBranchLabel, flowStepMeta } from '../flow-labels';
|
|
2
3
|
import { computed } from 'vue';
|
|
3
4
|
import { CopyDocument, Delete, Edit, Timer } from '@element-plus/icons-vue';
|
|
4
5
|
import type { AppiumRecordedStep } from '../types';
|
|
5
6
|
import { buildConditionLayouts, FLOW_BRANCH_GAP, FLOW_NODE_WIDTH } from '../flow-layout';
|
|
6
7
|
import FlowStepEditor from './FlowStepEditor.vue';
|
|
8
|
+
import FlowActionMenu from './FlowActionMenu.vue';
|
|
7
9
|
|
|
8
10
|
type BranchName = 'yes' | 'no';
|
|
9
11
|
type FlowKind = 'action' | 'condition' | 'assertion';
|
|
@@ -23,6 +25,10 @@ type InsertAction =
|
|
|
23
25
|
| 'launchApp'
|
|
24
26
|
| 'clearAppData'
|
|
25
27
|
| 'popupCondition'
|
|
28
|
+
| 'checkboxState'
|
|
29
|
+
| 'radioButtonState'
|
|
30
|
+
| 'aiRecognition'
|
|
31
|
+
| 'textClick'
|
|
26
32
|
| 'tapIfExists'
|
|
27
33
|
| 'inputIfExists'
|
|
28
34
|
| 'clearIfExists'
|
|
@@ -32,7 +38,12 @@ type InsertAction =
|
|
|
32
38
|
| 'assertText'
|
|
33
39
|
| 'waitDisappear'
|
|
34
40
|
| 'waitActivity'
|
|
35
|
-
| 'runScript'
|
|
41
|
+
| 'runScript'
|
|
42
|
+
| 'noop'
|
|
43
|
+
| 'log'
|
|
44
|
+
| 'visualChangeStart'
|
|
45
|
+
| 'visualChangeEnd'
|
|
46
|
+
| 'visualChange';
|
|
36
47
|
|
|
37
48
|
const PASTE_COMMAND = '__paste_flow_nodes__';
|
|
38
49
|
|
|
@@ -76,6 +87,9 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
76
87
|
{ type: 'clearInput', label: '清空输入' },
|
|
77
88
|
{ type: 'coordinateTap', label: '点击坐标' },
|
|
78
89
|
{ type: 'longPress', label: '长按' },
|
|
90
|
+
{ type: 'checkboxState', label: '判断 Checkbox 状态' },
|
|
91
|
+
{ type: 'textClick', label: '文字点击' },
|
|
92
|
+
{ type: 'radioButtonState', label: '判断 RadioButton 状态' },
|
|
79
93
|
],
|
|
80
94
|
},
|
|
81
95
|
{
|
|
@@ -94,6 +108,7 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
94
108
|
actions: [
|
|
95
109
|
{ type: 'delay', label: '添加延时' },
|
|
96
110
|
{ type: 'popupCondition', label: '判断存在' },
|
|
111
|
+
{ type: 'aiRecognition', label: 'AI 识别' },
|
|
97
112
|
{ type: 'tapIfExists', label: '存在则点击' },
|
|
98
113
|
{ type: 'inputIfExists', label: '存在则输入' },
|
|
99
114
|
{ type: 'clearIfExists', label: '存在则清空' },
|
|
@@ -103,17 +118,20 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
103
118
|
{ type: 'assertText', label: '断言文本' },
|
|
104
119
|
{ type: 'waitDisappear', label: '等待元素消失' },
|
|
105
120
|
{ type: 'waitActivity', label: '等待 Activity' },
|
|
121
|
+
{ type: 'visualChangeStart', label: '检测画面变化开始节点' },
|
|
122
|
+
{ type: 'visualChangeEnd', label: '检测画面变化结束节点' },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
title: '流程控制',
|
|
127
|
+
actions: [
|
|
128
|
+
{ type: 'noop', label: '空节点' },
|
|
129
|
+
{ type: 'log', label: '输出日志' },
|
|
130
|
+
{ type: 'runScript', label: '连接脚本' },
|
|
106
131
|
],
|
|
107
132
|
},
|
|
108
|
-
{ title: '流程控制', actions: [{ type: 'runScript', label: '连接脚本' }] },
|
|
109
133
|
];
|
|
110
134
|
|
|
111
|
-
function defaultKind(step: AppiumRecordedStep): FlowKind {
|
|
112
|
-
if (step.flow?.nodeKind) return step.flow.nodeKind;
|
|
113
|
-
if (step.type === 'assertExists' || step.type === 'assertText') return 'assertion';
|
|
114
|
-
return 'action';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
135
|
function kindLabel(kind: FlowKind) {
|
|
118
136
|
return { action: '操作', condition: '判断', assertion: '校验' }[kind];
|
|
119
137
|
}
|
|
@@ -125,7 +143,13 @@ function typeLabel(step: AppiumRecordedStep) {
|
|
|
125
143
|
waitFor: '等待出现', waitDisappear: '等待消失', assertExists: '断言存在',
|
|
126
144
|
assertText: '断言文本', key: '按键', waitActivity: '等待 Activity', delay: '延时',
|
|
127
145
|
coordinateTap: '坐标点击', swipe: '滑动', launchApp: '启动 APP', clearAppData: '清理 APP 缓存', longPress: '长按',
|
|
128
|
-
pinch: '双指缩放', runScript: '连接脚本', screenshot: '截图',
|
|
146
|
+
pinch: '双指缩放', runScript: '连接脚本', screenshot: '截图', visualChange: '检测画面变化',
|
|
147
|
+
noop: '空节点',
|
|
148
|
+
log: '输出日志',
|
|
149
|
+
checkboxState: 'Checkbox 状态',
|
|
150
|
+
radioButtonState: 'RadioButton 状态',
|
|
151
|
+
aiRecognition: 'AI 识别',
|
|
152
|
+
textClick: '文字点击',
|
|
129
153
|
};
|
|
130
154
|
return labels[step.type] || step.type;
|
|
131
155
|
}
|
|
@@ -133,12 +157,20 @@ function typeLabel(step: AppiumRecordedStep) {
|
|
|
133
157
|
function stepMeta(step: AppiumRecordedStep) {
|
|
134
158
|
if (step.type === 'delay') return `${step.timeoutMs || 1000}ms`;
|
|
135
159
|
if (step.type === 'input' || step.type === 'inputIfExists') return `输入内容:${step.value || '空'}`;
|
|
136
|
-
if (step.type === 'longPress') return
|
|
160
|
+
if (step.type === 'longPress') return flowStepMeta(step);
|
|
137
161
|
if (step.type === 'coordinateTap') return `${step.fallback?.centerX || ''},${step.fallback?.centerY || ''}`;
|
|
138
162
|
if (step.type === 'swipe') {
|
|
139
163
|
const swipe = step.swipe;
|
|
140
164
|
return swipe ? `[${swipe.startX},${swipe.startY}] -> [${swipe.endX},${swipe.endY}]` : '';
|
|
141
165
|
}
|
|
166
|
+
if (step.type === 'visualChange') {
|
|
167
|
+
const config = step.visualChange;
|
|
168
|
+
if (!config) return '';
|
|
169
|
+
if (config.role === 'start') return `${config.pairLabel || '检测画面变化'} · 开始 · 阈值 ${config.changeRatioThreshold}%`;
|
|
170
|
+
if (config.role === 'end') return `${config.pairLabel || '检测画面变化'} · 结束`;
|
|
171
|
+
return `${config.startStepId && config.endStepId ? '起止节点截图对比' : '请选择起止节点'} · 阈值 ${config.changeRatioThreshold}%`;
|
|
172
|
+
}
|
|
173
|
+
if (step.type === 'noop') return '';
|
|
142
174
|
if (defaultKind(step) === 'condition' && step.value) {
|
|
143
175
|
return `${step.flow?.textMatch === 'exact' ? '精准匹配' : '模糊匹配'}:${step.value}`;
|
|
144
176
|
}
|
|
@@ -199,6 +231,12 @@ function isActionDisabled(action: InsertAction) {
|
|
|
199
231
|
return Boolean(props.disabled && !props.allowedLockedActions?.includes(action));
|
|
200
232
|
}
|
|
201
233
|
|
|
234
|
+
function canCopyStep(step: AppiumRecordedStep) {
|
|
235
|
+
return step.type !== 'launchApp'
|
|
236
|
+
&& step.type !== 'clearAppData'
|
|
237
|
+
&& step.visualChange?.role !== 'end';
|
|
238
|
+
}
|
|
239
|
+
|
|
202
240
|
function insert(branch: BranchName, command: string | number | object) {
|
|
203
241
|
if (command === PASTE_COMMAND) emit('paste', props.conditionIndex, branch);
|
|
204
242
|
else emit('insertBranchAction', props.conditionIndex, branch, command as InsertAction);
|
|
@@ -244,7 +282,7 @@ function branchSplitPath() {
|
|
|
244
282
|
<svg class="appium-flow-line appium-flow-branch__line" viewBox="0 0 10 100" preserveAspectRatio="none" aria-hidden="true">
|
|
245
283
|
<path d="M5 0 V100" />
|
|
246
284
|
</svg>
|
|
247
|
-
<strong class="appium-flow-branch__label">{{ branch
|
|
285
|
+
<strong class="appium-flow-branch__label">{{ flowBranchLabel(condition, branch) }}</strong>
|
|
248
286
|
<div v-if="branchItems(branch).length" class="appium-flow-branch-steps">
|
|
249
287
|
<div v-for="item in branchItems(branch)" :key="item.step.id" class="appium-flow-branch-step-group">
|
|
250
288
|
<div
|
|
@@ -275,7 +313,7 @@ function branchSplitPath() {
|
|
|
275
313
|
</button>
|
|
276
314
|
<span class="appium-flow-node__actions" @click.stop>
|
|
277
315
|
<el-button
|
|
278
|
-
v-if="!copyMode"
|
|
316
|
+
v-if="!copyMode && canCopyStep(item.step)"
|
|
279
317
|
text
|
|
280
318
|
size="small"
|
|
281
319
|
:icon="CopyDocument"
|
|
@@ -332,11 +370,11 @@ function branchSplitPath() {
|
|
|
332
370
|
</div>
|
|
333
371
|
<span v-else class="appium-flow-branch__target">{{ targetLabel(branch) }}</span>
|
|
334
372
|
<div class="appium-flow-branch__actions">
|
|
335
|
-
<
|
|
336
|
-
|
|
373
|
+
<FlowActionMenu
|
|
374
|
+
:groups="actionGroups"
|
|
337
375
|
:disabled="!canOpenInsertMenu()"
|
|
338
|
-
|
|
339
|
-
|
|
376
|
+
:clipboard-count="clipboardCount"
|
|
377
|
+
:is-action-disabled="isActionDisabled"
|
|
340
378
|
@command="insert(branch, $event)"
|
|
341
379
|
>
|
|
342
380
|
<el-button
|
|
@@ -348,25 +386,7 @@ function branchSplitPath() {
|
|
|
348
386
|
>
|
|
349
387
|
插入操作
|
|
350
388
|
</el-button>
|
|
351
|
-
|
|
352
|
-
<el-dropdown-menu>
|
|
353
|
-
<el-dropdown-item v-if="clipboardCount" :command="PASTE_COMMAND" divided>
|
|
354
|
-
粘贴 {{ clipboardCount }} 个节点
|
|
355
|
-
</el-dropdown-item>
|
|
356
|
-
<template v-for="group in actionGroups" :key="group.title">
|
|
357
|
-
<div class="appium-action-dropdown__group">{{ group.title }}</div>
|
|
358
|
-
<el-dropdown-item
|
|
359
|
-
v-for="action in group.actions"
|
|
360
|
-
:key="action.type"
|
|
361
|
-
:command="action.type"
|
|
362
|
-
:disabled="isActionDisabled(action.type)"
|
|
363
|
-
>
|
|
364
|
-
{{ action.label }}
|
|
365
|
-
</el-dropdown-item>
|
|
366
|
-
</template>
|
|
367
|
-
</el-dropdown-menu>
|
|
368
|
-
</template>
|
|
369
|
-
</el-dropdown>
|
|
389
|
+
</FlowActionMenu>
|
|
370
390
|
</div>
|
|
371
391
|
</div>
|
|
372
392
|
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{ scriptName: string; saving: boolean }>();
|
|
3
|
+
defineEmits<{ save: []; discard: []; cancel: [] }>();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<el-dialog
|
|
8
|
+
:model-value="true" title="新建脚本" width="min(480px, calc(100vw - 32px))"
|
|
9
|
+
append-to-body :close-on-click-modal="false" :close-on-press-escape="!saving"
|
|
10
|
+
:show-close="!saving" @close="$emit('cancel')"
|
|
11
|
+
>
|
|
12
|
+
<p class="new-script-message">是否保存当前脚本「{{ scriptName || '未命名脚本' }}」后再新建?</p>
|
|
13
|
+
<template #footer>
|
|
14
|
+
<div class="new-script-actions">
|
|
15
|
+
<el-button :disabled="saving" @click="$emit('cancel')">取消</el-button>
|
|
16
|
+
<el-button :disabled="saving" @click="$emit('discard')">不保存并新建</el-button>
|
|
17
|
+
<el-button type="primary" :loading="saving" @click="$emit('save')">保存并新建</el-button>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
</el-dialog>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<style scoped>
|
|
24
|
+
.new-script-message { overflow-wrap: anywhere; }
|
|
25
|
+
.new-script-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
|
26
|
+
.new-script-actions .el-button { margin: 0; }
|
|
27
|
+
</style>
|
|
@@ -21,6 +21,8 @@ defineProps<{
|
|
|
21
21
|
<el-descriptions-item label="content-desc">{{ node.contentDesc || '-' }}</el-descriptions-item>
|
|
22
22
|
<el-descriptions-item label="text">{{ node.text || '-' }}</el-descriptions-item>
|
|
23
23
|
<el-descriptions-item label="class">{{ node.className || '-' }}</el-descriptions-item>
|
|
24
|
+
<el-descriptions-item v-if="node.checkable !== undefined" label="checkable">{{ String(node.checkable) }}</el-descriptions-item>
|
|
25
|
+
<el-descriptions-item v-if="node.checkable" label="checked">{{ node.checked === undefined ? '-' : String(node.checked) }}</el-descriptions-item>
|
|
24
26
|
<el-descriptions-item label="当前 Activity">{{ currentActivity || '-' }}</el-descriptions-item>
|
|
25
27
|
<el-descriptions-item label="selector">
|
|
26
28
|
<span v-if="currentActivity">Activity {{ currentActivity }} · </span>
|
|
@@ -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';
|
|
@@ -9,6 +9,7 @@ import { labelFlowStep } from '../flow-labels';
|
|
|
9
9
|
import type { FlowActionGroup, FlowBranch, InsertAction } from '../flow-graph';
|
|
10
10
|
|
|
11
11
|
const props = defineProps<{
|
|
12
|
+
aiRecognitionModelConfigured?: boolean;
|
|
12
13
|
steps: AppiumRecordedStep[];
|
|
13
14
|
disabled?: boolean;
|
|
14
15
|
removeDisabled?: boolean;
|
|
@@ -35,6 +36,7 @@ const copyMode = shallowRef(false);
|
|
|
35
36
|
const selectedCopyIndexes = shallowRef<number[]>([]);
|
|
36
37
|
const flowDialogVisible = shallowRef(false);
|
|
37
38
|
const mainResetViewToken = shallowRef(0);
|
|
39
|
+
const dialogResetViewToken = shallowRef(0);
|
|
38
40
|
|
|
39
41
|
const insertActionGroups: FlowActionGroup[] = [
|
|
40
42
|
{
|
|
@@ -45,6 +47,9 @@ const insertActionGroups: FlowActionGroup[] = [
|
|
|
45
47
|
{ type: 'clearInput', label: '清空输入' },
|
|
46
48
|
{ type: 'coordinateTap', label: '点击坐标' },
|
|
47
49
|
{ type: 'longPress', label: '长按' },
|
|
50
|
+
{ type: 'checkboxState', label: '判断 Checkbox 状态' },
|
|
51
|
+
{ type: 'textClick', label: '文字点击' },
|
|
52
|
+
{ type: 'radioButtonState', label: '判断 RadioButton 状态' },
|
|
48
53
|
],
|
|
49
54
|
},
|
|
50
55
|
{
|
|
@@ -65,6 +70,7 @@ const insertActionGroups: FlowActionGroup[] = [
|
|
|
65
70
|
actions: [
|
|
66
71
|
{ type: 'delay', label: '添加延时' },
|
|
67
72
|
{ type: 'popupCondition', label: '判断存在' },
|
|
73
|
+
{ type: 'aiRecognition', label: 'AI 识别' },
|
|
68
74
|
{ type: 'tapIfExists', label: '存在则点击' },
|
|
69
75
|
{ type: 'inputIfExists', label: '存在则输入' },
|
|
70
76
|
{ type: 'clearIfExists', label: '存在则清空' },
|
|
@@ -74,11 +80,17 @@ const insertActionGroups: FlowActionGroup[] = [
|
|
|
74
80
|
{ type: 'assertText', label: '断言文本' },
|
|
75
81
|
{ type: 'waitDisappear', label: '等待元素消失' },
|
|
76
82
|
{ type: 'waitActivity', label: '等待 Activity' },
|
|
83
|
+
{ type: 'visualChangeStart', label: '检测画面变化开始节点' },
|
|
84
|
+
{ type: 'visualChangeEnd', label: '检测画面变化结束节点' },
|
|
77
85
|
],
|
|
78
86
|
},
|
|
79
87
|
{
|
|
80
88
|
title: '流程控制',
|
|
81
|
-
actions: [
|
|
89
|
+
actions: [
|
|
90
|
+
{ type: 'noop', label: '空节点' },
|
|
91
|
+
{ type: 'log', label: '输出日志' },
|
|
92
|
+
{ type: 'runScript', label: '连接脚本' },
|
|
93
|
+
],
|
|
82
94
|
},
|
|
83
95
|
];
|
|
84
96
|
|
|
@@ -169,6 +181,13 @@ function resetMainFlowPosition() {
|
|
|
169
181
|
mainResetViewToken.value += 1;
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
watch(flowDialogVisible, (visible) => {
|
|
185
|
+
if (!visible) return;
|
|
186
|
+
void nextTick(() => {
|
|
187
|
+
dialogResetViewToken.value += 1;
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
172
191
|
function canOpenInsertMenu() {
|
|
173
192
|
return !props.disabled || Boolean(props.allowedLockedActions?.length);
|
|
174
193
|
}
|
|
@@ -220,7 +239,6 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
220
239
|
<el-button
|
|
221
240
|
size="small"
|
|
222
241
|
:icon="Aim"
|
|
223
|
-
:disabled="!steps.length"
|
|
224
242
|
@click="resetMainFlowPosition"
|
|
225
243
|
>
|
|
226
244
|
还原位置
|
|
@@ -228,7 +246,6 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
228
246
|
<el-button
|
|
229
247
|
size="small"
|
|
230
248
|
:icon="FullScreen"
|
|
231
|
-
:disabled="!steps.length"
|
|
232
249
|
@click="flowDialogVisible = true"
|
|
233
250
|
>
|
|
234
251
|
放大
|
|
@@ -244,6 +261,7 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
244
261
|
:disabled="disabled"
|
|
245
262
|
:remove-disabled="removeDisabled"
|
|
246
263
|
:launching-step-id="launchingStepId"
|
|
264
|
+
:ai-recognition-model-configured="aiRecognitionModelConfigured"
|
|
247
265
|
:clipboard-count="clipboardCount"
|
|
248
266
|
:reset-view-token="mainResetViewToken"
|
|
249
267
|
:start-action-groups="startActionGroups"
|
|
@@ -307,7 +325,9 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
307
325
|
:disabled="disabled"
|
|
308
326
|
:remove-disabled="removeDisabled"
|
|
309
327
|
:launching-step-id="launchingStepId"
|
|
328
|
+
:ai-recognition-model-configured="aiRecognitionModelConfigured"
|
|
310
329
|
:clipboard-count="clipboardCount"
|
|
330
|
+
:reset-view-token="dialogResetViewToken"
|
|
311
331
|
:start-action-groups="startActionGroups"
|
|
312
332
|
:main-action-groups="mainActionGroups"
|
|
313
333
|
:can-open-insert-menu="canOpenInsertMenu()"
|