@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
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { findBrowser } from "./browser-finder.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { BiDiConnector } from "../transport/bidi-connection.js";
|
|
7
|
+
import Page from "./page.js";
|
|
8
|
+
import { BrowserContext } from "./browser-context.js";
|
|
9
|
+
import { validateEmulation } from "./emulation.js";
|
|
10
|
+
import { prepareDownloadsDir, removeIfEmpty } from "./downloads-dir.js";
|
|
11
|
+
import logger from "../logger/index.js";
|
|
12
|
+
import { browsersDir, projectDir, readConfig } from "../config/config.js";
|
|
13
|
+
/**
|
|
14
|
+
* Sent with session.new. With "ignore" the browser leaves a JavaScript dialog open and reports it
|
|
15
|
+
* (`browsingContext.userPromptOpened`), so the framework decides how to answer instead of the browser.
|
|
16
|
+
*/
|
|
17
|
+
export const SESSION_CAPABILITIES = {
|
|
18
|
+
alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } },
|
|
19
|
+
};
|
|
20
|
+
/** Time (ms) to wait for Firefox to answer browser.close before killing the process */
|
|
21
|
+
const BROWSER_CLOSE_GRACE = 2000;
|
|
22
|
+
const browserProfilePath = (browserName) => path.join(browsersDir(), "profiles", browserName, "user.js");
|
|
23
|
+
const baseBrowserLaunchFlags = {
|
|
24
|
+
// chrome: [],
|
|
25
|
+
firefox: ["--no-sandbox", "--no-remote"],
|
|
26
|
+
};
|
|
27
|
+
/** Read when a browser launches, since the profile folder depends on the active project */
|
|
28
|
+
const defaultLaunchOptions = (browserName) => ({
|
|
29
|
+
port: 9222,
|
|
30
|
+
profileDir: path.dirname(browserProfilePath(browserName)),
|
|
31
|
+
headless: true,
|
|
32
|
+
});
|
|
33
|
+
const regexPrefix = {
|
|
34
|
+
firefox: /WebDriver\sBiDi\slistening\son\s/,
|
|
35
|
+
// chrome: /DevTools\slistening\son/,
|
|
36
|
+
};
|
|
37
|
+
export const browserLaunchFlag = (browserName, launchOptions) => {
|
|
38
|
+
const defLaunchOpts = defaultLaunchOptions(browserName);
|
|
39
|
+
const port = launchOptions.port || defLaunchOpts.port;
|
|
40
|
+
const profileDir = launchOptions.profileDir || defLaunchOpts.profileDir;
|
|
41
|
+
const headless = launchOptions.headless ?? defLaunchOpts.headless;
|
|
42
|
+
const baseLaunchFlags = [...baseBrowserLaunchFlags[browserName]];
|
|
43
|
+
if (headless) {
|
|
44
|
+
baseLaunchFlags.push("--headless");
|
|
45
|
+
}
|
|
46
|
+
baseLaunchFlags.push(`--remote-debugging-port=${port}`);
|
|
47
|
+
switch (browserName) {
|
|
48
|
+
// case "chrome":
|
|
49
|
+
// baseLaunchFlags.push(`--user-data-dir=${profileDir}`);
|
|
50
|
+
// break;
|
|
51
|
+
case "firefox":
|
|
52
|
+
baseLaunchFlags.push("--profile", profileDir);
|
|
53
|
+
// The home page (about:home) is privileged: BiDi refuses commands such as setViewport on it
|
|
54
|
+
baseLaunchFlags.push("about:blank");
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
logger.verbose("Launch flags for browser %s: ", browserName, {
|
|
58
|
+
baseLaunchFlags,
|
|
59
|
+
});
|
|
60
|
+
return baseLaunchFlags;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Stops what a failed launch started. A browser left running holds the profile (the next launch
|
|
64
|
+
* fails with "already open") and keeps the test process alive.
|
|
65
|
+
*/
|
|
66
|
+
export function abandonLaunch(browserProc, biDiConnector) {
|
|
67
|
+
biDiConnector?.kill();
|
|
68
|
+
browserProc.kill();
|
|
69
|
+
}
|
|
70
|
+
// On macOS a spawned browser is attributed to the terminal, which is denied access to
|
|
71
|
+
// ~/Library/Application Support/Firefox (profiles.ini). Point Firefox at a local app data dir instead.
|
|
72
|
+
const browserAppDataPath = (browserName) => path.join(browsersDir(), "app-data", browserName);
|
|
73
|
+
const browserLaunchEnv = (browserName) => {
|
|
74
|
+
const appDataPath = browserAppDataPath(browserName);
|
|
75
|
+
mkdirSync(appDataPath, { recursive: true });
|
|
76
|
+
logger.debug("Using app data dir for %s: %s", browserName, appDataPath);
|
|
77
|
+
switch (browserName) {
|
|
78
|
+
case "firefox":
|
|
79
|
+
return { MOZ_APP_DATA: appDataPath };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const browserProfiles = {
|
|
83
|
+
// chrome: "",
|
|
84
|
+
firefox: `
|
|
85
|
+
user_pref("devtools.debugger.remote-enabled", true);
|
|
86
|
+
user_pref("devtools.debugger.prompt-connection", false);
|
|
87
|
+
user_pref("devtools.chrome.enabled", true);
|
|
88
|
+
// The home page (about:home) is privileged: BiDi refuses commands such as setViewport on it
|
|
89
|
+
user_pref("browser.startup.page", 0);
|
|
90
|
+
user_pref("browser.startup.homepage", "about:blank");
|
|
91
|
+
user_pref("browser.newtabpage.enabled", false);
|
|
92
|
+
`,
|
|
93
|
+
};
|
|
94
|
+
// Firefox logs "WebDriver BiDi listening on ws://<host>:<port>" without the endpoint path
|
|
95
|
+
const browserWsPath = {
|
|
96
|
+
firefox: "/session",
|
|
97
|
+
};
|
|
98
|
+
const browserWsRegex = {
|
|
99
|
+
firefox: /ws:\/\/(\d){1,3}.(\d){1,3}.(\d){1,3}.(\d){1,3}:(\d){4}/g,
|
|
100
|
+
// chrome:
|
|
101
|
+
// /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,
|
|
102
|
+
};
|
|
103
|
+
export class Browser {
|
|
104
|
+
constructor({ browserProc, biDiConnector, downloadsDir, }) {
|
|
105
|
+
this.userContexts = new Map();
|
|
106
|
+
this.browserProc = browserProc;
|
|
107
|
+
this.biDiConnector = biDiConnector;
|
|
108
|
+
this.downloadsDir = downloadsDir;
|
|
109
|
+
this.host = {
|
|
110
|
+
connector: biDiConnector,
|
|
111
|
+
newPage: (options) => this.newPage(options),
|
|
112
|
+
forget: (context) => void this.userContexts.delete(context.id),
|
|
113
|
+
};
|
|
114
|
+
this.defaultContext = new BrowserContext(this.host, "default", true);
|
|
115
|
+
}
|
|
116
|
+
/** Resolves when the browser process has exited, whether `close()` or a person closed it */
|
|
117
|
+
get exited() {
|
|
118
|
+
this.exitedPromise ??= new Promise((resolve) => {
|
|
119
|
+
if (this.browserProc.exitCode !== null || this.browserProc.signalCode)
|
|
120
|
+
return resolve();
|
|
121
|
+
this.browserProc.once("exit", () => resolve());
|
|
122
|
+
});
|
|
123
|
+
return this.exitedPromise;
|
|
124
|
+
}
|
|
125
|
+
static async launch(browserName, launchOptions = defaultLaunchOptions(browserName),
|
|
126
|
+
/** Aborting kills the browser process and rejects the launch */
|
|
127
|
+
signal) {
|
|
128
|
+
signal?.throwIfAborted();
|
|
129
|
+
logger.verbose("Trying to launch browser %s", browserName);
|
|
130
|
+
const browserLocation = findBrowser(browserName);
|
|
131
|
+
logger.debug("Found browser install location at: %s", browserLocation);
|
|
132
|
+
ensureBrowserProfile(browserName);
|
|
133
|
+
logger.verbose("Trying to spawn %s process", browserName);
|
|
134
|
+
const browserProc = spawn(browserLocation, browserLaunchFlag(browserName, launchOptions), { env: { ...process.env, ...browserLaunchEnv(browserName) } });
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const onUnexpectedClose = (code) => {
|
|
137
|
+
reject(new Error(`Browser process exited unexpectedly with code ${code}`));
|
|
138
|
+
};
|
|
139
|
+
browserProc.on("close", onUnexpectedClose);
|
|
140
|
+
const onAbort = () => {
|
|
141
|
+
logger.verbose("Launch of %s aborted, killing process", browserName);
|
|
142
|
+
browserProc.kill();
|
|
143
|
+
reject(signal?.reason);
|
|
144
|
+
};
|
|
145
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
146
|
+
browserProc.stdout.on("data", (data) => {
|
|
147
|
+
logger.debug(`${browserName} stdout: ${data}`);
|
|
148
|
+
if (String(data).match(/!!!\scould\snot\sstart\sserver\son\sport\s(\d){4}/g)) {
|
|
149
|
+
browserProc.kill();
|
|
150
|
+
reject(`PORT ${launchOptions.port} is already in use!`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
browserProc.stderr.on("data", async (data) => {
|
|
154
|
+
logger.debug(`${browserName} stderr: ${data}`);
|
|
155
|
+
const urlMatch = String(data).match(browserWsRegex[browserName]);
|
|
156
|
+
logger.debug("Browser announced its endpoint", { urlMatch });
|
|
157
|
+
if (!Array.isArray(urlMatch) || urlMatch.length === 0) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Killing the process mid-handshake rejects pending BiDi calls; surface that via reject
|
|
161
|
+
// instead of an unhandled rejection from this async listener
|
|
162
|
+
let biDiConnector;
|
|
163
|
+
try {
|
|
164
|
+
let url = urlMatch[0] + browserWsPath[browserName];
|
|
165
|
+
logger.verbose("Websocket URL match: %s", url);
|
|
166
|
+
biDiConnector = await BiDiConnector.connect(url, {
|
|
167
|
+
commandTimeout: await readCommandTimeout(),
|
|
168
|
+
});
|
|
169
|
+
logger.verbose("Starting new session");
|
|
170
|
+
const status = await biDiConnector.send("session.status", {});
|
|
171
|
+
logger.verbose("Session status", status);
|
|
172
|
+
await biDiConnector.send("session.new", {
|
|
173
|
+
capabilities: SESSION_CAPABILITIES,
|
|
174
|
+
});
|
|
175
|
+
logger.verbose("Session started successfully");
|
|
176
|
+
const downloadsDir = await prepareDownloadsDir(biDiConnector, path.resolve(projectDir(), (await readOptionalConfig("downloadsDir")) ?? "result/downloads"));
|
|
177
|
+
const browser = new Browser({
|
|
178
|
+
browserProc,
|
|
179
|
+
biDiConnector,
|
|
180
|
+
downloadsDir,
|
|
181
|
+
});
|
|
182
|
+
logger.verbose("Requesting current browser tree");
|
|
183
|
+
const browserTree = await biDiConnector.send("browsingContext.getTree", {});
|
|
184
|
+
logger.verbose("Received current browser tree");
|
|
185
|
+
logger.debug("Browser tree: ", { browserTree });
|
|
186
|
+
const pageContext = browserTree.contexts[0];
|
|
187
|
+
let page = undefined;
|
|
188
|
+
if (pageContext) {
|
|
189
|
+
page = new Page(biDiConnector, pageContext.context);
|
|
190
|
+
browser.defaultContext.register(page);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
page = await browser.newPage({ type: "tab" });
|
|
194
|
+
}
|
|
195
|
+
await page.applyDefaultViewport();
|
|
196
|
+
// Requests made before tracking starts are invisible to it, and a dialog nobody watches would hang the page
|
|
197
|
+
await page.startNetworkTracking();
|
|
198
|
+
await page.startDialogHandling();
|
|
199
|
+
await page.startLogging();
|
|
200
|
+
await page.startDownloadTracking();
|
|
201
|
+
await page.startFileChooserTracking();
|
|
202
|
+
browserProc.off("close", onUnexpectedClose);
|
|
203
|
+
signal?.removeEventListener("abort", onAbort);
|
|
204
|
+
resolve({
|
|
205
|
+
browser,
|
|
206
|
+
page,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
browserProc.off("close", onUnexpectedClose);
|
|
211
|
+
signal?.removeEventListener("abort", onAbort);
|
|
212
|
+
abandonLaunch(browserProc, biDiConnector);
|
|
213
|
+
reject(err);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** Closes the browser via BiDi browser.close, then makes sure the process has exited */
|
|
219
|
+
async close() {
|
|
220
|
+
if (this.browserProc.exitCode !== null || this.browserProc.signalCode) {
|
|
221
|
+
this.biDiConnector.kill();
|
|
222
|
+
removeIfEmpty(this.downloadsDir);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const exited = new Promise((resolve) => this.browserProc.once("exit", resolve));
|
|
226
|
+
// User contexts outlive the browser in its profile unless they are removed
|
|
227
|
+
await Promise.allSettled([...this.userContexts.values()].map((context) => context.close()));
|
|
228
|
+
try {
|
|
229
|
+
// Firefox may exit without answering; a lost reply must not hold up teardown
|
|
230
|
+
await Promise.race([
|
|
231
|
+
this.biDiConnector.send("browser.close", {}),
|
|
232
|
+
sleep(BROWSER_CLOSE_GRACE, undefined, { ref: false }),
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// browser may already be shutting down
|
|
237
|
+
}
|
|
238
|
+
this.biDiConnector.kill();
|
|
239
|
+
this.browserProc.kill();
|
|
240
|
+
await exited;
|
|
241
|
+
// A folder nothing was downloaded into would only pile up
|
|
242
|
+
removeIfEmpty(this.downloadsDir);
|
|
243
|
+
}
|
|
244
|
+
/** Alias of close() */
|
|
245
|
+
async kill() {
|
|
246
|
+
await this.close();
|
|
247
|
+
}
|
|
248
|
+
async newPage({ type, background, userContext, }) {
|
|
249
|
+
const createBrowserContext = await this.biDiConnector.send("browsingContext.create", {
|
|
250
|
+
type,
|
|
251
|
+
background,
|
|
252
|
+
userContext,
|
|
253
|
+
});
|
|
254
|
+
logger.debug("Created browsing context", { createBrowserContext });
|
|
255
|
+
const page = new Page(this.biDiConnector, createBrowserContext.context);
|
|
256
|
+
this.contextFor(userContext).register(page);
|
|
257
|
+
await page.applyDefaultViewport();
|
|
258
|
+
await page.startNetworkTracking();
|
|
259
|
+
await page.startDialogHandling();
|
|
260
|
+
await page.startLogging();
|
|
261
|
+
await page.startDownloadTracking();
|
|
262
|
+
await page.startFileChooserTracking();
|
|
263
|
+
return page;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Creates an isolated context: its own cookies, storage and cache. `browser.close()` removes the contexts
|
|
267
|
+
* that are still open, as Firefox would otherwise keep them in its profile.
|
|
268
|
+
* @example
|
|
269
|
+
* const alice = await browser.newContext();
|
|
270
|
+
* const page = await alice.newPage();
|
|
271
|
+
*/
|
|
272
|
+
async newContext(options = {}) {
|
|
273
|
+
const { acceptInsecureCerts, unhandledPromptBehavior, ...emulation } = options;
|
|
274
|
+
// A wrong value must not leave a context behind, so it is refused before one is created
|
|
275
|
+
validateEmulation(emulation);
|
|
276
|
+
const { userContext } = await this.biDiConnector.send("browser.createUserContext", {
|
|
277
|
+
...(acceptInsecureCerts !== undefined && { acceptInsecureCerts }),
|
|
278
|
+
...(unhandledPromptBehavior && { unhandledPromptBehavior }),
|
|
279
|
+
});
|
|
280
|
+
const context = new BrowserContext(this.host, userContext, false);
|
|
281
|
+
this.userContexts.set(userContext, context);
|
|
282
|
+
if (Object.keys(emulation).length > 0) {
|
|
283
|
+
try {
|
|
284
|
+
await context.emulate(emulation);
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
// Firefox keeps a context in its profile until it is removed
|
|
288
|
+
await context.close().catch((closeErr) => logger.debug("Could not remove a context whose emulation failed", {
|
|
289
|
+
closeErr,
|
|
290
|
+
}));
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return context;
|
|
295
|
+
}
|
|
296
|
+
/** The default context, then the contexts this browser created that are still open */
|
|
297
|
+
contexts() {
|
|
298
|
+
return [this.defaultContext, ...this.userContexts.values()];
|
|
299
|
+
}
|
|
300
|
+
contextFor(userContext) {
|
|
301
|
+
if (userContext === undefined || userContext === "default")
|
|
302
|
+
return this.defaultContext;
|
|
303
|
+
return this.userContexts.get(userContext) ?? this.adopt(userContext);
|
|
304
|
+
}
|
|
305
|
+
/** A user context this browser did not create (Firefox keeps them in its profile): usable, never closed by `close()` */
|
|
306
|
+
adopt(userContext) {
|
|
307
|
+
const context = new BrowserContext(this.host, userContext, false);
|
|
308
|
+
this.userContexts.set(userContext, context);
|
|
309
|
+
return context;
|
|
310
|
+
}
|
|
311
|
+
async getCookie(filter, partition) {
|
|
312
|
+
return await this.biDiConnector.send("storage.getCookies", {
|
|
313
|
+
filter,
|
|
314
|
+
partition,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
async setCookie(cookie, partition) {
|
|
318
|
+
return await this.biDiConnector.send("storage.setCookie", {
|
|
319
|
+
partition,
|
|
320
|
+
cookie,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** A config value, or undefined when it is not set or the config cannot be read */
|
|
325
|
+
async function readOptionalConfig(key) {
|
|
326
|
+
try {
|
|
327
|
+
return await readConfig(key);
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
logger.debug("Could not read %s from config, using the default", key, {
|
|
331
|
+
err,
|
|
332
|
+
});
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** The configured BiDi command timeout; a missing or unreadable config falls back to the connector default */
|
|
337
|
+
async function readCommandTimeout() {
|
|
338
|
+
try {
|
|
339
|
+
return (await readConfig("bidi"))?.commandTimeout;
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
logger.debug("Could not read bidi config, using defaults", { err });
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** Writes the browser's user.js unless it already holds the current prefs; an outdated one is replaced */
|
|
347
|
+
export function ensureBrowserProfile(browserName, profilePath = browserProfilePath(browserName)) {
|
|
348
|
+
logger.verbose("Checking browser profile for: %s", browserName);
|
|
349
|
+
if (!profilePath) {
|
|
350
|
+
logger.verbose("No browser profile path declared, skipping...");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (existsSync(profilePath)) {
|
|
354
|
+
if (readFileSync(profilePath, "utf8") === browserProfiles[browserName]) {
|
|
355
|
+
logger.verbose("Browser profile is up to date at: %s", profilePath);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
logger.verbose("Browser profile at %s is outdated, rewriting it", profilePath);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
logger.verbose("No browser profile created yet for %s", browserName);
|
|
362
|
+
}
|
|
363
|
+
createBrowserProfile(browserName, profilePath);
|
|
364
|
+
}
|
|
365
|
+
function createBrowserProfile(browserName, profilePath) {
|
|
366
|
+
logger.verbose("Creating browser profile for %s at %s", browserName, profilePath);
|
|
367
|
+
mkdirSync(path.dirname(profilePath), { recursive: true });
|
|
368
|
+
writeFileSync(profilePath, browserProfiles[browserName], {
|
|
369
|
+
encoding: "utf8",
|
|
370
|
+
});
|
|
371
|
+
logger.verbose("Browser profile created");
|
|
372
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
import type { BrowsingContext } from "../types/bidi-modules/browsing-context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Tracks which browsing contexts (iframes) belong to which top-level context, so events that
|
|
5
|
+
* carry a child context id can be attributed to the page that owns it.
|
|
6
|
+
* Destroyed contexts stay known: late events for them must still resolve to their page.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ContextTree {
|
|
9
|
+
private connector;
|
|
10
|
+
private subscription;
|
|
11
|
+
private parents;
|
|
12
|
+
private constructor();
|
|
13
|
+
static create(connector: BiDiConnector): Promise<ContextTree>;
|
|
14
|
+
private onContext;
|
|
15
|
+
private add;
|
|
16
|
+
/** The direct parent, or null for a top-level or unknown context */
|
|
17
|
+
parentOf(context: BrowsingContext): BrowsingContext | null;
|
|
18
|
+
/** The top-level context that contains `context`; an unknown context is its own root */
|
|
19
|
+
rootOf(context: BrowsingContext): BrowsingContext;
|
|
20
|
+
/** True when `context` is `root` or one of its descendants; false for unknown contexts */
|
|
21
|
+
isWithin(context: BrowsingContext, root: BrowsingContext): boolean;
|
|
22
|
+
dispose(): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks which browsing contexts (iframes) belong to which top-level context, so events that
|
|
3
|
+
* carry a child context id can be attributed to the page that owns it.
|
|
4
|
+
* Destroyed contexts stay known: late events for them must still resolve to their page.
|
|
5
|
+
*/
|
|
6
|
+
export class ContextTree {
|
|
7
|
+
constructor(connector, subscription) {
|
|
8
|
+
this.connector = connector;
|
|
9
|
+
this.subscription = subscription;
|
|
10
|
+
this.parents = new Map();
|
|
11
|
+
this.onContext = (info) => this.add(info);
|
|
12
|
+
}
|
|
13
|
+
static async create(connector) {
|
|
14
|
+
// Subscribe first: a context created between getTree and subscribing would be missed
|
|
15
|
+
const subscription = await connector.subscribe([
|
|
16
|
+
"browsingContext.contextCreated",
|
|
17
|
+
"browsingContext.contextDestroyed",
|
|
18
|
+
]);
|
|
19
|
+
const tree = new ContextTree(connector, subscription);
|
|
20
|
+
connector.onEvent("browsingContext.contextCreated", tree.onContext);
|
|
21
|
+
connector.onEvent("browsingContext.contextDestroyed", tree.onContext);
|
|
22
|
+
try {
|
|
23
|
+
const { contexts } = await connector.send("browsingContext.getTree", {});
|
|
24
|
+
contexts.forEach((info) => tree.add(info));
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
await tree.dispose();
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
return tree;
|
|
31
|
+
}
|
|
32
|
+
add(info) {
|
|
33
|
+
this.parents.set(info.context, info.parent ?? null);
|
|
34
|
+
info.children?.forEach((child) => this.add(child));
|
|
35
|
+
}
|
|
36
|
+
/** The direct parent, or null for a top-level or unknown context */
|
|
37
|
+
parentOf(context) {
|
|
38
|
+
return this.parents.get(context) ?? null;
|
|
39
|
+
}
|
|
40
|
+
/** The top-level context that contains `context`; an unknown context is its own root */
|
|
41
|
+
rootOf(context) {
|
|
42
|
+
let current = context;
|
|
43
|
+
for (let parent = this.parentOf(current); parent !== null; parent = this.parentOf(current)) {
|
|
44
|
+
current = parent;
|
|
45
|
+
}
|
|
46
|
+
return current;
|
|
47
|
+
}
|
|
48
|
+
/** True when `context` is `root` or one of its descendants; false for unknown contexts */
|
|
49
|
+
isWithin(context, root) {
|
|
50
|
+
if (context === root)
|
|
51
|
+
return true;
|
|
52
|
+
return this.parents.has(context) && this.rootOf(context) === root;
|
|
53
|
+
}
|
|
54
|
+
async dispose() {
|
|
55
|
+
this.connector.offEvent("browsingContext.contextCreated", this.onContext);
|
|
56
|
+
this.connector.offEvent("browsingContext.contextDestroyed", this.onContext);
|
|
57
|
+
this.parents.clear();
|
|
58
|
+
await this.subscription.unsubscribe();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
import type { UserPromptOpenedParameters, UserPromptType } from "../types/bidi-modules/browsing-context.js";
|
|
3
|
+
export interface DialogResult {
|
|
4
|
+
accepted: boolean;
|
|
5
|
+
/** What was typed into a `prompt` */
|
|
6
|
+
userText?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A JavaScript dialog (`alert`, `confirm`, `prompt`) or a `beforeunload` prompt that is open in the page.
|
|
10
|
+
* Answer it once with `accept()` or `dismiss()`; `page.on("dialog")` hands these out.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Dialog {
|
|
13
|
+
private connector;
|
|
14
|
+
/** The top-level context of the page; prompts are answered there, also when an iframe opened them */
|
|
15
|
+
private context;
|
|
16
|
+
private info;
|
|
17
|
+
/** Resolves when the browser reports the prompt closed */
|
|
18
|
+
readonly closed: Promise<DialogResult>;
|
|
19
|
+
private resolveClosed;
|
|
20
|
+
private answered;
|
|
21
|
+
constructor(connector: BiDiConnector,
|
|
22
|
+
/** The top-level context of the page; prompts are answered there, also when an iframe opened them */
|
|
23
|
+
context: string, info: UserPromptOpenedParameters);
|
|
24
|
+
type(): UserPromptType;
|
|
25
|
+
message(): string;
|
|
26
|
+
/** The text a `prompt` starts with; empty for the other kinds */
|
|
27
|
+
defaultValue(): string;
|
|
28
|
+
/** True once the dialog is answered, by you or by the browser */
|
|
29
|
+
get handled(): boolean;
|
|
30
|
+
/** Clicks OK; `promptText` is typed into a `prompt` first */
|
|
31
|
+
accept(promptText?: string): Promise<void>;
|
|
32
|
+
/** Clicks Cancel */
|
|
33
|
+
dismiss(): Promise<void>;
|
|
34
|
+
/** Called by the page when the browser reports the prompt closed */
|
|
35
|
+
markClosed(result: DialogResult): void;
|
|
36
|
+
private answer;
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BiDiError } from "../transport/bidi-error.js";
|
|
2
|
+
/**
|
|
3
|
+
* A JavaScript dialog (`alert`, `confirm`, `prompt`) or a `beforeunload` prompt that is open in the page.
|
|
4
|
+
* Answer it once with `accept()` or `dismiss()`; `page.on("dialog")` hands these out.
|
|
5
|
+
*/
|
|
6
|
+
export class Dialog {
|
|
7
|
+
constructor(connector,
|
|
8
|
+
/** The top-level context of the page; prompts are answered there, also when an iframe opened them */
|
|
9
|
+
context, info) {
|
|
10
|
+
this.connector = connector;
|
|
11
|
+
this.context = context;
|
|
12
|
+
this.info = info;
|
|
13
|
+
this.closed = new Promise((resolve) => (this.resolveClosed = resolve));
|
|
14
|
+
// With another handler than "ignore" the browser has answered already
|
|
15
|
+
this.answered = info.handler !== "ignore";
|
|
16
|
+
}
|
|
17
|
+
type() {
|
|
18
|
+
return this.info.type;
|
|
19
|
+
}
|
|
20
|
+
message() {
|
|
21
|
+
return this.info.message;
|
|
22
|
+
}
|
|
23
|
+
/** The text a `prompt` starts with; empty for the other kinds */
|
|
24
|
+
defaultValue() {
|
|
25
|
+
return this.info.defaultValue ?? "";
|
|
26
|
+
}
|
|
27
|
+
/** True once the dialog is answered, by you or by the browser */
|
|
28
|
+
get handled() {
|
|
29
|
+
return this.answered;
|
|
30
|
+
}
|
|
31
|
+
/** Clicks OK; `promptText` is typed into a `prompt` first */
|
|
32
|
+
accept(promptText) {
|
|
33
|
+
return this.answer({ accept: true, ...(promptText !== undefined && { userText: promptText }) });
|
|
34
|
+
}
|
|
35
|
+
/** Clicks Cancel */
|
|
36
|
+
dismiss() {
|
|
37
|
+
return this.answer({ accept: false });
|
|
38
|
+
}
|
|
39
|
+
/** Called by the page when the browser reports the prompt closed */
|
|
40
|
+
markClosed(result) {
|
|
41
|
+
this.answered = true;
|
|
42
|
+
this.resolveClosed(result);
|
|
43
|
+
}
|
|
44
|
+
async answer(params) {
|
|
45
|
+
if (this.answered)
|
|
46
|
+
throw new Error("dialog already handled");
|
|
47
|
+
this.answered = true;
|
|
48
|
+
try {
|
|
49
|
+
await this.connector.send("browsingContext.handleUserPrompt", {
|
|
50
|
+
context: this.context,
|
|
51
|
+
...params,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// Closed meanwhile (a navigation, another handler): the dialog is gone, which is what was wanted
|
|
56
|
+
if (err instanceof BiDiError && err.code === "no such alert")
|
|
57
|
+
return;
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
import { WaitTimeoutError } from "../wait/wait-until.js";
|
|
3
|
+
import type { ContextTree } from "./context-tree.js";
|
|
4
|
+
/** A download did not complete */
|
|
5
|
+
export declare class DownloadError extends Error {
|
|
6
|
+
readonly url: string;
|
|
7
|
+
/** "canceled": the browser stopped it or the server broke the connection; "page closed" */
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
constructor(url: string, reason: string);
|
|
10
|
+
}
|
|
11
|
+
/** A download did not finish in time */
|
|
12
|
+
export declare class DownloadTimeoutError extends WaitTimeoutError<undefined> {
|
|
13
|
+
constructor(url: string, timeout: number);
|
|
14
|
+
}
|
|
15
|
+
type Outcome = {
|
|
16
|
+
status: "complete";
|
|
17
|
+
path: string;
|
|
18
|
+
} | {
|
|
19
|
+
status: "canceled";
|
|
20
|
+
} | {
|
|
21
|
+
status: "closed";
|
|
22
|
+
};
|
|
23
|
+
/** A file the page started to download */
|
|
24
|
+
export declare class Download {
|
|
25
|
+
private readonly address;
|
|
26
|
+
private readonly filename;
|
|
27
|
+
private readonly defaultTimeout;
|
|
28
|
+
private settle;
|
|
29
|
+
private outcome;
|
|
30
|
+
constructor(address: string, filename: string, defaultTimeout: number);
|
|
31
|
+
url(): string;
|
|
32
|
+
/** The name the server (or the link) suggested */
|
|
33
|
+
suggestedFilename(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Waits until the download finished and returns where the browser saved it. Rejects with DownloadError when it was
|
|
36
|
+
* cancelled, and with DownloadTimeoutError after `timeout` ms (default: the navigation timeout).
|
|
37
|
+
*/
|
|
38
|
+
path(options?: {
|
|
39
|
+
timeout?: number;
|
|
40
|
+
}): Promise<string>;
|
|
41
|
+
/** Waits for the end of the download: null when it completed, otherwise why it did not ("canceled") */
|
|
42
|
+
failure(): Promise<string | null>;
|
|
43
|
+
/** @internal */
|
|
44
|
+
finish(outcome: Outcome): void;
|
|
45
|
+
}
|
|
46
|
+
interface EventMap {
|
|
47
|
+
download: [Download];
|
|
48
|
+
}
|
|
49
|
+
/** Follows the downloads of one page (frames included) */
|
|
50
|
+
export declare class DownloadTracker {
|
|
51
|
+
private connector;
|
|
52
|
+
private tree;
|
|
53
|
+
private contextId;
|
|
54
|
+
private timeout;
|
|
55
|
+
private events;
|
|
56
|
+
private active;
|
|
57
|
+
private subscription;
|
|
58
|
+
private disposed;
|
|
59
|
+
private constructor();
|
|
60
|
+
static start(connector: BiDiConnector, tree: ContextTree, contextId: string, options: {
|
|
61
|
+
timeout: number;
|
|
62
|
+
}): Promise<DownloadTracker>;
|
|
63
|
+
on(event: "download", listener: (...args: EventMap["download"]) => void): void;
|
|
64
|
+
off(event: "download", listener: (...args: EventMap["download"]) => void): void;
|
|
65
|
+
/** Stops tracking; downloads still going on reject with "page closed". Idempotent */
|
|
66
|
+
dispose(): Promise<void>;
|
|
67
|
+
private removeListeners;
|
|
68
|
+
private onBegin;
|
|
69
|
+
private onEnd;
|
|
70
|
+
}
|
|
71
|
+
export {};
|