@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 +15 -13
- package/src/client.ts +11 -1
- package/src/launch.ts +60 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nativedesktop/test",
|
|
3
|
-
"version": "0.
|
|
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
|
-
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
"
|
|
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
|
-
|
|
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 (;;) {
|