@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativedesktop/test",
3
- "version": "0.2.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.2.0",
26
- "@nativedesktop/react": "0.2.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 type {
13
- DragParams,
14
- DragResult,
15
- ClickResult,
16
- GetTreeResult,
17
- JsonNode,
18
- KeysResult,
19
- ScreenshotResult,
20
- ScrollResult,
21
- SetValueResult,
22
- TypeResult,
23
- WaitCondition,
24
- WaitForResult,
25
- WebViewEvalResult,
26
- WebViewInfo,
27
- WindowsResult,
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
- return findNode(t.root, testId);
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
- return findAllNodes(t.root, testId);
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
- return findMatchingNode(t.root, pred);
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 { ScreenshotResult } from "@nativedesktop/react/rpc";
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 Error(`${msg.error.message} (${msg.error.code})`));
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