@nativedesktop/test 0.2.0 → 0.3.1
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/package.json +3 -3
- package/src/launch.ts +45 -23
- package/src/screenshot.ts +7 -1
- package/src/socket.ts +20 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nativedesktop/test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/FormalSnake/NativeDesktop#readme",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
".": "./src/index.ts"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@nativedesktop/host": "0.
|
|
26
|
-
"@nativedesktop/react": "0.
|
|
25
|
+
"@nativedesktop/host": "0.3.1",
|
|
26
|
+
"@nativedesktop/react": "0.3.1"
|
|
27
27
|
}
|
|
28
28
|
}
|
package/src/launch.ts
CHANGED
|
@@ -8,23 +8,24 @@ import { tmpdir } from "node:os";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import type { Backend } from "@nativedesktop/host";
|
|
10
10
|
import { resolveBackend, resolveHostBinary } from "@nativedesktop/host";
|
|
11
|
-
import { AutomationClient } from "./socket.ts";
|
|
12
|
-
import
|
|
13
|
-
DragParams,
|
|
14
|
-
DragResult,
|
|
15
|
-
ClickResult,
|
|
16
|
-
GetTreeResult,
|
|
17
|
-
JsonNode,
|
|
18
|
-
KeysResult,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
import { AutomationClient, AutomationRpcError } from "./socket.ts";
|
|
12
|
+
import {
|
|
13
|
+
type DragParams,
|
|
14
|
+
type DragResult,
|
|
15
|
+
type ClickResult,
|
|
16
|
+
type GetTreeResult,
|
|
17
|
+
type JsonNode,
|
|
18
|
+
type KeysResult,
|
|
19
|
+
RPC_ERRORS,
|
|
20
|
+
type ScreenshotResult,
|
|
21
|
+
type ScrollResult,
|
|
22
|
+
type SetValueResult,
|
|
23
|
+
type TypeResult,
|
|
24
|
+
type WaitCondition,
|
|
25
|
+
type WaitForResult,
|
|
26
|
+
type WebViewEvalResult,
|
|
27
|
+
type WebViewInfo,
|
|
28
|
+
type WindowsResult,
|
|
28
29
|
} from "@nativedesktop/react/rpc";
|
|
29
30
|
import { TimedClient } from "./client.ts";
|
|
30
31
|
import { type DialogScript, dialogScriptEnv } from "./dialogs.ts";
|
|
@@ -351,14 +352,32 @@ export class AppHandle {
|
|
|
351
352
|
return this.rpc.call("getTree", { window });
|
|
352
353
|
}
|
|
353
354
|
|
|
355
|
+
// A window closing between windows() (or an earlier query) and this call
|
|
356
|
+
// races the window ref out from under `opts.window`; getTree answers the
|
|
357
|
+
// distinguishable windowGone error rather than invalidParams (see
|
|
358
|
+
// schema/rpc.json), and every find* helper below treats that exactly like
|
|
359
|
+
// an unmatched testId: null, an empty list, or a "not found" throw. tree()
|
|
360
|
+
// itself does NOT swallow it: a raw tree() caller needs to know the window
|
|
361
|
+
// is gone rather than get a fake empty snapshot that reads the same as
|
|
362
|
+
// "this window really has no children".
|
|
363
|
+
private isWindowGone(e: unknown): boolean {
|
|
364
|
+
return e instanceof AutomationRpcError && e.code === RPC_ERRORS.windowGone.code;
|
|
365
|
+
}
|
|
366
|
+
|
|
354
367
|
async find(testId: string, opts: { window?: number } = {}): Promise<JsonNode | null> {
|
|
355
|
-
const t = await this.tree(opts.window)
|
|
356
|
-
|
|
368
|
+
const t = await this.tree(opts.window).catch((e) => {
|
|
369
|
+
if (this.isWindowGone(e)) return null;
|
|
370
|
+
throw e;
|
|
371
|
+
});
|
|
372
|
+
return t ? findNode(t.root, testId) : null;
|
|
357
373
|
}
|
|
358
374
|
|
|
359
375
|
async findAll(testId: string, opts: { window?: number } = {}): Promise<JsonNode[]> {
|
|
360
|
-
const t = await this.tree(opts.window)
|
|
361
|
-
|
|
376
|
+
const t = await this.tree(opts.window).catch((e) => {
|
|
377
|
+
if (this.isWindowGone(e)) return null;
|
|
378
|
+
throw e;
|
|
379
|
+
});
|
|
380
|
+
return t ? findAllNodes(t.root, testId) : [];
|
|
362
381
|
}
|
|
363
382
|
|
|
364
383
|
async mustFind(testId: string, opts: { window?: number } = {}): Promise<JsonNode> {
|
|
@@ -368,8 +387,11 @@ export class AppHandle {
|
|
|
368
387
|
}
|
|
369
388
|
|
|
370
389
|
async findMatching(pred: (n: JsonNode) => boolean, opts: { window?: number } = {}): Promise<JsonNode | null> {
|
|
371
|
-
const t = await this.tree(opts.window)
|
|
372
|
-
|
|
390
|
+
const t = await this.tree(opts.window).catch((e) => {
|
|
391
|
+
if (this.isWindowGone(e)) return null;
|
|
392
|
+
throw e;
|
|
393
|
+
});
|
|
394
|
+
return t ? findMatchingNode(t.root, pred) : null;
|
|
373
395
|
}
|
|
374
396
|
|
|
375
397
|
// --- actions (single RPC, host-side resolution, no retry loops) ------------
|
package/src/screenshot.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { resolve as resolvePath } from "node:path";
|
|
10
10
|
import type { Backend } from "@nativedesktop/host";
|
|
11
|
-
import type
|
|
11
|
+
import { RPC_ERRORS, type ScreenshotResult } from "@nativedesktop/react/rpc";
|
|
12
12
|
import { pngSize } from "./png.ts";
|
|
13
|
+
import { AutomationRpcError } from "./socket.ts";
|
|
13
14
|
|
|
14
15
|
export interface ScreenshotOptions {
|
|
15
16
|
window?: number;
|
|
@@ -50,6 +51,11 @@ export async function takeScreenshot(
|
|
|
50
51
|
}
|
|
51
52
|
return shot;
|
|
52
53
|
} catch (e) {
|
|
54
|
+
// A closed window is never coming back mid-run, so retrying just
|
|
55
|
+
// burns the retry budget on a guaranteed repeat failure; fail fast
|
|
56
|
+
// with the distinguishable error instead of the generic "N attempts"
|
|
57
|
+
// wrapper the frame-invalidation races below are meant for.
|
|
58
|
+
if (e instanceof AutomationRpcError && e.code === RPC_ERRORS.windowGone.code) throw e;
|
|
53
59
|
lastErr = e as Error;
|
|
54
60
|
if (attempt < retries - 1) await new Promise((r) => setTimeout(r, 150));
|
|
55
61
|
}
|
package/src/socket.ts
CHANGED
|
@@ -21,6 +21,25 @@ interface JsonRpcResponse {
|
|
|
21
21
|
error?: JsonRpcError;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A rejected call's `code`/`data` survive on the error object (matching
|
|
26
|
+
* schema/rpc.json's `errors` table, e.g. RPC_ERRORS.windowGone.code), so a
|
|
27
|
+
* caller can branch on the error kind without parsing `.message` text.
|
|
28
|
+
* `.message` keeps the existing "<msg> (<code>)" shape so callers that
|
|
29
|
+
* already substring-match a code keep working.
|
|
30
|
+
*/
|
|
31
|
+
export class AutomationRpcError extends Error {
|
|
32
|
+
readonly code: number;
|
|
33
|
+
readonly data: unknown;
|
|
34
|
+
|
|
35
|
+
constructor(code: number, message: string, data?: unknown) {
|
|
36
|
+
super(`${message} (${code})`);
|
|
37
|
+
this.name = "AutomationRpcError";
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.data = data;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
24
43
|
export class AutomationClient {
|
|
25
44
|
private socket!: import("bun").Socket;
|
|
26
45
|
private inbox = new Uint8Array(0);
|
|
@@ -75,7 +94,7 @@ export class AutomationClient {
|
|
|
75
94
|
const pending = this.pending.get(msg.id);
|
|
76
95
|
if (!pending) return; // unknown/stale id — drop
|
|
77
96
|
this.pending.delete(msg.id);
|
|
78
|
-
if (msg.error) pending.reject(new
|
|
97
|
+
if (msg.error) pending.reject(new AutomationRpcError(msg.error.code, msg.error.message, msg.error.data));
|
|
79
98
|
else pending.resolve(msg.result);
|
|
80
99
|
}
|
|
81
100
|
|