@limrun/api 0.20.2 → 0.21.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/CHANGELOG.md +30 -0
- package/client.d.mts +1 -1
- package/client.d.mts.map +1 -1
- package/client.d.ts +1 -1
- package/client.d.ts.map +1 -1
- package/client.js +15 -17
- package/client.js.map +1 -1
- package/client.mjs +15 -17
- package/client.mjs.map +1 -1
- package/folder-sync.d.mts +1 -1
- package/folder-sync.d.mts.map +1 -1
- package/folder-sync.d.ts +1 -1
- package/folder-sync.d.ts.map +1 -1
- package/instance-client.d.mts +109 -0
- package/instance-client.d.mts.map +1 -1
- package/instance-client.d.ts +109 -0
- package/instance-client.d.ts.map +1 -1
- package/instance-client.js +262 -69
- package/instance-client.js.map +1 -1
- package/instance-client.mjs +262 -69
- package/instance-client.mjs.map +1 -1
- package/internal/utils/query.d.mts +5 -0
- package/internal/utils/query.d.mts.map +1 -0
- package/internal/utils/query.d.ts +5 -0
- package/internal/utils/query.d.ts.map +1 -0
- package/internal/utils/query.js +23 -0
- package/internal/utils/query.js.map +1 -0
- package/internal/utils/query.mjs +20 -0
- package/internal/utils/query.mjs.map +1 -0
- package/internal/utils.d.mts +1 -0
- package/internal/utils.d.ts +1 -0
- package/internal/utils.js +1 -0
- package/internal/utils.js.map +1 -1
- package/internal/utils.mjs +1 -0
- package/ios-client.d.mts +12 -4
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +12 -4
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +7 -2
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +7 -2
- package/ios-client.mjs.map +1 -1
- package/package.json +12 -1
- package/resources/android-instances.d.mts +1 -0
- package/resources/android-instances.d.mts.map +1 -1
- package/resources/android-instances.d.ts +1 -0
- package/resources/android-instances.d.ts.map +1 -1
- package/resources/ios-instances.d.mts +1 -1
- package/resources/ios-instances.d.mts.map +1 -1
- package/resources/ios-instances.d.ts +1 -1
- package/resources/ios-instances.d.ts.map +1 -1
- package/src/client.ts +18 -21
- package/src/folder-sync.ts +2 -2
- package/src/instance-client.ts +516 -96
- package/src/internal/utils/query.ts +23 -0
- package/src/internal/utils.ts +1 -0
- package/src/ios-client.ts +25 -7
- package/src/resources/android-instances.ts +2 -0
- package/src/resources/ios-instances.ts +1 -1
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/src/instance-client.ts
CHANGED
|
@@ -23,6 +23,47 @@ export type InstanceClient = {
|
|
|
23
23
|
* @returns A promise that resolves to the screenshot data
|
|
24
24
|
*/
|
|
25
25
|
screenshot: () => Promise<ScreenshotData>;
|
|
26
|
+
/**
|
|
27
|
+
* Fetch Android UI hierarchy from UIAutomator.
|
|
28
|
+
*/
|
|
29
|
+
getElementTree: () => Promise<ElementTreeData>;
|
|
30
|
+
/**
|
|
31
|
+
* Find matching elements by Android-native selector.
|
|
32
|
+
*/
|
|
33
|
+
findElement: (selector: AndroidSelector, limit?: number) => Promise<FindElementResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Tap an element by selector/reference (or explicit coordinates).
|
|
36
|
+
*/
|
|
37
|
+
tap: (target: AndroidElementTarget) => Promise<TapResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Set text into an element or currently focused input.
|
|
40
|
+
*/
|
|
41
|
+
setText: (target: AndroidElementTarget | undefined, text: string) => Promise<SetTextResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Press an Android key by key name, optionally with modifiers.
|
|
44
|
+
* Accepted key strings are case-insensitive and may be plain names like
|
|
45
|
+
* `'BACK'`, `'ENTER'`, `'A'`, `'TAB'`, full Android constants like
|
|
46
|
+
* `'KEYCODE_TAB'`, or digit strings like `'4'`.
|
|
47
|
+
* Supported modifiers are `'shift'`, `'ctrl'`/`'control'`, `'alt'`/`'option'`,
|
|
48
|
+
* `'meta'`/`'command'`/`'cmd'`, `'sym'`, and `'fn'`/`'function'`.
|
|
49
|
+
*/
|
|
50
|
+
pressKey: (key: string, modifiers?: string[]) => Promise<PressKeyResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Scroll around the entire screen.
|
|
53
|
+
*/
|
|
54
|
+
scrollScreen: (direction: ScrollDirection, amount?: number) => Promise<ScrollResult>;
|
|
55
|
+
/**
|
|
56
|
+
* Scroll inside an element matched by selector/reference.
|
|
57
|
+
*/
|
|
58
|
+
scrollElement: (
|
|
59
|
+
target: AndroidElementTarget,
|
|
60
|
+
direction: ScrollDirection,
|
|
61
|
+
amount?: number,
|
|
62
|
+
) => Promise<ScrollResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Open a URL/deeplink on Android.
|
|
65
|
+
*/
|
|
66
|
+
openUrl: (url: string) => Promise<OpenUrlResult>;
|
|
26
67
|
/**
|
|
27
68
|
* Disconnect from the Limbar instance
|
|
28
69
|
*/
|
|
@@ -57,6 +98,54 @@ export type InstanceClient = {
|
|
|
57
98
|
*/
|
|
58
99
|
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
|
|
59
100
|
|
|
101
|
+
export type ScrollDirection = 'up' | 'down' | 'left' | 'right';
|
|
102
|
+
|
|
103
|
+
export type AndroidSelector = {
|
|
104
|
+
resourceId?: string;
|
|
105
|
+
text?: string;
|
|
106
|
+
contentDesc?: string;
|
|
107
|
+
className?: string;
|
|
108
|
+
packageName?: string;
|
|
109
|
+
index?: number;
|
|
110
|
+
clickable?: boolean;
|
|
111
|
+
enabled?: boolean;
|
|
112
|
+
focused?: boolean;
|
|
113
|
+
boundsContains?: {
|
|
114
|
+
x: number;
|
|
115
|
+
y: number;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type AndroidElementTarget = {
|
|
120
|
+
selector?: AndroidSelector;
|
|
121
|
+
x?: number;
|
|
122
|
+
y?: number;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type AndroidElementNode = {
|
|
126
|
+
index?: string;
|
|
127
|
+
text?: string;
|
|
128
|
+
resourceId?: string;
|
|
129
|
+
className?: string;
|
|
130
|
+
packageName?: string;
|
|
131
|
+
contentDesc?: string;
|
|
132
|
+
clickable?: boolean;
|
|
133
|
+
enabled?: boolean;
|
|
134
|
+
focusable?: boolean;
|
|
135
|
+
focused?: boolean;
|
|
136
|
+
scrollable?: boolean;
|
|
137
|
+
selected?: boolean;
|
|
138
|
+
bounds?: string;
|
|
139
|
+
parsedBounds?: {
|
|
140
|
+
left: number;
|
|
141
|
+
top: number;
|
|
142
|
+
right: number;
|
|
143
|
+
bottom: number;
|
|
144
|
+
centerX: number;
|
|
145
|
+
centerY: number;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
60
149
|
/**
|
|
61
150
|
* Configuration options for creating an Instance API client
|
|
62
151
|
*/
|
|
@@ -100,11 +189,6 @@ export type InstanceClientOptions = {
|
|
|
100
189
|
maxReconnectDelay?: number;
|
|
101
190
|
};
|
|
102
191
|
|
|
103
|
-
type ScreenshotRequest = {
|
|
104
|
-
type: 'screenshot';
|
|
105
|
-
id: string;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
192
|
type ScreenshotResponse = {
|
|
109
193
|
type: 'screenshot';
|
|
110
194
|
dataUri: string;
|
|
@@ -115,6 +199,41 @@ type ScreenshotData = {
|
|
|
115
199
|
dataUri: string;
|
|
116
200
|
};
|
|
117
201
|
|
|
202
|
+
export type ElementTreeData = {
|
|
203
|
+
xml: string;
|
|
204
|
+
nodes: AndroidElementNode[];
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export type FindElementResult = {
|
|
208
|
+
elements: AndroidElementNode[];
|
|
209
|
+
count: number;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export type TapResult = {
|
|
213
|
+
x: number;
|
|
214
|
+
y: number;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export type SetTextResult = {
|
|
218
|
+
textLength: number;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export type PressKeyResult = {
|
|
222
|
+
key: string;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export type ScrollResult = {
|
|
226
|
+
direction: ScrollDirection;
|
|
227
|
+
startX: number;
|
|
228
|
+
startY: number;
|
|
229
|
+
endX: number;
|
|
230
|
+
endY: number;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export type OpenUrlResult = {
|
|
234
|
+
url: string;
|
|
235
|
+
};
|
|
236
|
+
|
|
118
237
|
type ScreenshotErrorResponse = {
|
|
119
238
|
type: 'screenshotError';
|
|
120
239
|
message: string;
|
|
@@ -133,12 +252,123 @@ type AssetResultResponse = {
|
|
|
133
252
|
message?: string;
|
|
134
253
|
};
|
|
135
254
|
|
|
255
|
+
type CommandError = {
|
|
256
|
+
code?: string;
|
|
257
|
+
message?: string;
|
|
258
|
+
retriable?: boolean;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
type ScreenshotResultMessage = {
|
|
262
|
+
type: 'screenshotResult';
|
|
263
|
+
id: string;
|
|
264
|
+
payload?: ScreenshotData;
|
|
265
|
+
error?: CommandError;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
type GetElementTreeResultMessage = {
|
|
269
|
+
type: 'getElementTreeResult';
|
|
270
|
+
id: string;
|
|
271
|
+
payload?: ElementTreeData;
|
|
272
|
+
error?: CommandError;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
type FindElementResultMessage = {
|
|
276
|
+
type: 'findElementResult';
|
|
277
|
+
id: string;
|
|
278
|
+
payload?: FindElementResult;
|
|
279
|
+
error?: CommandError;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
type TapResultMessage = {
|
|
283
|
+
type: 'tapResult';
|
|
284
|
+
id: string;
|
|
285
|
+
payload?: TapResult;
|
|
286
|
+
error?: CommandError;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
type SetTextResultMessage = {
|
|
290
|
+
type: 'setTextResult';
|
|
291
|
+
id: string;
|
|
292
|
+
payload?: SetTextResult;
|
|
293
|
+
error?: CommandError;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
type PressKeyResultMessage = {
|
|
297
|
+
type: 'pressKeyResult';
|
|
298
|
+
id: string;
|
|
299
|
+
payload?: PressKeyResult;
|
|
300
|
+
error?: CommandError;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
type ScrollScreenResultMessage = {
|
|
304
|
+
type: 'scrollScreenResult';
|
|
305
|
+
id: string;
|
|
306
|
+
payload?: ScrollResult;
|
|
307
|
+
error?: CommandError;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
type ScrollElementResultMessage = {
|
|
311
|
+
type: 'scrollElementResult';
|
|
312
|
+
id: string;
|
|
313
|
+
payload?: ScrollResult;
|
|
314
|
+
error?: CommandError;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
type OpenUrlResultMessage = {
|
|
318
|
+
type: 'openUrlResult';
|
|
319
|
+
id: string;
|
|
320
|
+
payload?: OpenUrlResult;
|
|
321
|
+
error?: CommandError;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
type KnownCommandResultMessage =
|
|
325
|
+
| ScreenshotResultMessage
|
|
326
|
+
| GetElementTreeResultMessage
|
|
327
|
+
| FindElementResultMessage
|
|
328
|
+
| TapResultMessage
|
|
329
|
+
| SetTextResultMessage
|
|
330
|
+
| PressKeyResultMessage
|
|
331
|
+
| ScrollScreenResultMessage
|
|
332
|
+
| ScrollElementResultMessage
|
|
333
|
+
| OpenUrlResultMessage;
|
|
334
|
+
|
|
136
335
|
type ServerMessage =
|
|
137
336
|
| ScreenshotResponse
|
|
138
337
|
| ScreenshotErrorResponse
|
|
139
338
|
| AssetResultResponse
|
|
339
|
+
| KnownCommandResultMessage
|
|
140
340
|
| { type: string; [key: string]: unknown };
|
|
141
341
|
|
|
342
|
+
type CommandRequestMap = {
|
|
343
|
+
screenshot: {};
|
|
344
|
+
getElementTree: {};
|
|
345
|
+
findElement: { selector: AndroidSelector; limit?: number };
|
|
346
|
+
tap: AndroidElementTarget;
|
|
347
|
+
setText: { text: string } & AndroidElementTarget;
|
|
348
|
+
pressKey: { keyName?: string; key?: string; modifiers?: string[] };
|
|
349
|
+
scrollScreen: { direction: ScrollDirection; amount?: number };
|
|
350
|
+
scrollElement: AndroidElementTarget & { direction: ScrollDirection; amount?: number };
|
|
351
|
+
openUrl: { url: string };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
type CommandResultMap = {
|
|
355
|
+
screenshot: ScreenshotData;
|
|
356
|
+
getElementTree: ElementTreeData;
|
|
357
|
+
findElement: FindElementResult;
|
|
358
|
+
tap: TapResult;
|
|
359
|
+
setText: SetTextResult;
|
|
360
|
+
pressKey: PressKeyResult;
|
|
361
|
+
scrollScreen: ScrollResult;
|
|
362
|
+
scrollElement: ScrollResult;
|
|
363
|
+
openUrl: OpenUrlResult;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
type PendingRequest<T> = {
|
|
367
|
+
resolve: (value: T) => void;
|
|
368
|
+
reject: (reason: Error) => void;
|
|
369
|
+
timeout: NodeJS.Timeout;
|
|
370
|
+
};
|
|
371
|
+
|
|
142
372
|
/**
|
|
143
373
|
* Creates a client for interacting with a Limbar instance
|
|
144
374
|
* @param options Configuration options including webrtcUrl, token and log level
|
|
@@ -158,21 +388,8 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
158
388
|
let intentionalDisconnect = false;
|
|
159
389
|
let lastError: string | undefined;
|
|
160
390
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => void;
|
|
165
|
-
rejecter: (reason?: any) => void;
|
|
166
|
-
}
|
|
167
|
-
> = new Map();
|
|
168
|
-
|
|
169
|
-
const assetRequests: Map<
|
|
170
|
-
string,
|
|
171
|
-
{
|
|
172
|
-
resolver: (value: void | PromiseLike<void>) => void;
|
|
173
|
-
rejecter: (reason?: any) => void;
|
|
174
|
-
}
|
|
175
|
-
> = new Map();
|
|
391
|
+
const pendingRequests: Map<string, PendingRequest<unknown>> = new Map();
|
|
392
|
+
const pendingAssetRequestsByUrl: Map<string, Array<PendingRequest<void>>> = new Map();
|
|
176
393
|
|
|
177
394
|
const stateChangeCallbacks: Set<ConnectionStateCallback> = new Set();
|
|
178
395
|
|
|
@@ -207,10 +424,18 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
207
424
|
};
|
|
208
425
|
|
|
209
426
|
const failPendingRequests = (reason: string): void => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
427
|
+
pendingRequests.forEach((request) => {
|
|
428
|
+
clearTimeout(request.timeout);
|
|
429
|
+
request.reject(new Error(reason));
|
|
430
|
+
});
|
|
431
|
+
pendingRequests.clear();
|
|
432
|
+
pendingAssetRequestsByUrl.forEach((requests) => {
|
|
433
|
+
requests.forEach((request) => {
|
|
434
|
+
clearTimeout(request.timeout);
|
|
435
|
+
request.reject(new Error(reason));
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
pendingAssetRequestsByUrl.clear();
|
|
214
439
|
};
|
|
215
440
|
|
|
216
441
|
const cleanup = (): void => {
|
|
@@ -232,10 +457,109 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
232
457
|
};
|
|
233
458
|
|
|
234
459
|
let pingInterval: NodeJS.Timeout | undefined;
|
|
460
|
+
let requestCounter = 0;
|
|
235
461
|
|
|
236
462
|
return new Promise<InstanceClient>((resolveConnection, rejectConnection) => {
|
|
237
463
|
let hasResolved = false;
|
|
238
464
|
|
|
465
|
+
const nextRequestId = (prefix: string): string => {
|
|
466
|
+
requestCounter += 1;
|
|
467
|
+
return `${prefix}-${Date.now()}-${requestCounter}`;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const resolvePendingRequest = <T>(id: string, value: T): void => {
|
|
471
|
+
const request = pendingRequests.get(id) as PendingRequest<T> | undefined;
|
|
472
|
+
if (!request) {
|
|
473
|
+
logger.debug(`Received response for unknown/already-settled request: ${id}`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
clearTimeout(request.timeout);
|
|
477
|
+
pendingRequests.delete(id);
|
|
478
|
+
request.resolve(value);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const rejectPendingRequest = (id: string, error: Error): void => {
|
|
482
|
+
const request = pendingRequests.get(id);
|
|
483
|
+
if (!request) {
|
|
484
|
+
logger.debug(`Received error for unknown/already-settled request: ${id}`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
clearTimeout(request.timeout);
|
|
488
|
+
pendingRequests.delete(id);
|
|
489
|
+
request.reject(error);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const extractErrorMessage = (message: ServerMessage): string | undefined => {
|
|
493
|
+
if ('message' in message && typeof message.message === 'string') {
|
|
494
|
+
return message.message;
|
|
495
|
+
}
|
|
496
|
+
if ('error' in message && message.error && typeof message.error === 'object') {
|
|
497
|
+
const obj = message.error as CommandError;
|
|
498
|
+
if (typeof obj.message === 'string' && obj.message) {
|
|
499
|
+
return obj.message;
|
|
500
|
+
}
|
|
501
|
+
if (typeof obj.code === 'string' && obj.code) {
|
|
502
|
+
return obj.code;
|
|
503
|
+
}
|
|
504
|
+
// Presence of an error object itself is treated as failure, even if message/code are absent.
|
|
505
|
+
return `Server returned ${String(message.type)} with an error payload but no error message/code`;
|
|
506
|
+
}
|
|
507
|
+
return undefined;
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const isKnownCommandResultMessage = (message: ServerMessage): message is KnownCommandResultMessage => {
|
|
511
|
+
if (!('type' in message) || typeof message.type !== 'string') {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
switch (message.type) {
|
|
515
|
+
case 'screenshotResult':
|
|
516
|
+
case 'getElementTreeResult':
|
|
517
|
+
case 'findElementResult':
|
|
518
|
+
case 'tapResult':
|
|
519
|
+
case 'setTextResult':
|
|
520
|
+
case 'pressKeyResult':
|
|
521
|
+
case 'scrollScreenResult':
|
|
522
|
+
case 'scrollElementResult':
|
|
523
|
+
case 'openUrlResult':
|
|
524
|
+
return 'id' in message && typeof message.id === 'string';
|
|
525
|
+
default:
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const sendRequest = async <K extends keyof CommandRequestMap>(
|
|
531
|
+
type: K,
|
|
532
|
+
params: CommandRequestMap[K],
|
|
533
|
+
timeoutMs: number = 30000,
|
|
534
|
+
): Promise<CommandResultMap[K]> => {
|
|
535
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
536
|
+
return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
|
|
537
|
+
}
|
|
538
|
+
const id = nextRequestId('ts-client');
|
|
539
|
+
const command =
|
|
540
|
+
Object.keys(params).length > 0 ? { type, id, ...params, payload: params } : { type, id };
|
|
541
|
+
return new Promise<CommandResultMap[K]>((resolve, reject) => {
|
|
542
|
+
const timeout = setTimeout(() => {
|
|
543
|
+
if (pendingRequests.has(id)) {
|
|
544
|
+
pendingRequests.delete(id);
|
|
545
|
+
reject(new Error(`Request ${type} timed out`));
|
|
546
|
+
}
|
|
547
|
+
}, timeoutMs);
|
|
548
|
+
pendingRequests.set(id, {
|
|
549
|
+
resolve: (value: unknown) => resolve(value as CommandResultMap[K]),
|
|
550
|
+
reject: (reason: Error) => reject(reason),
|
|
551
|
+
timeout,
|
|
552
|
+
});
|
|
553
|
+
ws!.send(JSON.stringify(command), (err?: Error) => {
|
|
554
|
+
if (err) {
|
|
555
|
+
clearTimeout(timeout);
|
|
556
|
+
pendingRequests.delete(id);
|
|
557
|
+
reject(err);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
};
|
|
562
|
+
|
|
239
563
|
// Reconnection logic with exponential backoff
|
|
240
564
|
const scheduleReconnect = (): void => {
|
|
241
565
|
if (intentionalDisconnect) {
|
|
@@ -290,20 +614,30 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
290
614
|
logger.warn('Received invalid screenshot message:', message);
|
|
291
615
|
break;
|
|
292
616
|
}
|
|
293
|
-
|
|
294
617
|
const screenshotMessage = message as ScreenshotResponse;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
618
|
+
logger.debug(`Received screenshot data URI for request ${screenshotMessage.id}.`);
|
|
619
|
+
resolvePendingRequest<ScreenshotData>(screenshotMessage.id, {
|
|
620
|
+
dataUri: screenshotMessage.dataUri,
|
|
621
|
+
});
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
case 'screenshotResult': {
|
|
625
|
+
const resultMessage = message as ScreenshotResultMessage;
|
|
626
|
+
const errorMessage = extractErrorMessage(resultMessage);
|
|
627
|
+
if (errorMessage) {
|
|
628
|
+
rejectPendingRequest(resultMessage.id, new Error(errorMessage));
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
const dataUri =
|
|
632
|
+
typeof resultMessage.payload?.dataUri === 'string' ? resultMessage.payload.dataUri : '';
|
|
633
|
+
if (!dataUri) {
|
|
634
|
+
rejectPendingRequest(
|
|
635
|
+
resultMessage.id,
|
|
636
|
+
new Error('Received screenshotResult without payload.dataUri'),
|
|
300
637
|
);
|
|
301
638
|
break;
|
|
302
639
|
}
|
|
303
|
-
|
|
304
|
-
logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
|
|
305
|
-
request.resolver({ dataUri: screenshotMessage.dataUri });
|
|
306
|
-
screenshotRequests.delete(screenshotMessage.id);
|
|
640
|
+
resolvePendingRequest<ScreenshotData>(resultMessage.id, { dataUri });
|
|
307
641
|
break;
|
|
308
642
|
}
|
|
309
643
|
case 'screenshotError': {
|
|
@@ -311,50 +645,55 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
311
645
|
logger.warn('Received invalid screenshot error message:', message);
|
|
312
646
|
break;
|
|
313
647
|
}
|
|
314
|
-
|
|
315
648
|
const errorMessage = message as ScreenshotErrorResponse;
|
|
316
|
-
const request = screenshotRequests.get(errorMessage.id);
|
|
317
|
-
|
|
318
|
-
if (!request) {
|
|
319
|
-
logger.warn(
|
|
320
|
-
`Received screenshot error for unknown or already handled session: ${errorMessage.id}`,
|
|
321
|
-
);
|
|
322
|
-
break;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
649
|
logger.error(
|
|
326
|
-
`Server reported an error capturing screenshot for
|
|
650
|
+
`Server reported an error capturing screenshot for request ${errorMessage.id}:`,
|
|
327
651
|
errorMessage.message,
|
|
328
652
|
);
|
|
329
|
-
|
|
330
|
-
screenshotRequests.delete(errorMessage.id);
|
|
653
|
+
rejectPendingRequest(errorMessage.id, new Error(errorMessage.message));
|
|
331
654
|
break;
|
|
332
655
|
}
|
|
333
656
|
case 'assetResult': {
|
|
334
657
|
logger.debug('Received assetResult:', message);
|
|
335
|
-
const
|
|
336
|
-
|
|
658
|
+
const url = message.url as string;
|
|
659
|
+
const queue = pendingAssetRequestsByUrl.get(url);
|
|
660
|
+
if (!queue || queue.length === 0) {
|
|
337
661
|
logger.warn(`Received assetResult for unknown or already handled url: ${message.url}`);
|
|
338
662
|
break;
|
|
339
663
|
}
|
|
664
|
+
const request = queue.shift()!;
|
|
665
|
+
if (queue.length === 0) {
|
|
666
|
+
pendingAssetRequestsByUrl.delete(url);
|
|
667
|
+
} else {
|
|
668
|
+
pendingAssetRequestsByUrl.set(url, queue);
|
|
669
|
+
}
|
|
670
|
+
clearTimeout(request.timeout);
|
|
340
671
|
if (message.result === 'success') {
|
|
341
672
|
logger.debug('Asset result is success');
|
|
342
|
-
request.
|
|
343
|
-
assetRequests.delete(message.url as string);
|
|
673
|
+
request.resolve();
|
|
344
674
|
break;
|
|
345
675
|
}
|
|
346
|
-
const
|
|
676
|
+
const assetErrorMessage =
|
|
347
677
|
typeof message.message === 'string' && message.message ?
|
|
348
678
|
message.message
|
|
349
679
|
: `Asset processing failed: ${JSON.stringify(message)}`;
|
|
350
|
-
logger.debug('Asset result is failure',
|
|
351
|
-
request.
|
|
352
|
-
assetRequests.delete(message.url as string);
|
|
680
|
+
logger.debug('Asset result is failure', assetErrorMessage);
|
|
681
|
+
request.reject(new Error(assetErrorMessage));
|
|
353
682
|
break;
|
|
354
683
|
}
|
|
355
|
-
default:
|
|
684
|
+
default: {
|
|
685
|
+
if (isKnownCommandResultMessage(message)) {
|
|
686
|
+
const err = extractErrorMessage(message);
|
|
687
|
+
if (err) {
|
|
688
|
+
rejectPendingRequest(message.id, new Error(err));
|
|
689
|
+
} else {
|
|
690
|
+
resolvePendingRequest(message.id, message.payload ?? message);
|
|
691
|
+
}
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
356
694
|
logger.warn(`Received unexpected message type: ${message.type}`);
|
|
357
695
|
break;
|
|
696
|
+
}
|
|
358
697
|
}
|
|
359
698
|
});
|
|
360
699
|
|
|
@@ -410,6 +749,14 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
410
749
|
hasResolved = true;
|
|
411
750
|
resolveConnection({
|
|
412
751
|
screenshot,
|
|
752
|
+
getElementTree,
|
|
753
|
+
findElement,
|
|
754
|
+
tap,
|
|
755
|
+
setText,
|
|
756
|
+
pressKey,
|
|
757
|
+
scrollScreen,
|
|
758
|
+
scrollElement,
|
|
759
|
+
openUrl,
|
|
413
760
|
disconnect,
|
|
414
761
|
startAdbTunnel,
|
|
415
762
|
sendAsset,
|
|
@@ -421,45 +768,82 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
421
768
|
};
|
|
422
769
|
|
|
423
770
|
const screenshot = async (): Promise<ScreenshotData> => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
771
|
+
return sendRequest('screenshot', {});
|
|
772
|
+
};
|
|
427
773
|
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
774
|
+
const getElementTree = async (): Promise<ElementTreeData> => {
|
|
775
|
+
const result = await sendRequest('getElementTree', {});
|
|
776
|
+
return {
|
|
777
|
+
xml: typeof result.xml === 'string' ? result.xml : '',
|
|
778
|
+
nodes: Array.isArray(result.nodes) ? result.nodes : [],
|
|
432
779
|
};
|
|
780
|
+
};
|
|
433
781
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
782
|
+
const findElement = async (selector: AndroidSelector, limit = 20): Promise<FindElementResult> => {
|
|
783
|
+
const result = await sendRequest('findElement', { selector, limit });
|
|
784
|
+
const elements = Array.isArray(result.elements) ? result.elements : [];
|
|
785
|
+
return {
|
|
786
|
+
elements,
|
|
787
|
+
count: typeof result.count === 'number' ? result.count : elements.length,
|
|
788
|
+
};
|
|
789
|
+
};
|
|
442
790
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
791
|
+
const tap = async (target: AndroidElementTarget): Promise<TapResult> => {
|
|
792
|
+
const result = await sendRequest('tap', target);
|
|
793
|
+
return {
|
|
794
|
+
x: Number(result.x ?? 0),
|
|
795
|
+
y: Number(result.y ?? 0),
|
|
796
|
+
};
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const setText = async (
|
|
800
|
+
target: AndroidElementTarget | undefined,
|
|
801
|
+
text: string,
|
|
802
|
+
): Promise<SetTextResult> => {
|
|
803
|
+
const payload: CommandRequestMap['setText'] = { text };
|
|
804
|
+
if (target?.selector) payload.selector = target.selector;
|
|
805
|
+
if (typeof target?.x === 'number') payload.x = target.x;
|
|
806
|
+
if (typeof target?.y === 'number') payload.y = target.y;
|
|
807
|
+
const result = await sendRequest('setText', payload);
|
|
808
|
+
return {
|
|
809
|
+
textLength: Number(result.textLength ?? text.length),
|
|
810
|
+
};
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const pressKey = async (key: string, modifiers?: string[]): Promise<PressKeyResult> => {
|
|
814
|
+
const payload: CommandRequestMap['pressKey'] = {
|
|
815
|
+
keyName: key,
|
|
816
|
+
...(modifiers ? { modifiers } : {}),
|
|
817
|
+
};
|
|
818
|
+
const result = await sendRequest('pressKey', payload);
|
|
819
|
+
return {
|
|
820
|
+
key: typeof result.key === 'string' ? result.key : String(key),
|
|
821
|
+
};
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const scrollScreen = async (direction: ScrollDirection, amount = 6): Promise<ScrollResult> => {
|
|
825
|
+
const result = await sendRequest('scrollScreen', { direction, amount });
|
|
826
|
+
return result;
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
const scrollElement = async (
|
|
830
|
+
target: AndroidElementTarget,
|
|
831
|
+
direction: ScrollDirection,
|
|
832
|
+
amount = 6,
|
|
833
|
+
): Promise<ScrollResult> => {
|
|
834
|
+
const result = await sendRequest('scrollElement', {
|
|
835
|
+
...target,
|
|
836
|
+
direction,
|
|
837
|
+
amount,
|
|
462
838
|
});
|
|
839
|
+
return result;
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const openUrl = async (url: string): Promise<OpenUrlResult> => {
|
|
843
|
+
const result = await sendRequest('openUrl', { url });
|
|
844
|
+
return {
|
|
845
|
+
url: typeof result.url === 'string' ? result.url : url,
|
|
846
|
+
};
|
|
463
847
|
};
|
|
464
848
|
|
|
465
849
|
const disconnect = (): void => {
|
|
@@ -518,13 +902,49 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
|
|
|
518
902
|
type: 'asset',
|
|
519
903
|
url,
|
|
520
904
|
};
|
|
521
|
-
ws.send(JSON.stringify(assetRequest), (err?: Error) => {
|
|
522
|
-
if (err) {
|
|
523
|
-
logger.error('Failed to send asset request:', err);
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
905
|
return new Promise<void>((resolve, reject) => {
|
|
527
|
-
|
|
906
|
+
let request: PendingRequest<void>;
|
|
907
|
+
const timeout = setTimeout(() => {
|
|
908
|
+
const queue = pendingAssetRequestsByUrl.get(url);
|
|
909
|
+
if (!queue) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const idx = queue.indexOf(request);
|
|
913
|
+
if (idx >= 0) {
|
|
914
|
+
queue.splice(idx, 1);
|
|
915
|
+
reject(new Error(`Request asset timed out for url: ${url}`));
|
|
916
|
+
}
|
|
917
|
+
if (queue.length === 0) {
|
|
918
|
+
pendingAssetRequestsByUrl.delete(url);
|
|
919
|
+
} else {
|
|
920
|
+
pendingAssetRequestsByUrl.set(url, queue);
|
|
921
|
+
}
|
|
922
|
+
}, 30000);
|
|
923
|
+
request = {
|
|
924
|
+
resolve,
|
|
925
|
+
reject: (reason: Error) => reject(reason),
|
|
926
|
+
timeout,
|
|
927
|
+
};
|
|
928
|
+
const queue = pendingAssetRequestsByUrl.get(url) ?? [];
|
|
929
|
+
queue.push(request);
|
|
930
|
+
pendingAssetRequestsByUrl.set(url, queue);
|
|
931
|
+
ws!.send(JSON.stringify(assetRequest), (err?: Error) => {
|
|
932
|
+
if (err) {
|
|
933
|
+
clearTimeout(timeout);
|
|
934
|
+
const queued = pendingAssetRequestsByUrl.get(url) ?? [];
|
|
935
|
+
const idx = queued.indexOf(request);
|
|
936
|
+
if (idx >= 0) {
|
|
937
|
+
queued.splice(idx, 1);
|
|
938
|
+
}
|
|
939
|
+
if (queued.length === 0) {
|
|
940
|
+
pendingAssetRequestsByUrl.delete(url);
|
|
941
|
+
} else {
|
|
942
|
+
pendingAssetRequestsByUrl.set(url, queued);
|
|
943
|
+
}
|
|
944
|
+
logger.error('Failed to send asset request:', err);
|
|
945
|
+
reject(err);
|
|
946
|
+
}
|
|
947
|
+
});
|
|
528
948
|
});
|
|
529
949
|
};
|
|
530
950
|
|