@ceraph/react-native-mcp 0.2.2 → 0.3.2
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/LICENSE +116 -15
- package/README.md +79 -77
- package/assets/default.png +0 -0
- package/dist/app-lifecycle.d.ts +50 -0
- package/dist/app-lifecycle.js +487 -0
- package/dist/camera-image-writer.d.ts +43 -0
- package/dist/camera-image-writer.js +280 -0
- package/dist/camera-registry-sync.d.ts +18 -0
- package/dist/camera-registry-sync.js +117 -0
- package/dist/cli.d.ts +0 -7
- package/dist/cli.js +41 -9
- package/dist/device-autonomy.d.ts +30 -0
- package/dist/device-autonomy.js +117 -0
- package/dist/error-parser.d.ts +6 -26
- package/dist/error-parser.js +4 -74
- package/dist/expo-manager.d.ts +2 -74
- package/dist/expo-manager.js +11 -125
- package/dist/index.d.ts +0 -7
- package/dist/index.js +1266 -56
- package/dist/init/ast-camera.d.ts +29 -0
- package/dist/init/ast-camera.js +267 -0
- package/dist/init/ast-layout.d.ts +15 -0
- package/dist/init/ast-layout.js +167 -0
- package/dist/init/claude-hook-constants.d.ts +9 -0
- package/dist/init/claude-hook-constants.js +91 -0
- package/dist/init/lan-ip.d.ts +11 -0
- package/dist/init/lan-ip.js +51 -0
- package/dist/init/monorepo.d.ts +13 -0
- package/dist/init/monorepo.js +185 -0
- package/dist/init/oauth.d.ts +52 -0
- package/dist/init/oauth.js +220 -0
- package/dist/init/package-manager.d.ts +11 -0
- package/dist/init/package-manager.js +60 -0
- package/dist/init/prompt.d.ts +12 -0
- package/dist/init/prompt.js +68 -0
- package/dist/init/shell-profile.d.ts +22 -0
- package/dist/init/shell-profile.js +85 -0
- package/dist/init/steps.d.ts +135 -0
- package/dist/init/steps.js +399 -0
- package/dist/init/url-scheme.d.ts +42 -0
- package/dist/init/url-scheme.js +187 -0
- package/dist/init/walkthrough.d.ts +76 -0
- package/dist/init/walkthrough.js +340 -0
- package/dist/init.d.ts +7 -7
- package/dist/init.js +280 -120
- package/dist/iproxy-manager.d.ts +32 -0
- package/dist/iproxy-manager.js +216 -0
- package/dist/mac-caffeinate.d.ts +10 -0
- package/dist/mac-caffeinate.js +56 -0
- package/dist/permission-interceptor.d.ts +29 -0
- package/dist/permission-interceptor.js +185 -0
- package/dist/prebuild-detector.d.ts +0 -30
- package/dist/prebuild-detector.js +1 -42
- package/dist/preflight.d.ts +34 -0
- package/dist/preflight.js +847 -0
- package/dist/screen.d.ts +132 -43
- package/dist/screen.js +668 -94
- package/dist/shim/boot.d.ts +41 -0
- package/dist/shim/boot.js +141 -0
- package/dist/shim/camera.d.ts +22 -0
- package/dist/shim/camera.js +62 -0
- package/dist/shim/config.d.ts +6 -0
- package/dist/shim/config.js +56 -0
- package/dist/shim/deep-link.d.ts +1 -0
- package/dist/shim/deep-link.js +25 -0
- package/dist/shim/dev-guard.d.ts +1 -0
- package/dist/shim/dev-guard.js +3 -0
- package/dist/shim/error-handler.d.ts +20 -0
- package/dist/shim/error-handler.js +66 -0
- package/dist/shim/fetch-interceptor.d.ts +13 -0
- package/dist/shim/fetch-interceptor.js +93 -0
- package/dist/shim/index.d.ts +6 -0
- package/dist/shim/index.js +6 -0
- package/dist/shim/keep-awake.d.ts +13 -0
- package/dist/shim/keep-awake.js +118 -0
- package/dist/shim/reload.d.ts +23 -0
- package/dist/shim/reload.js +76 -0
- package/dist/shim/signal-capture.d.ts +11 -0
- package/dist/shim/signal-capture.js +15 -0
- package/dist/shim/signal-transport.d.ts +17 -0
- package/dist/shim/signal-transport.js +43 -0
- package/dist/signal-listener.d.ts +27 -0
- package/dist/signal-listener.js +135 -0
- package/dist/simulator-boot.d.ts +52 -0
- package/dist/simulator-boot.js +227 -0
- package/dist/target.d.ts +48 -0
- package/dist/target.js +267 -0
- package/dist/uninstall/cli-runner.d.ts +32 -0
- package/dist/uninstall/cli-runner.js +223 -0
- package/dist/uninstall/footprint.d.ts +40 -0
- package/dist/uninstall/footprint.js +288 -0
- package/dist/uninstall/mcp-tools.d.ts +14 -0
- package/dist/uninstall/mcp-tools.js +175 -0
- package/dist/uninstall/revert-auth.d.ts +22 -0
- package/dist/uninstall/revert-auth.js +31 -0
- package/dist/uninstall/revert-boot.d.ts +24 -0
- package/dist/uninstall/revert-boot.js +242 -0
- package/dist/uninstall/revert-camera.d.ts +12 -0
- package/dist/uninstall/revert-camera.js +199 -0
- package/dist/uninstall/revert-ceraph-dir.d.ts +27 -0
- package/dist/uninstall/revert-ceraph-dir.js +38 -0
- package/dist/uninstall/revert-claude-hooks.d.ts +19 -0
- package/dist/uninstall/revert-claude-hooks.js +191 -0
- package/dist/uninstall/revert-gitignore.d.ts +17 -0
- package/dist/uninstall/revert-gitignore.js +43 -0
- package/dist/uninstall/revert-mcp-clients.d.ts +57 -0
- package/dist/uninstall/revert-mcp-clients.js +194 -0
- package/dist/uninstall/revert-package.d.ts +34 -0
- package/dist/uninstall/revert-package.js +98 -0
- package/dist/uninstall/revert-scheme.d.ts +36 -0
- package/dist/uninstall/revert-scheme.js +139 -0
- package/dist/uninstall/revert-signal-host-env.d.ts +31 -0
- package/dist/uninstall/revert-signal-host-env.js +61 -0
- package/dist/uninstall/walkthrough.d.ts +80 -0
- package/dist/uninstall/walkthrough.js +1244 -0
- package/dist/utils/atomic-write.d.ts +1 -0
- package/dist/utils/atomic-write.js +30 -0
- package/dist/wait-for-device.d.ts +68 -0
- package/dist/wait-for-device.js +368 -0
- package/dist/wda-manager.d.ts +38 -0
- package/dist/wda-manager.js +186 -0
- package/dist/wda-simulator.d.ts +28 -0
- package/dist/wda-simulator.js +257 -0
- package/package.json +59 -5
package/dist/screen.js
CHANGED
|
@@ -1,67 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
* WebDriverAgent client with pixel ratio correction.
|
|
3
|
-
*
|
|
4
|
-
* Talks to the WDA HTTP server running on localhost:8100 to perform
|
|
5
|
-
* taps and element lookups on a connected iOS device.
|
|
6
|
-
*/
|
|
7
|
-
const WDA_BASE_URL = "http://localhost:8100";
|
|
1
|
+
import { DEFAULT_DEVICE_WDA_BASE_URL, TargetResolver } from "./target.js";
|
|
8
2
|
const WDA_TIMEOUT_MS = 5000;
|
|
9
|
-
/**
|
|
10
|
-
* Fetch wrapper with timeout for WDA requests.
|
|
11
|
-
*/
|
|
12
|
-
async function wdaFetch(path, options = {}) {
|
|
13
|
-
const url = `${WDA_BASE_URL}${path}`;
|
|
14
|
-
const controller = new AbortController();
|
|
15
|
-
const timeout = setTimeout(() => controller.abort(), WDA_TIMEOUT_MS);
|
|
16
|
-
try {
|
|
17
|
-
const response = await fetch(url, {
|
|
18
|
-
...options,
|
|
19
|
-
signal: controller.signal,
|
|
20
|
-
headers: {
|
|
21
|
-
"Content-Type": "application/json",
|
|
22
|
-
...options.headers,
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
return response;
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
clearTimeout(timeout);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
3
|
export class ScreenManager {
|
|
32
4
|
sessionId = null;
|
|
33
5
|
pixelRatio = null;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
6
|
+
targetResolver;
|
|
7
|
+
sessionBaseUrl = null;
|
|
8
|
+
constructor(opts = {}) {
|
|
9
|
+
this.targetResolver = opts.targetResolver ?? new TargetResolver();
|
|
10
|
+
}
|
|
11
|
+
async snapshotTarget() {
|
|
12
|
+
try {
|
|
13
|
+
const info = await this.targetResolver.resolve();
|
|
14
|
+
return { baseUrl: info.baseUrl, ready: info.wdaReady };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { baseUrl: DEFAULT_DEVICE_WDA_BASE_URL, ready: true };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async wdaFetchAt(baseUrl, path, options = {}, timeoutMs = WDA_TIMEOUT_MS) {
|
|
21
|
+
const url = `${baseUrl}${path}`;
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
...options,
|
|
27
|
+
signal: controller.signal,
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
...options.headers,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async wdaFetch(path, options = {}, timeoutMs = WDA_TIMEOUT_MS) {
|
|
40
|
+
const snapshot = await this.snapshotTarget();
|
|
41
|
+
if (!snapshot.ready) {
|
|
42
|
+
throw new Error("WebDriverAgent is not ready for the selected target. If you " +
|
|
43
|
+
"set CERAPH_TARGET=simulator (or are using a simulator with " +
|
|
44
|
+
"auto-detection), call `rn_wda_start` first to build and " +
|
|
45
|
+
"launch WDA against the simulator.");
|
|
46
|
+
}
|
|
47
|
+
const baseUrl = this.sessionBaseUrl ?? snapshot.baseUrl;
|
|
48
|
+
return this.wdaFetchAt(baseUrl, path, options, timeoutMs);
|
|
49
|
+
}
|
|
37
50
|
async isAvailable() {
|
|
51
|
+
const snapshot = await this.snapshotTarget();
|
|
52
|
+
if (!snapshot.ready)
|
|
53
|
+
return false;
|
|
38
54
|
try {
|
|
39
|
-
const res = await
|
|
55
|
+
const res = await this.wdaFetchAt(snapshot.baseUrl, "/status");
|
|
40
56
|
return res.ok;
|
|
41
57
|
}
|
|
42
58
|
catch {
|
|
43
59
|
return false;
|
|
44
60
|
}
|
|
45
61
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Get or create a WDA session. Sessions are cached and revalidated.
|
|
48
|
-
*/
|
|
49
62
|
async ensureSession() {
|
|
50
|
-
|
|
63
|
+
const snapshot = await this.snapshotTarget();
|
|
64
|
+
if (!snapshot.ready) {
|
|
65
|
+
throw new Error("WebDriverAgent is not ready for the selected target. If you " +
|
|
66
|
+
"set CERAPH_TARGET=simulator (or are using a simulator with " +
|
|
67
|
+
"auto-detection), call `rn_wda_start` first to build and " +
|
|
68
|
+
"launch WDA against the simulator.");
|
|
69
|
+
}
|
|
70
|
+
const currentBaseUrl = snapshot.baseUrl;
|
|
71
|
+
if (this.sessionBaseUrl && this.sessionBaseUrl !== currentBaseUrl) {
|
|
72
|
+
this.sessionId = null;
|
|
73
|
+
this.pixelRatio = null;
|
|
74
|
+
}
|
|
75
|
+
this.sessionBaseUrl = currentBaseUrl;
|
|
51
76
|
if (this.sessionId) {
|
|
52
77
|
try {
|
|
53
|
-
const res = await
|
|
78
|
+
const res = await this.wdaFetchAt(currentBaseUrl, `/session/${this.sessionId}`);
|
|
54
79
|
if (res.ok)
|
|
55
80
|
return this.sessionId;
|
|
56
81
|
}
|
|
57
82
|
catch {
|
|
58
|
-
// Session is stale, create a new one
|
|
59
83
|
}
|
|
60
84
|
this.sessionId = null;
|
|
61
85
|
}
|
|
62
|
-
// Try to find an existing session from /status
|
|
63
86
|
try {
|
|
64
|
-
const statusRes = await
|
|
87
|
+
const statusRes = await this.wdaFetchAt(currentBaseUrl, "/status");
|
|
65
88
|
if (statusRes.ok) {
|
|
66
89
|
const statusData = (await statusRes.json());
|
|
67
90
|
const existingId = statusData.sessionId || statusData.value?.sessionId;
|
|
@@ -72,10 +95,8 @@ export class ScreenManager {
|
|
|
72
95
|
}
|
|
73
96
|
}
|
|
74
97
|
catch {
|
|
75
|
-
// Fall through to session creation
|
|
76
98
|
}
|
|
77
|
-
|
|
78
|
-
const res = await wdaFetch("/session", {
|
|
99
|
+
const res = await this.wdaFetchAt(currentBaseUrl, "/session", {
|
|
79
100
|
method: "POST",
|
|
80
101
|
body: JSON.stringify({
|
|
81
102
|
capabilities: {
|
|
@@ -95,63 +116,51 @@ export class ScreenManager {
|
|
|
95
116
|
this.sessionId = sessionId;
|
|
96
117
|
return sessionId;
|
|
97
118
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Determine the pixel ratio between screenshot coordinates and
|
|
100
|
-
* device logical coordinates.
|
|
101
|
-
*
|
|
102
|
-
* Screenshot images are in physical pixels; WDA taps use logical points.
|
|
103
|
-
* The ratio is typically 2 (for @2x Retina) or 3 (for @3x).
|
|
104
|
-
*/
|
|
105
119
|
async getPixelRatio() {
|
|
106
120
|
if (this.pixelRatio)
|
|
107
121
|
return this.pixelRatio;
|
|
108
122
|
const sessionId = await this.ensureSession();
|
|
109
|
-
|
|
110
|
-
const windowRes = await wdaFetch(`/session/${sessionId}/window/size`);
|
|
123
|
+
const windowRes = await this.wdaFetch(`/session/${sessionId}/window/size`);
|
|
111
124
|
if (!windowRes.ok) {
|
|
112
125
|
throw new Error(`Failed to get window size: ${windowRes.status}`);
|
|
113
126
|
}
|
|
114
127
|
const windowData = (await windowRes.json());
|
|
115
128
|
const logicalWidth = windowData.value.width;
|
|
116
|
-
|
|
117
|
-
const screenshotRes = await wdaFetch(`/session/${sessionId}/screenshot`);
|
|
129
|
+
const screenshotRes = await this.wdaFetch(`/session/${sessionId}/screenshot`);
|
|
118
130
|
if (!screenshotRes.ok) {
|
|
119
|
-
// Fallback: assume 3x for modern iPhones
|
|
120
131
|
this.pixelRatio = 3;
|
|
121
132
|
return this.pixelRatio;
|
|
122
133
|
}
|
|
123
134
|
const screenshotData = (await screenshotRes.json());
|
|
124
135
|
const base64 = screenshotData.value;
|
|
125
|
-
// Decode the PNG header to get image width.
|
|
126
|
-
// PNG stores width as a big-endian 32-bit int at byte offset 16.
|
|
127
136
|
const pngBuffer = Buffer.from(base64, "base64");
|
|
128
137
|
if (pngBuffer.length < 24) {
|
|
129
|
-
// Not enough data to read PNG header; fallback
|
|
130
138
|
this.pixelRatio = 3;
|
|
131
139
|
return this.pixelRatio;
|
|
132
140
|
}
|
|
133
141
|
const imageWidth = pngBuffer.readUInt32BE(16);
|
|
134
142
|
this.pixelRatio = Math.round(imageWidth / logicalWidth);
|
|
135
|
-
// Sanity check
|
|
136
143
|
if (this.pixelRatio < 1 || this.pixelRatio > 4) {
|
|
137
|
-
this.pixelRatio = 3;
|
|
144
|
+
this.pixelRatio = 3;
|
|
138
145
|
}
|
|
139
146
|
return this.pixelRatio;
|
|
140
147
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
async getWindowSize() {
|
|
149
|
+
const sessionId = await this.ensureSession();
|
|
150
|
+
const res = await this.wdaFetch(`/session/${sessionId}/window/size`);
|
|
151
|
+
if (!res.ok) {
|
|
152
|
+
throw new Error(`Failed to get window size: ${res.status}`);
|
|
153
|
+
}
|
|
154
|
+
const data = (await res.json());
|
|
155
|
+
return data.value;
|
|
156
|
+
}
|
|
148
157
|
async tap(x, y, fromScreenshot = true) {
|
|
149
158
|
try {
|
|
150
159
|
const sessionId = await this.ensureSession();
|
|
151
160
|
const ratio = await this.getPixelRatio();
|
|
152
161
|
const correctedX = fromScreenshot ? x / ratio : x;
|
|
153
162
|
const correctedY = fromScreenshot ? y / ratio : y;
|
|
154
|
-
const res = await wdaFetch(`/session/${sessionId}/wda/tap/0`, {
|
|
163
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/tap/0`, {
|
|
155
164
|
method: "POST",
|
|
156
165
|
body: JSON.stringify({ x: correctedX, y: correctedY }),
|
|
157
166
|
});
|
|
@@ -186,14 +195,10 @@ export class ScreenManager {
|
|
|
186
195
|
};
|
|
187
196
|
}
|
|
188
197
|
}
|
|
189
|
-
/**
|
|
190
|
-
* Find an element in the UI tree and tap its center.
|
|
191
|
-
*/
|
|
192
198
|
async findAndTap(query) {
|
|
193
199
|
try {
|
|
194
200
|
const sessionId = await this.ensureSession();
|
|
195
|
-
|
|
196
|
-
const sourceRes = await wdaFetch(`/session/${sessionId}/source?format=json`);
|
|
201
|
+
const sourceRes = await this.wdaFetch(`/session/${sessionId}/source?format=json`);
|
|
197
202
|
if (!sourceRes.ok) {
|
|
198
203
|
return {
|
|
199
204
|
success: false,
|
|
@@ -202,10 +207,8 @@ export class ScreenManager {
|
|
|
202
207
|
}
|
|
203
208
|
const sourceData = (await sourceRes.json());
|
|
204
209
|
const root = sourceData.value;
|
|
205
|
-
// Find matching elements
|
|
206
210
|
const matches = this.searchElements(root, query);
|
|
207
211
|
if (matches.length === 0) {
|
|
208
|
-
// Collect a summary of visible elements for debugging
|
|
209
212
|
const visible = this.collectVisibleElements(root, 50);
|
|
210
213
|
return {
|
|
211
214
|
success: false,
|
|
@@ -213,7 +216,6 @@ export class ScreenManager {
|
|
|
213
216
|
error: `No element found matching query: ${JSON.stringify(query)}`,
|
|
214
217
|
};
|
|
215
218
|
}
|
|
216
|
-
// Select the element (by index or first match)
|
|
217
219
|
const index = query.index ?? 0;
|
|
218
220
|
if (index >= matches.length) {
|
|
219
221
|
return {
|
|
@@ -234,11 +236,9 @@ export class ScreenManager {
|
|
|
234
236
|
error: "Matched element has no bounds/rect information.",
|
|
235
237
|
};
|
|
236
238
|
}
|
|
237
|
-
// Calculate center point (already in device/logical coordinates)
|
|
238
239
|
const centerX = rect.x + rect.width / 2;
|
|
239
240
|
const centerY = rect.y + rect.height / 2;
|
|
240
|
-
|
|
241
|
-
const tapRes = await wdaFetch(`/session/${sessionId}/wda/tap/0`, {
|
|
241
|
+
const tapRes = await this.wdaFetch(`/session/${sessionId}/wda/tap/0`, {
|
|
242
242
|
method: "POST",
|
|
243
243
|
body: JSON.stringify({ x: centerX, y: centerY }),
|
|
244
244
|
});
|
|
@@ -274,9 +274,593 @@ export class ScreenManager {
|
|
|
274
274
|
};
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
async findElement(query) {
|
|
278
|
+
try {
|
|
279
|
+
const sessionId = await this.ensureSession();
|
|
280
|
+
const sourceRes = await this.wdaFetch(`/session/${sessionId}/source?format=json`);
|
|
281
|
+
if (!sourceRes.ok) {
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
error: `Failed to get element tree: ${sourceRes.status}`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const sourceData = (await sourceRes.json());
|
|
288
|
+
const matches = this.searchElements(sourceData.value, query);
|
|
289
|
+
if (matches.length === 0) {
|
|
290
|
+
return { success: false, matchCount: 0 };
|
|
291
|
+
}
|
|
292
|
+
const index = query.index ?? 0;
|
|
293
|
+
if (index >= matches.length) {
|
|
294
|
+
return { success: false, matchCount: matches.length };
|
|
295
|
+
}
|
|
296
|
+
return { success: true, element: matches[index], matchCount: matches.length };
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
return {
|
|
300
|
+
success: false,
|
|
301
|
+
error: err instanceof Error ? err.message : "Unknown error during findElement",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async swipe(opts) {
|
|
306
|
+
try {
|
|
307
|
+
const sessionId = await this.ensureSession();
|
|
308
|
+
const windowSize = await this.getWindowSize();
|
|
309
|
+
const ratio = await this.getPixelRatio();
|
|
310
|
+
const fromScreenshot = (opts.coordinateSource ?? "device") === "screenshot";
|
|
311
|
+
let fromX = opts.from?.x ?? windowSize.width / 2;
|
|
312
|
+
let fromY = opts.from?.y ?? windowSize.height / 2;
|
|
313
|
+
if (fromScreenshot) {
|
|
314
|
+
fromX = fromX / ratio;
|
|
315
|
+
fromY = fromY / ratio;
|
|
316
|
+
}
|
|
317
|
+
const axis = opts.direction === "up" || opts.direction === "down"
|
|
318
|
+
? windowSize.height
|
|
319
|
+
: windowSize.width;
|
|
320
|
+
const distance = opts.distancePx ?? Math.floor(axis * 0.6);
|
|
321
|
+
let toX = fromX;
|
|
322
|
+
let toY = fromY;
|
|
323
|
+
switch (opts.direction) {
|
|
324
|
+
case "up":
|
|
325
|
+
toY = fromY - distance;
|
|
326
|
+
break;
|
|
327
|
+
case "down":
|
|
328
|
+
toY = fromY + distance;
|
|
329
|
+
break;
|
|
330
|
+
case "left":
|
|
331
|
+
toX = fromX - distance;
|
|
332
|
+
break;
|
|
333
|
+
case "right":
|
|
334
|
+
toX = fromX + distance;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
toX = Math.max(0, Math.min(windowSize.width - 1, toX));
|
|
338
|
+
toY = Math.max(0, Math.min(windowSize.height - 1, toY));
|
|
339
|
+
fromX = Math.max(0, Math.min(windowSize.width - 1, fromX));
|
|
340
|
+
fromY = Math.max(0, Math.min(windowSize.height - 1, fromY));
|
|
341
|
+
const durationMs = opts.durationMs ?? 300;
|
|
342
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/dragfromtoforduration`, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
body: JSON.stringify({
|
|
345
|
+
fromX,
|
|
346
|
+
fromY,
|
|
347
|
+
toX,
|
|
348
|
+
toY,
|
|
349
|
+
duration: durationMs / 1000,
|
|
350
|
+
}),
|
|
351
|
+
});
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
const text = await res.text().catch(() => "unknown");
|
|
354
|
+
return {
|
|
355
|
+
success: false,
|
|
356
|
+
from: { x: fromX, y: fromY },
|
|
357
|
+
to: { x: toX, y: toY },
|
|
358
|
+
durationMs,
|
|
359
|
+
error: `WDA swipe failed: ${res.status} ${text}`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
success: true,
|
|
364
|
+
from: { x: fromX, y: fromY },
|
|
365
|
+
to: { x: toX, y: toY },
|
|
366
|
+
durationMs,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
from: { x: 0, y: 0 },
|
|
373
|
+
to: { x: 0, y: 0 },
|
|
374
|
+
durationMs: opts.durationMs ?? 300,
|
|
375
|
+
error: err instanceof Error ? err.message : "Unknown error during swipe",
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async scrollToElement(query, opts = {}) {
|
|
380
|
+
const maxSwipes = opts.maxSwipes ?? 10;
|
|
381
|
+
const direction = opts.direction ?? "up";
|
|
382
|
+
for (let i = 0; i <= maxSwipes; i++) {
|
|
383
|
+
const found = await this.findElement(query);
|
|
384
|
+
if (found.success && found.element?.rect) {
|
|
385
|
+
return {
|
|
386
|
+
success: true,
|
|
387
|
+
swipes: i,
|
|
388
|
+
element: {
|
|
389
|
+
type: found.element.type ?? "unknown",
|
|
390
|
+
label: found.element.label ?? found.element.name ?? "",
|
|
391
|
+
bounds: found.element.rect,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (i === maxSwipes)
|
|
396
|
+
break;
|
|
397
|
+
const swipeRes = await this.swipe({
|
|
398
|
+
direction,
|
|
399
|
+
distancePx: opts.distancePx,
|
|
400
|
+
});
|
|
401
|
+
if (!swipeRes.success) {
|
|
402
|
+
return {
|
|
403
|
+
success: false,
|
|
404
|
+
swipes: i,
|
|
405
|
+
error: swipeRes.error ?? "Swipe failed during scrollToElement",
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
success: false,
|
|
411
|
+
swipes: maxSwipes,
|
|
412
|
+
error: `Element not found after ${maxSwipes} swipes: ${JSON.stringify(query)}`,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
async longPress(x, y, durationMs = 1000, fromScreenshot = false) {
|
|
416
|
+
try {
|
|
417
|
+
const sessionId = await this.ensureSession();
|
|
418
|
+
let pressX = x;
|
|
419
|
+
let pressY = y;
|
|
420
|
+
if (fromScreenshot) {
|
|
421
|
+
const ratio = await this.getPixelRatio();
|
|
422
|
+
pressX = x / ratio;
|
|
423
|
+
pressY = y / ratio;
|
|
424
|
+
}
|
|
425
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/touchAndHold`, {
|
|
426
|
+
method: "POST",
|
|
427
|
+
body: JSON.stringify({
|
|
428
|
+
x: pressX,
|
|
429
|
+
y: pressY,
|
|
430
|
+
duration: durationMs / 1000,
|
|
431
|
+
}),
|
|
432
|
+
});
|
|
433
|
+
if (!res.ok) {
|
|
434
|
+
const text = await res.text().catch(() => "unknown");
|
|
435
|
+
return {
|
|
436
|
+
success: false,
|
|
437
|
+
error: `WDA touchAndHold failed: ${res.status} ${text}`,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return { success: true, details: { x: pressX, y: pressY, durationMs } };
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
return {
|
|
444
|
+
success: false,
|
|
445
|
+
error: err instanceof Error ? err.message : "Unknown error during longPress",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async longPressElement(query, durationMs = 1000) {
|
|
450
|
+
const found = await this.findElement(query);
|
|
451
|
+
if (!found.success || !found.element?.rect) {
|
|
452
|
+
return {
|
|
453
|
+
success: false,
|
|
454
|
+
error: found.error ?? `No element found matching ${JSON.stringify(query)}`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const rect = found.element.rect;
|
|
458
|
+
const centerX = rect.x + rect.width / 2;
|
|
459
|
+
const centerY = rect.y + rect.height / 2;
|
|
460
|
+
const res = await this.longPress(centerX, centerY, durationMs, false);
|
|
461
|
+
return {
|
|
462
|
+
...res,
|
|
463
|
+
element: {
|
|
464
|
+
type: found.element.type ?? "unknown",
|
|
465
|
+
label: found.element.label ?? found.element.name ?? "",
|
|
466
|
+
bounds: rect,
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async type(text, opts = {}) {
|
|
471
|
+
try {
|
|
472
|
+
const sessionId = await this.ensureSession();
|
|
473
|
+
const chars = Array.from(text);
|
|
474
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/keys`, {
|
|
475
|
+
method: "POST",
|
|
476
|
+
body: JSON.stringify({ value: chars }),
|
|
477
|
+
});
|
|
478
|
+
if (!res.ok) {
|
|
479
|
+
const t = await res.text().catch(() => "unknown");
|
|
480
|
+
return {
|
|
481
|
+
success: false,
|
|
482
|
+
error: `WDA keys failed: ${res.status} ${t}`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (opts.hideKeyboardAfter) {
|
|
486
|
+
await this.wdaFetch(`/session/${sessionId}/wda/keys`, {
|
|
487
|
+
method: "POST",
|
|
488
|
+
body: JSON.stringify({ value: ["\n"] }),
|
|
489
|
+
}).catch(() => undefined);
|
|
490
|
+
}
|
|
491
|
+
return { success: true };
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
return {
|
|
495
|
+
success: false,
|
|
496
|
+
error: err instanceof Error ? err.message : "Unknown error during type",
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async clearText(query) {
|
|
501
|
+
try {
|
|
502
|
+
const found = await this.findElement(query);
|
|
503
|
+
if (!found.success || !found.element?.rect) {
|
|
504
|
+
return {
|
|
505
|
+
success: false,
|
|
506
|
+
error: found.error ?? `No element found matching ${JSON.stringify(query)}`,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const rect = found.element.rect;
|
|
510
|
+
const currentValue = String(found.element.value ?? "");
|
|
511
|
+
const tapRes = await this.tap(rect.x + rect.width / 2, rect.y + rect.height / 2, false);
|
|
512
|
+
if (!tapRes.success) {
|
|
513
|
+
return {
|
|
514
|
+
success: false,
|
|
515
|
+
error: tapRes.error ?? "Failed to focus element before clearing",
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const backspaces = new Array(Math.max(1, currentValue.length)).fill("\uE003");
|
|
519
|
+
const sessionId = await this.ensureSession();
|
|
520
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/keys`, {
|
|
521
|
+
method: "POST",
|
|
522
|
+
body: JSON.stringify({ value: backspaces }),
|
|
523
|
+
});
|
|
524
|
+
if (!res.ok) {
|
|
525
|
+
const t = await res.text().catch(() => "unknown");
|
|
526
|
+
return {
|
|
527
|
+
success: false,
|
|
528
|
+
error: `WDA keys (backspace) failed: ${res.status} ${t}`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
return { success: true, details: { cleared: currentValue.length } };
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
return {
|
|
535
|
+
success: false,
|
|
536
|
+
error: err instanceof Error ? err.message : "Unknown error during clearText",
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async pressKey(key) {
|
|
541
|
+
try {
|
|
542
|
+
const sessionId = await this.ensureSession();
|
|
543
|
+
if (key === "lock") {
|
|
544
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/lock`, {
|
|
545
|
+
method: "POST",
|
|
546
|
+
body: JSON.stringify({}),
|
|
547
|
+
});
|
|
548
|
+
if (!res.ok) {
|
|
549
|
+
const t = await res.text().catch(() => "unknown");
|
|
550
|
+
return { success: false, error: `WDA lock failed: ${res.status} ${t}` };
|
|
551
|
+
}
|
|
552
|
+
return { success: true };
|
|
553
|
+
}
|
|
554
|
+
const name = key === "home" ? "home" : key === "volumeUp" ? "volumeup" : "volumedown";
|
|
555
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/pressButton`, {
|
|
556
|
+
method: "POST",
|
|
557
|
+
body: JSON.stringify({ name }),
|
|
558
|
+
});
|
|
559
|
+
if (!res.ok) {
|
|
560
|
+
const t = await res.text().catch(() => "unknown");
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
error: `WDA pressButton failed: ${res.status} ${t}`,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return { success: true };
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
return {
|
|
570
|
+
success: false,
|
|
571
|
+
error: err instanceof Error ? err.message : "Unknown error during pressKey",
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async unlock() {
|
|
576
|
+
try {
|
|
577
|
+
const sessionId = await this.ensureSession();
|
|
578
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/unlock`, {
|
|
579
|
+
method: "POST",
|
|
580
|
+
body: JSON.stringify({}),
|
|
581
|
+
});
|
|
582
|
+
if (!res.ok) {
|
|
583
|
+
const t = await res.text().catch(() => "unknown");
|
|
584
|
+
return { success: false, error: `WDA unlock failed: ${res.status} ${t}` };
|
|
585
|
+
}
|
|
586
|
+
return { success: true };
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
return {
|
|
590
|
+
success: false,
|
|
591
|
+
error: err instanceof Error ? err.message : "Unknown error during unlock",
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async isLocked() {
|
|
596
|
+
try {
|
|
597
|
+
const sessionId = await this.ensureSession();
|
|
598
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/locked`);
|
|
599
|
+
if (!res.ok)
|
|
600
|
+
return null;
|
|
601
|
+
const data = (await res.json());
|
|
602
|
+
return typeof data.value === "boolean" ? data.value : null;
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async screenshot() {
|
|
609
|
+
try {
|
|
610
|
+
const sessionId = await this.ensureSession();
|
|
611
|
+
const res = await this.wdaFetch(`/session/${sessionId}/screenshot`);
|
|
612
|
+
if (!res.ok) {
|
|
613
|
+
const t = await res.text().catch(() => "unknown");
|
|
614
|
+
return { success: false, error: `WDA screenshot failed: ${res.status} ${t}` };
|
|
615
|
+
}
|
|
616
|
+
const data = (await res.json());
|
|
617
|
+
return { success: true, base64: data.value };
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
return {
|
|
621
|
+
success: false,
|
|
622
|
+
error: err instanceof Error ? err.message : "Unknown error during screenshot",
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async getSource() {
|
|
627
|
+
try {
|
|
628
|
+
const sessionId = await this.ensureSession();
|
|
629
|
+
const res = await this.wdaFetch(`/session/${sessionId}/source?format=json`);
|
|
630
|
+
if (!res.ok) {
|
|
631
|
+
const t = await res.text().catch(() => "unknown");
|
|
632
|
+
return {
|
|
633
|
+
success: false,
|
|
634
|
+
error: `WDA source failed: ${res.status} ${t}`,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
const data = (await res.json());
|
|
638
|
+
return { success: true, source: data.value };
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
return {
|
|
642
|
+
success: false,
|
|
643
|
+
error: err instanceof Error ? err.message : "Unknown error during getSource",
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async waitFor(query, opts = {}) {
|
|
648
|
+
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
649
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 500;
|
|
650
|
+
const disappear = opts.disappear ?? false;
|
|
651
|
+
const start = Date.now();
|
|
652
|
+
while (Date.now() - start < timeoutMs) {
|
|
653
|
+
const found = await this.findElement(query);
|
|
654
|
+
const matched = found.success && !!found.element;
|
|
655
|
+
if (!disappear && matched) {
|
|
656
|
+
const rect = found.element.rect;
|
|
657
|
+
return {
|
|
658
|
+
success: true,
|
|
659
|
+
found: true,
|
|
660
|
+
elapsedMs: Date.now() - start,
|
|
661
|
+
element: rect
|
|
662
|
+
? {
|
|
663
|
+
type: found.element.type ?? "unknown",
|
|
664
|
+
label: found.element.label ?? found.element.name ?? "",
|
|
665
|
+
bounds: rect,
|
|
666
|
+
}
|
|
667
|
+
: undefined,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
if (disappear && !matched) {
|
|
671
|
+
return {
|
|
672
|
+
success: true,
|
|
673
|
+
found: false,
|
|
674
|
+
elapsedMs: Date.now() - start,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
success: false,
|
|
681
|
+
found: !disappear,
|
|
682
|
+
elapsedMs: Date.now() - start,
|
|
683
|
+
error: disappear
|
|
684
|
+
? `Element still visible after ${timeoutMs}ms: ${JSON.stringify(query)}`
|
|
685
|
+
: `Element not visible after ${timeoutMs}ms: ${JSON.stringify(query)}`,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
async assertVisible(query) {
|
|
689
|
+
const found = await this.findElement(query);
|
|
690
|
+
if (found.error) {
|
|
691
|
+
return { success: false, visible: false, error: found.error };
|
|
692
|
+
}
|
|
693
|
+
if (found.success && found.element?.rect) {
|
|
694
|
+
return {
|
|
695
|
+
success: true,
|
|
696
|
+
visible: true,
|
|
697
|
+
element: {
|
|
698
|
+
type: found.element.type ?? "unknown",
|
|
699
|
+
label: found.element.label ?? found.element.name ?? "",
|
|
700
|
+
bounds: found.element.rect,
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
return { success: true, visible: false };
|
|
705
|
+
}
|
|
706
|
+
async assertNotVisible(query) {
|
|
707
|
+
const found = await this.findElement(query);
|
|
708
|
+
if (found.error) {
|
|
709
|
+
return { success: false, visible: false, error: found.error };
|
|
710
|
+
}
|
|
711
|
+
const visible = found.success && !!found.element;
|
|
712
|
+
return { success: true, visible };
|
|
713
|
+
}
|
|
714
|
+
async getOrientation() {
|
|
715
|
+
try {
|
|
716
|
+
const sessionId = await this.ensureSession();
|
|
717
|
+
const res = await this.wdaFetch(`/session/${sessionId}/orientation`);
|
|
718
|
+
if (!res.ok)
|
|
719
|
+
return null;
|
|
720
|
+
const data = (await res.json());
|
|
721
|
+
return data.value ?? null;
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async setOrientation(orientation) {
|
|
728
|
+
try {
|
|
729
|
+
const sessionId = await this.ensureSession();
|
|
730
|
+
const res = await this.wdaFetch(`/session/${sessionId}/orientation`, {
|
|
731
|
+
method: "POST",
|
|
732
|
+
body: JSON.stringify({
|
|
733
|
+
orientation: orientation.toUpperCase(),
|
|
734
|
+
}),
|
|
735
|
+
});
|
|
736
|
+
if (!res.ok) {
|
|
737
|
+
const t = await res.text().catch(() => "unknown");
|
|
738
|
+
return {
|
|
739
|
+
success: false,
|
|
740
|
+
error: `WDA setOrientation failed: ${res.status} ${t}`,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return { success: true };
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
return {
|
|
747
|
+
success: false,
|
|
748
|
+
error: err instanceof Error
|
|
749
|
+
? err.message
|
|
750
|
+
: "Unknown error during setOrientation",
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
async getActiveAppInfo() {
|
|
755
|
+
try {
|
|
756
|
+
const sessionId = await this.ensureSession();
|
|
757
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/activeAppInfo`);
|
|
758
|
+
if (!res.ok) {
|
|
759
|
+
const t = await res.text().catch(() => "unknown");
|
|
760
|
+
return {
|
|
761
|
+
success: false,
|
|
762
|
+
error: `WDA activeAppInfo failed: ${res.status} ${t}`,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const data = (await res.json());
|
|
766
|
+
return {
|
|
767
|
+
success: true,
|
|
768
|
+
bundleId: data.value?.bundleId,
|
|
769
|
+
pid: data.value?.pid,
|
|
770
|
+
name: data.value?.name,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
catch (err) {
|
|
774
|
+
return {
|
|
775
|
+
success: false,
|
|
776
|
+
error: err instanceof Error
|
|
777
|
+
? err.message
|
|
778
|
+
: "Unknown error during getActiveAppInfo",
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async activateApp(bundleId) {
|
|
783
|
+
try {
|
|
784
|
+
const sessionId = await this.ensureSession();
|
|
785
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/apps/launch`, {
|
|
786
|
+
method: "POST",
|
|
787
|
+
body: JSON.stringify({ bundleId }),
|
|
788
|
+
});
|
|
789
|
+
if (!res.ok) {
|
|
790
|
+
const t = await res.text().catch(() => "unknown");
|
|
791
|
+
return {
|
|
792
|
+
success: false,
|
|
793
|
+
error: `WDA apps/launch failed: ${res.status} ${t}`,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
return { success: true };
|
|
797
|
+
}
|
|
798
|
+
catch (err) {
|
|
799
|
+
return {
|
|
800
|
+
success: false,
|
|
801
|
+
error: err instanceof Error
|
|
802
|
+
? err.message
|
|
803
|
+
: "Unknown error during activateApp",
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async openUrl(url) {
|
|
808
|
+
try {
|
|
809
|
+
const sessionId = await this.ensureSession();
|
|
810
|
+
const res = await this.wdaFetch(`/session/${sessionId}/url`, {
|
|
811
|
+
method: "POST",
|
|
812
|
+
body: JSON.stringify({ url }),
|
|
813
|
+
});
|
|
814
|
+
if (!res.ok) {
|
|
815
|
+
const t = await res.text().catch(() => "unknown");
|
|
816
|
+
return {
|
|
817
|
+
success: false,
|
|
818
|
+
error: `WDA openUrl failed: ${res.status} ${t}`,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
return { success: true };
|
|
822
|
+
}
|
|
823
|
+
catch (err) {
|
|
824
|
+
return {
|
|
825
|
+
success: false,
|
|
826
|
+
error: err instanceof Error ? err.message : "Unknown error during openUrl",
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async terminateAppViaWDA(bundleId) {
|
|
831
|
+
try {
|
|
832
|
+
const sessionId = await this.ensureSession();
|
|
833
|
+
const res = await this.wdaFetch(`/session/${sessionId}/wda/apps/terminate`, {
|
|
834
|
+
method: "POST",
|
|
835
|
+
body: JSON.stringify({ bundleId }),
|
|
836
|
+
});
|
|
837
|
+
if (!res.ok) {
|
|
838
|
+
const t = await res.text().catch(() => "unknown");
|
|
839
|
+
return {
|
|
840
|
+
success: false,
|
|
841
|
+
error: `WDA apps/terminate failed: ${res.status} ${t}`,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
return { success: true };
|
|
845
|
+
}
|
|
846
|
+
catch (err) {
|
|
847
|
+
return {
|
|
848
|
+
success: false,
|
|
849
|
+
error: err instanceof Error
|
|
850
|
+
? err.message
|
|
851
|
+
: "Unknown error during terminateAppViaWDA",
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async pingStatus() {
|
|
856
|
+
try {
|
|
857
|
+
const res = await this.wdaFetch("/status", {}, 2000);
|
|
858
|
+
return res.ok;
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
280
864
|
searchElements(element, query) {
|
|
281
865
|
const results = [];
|
|
282
866
|
if (this.elementMatches(element, query)) {
|
|
@@ -290,9 +874,6 @@ export class ScreenManager {
|
|
|
290
874
|
}
|
|
291
875
|
return results;
|
|
292
876
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Check whether a single element matches the given query.
|
|
295
|
-
*/
|
|
296
877
|
elementMatches(element, query) {
|
|
297
878
|
if (query.text !== undefined) {
|
|
298
879
|
const searchText = query.text.toLowerCase();
|
|
@@ -317,14 +898,10 @@ export class ScreenManager {
|
|
|
317
898
|
return false;
|
|
318
899
|
}
|
|
319
900
|
}
|
|
320
|
-
// At least one query field must be specified
|
|
321
901
|
return (query.text !== undefined ||
|
|
322
902
|
query.accessibilityLabel !== undefined ||
|
|
323
903
|
query.type !== undefined);
|
|
324
904
|
}
|
|
325
|
-
/**
|
|
326
|
-
* Collect a summary of visible elements for debugging (when no match found).
|
|
327
|
-
*/
|
|
328
905
|
collectVisibleElements(element, limit) {
|
|
329
906
|
const results = [];
|
|
330
907
|
if (limit <= 0)
|
|
@@ -332,7 +909,6 @@ export class ScreenManager {
|
|
|
332
909
|
const label = String(element.label ?? element.name ?? "");
|
|
333
910
|
const text = String(element.value ?? "");
|
|
334
911
|
const type = String(element.type ?? "");
|
|
335
|
-
// Only include elements that have some identifying info
|
|
336
912
|
if (label || text) {
|
|
337
913
|
results.push({ type, label, text });
|
|
338
914
|
}
|
|
@@ -347,11 +923,9 @@ export class ScreenManager {
|
|
|
347
923
|
}
|
|
348
924
|
return results;
|
|
349
925
|
}
|
|
350
|
-
/**
|
|
351
|
-
* Invalidate cached session and pixel ratio (e.g., after app restart).
|
|
352
|
-
*/
|
|
353
926
|
reset() {
|
|
354
927
|
this.sessionId = null;
|
|
355
928
|
this.pixelRatio = null;
|
|
929
|
+
this.sessionBaseUrl = null;
|
|
356
930
|
}
|
|
357
931
|
}
|