@itmetsam/samurai-framework 0.7.1 → 0.9.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 +31 -12
- package/dist/api.d.ts +1 -0
- package/dist/api.js +1 -0
- package/dist/browser/browser-finder.js +2 -2
- package/dist/browser/browser.d.ts +11 -15
- package/dist/browser/browser.js +79 -58
- package/dist/browser/chrome-launch.d.ts +57 -0
- package/dist/browser/chrome-launch.js +127 -0
- package/dist/browser/download-folders.d.ts +27 -0
- package/dist/browser/download-folders.js +97 -0
- package/dist/browser/file-chooser.d.ts +4 -3
- package/dist/browser/file-chooser.js +9 -8
- package/dist/browser/generated-files.d.ts +30 -0
- package/dist/browser/generated-files.js +116 -0
- package/dist/browser/page-logs.d.ts +2 -0
- package/dist/browser/page-logs.js +25 -5
- package/dist/browser/page.d.ts +37 -0
- package/dist/browser/page.js +129 -2
- package/dist/browser/screenshot.js +16 -2
- package/dist/browser/session-capabilities.d.ts +12 -0
- package/dist/browser/session-capabilities.js +8 -0
- package/dist/browsers/index.d.ts +2 -2
- package/dist/browsers/index.js +2 -2
- package/dist/browsers/overrides.d.ts +2 -0
- package/dist/browsers/overrides.js +2 -0
- package/dist/browsers/pins.d.ts +1 -1
- package/dist/browsers/pins.js +2 -2
- package/dist/browsers/resolve.d.ts +2 -0
- package/dist/browsers/resolve.js +14 -2
- package/dist/browsers/store.d.ts +2 -0
- package/dist/browsers/store.js +8 -0
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +20 -5
- package/dist/locator/locator.d.ts +17 -5
- package/dist/locator/locator.js +77 -10
- package/dist/network/data-collector.d.ts +1 -0
- package/dist/network/data-collector.js +23 -5
- package/dist/network/network-tracker.d.ts +8 -1
- package/dist/network/network-tracker.js +42 -3
- package/dist/network/router.d.ts +5 -0
- package/dist/network/router.js +33 -11
- package/dist/project/files-store.d.ts +28 -0
- package/dist/project/files-store.js +106 -0
- package/dist/project/index.d.ts +1 -0
- package/dist/project/index.js +1 -0
- package/dist/recorder/apply.js +8 -6
- package/dist/recorder/files-capture.d.ts +26 -0
- package/dist/recorder/files-capture.js +92 -0
- package/dist/recorder/files-reader.d.ts +7 -0
- package/dist/recorder/files-reader.js +25 -0
- package/dist/recorder/index.d.ts +1 -1
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/normalise.d.ts +26 -2
- package/dist/recorder/normalise.js +48 -1
- package/dist/recorder/recorder.d.ts +38 -0
- package/dist/recorder/recorder.js +160 -1
- package/dist/recorder/session.d.ts +14 -1
- package/dist/recorder/session.js +36 -9
- package/dist/recorder/toolbar/controller.d.ts +15 -5
- package/dist/recorder/toolbar/controller.js +61 -7
- package/dist/recorder/toolbar/protocol.d.ts +10 -0
- package/dist/recorder/toolbar/source.d.ts +1 -1
- package/dist/recorder/toolbar/source.js +35 -2
- package/dist/recorder/toolbar/styles.d.ts +1 -1
- package/dist/recorder/toolbar/styles.js +31 -0
- package/dist/runner/project.js +2 -0
- package/dist/script/preload.js +65 -0
- package/dist/steps/edit.js +38 -3
- package/dist/steps/emit.d.ts +3 -1
- package/dist/steps/emit.js +19 -0
- package/dist/steps/files.d.ts +8 -0
- package/dist/steps/files.js +33 -0
- package/dist/steps/index.d.ts +3 -2
- package/dist/steps/index.js +2 -1
- package/dist/steps/model.d.ts +28 -0
- package/dist/steps/parse.js +79 -0
- package/dist/transport/bidi-connection.d.ts +6 -0
- package/dist/transport/bidi-connection.js +13 -0
- package/dist/types/browser.d.ts +1 -1
- package/dist/types/config.d.ts +7 -2
- package/package.json +3 -2
- package/dist/browser/file-paths.d.ts +0 -4
- package/dist/browser/file-paths.js +0 -18
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sent with session.new (Firefox) or as capabilities of the chromedriver session (Chrome). With "ignore" the browser
|
|
3
|
+
* leaves a JavaScript dialog open and reports it (`browsingContext.userPromptOpened`), so the framework decides how
|
|
4
|
+
* to answer instead of the browser.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SESSION_CAPABILITIES: {
|
|
7
|
+
alwaysMatch: {
|
|
8
|
+
unhandledPromptBehavior: {
|
|
9
|
+
default: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sent with session.new (Firefox) or as capabilities of the chromedriver session (Chrome). With "ignore" the browser
|
|
3
|
+
* leaves a JavaScript dialog open and reports it (`browsingContext.userPromptOpened`), so the framework decides how
|
|
4
|
+
* to answer instead of the browser.
|
|
5
|
+
*/
|
|
6
|
+
export const SESSION_CAPABILITIES = {
|
|
7
|
+
alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } },
|
|
8
|
+
};
|
package/dist/browsers/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { browsersCacheDir } from "./cache-dir.js";
|
|
2
2
|
export type { CachedBuild, Downloader } from "./downloader.js";
|
|
3
|
-
export { ENV_PATH, type Override } from "./overrides.js";
|
|
3
|
+
export { ENV_CHROMEDRIVER_PATH, ENV_PATH, type Override } from "./overrides.js";
|
|
4
4
|
export { LAUNCHABLE, MANAGED_BROWSERS, PINNED, isManagedBrowser, type ManagedBrowser, } from "./pins.js";
|
|
5
5
|
export { browserStatus, projectBrowser, projectExecutable, resolveBrowser, resolveExecutable, type ResolvedBrowser, } from "./resolve.js";
|
|
6
|
-
export { BrowserNotInstalledError, installBrowser, listInstalled, managedBuildDir, managedExecutable, uninstallBrowser, type BrowserStatus, type InstalledBuild, type StoreOptions, } from "./store.js";
|
|
6
|
+
export { BrowserNotInstalledError, installBrowser, listInstalled, managedBuildDir, managedChromedriver, managedExecutable, uninstallBrowser, type BrowserStatus, type InstalledBuild, type StoreOptions, } from "./store.js";
|
package/dist/browsers/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { browsersCacheDir } from "./cache-dir.js";
|
|
2
|
-
export { ENV_PATH } from "./overrides.js";
|
|
2
|
+
export { ENV_CHROMEDRIVER_PATH, ENV_PATH } from "./overrides.js";
|
|
3
3
|
export { LAUNCHABLE, MANAGED_BROWSERS, PINNED, isManagedBrowser, } from "./pins.js";
|
|
4
4
|
export { browserStatus, projectBrowser, projectExecutable, resolveBrowser, resolveExecutable, } from "./resolve.js";
|
|
5
|
-
export { BrowserNotInstalledError, installBrowser, listInstalled, managedBuildDir, managedExecutable, uninstallBrowser, } from "./store.js";
|
|
5
|
+
export { BrowserNotInstalledError, installBrowser, listInstalled, managedBuildDir, managedChromedriver, managedExecutable, uninstallBrowser, } from "./store.js";
|
|
@@ -12,6 +12,8 @@ export interface OverrideSources {
|
|
|
12
12
|
/** What relative `browserPaths` resolve from */
|
|
13
13
|
projectDir?: string;
|
|
14
14
|
}
|
|
15
|
+
/** Chrome is driven through a chromedriver of the same major version; this points a launch at another one */
|
|
16
|
+
export declare const ENV_CHROMEDRIVER_PATH = "SAMURAI_CHROMEDRIVER_PATH";
|
|
15
17
|
export declare const ENV_PATH: Record<ManagedBrowser, string>;
|
|
16
18
|
/** The env variable, else the config's `browserPaths`; undefined: use the managed build */
|
|
17
19
|
export declare function overrideOf(name: ManagedBrowser, { env, browserPaths, projectDir, }?: OverrideSources): Override | undefined;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
/** Chrome is driven through a chromedriver of the same major version; this points a launch at another one */
|
|
3
|
+
export const ENV_CHROMEDRIVER_PATH = "SAMURAI_CHROMEDRIVER_PATH";
|
|
2
4
|
export const ENV_PATH = {
|
|
3
5
|
firefox: "SAMURAI_FIREFOX_PATH",
|
|
4
6
|
chrome: "SAMURAI_CHROME_PATH",
|
package/dist/browsers/pins.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export declare const PINNED: {
|
|
|
11
11
|
};
|
|
12
12
|
export type ManagedBrowser = keyof typeof PINNED;
|
|
13
13
|
export declare const MANAGED_BROWSERS: ManagedBrowser[];
|
|
14
|
-
/** The managed browsers the framework can launch
|
|
14
|
+
/** The managed browsers the framework can launch (Chrome through its chromedriver) */
|
|
15
15
|
export declare const LAUNCHABLE: readonly ManagedBrowser[];
|
|
16
16
|
export declare function isManagedBrowser(name: string): name is ManagedBrowser;
|
package/dist/browsers/pins.js
CHANGED
|
@@ -7,8 +7,8 @@ export const PINNED = {
|
|
|
7
7
|
},
|
|
8
8
|
};
|
|
9
9
|
export const MANAGED_BROWSERS = Object.keys(PINNED);
|
|
10
|
-
/** The managed browsers the framework can launch
|
|
11
|
-
export const LAUNCHABLE = ["firefox"];
|
|
10
|
+
/** The managed browsers the framework can launch (Chrome through its chromedriver) */
|
|
11
|
+
export const LAUNCHABLE = ["firefox", "chrome"];
|
|
12
12
|
export function isManagedBrowser(name) {
|
|
13
13
|
return Object.prototype.hasOwnProperty.call(PINNED, name);
|
|
14
14
|
}
|
|
@@ -9,6 +9,8 @@ export interface ResolvedBrowser {
|
|
|
9
9
|
label: string;
|
|
10
10
|
/** The managed build's folder; undefined when an env or config path is used instead */
|
|
11
11
|
buildDir?: string;
|
|
12
|
+
/** Chrome only: the chromedriver that starts it and speaks BiDi for it */
|
|
13
|
+
driverPath?: string;
|
|
12
14
|
}
|
|
13
15
|
/** The browser a launch uses: the env path, else the config's `browserPaths`, else the pinned build. Never downloads */
|
|
14
16
|
export declare function resolveBrowser(name: ManagedBrowser, sources?: OverrideSources & StoreOptions): ResolvedBrowser;
|
package/dist/browsers/resolve.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { projectConfig, projectDir, readConfig } from "../config/config.js";
|
|
3
3
|
import logger from "../logger/index.js";
|
|
4
|
-
import { ENV_PATH, overrideOf } from "./overrides.js";
|
|
4
|
+
import { ENV_CHROMEDRIVER_PATH, ENV_PATH, overrideOf, } from "./overrides.js";
|
|
5
5
|
import { MANAGED_BROWSERS, PINNED } from "./pins.js";
|
|
6
|
-
import { managedBuildDir, managedExecutable, statusOf, } from "./store.js";
|
|
6
|
+
import { managedBuildDir, managedChromedriver, managedExecutable, statusOf, } from "./store.js";
|
|
7
7
|
/** The browser a launch uses: the env path, else the config's `browserPaths`, else the pinned build. Never downloads */
|
|
8
8
|
export function resolveBrowser(name, sources = {}) {
|
|
9
|
+
const driverPath = name === "chrome" ? resolveChromedriver(sources) : undefined;
|
|
9
10
|
const override = overrideOf(name, sources);
|
|
10
11
|
if (!override)
|
|
11
12
|
return {
|
|
12
13
|
executablePath: managedExecutable(name, sources),
|
|
13
14
|
label: PINNED[name].label,
|
|
14
15
|
buildDir: managedBuildDir(name, sources),
|
|
16
|
+
...(driverPath && { driverPath }),
|
|
15
17
|
};
|
|
16
18
|
if (!existsSync(override.path))
|
|
17
19
|
throw new Error(override.from === "env"
|
|
@@ -20,8 +22,18 @@ export function resolveBrowser(name, sources = {}) {
|
|
|
20
22
|
return {
|
|
21
23
|
executablePath: override.path,
|
|
22
24
|
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
25
|
+
...(driverPath && { driverPath }),
|
|
23
26
|
};
|
|
24
27
|
}
|
|
28
|
+
/** `SAMURAI_CHROMEDRIVER_PATH` (which must exist), else the chromedriver installed with the pinned Chrome */
|
|
29
|
+
function resolveChromedriver(sources) {
|
|
30
|
+
const fromEnv = (sources.env ?? process.env)[ENV_CHROMEDRIVER_PATH];
|
|
31
|
+
if (!fromEnv)
|
|
32
|
+
return managedChromedriver(sources);
|
|
33
|
+
if (!existsSync(fromEnv))
|
|
34
|
+
throw new Error(`${ENV_CHROMEDRIVER_PATH} points to a missing file: ${fromEnv}`);
|
|
35
|
+
return fromEnv;
|
|
36
|
+
}
|
|
25
37
|
/** The binary a launch uses; see `resolveBrowser` */
|
|
26
38
|
export function resolveExecutable(name, sources = {}) {
|
|
27
39
|
return resolveBrowser(name, sources).executablePath;
|
package/dist/browsers/store.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export declare function installBrowser(name: ManagedBrowser, options?: StoreOpti
|
|
|
44
44
|
export declare function statusOf(name: ManagedBrowser, options?: StoreOptions & OverrideSources): BrowserStatus;
|
|
45
45
|
/** The pinned build's executable; throws BrowserNotInstalledError when it isn't complete */
|
|
46
46
|
export declare function managedExecutable(name: ManagedBrowser, options?: StoreOptions): string;
|
|
47
|
+
/** The chromedriver installed with the pinned Chrome; throws BrowserNotInstalledError when Chrome's install isn't complete */
|
|
48
|
+
export declare function managedChromedriver(options?: StoreOptions): string;
|
|
47
49
|
/** The folder the pinned build of `name` is, or would be, installed in */
|
|
48
50
|
export declare function managedBuildDir(name: ManagedBrowser, options?: StoreOptions): string;
|
|
49
51
|
/** Removes a build (default: the pinned one); Chrome takes its chromedriver along */
|
package/dist/browsers/store.js
CHANGED
|
@@ -127,6 +127,14 @@ export function managedExecutable(name, options = {}) {
|
|
|
127
127
|
throw new BrowserNotInstalledError(name, status.label);
|
|
128
128
|
return status.executablePath;
|
|
129
129
|
}
|
|
130
|
+
/** The chromedriver installed with the pinned Chrome; throws BrowserNotInstalledError when Chrome's install isn't complete */
|
|
131
|
+
export function managedChromedriver(options = {}) {
|
|
132
|
+
const { cacheDir, downloader } = store(options);
|
|
133
|
+
const status = statusOf("chrome", { ...options, env: {} });
|
|
134
|
+
if (status.state !== "installed")
|
|
135
|
+
throw new BrowserNotInstalledError("chrome", status.label);
|
|
136
|
+
return downloader.executablePath(cacheDir, "chromedriver", PINNED.chrome.buildId);
|
|
137
|
+
}
|
|
130
138
|
/** The folder the pinned build of `name` is, or would be, installed in */
|
|
131
139
|
export function managedBuildDir(name, options = {}) {
|
|
132
140
|
const { cacheDir, downloader } = store(options);
|
package/dist/cli/main.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const USAGE = "samurai: browser tests over WebDriver BiDi\n\nUsage:\n samurai run [options] Run the tests (the default command)\n samurai list [options] List the tests without running them\n samurai record <spec> Open a browser and record what you do into a spec file\n samurai init [folder] Scaffold a project: config, an example test, .env.example\n samurai flow <command> List, check and run the flows in ./flows (see below)\n samurai browsers [command] List, install and remove the browsers SAMURAI launches\n\nOptions for run and list:\n --env <name> Environment to run against (or SAMURAI_ENV)\n --file <spec> Only this spec file; repeatable\n --grep <text> Only tests whose full name contains the text\n --group <name> Only the tests of this group of the config's groups\n --json Machine-readable output: JSON lines for run, one array for list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless, --no-headless Run the browser without a window, or with one.\n Default: the config's use.headless, else headless when CI is set\n --port <n> Browser debugging port (default 9223)\n\nOptions for record (also --env, --port, --json):\n --new <title> Add a test with this title (creates the spec file if needed)\n --test <name|index> Record into this test; not needed when the file has one\n --at <n> Insert from step n (default: after the last step)\n --url <url> Page to start on (default: the environment's baseURL)\n Stop with Ctrl+C or by closing the browser window.\n\n -h, --help Show this help\n -v, --version Show the version\n\nExit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.\n\nsamurai flow:\n list The flows in ./flows\n check [<flow>...] Check flows without running them\n run <flow>... | --all Run flows; each writes result/flows/<id>.json\n Options for run: --env, --headless, --timeout, --expect-timeout, --port, --json, --allow-unsupported\n Run `samurai flow` alone for the details.\n\nsamurai browsers:\n list The pinned browsers and every build in the cache (the default)\n install [firefox|chrome|all]\n Download the pinned builds (default: all)\n uninstall <name> [--build <id>] | --unused\n\nRun it in the project folder, the one holding samurai.config.ts.\n";
|
|
1
|
+
export declare const USAGE = "samurai: browser tests over WebDriver BiDi\n\nUsage:\n samurai run [options] Run the tests (the default command)\n samurai list [options] List the tests without running them\n samurai record <spec> Open a browser and record what you do into a spec file\n samurai init [folder] Scaffold a project: config, an example test, .env.example\n samurai flow <command> List, check and run the flows in ./flows (see below)\n samurai browsers [command] List, install and remove the browsers SAMURAI launches\n\nOptions for run and list:\n --env <name> Environment to run against (or SAMURAI_ENV)\n --file <spec> Only this spec file; repeatable\n --grep <text> Only tests whose full name contains the text\n --group <name> Only the tests of this group of the config's groups\n --json Machine-readable output: JSON lines for run, one array for list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless, --no-headless Run the browser without a window, or with one.\n Default: the config's use.headless, else headless when CI is set\n --port <n> Browser debugging port (default 9223)\n\nOptions for record (also --env, --port, --json, --headless, --no-headless):\n --new <title> Add a test with this title (creates the spec file if needed)\n --test <name|index> Record into this test; not needed when the file has one\n --at <n> Insert from step n (default: after the last step)\n --url <url> Page to start on (default: the environment's baseURL)\n Stop with Ctrl+C or by closing the browser window.\n\n -h, --help Show this help\n -v, --version Show the version\n\nExit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.\n\nsamurai flow:\n list The flows in ./flows\n check [<flow>...] Check flows without running them\n run <flow>... | --all Run flows; each writes result/flows/<id>.json\n Options for run: --env, --headless, --timeout, --expect-timeout, --port, --json, --allow-unsupported\n Run `samurai flow` alone for the details.\n\nsamurai browsers:\n list The pinned browsers and every build in the cache (the default)\n install [firefox|chrome|all]\n Download the pinned builds (default: all)\n uninstall <name> [--build <id>] | --unused\n\nRun it in the project folder, the one holding samurai.config.ts.\n";
|
|
2
2
|
export interface Io {
|
|
3
3
|
out: (text: string) => void;
|
|
4
4
|
err: (text: string) => void;
|
package/dist/cli/main.js
CHANGED
|
@@ -31,7 +31,7 @@ Options for run:
|
|
|
31
31
|
Default: the config's use.headless, else headless when CI is set
|
|
32
32
|
--port <n> Browser debugging port (default 9223)
|
|
33
33
|
|
|
34
|
-
Options for record (also --env, --port, --json):
|
|
34
|
+
Options for record (also --env, --port, --json, --headless, --no-headless):
|
|
35
35
|
--new <title> Add a test with this title (creates the spec file if needed)
|
|
36
36
|
--test <name|index> Record into this test; not needed when the file has one
|
|
37
37
|
--at <n> Insert from step n (default: after the last step)
|
|
@@ -114,6 +114,7 @@ export async function main(argv, io) {
|
|
|
114
114
|
...(values.new !== undefined && { create: values.new }),
|
|
115
115
|
...(at !== undefined && { at }),
|
|
116
116
|
...(values.url !== undefined && { url: values.url }),
|
|
117
|
+
...(flags.headless !== undefined && { headless: flags.headless }),
|
|
117
118
|
...(flags.port !== undefined && { port: flags.port }),
|
|
118
119
|
...(io.signal && { signal: io.signal }),
|
|
119
120
|
onStarted: () => {
|
|
@@ -132,13 +133,18 @@ export async function main(argv, io) {
|
|
|
132
133
|
io.out(state === "paused" ? "Paused\n" : "Recording\n");
|
|
133
134
|
}
|
|
134
135
|
},
|
|
136
|
+
onMissingFile: (_file, text) => {
|
|
137
|
+
if (!values.json)
|
|
138
|
+
io.out(`${text}\n`);
|
|
139
|
+
},
|
|
135
140
|
onEvent: (event) => {
|
|
136
141
|
if (values.json)
|
|
137
142
|
return void io.out(`${JSON.stringify(event)}\n`);
|
|
138
|
-
if (event.op === "
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
if (event.op === "remove") {
|
|
144
|
+
count--;
|
|
145
|
+
return void io.out(` - step ${event.index + 1} (the click that opened the file picker)\n`);
|
|
146
|
+
}
|
|
147
|
+
io.out(` ${event.op === "insert" ? "+" : "~"} ${stepToSource(event.step)}\n`);
|
|
142
148
|
if (event.op === "insert")
|
|
143
149
|
count++;
|
|
144
150
|
},
|
|
@@ -148,6 +154,15 @@ export async function main(argv, io) {
|
|
|
148
154
|
if (result.unsetSecrets.length > 0) {
|
|
149
155
|
io.out(`Set ${result.unsetSecrets.map((name) => `SAMURAI_SECRET_${name}`).join(", ")} (environment variable or .env.<environment>) before running it.\n`);
|
|
150
156
|
}
|
|
157
|
+
if (result.missingFiles.length > 0) {
|
|
158
|
+
io.out(`Add ${result.missingFiles.map((f) => f.path).join(", ")} before running it (or use generatedFile).\n`);
|
|
159
|
+
}
|
|
160
|
+
const hasFiles = result.files.length + result.missingFiles.length > 0;
|
|
161
|
+
const folder = result.filesFolder === "" ? "." : result.filesFolder;
|
|
162
|
+
if (hasFiles && result.filesIgnored)
|
|
163
|
+
io.out(`${folder}/ is ignored by git; CI won't have these files.\n`);
|
|
164
|
+
if (hasFiles && result.firstFiles)
|
|
165
|
+
io.out(`Recorded files are stored in ${folder}/ and committed with your tests.\n`);
|
|
151
166
|
}
|
|
152
167
|
return 0;
|
|
153
168
|
}
|
|
@@ -3,6 +3,7 @@ import type { BrowsingContext, ElementRectangle } from "../types/bidi-modules/br
|
|
|
3
3
|
import { type HelperInstaller } from "../script/helpers.js";
|
|
4
4
|
import { ElementHandle } from "../script/element-handle.js";
|
|
5
5
|
import { type ElementScreenshotOptions } from "../browser/screenshot.js";
|
|
6
|
+
import { type FileInput } from "../browser/generated-files.js";
|
|
6
7
|
import { type WaitForState } from "./element-state.js";
|
|
7
8
|
import { type Selector, type TextOptions } from "./selector.js";
|
|
8
9
|
export type { WaitForState } from "./element-state.js";
|
|
@@ -116,14 +117,25 @@ export default class Locator {
|
|
|
116
117
|
*/
|
|
117
118
|
clickRect(rect: Pick<ElementRectangle, "x" | "y" | "width" | "height">): Promise<void>;
|
|
118
119
|
/**
|
|
119
|
-
* Sets the files of an `<input type=file
|
|
120
|
-
*
|
|
120
|
+
* Sets the files of an `<input type=file>`; `[]` clears it. Paths are relative to the project folder (absolute
|
|
121
|
+
* ones are used as they are); `generatedFile(…)` writes a file of a given size when the step runs.
|
|
121
122
|
* Waits for the input to be attached and enabled; it need not be visible, file inputs are often hidden behind a label.
|
|
122
123
|
* @example
|
|
123
|
-
* await page.
|
|
124
|
-
* await page.
|
|
124
|
+
* await page.getByLabel("Avatar").setInputFiles("files/avatar.png");
|
|
125
|
+
* await page.getByLabel("Upload").setInputFiles(generatedFile({ name: "huge.pdf", size: "60 MB" }));
|
|
125
126
|
*/
|
|
126
|
-
setInputFiles(files:
|
|
127
|
+
setInputFiles(files: FileInput | FileInput[], options?: {
|
|
128
|
+
timeout?: number;
|
|
129
|
+
}): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Drops files on the element, as dragging them from the desktop does: `dragenter`, `dragover` and `drop`
|
|
132
|
+
* with a `DataTransfer` holding the files. Takes what `setInputFiles` takes. The events are synthetic
|
|
133
|
+
* (`isTrusted` is false); a page that only accepts trusted drops ignores them.
|
|
134
|
+
* Waits for the element to be attached and visible.
|
|
135
|
+
* @example
|
|
136
|
+
* await page.getByTestId("dropzone").dropFiles(["files/a.png", "files/b.png"]);
|
|
137
|
+
*/
|
|
138
|
+
dropFiles(files: FileInput | FileInput[], options?: {
|
|
127
139
|
timeout?: number;
|
|
128
140
|
}): Promise<void>;
|
|
129
141
|
/**
|
package/dist/locator/locator.js
CHANGED
|
@@ -3,7 +3,7 @@ import { callFunction, ScriptError } from "../script/call-function.js";
|
|
|
3
3
|
import { CALL_PROBE, HELPER_SANDBOX, HELPERS_MISSING, } from "../script/helpers.js";
|
|
4
4
|
import { ElementHandle } from "../script/element-handle.js";
|
|
5
5
|
import { takeScreenshot, } from "../browser/screenshot.js";
|
|
6
|
-
import {
|
|
6
|
+
import { isGeneratedFile, resolveFileInputs, } from "../browser/generated-files.js";
|
|
7
7
|
import { resolveTimeout, waitUntil, WaitTimeoutError, } from "../wait/wait-until.js";
|
|
8
8
|
import { ActionTimeoutError } from "./action-timeout-error.js";
|
|
9
9
|
import { allPass, DETACHED_STATE, evaluateChecks, parseElementState, PROBE_ELEMENT, } from "./element-state.js";
|
|
@@ -27,6 +27,31 @@ const FILE_INPUT_INFO = `(el) => ({
|
|
|
27
27
|
multiple: Boolean(el.multiple),
|
|
28
28
|
})`;
|
|
29
29
|
const FILE_INPUT_CHECKS = ["attached", "enabled"];
|
|
30
|
+
const DROP_CHECKS = ["attached", "visible"];
|
|
31
|
+
/** The sandbox `dropFiles` builds its files in */
|
|
32
|
+
const DROP_SANDBOX = "samurai-drop";
|
|
33
|
+
const DROP_KEY = `globalThis[Symbol.for("samurai.drop")]`;
|
|
34
|
+
const DROP_INPUT = `() => {
|
|
35
|
+
const input = document.createElement("input");
|
|
36
|
+
input.type = "file";
|
|
37
|
+
input.multiple = true;
|
|
38
|
+
${DROP_KEY} = input;
|
|
39
|
+
return input;
|
|
40
|
+
}`;
|
|
41
|
+
/** Fires dragenter, dragover and drop at the element's centre with the input's files (retyped where asked) */
|
|
42
|
+
const DROP_EVENTS = `(el, types) => {
|
|
43
|
+
const input = ${DROP_KEY};
|
|
44
|
+
delete ${DROP_KEY};
|
|
45
|
+
const data = new DataTransfer();
|
|
46
|
+
Array.from(input.files).forEach((file, i) => {
|
|
47
|
+
data.items.add(types[i] ? new File([file], file.name, { type: types[i], lastModified: file.lastModified }) : file);
|
|
48
|
+
});
|
|
49
|
+
const box = el.getBoundingClientRect();
|
|
50
|
+
const at = { clientX: box.left + box.width / 2, clientY: box.top + box.height / 2 };
|
|
51
|
+
for (const type of ["dragenter", "dragover", "drop"]) {
|
|
52
|
+
el.dispatchEvent(new DragEvent(type, Object.assign({ bubbles: true, cancelable: true, composed: true, dataTransfer: data }, at)));
|
|
53
|
+
}
|
|
54
|
+
}`;
|
|
30
55
|
const SCREENSHOT_CHECKS = ["attached", "visible"];
|
|
31
56
|
const CLICK_CHECKS = [
|
|
32
57
|
"attached",
|
|
@@ -63,6 +88,16 @@ const NAMED_KEYS = {
|
|
|
63
88
|
};
|
|
64
89
|
/** Unicode code point WebDriver uses for the Backspace key */
|
|
65
90
|
const BACKSPACE = "";
|
|
91
|
+
/**
|
|
92
|
+
* Chrome answers a command sent while the page swaps documents with an unknown error that says the context
|
|
93
|
+
* is gone for the moment; the same call a moment later works.
|
|
94
|
+
*/
|
|
95
|
+
const DOCUMENT_SWAP = /Cannot find context with specified id|execution contexts? (was |were )?(cleared|destroyed)/i;
|
|
96
|
+
function isDocumentSwap(err) {
|
|
97
|
+
return (err instanceof BiDiError &&
|
|
98
|
+
err.code === "unknown error" &&
|
|
99
|
+
DOCUMENT_SWAP.test(err.message));
|
|
100
|
+
}
|
|
66
101
|
export default class Locator {
|
|
67
102
|
/**
|
|
68
103
|
* `selector` is an xpath, or a chain of selector steps that each search inside the previous step's matches.
|
|
@@ -191,7 +226,15 @@ export default class Locator {
|
|
|
191
226
|
nodes = picked ? [picked] : [];
|
|
192
227
|
}
|
|
193
228
|
else {
|
|
194
|
-
|
|
229
|
+
try {
|
|
230
|
+
nodes = await this.locateStep(selector, index > 0, nodes, index === this.chain.length - 1 ? max : undefined);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
// The page is swapping documents: nothing matches right now, and the waits that poll try again
|
|
234
|
+
if (isDocumentSwap(err))
|
|
235
|
+
return [];
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
195
238
|
}
|
|
196
239
|
if (nodes.length === 0)
|
|
197
240
|
return [];
|
|
@@ -310,7 +353,8 @@ export default class Locator {
|
|
|
310
353
|
}
|
|
311
354
|
catch (err) {
|
|
312
355
|
// The document changed between the search and the probe
|
|
313
|
-
if (err instanceof BiDiError && err.code === "no such node")
|
|
356
|
+
if ((err instanceof BiDiError && err.code === "no such node") ||
|
|
357
|
+
isDocumentSwap(err))
|
|
314
358
|
return DETACHED_STATE;
|
|
315
359
|
throw err;
|
|
316
360
|
}
|
|
@@ -410,17 +454,15 @@ export default class Locator {
|
|
|
410
454
|
});
|
|
411
455
|
}
|
|
412
456
|
/**
|
|
413
|
-
* Sets the files of an `<input type=file
|
|
414
|
-
*
|
|
457
|
+
* Sets the files of an `<input type=file>`; `[]` clears it. Paths are relative to the project folder (absolute
|
|
458
|
+
* ones are used as they are); `generatedFile(…)` writes a file of a given size when the step runs.
|
|
415
459
|
* Waits for the input to be attached and enabled; it need not be visible, file inputs are often hidden behind a label.
|
|
416
460
|
* @example
|
|
417
|
-
* await page.
|
|
418
|
-
* await page.
|
|
461
|
+
* await page.getByLabel("Avatar").setInputFiles("files/avatar.png");
|
|
462
|
+
* await page.getByLabel("Upload").setInputFiles(generatedFile({ name: "huge.pdf", size: "60 MB" }));
|
|
419
463
|
*/
|
|
420
464
|
async setInputFiles(files, options) {
|
|
421
|
-
const paths =
|
|
422
|
-
for (const file of paths)
|
|
423
|
-
await assertIsFile(file, "setInputFiles");
|
|
465
|
+
const paths = await resolveFileInputs(files, "setInputFiles");
|
|
424
466
|
await this.waitForActionable("setInputFiles", FILE_INPUT_CHECKS, { scroll: false, hitTest: false }, options?.timeout === undefined ? undefined : { timeout: options.timeout });
|
|
425
467
|
const missing = () => new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
|
|
426
468
|
const el = await this.resolveOne();
|
|
@@ -450,6 +492,31 @@ export default class Locator {
|
|
|
450
492
|
files: paths,
|
|
451
493
|
});
|
|
452
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Drops files on the element, as dragging them from the desktop does: `dragenter`, `dragover` and `drop`
|
|
497
|
+
* with a `DataTransfer` holding the files. Takes what `setInputFiles` takes. The events are synthetic
|
|
498
|
+
* (`isTrusted` is false); a page that only accepts trusted drops ignores them.
|
|
499
|
+
* Waits for the element to be attached and visible.
|
|
500
|
+
* @example
|
|
501
|
+
* await page.getByTestId("dropzone").dropFiles(["files/a.png", "files/b.png"]);
|
|
502
|
+
*/
|
|
503
|
+
async dropFiles(files, options) {
|
|
504
|
+
const list = Array.isArray(files) ? files : [files];
|
|
505
|
+
const paths = await resolveFileInputs(list, "dropFiles");
|
|
506
|
+
const types = list.map((file) => isGeneratedFile(file) ? (file.type ?? "") : "");
|
|
507
|
+
await this.waitForActionable("dropFiles", DROP_CHECKS, { scroll: true, hitTest: false }, options?.timeout === undefined ? undefined : { timeout: options.timeout });
|
|
508
|
+
const el = await this.resolveOne();
|
|
509
|
+
if (!el)
|
|
510
|
+
throw new Error(`Failed to locate element ${this.selector}. Details: no element matches`);
|
|
511
|
+
// A file input of our own, made in a sandbox and never attached: the page doesn't see it
|
|
512
|
+
const input = await this.call(DROP_INPUT, [], DROP_SANDBOX);
|
|
513
|
+
await this.biDiConnector.send("input.setFiles", {
|
|
514
|
+
context: this.contextId,
|
|
515
|
+
element: { sharedId: input.sharedId },
|
|
516
|
+
files: paths,
|
|
517
|
+
});
|
|
518
|
+
await this.call(DROP_EVENTS, [el, types], DROP_SANDBOX);
|
|
519
|
+
}
|
|
453
520
|
/**
|
|
454
521
|
* Takes a screenshot of just this element, after scrolling it into view and waiting until it is attached and
|
|
455
522
|
* visible. Returns the image (also written to `options.path`).
|
|
@@ -12,6 +12,7 @@ export declare class ResponseBodyUnavailableError extends Error {
|
|
|
12
12
|
export declare class DataCollector {
|
|
13
13
|
private connector;
|
|
14
14
|
private id;
|
|
15
|
+
private maxBodySize;
|
|
15
16
|
private disposed;
|
|
16
17
|
private constructor();
|
|
17
18
|
static start(connector: BiDiConnector, contextId: string, options?: {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { BiDiError } from "../transport/bidi-error.js";
|
|
2
2
|
/** How large a response body may be to be kept: 10 MiB */
|
|
3
3
|
const DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024;
|
|
4
|
+
/**
|
|
5
|
+
* Chrome counts the headers of a response in the size it compares with the collector's limit (a 14-byte body
|
|
6
|
+
* arrives as about 200 bytes), Firefox counts the body. Chrome's limit is raised by this much and the body is
|
|
7
|
+
* measured after it is read.
|
|
8
|
+
*/
|
|
9
|
+
const CHROME_HEADER_ALLOWANCE = 64 * 1024;
|
|
4
10
|
/** The body of a response cannot be read */
|
|
5
11
|
export class ResponseBodyUnavailableError extends Error {
|
|
6
12
|
constructor(url, reason) {
|
|
@@ -15,18 +21,22 @@ export class ResponseBodyUnavailableError extends Error {
|
|
|
15
21
|
* started. This registers one for a page and reads bodies through it.
|
|
16
22
|
*/
|
|
17
23
|
export class DataCollector {
|
|
18
|
-
constructor(connector, id) {
|
|
24
|
+
constructor(connector, id, maxBodySize) {
|
|
19
25
|
this.connector = connector;
|
|
20
26
|
this.id = id;
|
|
27
|
+
this.maxBodySize = maxBodySize;
|
|
21
28
|
this.disposed = false;
|
|
22
29
|
}
|
|
23
30
|
static async start(connector, contextId, options = {}) {
|
|
31
|
+
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
24
32
|
const { collector } = await connector.send("network.addDataCollector", {
|
|
25
33
|
dataTypes: ["response"],
|
|
26
|
-
maxEncodedDataSize:
|
|
34
|
+
maxEncodedDataSize: connector.browserName === "chrome"
|
|
35
|
+
? maxBodySize + CHROME_HEADER_ALLOWANCE
|
|
36
|
+
: maxBodySize,
|
|
27
37
|
contexts: [contextId],
|
|
28
38
|
});
|
|
29
|
-
return new DataCollector(connector, collector);
|
|
39
|
+
return new DataCollector(connector, collector, maxBodySize);
|
|
30
40
|
}
|
|
31
41
|
/** The body of the response to request `requestId`; `url` only names it in errors */
|
|
32
42
|
async read(requestId, url) {
|
|
@@ -36,7 +46,13 @@ export class DataCollector {
|
|
|
36
46
|
request: requestId,
|
|
37
47
|
collector: this.id,
|
|
38
48
|
});
|
|
39
|
-
|
|
49
|
+
const body = bytes.type === "base64"
|
|
50
|
+
? Buffer.from(bytes.value, "base64")
|
|
51
|
+
: Buffer.from(bytes.value, "utf8");
|
|
52
|
+
if (body.length > this.maxBodySize) {
|
|
53
|
+
throw new ResponseBodyUnavailableError(url, `the response is larger than maxBodySize (${this.maxBodySize} bytes)`);
|
|
54
|
+
}
|
|
55
|
+
return body;
|
|
40
56
|
}
|
|
41
57
|
catch (err) {
|
|
42
58
|
if (err instanceof BiDiError && err.code === "no such network data") {
|
|
@@ -67,7 +83,9 @@ export class DataCollector {
|
|
|
67
83
|
return;
|
|
68
84
|
this.disposed = true;
|
|
69
85
|
try {
|
|
70
|
-
await this.connector.send("network.removeDataCollector", {
|
|
86
|
+
await this.connector.send("network.removeDataCollector", {
|
|
87
|
+
collector: this.id,
|
|
88
|
+
});
|
|
71
89
|
}
|
|
72
90
|
catch (err) {
|
|
73
91
|
if (!(err instanceof BiDiError && err.code === "no such network collector"))
|
|
@@ -54,6 +54,8 @@ export declare class NetworkTracker {
|
|
|
54
54
|
private connector;
|
|
55
55
|
private events;
|
|
56
56
|
private active;
|
|
57
|
+
/** The context each request in flight belongs to, by the same key */
|
|
58
|
+
private contexts;
|
|
57
59
|
/** Requests that may still be the source of a redirect, by hop key */
|
|
58
60
|
private hops;
|
|
59
61
|
private waiters;
|
|
@@ -73,7 +75,7 @@ export declare class NetworkTracker {
|
|
|
73
75
|
* Resolves once no request has been in flight for `idleTime` ms. A quiet page resolves after `idleTime`.
|
|
74
76
|
* Rejects with NetworkIdleTimeoutError after `timeout` ms, or with "page closed" when disposed.
|
|
75
77
|
*/
|
|
76
|
-
waitForIdle({ idleTime, timeout }: {
|
|
78
|
+
waitForIdle({ idleTime, timeout, }: {
|
|
77
79
|
idleTime: number;
|
|
78
80
|
timeout: number;
|
|
79
81
|
}): Promise<void>;
|
|
@@ -85,6 +87,11 @@ export declare class NetworkTracker {
|
|
|
85
87
|
private onBeforeRequestSent;
|
|
86
88
|
private onResponseCompleted;
|
|
87
89
|
private onFetchError;
|
|
90
|
+
/**
|
|
91
|
+
* A document replaced another: the requests of the old one that are still in flight are over. Firefox reports each
|
|
92
|
+
* as failed (NS_BINDING_ABORTED); Chrome says nothing, which would keep the page "busy" for good.
|
|
93
|
+
*/
|
|
94
|
+
private onNavigationCommitted;
|
|
88
95
|
/** A listener that throws must not stop the tracker or the other listeners */
|
|
89
96
|
private emit;
|
|
90
97
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
import logger from "../logger/index.js";
|
|
3
3
|
import { WaitTimeoutError } from "../wait/wait-until.js";
|
|
4
|
+
/** The error text of a request that the page left before it finished, when the browser says none */
|
|
5
|
+
const ABORTED_BY_NAVIGATION = "aborted by navigation";
|
|
4
6
|
/** How many URLs a timeout message names */
|
|
5
7
|
const URLS_IN_MESSAGE = 5;
|
|
6
8
|
/** waitForIdle ran out of time with requests still in flight; `last` holds (up to five of) their URLs */
|
|
@@ -24,6 +26,8 @@ export class NetworkTracker {
|
|
|
24
26
|
this.connector = connector;
|
|
25
27
|
this.events = new EventEmitter();
|
|
26
28
|
this.active = new Map();
|
|
29
|
+
/** The context each request in flight belongs to, by the same key */
|
|
30
|
+
this.contexts = new Map();
|
|
27
31
|
/** Requests that may still be the source of a redirect, by hop key */
|
|
28
32
|
this.hops = new Map();
|
|
29
33
|
this.waiters = new Set();
|
|
@@ -41,6 +45,8 @@ export class NetworkTracker {
|
|
|
41
45
|
return;
|
|
42
46
|
this.hops.set(key, request);
|
|
43
47
|
this.active.set(key, request.url);
|
|
48
|
+
if (params.context)
|
|
49
|
+
this.contexts.set(key, params.context);
|
|
44
50
|
this.emit("request", request);
|
|
45
51
|
this.events.emit("change");
|
|
46
52
|
};
|
|
@@ -51,6 +57,7 @@ export class NetworkTracker {
|
|
|
51
57
|
const request = this.hops.get(key);
|
|
52
58
|
if (!this.active.delete(key) || !request)
|
|
53
59
|
return;
|
|
60
|
+
this.contexts.delete(key);
|
|
54
61
|
const { response } = params;
|
|
55
62
|
// A redirect response keeps its request around: the next hop links back to it
|
|
56
63
|
if (response.status < 300 || response.status >= 400)
|
|
@@ -72,10 +79,34 @@ export class NetworkTracker {
|
|
|
72
79
|
const request = this.hops.get(key);
|
|
73
80
|
if (!this.active.delete(key) || !request)
|
|
74
81
|
return;
|
|
82
|
+
this.contexts.delete(key);
|
|
75
83
|
this.hops.delete(key);
|
|
76
84
|
this.emit("requestfailed", Object.assign(request, { errorText: params.errorText }));
|
|
77
85
|
this.events.emit("change");
|
|
78
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* A document replaced another: the requests of the old one that are still in flight are over. Firefox reports each
|
|
89
|
+
* as failed (NS_BINDING_ABORTED); Chrome says nothing, which would keep the page "busy" for good.
|
|
90
|
+
*/
|
|
91
|
+
this.onNavigationCommitted = (params) => {
|
|
92
|
+
if (!this.owns(params.context))
|
|
93
|
+
return;
|
|
94
|
+
let changed = false;
|
|
95
|
+
for (const [key, requestContext] of [...this.contexts]) {
|
|
96
|
+
const request = this.hops.get(key);
|
|
97
|
+
if (!request || request.navigation === params.navigation)
|
|
98
|
+
continue;
|
|
99
|
+
if (!this.tree.isWithin(requestContext, params.context))
|
|
100
|
+
continue;
|
|
101
|
+
this.contexts.delete(key);
|
|
102
|
+
this.active.delete(key);
|
|
103
|
+
this.hops.delete(key);
|
|
104
|
+
this.emit("requestfailed", Object.assign(request, { errorText: ABORTED_BY_NAVIGATION }));
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
if (changed)
|
|
108
|
+
this.events.emit("change");
|
|
109
|
+
};
|
|
79
110
|
}
|
|
80
111
|
static async start(connector, tree, contextId, options = {}) {
|
|
81
112
|
const tracker = new NetworkTracker(tree, contextId, options.ignore ?? (() => false), connector);
|
|
@@ -83,11 +114,13 @@ export class NetworkTracker {
|
|
|
83
114
|
connector.onEvent("network.beforeRequestSent", tracker.onBeforeRequestSent);
|
|
84
115
|
connector.onEvent("network.responseCompleted", tracker.onResponseCompleted);
|
|
85
116
|
connector.onEvent("network.fetchError", tracker.onFetchError);
|
|
117
|
+
connector.onEvent("browsingContext.navigationCommitted", tracker.onNavigationCommitted);
|
|
86
118
|
try {
|
|
87
119
|
tracker.subscription = await connector.subscribe([
|
|
88
120
|
"network.beforeRequestSent",
|
|
89
121
|
"network.responseCompleted",
|
|
90
122
|
"network.fetchError",
|
|
123
|
+
"browsingContext.navigationCommitted",
|
|
91
124
|
]);
|
|
92
125
|
}
|
|
93
126
|
catch (err) {
|
|
@@ -118,10 +151,12 @@ export class NetworkTracker {
|
|
|
118
151
|
* Resolves once no request has been in flight for `idleTime` ms. A quiet page resolves after `idleTime`.
|
|
119
152
|
* Rejects with NetworkIdleTimeoutError after `timeout` ms, or with "page closed" when disposed.
|
|
120
153
|
*/
|
|
121
|
-
waitForIdle({ idleTime, timeout }) {
|
|
154
|
+
waitForIdle({ idleTime, timeout, }) {
|
|
122
155
|
return new Promise((resolve, reject) => {
|
|
123
156
|
let idleTimer;
|
|
124
|
-
const waiter = {
|
|
157
|
+
const waiter = {
|
|
158
|
+
reject: (reason) => finish(() => reject(reason)),
|
|
159
|
+
};
|
|
125
160
|
const finish = (settle) => {
|
|
126
161
|
clearTimeout(idleTimer);
|
|
127
162
|
clearTimeout(deadline);
|
|
@@ -147,6 +182,7 @@ export class NetworkTracker {
|
|
|
147
182
|
this.disposed = true;
|
|
148
183
|
this.removeListeners();
|
|
149
184
|
this.active.clear();
|
|
185
|
+
this.contexts.clear();
|
|
150
186
|
this.hops.clear();
|
|
151
187
|
[...this.waiters].forEach((waiter) => waiter.reject(new Error("page closed")));
|
|
152
188
|
await this.subscription?.unsubscribe();
|
|
@@ -155,6 +191,7 @@ export class NetworkTracker {
|
|
|
155
191
|
this.connector.offEvent("network.beforeRequestSent", this.onBeforeRequestSent);
|
|
156
192
|
this.connector.offEvent("network.responseCompleted", this.onResponseCompleted);
|
|
157
193
|
this.connector.offEvent("network.fetchError", this.onFetchError);
|
|
194
|
+
this.connector.offEvent("browsingContext.navigationCommitted", this.onNavigationCommitted);
|
|
158
195
|
}
|
|
159
196
|
owns(context) {
|
|
160
197
|
return context !== null && this.tree.isWithin(context, this.contextId);
|
|
@@ -190,7 +227,9 @@ export function toRequest(data, navigation, isBlocked, redirectedFrom) {
|
|
|
190
227
|
export function toHeaders(headers) {
|
|
191
228
|
const result = {};
|
|
192
229
|
for (const { name, value } of headers) {
|
|
193
|
-
const text = value.type === "base64"
|
|
230
|
+
const text = value.type === "base64"
|
|
231
|
+
? Buffer.from(value.value, "base64").toString("utf8")
|
|
232
|
+
: value.value;
|
|
194
233
|
const key = name.toLowerCase();
|
|
195
234
|
result[key] = key in result ? `${result[key]}, ${text}` : text;
|
|
196
235
|
}
|