@itmetsam/samurai-framework 0.1.0 → 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/README.md +129 -0
- package/bin/samurai.mjs +31 -0
- package/dist/api.js +3 -0
- package/dist/assert/assertion-error.d.ts +23 -0
- package/dist/assert/assertion-error.js +45 -0
- package/dist/assert/expect.d.ts +55 -0
- package/dist/assert/expect.js +188 -0
- package/dist/browser/browser-context.d.ts +91 -0
- package/dist/browser/browser-context.js +146 -0
- package/dist/browser/browser-finder.d.ts +2 -0
- package/dist/browser/browser-finder.js +95 -0
- package/dist/browser/browser.d.ts +95 -0
- package/dist/browser/browser.js +372 -0
- package/dist/browser/context-tree.d.ts +23 -0
- package/dist/browser/context-tree.js +60 -0
- package/dist/browser/dialog.d.ts +37 -0
- package/dist/browser/dialog.js +61 -0
- package/dist/browser/download.d.ts +71 -0
- package/dist/browser/download.js +137 -0
- package/dist/browser/downloads-dir.d.ts +8 -0
- package/dist/browser/downloads-dir.js +38 -0
- package/dist/browser/emulation.d.ts +61 -0
- package/dist/browser/emulation.js +144 -0
- package/dist/browser/file-chooser.d.ts +39 -0
- package/dist/browser/file-chooser.js +86 -0
- package/dist/browser/file-paths.d.ts +4 -0
- package/dist/browser/file-paths.js +18 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/navigation-error.d.ts +13 -0
- package/dist/browser/navigation-error.js +19 -0
- package/dist/browser/page-logs.d.ts +86 -0
- package/dist/browser/page-logs.js +149 -0
- package/dist/browser/page-wait.d.ts +9 -0
- package/dist/browser/page-wait.js +17 -0
- package/dist/browser/page.d.ts +370 -0
- package/dist/browser/page.js +1038 -0
- package/dist/browser/pdf.d.ts +72 -0
- package/dist/browser/pdf.js +37 -0
- package/dist/browser/screenshot.d.ts +29 -0
- package/dist/browser/screenshot.js +42 -0
- package/dist/cli/args.d.ts +35 -0
- package/dist/cli/args.js +69 -0
- package/dist/cli/cli.cli-test.d.ts +1 -0
- package/dist/cli/cli.cli-test.js +170 -0
- package/dist/cli/console-reporter.d.ts +15 -0
- package/dist/cli/console-reporter.js +52 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +61 -0
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.js +156 -0
- package/dist/config/config.d.ts +24 -0
- package/dist/config/config.js +42 -0
- package/dist/config/default.d.ts +2 -0
- package/dist/config/default.js +3 -0
- package/dist/config/mask.d.ts +11 -0
- package/dist/config/mask.js +85 -0
- package/dist/config/run-settings.d.ts +28 -0
- package/dist/config/run-settings.js +61 -0
- package/dist/config/secrets.d.ts +10 -0
- package/dist/config/secrets.js +67 -0
- package/dist/config/variables.d.ts +11 -0
- package/dist/config/variables.js +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -0
- package/dist/locator/action-timeout-error.d.ts +25 -0
- package/dist/locator/action-timeout-error.js +35 -0
- package/dist/locator/element-state.d.ts +44 -0
- package/dist/locator/element-state.js +109 -0
- package/dist/locator/label-locator.d.ts +13 -0
- package/dist/locator/label-locator.js +49 -0
- package/dist/locator/locator.d.ts +168 -0
- package/dist/locator/locator.js +591 -0
- package/dist/locator/selector-errors.d.ts +9 -0
- package/dist/locator/selector-errors.js +15 -0
- package/dist/locator/selector.d.ts +60 -0
- package/dist/locator/selector.js +113 -0
- package/dist/locator/text-locator.d.ts +13 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +30 -0
- package/dist/network/data-collector.d.ts +26 -0
- package/dist/network/data-collector.js +77 -0
- package/dist/network/network-tracker.d.ts +94 -0
- package/dist/network/network-tracker.js +198 -0
- package/dist/network/network-wait.d.ts +16 -0
- package/dist/network/network-wait.js +24 -0
- package/dist/network/response.d.ts +22 -0
- package/dist/network/response.js +42 -0
- package/dist/network/router.d.ts +120 -0
- package/dist/network/router.js +343 -0
- package/dist/network/url-match.d.ts +8 -0
- package/dist/network/url-match.js +38 -0
- package/dist/recorder/apply.d.ts +3 -0
- package/dist/recorder/apply.js +10 -0
- package/dist/recorder/capture.d.ts +18 -0
- package/dist/recorder/capture.js +198 -0
- package/dist/recorder/index.d.ts +3 -0
- package/dist/recorder/index.js +3 -0
- package/dist/recorder/normalise.d.ts +68 -0
- package/dist/recorder/normalise.js +104 -0
- package/dist/recorder/ranking.d.ts +9 -0
- package/dist/recorder/ranking.js +21 -0
- package/dist/recorder/recorder.d.ts +52 -0
- package/dist/recorder/recorder.js +143 -0
- package/dist/recorder/session.d.ts +49 -0
- package/dist/recorder/session.js +113 -0
- package/dist/runner/module-type.d.ts +6 -0
- package/dist/runner/module-type.js +29 -0
- package/dist/runner/page-logs-report.d.ts +40 -0
- package/dist/runner/page-logs-report.js +44 -0
- package/dist/runner/prepare-run.d.ts +14 -0
- package/dist/runner/prepare-run.js +20 -0
- package/dist/runner/project.d.ts +17 -0
- package/dist/runner/project.js +33 -0
- package/dist/runner/registry.d.ts +7 -0
- package/dist/runner/registry.js +40 -0
- package/dist/runner/reporter.d.ts +43 -0
- package/dist/runner/reporter.js +83 -0
- package/dist/runner/run.d.ts +27 -0
- package/dist/runner/run.js +34 -0
- package/dist/runner/test-error.d.ts +3 -0
- package/dist/runner/test-error.js +18 -0
- package/dist/runner/test-runner.d.ts +44 -0
- package/dist/runner/test-runner.js +205 -0
- package/dist/script/call-function.d.ts +34 -0
- package/dist/script/call-function.js +71 -0
- package/dist/script/element-handle.d.ts +10 -0
- package/{src/script/element-handle.ts → dist/script/element-handle.js} +4 -4
- package/dist/script/helpers.d.ts +28 -0
- package/dist/script/helpers.js +50 -0
- package/dist/script/preload.d.ts +31 -0
- package/dist/script/preload.js +71 -0
- package/dist/script/serialize.d.ts +29 -0
- package/dist/script/serialize.js +207 -0
- package/dist/steps/build.d.ts +5 -0
- package/dist/steps/build.js +38 -0
- package/dist/steps/edit.d.ts +30 -0
- package/dist/steps/edit.js +80 -0
- package/dist/steps/emit.d.ts +6 -0
- package/dist/steps/emit.js +73 -0
- package/dist/steps/index.d.ts +5 -0
- package/dist/steps/index.js +4 -0
- package/dist/steps/model.d.ts +103 -0
- package/dist/steps/model.js +1 -0
- package/dist/steps/parse.d.ts +3 -0
- package/dist/steps/parse.js +272 -0
- package/dist/transport/bidi-connection.d.ts +46 -0
- package/dist/transport/bidi-connection.js +176 -0
- package/dist/transport/bidi-error.d.ts +10 -0
- package/dist/transport/bidi-error.js +10 -0
- package/dist/transport/subscriptions.d.ts +36 -0
- package/dist/transport/subscriptions.js +81 -0
- package/{src → dist}/types/test.d.ts +10 -4
- package/dist/wait/wait-until.d.ts +20 -0
- package/dist/wait/wait-until.js +63 -0
- package/package.json +44 -7
- package/.claude/settings.local.json +0 -7
- package/.env.example +0 -3
- package/.github/actions/setup/action.yml +0 -16
- package/.github/workflows/ci.yml +0 -71
- package/.trunk/configs/.markdownlint.yaml +0 -2
- package/.trunk/configs/.yamllint.yaml +0 -7
- package/.trunk/configs/svgo.config.mjs +0 -14
- package/.trunk/trunk.yaml +0 -34
- package/docs/superpowers/bidi-roadmap.md +0 -60
- package/docs/superpowers/plans/2026-09-29-auto-wait.md +0 -1827
- package/docs/superpowers/plans/2026-09-29-expect.md +0 -1350
- package/docs/superpowers/plans/2026-10-01-ci.md +0 -350
- package/docs/superpowers/plans/2026-10-01-test-api-alignment.md +0 -1821
- package/docs/superpowers/recorder-roadmap.md +0 -52
- package/docs/superpowers/specs/2026-09-29-auto-wait-design.md +0 -185
- package/docs/superpowers/specs/2026-09-29-bidi-foundation-design.md +0 -130
- package/docs/superpowers/specs/2026-09-29-contexts-and-storage-design.md +0 -88
- package/docs/superpowers/specs/2026-09-29-downloads-and-file-dialogs-design.md +0 -94
- package/docs/superpowers/specs/2026-09-29-emulation-design.md +0 -88
- package/docs/superpowers/specs/2026-09-29-expect-design.md +0 -130
- package/docs/superpowers/specs/2026-09-29-locate-nodes-design.md +0 -90
- package/docs/superpowers/specs/2026-09-29-logs-and-errors-design.md +0 -87
- package/docs/superpowers/specs/2026-09-29-navigation-waits-design.md +0 -83
- package/docs/superpowers/specs/2026-09-29-network-data-design.md +0 -78
- package/docs/superpowers/specs/2026-09-29-network-interception-design.md +0 -91
- package/docs/superpowers/specs/2026-09-29-network-tracking-design.md +0 -86
- package/docs/superpowers/specs/2026-09-29-page-api-design.md +0 -90
- package/docs/superpowers/specs/2026-09-29-preload-scripts-design.md +0 -82
- package/docs/superpowers/specs/2026-09-29-script-call-function-design.md +0 -107
- package/docs/superpowers/specs/2026-09-29-set-files-design.md +0 -62
- package/docs/superpowers/specs/2026-09-29-user-prompts-design.md +0 -75
- package/docs/superpowers/specs/2026-10-01-ci-design.md +0 -106
- package/docs/superpowers/specs/2026-10-01-test-api-alignment-design.md +0 -241
- package/public/logo-dark.svg +0 -236
- package/public/logo-light.svg +0 -262
- package/samurai.config.ts +0 -7
- package/src/assert/assertion-error.test.ts +0 -55
- package/src/assert/assertion-error.ts +0 -62
- package/src/assert/expect-locator.test.ts +0 -217
- package/src/assert/expect.test.ts +0 -73
- package/src/assert/expect.ts +0 -299
- package/src/browser/browser-context.browser-test.ts +0 -201
- package/src/browser/browser-context.test.ts +0 -407
- package/src/browser/browser-context.ts +0 -208
- package/src/browser/browser-finder.ts +0 -110
- package/src/browser/browser.test.ts +0 -88
- package/src/browser/browser.ts +0 -515
- package/src/browser/context-tree.test.ts +0 -86
- package/src/browser/context-tree.ts +0 -70
- package/src/browser/dialog.browser-test.ts +0 -144
- package/src/browser/dialog.test.ts +0 -322
- package/src/browser/dialog.ts +0 -83
- package/src/browser/download.test.ts +0 -161
- package/src/browser/download.ts +0 -172
- package/src/browser/downloads-dir.test.ts +0 -75
- package/src/browser/downloads-dir.ts +0 -39
- package/src/browser/emulation.browser-test.ts +0 -186
- package/src/browser/emulation.test.ts +0 -200
- package/src/browser/emulation.ts +0 -204
- package/src/browser/file-chooser.test.ts +0 -142
- package/src/browser/file-chooser.ts +0 -99
- package/src/browser/file-paths.ts +0 -18
- package/src/browser/goto.browser-test.ts +0 -44
- package/src/browser/navigation-error.ts +0 -26
- package/src/browser/navigation.browser-test.ts +0 -172
- package/src/browser/page-api.browser-test.ts +0 -228
- package/src/browser/page-logs.browser-test.ts +0 -119
- package/src/browser/page-logs.test.ts +0 -212
- package/src/browser/page-logs.ts +0 -208
- package/src/browser/page-wait.ts +0 -19
- package/src/browser/page.test.ts +0 -1567
- package/src/browser/page.ts +0 -1176
- package/src/browser/pdf.ts +0 -62
- package/src/browser/screenshot.ts +0 -65
- package/src/browser/transfers.browser-test.ts +0 -180
- package/src/config/config.ts +0 -33
- package/src/config/default.ts +0 -5
- package/src/config/fixtures.test.ts +0 -137
- package/src/config/mask.test.ts +0 -143
- package/src/config/mask.ts +0 -87
- package/src/config/run-settings.test.ts +0 -163
- package/src/config/run-settings.ts +0 -114
- package/src/config/secrets.ts +0 -70
- package/src/config/variables.ts +0 -31
- package/src/index.ts +0 -10
- package/src/locator/action-timeout-error.test.ts +0 -83
- package/src/locator/action-timeout-error.ts +0 -72
- package/src/locator/element-state.test.ts +0 -181
- package/src/locator/element-state.ts +0 -159
- package/src/locator/locate-nodes.browser-test.ts +0 -169
- package/src/locator/locator-actions.test.ts +0 -355
- package/src/locator/locator-chain.test.ts +0 -292
- package/src/locator/locator.browser-test.ts +0 -137
- package/src/locator/locator.test.ts +0 -179
- package/src/locator/locator.ts +0 -656
- package/src/locator/selector-errors.ts +0 -18
- package/src/locator/selector.test.ts +0 -124
- package/src/locator/selector.ts +0 -107
- package/src/locator/set-files.browser-test.ts +0 -141
- package/src/locator/set-files.test.ts +0 -191
- package/src/locator/text-locator.test.ts +0 -118
- package/src/logger/index.test.ts +0 -28
- package/src/logger/index.ts +0 -40
- package/src/network/data-collector.test.ts +0 -111
- package/src/network/data-collector.ts +0 -92
- package/src/network/network-data.browser-test.ts +0 -192
- package/src/network/network-tracker.browser-test.ts +0 -151
- package/src/network/network-tracker.test.ts +0 -307
- package/src/network/network-tracker.ts +0 -286
- package/src/network/network-wait.ts +0 -28
- package/src/network/response.test.ts +0 -93
- package/src/network/response.ts +0 -58
- package/src/network/router.browser-test.ts +0 -260
- package/src/network/router.test.ts +0 -528
- package/src/network/router.ts +0 -420
- package/src/network/url-match.test.ts +0 -69
- package/src/network/url-match.ts +0 -41
- package/src/runner/page-logs-report.test.ts +0 -144
- package/src/runner/page-logs-report.ts +0 -82
- package/src/runner/prepare-run.test.ts +0 -33
- package/src/runner/prepare-run.ts +0 -36
- package/src/runner/registry.test.ts +0 -59
- package/src/runner/registry.ts +0 -46
- package/src/runner/reporter.test.ts +0 -73
- package/src/runner/reporter.ts +0 -109
- package/src/runner/test-error.test.ts +0 -39
- package/src/runner/test-error.ts +0 -20
- package/src/runner/test-runner.ts +0 -184
- package/src/script/call-function.browser-test.ts +0 -112
- package/src/script/call-function.test.ts +0 -174
- package/src/script/call-function.ts +0 -113
- package/src/script/helpers.test.ts +0 -116
- package/src/script/helpers.ts +0 -68
- package/src/script/preload.browser-test.ts +0 -144
- package/src/script/preload.test.ts +0 -180
- package/src/script/preload.ts +0 -102
- package/src/script/serialize.test.ts +0 -182
- package/src/script/serialize.ts +0 -203
- package/src/testing/browser-fixture.ts +0 -18
- package/src/testing/fake-websocket.ts +0 -72
- package/src/testing/stub-connector.ts +0 -154
- package/src/tests/index.spec.ts +0 -30
- package/src/transport/bidi-connection.test.ts +0 -155
- package/src/transport/bidi-connection.ts +0 -256
- package/src/transport/bidi-error.ts +0 -23
- package/src/transport/foundation.browser-test.ts +0 -73
- package/src/transport/subscriptions.test.ts +0 -140
- package/src/transport/subscriptions.ts +0 -110
- package/src/types/bidi-commands.test-d.ts +0 -55
- package/src/wait/wait-until.test.ts +0 -117
- package/src/wait/wait-until.ts +0 -86
- package/tsconfig.json +0 -44
- /package/{src/api.ts → dist/api.d.ts} +0 -0
- /package/{src/locator/text-locator.ts → dist/locator/text-locator.js} +0 -0
- /package/{src → dist}/types/bidi-modules/browser.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/browsing-context.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/emulation.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/input.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/log.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/network.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/permissions.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/script.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/session.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/storage.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/web-extensions.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/command.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/error.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/event.d.ts +0 -0
- /package/{src → dist}/types/bidi.d.ts +0 -0
- /package/{src → dist}/types/browser.d.ts +0 -0
- /package/{src → dist}/types/config.d.ts +0 -0
- /package/{src → dist}/types/env.d.ts +0 -0
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { after, before, test } from "node:test";
|
|
3
|
-
import { Browser } from "../browser/browser.js";
|
|
4
|
-
import type Page from "../browser/page.js";
|
|
5
|
-
import { setContent } from "../testing/browser-fixture.js";
|
|
6
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
7
|
-
import { callFunction } from "./call-function.js";
|
|
8
|
-
import { HELPER_SANDBOX } from "./helpers.js";
|
|
9
|
-
|
|
10
|
-
let browser: Browser;
|
|
11
|
-
let page: Page;
|
|
12
|
-
let connector: BiDiConnector;
|
|
13
|
-
/** Commands sent to the browser, by method, to count registrations */
|
|
14
|
-
const sentMethods: string[] = [];
|
|
15
|
-
|
|
16
|
-
before(async () => {
|
|
17
|
-
({ browser, page } = await Browser.launch("firefox", { port: 9234, headless: true }));
|
|
18
|
-
connector = (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
|
|
19
|
-
const send = connector.send.bind(connector);
|
|
20
|
-
connector.send = ((method: string, params: never, options: never) => {
|
|
21
|
-
sentMethods.push(method);
|
|
22
|
-
return send(method as never, params, options);
|
|
23
|
-
}) as typeof connector.send;
|
|
24
|
-
await page.navigateTo("about:blank", "complete");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
after(async () => {
|
|
28
|
-
await page.dispose();
|
|
29
|
-
await browser?.close();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const OPTIONS = { timeout: 20000 };
|
|
33
|
-
const evaluate = <T>(fn: string, sandbox?: string) =>
|
|
34
|
-
callFunction<T>(connector, page.contextId, fn, [], sandbox === undefined ? {} : { sandbox });
|
|
35
|
-
|
|
36
|
-
test("an init script runs before page scripts of the next document", OPTIONS, async () => {
|
|
37
|
-
const handle = await page.addInitScript(() => {
|
|
38
|
-
(globalThis as any).__x = 1;
|
|
39
|
-
});
|
|
40
|
-
await page.navigateTo(
|
|
41
|
-
"data:text/html,<script>window.seenByPage = window.__x</script>",
|
|
42
|
-
"complete",
|
|
43
|
-
);
|
|
44
|
-
assert.equal(await evaluate("() => window.seenByPage"), 1);
|
|
45
|
-
await handle.dispose();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("an init script registered after load applies to the current document and the next one", OPTIONS, async () => {
|
|
49
|
-
await page.navigateTo("about:blank", "complete");
|
|
50
|
-
const handle = await page.addInitScript(() => {
|
|
51
|
-
(globalThis as any).__late = "yes";
|
|
52
|
-
});
|
|
53
|
-
assert.equal(await evaluate("() => window.__late"), "yes"); // current document, via the immediate run
|
|
54
|
-
await page.navigateTo("about:blank?again", "complete");
|
|
55
|
-
assert.equal(await evaluate("() => window.__late"), "yes"); // next document, via the registration
|
|
56
|
-
await handle.dispose();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("an init script receives its argument", OPTIONS, async () => {
|
|
60
|
-
const handle = await page.addInitScript(
|
|
61
|
-
(flags: { beta: boolean; name: string }) => {
|
|
62
|
-
(globalThis as any).__flags = flags;
|
|
63
|
-
},
|
|
64
|
-
{ beta: true, name: `q"uote 'x' \\` },
|
|
65
|
-
);
|
|
66
|
-
await page.navigateTo("about:blank?args", "complete");
|
|
67
|
-
assert.deepEqual(await evaluate("() => window.__flags"), { beta: true, name: `q"uote 'x' \\` });
|
|
68
|
-
await handle.dispose();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("a string init script runs as statements", OPTIONS, async () => {
|
|
72
|
-
const handle = await page.addInitScript(`window.__a = 1; window.__b = window.__a + 1;`);
|
|
73
|
-
await page.navigateTo("about:blank?string", "complete");
|
|
74
|
-
assert.equal(await evaluate("() => window.__b"), 2);
|
|
75
|
-
await handle.dispose();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("dispose stops the effect on the next navigation", OPTIONS, async () => {
|
|
79
|
-
const handle = await page.addInitScript(() => {
|
|
80
|
-
(globalThis as any).__gone = true;
|
|
81
|
-
});
|
|
82
|
-
await page.navigateTo("about:blank?with", "complete");
|
|
83
|
-
assert.equal(await evaluate("() => window.__gone"), true);
|
|
84
|
-
await handle.dispose();
|
|
85
|
-
await page.navigateTo("about:blank?without", "complete");
|
|
86
|
-
assert.equal(await evaluate("() => window.__gone"), undefined);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("an init script also applies to iframes", OPTIONS, async () => {
|
|
90
|
-
const handle = await page.addInitScript(() => {
|
|
91
|
-
(globalThis as any).__inFrame = "yes";
|
|
92
|
-
});
|
|
93
|
-
await page.navigateTo("about:blank?frames", "complete");
|
|
94
|
-
const seen = await callFunction<Promise<unknown>>(
|
|
95
|
-
connector,
|
|
96
|
-
page.contextId,
|
|
97
|
-
`async () => {
|
|
98
|
-
const frame = document.body.appendChild(document.createElement("iframe"));
|
|
99
|
-
frame.src = "about:blank";
|
|
100
|
-
await new Promise((resolve) => frame.addEventListener("load", resolve, { once: true }));
|
|
101
|
-
return frame.contentWindow.__inFrame;
|
|
102
|
-
}`,
|
|
103
|
-
);
|
|
104
|
-
assert.equal(seen, "yes");
|
|
105
|
-
await handle.dispose();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("a throwing init script does not break registration or the page", OPTIONS, async () => {
|
|
109
|
-
const handle = await page.addInitScript(`throw new Error("init script boom");`);
|
|
110
|
-
await page.navigateTo("about:blank?boom", "complete");
|
|
111
|
-
assert.equal(await evaluate("() => 1 + 1"), 2);
|
|
112
|
-
await handle.dispose();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("the helper realm is registered once and invisible to the page", OPTIONS, async () => {
|
|
116
|
-
await page.navigateTo("about:blank?helpers", "complete");
|
|
117
|
-
await setContent(page, `<button id="b">Go</button>`);
|
|
118
|
-
const button = page.locator("button[@id='b']");
|
|
119
|
-
assert.equal(await button.isEnabled(), true);
|
|
120
|
-
|
|
121
|
-
// not on the page's own window ...
|
|
122
|
-
assert.equal(await evaluate("() => typeof window.__samurai"), "undefined");
|
|
123
|
-
// ... only in the sandbox realm
|
|
124
|
-
assert.equal(await evaluate("() => typeof globalThis.__samurai", HELPER_SANDBOX), "object");
|
|
125
|
-
|
|
126
|
-
const registrations = () =>
|
|
127
|
-
sentMethods.filter((method) => method === "script.addPreloadScript").length;
|
|
128
|
-
const before = registrations();
|
|
129
|
-
// more probes, after navigations: the registration is reused, and the helpers are there in each new document
|
|
130
|
-
for (const url of ["about:blank?one", "about:blank?two"]) {
|
|
131
|
-
await page.navigateTo(url, "complete");
|
|
132
|
-
await setContent(page, `<button id="b">Go</button>`);
|
|
133
|
-
assert.equal(await page.locator("button[@id='b']").isEnabled(), true);
|
|
134
|
-
}
|
|
135
|
-
assert.equal(registrations(), before);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("page scripts cannot tamper with the helpers", OPTIONS, async () => {
|
|
139
|
-
await page.navigateTo("about:blank?tamper", "complete");
|
|
140
|
-
await setContent(page, `<button id="b">Go</button>`);
|
|
141
|
-
await evaluate("() => { window.__samurai = { probeElement: () => 'hacked' }; }");
|
|
142
|
-
const state = await page.locator("button[@id='b']").isEnabled();
|
|
143
|
-
assert.equal(state, true);
|
|
144
|
-
});
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { autoReply, FakeWebSocket } from "../testing/fake-websocket.js";
|
|
4
|
-
import { BiDiConnector } from "../transport/bidi-connection.js";
|
|
5
|
-
import { addPreload } from "./preload.js";
|
|
6
|
-
import { ScriptError } from "./call-function.js";
|
|
7
|
-
|
|
8
|
-
const ok = { type: "success", realm: "r", result: { type: "undefined" } };
|
|
9
|
-
const frame = (context: string, children: object[] = []) => ({ context, parent: null, children });
|
|
10
|
-
|
|
11
|
-
interface Setup {
|
|
12
|
-
tree?: (params: any) => object[];
|
|
13
|
-
callFunction?: (params: any) => object | Error;
|
|
14
|
-
removePreload?: () => object | Error;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function setup({ tree, callFunction, removePreload }: Setup = {}) {
|
|
18
|
-
const ws = new FakeWebSocket();
|
|
19
|
-
const connector = new BiDiConnector(ws as unknown as WebSocket);
|
|
20
|
-
const stop = autoReply(ws, {
|
|
21
|
-
"script.addPreloadScript": () => ({ script: "preload-1" }),
|
|
22
|
-
"browsingContext.getTree": (params) => ({ contexts: tree?.(params) ?? [] }),
|
|
23
|
-
"script.callFunction": callFunction ?? (() => ok),
|
|
24
|
-
"script.removePreloadScript": removePreload ?? (() => ({})),
|
|
25
|
-
});
|
|
26
|
-
const methods = () => ws.sent.map((m) => m.method);
|
|
27
|
-
return { ws, connector, stop, methods };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("addPreload", () => {
|
|
31
|
-
it("registers the script with its contexts and sandbox", async () => {
|
|
32
|
-
const { ws, connector, stop } = setup();
|
|
33
|
-
const handle = await addPreload(connector, {
|
|
34
|
-
source: "() => { globalThis.x = 1 }",
|
|
35
|
-
contexts: ["top"],
|
|
36
|
-
sandbox: "samurai",
|
|
37
|
-
});
|
|
38
|
-
assert.equal(handle.id, "preload-1");
|
|
39
|
-
assert.deepEqual(ws.sent[0]!.params, {
|
|
40
|
-
functionDeclaration: "() => { globalThis.x = 1 }",
|
|
41
|
-
contexts: ["top"],
|
|
42
|
-
sandbox: "samurai",
|
|
43
|
-
});
|
|
44
|
-
stop();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("omits contexts and sandbox when not given", async () => {
|
|
48
|
-
const { ws, connector, stop } = setup();
|
|
49
|
-
await addPreload(connector, { source: "() => {}" });
|
|
50
|
-
assert.deepEqual(ws.sent[0]!.params, { functionDeclaration: "() => {}" });
|
|
51
|
-
stop();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("also runs the source in every loaded context under the given roots, same sandbox", async () => {
|
|
55
|
-
const { ws, connector, stop, methods } = setup({
|
|
56
|
-
tree: (params) =>
|
|
57
|
-
params.root === "top" ? [frame("top", [frame("child", [frame("grandchild")])])] : [],
|
|
58
|
-
});
|
|
59
|
-
await addPreload(connector, { source: "() => 1", contexts: ["top"], sandbox: "samurai" });
|
|
60
|
-
// registered first, so a document created meanwhile is not missed
|
|
61
|
-
assert.equal(methods()[0], "script.addPreloadScript");
|
|
62
|
-
const getTrees = ws.sent.filter((m) => m.method === "browsingContext.getTree");
|
|
63
|
-
assert.deepEqual(getTrees.map((m) => m.params), [{ root: "top" }]);
|
|
64
|
-
const calls = ws.sent.filter((m) => m.method === "script.callFunction").map((m) => m.params as any);
|
|
65
|
-
assert.deepEqual(calls.map((c) => c.target).sort((a, b) => a.context.localeCompare(b.context)), [
|
|
66
|
-
{ context: "child", sandbox: "samurai" },
|
|
67
|
-
{ context: "grandchild", sandbox: "samurai" },
|
|
68
|
-
{ context: "top", sandbox: "samurai" },
|
|
69
|
-
]);
|
|
70
|
-
assert.ok(calls.every((c) => c.functionDeclaration === "() => 1" && c.awaitPromise === false));
|
|
71
|
-
stop();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("without contexts it covers every top-level context", async () => {
|
|
75
|
-
const { ws, connector, stop } = setup({ tree: () => [frame("a"), frame("b", [frame("b-frame")])] });
|
|
76
|
-
await addPreload(connector, { source: "() => 1" });
|
|
77
|
-
assert.deepEqual(ws.sent.find((m) => m.method === "browsingContext.getTree")!.params, {});
|
|
78
|
-
const targets = ws.sent
|
|
79
|
-
.filter((m) => m.method === "script.callFunction")
|
|
80
|
-
.map((m) => (m.params as any).target.context)
|
|
81
|
-
.sort();
|
|
82
|
-
assert.deepEqual(targets, ["a", "b", "b-frame"]);
|
|
83
|
-
stop();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("ignores a context that disappeared while running", async () => {
|
|
87
|
-
const ws = new FakeWebSocket();
|
|
88
|
-
const connector = new BiDiConnector(ws as unknown as WebSocket);
|
|
89
|
-
const timer = setInterval(() => {
|
|
90
|
-
for (const m of ws.sent.splice(0)) {
|
|
91
|
-
if (m.method === "script.addPreloadScript") ws.reply(m.id, { script: "p" });
|
|
92
|
-
else if (m.method === "browsingContext.getTree") ws.reply(m.id, { contexts: [frame("top")] });
|
|
93
|
-
else ws.replyError(m.id, "no such frame", "frame is gone");
|
|
94
|
-
}
|
|
95
|
-
}, 1).unref();
|
|
96
|
-
await assert.doesNotReject(addPreload(connector, { source: "() => 1", contexts: ["top"] }));
|
|
97
|
-
clearInterval(timer);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("throws a ScriptError from the immediate run by default", async () => {
|
|
101
|
-
const { connector, stop } = setup({
|
|
102
|
-
tree: () => [frame("top")],
|
|
103
|
-
callFunction: () => ({
|
|
104
|
-
type: "exception",
|
|
105
|
-
realm: "r",
|
|
106
|
-
exceptionDetails: { text: "ReferenceError: nope", stackTrace: { callFrames: [] } },
|
|
107
|
-
}),
|
|
108
|
-
});
|
|
109
|
-
await assert.rejects(addPreload(connector, { source: "() => nope" }), ScriptError);
|
|
110
|
-
stop();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("removes the registration again when the immediate run throws", async () => {
|
|
114
|
-
const { ws, connector, stop, methods } = setup({
|
|
115
|
-
tree: () => [frame("top")],
|
|
116
|
-
callFunction: () => ({
|
|
117
|
-
type: "exception",
|
|
118
|
-
realm: "r",
|
|
119
|
-
exceptionDetails: { text: "boom", stackTrace: { callFrames: [] } },
|
|
120
|
-
}),
|
|
121
|
-
});
|
|
122
|
-
await assert.rejects(addPreload(connector, { source: "() => 1" }), ScriptError);
|
|
123
|
-
assert.ok(methods().includes("script.removePreloadScript"));
|
|
124
|
-
assert.deepEqual(
|
|
125
|
-
ws.sent.find((m) => m.method === "script.removePreloadScript")!.params,
|
|
126
|
-
{ script: "preload-1" },
|
|
127
|
-
);
|
|
128
|
-
stop();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("with onRunError log the immediate run's exception does not fail the registration", async () => {
|
|
132
|
-
const { connector, stop } = setup({
|
|
133
|
-
tree: () => [frame("top")],
|
|
134
|
-
callFunction: () => ({
|
|
135
|
-
type: "exception",
|
|
136
|
-
realm: "r",
|
|
137
|
-
exceptionDetails: { text: "boom", stackTrace: { callFrames: [] } },
|
|
138
|
-
}),
|
|
139
|
-
});
|
|
140
|
-
const handle = await addPreload(connector, { source: "() => 1", onRunError: "log" });
|
|
141
|
-
assert.equal(handle.id, "preload-1");
|
|
142
|
-
stop();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("rethrows when registering fails", async () => {
|
|
146
|
-
const ws = new FakeWebSocket();
|
|
147
|
-
const connector = new BiDiConnector(ws as unknown as WebSocket);
|
|
148
|
-
const timer = setInterval(() => {
|
|
149
|
-
for (const m of ws.sent.splice(0)) ws.replyError(m.id, "invalid argument", "bad script");
|
|
150
|
-
}, 1).unref();
|
|
151
|
-
await assert.rejects(addPreload(connector, { source: "not a function" }), { code: "invalid argument" });
|
|
152
|
-
clearInterval(timer);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("dispose removes the registration once, however often it is called", async () => {
|
|
156
|
-
const { ws, connector, stop } = setup();
|
|
157
|
-
const handle = await addPreload(connector, { source: "() => 1" });
|
|
158
|
-
await handle.dispose();
|
|
159
|
-
await handle.dispose();
|
|
160
|
-
const removals = ws.sent.filter((m) => m.method === "script.removePreloadScript");
|
|
161
|
-
assert.equal(removals.length, 1);
|
|
162
|
-
assert.deepEqual(removals[0]!.params, { script: "preload-1" });
|
|
163
|
-
stop();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("dispose swallows no such script", async () => {
|
|
167
|
-
const ws = new FakeWebSocket();
|
|
168
|
-
const connector = new BiDiConnector(ws as unknown as WebSocket);
|
|
169
|
-
const timer = setInterval(() => {
|
|
170
|
-
for (const m of ws.sent.splice(0)) {
|
|
171
|
-
if (m.method === "script.addPreloadScript") ws.reply(m.id, { script: "p" });
|
|
172
|
-
else if (m.method === "browsingContext.getTree") ws.reply(m.id, { contexts: [] });
|
|
173
|
-
else ws.replyError(m.id, "no such script", "already removed");
|
|
174
|
-
}
|
|
175
|
-
}, 1).unref();
|
|
176
|
-
const handle = await addPreload(connector, { source: "() => 1" });
|
|
177
|
-
await assert.doesNotReject(handle.dispose());
|
|
178
|
-
clearInterval(timer);
|
|
179
|
-
});
|
|
180
|
-
});
|
package/src/script/preload.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import logger from "../logger/index.js";
|
|
2
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
3
|
-
import { BiDiError } from "../transport/bidi-error.js";
|
|
4
|
-
import type { Info } from "../types/bidi-modules/browsing-context.js";
|
|
5
|
-
import { callFunction, ScriptError } from "./call-function.js";
|
|
6
|
-
|
|
7
|
-
export interface PreloadHandle {
|
|
8
|
-
readonly id: string;
|
|
9
|
-
/** Idempotent. Documents that are already loaded keep the effect until they navigate */
|
|
10
|
-
dispose(): Promise<void>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface RunOptions {
|
|
14
|
-
/** A function declaration, e.g. `() => { … }` */
|
|
15
|
-
source: string;
|
|
16
|
-
/** Top-level contexts to cover, including their iframes. Default: every top-level context */
|
|
17
|
-
contexts?: string[];
|
|
18
|
-
/** Named sandbox realm; without it the script runs in the page's own realm */
|
|
19
|
-
sandbox?: string;
|
|
20
|
-
/**
|
|
21
|
-
* What to do when the immediate run in an already-loaded document throws.
|
|
22
|
-
* "log" suits user init scripts (the browser reports such errors for later documents itself)
|
|
23
|
-
* @default "throw"
|
|
24
|
-
*/
|
|
25
|
-
onRunError?: "throw" | "log";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Registers `source` to run before any page script in every future document of `contexts`, and
|
|
30
|
-
* also runs it once in the documents that are loaded now: preload scripts only affect documents
|
|
31
|
-
* created after registration, and callers should not have to care about that ordering.
|
|
32
|
-
*/
|
|
33
|
-
export async function addPreload(
|
|
34
|
-
connector: BiDiConnector,
|
|
35
|
-
options: RunOptions,
|
|
36
|
-
): Promise<PreloadHandle> {
|
|
37
|
-
const { script } = await connector.send("script.addPreloadScript", {
|
|
38
|
-
functionDeclaration: options.source,
|
|
39
|
-
...(options.contexts && { contexts: options.contexts }),
|
|
40
|
-
...(options.sandbox !== undefined && { sandbox: options.sandbox }),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
let removed = false;
|
|
44
|
-
const dispose = async () => {
|
|
45
|
-
if (removed) return;
|
|
46
|
-
removed = true;
|
|
47
|
-
try {
|
|
48
|
-
await connector.send("script.removePreloadScript", { script });
|
|
49
|
-
} catch (err) {
|
|
50
|
-
if (err instanceof BiDiError && err.code === "no such script") return;
|
|
51
|
-
throw err;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
// Registered first, so a document created while this runs is covered by the registration
|
|
57
|
-
await runInLoadedContexts(connector, options);
|
|
58
|
-
} catch (err) {
|
|
59
|
-
await dispose().catch((disposeErr) =>
|
|
60
|
-
logger.debug("Could not remove preload script after a failed run", { disposeErr }),
|
|
61
|
-
);
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
64
|
-
return { id: script, dispose };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Runs `source` once in every loaded context (iframes included) under `contexts` */
|
|
68
|
-
export async function runInLoadedContexts(
|
|
69
|
-
connector: BiDiConnector,
|
|
70
|
-
{ source, contexts, sandbox, onRunError = "throw" }: RunOptions,
|
|
71
|
-
): Promise<void> {
|
|
72
|
-
const trees = contexts
|
|
73
|
-
? await Promise.all(
|
|
74
|
-
contexts.map((root) => connector.send("browsingContext.getTree", { root })),
|
|
75
|
-
)
|
|
76
|
-
: [await connector.send("browsingContext.getTree", {})];
|
|
77
|
-
const loaded = trees.flatMap((tree) => tree.contexts.flatMap(flatten));
|
|
78
|
-
|
|
79
|
-
const outcomes = await Promise.allSettled(
|
|
80
|
-
loaded.map((context) =>
|
|
81
|
-
callFunction(connector, context, source, [], {
|
|
82
|
-
awaitPromise: false,
|
|
83
|
-
...(sandbox !== undefined && { sandbox }),
|
|
84
|
-
}),
|
|
85
|
-
),
|
|
86
|
-
);
|
|
87
|
-
for (const outcome of outcomes) {
|
|
88
|
-
if (outcome.status === "fulfilled") continue;
|
|
89
|
-
const err: unknown = outcome.reason;
|
|
90
|
-
// The context closed between listing and running: nothing left to cover
|
|
91
|
-
if (err instanceof BiDiError && err.code === "no such frame") continue;
|
|
92
|
-
if (err instanceof ScriptError && onRunError === "log") {
|
|
93
|
-
logger.warn("Script threw while running in a loaded document: %s", err.text);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
throw err;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function flatten(info: Info): string[] {
|
|
101
|
-
return [info.context, ...(info.children ?? []).flatMap(flatten)];
|
|
102
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type { RemoteValue } from "../types/bidi-modules/script.js";
|
|
4
|
-
import { ElementHandle } from "./element-handle.js";
|
|
5
|
-
import { fromRemoteValue, RemoteObject, toLocalValue } from "./serialize.js";
|
|
6
|
-
|
|
7
|
-
describe("toLocalValue", () => {
|
|
8
|
-
it("serializes primitives", () => {
|
|
9
|
-
assert.deepEqual(toLocalValue(undefined), { type: "undefined" });
|
|
10
|
-
assert.deepEqual(toLocalValue(null), { type: "null" });
|
|
11
|
-
assert.deepEqual(toLocalValue(true), { type: "boolean", value: true });
|
|
12
|
-
assert.deepEqual(toLocalValue("a\"b'c\\d\n"), { type: "string", value: "a\"b'c\\d\n" });
|
|
13
|
-
assert.deepEqual(toLocalValue(12.5), { type: "number", value: 12.5 });
|
|
14
|
-
assert.deepEqual(toLocalValue(10n), { type: "bigint", value: "10" });
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("serializes the special numbers", () => {
|
|
18
|
-
assert.deepEqual(toLocalValue(NaN), { type: "number", value: "NaN" });
|
|
19
|
-
assert.deepEqual(toLocalValue(Infinity), { type: "number", value: "Infinity" });
|
|
20
|
-
assert.deepEqual(toLocalValue(-Infinity), { type: "number", value: "-Infinity" });
|
|
21
|
-
assert.deepEqual(toLocalValue(-0), { type: "number", value: "-0" });
|
|
22
|
-
assert.deepEqual(toLocalValue(0), { type: "number", value: 0 });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("serializes dates and regular expressions", () => {
|
|
26
|
-
assert.deepEqual(toLocalValue(new Date("2026-09-29T10:00:00.000Z")), {
|
|
27
|
-
type: "date",
|
|
28
|
-
value: "2026-09-29T10:00:00.000Z",
|
|
29
|
-
});
|
|
30
|
-
assert.deepEqual(toLocalValue(/ab+c/gi), { type: "regexp", value: { pattern: "ab+c", flags: "gi" } });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("serializes arrays, sets, maps and plain objects, nested", () => {
|
|
34
|
-
assert.deepEqual(toLocalValue([1, "a"]), {
|
|
35
|
-
type: "array",
|
|
36
|
-
value: [
|
|
37
|
-
{ type: "number", value: 1 },
|
|
38
|
-
{ type: "string", value: "a" },
|
|
39
|
-
],
|
|
40
|
-
});
|
|
41
|
-
assert.deepEqual(toLocalValue(new Set([1])), { type: "set", value: [{ type: "number", value: 1 }] });
|
|
42
|
-
assert.deepEqual(toLocalValue(new Map<unknown, unknown>([["k", true], [2, null]])), {
|
|
43
|
-
type: "map",
|
|
44
|
-
value: [
|
|
45
|
-
["k", { type: "boolean", value: true }],
|
|
46
|
-
[{ type: "number", value: 2 }, { type: "null" }],
|
|
47
|
-
],
|
|
48
|
-
});
|
|
49
|
-
assert.deepEqual(toLocalValue({ a: { b: [undefined] } }), {
|
|
50
|
-
type: "object",
|
|
51
|
-
value: [
|
|
52
|
-
[
|
|
53
|
-
"a",
|
|
54
|
-
{
|
|
55
|
-
type: "object",
|
|
56
|
-
value: [["b", { type: "array", value: [{ type: "undefined" }] }]],
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
],
|
|
60
|
-
});
|
|
61
|
-
assert.deepEqual(toLocalValue(Object.create(null)), { type: "object", value: [] });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("serializes an ElementHandle as a shared reference", () => {
|
|
65
|
-
assert.deepEqual(toLocalValue(new ElementHandle("node-1")), { sharedId: "node-1" });
|
|
66
|
-
assert.deepEqual(toLocalValue(new ElementHandle("node-1", "h1")), { sharedId: "node-1", handle: "h1" });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("rejects what cannot cross the wire", () => {
|
|
70
|
-
assert.throws(() => toLocalValue(() => 1), /cannot serialize function/);
|
|
71
|
-
assert.throws(() => toLocalValue(Symbol("s")), /cannot serialize symbol/);
|
|
72
|
-
assert.throws(() => toLocalValue(new (class Foo {})()), /cannot serialize Foo/);
|
|
73
|
-
assert.throws(() => toLocalValue(Buffer.from("a")), /cannot serialize Buffer/);
|
|
74
|
-
assert.throws(() => toLocalValue({ nested: () => 1 }), /cannot serialize function/);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("rejects cycles and absurd depth", () => {
|
|
78
|
-
const loop: Record<string, unknown> = {};
|
|
79
|
-
loop.self = loop;
|
|
80
|
-
assert.throws(() => toLocalValue(loop), /circular/);
|
|
81
|
-
const list: unknown[] = [];
|
|
82
|
-
list.push(list);
|
|
83
|
-
assert.throws(() => toLocalValue(list), /circular/);
|
|
84
|
-
let deep: unknown = 1;
|
|
85
|
-
for (let i = 0; i < 150; i++) deep = [deep];
|
|
86
|
-
assert.throws(() => toLocalValue(deep), /too deeply nested/);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("allows the same object twice when it is not a cycle", () => {
|
|
90
|
-
const shared = { a: 1 };
|
|
91
|
-
assert.doesNotThrow(() => toLocalValue([shared, shared]));
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe("fromRemoteValue", () => {
|
|
96
|
-
it("deserializes primitives and special numbers", () => {
|
|
97
|
-
assert.equal(fromRemoteValue({ type: "undefined" }), undefined);
|
|
98
|
-
assert.equal(fromRemoteValue({ type: "null" }), null);
|
|
99
|
-
assert.equal(fromRemoteValue({ type: "string", value: "x" }), "x");
|
|
100
|
-
assert.equal(fromRemoteValue({ type: "boolean", value: false }), false);
|
|
101
|
-
assert.equal(fromRemoteValue({ type: "number", value: 3 }), 3);
|
|
102
|
-
assert.ok(Number.isNaN(fromRemoteValue({ type: "number", value: "NaN" } as never)));
|
|
103
|
-
assert.equal(fromRemoteValue({ type: "number", value: "Infinity" } as never), Infinity);
|
|
104
|
-
assert.equal(fromRemoteValue({ type: "number", value: "-Infinity" } as never), -Infinity);
|
|
105
|
-
assert.ok(Object.is(fromRemoteValue({ type: "number", value: "-0" } as never), -0));
|
|
106
|
-
assert.equal(fromRemoteValue({ type: "bigint", value: "12345678901234567890" } as never), 12345678901234567890n);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("deserializes dates and regular expressions", () => {
|
|
110
|
-
const date = fromRemoteValue({ type: "date", value: "2026-09-29T10:00:00.000Z" }) as Date;
|
|
111
|
-
assert.equal(date.toISOString(), "2026-09-29T10:00:00.000Z");
|
|
112
|
-
const regexp = fromRemoteValue({ type: "regexp", value: { pattern: "a+", flags: "i" } } as never) as RegExp;
|
|
113
|
-
assert.equal(regexp.source, "a+");
|
|
114
|
-
assert.equal(regexp.flags, "i");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("deserializes collections, nested", () => {
|
|
118
|
-
const value: RemoteValue = {
|
|
119
|
-
type: "object",
|
|
120
|
-
value: [
|
|
121
|
-
["list", { type: "array", value: [{ type: "number", value: 1 }, { type: "string", value: "a" }] }],
|
|
122
|
-
[{ type: "number", value: 5 }, { type: "null" }],
|
|
123
|
-
["set", { type: "set", value: [{ type: "boolean", value: true }] } as never],
|
|
124
|
-
["map", { type: "map", value: [["k", { type: "number", value: 2 }]] } as never],
|
|
125
|
-
],
|
|
126
|
-
};
|
|
127
|
-
const result = fromRemoteValue(value) as Record<string, unknown>;
|
|
128
|
-
assert.deepEqual(result.list, [1, "a"]);
|
|
129
|
-
assert.equal(result["5"], null);
|
|
130
|
-
assert.deepEqual([...(result.set as Set<unknown>)], [true]);
|
|
131
|
-
assert.deepEqual([...(result.map as Map<unknown, unknown>)], [["k", 2]]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("turns a node into an ElementHandle", () => {
|
|
135
|
-
const handle = fromRemoteValue({ type: "node", sharedId: "n1", handle: "h1", value: {} } as never);
|
|
136
|
-
assert.ok(handle instanceof ElementHandle);
|
|
137
|
-
assert.equal(handle.sharedId, "n1");
|
|
138
|
-
assert.equal(handle.handle, "h1");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("returns an opaque RemoteObject for values that cannot be copied", () => {
|
|
142
|
-
for (const type of ["function", "window", "promise", "symbol", "error", "weakmap"]) {
|
|
143
|
-
const value = fromRemoteValue({ type, handle: "h" } as never);
|
|
144
|
-
assert.ok(value instanceof RemoteObject, type);
|
|
145
|
-
assert.equal((value as RemoteObject).type, type);
|
|
146
|
-
assert.equal((value as RemoteObject).handle, "h");
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("returns a RemoteObject for an object cut off by the serialization depth", () => {
|
|
151
|
-
const value = fromRemoteValue({ type: "object", handle: "h" } as never);
|
|
152
|
-
assert.ok(value instanceof RemoteObject);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("resolves internalId back references, cycles included", () => {
|
|
156
|
-
const value = {
|
|
157
|
-
type: "object",
|
|
158
|
-
internalId: "1",
|
|
159
|
-
value: [["self", { type: "object", internalId: "1" }]],
|
|
160
|
-
} as RemoteValue;
|
|
161
|
-
const result = fromRemoteValue(value) as { self: unknown };
|
|
162
|
-
assert.equal(result.self, result);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("keeps handles when asked, attaching the deserialized value", () => {
|
|
166
|
-
const value = { type: "object", handle: "h", value: [["a", { type: "number", value: 1 }]] } as RemoteValue;
|
|
167
|
-
const kept = fromRemoteValue(value, { keepHandles: true }) as RemoteObject;
|
|
168
|
-
assert.ok(kept instanceof RemoteObject);
|
|
169
|
-
assert.equal(kept.handle, "h");
|
|
170
|
-
assert.deepEqual(kept.value, { a: 1 });
|
|
171
|
-
assert.deepEqual(fromRemoteValue(value), { a: 1 });
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
describe("round trip", () => {
|
|
176
|
-
it("survives toLocalValue → fromRemoteValue for plain data", () => {
|
|
177
|
-
const data = { a: [1, "two", { three: true }], n: null, d: new Date(0), big: 5n };
|
|
178
|
-
// A local value has the same shape a remote value has for these types, except `value` keys
|
|
179
|
-
const remote = toLocalValue(data) as unknown as RemoteValue;
|
|
180
|
-
assert.deepEqual(fromRemoteValue(remote), data);
|
|
181
|
-
});
|
|
182
|
-
});
|