@nativedesktop/test 0.1.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativedesktop/test",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/FormalSnake/NativeDesktop#readme",
@@ -11,16 +11,18 @@
11
11
  "directory": "packages/test"
12
12
  },
13
13
  "publishConfig": {
14
- "access": "public"
15
- },
16
- "files": ["src"],
17
- "main": "./src/index.ts",
18
- "types": "./src/index.ts",
19
- "exports": {
20
- ".": "./src/index.ts"
21
- },
22
- "dependencies": {
23
- "@nativedesktop/host": "0.1.0",
24
- "@nativedesktop/react": "0.1.0"
25
- }
14
+ "access": "public"
15
+ },
16
+ "files": [
17
+ "src"
18
+ ],
19
+ "main": "./src/index.ts",
20
+ "types": "./src/index.ts",
21
+ "exports": {
22
+ ".": "./src/index.ts"
23
+ },
24
+ "dependencies": {
25
+ "@nativedesktop/host": "0.2.0",
26
+ "@nativedesktop/react": "0.2.0"
27
+ }
26
28
  }
package/src/client.ts CHANGED
@@ -5,6 +5,11 @@
5
5
  import { AutomationClient } from "./socket.ts";
6
6
  import type { RpcMethodName, RpcParams, RpcResult } from "@nativedesktop/react/rpc";
7
7
 
8
+ /// Room for the host to answer after its own deadline expires, for calls that
9
+ /// declare one (waitFor, webviewEval). Small: the host answers a timeout in
10
+ /// one poll interval.
11
+ const HOST_ANSWER_SLACK_MS = 2000;
12
+
8
13
  export class TimedClient {
9
14
  constructor(
10
15
  private readonly inner: AutomationClient,
@@ -20,7 +25,12 @@ export class TimedClient {
20
25
  method: M,
21
26
  ...params: RpcParams<M> extends undefined ? [] : [RpcParams<M>]
22
27
  ): Promise<RpcResult<M>> {
23
- const ms = this.timeoutMs;
28
+ // A call that carries its OWN deadline must not be cut short by the
29
+ // blanket one: a `waitFor` asked to wait 15s failed here at the 8s default,
30
+ // which reads as a product bug rather than a harness setting.
31
+ const declared = (params[0] as { timeoutMs?: number } | undefined)?.timeoutMs;
32
+ const ms =
33
+ typeof declared === "number" ? Math.max(this.timeoutMs, declared + HOST_ANSWER_SLACK_MS) : this.timeoutMs;
24
34
  let timer: ReturnType<typeof setTimeout>;
25
35
  const timeout = new Promise<never>((_resolve, reject) => {
26
36
  timer = setTimeout(() => {
package/src/launch.ts CHANGED
@@ -22,6 +22,8 @@ import type {
22
22
  TypeResult,
23
23
  WaitCondition,
24
24
  WaitForResult,
25
+ WebViewEvalResult,
26
+ WebViewInfo,
25
27
  WindowsResult,
26
28
  } from "@nativedesktop/react/rpc";
27
29
  import { TimedClient } from "./client.ts";
@@ -143,8 +145,12 @@ interface ResolvedConfig {
143
145
  onStderr?: (line: string) => void;
144
146
  }
145
147
 
148
+ /// The slice of Bun's FileSink the log pump uses. `write` is typed
149
+ /// `number | Promise<number>` upstream (it buffers, and only awaits on flush),
150
+ /// so narrowing it to `number` here made `bunx tsc --noEmit` fail for every
151
+ /// consumer of this package.
146
152
  interface LineSink {
147
- write(chunk: Uint8Array): number;
153
+ write(chunk: Uint8Array): number | Promise<number>;
148
154
  end(): void;
149
155
  }
150
156
 
@@ -444,6 +450,59 @@ export class AppHandle {
444
450
  return this.waitFor(contains ? { testId, valueContains: rendered } : { testId, valueEquals: rendered }, rest);
445
451
  }
446
452
 
453
+ // --- webview / page ------------------------------------------------------
454
+ // The page half of the vocabulary. None of it needs the app to forward
455
+ // events: url and title come off the engine, page text and eval run in the
456
+ // page itself.
457
+
458
+ waitForUrl(testId: string, urlContains: string, opts?: WaitOpts): Promise<WaitForResult> {
459
+ return this.waitFor({ testId, urlContains }, opts);
460
+ }
461
+
462
+ waitForPageTitle(testId: string, pageTitleContains: string, opts?: WaitOpts): Promise<WaitForResult> {
463
+ return this.waitFor({ testId, pageTitleContains }, opts);
464
+ }
465
+
466
+ /** Injects `document.body.innerText` into the page, re-probed at most once
467
+ * per 250ms — so a match can lag the page by one probe. */
468
+ waitForPageText(testId: string, pageTextContains: string, opts?: WaitOpts): Promise<WaitForResult> {
469
+ return this.waitFor({ testId, pageTextContains }, opts);
470
+ }
471
+
472
+ webviewInfo(target: Target, opts: { window?: number } = {}): Promise<WebViewInfo> {
473
+ return this.rpc.call("webviewInfo", { ...resolveTarget(target), ...opts });
474
+ }
475
+
476
+ /** Evaluates `code` in the page (optionally in a named isolated world) and
477
+ * answers the result's string rendering. A thrown exception comes back as
478
+ * `{ok: false, error}`, not a rejection — the code ran. */
479
+ evalInPage(
480
+ target: Target,
481
+ code: string,
482
+ opts: { world?: string; window?: number; timeoutMs?: number } = {},
483
+ ): Promise<WebViewEvalResult> {
484
+ return this.rpc.call("webviewEval", { ...resolveTarget(target), code, ...opts });
485
+ }
486
+
487
+ /** Navigates a webview and waits for the page to commit that URL. The
488
+ * navigate rides the widget's own `url` prop on the app side, so this drives
489
+ * the engine directly instead: `location.href = ...` in the page, then the
490
+ * host-side url predicate. */
491
+ async openAndAwaitLoad(testId: string, url: string, opts: WaitOpts = {}): Promise<WaitForResult> {
492
+ await this.evalInPage({ testId }, `location.href = ${JSON.stringify(url)}`, { window: opts.window });
493
+ // Match on the URL minus its scheme: engines normalise (trailing slash,
494
+ // percent-encoding, http/https upgrades), so the full string rarely
495
+ // survives verbatim.
496
+ return this.waitForUrl(testId, url.replace(/^[a-z]+:\/\//i, ""), opts);
497
+ }
498
+
499
+ /** Screenshot of the window holding a webview. On GTK the in-process
500
+ * snapshot already rasterizes live WebKit content, so this is the ordinary
501
+ * capture with a floor that rejects a blank frame. */
502
+ screenshotPage(path: string, opts: ScreenshotOptions = {}): Promise<ScreenshotResult> {
503
+ return this.screenshot(path, { minBytes: 2048, ...opts });
504
+ }
505
+
447
506
  async waitForMarker(marker: string, timeoutMs = 5000): Promise<void> {
448
507
  const deadline = Date.now() + timeoutMs;
449
508
  for (;;) {