android-midscene-automation 0.1.26 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/FAQ.md +14 -0
- package/README.md +31 -3
- package/bin/android-midscene-automation.js +71 -1
- package/package.json +4 -1
- package/server/appium-recorder/appium-runner.ts +273 -28
- package/server/appium-recorder/report.ts +186 -3
- package/server/appium-recorder/repository.ts +24 -3
- package/server/appium-recorder/visual-change.ts +184 -0
- package/server/http-api.ts +3 -4
- package/src/appium-recorder/AppiumPage.vue +465 -139
- package/src/appium-recorder/api.ts +1 -0
- package/src/appium-recorder/components/FlowCanvas.vue +14 -1
- package/src/appium-recorder/components/FlowStepEditor.vue +121 -2
- package/src/appium-recorder/components/NestedConditionBranches.vue +26 -4
- package/src/appium-recorder/components/RecordedSteps.vue +13 -1
- package/src/appium-recorder/components/VisualChangeDialog.vue +151 -0
- package/src/appium-recorder/flow-copy.ts +47 -1
- package/src/appium-recorder/flow-graph.ts +101 -6
- package/src/appium-recorder/flow-labels.ts +6 -1
- package/src/appium-recorder/flow-normalize.ts +67 -0
- package/src/appium-recorder/flow-remove.ts +67 -0
- package/src/appium-recorder/types.ts +25 -1
- package/src/appium-recorder/visual-change-pairs.ts +147 -0
- package/src/appium-recorder/visual-change.ts +75 -0
- package/src/components/device/DevicePreviewPanel.vue +148 -4
- package/src/style.css +39 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, shallowRef } from 'vue';
|
|
2
|
+
import { computed, shallowRef, watch } from 'vue';
|
|
3
3
|
import { Cpu, Refresh } from '@element-plus/icons-vue';
|
|
4
4
|
import type { AndroidDevice, DeviceAction } from '../../types';
|
|
5
5
|
|
|
@@ -11,6 +11,13 @@ type DeviceOverlayBounds = {
|
|
|
11
11
|
bottom: number;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
type DeviceRegionSelection = {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
14
21
|
const props = defineProps<{
|
|
15
22
|
available: boolean;
|
|
16
23
|
devices: AndroidDevice[];
|
|
@@ -21,6 +28,9 @@ const props = defineProps<{
|
|
|
21
28
|
actions?: readonly DeviceAction[];
|
|
22
29
|
overlayBounds?: readonly DeviceOverlayBounds[];
|
|
23
30
|
selectedBounds?: DeviceOverlayBounds;
|
|
31
|
+
selectedRegion?: DeviceOverlayBounds;
|
|
32
|
+
regionSelection?: boolean;
|
|
33
|
+
regionDrawMode?: boolean;
|
|
24
34
|
deviceWidth?: number;
|
|
25
35
|
deviceHeight?: number;
|
|
26
36
|
}>();
|
|
@@ -32,6 +42,7 @@ const emit = defineEmits<{
|
|
|
32
42
|
previewError: [];
|
|
33
43
|
refreshPreview: [];
|
|
34
44
|
tap: [point: { x: number; y: number }];
|
|
45
|
+
regionSelect: [region: DeviceRegionSelection];
|
|
35
46
|
swipe: [gesture: {
|
|
36
47
|
startX: number;
|
|
37
48
|
startY: number;
|
|
@@ -43,11 +54,14 @@ const emit = defineEmits<{
|
|
|
43
54
|
|
|
44
55
|
const imageSize = shallowRef({ width: 0, height: 0 });
|
|
45
56
|
let pointerSession: {
|
|
57
|
+
mode: 'gesture' | 'region-draw' | 'region-move';
|
|
46
58
|
pointerId: number;
|
|
47
59
|
start: { x: number; y: number };
|
|
48
60
|
startClientX: number;
|
|
49
61
|
startClientY: number;
|
|
62
|
+
origin?: DeviceOverlayBounds;
|
|
50
63
|
} | null = null;
|
|
64
|
+
const draftRegion = shallowRef<DeviceOverlayBounds | null>(null);
|
|
51
65
|
const hasPreview = computed(() => Boolean(props.frameUrl || props.imageUrl));
|
|
52
66
|
const imageBoxStyle = computed(() => {
|
|
53
67
|
if (props.frameUrl) return {};
|
|
@@ -61,6 +75,12 @@ const overlayViewBox = computed(() => {
|
|
|
61
75
|
return width && height ? `0 0 ${width} ${height}` : '';
|
|
62
76
|
});
|
|
63
77
|
|
|
78
|
+
watch(() => props.regionSelection, (enabled) => {
|
|
79
|
+
if (enabled) return;
|
|
80
|
+
pointerSession = null;
|
|
81
|
+
draftRegion.value = null;
|
|
82
|
+
});
|
|
83
|
+
|
|
64
84
|
function updateImageSize(event: Event) {
|
|
65
85
|
const image = event.target as HTMLImageElement;
|
|
66
86
|
imageSize.value = { width: image.naturalWidth, height: image.naturalHeight };
|
|
@@ -97,18 +117,109 @@ function getDevicePoint(target: HTMLElement, clientX: number, clientY: number) {
|
|
|
97
117
|
};
|
|
98
118
|
}
|
|
99
119
|
|
|
120
|
+
function boundsFromPoints(
|
|
121
|
+
id: string,
|
|
122
|
+
start: { x: number; y: number },
|
|
123
|
+
end: { x: number; y: number },
|
|
124
|
+
) {
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
left: Math.min(start.x, end.x),
|
|
128
|
+
top: Math.min(start.y, end.y),
|
|
129
|
+
right: Math.max(start.x, end.x),
|
|
130
|
+
bottom: Math.max(start.y, end.y),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function pointInBounds(point: { x: number; y: number }, bounds?: DeviceOverlayBounds) {
|
|
135
|
+
if (!bounds) return false;
|
|
136
|
+
return point.x >= bounds.left
|
|
137
|
+
&& point.x <= bounds.right
|
|
138
|
+
&& point.y >= bounds.top
|
|
139
|
+
&& point.y <= bounds.bottom;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function clampRegion(bounds: DeviceOverlayBounds) {
|
|
143
|
+
const width = props.deviceWidth || imageSize.value.width;
|
|
144
|
+
const height = props.deviceHeight || imageSize.value.height;
|
|
145
|
+
const regionWidth = Math.max(1, bounds.right - bounds.left);
|
|
146
|
+
const regionHeight = Math.max(1, bounds.bottom - bounds.top);
|
|
147
|
+
const left = Math.max(0, Math.min(bounds.left, Math.max(0, width - regionWidth)));
|
|
148
|
+
const top = Math.max(0, Math.min(bounds.top, Math.max(0, height - regionHeight)));
|
|
149
|
+
return {
|
|
150
|
+
id: bounds.id,
|
|
151
|
+
left,
|
|
152
|
+
top,
|
|
153
|
+
right: left + regionWidth,
|
|
154
|
+
bottom: top + regionHeight,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function moveBounds(
|
|
159
|
+
origin: DeviceOverlayBounds,
|
|
160
|
+
start: { x: number; y: number },
|
|
161
|
+
end: { x: number; y: number },
|
|
162
|
+
) {
|
|
163
|
+
const dx = end.x - start.x;
|
|
164
|
+
const dy = end.y - start.y;
|
|
165
|
+
return clampRegion({
|
|
166
|
+
id: origin.id,
|
|
167
|
+
left: origin.left + dx,
|
|
168
|
+
top: origin.top + dy,
|
|
169
|
+
right: origin.right + dx,
|
|
170
|
+
bottom: origin.bottom + dy,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function emitRegion(bounds: DeviceOverlayBounds) {
|
|
175
|
+
const width = bounds.right - bounds.left;
|
|
176
|
+
const height = bounds.bottom - bounds.top;
|
|
177
|
+
if (width < 2 || height < 2) return;
|
|
178
|
+
emit('regionSelect', {
|
|
179
|
+
x: bounds.left,
|
|
180
|
+
y: bounds.top,
|
|
181
|
+
width,
|
|
182
|
+
height,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
100
186
|
function handlePointerDown(event: PointerEvent) {
|
|
101
187
|
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
|
102
188
|
const target = event.currentTarget as HTMLElement;
|
|
103
189
|
const point = getDevicePoint(target, event.clientX, event.clientY);
|
|
104
190
|
if (!point) return;
|
|
191
|
+
if (props.regionSelection) {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
event.stopPropagation();
|
|
194
|
+
}
|
|
105
195
|
target.setPointerCapture(event.pointerId);
|
|
196
|
+
const isMovingRegion = props.regionSelection
|
|
197
|
+
&& !props.regionDrawMode
|
|
198
|
+
&& pointInBounds(point, props.selectedRegion);
|
|
106
199
|
pointerSession = {
|
|
200
|
+
mode: props.regionSelection ? (isMovingRegion ? 'region-move' : 'region-draw') : 'gesture',
|
|
107
201
|
pointerId: event.pointerId,
|
|
108
202
|
start: point,
|
|
109
203
|
startClientX: event.clientX,
|
|
110
204
|
startClientY: event.clientY,
|
|
205
|
+
origin: isMovingRegion && props.selectedRegion ? { ...props.selectedRegion } : undefined,
|
|
111
206
|
};
|
|
207
|
+
if (props.regionSelection) {
|
|
208
|
+
draftRegion.value = pointerSession.origin || boundsFromPoints('draft-region', point, point);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function handlePointerMove(event: PointerEvent) {
|
|
213
|
+
if (!pointerSession || pointerSession.pointerId !== event.pointerId || pointerSession.mode === 'gesture') return;
|
|
214
|
+
const target = event.currentTarget as HTMLElement;
|
|
215
|
+
const point = getDevicePoint(target, event.clientX, event.clientY);
|
|
216
|
+
if (!point) return;
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
if (pointerSession.mode === 'region-move' && pointerSession.origin) {
|
|
219
|
+
draftRegion.value = moveBounds(pointerSession.origin, pointerSession.start, point);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
draftRegion.value = boundsFromPoints('draft-region', pointerSession.start, point);
|
|
112
223
|
}
|
|
113
224
|
|
|
114
225
|
function handlePointerUp(event: PointerEvent) {
|
|
@@ -118,7 +229,17 @@ function handlePointerUp(event: PointerEvent) {
|
|
|
118
229
|
const session = pointerSession;
|
|
119
230
|
pointerSession = null;
|
|
120
231
|
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
|
|
121
|
-
if (!point)
|
|
232
|
+
if (!point) {
|
|
233
|
+
if (session.mode !== 'gesture') draftRegion.value = null;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (session.mode !== 'gesture') {
|
|
238
|
+
const bounds = draftRegion.value || boundsFromPoints('selected-region', session.start, point);
|
|
239
|
+
draftRegion.value = null;
|
|
240
|
+
emitRegion(bounds);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
122
243
|
|
|
123
244
|
const moved = Math.hypot(
|
|
124
245
|
event.clientX - session.startClientX,
|
|
@@ -139,7 +260,10 @@ function handlePointerUp(event: PointerEvent) {
|
|
|
139
260
|
}
|
|
140
261
|
|
|
141
262
|
function handlePointerCancel(event: PointerEvent) {
|
|
142
|
-
if (pointerSession?.pointerId === event.pointerId)
|
|
263
|
+
if (pointerSession?.pointerId === event.pointerId) {
|
|
264
|
+
pointerSession = null;
|
|
265
|
+
draftRegion.value = null;
|
|
266
|
+
}
|
|
143
267
|
}
|
|
144
268
|
</script>
|
|
145
269
|
|
|
@@ -208,7 +332,9 @@ function handlePointerCancel(event: PointerEvent) {
|
|
|
208
332
|
>
|
|
209
333
|
<div
|
|
210
334
|
class="device-preview__surface"
|
|
335
|
+
:class="{ 'device-preview__surface--selecting': regionSelection }"
|
|
211
336
|
@pointerdown="handlePointerDown"
|
|
337
|
+
@pointermove="handlePointerMove"
|
|
212
338
|
@pointerup="handlePointerUp"
|
|
213
339
|
@pointercancel="handlePointerCancel"
|
|
214
340
|
/>
|
|
@@ -229,7 +355,7 @@ function handlePointerCancel(event: PointerEvent) {
|
|
|
229
355
|
@error="emit('previewError')"
|
|
230
356
|
/>
|
|
231
357
|
<svg
|
|
232
|
-
v-if="overlayViewBox && (overlayBounds?.length || selectedBounds)"
|
|
358
|
+
v-if="overlayViewBox && (overlayBounds?.length || selectedBounds || selectedRegion || draftRegion)"
|
|
233
359
|
class="device-preview__overlay"
|
|
234
360
|
:viewBox="overlayViewBox"
|
|
235
361
|
preserveAspectRatio="xMidYMid meet"
|
|
@@ -254,6 +380,24 @@ function handlePointerCancel(event: PointerEvent) {
|
|
|
254
380
|
:height="Math.max(0, selectedBounds.bottom - selectedBounds.top)"
|
|
255
381
|
vector-effect="non-scaling-stroke"
|
|
256
382
|
/>
|
|
383
|
+
<rect
|
|
384
|
+
v-if="selectedRegion && !draftRegion"
|
|
385
|
+
class="device-preview__selected-region"
|
|
386
|
+
:x="selectedRegion.left"
|
|
387
|
+
:y="selectedRegion.top"
|
|
388
|
+
:width="Math.max(0, selectedRegion.right - selectedRegion.left)"
|
|
389
|
+
:height="Math.max(0, selectedRegion.bottom - selectedRegion.top)"
|
|
390
|
+
vector-effect="non-scaling-stroke"
|
|
391
|
+
/>
|
|
392
|
+
<rect
|
|
393
|
+
v-if="draftRegion"
|
|
394
|
+
class="device-preview__draft-region"
|
|
395
|
+
:x="draftRegion.left"
|
|
396
|
+
:y="draftRegion.top"
|
|
397
|
+
:width="Math.max(0, draftRegion.right - draftRegion.left)"
|
|
398
|
+
:height="Math.max(0, draftRegion.bottom - draftRegion.top)"
|
|
399
|
+
vector-effect="non-scaling-stroke"
|
|
400
|
+
/>
|
|
257
401
|
</svg>
|
|
258
402
|
</div>
|
|
259
403
|
<div v-else class="device-preview__empty">
|
package/src/style.css
CHANGED
|
@@ -1251,6 +1251,10 @@ select {
|
|
|
1251
1251
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1252
1252
|
}
|
|
1253
1253
|
|
|
1254
|
+
.appium-flow-editor__grid--visual {
|
|
1255
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1254
1258
|
.appium-swipe-message-box {
|
|
1255
1259
|
width: min(560px, calc(100vw - 32px));
|
|
1256
1260
|
}
|
|
@@ -1261,6 +1265,26 @@ select {
|
|
|
1261
1265
|
gap: 10px 12px;
|
|
1262
1266
|
}
|
|
1263
1267
|
|
|
1268
|
+
.appium-visual-change-dialog {
|
|
1269
|
+
margin-left: auto;
|
|
1270
|
+
margin-right: 32px;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
.appium-visual-change-dialog .el-dialog__body {
|
|
1274
|
+
padding-top: 8px;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.appium-visual-change-form {
|
|
1278
|
+
display: grid;
|
|
1279
|
+
gap: 10px;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
.appium-visual-change-form__grid {
|
|
1283
|
+
display: grid;
|
|
1284
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1285
|
+
gap: 10px 12px;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1264
1288
|
.appium-flow-insert {
|
|
1265
1289
|
position: relative;
|
|
1266
1290
|
z-index: 1;
|
|
@@ -2332,6 +2356,10 @@ select {
|
|
|
2332
2356
|
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3E%3Ccircle cx='14' cy='14' r='7' fill='none' stroke='%231f2937' stroke-width='2'/%3E%3Ccircle cx='14' cy='14' r='2' fill='%231f2937'/%3E%3C/svg%3E") 14 14, crosshair;
|
|
2333
2357
|
}
|
|
2334
2358
|
|
|
2359
|
+
.device-preview__surface--selecting {
|
|
2360
|
+
cursor: crosshair;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2335
2363
|
.device-preview__frame {
|
|
2336
2364
|
width: 100%;
|
|
2337
2365
|
height: 100%;
|
|
@@ -2366,6 +2394,17 @@ select {
|
|
|
2366
2394
|
stroke-width: 2;
|
|
2367
2395
|
}
|
|
2368
2396
|
|
|
2397
|
+
.device-preview__selected-region,
|
|
2398
|
+
.device-preview__draft-region {
|
|
2399
|
+
fill: rgba(22, 119, 255, 0.16);
|
|
2400
|
+
stroke: #1677ff;
|
|
2401
|
+
stroke-width: 2;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
.device-preview__draft-region {
|
|
2405
|
+
stroke-dasharray: 8 5;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2369
2408
|
@media (max-width: 1200px) {
|
|
2370
2409
|
.device-actions {
|
|
2371
2410
|
flex-wrap: wrap;
|