@loadmill/droid-cua 2.4.0 → 2.5.0
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/build/src/device/android/actions.js +11 -7
- package/build/src/device/cloud/actions.js +13 -8
- package/build/src/device/ios/actions.js +13 -9
- package/build/src/device/screenshot-resolution.js +33 -0
- package/build/src/device/scroll-gesture.js +20 -0
- package/build/src/test-store/test-manager.js +3 -5
- package/build/src/test-store/test-script.js +50 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { logger } from "../../utils/logger.js";
|
|
2
2
|
import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
|
|
3
3
|
import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
|
|
4
|
+
import { resolveScrollGesture } from "../scroll-gesture.js";
|
|
4
5
|
import { execAdb } from "./tools.js";
|
|
5
6
|
function adbShell(deviceId, command) {
|
|
6
7
|
return execAdb(["-s", deviceId, "shell", command]);
|
|
@@ -74,13 +75,16 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
74
75
|
await adbShell(deviceId, `input tap ${realX} ${realY}`);
|
|
75
76
|
break;
|
|
76
77
|
case "scroll":
|
|
77
|
-
const scrollX =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
const { scrollX, scrollY, startX, startY, endX, endY, hasAnchor } = resolveScrollGesture(action, {
|
|
79
|
+
scale,
|
|
80
|
+
fallbackStartX: 500,
|
|
81
|
+
fallbackStartY: 500
|
|
82
|
+
});
|
|
83
|
+
addOutput({
|
|
84
|
+
type: 'action',
|
|
85
|
+
text: `Scrolling from (${startX}, ${startY}) to (${endX}, ${endY}) by (${scrollX}, ${scrollY})`,
|
|
86
|
+
...meta({ scrollX, scrollY, startX, startY, endX, endY, anchorSource: hasAnchor ? 'action' : 'fallback', unit: 'px' })
|
|
87
|
+
});
|
|
84
88
|
await adbShell(deviceId, `input swipe ${startX} ${startY} ${endX} ${endY} 500`);
|
|
85
89
|
break;
|
|
86
90
|
case "drag":
|
|
@@ -2,6 +2,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
2
2
|
import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
|
|
3
3
|
import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
|
|
4
4
|
import { getActiveSession, getDevicePixelRatio } from "./connection.js";
|
|
5
|
+
import { resolveScrollGesture } from "../scroll-gesture.js";
|
|
5
6
|
function normalizeMobileKeypress(platform, keys = []) {
|
|
6
7
|
if (!Array.isArray(keys) || keys.length === 0) {
|
|
7
8
|
throw new Error("Keypress action is missing keys");
|
|
@@ -93,14 +94,18 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
93
94
|
break;
|
|
94
95
|
}
|
|
95
96
|
case "scroll": {
|
|
96
|
-
const scrollX =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
addOutput({
|
|
103
|
-
|
|
97
|
+
const { scrollX, scrollY, startX, startY, endX, endY, hasAnchor } = resolveScrollGesture(action, {
|
|
98
|
+
scale,
|
|
99
|
+
dpr,
|
|
100
|
+
fallbackStartX: 200,
|
|
101
|
+
fallbackStartY: 400
|
|
102
|
+
});
|
|
103
|
+
addOutput({
|
|
104
|
+
type: "action",
|
|
105
|
+
text: `Scrolling from (${startX}, ${startY}) to (${endX}, ${endY}) by (${scrollX}, ${scrollY})`,
|
|
106
|
+
...meta({ scrollX, scrollY, startX, startY, endX, endY, anchorSource: hasAnchor ? "action" : "fallback" })
|
|
107
|
+
});
|
|
108
|
+
await session.client.scroll(session.sessionId, startX, startY, endX, endY);
|
|
104
109
|
break;
|
|
105
110
|
}
|
|
106
111
|
case "drag": {
|
|
@@ -8,6 +8,7 @@ import { getActiveSession, getDevicePixelRatio } from "./connection.js";
|
|
|
8
8
|
import { logger } from "../../utils/logger.js";
|
|
9
9
|
import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
|
|
10
10
|
import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
|
|
11
|
+
import { resolveScrollGesture } from "../scroll-gesture.js";
|
|
11
12
|
function normalizeMobileKeypress(keys = []) {
|
|
12
13
|
if (!Array.isArray(keys) || keys.length === 0) {
|
|
13
14
|
throw new Error("Keypress action is missing keys");
|
|
@@ -92,15 +93,18 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
|
|
|
92
93
|
}
|
|
93
94
|
case "scroll": {
|
|
94
95
|
const dpr = getDevicePixelRatio();
|
|
95
|
-
const scrollX =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
const { scrollX, scrollY, startX, startY, endX, endY, hasAnchor } = resolveScrollGesture(action, {
|
|
97
|
+
scale,
|
|
98
|
+
dpr,
|
|
99
|
+
fallbackStartX: 197,
|
|
100
|
+
fallbackStartY: 426
|
|
101
|
+
});
|
|
102
|
+
addOutput({
|
|
103
|
+
type: "action",
|
|
104
|
+
text: `Scrolling from (${startX}, ${startY}) to (${endX}, ${endY}) by (${scrollX}, ${scrollY}) points`,
|
|
105
|
+
...meta({ scrollX, scrollY, startX, startY, endX, endY, anchorSource: hasAnchor ? "action" : "fallback", unit: "points" })
|
|
106
|
+
});
|
|
107
|
+
await appium.scroll(session.sessionId, startX, startY, endX, endY);
|
|
104
108
|
break;
|
|
105
109
|
}
|
|
106
110
|
case "drag": {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const TARGET_SCALED_WIDTH = 400;
|
|
2
|
+
export const SCREENSHOT_RESOLUTION_MODE_DOWNSCALED = "downscaled";
|
|
3
|
+
export const SCREENSHOT_RESOLUTION_MODE_NATIVE = "native";
|
|
4
|
+
export function normalizeScreenshotResolutionMode(value) {
|
|
5
|
+
return value === SCREENSHOT_RESOLUTION_MODE_NATIVE
|
|
6
|
+
? SCREENSHOT_RESOLUTION_MODE_NATIVE
|
|
7
|
+
: SCREENSHOT_RESOLUTION_MODE_DOWNSCALED;
|
|
8
|
+
}
|
|
9
|
+
export function validateScreenshotResolutionMode(value, label) {
|
|
10
|
+
if (typeof value !== "string") {
|
|
11
|
+
throw new Error(`${label} must be one of: downscaled, native.`);
|
|
12
|
+
}
|
|
13
|
+
const normalized = normalizeScreenshotResolutionMode(value);
|
|
14
|
+
if (normalized !== value) {
|
|
15
|
+
throw new Error(`${label} must be one of: downscaled, native.`);
|
|
16
|
+
}
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
export function buildResolutionAwareDeviceInfo({ width, height, screenshotResolutionMode, }) {
|
|
20
|
+
const normalizedMode = normalizeScreenshotResolutionMode(screenshotResolutionMode);
|
|
21
|
+
const scale = normalizedMode === SCREENSHOT_RESOLUTION_MODE_NATIVE || width <= TARGET_SCALED_WIDTH
|
|
22
|
+
? 1.0
|
|
23
|
+
: TARGET_SCALED_WIDTH / width;
|
|
24
|
+
return {
|
|
25
|
+
scaled_width: Math.round(width * scale),
|
|
26
|
+
scaled_height: Math.round(height * scale),
|
|
27
|
+
scale,
|
|
28
|
+
screenshot_resolution_mode: normalizedMode,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function readScreenshotResolutionModeFromEnv() {
|
|
32
|
+
return normalizeScreenshotResolutionMode(process.env.DROID_CUA_SCREENSHOT_RESOLUTION_MODE);
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The model returns scroll actions with `scroll_x` / `scroll_y` plus optional
|
|
2
|
+
// anchor coordinates `x` / `y`. This helper converts those model coordinates
|
|
3
|
+
// into backend gesture coordinates: startX/startY and endX/endY.
|
|
4
|
+
export function resolveScrollGesture(action, { scale = 1.0, dpr = 1.0, fallbackStartX = 0, fallbackStartY = 0 } = {}) {
|
|
5
|
+
const divisor = scale * dpr;
|
|
6
|
+
const scrollX = Math.round((action?.scroll_x ?? 0) / divisor);
|
|
7
|
+
const scrollY = Math.round((action?.scroll_y ?? 0) / divisor);
|
|
8
|
+
const hasAnchor = Number.isFinite(action?.x) && Number.isFinite(action?.y);
|
|
9
|
+
const startX = hasAnchor ? Math.round(action.x / divisor) : Math.round(fallbackStartX);
|
|
10
|
+
const startY = hasAnchor ? Math.round(action.y / divisor) : Math.round(fallbackStartY);
|
|
11
|
+
return {
|
|
12
|
+
scrollX,
|
|
13
|
+
scrollY,
|
|
14
|
+
startX,
|
|
15
|
+
startY,
|
|
16
|
+
endX: startX + scrollX,
|
|
17
|
+
endY: startY - scrollY,
|
|
18
|
+
hasAnchor
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile, writeFile, unlink, stat, mkdir } from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { countExecutableInstructions, parseTestScript } from "./test-script.js";
|
|
3
4
|
// Tests directory is relative to current working directory
|
|
4
5
|
const TESTS_DIR = path.join(process.cwd(), "tests");
|
|
5
6
|
/**
|
|
@@ -27,10 +28,7 @@ export async function loadTest(name) {
|
|
|
27
28
|
const filename = name.endsWith(".dcua") ? name : `${name}.dcua`;
|
|
28
29
|
const filepath = path.join(TESTS_DIR, filename);
|
|
29
30
|
const content = await readFile(filepath, "utf-8");
|
|
30
|
-
return content
|
|
31
|
-
.split("\n")
|
|
32
|
-
.map(line => line.trim())
|
|
33
|
-
.filter(line => line.length > 0);
|
|
31
|
+
return parseTestScript(content);
|
|
34
32
|
}
|
|
35
33
|
/**
|
|
36
34
|
* Get the raw content of a test file
|
|
@@ -60,7 +58,7 @@ export async function listTests() {
|
|
|
60
58
|
const filepath = path.join(TESTS_DIR, filename);
|
|
61
59
|
const stats = await stat(filepath);
|
|
62
60
|
const content = await readFile(filepath, "utf-8");
|
|
63
|
-
const lines = content
|
|
61
|
+
const lines = countExecutableInstructions(content);
|
|
64
62
|
return {
|
|
65
63
|
name: filename.replace(".dcua", ""),
|
|
66
64
|
filename: filename,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the first comment marker in a test script line.
|
|
3
|
+
* `//` starts a comment only at line start or when preceded by whitespace.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} line
|
|
6
|
+
* @returns {number}
|
|
7
|
+
*/
|
|
8
|
+
export function findCommentStart(line) {
|
|
9
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
10
|
+
if (line[i] !== "/" || line[i + 1] !== "/") {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (i === 0 || /\s/.test(line[i - 1])) {
|
|
14
|
+
return i;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return -1;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Remove inline comments and surrounding instruction whitespace from a line.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} line
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export function stripInstructionComment(line) {
|
|
26
|
+
const commentStart = findCommentStart(line);
|
|
27
|
+
const instruction = commentStart >= 0 ? line.slice(0, commentStart) : line;
|
|
28
|
+
return instruction.trim();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse executable instructions from raw .dcua content.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} content
|
|
34
|
+
* @returns {string[]}
|
|
35
|
+
*/
|
|
36
|
+
export function parseTestScript(content) {
|
|
37
|
+
return content
|
|
38
|
+
.split("\n")
|
|
39
|
+
.map(stripInstructionComment)
|
|
40
|
+
.filter((line) => line.length > 0);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Count executable instructions in raw .dcua content.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} content
|
|
46
|
+
* @returns {number}
|
|
47
|
+
*/
|
|
48
|
+
export function countExecutableInstructions(content) {
|
|
49
|
+
return parseTestScript(content).length;
|
|
50
|
+
}
|