@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,137 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import logger from "../logger/index.js";
|
|
3
|
+
import { WaitTimeoutError } from "../wait/wait-until.js";
|
|
4
|
+
/** A download did not complete */
|
|
5
|
+
export class DownloadError extends Error {
|
|
6
|
+
constructor(url, reason) {
|
|
7
|
+
super(`Download of ${url} failed: ${reason}`);
|
|
8
|
+
this.name = "DownloadError";
|
|
9
|
+
this.url = url;
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** A download did not finish in time */
|
|
14
|
+
export class DownloadTimeoutError extends WaitTimeoutError {
|
|
15
|
+
constructor(url, timeout) {
|
|
16
|
+
super(timeout, undefined);
|
|
17
|
+
this.name = "DownloadTimeoutError";
|
|
18
|
+
this.message = `Download of ${url} did not finish within ${timeout}ms`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** A file the page started to download */
|
|
22
|
+
export class Download {
|
|
23
|
+
constructor(address, filename, defaultTimeout) {
|
|
24
|
+
this.address = address;
|
|
25
|
+
this.filename = filename;
|
|
26
|
+
this.defaultTimeout = defaultTimeout;
|
|
27
|
+
this.outcome = new Promise((resolve) => (this.settle = resolve));
|
|
28
|
+
}
|
|
29
|
+
url() {
|
|
30
|
+
return this.address;
|
|
31
|
+
}
|
|
32
|
+
/** The name the server (or the link) suggested */
|
|
33
|
+
suggestedFilename() {
|
|
34
|
+
return this.filename;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Waits until the download finished and returns where the browser saved it. Rejects with DownloadError when it was
|
|
38
|
+
* cancelled, and with DownloadTimeoutError after `timeout` ms (default: the navigation timeout).
|
|
39
|
+
*/
|
|
40
|
+
async path(options = {}) {
|
|
41
|
+
const timeout = options.timeout ?? this.defaultTimeout;
|
|
42
|
+
let timer;
|
|
43
|
+
const late = new Promise((_, reject) => {
|
|
44
|
+
timer = setTimeout(() => reject(new DownloadTimeoutError(this.address, timeout)), timeout);
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const outcome = await Promise.race([this.outcome, late]);
|
|
48
|
+
if (outcome.status === "complete")
|
|
49
|
+
return outcome.path;
|
|
50
|
+
throw new DownloadError(this.address, outcome.status === "closed" ? "page closed" : "canceled");
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Waits for the end of the download: null when it completed, otherwise why it did not ("canceled") */
|
|
57
|
+
async failure() {
|
|
58
|
+
const outcome = await this.outcome;
|
|
59
|
+
if (outcome.status === "complete")
|
|
60
|
+
return null;
|
|
61
|
+
return outcome.status === "closed" ? "page closed" : "canceled";
|
|
62
|
+
}
|
|
63
|
+
/** @internal */
|
|
64
|
+
finish(outcome) {
|
|
65
|
+
this.settle(outcome);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Follows the downloads of one page (frames included) */
|
|
69
|
+
export class DownloadTracker {
|
|
70
|
+
constructor(connector, tree, contextId, timeout) {
|
|
71
|
+
this.connector = connector;
|
|
72
|
+
this.tree = tree;
|
|
73
|
+
this.contextId = contextId;
|
|
74
|
+
this.timeout = timeout;
|
|
75
|
+
this.events = new EventEmitter();
|
|
76
|
+
this.active = new Map();
|
|
77
|
+
this.disposed = false;
|
|
78
|
+
this.onBegin = (params) => {
|
|
79
|
+
if (!this.tree.isWithin(params.context, this.contextId))
|
|
80
|
+
return;
|
|
81
|
+
const download = new Download(params.url, params.suggestedFilename, this.timeout);
|
|
82
|
+
this.active.set(params.navigation, download);
|
|
83
|
+
for (const listener of this.events.rawListeners("download")) {
|
|
84
|
+
try {
|
|
85
|
+
listener(download);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
logger.error("A download listener threw", { err });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
this.onEnd = (params) => {
|
|
93
|
+
const download = this.active.get(params.navigation);
|
|
94
|
+
if (!download)
|
|
95
|
+
return;
|
|
96
|
+
this.active.delete(params.navigation);
|
|
97
|
+
download.finish(params.status === "complete" ? { status: "complete", path: params.filepath } : { status: "canceled" });
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
static async start(connector, tree, contextId, options) {
|
|
101
|
+
const tracker = new DownloadTracker(connector, tree, contextId, options.timeout);
|
|
102
|
+
connector.onEvent("browsingContext.downloadWillBegin", tracker.onBegin);
|
|
103
|
+
connector.onEvent("browsingContext.downloadEnd", tracker.onEnd);
|
|
104
|
+
try {
|
|
105
|
+
tracker.subscription = await connector.subscribe([
|
|
106
|
+
"browsingContext.downloadWillBegin",
|
|
107
|
+
"browsingContext.downloadEnd",
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
tracker.removeListeners();
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
return tracker;
|
|
115
|
+
}
|
|
116
|
+
on(event, listener) {
|
|
117
|
+
this.events.on(event, listener);
|
|
118
|
+
}
|
|
119
|
+
off(event, listener) {
|
|
120
|
+
this.events.off(event, listener);
|
|
121
|
+
}
|
|
122
|
+
/** Stops tracking; downloads still going on reject with "page closed". Idempotent */
|
|
123
|
+
async dispose() {
|
|
124
|
+
if (this.disposed)
|
|
125
|
+
return;
|
|
126
|
+
this.disposed = true;
|
|
127
|
+
this.removeListeners();
|
|
128
|
+
for (const download of this.active.values())
|
|
129
|
+
download.finish({ status: "closed" });
|
|
130
|
+
this.active.clear();
|
|
131
|
+
await this.subscription?.unsubscribe();
|
|
132
|
+
}
|
|
133
|
+
removeListeners() {
|
|
134
|
+
this.connector.offEvent("browsingContext.downloadWillBegin", this.onBegin);
|
|
135
|
+
this.connector.offEvent("browsingContext.downloadEnd", this.onEnd);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
/**
|
|
3
|
+
* Makes downloads land in a folder of their own, so a test knows where to look and runs do not mix: a new folder
|
|
4
|
+
* below `baseDir` is created and the browser told to save there. Returns it, or undefined when the browser cannot be told.
|
|
5
|
+
*/
|
|
6
|
+
export declare function prepareDownloadsDir(connector: BiDiConnector, baseDir: string): Promise<string | undefined>;
|
|
7
|
+
/** Removes `dir` when nothing was downloaded into it. Never throws */
|
|
8
|
+
export declare function removeIfEmpty(dir: string | undefined): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { mkdirSync, rmdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import logger from "../logger/index.js";
|
|
4
|
+
import { BiDiError } from "../transport/bidi-error.js";
|
|
5
|
+
let counter = 0;
|
|
6
|
+
/**
|
|
7
|
+
* Makes downloads land in a folder of their own, so a test knows where to look and runs do not mix: a new folder
|
|
8
|
+
* below `baseDir` is created and the browser told to save there. Returns it, or undefined when the browser cannot be told.
|
|
9
|
+
*/
|
|
10
|
+
export async function prepareDownloadsDir(connector, baseDir) {
|
|
11
|
+
const dir = path.join(path.resolve(baseDir), `dl-${Date.now().toString(36)}-${process.pid}-${counter++}`);
|
|
12
|
+
mkdirSync(dir, { recursive: true });
|
|
13
|
+
try {
|
|
14
|
+
await connector.send("browser.setDownloadBehavior", {
|
|
15
|
+
downloadBehavior: { type: "allowed", destinationFolder: dir },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
removeIfEmpty(dir);
|
|
20
|
+
if (err instanceof BiDiError && (err.code === "unknown command" || err.code === "unsupported operation")) {
|
|
21
|
+
logger.warn("The browser cannot be told where to save downloads; they go to its own folder");
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
/** Removes `dir` when nothing was downloaded into it. Never throws */
|
|
29
|
+
export function removeIfEmpty(dir) {
|
|
30
|
+
if (!dir)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
rmdirSync(dir);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Not empty, or already gone: leave it
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { UnsupportedOperationError } from "../locator/selector-errors.js";
|
|
2
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
3
|
+
declare const ORIENTATIONS: readonly ["portrait-primary", "portrait-secondary", "landscape-primary", "landscape-secondary"];
|
|
4
|
+
type OrientationType = (typeof ORIENTATIONS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* What to pretend about the browser. `undefined` leaves an option as it is, `null` puts the real value back.
|
|
7
|
+
* Which options a browser supports differs: an unsupported one throws UnsupportedOperationError.
|
|
8
|
+
*/
|
|
9
|
+
export interface EmulationOptions {
|
|
10
|
+
/** A BCP 47 tag, e.g. "nl-NL": `navigator.language`, `Intl` formatting */
|
|
11
|
+
locale?: string | null;
|
|
12
|
+
/** An IANA timezone, e.g. "Europe/Amsterdam" */
|
|
13
|
+
timezone?: string | null;
|
|
14
|
+
userAgent?: string | null;
|
|
15
|
+
/** Needs the geolocation permission: see `page.grantPermission("geolocation")` */
|
|
16
|
+
geolocation?: {
|
|
17
|
+
latitude: number;
|
|
18
|
+
longitude: number;
|
|
19
|
+
accuracy?: number;
|
|
20
|
+
} | null;
|
|
21
|
+
/** `true`: no network (`navigator.onLine` is false and requests fail); `false` or `null`: back online */
|
|
22
|
+
offline?: boolean | null;
|
|
23
|
+
/** "portrait" and "landscape" mean the primary orientation of that kind */
|
|
24
|
+
orientation?: "portrait" | "landscape" | OrientationType | null;
|
|
25
|
+
/** The size `screen.width` and `screen.height` report */
|
|
26
|
+
screen?: {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
} | null;
|
|
30
|
+
/** How many touch points `navigator.maxTouchPoints` reports */
|
|
31
|
+
touch?: number | null;
|
|
32
|
+
/** `false` turns JavaScript off; `true` or `null` on again */
|
|
33
|
+
javaScriptEnabled?: boolean | null;
|
|
34
|
+
}
|
|
35
|
+
export type EmulationTarget = {
|
|
36
|
+
contexts: string[];
|
|
37
|
+
} | {
|
|
38
|
+
userContexts: string[];
|
|
39
|
+
};
|
|
40
|
+
/** A part of the options could not be applied; what was applied before it stays applied */
|
|
41
|
+
export declare class EmulationError extends Error {
|
|
42
|
+
/** The options that were applied before the failure, in the order they were applied */
|
|
43
|
+
readonly applied: string[];
|
|
44
|
+
readonly failed: string;
|
|
45
|
+
readonly cause: unknown;
|
|
46
|
+
constructor(failed: string, applied: string[], cause: unknown);
|
|
47
|
+
}
|
|
48
|
+
/** The browser does not implement an emulation command */
|
|
49
|
+
export declare class EmulationUnsupportedError extends UnsupportedOperationError {
|
|
50
|
+
readonly option: string;
|
|
51
|
+
readonly applied: string[];
|
|
52
|
+
constructor(option: string, applied: string[]);
|
|
53
|
+
}
|
|
54
|
+
/** Throws RangeError when a value is invalid, without sending anything */
|
|
55
|
+
export declare function validateEmulation(options: EmulationOptions): void;
|
|
56
|
+
/**
|
|
57
|
+
* Applies the options to `target`, one command after the other. Nothing is rolled back when one fails:
|
|
58
|
+
* the error says what was applied.
|
|
59
|
+
*/
|
|
60
|
+
export declare function applyEmulation(connector: BiDiConnector, target: EmulationTarget, options: EmulationOptions): Promise<void>;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { UnsupportedOperationError } from "../locator/selector-errors.js";
|
|
2
|
+
import { BiDiError } from "../transport/bidi-error.js";
|
|
3
|
+
const ORIENTATIONS = [
|
|
4
|
+
"portrait-primary",
|
|
5
|
+
"portrait-secondary",
|
|
6
|
+
"landscape-primary",
|
|
7
|
+
"landscape-secondary",
|
|
8
|
+
];
|
|
9
|
+
/** A part of the options could not be applied; what was applied before it stays applied */
|
|
10
|
+
export class EmulationError extends Error {
|
|
11
|
+
constructor(failed, applied, cause) {
|
|
12
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
13
|
+
super(`emulate(): ${failed} failed${applied.length ? ` after applying ${applied.join(", ")}` : ""}: ${detail}`);
|
|
14
|
+
this.name = "EmulationError";
|
|
15
|
+
this.failed = failed;
|
|
16
|
+
this.applied = applied;
|
|
17
|
+
this.cause = cause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The browser does not implement an emulation command */
|
|
21
|
+
export class EmulationUnsupportedError extends UnsupportedOperationError {
|
|
22
|
+
constructor(option, applied) {
|
|
23
|
+
super(`emulate({ ${option} }) is not supported by the browser`);
|
|
24
|
+
this.name = "EmulationUnsupportedError";
|
|
25
|
+
this.option = option;
|
|
26
|
+
this.applied = applied;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
30
|
+
const isPositiveInteger = (value) => Number.isInteger(value) && value > 0;
|
|
31
|
+
/** Turns the options into commands, in a fixed order; throws RangeError for an invalid value, before anything is sent */
|
|
32
|
+
function plan(options) {
|
|
33
|
+
const steps = [];
|
|
34
|
+
if (options.locale !== undefined) {
|
|
35
|
+
if (options.locale !== null) {
|
|
36
|
+
try {
|
|
37
|
+
if (options.locale === "" || Intl.getCanonicalLocales(options.locale).length !== 1)
|
|
38
|
+
throw new RangeError();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new RangeError(`emulate(): invalid locale "${options.locale}"`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
steps.push({ option: "locale", method: "emulation.setLocaleOverride", params: { locale: options.locale } });
|
|
45
|
+
}
|
|
46
|
+
if (options.timezone !== undefined) {
|
|
47
|
+
if (options.timezone !== null) {
|
|
48
|
+
try {
|
|
49
|
+
new Intl.DateTimeFormat(undefined, { timeZone: options.timezone });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new RangeError(`emulate(): unknown timezone "${options.timezone}"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
steps.push({ option: "timezone", method: "emulation.setTimezoneOverride", params: { timezone: options.timezone } });
|
|
56
|
+
}
|
|
57
|
+
if (options.userAgent !== undefined) {
|
|
58
|
+
if (options.userAgent === "")
|
|
59
|
+
throw new RangeError("emulate(): userAgent must not be empty");
|
|
60
|
+
steps.push({ option: "userAgent", method: "emulation.setUserAgentOverride", params: { userAgent: options.userAgent } });
|
|
61
|
+
}
|
|
62
|
+
if (options.geolocation !== undefined) {
|
|
63
|
+
const place = options.geolocation;
|
|
64
|
+
if (place !== null) {
|
|
65
|
+
if (!isNumber(place.latitude) || place.latitude < -90 || place.latitude > 90) {
|
|
66
|
+
throw new RangeError("emulate(): latitude must be between -90 and 90");
|
|
67
|
+
}
|
|
68
|
+
if (!isNumber(place.longitude) || place.longitude < -180 || place.longitude > 180) {
|
|
69
|
+
throw new RangeError("emulate(): longitude must be between -180 and 180");
|
|
70
|
+
}
|
|
71
|
+
if (place.accuracy !== undefined && (!isNumber(place.accuracy) || place.accuracy < 0)) {
|
|
72
|
+
throw new RangeError("emulate(): accuracy must be 0 or more");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
steps.push({
|
|
76
|
+
option: "geolocation",
|
|
77
|
+
method: "emulation.setGeolocationOverride",
|
|
78
|
+
params: { coordinates: place },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (options.offline !== undefined) {
|
|
82
|
+
steps.push({
|
|
83
|
+
option: "offline",
|
|
84
|
+
method: "emulation.setNetworkConditions",
|
|
85
|
+
params: { networkConditions: options.offline ? { type: "offline" } : null },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (options.orientation !== undefined) {
|
|
89
|
+
let screenOrientation = null;
|
|
90
|
+
if (options.orientation !== null) {
|
|
91
|
+
const type = (options.orientation === "portrait" || options.orientation === "landscape"
|
|
92
|
+
? `${options.orientation}-primary`
|
|
93
|
+
: options.orientation);
|
|
94
|
+
if (!ORIENTATIONS.includes(type))
|
|
95
|
+
throw new RangeError(`emulate(): unknown orientation "${options.orientation}"`);
|
|
96
|
+
screenOrientation = { natural: type.startsWith("portrait") ? "portrait" : "landscape", type };
|
|
97
|
+
}
|
|
98
|
+
steps.push({ option: "orientation", method: "emulation.setScreenOrientationOverride", params: { screenOrientation } });
|
|
99
|
+
}
|
|
100
|
+
if (options.screen !== undefined) {
|
|
101
|
+
const area = options.screen;
|
|
102
|
+
if (area !== null && !(isPositiveInteger(area.width) && isPositiveInteger(area.height))) {
|
|
103
|
+
throw new RangeError("emulate(): screen width and height must be positive integers");
|
|
104
|
+
}
|
|
105
|
+
steps.push({ option: "screen", method: "emulation.setScreenSettingsOverride", params: { screenArea: area } });
|
|
106
|
+
}
|
|
107
|
+
if (options.touch !== undefined) {
|
|
108
|
+
if (options.touch !== null && !isPositiveInteger(options.touch)) {
|
|
109
|
+
throw new RangeError("emulate(): touch must be a positive integer");
|
|
110
|
+
}
|
|
111
|
+
steps.push({ option: "touch", method: "emulation.setTouchOverride", params: { maxTouchPoints: options.touch } });
|
|
112
|
+
}
|
|
113
|
+
if (options.javaScriptEnabled !== undefined) {
|
|
114
|
+
steps.push({
|
|
115
|
+
option: "javaScriptEnabled",
|
|
116
|
+
method: "emulation.setScriptingEnabled",
|
|
117
|
+
params: { enabled: options.javaScriptEnabled === false ? false : null },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return steps;
|
|
121
|
+
}
|
|
122
|
+
/** Throws RangeError when a value is invalid, without sending anything */
|
|
123
|
+
export function validateEmulation(options) {
|
|
124
|
+
plan(options);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Applies the options to `target`, one command after the other. Nothing is rolled back when one fails:
|
|
128
|
+
* the error says what was applied.
|
|
129
|
+
*/
|
|
130
|
+
export async function applyEmulation(connector, target, options) {
|
|
131
|
+
const applied = [];
|
|
132
|
+
for (const step of plan(options)) {
|
|
133
|
+
try {
|
|
134
|
+
await connector.send(step.method, { ...step.params, ...target });
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (err instanceof BiDiError && (err.code === "unknown command" || err.code === "unsupported operation")) {
|
|
138
|
+
throw new EmulationUnsupportedError(step.option, applied);
|
|
139
|
+
}
|
|
140
|
+
throw new EmulationError(step.option, applied, err);
|
|
141
|
+
}
|
|
142
|
+
applied.push(step.option);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
import type { FileDialogInfo } from "../types/bidi-modules/input.js";
|
|
3
|
+
import type { ContextTree } from "./context-tree.js";
|
|
4
|
+
/**
|
|
5
|
+
* A native file picker the page asked for (a click on an `<input type=file>` or its label). Nothing appears on screen:
|
|
6
|
+
* answer it with `setFiles()`.
|
|
7
|
+
*/
|
|
8
|
+
export declare class FileChooser {
|
|
9
|
+
private connector;
|
|
10
|
+
private info;
|
|
11
|
+
constructor(connector: BiDiConnector, info: FileDialogInfo);
|
|
12
|
+
/** Whether the input lets the user pick several files */
|
|
13
|
+
isMultiple(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Picks the files (an empty list picks none). Paths resolve against the working directory and must exist on the
|
|
16
|
+
* machine the browser runs on.
|
|
17
|
+
*/
|
|
18
|
+
setFiles(files: string | string[]): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
interface EventMap {
|
|
21
|
+
filechooser: [FileChooser];
|
|
22
|
+
}
|
|
23
|
+
/** Follows the file pickers of one page (frames included) */
|
|
24
|
+
export declare class FileChooserTracker {
|
|
25
|
+
private connector;
|
|
26
|
+
private tree;
|
|
27
|
+
private contextId;
|
|
28
|
+
private events;
|
|
29
|
+
private subscription;
|
|
30
|
+
private disposed;
|
|
31
|
+
private constructor();
|
|
32
|
+
static start(connector: BiDiConnector, tree: ContextTree, contextId: string): Promise<FileChooserTracker>;
|
|
33
|
+
on(event: "filechooser", listener: (...args: EventMap["filechooser"]) => void): void;
|
|
34
|
+
off(event: "filechooser", listener: (...args: EventMap["filechooser"]) => void): void;
|
|
35
|
+
/** Idempotent */
|
|
36
|
+
dispose(): Promise<void>;
|
|
37
|
+
private onOpened;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import logger from "../logger/index.js";
|
|
3
|
+
import { assertIsFile, resolveFiles } from "./file-paths.js";
|
|
4
|
+
/**
|
|
5
|
+
* A native file picker the page asked for (a click on an `<input type=file>` or its label). Nothing appears on screen:
|
|
6
|
+
* answer it with `setFiles()`.
|
|
7
|
+
*/
|
|
8
|
+
export class FileChooser {
|
|
9
|
+
constructor(connector, info) {
|
|
10
|
+
this.connector = connector;
|
|
11
|
+
this.info = info;
|
|
12
|
+
}
|
|
13
|
+
/** Whether the input lets the user pick several files */
|
|
14
|
+
isMultiple() {
|
|
15
|
+
return this.info.multiple;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Picks the files (an empty list picks none). Paths resolve against the working directory and must exist on the
|
|
19
|
+
* machine the browser runs on.
|
|
20
|
+
*/
|
|
21
|
+
async setFiles(files) {
|
|
22
|
+
const sharedId = this.info.element?.sharedId;
|
|
23
|
+
if (!sharedId)
|
|
24
|
+
throw new Error("file chooser has no element reference");
|
|
25
|
+
const paths = resolveFiles(files);
|
|
26
|
+
for (const file of paths)
|
|
27
|
+
await assertIsFile(file, "setFiles");
|
|
28
|
+
if (paths.length > 1 && !this.info.multiple) {
|
|
29
|
+
throw new Error("file chooser does not accept multiple files");
|
|
30
|
+
}
|
|
31
|
+
await this.connector.send("input.setFiles", {
|
|
32
|
+
context: this.info.context,
|
|
33
|
+
element: { sharedId },
|
|
34
|
+
files: paths,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Follows the file pickers of one page (frames included) */
|
|
39
|
+
export class FileChooserTracker {
|
|
40
|
+
constructor(connector, tree, contextId) {
|
|
41
|
+
this.connector = connector;
|
|
42
|
+
this.tree = tree;
|
|
43
|
+
this.contextId = contextId;
|
|
44
|
+
this.events = new EventEmitter();
|
|
45
|
+
this.disposed = false;
|
|
46
|
+
this.onOpened = (params) => {
|
|
47
|
+
if (!this.tree.isWithin(params.context, this.contextId))
|
|
48
|
+
return;
|
|
49
|
+
const chooser = new FileChooser(this.connector, params);
|
|
50
|
+
for (const listener of this.events.rawListeners("filechooser")) {
|
|
51
|
+
try {
|
|
52
|
+
listener(chooser);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
logger.error("A filechooser listener threw", { err });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
static async start(connector, tree, contextId) {
|
|
61
|
+
const tracker = new FileChooserTracker(connector, tree, contextId);
|
|
62
|
+
connector.onEvent("input.fileDialogOpened", tracker.onOpened);
|
|
63
|
+
try {
|
|
64
|
+
tracker.subscription = await connector.subscribe(["input.fileDialogOpened"]);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
connector.offEvent("input.fileDialogOpened", tracker.onOpened);
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
return tracker;
|
|
71
|
+
}
|
|
72
|
+
on(event, listener) {
|
|
73
|
+
this.events.on(event, listener);
|
|
74
|
+
}
|
|
75
|
+
off(event, listener) {
|
|
76
|
+
this.events.off(event, listener);
|
|
77
|
+
}
|
|
78
|
+
/** Idempotent */
|
|
79
|
+
async dispose() {
|
|
80
|
+
if (this.disposed)
|
|
81
|
+
return;
|
|
82
|
+
this.disposed = true;
|
|
83
|
+
this.connector.offEvent("input.fileDialogOpened", this.onOpened);
|
|
84
|
+
await this.subscription?.unsubscribe();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Absolute paths for `files` (relative ones resolve against the working directory) */
|
|
2
|
+
export declare function resolveFiles(files: string | string[]): string[];
|
|
3
|
+
/** Throws unless `file` is an existing regular file; `operation` names the caller in the message */
|
|
4
|
+
export declare function assertIsFile(file: string, operation: string): Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/** Absolute paths for `files` (relative ones resolve against the working directory) */
|
|
4
|
+
export function resolveFiles(files) {
|
|
5
|
+
return (Array.isArray(files) ? files : [files]).map((file) => path.resolve(file));
|
|
6
|
+
}
|
|
7
|
+
/** Throws unless `file` is an existing regular file; `operation` names the caller in the message */
|
|
8
|
+
export async function assertIsFile(file, operation) {
|
|
9
|
+
let info;
|
|
10
|
+
try {
|
|
11
|
+
info = await stat(file);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new Error(`${operation}(): file not found: ${file}`);
|
|
15
|
+
}
|
|
16
|
+
if (!info.isFile())
|
|
17
|
+
throw new Error(`${operation}(): not a file: ${file}`);
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { WaitTimeoutError } from "../wait/wait-until.js";
|
|
2
|
+
/** A navigation did not end in a loaded page */
|
|
3
|
+
export declare class NavigationError extends Error {
|
|
4
|
+
readonly url: string;
|
|
5
|
+
readonly reason: string;
|
|
6
|
+
/** The BiDi error code, or "timeout" */
|
|
7
|
+
readonly code: string | undefined;
|
|
8
|
+
constructor(url: string, reason: string, code?: string, operation?: string);
|
|
9
|
+
}
|
|
10
|
+
/** The page did not reach the awaited load state in time */
|
|
11
|
+
export declare class LoadStateTimeoutError extends WaitTimeoutError<undefined> {
|
|
12
|
+
constructor(state: string, timeout: number);
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { WaitTimeoutError } from "../wait/wait-until.js";
|
|
2
|
+
/** A navigation did not end in a loaded page */
|
|
3
|
+
export class NavigationError extends Error {
|
|
4
|
+
constructor(url, reason, code, operation = "navigateTo") {
|
|
5
|
+
super(`${operation}(): ${url} failed: ${reason}`);
|
|
6
|
+
this.name = "NavigationError";
|
|
7
|
+
this.url = url;
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** The page did not reach the awaited load state in time */
|
|
13
|
+
export class LoadStateTimeoutError extends WaitTimeoutError {
|
|
14
|
+
constructor(state, timeout) {
|
|
15
|
+
super(timeout, undefined);
|
|
16
|
+
this.name = "LoadStateTimeoutError";
|
|
17
|
+
this.message = `waitForLoadState("${state}") did not finish within ${timeout}ms`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
import type { Level } from "../types/bidi-modules/log.js";
|
|
3
|
+
import type { StackTrace } from "../types/bidi-modules/script.js";
|
|
4
|
+
import type { ContextTree } from "./context-tree.js";
|
|
5
|
+
/** One line of the browser's log, as kept for reports */
|
|
6
|
+
export interface LogRecord {
|
|
7
|
+
level: Level;
|
|
8
|
+
/** "console", "javascript" (an uncaught exception), or another type the browser reports */
|
|
9
|
+
type: string;
|
|
10
|
+
/** The console method, for `type: "console"` */
|
|
11
|
+
method?: string;
|
|
12
|
+
text: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SourceLocation {
|
|
16
|
+
url: string;
|
|
17
|
+
lineNumber: number;
|
|
18
|
+
columnNumber: number;
|
|
19
|
+
}
|
|
20
|
+
/** A call to `console.log`, `console.warn`, … in the page */
|
|
21
|
+
export declare class ConsoleMessage {
|
|
22
|
+
/** "debug" | "info" | "warn" | "error" */
|
|
23
|
+
readonly level: Level;
|
|
24
|
+
private readonly method;
|
|
25
|
+
private readonly message;
|
|
26
|
+
/** The arguments as plain values; DOM nodes and functions stay opaque (ElementHandle, RemoteObject) */
|
|
27
|
+
readonly args: unknown[];
|
|
28
|
+
readonly timestamp: number;
|
|
29
|
+
readonly stackTrace?: StackTrace | undefined;
|
|
30
|
+
constructor(
|
|
31
|
+
/** "debug" | "info" | "warn" | "error" */
|
|
32
|
+
level: Level, method: string, message: string,
|
|
33
|
+
/** The arguments as plain values; DOM nodes and functions stay opaque (ElementHandle, RemoteObject) */
|
|
34
|
+
args: unknown[], timestamp: number, stackTrace?: StackTrace | undefined);
|
|
35
|
+
/** The console method: "log", "warn", "error", "info", "debug", … */
|
|
36
|
+
type(): string;
|
|
37
|
+
/** What the browser prints for the call */
|
|
38
|
+
text(): string;
|
|
39
|
+
/** Where the call was made, when the browser says */
|
|
40
|
+
location(): SourceLocation | undefined;
|
|
41
|
+
}
|
|
42
|
+
/** An exception nobody caught in the page */
|
|
43
|
+
export declare class PageError extends Error {
|
|
44
|
+
readonly timestamp: number;
|
|
45
|
+
constructor(text: string, timestamp: number, stackTrace?: StackTrace);
|
|
46
|
+
}
|
|
47
|
+
interface EventMap {
|
|
48
|
+
console: [ConsoleMessage];
|
|
49
|
+
pageerror: [PageError];
|
|
50
|
+
}
|
|
51
|
+
export type LogEventName = keyof EventMap;
|
|
52
|
+
/**
|
|
53
|
+
* Collects the browser's log (`console.*` calls and uncaught exceptions) of one page, frames included,
|
|
54
|
+
* in a ring buffer, and emits `console` and `pageerror` events.
|
|
55
|
+
*/
|
|
56
|
+
export declare class PageLogs {
|
|
57
|
+
private connector;
|
|
58
|
+
private tree;
|
|
59
|
+
private contextId;
|
|
60
|
+
private maxEntries;
|
|
61
|
+
private events;
|
|
62
|
+
private buffer;
|
|
63
|
+
private uncaught;
|
|
64
|
+
private droppedCount;
|
|
65
|
+
private disposed;
|
|
66
|
+
private subscription;
|
|
67
|
+
private constructor();
|
|
68
|
+
static start(connector: BiDiConnector, tree: ContextTree, contextId: string, options?: {
|
|
69
|
+
maxEntries?: number;
|
|
70
|
+
}): Promise<PageLogs>;
|
|
71
|
+
/** The newest entries, oldest first (a copy) */
|
|
72
|
+
get entries(): readonly LogRecord[];
|
|
73
|
+
/** Uncaught exceptions seen since the start or the last `clear()` */
|
|
74
|
+
get errors(): readonly PageError[];
|
|
75
|
+
/** How many entries fell out of the buffer */
|
|
76
|
+
get dropped(): number;
|
|
77
|
+
on<E extends LogEventName>(event: E, listener: (...args: EventMap[E]) => void): void;
|
|
78
|
+
off<E extends LogEventName>(event: E, listener: (...args: EventMap[E]) => void): void;
|
|
79
|
+
clear(): void;
|
|
80
|
+
/** Idempotent */
|
|
81
|
+
dispose(): Promise<void>;
|
|
82
|
+
private onEntry;
|
|
83
|
+
/** A listener that throws must not stop the recording or the other listeners */
|
|
84
|
+
private emit;
|
|
85
|
+
}
|
|
86
|
+
export {};
|