@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
package/src/browser/browser.ts
DELETED
|
@@ -1,515 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
|
-
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import type { SupportedBrowser } from "../types/browser.js";
|
|
6
|
-
import { findBrowser } from "./browser-finder.js";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import { BiDiConnector } from "../transport/bidi-connection.js";
|
|
9
|
-
import Page from "./page.js";
|
|
10
|
-
import { BrowserContext, type ContextHost } from "./browser-context.js";
|
|
11
|
-
import type { UserPromptHandler } from "../types/bidi-modules/session.js";
|
|
12
|
-
import { validateEmulation, type EmulationOptions } from "./emulation.js";
|
|
13
|
-
import { prepareDownloadsDir, removeIfEmpty } from "./downloads-dir.js";
|
|
14
|
-
import type { BiDiCommands } from "../types/bidi.js";
|
|
15
|
-
import logger from "../logger/index.js";
|
|
16
|
-
import { readConfig } from "../config/config.js";
|
|
17
|
-
import type {
|
|
18
|
-
CookieFilter,
|
|
19
|
-
PartialCookie,
|
|
20
|
-
PartitionDescriptor,
|
|
21
|
-
} from "../types/bidi-modules/storage.js";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Sent with session.new. With "ignore" the browser leaves a JavaScript dialog open and reports it
|
|
25
|
-
* (`browsingContext.userPromptOpened`), so the framework decides how to answer instead of the browser.
|
|
26
|
-
*/
|
|
27
|
-
export const SESSION_CAPABILITIES = {
|
|
28
|
-
alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } },
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** Time (ms) to wait for Firefox to answer browser.close before killing the process */
|
|
32
|
-
const BROWSER_CLOSE_GRACE = 2000;
|
|
33
|
-
|
|
34
|
-
const browserProfilePath: Record<SupportedBrowser, string> = {
|
|
35
|
-
// chrome: path.resolve("browsers/profiles/chrome"),
|
|
36
|
-
firefox: path.resolve("browsers/profiles/firefox/user.js"),
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const baseBrowserLaunchFlags: Record<SupportedBrowser, string[]> = {
|
|
40
|
-
// chrome: [],
|
|
41
|
-
firefox: ["--no-sandbox", "--no-remote"],
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const defaultLaunchOptions: Record<SupportedBrowser, BrowserLaunchOptions> = {
|
|
45
|
-
firefox: {
|
|
46
|
-
port: 9222,
|
|
47
|
-
profileDir: path.dirname(browserProfilePath.firefox),
|
|
48
|
-
headless: true,
|
|
49
|
-
},
|
|
50
|
-
// chrome: {
|
|
51
|
-
// port: 9222,
|
|
52
|
-
// profileDir: path.dirname(browserProfilePath.chrome),
|
|
53
|
-
// headless: true,
|
|
54
|
-
// },
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const regexPrefix: Record<SupportedBrowser, RegExp> = {
|
|
58
|
-
firefox: /WebDriver\sBiDi\slistening\son\s/,
|
|
59
|
-
// chrome: /DevTools\slistening\son/,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const browserLaunchFlag = (
|
|
63
|
-
browserName: SupportedBrowser,
|
|
64
|
-
launchOptions: Partial<BrowserLaunchOptions>,
|
|
65
|
-
) => {
|
|
66
|
-
const defLaunchOpts = defaultLaunchOptions[browserName];
|
|
67
|
-
const port = launchOptions.port || defLaunchOpts.port;
|
|
68
|
-
const profileDir = launchOptions.profileDir || defLaunchOpts.profileDir;
|
|
69
|
-
const headless = launchOptions.headless ?? defLaunchOpts.headless;
|
|
70
|
-
|
|
71
|
-
const baseLaunchFlags = [...baseBrowserLaunchFlags[browserName]];
|
|
72
|
-
|
|
73
|
-
if (headless) {
|
|
74
|
-
baseLaunchFlags.push("--headless");
|
|
75
|
-
}
|
|
76
|
-
baseLaunchFlags.push(`--remote-debugging-port=${port}`);
|
|
77
|
-
|
|
78
|
-
switch (browserName) {
|
|
79
|
-
// case "chrome":
|
|
80
|
-
// baseLaunchFlags.push(`--user-data-dir=${profileDir}`);
|
|
81
|
-
// break;
|
|
82
|
-
case "firefox":
|
|
83
|
-
baseLaunchFlags.push("--profile", profileDir);
|
|
84
|
-
// The home page (about:home) is privileged: BiDi refuses commands such as setViewport on it
|
|
85
|
-
baseLaunchFlags.push("about:blank");
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
logger.verbose("Launch flags for browser %s: ", browserName, {
|
|
89
|
-
baseLaunchFlags,
|
|
90
|
-
});
|
|
91
|
-
return baseLaunchFlags;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Stops what a failed launch started. A browser left running holds the profile (the next launch
|
|
96
|
-
* fails with "already open") and keeps the test process alive.
|
|
97
|
-
*/
|
|
98
|
-
export function abandonLaunch(
|
|
99
|
-
browserProc: { kill(): boolean },
|
|
100
|
-
biDiConnector: { kill(): void } | undefined,
|
|
101
|
-
): void {
|
|
102
|
-
biDiConnector?.kill();
|
|
103
|
-
browserProc.kill();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// On macOS a spawned browser is attributed to the terminal, which is denied access to
|
|
107
|
-
// ~/Library/Application Support/Firefox (profiles.ini). Point Firefox at a local app data dir instead.
|
|
108
|
-
const browserAppDataPath: Record<SupportedBrowser, string> = {
|
|
109
|
-
firefox: path.resolve("browsers/app-data/firefox"),
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const browserLaunchEnv = (browserName: SupportedBrowser) => {
|
|
113
|
-
const appDataPath = browserAppDataPath[browserName];
|
|
114
|
-
mkdirSync(appDataPath, { recursive: true });
|
|
115
|
-
logger.debug("Using app data dir for %s: %s", browserName, appDataPath);
|
|
116
|
-
|
|
117
|
-
switch (browserName) {
|
|
118
|
-
case "firefox":
|
|
119
|
-
return { MOZ_APP_DATA: appDataPath };
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const browserProfiles: Record<SupportedBrowser, string> = {
|
|
124
|
-
// chrome: "",
|
|
125
|
-
firefox: `
|
|
126
|
-
user_pref("devtools.debugger.remote-enabled", true);
|
|
127
|
-
user_pref("devtools.debugger.prompt-connection", false);
|
|
128
|
-
user_pref("devtools.chrome.enabled", true);
|
|
129
|
-
// The home page (about:home) is privileged: BiDi refuses commands such as setViewport on it
|
|
130
|
-
user_pref("browser.startup.page", 0);
|
|
131
|
-
user_pref("browser.startup.homepage", "about:blank");
|
|
132
|
-
user_pref("browser.newtabpage.enabled", false);
|
|
133
|
-
`,
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// Firefox logs "WebDriver BiDi listening on ws://<host>:<port>" without the endpoint path
|
|
137
|
-
const browserWsPath: Record<SupportedBrowser, string> = {
|
|
138
|
-
firefox: "/session",
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const browserWsRegex: Record<SupportedBrowser, RegExp> = {
|
|
142
|
-
firefox: /ws:\/\/(\d){1,3}.(\d){1,3}.(\d){1,3}.(\d){1,3}:(\d){4}/g,
|
|
143
|
-
// chrome:
|
|
144
|
-
// /ws:\/\/(\d){1,3}.(\d){1,3}.(\d){1,3}.(\d){1,3}:(\d){4}\/devtools\/browser\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g,
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
export interface BrowserLaunchOptions {
|
|
148
|
-
/** Which port to use for the WebDeveloper tools */
|
|
149
|
-
port: number;
|
|
150
|
-
/** Firefox only */
|
|
151
|
-
profileDir: string;
|
|
152
|
-
/** Start the browser in headless mode or in UI mode */
|
|
153
|
-
headless: boolean;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Options of `browser.newContext()` */
|
|
157
|
-
export interface ContextOptions extends EmulationOptions {
|
|
158
|
-
acceptInsecureCerts?: boolean;
|
|
159
|
-
/** How this context's pages answer JavaScript dialogs; the session default is to leave them for the framework */
|
|
160
|
-
unhandledPromptBehavior?: UserPromptHandler;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export class Browser {
|
|
164
|
-
private browserProc: ChildProcessWithoutNullStreams;
|
|
165
|
-
private biDiConnector: BiDiConnector;
|
|
166
|
-
private userContexts = new Map<string, BrowserContext>();
|
|
167
|
-
private host: ContextHost;
|
|
168
|
-
/** The browser's own context, the one `Browser.launch`'s page lives in */
|
|
169
|
-
public readonly defaultContext: BrowserContext;
|
|
170
|
-
/** Where downloads are saved; undefined when the browser could not be told (it then uses its own folder) */
|
|
171
|
-
public readonly downloadsDir: string | undefined;
|
|
172
|
-
|
|
173
|
-
constructor({
|
|
174
|
-
browserProc,
|
|
175
|
-
biDiConnector,
|
|
176
|
-
downloadsDir,
|
|
177
|
-
}: {
|
|
178
|
-
browserProc: ChildProcessWithoutNullStreams;
|
|
179
|
-
biDiConnector: BiDiConnector;
|
|
180
|
-
downloadsDir?: string | undefined;
|
|
181
|
-
}) {
|
|
182
|
-
this.browserProc = browserProc;
|
|
183
|
-
this.biDiConnector = biDiConnector;
|
|
184
|
-
this.downloadsDir = downloadsDir;
|
|
185
|
-
this.host = {
|
|
186
|
-
connector: biDiConnector,
|
|
187
|
-
newPage: (options) => this.newPage(options),
|
|
188
|
-
forget: (context) => void this.userContexts.delete(context.id),
|
|
189
|
-
};
|
|
190
|
-
this.defaultContext = new BrowserContext(this.host, "default", true);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
static async launch(
|
|
194
|
-
browserName: SupportedBrowser,
|
|
195
|
-
launchOptions: Partial<BrowserLaunchOptions> = defaultLaunchOptions[
|
|
196
|
-
browserName
|
|
197
|
-
],
|
|
198
|
-
/** Aborting kills the browser process and rejects the launch */
|
|
199
|
-
signal?: AbortSignal,
|
|
200
|
-
) {
|
|
201
|
-
signal?.throwIfAborted();
|
|
202
|
-
logger.verbose("Trying to launch browser %s", browserName);
|
|
203
|
-
const browserLocation = findBrowser(browserName);
|
|
204
|
-
logger.debug("Found browser install location at: %s", browserLocation);
|
|
205
|
-
ensureBrowserProfile(browserName);
|
|
206
|
-
|
|
207
|
-
logger.verbose("Trying to spawn %s process", browserName);
|
|
208
|
-
|
|
209
|
-
const browserProc = spawn(
|
|
210
|
-
browserLocation,
|
|
211
|
-
browserLaunchFlag(browserName, launchOptions),
|
|
212
|
-
{ env: { ...process.env, ...browserLaunchEnv(browserName) } },
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
return new Promise<{ browser: Browser; page: Page }>((resolve, reject) => {
|
|
216
|
-
const onUnexpectedClose = (code: number | null) => {
|
|
217
|
-
reject(
|
|
218
|
-
new Error(`Browser process exited unexpectedly with code ${code}`),
|
|
219
|
-
);
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
browserProc.on("close", onUnexpectedClose);
|
|
223
|
-
|
|
224
|
-
const onAbort = () => {
|
|
225
|
-
logger.verbose("Launch of %s aborted, killing process", browserName);
|
|
226
|
-
browserProc.kill();
|
|
227
|
-
reject(signal?.reason);
|
|
228
|
-
};
|
|
229
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
230
|
-
|
|
231
|
-
browserProc.stdout.on("data", (data: any) => {
|
|
232
|
-
logger.debug(`${browserName} stdout: ${data}`);
|
|
233
|
-
if (
|
|
234
|
-
String(data).match(
|
|
235
|
-
/!!!\scould\snot\sstart\sserver\son\sport\s(\d){4}/g,
|
|
236
|
-
)
|
|
237
|
-
) {
|
|
238
|
-
browserProc.kill();
|
|
239
|
-
reject(`PORT ${launchOptions.port} is already in use!`);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
browserProc.stderr.on("data", async (data: any) => {
|
|
244
|
-
logger.error(`${browserName} stderr: ${data}`);
|
|
245
|
-
|
|
246
|
-
const urlMatch = String(data).match(browserWsRegex[browserName]);
|
|
247
|
-
console.log({ urlMatch });
|
|
248
|
-
if (!Array.isArray(urlMatch) || urlMatch.length === 0) {
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
// Killing the process mid-handshake rejects pending BiDi calls; surface that via reject
|
|
252
|
-
// instead of an unhandled rejection from this async listener
|
|
253
|
-
let biDiConnector: BiDiConnector | undefined;
|
|
254
|
-
try {
|
|
255
|
-
let url = urlMatch[0] + browserWsPath[browserName];
|
|
256
|
-
logger.verbose("Websocket URL match: %s", url);
|
|
257
|
-
biDiConnector = await BiDiConnector.connect(url, {
|
|
258
|
-
commandTimeout: await readCommandTimeout(),
|
|
259
|
-
});
|
|
260
|
-
logger.verbose("Starting new session");
|
|
261
|
-
const status = await biDiConnector.send("session.status", {});
|
|
262
|
-
logger.verbose("Session status", status);
|
|
263
|
-
await biDiConnector.send("session.new", {
|
|
264
|
-
capabilities: SESSION_CAPABILITIES,
|
|
265
|
-
});
|
|
266
|
-
logger.verbose("Session started successfully");
|
|
267
|
-
|
|
268
|
-
const downloadsDir = await prepareDownloadsDir(
|
|
269
|
-
biDiConnector,
|
|
270
|
-
(await readOptionalConfig("downloadsDir")) ?? "result/downloads",
|
|
271
|
-
);
|
|
272
|
-
const browser = new Browser({
|
|
273
|
-
browserProc,
|
|
274
|
-
biDiConnector,
|
|
275
|
-
downloadsDir,
|
|
276
|
-
});
|
|
277
|
-
logger.verbose("Requesting current browser tree");
|
|
278
|
-
const browserTree = await biDiConnector.send(
|
|
279
|
-
"browsingContext.getTree",
|
|
280
|
-
{},
|
|
281
|
-
);
|
|
282
|
-
logger.verbose("Received current browser tree");
|
|
283
|
-
logger.debug("Browser tree: ", { browserTree });
|
|
284
|
-
|
|
285
|
-
const pageContext = browserTree.contexts[0];
|
|
286
|
-
|
|
287
|
-
let page = undefined;
|
|
288
|
-
|
|
289
|
-
if (pageContext) {
|
|
290
|
-
page = new Page(biDiConnector, pageContext.context);
|
|
291
|
-
browser.defaultContext.register(page);
|
|
292
|
-
} else {
|
|
293
|
-
page = await browser.newPage({ type: "tab" });
|
|
294
|
-
}
|
|
295
|
-
await page.applyDefaultViewport();
|
|
296
|
-
// Requests made before tracking starts are invisible to it, and a dialog nobody watches would hang the page
|
|
297
|
-
await page.startNetworkTracking();
|
|
298
|
-
await page.startDialogHandling();
|
|
299
|
-
await page.startLogging();
|
|
300
|
-
await page.startDownloadTracking();
|
|
301
|
-
await page.startFileChooserTracking();
|
|
302
|
-
browserProc.off("close", onUnexpectedClose);
|
|
303
|
-
signal?.removeEventListener("abort", onAbort);
|
|
304
|
-
resolve({
|
|
305
|
-
browser,
|
|
306
|
-
page,
|
|
307
|
-
});
|
|
308
|
-
} catch (err) {
|
|
309
|
-
browserProc.off("close", onUnexpectedClose);
|
|
310
|
-
signal?.removeEventListener("abort", onAbort);
|
|
311
|
-
abandonLaunch(browserProc, biDiConnector);
|
|
312
|
-
reject(err);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** Closes the browser via BiDi browser.close, then makes sure the process has exited */
|
|
319
|
-
public async close() {
|
|
320
|
-
if (this.browserProc.exitCode !== null || this.browserProc.signalCode) {
|
|
321
|
-
this.biDiConnector.kill();
|
|
322
|
-
removeIfEmpty(this.downloadsDir);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
const exited = new Promise((resolve) =>
|
|
326
|
-
this.browserProc.once("exit", resolve),
|
|
327
|
-
);
|
|
328
|
-
// User contexts outlive the browser in its profile unless they are removed
|
|
329
|
-
await Promise.allSettled(
|
|
330
|
-
[...this.userContexts.values()].map((context) => context.close()),
|
|
331
|
-
);
|
|
332
|
-
try {
|
|
333
|
-
// Firefox may exit without answering; a lost reply must not hold up teardown
|
|
334
|
-
await Promise.race([
|
|
335
|
-
this.biDiConnector.send("browser.close", {}),
|
|
336
|
-
sleep(BROWSER_CLOSE_GRACE, undefined, { ref: false }),
|
|
337
|
-
]);
|
|
338
|
-
} catch {
|
|
339
|
-
// browser may already be shutting down
|
|
340
|
-
}
|
|
341
|
-
this.biDiConnector.kill();
|
|
342
|
-
this.browserProc.kill();
|
|
343
|
-
await exited;
|
|
344
|
-
// A folder nothing was downloaded into would only pile up
|
|
345
|
-
removeIfEmpty(this.downloadsDir);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/** Alias of close() */
|
|
349
|
-
public async kill() {
|
|
350
|
-
await this.close();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
public async newPage({
|
|
354
|
-
type,
|
|
355
|
-
background,
|
|
356
|
-
userContext,
|
|
357
|
-
}: BiDiCommands["browsingContext.create"]["params"]): Promise<Page> {
|
|
358
|
-
const createBrowserContext = await this.biDiConnector.send(
|
|
359
|
-
"browsingContext.create",
|
|
360
|
-
{
|
|
361
|
-
type,
|
|
362
|
-
background,
|
|
363
|
-
userContext,
|
|
364
|
-
},
|
|
365
|
-
);
|
|
366
|
-
logger.debug("Created browsing context", { createBrowserContext });
|
|
367
|
-
const page = new Page(this.biDiConnector, createBrowserContext.context);
|
|
368
|
-
this.contextFor(userContext).register(page);
|
|
369
|
-
await page.applyDefaultViewport();
|
|
370
|
-
await page.startNetworkTracking();
|
|
371
|
-
await page.startDialogHandling();
|
|
372
|
-
await page.startLogging();
|
|
373
|
-
await page.startDownloadTracking();
|
|
374
|
-
await page.startFileChooserTracking();
|
|
375
|
-
return page;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Creates an isolated context: its own cookies, storage and cache. `browser.close()` removes the contexts
|
|
380
|
-
* that are still open, as Firefox would otherwise keep them in its profile.
|
|
381
|
-
* @example
|
|
382
|
-
* const alice = await browser.newContext();
|
|
383
|
-
* const page = await alice.newPage();
|
|
384
|
-
*/
|
|
385
|
-
public async newContext(
|
|
386
|
-
options: ContextOptions = {},
|
|
387
|
-
): Promise<BrowserContext> {
|
|
388
|
-
const { acceptInsecureCerts, unhandledPromptBehavior, ...emulation } =
|
|
389
|
-
options;
|
|
390
|
-
// A wrong value must not leave a context behind, so it is refused before one is created
|
|
391
|
-
validateEmulation(emulation);
|
|
392
|
-
const { userContext } = await this.biDiConnector.send(
|
|
393
|
-
"browser.createUserContext",
|
|
394
|
-
{
|
|
395
|
-
...(acceptInsecureCerts !== undefined && { acceptInsecureCerts }),
|
|
396
|
-
...(unhandledPromptBehavior && { unhandledPromptBehavior }),
|
|
397
|
-
},
|
|
398
|
-
);
|
|
399
|
-
const context = new BrowserContext(this.host, userContext, false);
|
|
400
|
-
this.userContexts.set(userContext, context);
|
|
401
|
-
if (Object.keys(emulation).length > 0) {
|
|
402
|
-
try {
|
|
403
|
-
await context.emulate(emulation);
|
|
404
|
-
} catch (err) {
|
|
405
|
-
// Firefox keeps a context in its profile until it is removed
|
|
406
|
-
await context
|
|
407
|
-
.close()
|
|
408
|
-
.catch((closeErr) =>
|
|
409
|
-
logger.debug("Could not remove a context whose emulation failed", {
|
|
410
|
-
closeErr,
|
|
411
|
-
}),
|
|
412
|
-
);
|
|
413
|
-
throw err;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return context;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/** The default context, then the contexts this browser created that are still open */
|
|
420
|
-
public contexts(): BrowserContext[] {
|
|
421
|
-
return [this.defaultContext, ...this.userContexts.values()];
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
private contextFor(userContext: string | undefined): BrowserContext {
|
|
425
|
-
if (userContext === undefined || userContext === "default")
|
|
426
|
-
return this.defaultContext;
|
|
427
|
-
return this.userContexts.get(userContext) ?? this.adopt(userContext);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/** A user context this browser did not create (Firefox keeps them in its profile): usable, never closed by `close()` */
|
|
431
|
-
private adopt(userContext: string): BrowserContext {
|
|
432
|
-
const context = new BrowserContext(this.host, userContext, false);
|
|
433
|
-
this.userContexts.set(userContext, context);
|
|
434
|
-
return context;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
public async getCookie(filter: CookieFilter, partition: PartitionDescriptor) {
|
|
438
|
-
return await this.biDiConnector.send("storage.getCookies", {
|
|
439
|
-
filter,
|
|
440
|
-
partition,
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
public async setCookie(
|
|
445
|
-
cookie: PartialCookie,
|
|
446
|
-
partition?: PartitionDescriptor,
|
|
447
|
-
) {
|
|
448
|
-
return await this.biDiConnector.send("storage.setCookie", {
|
|
449
|
-
partition,
|
|
450
|
-
cookie,
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/** A config value, or undefined when it is not set or the config cannot be read */
|
|
456
|
-
async function readOptionalConfig<K extends "downloadsDir">(key: K) {
|
|
457
|
-
try {
|
|
458
|
-
return await readConfig(key);
|
|
459
|
-
} catch (err) {
|
|
460
|
-
logger.debug("Could not read %s from config, using the default", key, {
|
|
461
|
-
err,
|
|
462
|
-
});
|
|
463
|
-
return undefined;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/** The configured BiDi command timeout; a missing or unreadable config falls back to the connector default */
|
|
468
|
-
async function readCommandTimeout() {
|
|
469
|
-
try {
|
|
470
|
-
return (await readConfig("bidi"))?.commandTimeout;
|
|
471
|
-
} catch (err) {
|
|
472
|
-
logger.debug("Could not read bidi config, using defaults", { err });
|
|
473
|
-
return undefined;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/** Writes the browser's user.js unless it already holds the current prefs; an outdated one is replaced */
|
|
478
|
-
export function ensureBrowserProfile(
|
|
479
|
-
browserName: SupportedBrowser,
|
|
480
|
-
profilePath: string | undefined = browserProfilePath[browserName],
|
|
481
|
-
) {
|
|
482
|
-
logger.verbose("Checking browser profile for: %s", browserName);
|
|
483
|
-
if (!profilePath) {
|
|
484
|
-
logger.verbose("No browser profile path declared, skipping...");
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
if (existsSync(profilePath)) {
|
|
488
|
-
if (readFileSync(profilePath, "utf8") === browserProfiles[browserName]) {
|
|
489
|
-
logger.verbose("Browser profile is up to date at: %s", profilePath);
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
logger.verbose("Browser profile at %s is outdated, rewriting it", profilePath);
|
|
493
|
-
} else {
|
|
494
|
-
logger.verbose("No browser profile created yet for %s", browserName);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
createBrowserProfile(browserName, profilePath);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function createBrowserProfile(
|
|
501
|
-
browserName: SupportedBrowser,
|
|
502
|
-
profilePath: string,
|
|
503
|
-
) {
|
|
504
|
-
logger.verbose(
|
|
505
|
-
"Creating browser profile for %s at %s",
|
|
506
|
-
browserName,
|
|
507
|
-
profilePath,
|
|
508
|
-
);
|
|
509
|
-
mkdirSync(path.dirname(profilePath), { recursive: true });
|
|
510
|
-
|
|
511
|
-
writeFileSync(profilePath, browserProfiles[browserName], {
|
|
512
|
-
encoding: "utf8",
|
|
513
|
-
});
|
|
514
|
-
logger.verbose("Browser profile created");
|
|
515
|
-
}
|
|
@@ -1,86 +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 { ContextTree } from "./context-tree.js";
|
|
6
|
-
|
|
7
|
-
/** Answers subscribe/unsubscribe and getTree commands as they come in */
|
|
8
|
-
function setup(tree: object[] = [{ context: "root", parent: null, children: [{ context: "frame", parent: "root", children: [] }] }]) {
|
|
9
|
-
const ws = new FakeWebSocket();
|
|
10
|
-
const connector = new BiDiConnector(ws as unknown as WebSocket);
|
|
11
|
-
let answered = 0;
|
|
12
|
-
const pump = setInterval(() => {
|
|
13
|
-
while (answered < ws.sent.length) {
|
|
14
|
-
const message = ws.sent[answered++]!;
|
|
15
|
-
if (message.method === "session.subscribe") ws.reply(message.id, { subscription: `s${answered}` });
|
|
16
|
-
else if (message.method === "browsingContext.getTree") ws.reply(message.id, { contexts: tree });
|
|
17
|
-
else ws.reply(message.id);
|
|
18
|
-
}
|
|
19
|
-
}, 1).unref();
|
|
20
|
-
return { ws, connector, stop: () => clearInterval(pump) };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe("ContextTree", () => {
|
|
24
|
-
it("reads the initial tree, including nested frames", async () => {
|
|
25
|
-
const { connector, stop } = setup();
|
|
26
|
-
const tree = await ContextTree.create(connector);
|
|
27
|
-
assert.equal(tree.parentOf("frame"), "root");
|
|
28
|
-
assert.equal(tree.parentOf("root"), null);
|
|
29
|
-
assert.equal(tree.rootOf("frame"), "root");
|
|
30
|
-
assert.equal(tree.isWithin("frame", "root"), true);
|
|
31
|
-
assert.equal(tree.isWithin("root", "root"), true);
|
|
32
|
-
await tree.dispose();
|
|
33
|
-
stop();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("unknown contexts are not within anything", async () => {
|
|
37
|
-
const { connector, stop } = setup();
|
|
38
|
-
const tree = await ContextTree.create(connector);
|
|
39
|
-
assert.equal(tree.isWithin("nope", "root"), false);
|
|
40
|
-
assert.equal(tree.parentOf("nope"), null);
|
|
41
|
-
assert.equal(tree.rootOf("nope"), "nope");
|
|
42
|
-
await tree.dispose();
|
|
43
|
-
stop();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("tracks created contexts, also deeper than one level", async () => {
|
|
47
|
-
const { ws, connector, stop } = setup();
|
|
48
|
-
const tree = await ContextTree.create(connector);
|
|
49
|
-
ws.emitEvent("browsingContext.contextCreated", { context: "grandchild", parent: "frame", children: [] });
|
|
50
|
-
ws.emitEvent("browsingContext.contextCreated", { context: "other", parent: null, children: [] });
|
|
51
|
-
assert.equal(tree.rootOf("grandchild"), "root");
|
|
52
|
-
assert.equal(tree.isWithin("grandchild", "root"), true);
|
|
53
|
-
assert.equal(tree.isWithin("other", "root"), false);
|
|
54
|
-
await tree.dispose();
|
|
55
|
-
stop();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("still answers for destroyed contexts (late events)", async () => {
|
|
59
|
-
const { ws, connector, stop } = setup();
|
|
60
|
-
const tree = await ContextTree.create(connector);
|
|
61
|
-
ws.emitEvent("browsingContext.contextDestroyed", { context: "frame", parent: "root", children: [] });
|
|
62
|
-
assert.equal(tree.isWithin("frame", "root"), true);
|
|
63
|
-
await tree.dispose();
|
|
64
|
-
stop();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("dispose removes listeners and unsubscribes", async () => {
|
|
68
|
-
const { ws, connector, stop } = setup();
|
|
69
|
-
const tree = await ContextTree.create(connector);
|
|
70
|
-
await tree.dispose();
|
|
71
|
-
await tick(5);
|
|
72
|
-
ws.emitEvent("browsingContext.contextCreated", { context: "late", parent: "root", children: [] });
|
|
73
|
-
assert.equal(tree.parentOf("late"), null);
|
|
74
|
-
assert.equal(ws.sent.filter((m) => m.method === "session.unsubscribe").length, 2);
|
|
75
|
-
stop();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("subscribes before reading the tree so no creation is missed", async () => {
|
|
79
|
-
const { ws, connector, stop } = setup();
|
|
80
|
-
const tree = await ContextTree.create(connector);
|
|
81
|
-
const order = ws.sent.map((m) => m.method).filter((m) => m !== "session.unsubscribe");
|
|
82
|
-
assert.deepEqual(order, ["session.subscribe", "session.subscribe", "browsingContext.getTree"]);
|
|
83
|
-
await tree.dispose();
|
|
84
|
-
stop();
|
|
85
|
-
});
|
|
86
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
-
import type { Subscription } from "../transport/subscriptions.js";
|
|
3
|
-
import type { BrowsingContext, Info } from "../types/bidi-modules/browsing-context.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Tracks which browsing contexts (iframes) belong to which top-level context, so events that
|
|
7
|
-
* carry a child context id can be attributed to the page that owns it.
|
|
8
|
-
* Destroyed contexts stay known: late events for them must still resolve to their page.
|
|
9
|
-
*/
|
|
10
|
-
export class ContextTree {
|
|
11
|
-
private parents = new Map<BrowsingContext, BrowsingContext | null>();
|
|
12
|
-
|
|
13
|
-
private constructor(
|
|
14
|
-
private connector: BiDiConnector,
|
|
15
|
-
private subscription: Subscription,
|
|
16
|
-
) {}
|
|
17
|
-
|
|
18
|
-
public static async create(connector: BiDiConnector): Promise<ContextTree> {
|
|
19
|
-
// Subscribe first: a context created between getTree and subscribing would be missed
|
|
20
|
-
const subscription = await connector.subscribe([
|
|
21
|
-
"browsingContext.contextCreated",
|
|
22
|
-
"browsingContext.contextDestroyed",
|
|
23
|
-
]);
|
|
24
|
-
const tree = new ContextTree(connector, subscription);
|
|
25
|
-
connector.onEvent("browsingContext.contextCreated", tree.onContext);
|
|
26
|
-
connector.onEvent("browsingContext.contextDestroyed", tree.onContext);
|
|
27
|
-
try {
|
|
28
|
-
const { contexts } = await connector.send("browsingContext.getTree", {});
|
|
29
|
-
contexts.forEach((info) => tree.add(info));
|
|
30
|
-
} catch (err) {
|
|
31
|
-
await tree.dispose();
|
|
32
|
-
throw err;
|
|
33
|
-
}
|
|
34
|
-
return tree;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private onContext = (info: Info) => this.add(info);
|
|
38
|
-
|
|
39
|
-
private add(info: Info) {
|
|
40
|
-
this.parents.set(info.context, info.parent ?? null);
|
|
41
|
-
info.children?.forEach((child) => this.add(child));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The direct parent, or null for a top-level or unknown context */
|
|
45
|
-
public parentOf(context: BrowsingContext): BrowsingContext | null {
|
|
46
|
-
return this.parents.get(context) ?? null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** The top-level context that contains `context`; an unknown context is its own root */
|
|
50
|
-
public rootOf(context: BrowsingContext): BrowsingContext {
|
|
51
|
-
let current = context;
|
|
52
|
-
for (let parent = this.parentOf(current); parent !== null; parent = this.parentOf(current)) {
|
|
53
|
-
current = parent;
|
|
54
|
-
}
|
|
55
|
-
return current;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** True when `context` is `root` or one of its descendants; false for unknown contexts */
|
|
59
|
-
public isWithin(context: BrowsingContext, root: BrowsingContext): boolean {
|
|
60
|
-
if (context === root) return true;
|
|
61
|
-
return this.parents.has(context) && this.rootOf(context) === root;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public async dispose(): Promise<void> {
|
|
65
|
-
this.connector.offEvent("browsingContext.contextCreated", this.onContext);
|
|
66
|
-
this.connector.offEvent("browsingContext.contextDestroyed", this.onContext);
|
|
67
|
-
this.parents.clear();
|
|
68
|
-
await this.subscription.unsubscribe();
|
|
69
|
-
}
|
|
70
|
-
}
|