@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,112 +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 { BiDiError } from "../transport/bidi-error.js";
|
|
8
|
-
import { callFunction, disown, ScriptError } from "./call-function.js";
|
|
9
|
-
import { ElementHandle } from "./element-handle.js";
|
|
10
|
-
import { RemoteObject } from "./serialize.js";
|
|
11
|
-
|
|
12
|
-
let browser: Browser;
|
|
13
|
-
let page: Page;
|
|
14
|
-
let connector: BiDiConnector;
|
|
15
|
-
|
|
16
|
-
before(async () => {
|
|
17
|
-
({ browser, page } = await Browser.launch("firefox", { port: 9233, headless: true }));
|
|
18
|
-
connector = (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
|
|
19
|
-
await page.navigateTo("about:blank", "complete");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
after(async () => {
|
|
23
|
-
await browser?.close();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const OPTIONS = { timeout: 15000 };
|
|
27
|
-
const call = <T>(fn: string | ((...args: any[]) => unknown), args: unknown[] = [], options = {}) =>
|
|
28
|
-
callFunction<T>(connector, page.contextId, fn, args, options);
|
|
29
|
-
|
|
30
|
-
test("plain values, Map, Set, Date and RegExp round trip", OPTIONS, async () => {
|
|
31
|
-
const input = {
|
|
32
|
-
text: `quote " apostrophe ' backslash \\ newline \n`,
|
|
33
|
-
numbers: [1, -0, NaN, Infinity],
|
|
34
|
-
map: new Map<unknown, unknown>([["k", 1]]),
|
|
35
|
-
set: new Set([1, 2]),
|
|
36
|
-
date: new Date("2026-09-29T10:00:00.000Z"),
|
|
37
|
-
pattern: /a+b/gi,
|
|
38
|
-
big: 12345678901234567890n,
|
|
39
|
-
nothing: null,
|
|
40
|
-
missing: undefined,
|
|
41
|
-
};
|
|
42
|
-
const output = await call<typeof input>("(value) => value", [input]);
|
|
43
|
-
assert.equal(output.text, input.text);
|
|
44
|
-
assert.ok(Object.is(output.numbers[1], -0));
|
|
45
|
-
assert.ok(Number.isNaN(output.numbers[2]));
|
|
46
|
-
assert.equal(output.numbers[3], Infinity);
|
|
47
|
-
assert.deepEqual([...output.map], [["k", 1]]);
|
|
48
|
-
assert.deepEqual([...output.set], [1, 2]);
|
|
49
|
-
assert.equal(output.date.toISOString(), input.date.toISOString());
|
|
50
|
-
assert.equal(output.pattern.source, "a+b");
|
|
51
|
-
assert.equal(output.pattern.flags, "gi");
|
|
52
|
-
assert.equal(output.big, input.big);
|
|
53
|
-
assert.equal(output.nothing, null);
|
|
54
|
-
assert.equal(output.missing, undefined);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("an xpath with every quote style and a newline is located without escaping", OPTIONS, async () => {
|
|
58
|
-
await setContent(page, `<a id="x">Don't "stop"</a>`);
|
|
59
|
-
const xpath = `//a[@id="x" and contains(., "Don't")]\n`;
|
|
60
|
-
const link = page.locator(xpath);
|
|
61
|
-
assert.equal(await link.count(), 1);
|
|
62
|
-
assert.equal(await link.textContent(), `Don't "stop"`);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("awaitPromise resolves async functions", OPTIONS, async () => {
|
|
66
|
-
assert.equal(await call("async () => { await new Promise((r) => setTimeout(r, 50)); return 7; }"), 7);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("a function object is sent as its source", OPTIONS, async () => {
|
|
70
|
-
assert.equal(await call((a: number, b: number) => a + b, [2, 3]), 5);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("a throwing function becomes a ScriptError", OPTIONS, async () => {
|
|
74
|
-
await assert.rejects(call(`() => { null.x; }`), (err: unknown) => {
|
|
75
|
-
assert.ok(err instanceof ScriptError);
|
|
76
|
-
assert.match(err.text, /TypeError/);
|
|
77
|
-
return true;
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("nodes come back as ElementHandles and can be passed in again", OPTIONS, async () => {
|
|
82
|
-
await setContent(page, `<p id="p">hello</p>`);
|
|
83
|
-
const handle = await call<ElementHandle>(`() => document.getElementById("p")`);
|
|
84
|
-
assert.ok(handle instanceof ElementHandle);
|
|
85
|
-
assert.equal(await call<string>(`(el) => el.textContent`, [handle]), "hello");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("a removed node stays usable, a node from a previous document fails with no such node", OPTIONS, async () => {
|
|
89
|
-
await setContent(page, `<p id="p">hello</p>`);
|
|
90
|
-
const handle = await call<ElementHandle>(`() => document.getElementById("p")`);
|
|
91
|
-
await call(`() => document.getElementById("p").remove()`);
|
|
92
|
-
// Detached but alive: the browser still resolves the reference
|
|
93
|
-
assert.equal(await call<string>(`(el) => el.textContent`, [handle]), "hello");
|
|
94
|
-
await page.navigateTo("about:blank?next", "complete");
|
|
95
|
-
await assert.rejects(
|
|
96
|
-
call(`(el) => el.textContent`, [handle]),
|
|
97
|
-
(err: unknown) => err instanceof BiDiError && err.code === "no such node",
|
|
98
|
-
);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("ownership root keeps a handle that disown releases, twice without error", OPTIONS, async () => {
|
|
102
|
-
const kept = await call<RemoteObject>(`() => ({ a: 1 })`, [], { ownership: "root" });
|
|
103
|
-
assert.ok(kept instanceof RemoteObject);
|
|
104
|
-
assert.ok(kept.handle);
|
|
105
|
-
assert.deepEqual(kept.value, { a: 1 });
|
|
106
|
-
await disown(connector, page.contextId, [kept.handle!]);
|
|
107
|
-
await assert.doesNotReject(disown(connector, page.contextId, [kept.handle!]));
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("unserializable arguments fail before anything is sent", OPTIONS, async () => {
|
|
111
|
-
await assert.rejects(call(`(f) => f`, [() => 1]), TypeError);
|
|
112
|
-
});
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { FakeWebSocket, tick } from "../testing/fake-websocket.js";
|
|
4
|
-
import { BiDiConnector } from "../transport/bidi-connection.js";
|
|
5
|
-
import { callFunction, disown, ScriptError, ScriptSerializationError } from "./call-function.js";
|
|
6
|
-
import { ElementHandle } from "./element-handle.js";
|
|
7
|
-
import { RemoteObject } from "./serialize.js";
|
|
8
|
-
|
|
9
|
-
function setup(reply: (params: any) => object | Error) {
|
|
10
|
-
const ws = new FakeWebSocket();
|
|
11
|
-
const connector = new BiDiConnector(ws as unknown as WebSocket);
|
|
12
|
-
let answered = 0;
|
|
13
|
-
const pump = setInterval(() => {
|
|
14
|
-
while (answered < ws.sent.length) {
|
|
15
|
-
const { id, method, params } = ws.sent[answered++]!;
|
|
16
|
-
const result = method === "script.callFunction" || method === "script.disown" ? reply(params) : {};
|
|
17
|
-
if (result instanceof Error) ws.replyError(id, "no such handle", result.message);
|
|
18
|
-
else ws.reply(id, result);
|
|
19
|
-
}
|
|
20
|
-
}, 1);
|
|
21
|
-
return { ws, connector, stop: () => clearInterval(pump) };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const success = (result: object) => ({ type: "success", realm: "r", result });
|
|
25
|
-
|
|
26
|
-
describe("callFunction", () => {
|
|
27
|
-
it("sends the declaration, serialized arguments, target and defaults", async () => {
|
|
28
|
-
const { ws, connector, stop } = setup(() => success({ type: "number", value: 3 }));
|
|
29
|
-
const result = await callFunction(connector, "ctx", "(a, b) => a + b", [1, "x"]);
|
|
30
|
-
assert.equal(result, 3);
|
|
31
|
-
assert.deepEqual(ws.sent[0]!.params, {
|
|
32
|
-
functionDeclaration: "(a, b) => a + b",
|
|
33
|
-
awaitPromise: true,
|
|
34
|
-
target: { context: "ctx" },
|
|
35
|
-
arguments: [
|
|
36
|
-
{ type: "number", value: 1 },
|
|
37
|
-
{ type: "string", value: "x" },
|
|
38
|
-
],
|
|
39
|
-
resultOwnership: "none",
|
|
40
|
-
});
|
|
41
|
-
stop();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("omits arguments when there are none and passes options through", async () => {
|
|
45
|
-
const { ws, connector, stop } = setup(() => success({ type: "undefined" }));
|
|
46
|
-
await callFunction(connector, "ctx", "function() { return this; }", [], {
|
|
47
|
-
awaitPromise: false,
|
|
48
|
-
sandbox: "samurai",
|
|
49
|
-
ownership: "root",
|
|
50
|
-
thisArg: { a: 1 },
|
|
51
|
-
});
|
|
52
|
-
assert.deepEqual(ws.sent[0]!.params, {
|
|
53
|
-
functionDeclaration: "function() { return this; }",
|
|
54
|
-
awaitPromise: false,
|
|
55
|
-
target: { context: "ctx", sandbox: "samurai" },
|
|
56
|
-
resultOwnership: "root",
|
|
57
|
-
this: { type: "object", value: [["a", { type: "number", value: 1 }]] },
|
|
58
|
-
});
|
|
59
|
-
stop();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("serializes an ElementHandle argument as a shared reference", async () => {
|
|
63
|
-
const { ws, connector, stop } = setup(() => success({ type: "null" }));
|
|
64
|
-
await callFunction(connector, "ctx", "(el) => el.id", [new ElementHandle("n1")]);
|
|
65
|
-
assert.deepEqual((ws.sent[0]!.params as any).arguments, [{ sharedId: "n1" }]);
|
|
66
|
-
stop();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("accepts a function and sends its source", async () => {
|
|
70
|
-
const { ws, connector, stop } = setup(() => success({ type: "number", value: 2 }));
|
|
71
|
-
await callFunction(connector, "ctx", (n: number) => n * 2, [1]);
|
|
72
|
-
assert.match((ws.sent[0]!.params as any).functionDeclaration, /n\s*\*\s*2/);
|
|
73
|
-
stop();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("rejects a function whose source depends on the transpiler's __name helper", async () => {
|
|
77
|
-
const { connector, stop } = setup(() => success({ type: "null" }));
|
|
78
|
-
const named = () => {
|
|
79
|
-
function inner() {
|
|
80
|
-
return 1;
|
|
81
|
-
}
|
|
82
|
-
return inner();
|
|
83
|
-
};
|
|
84
|
-
// Under tsx (keepNames) `inner` is wrapped in __name(); the guard turns that into a clear error
|
|
85
|
-
if (named.toString().includes("__name(")) {
|
|
86
|
-
await assert.rejects(callFunction(connector, "ctx", named), ScriptSerializationError);
|
|
87
|
-
}
|
|
88
|
-
const guarded = "(x) => { __name(x, 'x'); }";
|
|
89
|
-
// A string is used as written, the guard is only for function objects
|
|
90
|
-
await assert.doesNotReject(callFunction(connector, "ctx", guarded));
|
|
91
|
-
const fake = Object.assign(() => 1, { toString: () => "() => { __name(a, 'a'); }" });
|
|
92
|
-
await assert.rejects(callFunction(connector, "ctx", fake), ScriptSerializationError);
|
|
93
|
-
stop();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("fails before sending when an argument cannot be serialized", async () => {
|
|
97
|
-
const { ws, connector, stop } = setup(() => success({ type: "null" }));
|
|
98
|
-
await assert.rejects(callFunction(connector, "ctx", "(f) => f", [() => 1]), TypeError);
|
|
99
|
-
assert.equal(ws.sent.length, 0);
|
|
100
|
-
stop();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("maps an exception result to ScriptError", async () => {
|
|
104
|
-
const { connector, stop } = setup(() => ({
|
|
105
|
-
type: "exception",
|
|
106
|
-
realm: "r",
|
|
107
|
-
exceptionDetails: {
|
|
108
|
-
text: "TypeError: x is null",
|
|
109
|
-
lineNumber: 1,
|
|
110
|
-
columnNumber: 2,
|
|
111
|
-
exception: { type: "error" },
|
|
112
|
-
stackTrace: { callFrames: [] },
|
|
113
|
-
},
|
|
114
|
-
}));
|
|
115
|
-
const longSource = "() => {" + " ".repeat(300) + "}";
|
|
116
|
-
await assert.rejects(callFunction(connector, "ctx", longSource), (err: unknown) => {
|
|
117
|
-
assert.ok(err instanceof ScriptError);
|
|
118
|
-
assert.equal(err.text, "TypeError: x is null");
|
|
119
|
-
assert.deepEqual(err.stackTrace, { callFrames: [] });
|
|
120
|
-
assert.ok(err.functionSource.length <= 201);
|
|
121
|
-
assert.match(err.message, /TypeError: x is null/);
|
|
122
|
-
return true;
|
|
123
|
-
});
|
|
124
|
-
stop();
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("returns nodes as ElementHandles", async () => {
|
|
128
|
-
const { connector, stop } = setup(() => success({ type: "node", sharedId: "n9", value: {} }));
|
|
129
|
-
const result = await callFunction<ElementHandle>(connector, "ctx", "() => document.body");
|
|
130
|
-
assert.ok(result instanceof ElementHandle);
|
|
131
|
-
assert.equal(result.sharedId, "n9");
|
|
132
|
-
stop();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("keeps handles for ownership root", async () => {
|
|
136
|
-
const { connector, stop } = setup(() =>
|
|
137
|
-
success({ type: "object", handle: "h1", value: [["a", { type: "number", value: 1 }]] }),
|
|
138
|
-
);
|
|
139
|
-
const result = await callFunction<RemoteObject>(connector, "ctx", "() => ({a: 1})", [], { ownership: "root" });
|
|
140
|
-
assert.ok(result instanceof RemoteObject);
|
|
141
|
-
assert.equal(result.handle, "h1");
|
|
142
|
-
assert.deepEqual(result.value, { a: 1 });
|
|
143
|
-
stop();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("does not keep handles for ownership none", async () => {
|
|
147
|
-
const { connector, stop } = setup(() => success({ type: "object", value: [["a", { type: "number", value: 1 }]] }));
|
|
148
|
-
assert.deepEqual(await callFunction(connector, "ctx", "() => ({a: 1})"), { a: 1 });
|
|
149
|
-
stop();
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe("disown", () => {
|
|
154
|
-
it("sends the handles for the context", async () => {
|
|
155
|
-
const { ws, connector, stop } = setup(() => ({}));
|
|
156
|
-
await disown(connector, "ctx", ["h1", "h2"]);
|
|
157
|
-
assert.deepEqual(ws.sent[0]!.params, { handles: ["h1", "h2"], target: { context: "ctx" } });
|
|
158
|
-
stop();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("sends nothing for no handles", async () => {
|
|
162
|
-
const { ws, connector, stop } = setup(() => ({}));
|
|
163
|
-
await disown(connector, "ctx", []);
|
|
164
|
-
await tick(5);
|
|
165
|
-
assert.equal(ws.sent.length, 0);
|
|
166
|
-
stop();
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("swallows no such handle", async () => {
|
|
170
|
-
const { connector, stop } = setup(() => new Error("gone"));
|
|
171
|
-
await assert.doesNotReject(disown(connector, "ctx", ["h1"]));
|
|
172
|
-
stop();
|
|
173
|
-
});
|
|
174
|
-
});
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
-
import { BiDiError } from "../transport/bidi-error.js";
|
|
3
|
-
import type { StackTrace } from "../types/bidi-modules/script.js";
|
|
4
|
-
import { fromRemoteValue, toLocalValue, type Arg } from "./serialize.js";
|
|
5
|
-
|
|
6
|
-
/** How much of a function's source `ScriptError` keeps */
|
|
7
|
-
const SOURCE_PREVIEW = 200;
|
|
8
|
-
|
|
9
|
-
export interface CallOptions {
|
|
10
|
-
/** @default true. The auto-wait probes pass false */
|
|
11
|
-
awaitPromise?: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* "root" keeps a handle alive in the browser; the result then comes back as a `RemoteObject`
|
|
14
|
-
* carrying it, and the caller must `disown` it. @default "none"
|
|
15
|
-
*/
|
|
16
|
-
ownership?: "none" | "root";
|
|
17
|
-
/** Named sandbox realm to run in (see the preload-scripts spec) */
|
|
18
|
-
sandbox?: string;
|
|
19
|
-
thisArg?: Arg;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** The page function threw */
|
|
23
|
-
export class ScriptError extends Error {
|
|
24
|
-
/** The browser's description, e.g. "TypeError: el is null" */
|
|
25
|
-
public readonly text: string;
|
|
26
|
-
public readonly stackTrace: StackTrace | undefined;
|
|
27
|
-
/** Start of the function that threw */
|
|
28
|
-
public readonly functionSource: string;
|
|
29
|
-
|
|
30
|
-
constructor(text: string, functionSource: string, stackTrace?: StackTrace) {
|
|
31
|
-
super(`Script threw: ${text}`);
|
|
32
|
-
this.name = "ScriptError";
|
|
33
|
-
this.text = text;
|
|
34
|
-
this.stackTrace = stackTrace;
|
|
35
|
-
this.functionSource =
|
|
36
|
-
functionSource.length > SOURCE_PREVIEW
|
|
37
|
-
? functionSource.slice(0, SOURCE_PREVIEW) + "…"
|
|
38
|
-
: functionSource;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** A function cannot be sent as source */
|
|
43
|
-
export class ScriptSerializationError extends Error {
|
|
44
|
-
constructor(message: string) {
|
|
45
|
-
super(message);
|
|
46
|
-
this.name = "ScriptSerializationError";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** The source to send for `fn`; a function object must not depend on transpiler helpers */
|
|
51
|
-
export function toFunctionDeclaration(fn: string | ((...args: any[]) => unknown)): string {
|
|
52
|
-
if (typeof fn === "string") return fn;
|
|
53
|
-
const source = fn.toString();
|
|
54
|
-
// tsx/esbuild wrap named functions in a __name() helper that does not exist in the page
|
|
55
|
-
if (source.includes("__name(")) {
|
|
56
|
-
throw new ScriptSerializationError(
|
|
57
|
-
"The function's source uses the transpiler's __name helper, which does not exist in the page. " +
|
|
58
|
-
"Use an arrow function without inner named functions, or pass the function as a string.",
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
return source;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Calls `fn` in the page with serialized arguments and returns its deserialized result */
|
|
65
|
-
export async function callFunction<T = unknown>(
|
|
66
|
-
connector: BiDiConnector,
|
|
67
|
-
context: string,
|
|
68
|
-
fn: string | ((...args: any[]) => unknown),
|
|
69
|
-
args: Arg[] = [],
|
|
70
|
-
options: CallOptions = {},
|
|
71
|
-
): Promise<T> {
|
|
72
|
-
const functionDeclaration = toFunctionDeclaration(fn);
|
|
73
|
-
const ownership = options.ownership ?? "none";
|
|
74
|
-
// Serialize first: a bad argument must fail before anything is sent
|
|
75
|
-
const serialized = args.map(toLocalValue);
|
|
76
|
-
const thisValue = "thisArg" in options ? toLocalValue(options.thisArg) : undefined;
|
|
77
|
-
|
|
78
|
-
const result = await connector.send("script.callFunction", {
|
|
79
|
-
functionDeclaration,
|
|
80
|
-
awaitPromise: options.awaitPromise ?? true,
|
|
81
|
-
target:
|
|
82
|
-
options.sandbox === undefined
|
|
83
|
-
? { context }
|
|
84
|
-
: { context, sandbox: options.sandbox },
|
|
85
|
-
...(serialized.length > 0 && { arguments: serialized }),
|
|
86
|
-
resultOwnership: ownership,
|
|
87
|
-
...(thisValue !== undefined && { this: thisValue }),
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
if (result.type === "exception") {
|
|
91
|
-
throw new ScriptError(
|
|
92
|
-
result.exceptionDetails.text,
|
|
93
|
-
functionDeclaration,
|
|
94
|
-
result.exceptionDetails.stackTrace,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
return fromRemoteValue(result.result, { keepHandles: ownership === "root" }) as T;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Releases handles obtained with `ownership: "root"`; a handle that is already gone is fine */
|
|
101
|
-
export async function disown(
|
|
102
|
-
connector: BiDiConnector,
|
|
103
|
-
context: string,
|
|
104
|
-
handles: string[],
|
|
105
|
-
): Promise<void> {
|
|
106
|
-
if (handles.length === 0) return;
|
|
107
|
-
try {
|
|
108
|
-
await connector.send("script.disown", { handles, target: { context } });
|
|
109
|
-
} catch (err) {
|
|
110
|
-
if (err instanceof BiDiError && err.code === "no such handle") return;
|
|
111
|
-
throw err;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { autoReply, FakeWebSocket, tick } from "../testing/fake-websocket.js";
|
|
4
|
-
import { BiDiConnector } from "../transport/bidi-connection.js";
|
|
5
|
-
import {
|
|
6
|
-
CALL_PROBE,
|
|
7
|
-
HELPER_SANDBOX,
|
|
8
|
-
HELPERS_MISSING,
|
|
9
|
-
HELPERS_SOURCE,
|
|
10
|
-
HelperRealm,
|
|
11
|
-
} from "./helpers.js";
|
|
12
|
-
|
|
13
|
-
const ok = { type: "success", realm: "r", result: { type: "undefined" } };
|
|
14
|
-
|
|
15
|
-
function setup(overrides: Record<string, (params: any) => object | Error> = {}) {
|
|
16
|
-
const ws = new FakeWebSocket();
|
|
17
|
-
const connector = new BiDiConnector(ws as unknown as WebSocket);
|
|
18
|
-
const stop = autoReply(ws, {
|
|
19
|
-
"script.addPreloadScript": () => ({ script: "helpers-1" }),
|
|
20
|
-
"browsingContext.getTree": () => ({ contexts: [{ context: "top", parent: null, children: [] }] }),
|
|
21
|
-
"script.callFunction": () => ok,
|
|
22
|
-
...overrides,
|
|
23
|
-
});
|
|
24
|
-
const count = (method: string) => ws.sent.filter((m) => m.method === method).length;
|
|
25
|
-
return { ws, connector, stop, count, realm: new HelperRealm(connector, "top") };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("helper source", () => {
|
|
29
|
-
it("compiles and installs probeElement on the global", () => {
|
|
30
|
-
const fakeGlobal: Record<string, any> = {};
|
|
31
|
-
new Function("globalThis", `(${HELPERS_SOURCE})()`)(fakeGlobal);
|
|
32
|
-
assert.equal(typeof fakeGlobal.__samurai.probeElement, "function");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("the call wrapper forwards to probeElement, or answers with the missing marker", () => {
|
|
36
|
-
const wrapper = (globalScope: object) =>
|
|
37
|
-
new Function("globalThis", `return (${CALL_PROBE})`)(globalScope) as (...args: unknown[]) => unknown;
|
|
38
|
-
const probeElement = (xpath: string, options: unknown) => ({ xpath, options });
|
|
39
|
-
assert.deepEqual(wrapper({ __samurai: { probeElement } })("//a", { scroll: true }), {
|
|
40
|
-
xpath: "//a",
|
|
41
|
-
options: { scroll: true },
|
|
42
|
-
});
|
|
43
|
-
assert.equal(wrapper({})("//a", {}), HELPERS_MISSING);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("uses the samurai sandbox", () => {
|
|
47
|
-
assert.equal(HELPER_SANDBOX, "samurai");
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("HelperRealm", () => {
|
|
52
|
-
it("registers once for the page context and runs in the sandbox", async () => {
|
|
53
|
-
const { ws, realm, stop } = setup();
|
|
54
|
-
await realm.ensureInstalled();
|
|
55
|
-
assert.deepEqual(ws.sent[0]!.params, {
|
|
56
|
-
functionDeclaration: HELPERS_SOURCE,
|
|
57
|
-
contexts: ["top"],
|
|
58
|
-
sandbox: "samurai",
|
|
59
|
-
});
|
|
60
|
-
stop();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("concurrent and repeated ensureInstalled share one registration", async () => {
|
|
64
|
-
const { realm, count, stop } = setup();
|
|
65
|
-
await Promise.all([realm.ensureInstalled(), realm.ensureInstalled()]);
|
|
66
|
-
await realm.ensureInstalled();
|
|
67
|
-
assert.equal(count("script.addPreloadScript"), 1);
|
|
68
|
-
stop();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("a failed install can be retried", async () => {
|
|
72
|
-
let attempts = 0;
|
|
73
|
-
const { realm, count, stop } = setup({
|
|
74
|
-
"script.addPreloadScript": () => (++attempts === 1 ? new Error("first fails") : { script: "helpers-1" }),
|
|
75
|
-
});
|
|
76
|
-
await assert.rejects(realm.ensureInstalled());
|
|
77
|
-
await realm.ensureInstalled();
|
|
78
|
-
assert.equal(count("script.addPreloadScript"), 2);
|
|
79
|
-
stop();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("reinstall runs the helpers in the current contexts without registering again", async () => {
|
|
83
|
-
const { ws, realm, count, stop } = setup();
|
|
84
|
-
await realm.ensureInstalled();
|
|
85
|
-
const before = count("script.addPreloadScript");
|
|
86
|
-
await realm.reinstall();
|
|
87
|
-
assert.equal(count("script.addPreloadScript"), before);
|
|
88
|
-
const last = ws.sent.filter((m) => m.method === "script.callFunction").pop()!.params as any;
|
|
89
|
-
assert.equal(last.functionDeclaration, HELPERS_SOURCE);
|
|
90
|
-
assert.deepEqual(last.target, { context: "top", sandbox: "samurai" });
|
|
91
|
-
stop();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("reinstall before the first install installs first", async () => {
|
|
95
|
-
const { realm, count, stop } = setup();
|
|
96
|
-
await realm.reinstall();
|
|
97
|
-
assert.equal(count("script.addPreloadScript"), 1);
|
|
98
|
-
stop();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("dispose removes the registration", async () => {
|
|
102
|
-
const { realm, count, stop } = setup({ "script.removePreloadScript": () => ({}) });
|
|
103
|
-
await realm.ensureInstalled();
|
|
104
|
-
await realm.dispose();
|
|
105
|
-
await tick(5);
|
|
106
|
-
assert.equal(count("script.removePreloadScript"), 1);
|
|
107
|
-
stop();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("dispose without an install sends nothing", async () => {
|
|
111
|
-
const { realm, count, stop } = setup();
|
|
112
|
-
await realm.dispose();
|
|
113
|
-
assert.equal(count("script.removePreloadScript"), 0);
|
|
114
|
-
stop();
|
|
115
|
-
});
|
|
116
|
-
});
|
package/src/script/helpers.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
-
import { PROBE_ELEMENT } from "../locator/element-state.js";
|
|
3
|
-
import { addPreload, runInLoadedContexts, type PreloadHandle } from "./preload.js";
|
|
4
|
-
|
|
5
|
-
/** Sandbox realm holding the framework's helpers: same DOM as the page, separate JS globals */
|
|
6
|
-
export const HELPER_SANDBOX = "samurai";
|
|
7
|
-
|
|
8
|
-
/** What `CALL_PROBE` answers with when the helpers are not installed in the document */
|
|
9
|
-
export const HELPERS_MISSING = "samurai:helpers-missing";
|
|
10
|
-
|
|
11
|
-
/** Installs `globalThis.__samurai` in the sandbox realm of a document */
|
|
12
|
-
export const HELPERS_SOURCE = `() => {
|
|
13
|
-
globalThis.__samurai = { probeElement: ${PROBE_ELEMENT} };
|
|
14
|
-
}`;
|
|
15
|
-
|
|
16
|
-
/** Calls the installed probe, or reports that the helpers are missing instead of throwing */
|
|
17
|
-
export const CALL_PROBE = `(...args) => {
|
|
18
|
-
const helpers = globalThis.__samurai;
|
|
19
|
-
return helpers ? helpers.probeElement(...args) : ${JSON.stringify(HELPERS_MISSING)};
|
|
20
|
-
}`;
|
|
21
|
-
|
|
22
|
-
/** What the locator needs from the helper realm */
|
|
23
|
-
export interface HelperInstaller {
|
|
24
|
-
/** Resolves once the helpers are registered for the page; installs on first use */
|
|
25
|
-
ensureInstalled(): Promise<void>;
|
|
26
|
-
/** Runs the helpers again in the loaded documents; used when a call found them missing */
|
|
27
|
-
reinstall(): Promise<void>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** The framework's helper library for one page: registered once, applied to every document */
|
|
31
|
-
export class HelperRealm implements HelperInstaller {
|
|
32
|
-
private installed: Promise<PreloadHandle> | undefined;
|
|
33
|
-
|
|
34
|
-
constructor(
|
|
35
|
-
private connector: BiDiConnector,
|
|
36
|
-
private contextId: string,
|
|
37
|
-
) {}
|
|
38
|
-
|
|
39
|
-
public ensureInstalled(): Promise<void> {
|
|
40
|
-
this.installed ??= addPreload(this.connector, this.options()).catch((err) => {
|
|
41
|
-
this.installed = undefined;
|
|
42
|
-
throw err;
|
|
43
|
-
});
|
|
44
|
-
return this.installed.then(() => undefined);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
public async reinstall(): Promise<void> {
|
|
48
|
-
if (!this.installed) return this.ensureInstalled();
|
|
49
|
-
await this.installed;
|
|
50
|
-
await runInLoadedContexts(this.connector, this.options());
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Removes the registration; nothing is sent when the helpers were never installed */
|
|
54
|
-
public async dispose(): Promise<void> {
|
|
55
|
-
const installed = this.installed;
|
|
56
|
-
this.installed = undefined;
|
|
57
|
-
if (!installed) return;
|
|
58
|
-
await (await installed).dispose();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private options() {
|
|
62
|
-
return {
|
|
63
|
-
source: HELPERS_SOURCE,
|
|
64
|
-
contexts: [this.contextId],
|
|
65
|
-
sandbox: HELPER_SANDBOX,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}
|