@rehearsal-labs/retest 0.2.0 → 0.3.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 +6 -5
- package/dist/api/source-location.d.ts.map +1 -1
- package/dist/api/source-location.js +3 -1
- package/dist/browser/builds.d.ts +35 -6
- package/dist/browser/builds.d.ts.map +1 -1
- package/dist/browser/builds.js +115 -16
- package/dist/browser/cdp/transport.d.ts.map +1 -1
- package/dist/browser/cdp/transport.js +4 -4
- package/dist/browser/chromium-process.d.ts +23 -0
- package/dist/browser/chromium-process.d.ts.map +1 -1
- package/dist/browser/chromium-process.js +78 -13
- package/dist/browser/electron.d.ts +1 -1
- package/dist/browser/electron.d.ts.map +1 -1
- package/dist/browser/electron.js +6 -3
- package/dist/browser/executables.d.ts +16 -1
- package/dist/browser/executables.d.ts.map +1 -1
- package/dist/browser/executables.js +50 -18
- package/dist/browser/firefox/accessible-names.d.ts +5 -4
- package/dist/browser/firefox/accessible-names.d.ts.map +1 -1
- package/dist/browser/firefox/accessible-names.js +11 -6
- package/dist/browser/firefox/bridge.js +3 -2
- package/dist/browser/firefox/executable.d.ts +14 -14
- package/dist/browser/firefox/executable.d.ts.map +1 -1
- package/dist/browser/firefox/executable.js +29 -21
- package/dist/browser/firefox/launch.js +1 -1
- package/dist/browser/firefox/navigation.d.ts +5 -2
- package/dist/browser/firefox/navigation.d.ts.map +1 -1
- package/dist/browser/firefox/navigation.js +48 -14
- package/dist/browser/firefox/orphans.d.ts.map +1 -1
- package/dist/browser/firefox/orphans.js +34 -2
- package/dist/browser/firefox/page.d.ts.map +1 -1
- package/dist/browser/firefox/page.js +113 -19
- package/dist/browser/firefox/process.d.ts +20 -1
- package/dist/browser/firefox/process.d.ts.map +1 -1
- package/dist/browser/firefox/process.js +124 -22
- package/dist/browser/firefox/profile.d.ts +5 -0
- package/dist/browser/firefox/profile.d.ts.map +1 -1
- package/dist/browser/firefox/profile.js +17 -1
- package/dist/browser/firefox/storage.d.ts.map +1 -1
- package/dist/browser/firefox/storage.js +4 -2
- package/dist/browser/firefox/subscriptions.d.ts +26 -0
- package/dist/browser/firefox/subscriptions.d.ts.map +1 -0
- package/dist/browser/firefox/subscriptions.js +10 -0
- package/dist/browser/launch.d.ts +6 -0
- package/dist/browser/launch.d.ts.map +1 -1
- package/dist/browser/launch.js +14 -1
- package/dist/cli/commands/doctor.js +3 -3
- package/dist/cli/commands/inspect.js +3 -2
- package/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/commands/install.js +26 -13
- package/dist/cli/commands/licences.d.ts +22 -0
- package/dist/cli/commands/licences.d.ts.map +1 -1
- package/dist/cli/commands/licences.js +62 -20
- package/dist/cli/commands/run.js +2 -2
- package/dist/cli/doctor/checks.js +4 -1
- package/dist/cli/inspect/diagnostics.js +3 -3
- package/dist/cli/inspect/test-timeline.js +10 -10
- package/dist/cli/install/doctor-rows.d.ts.map +1 -1
- package/dist/cli/install/doctor-rows.js +5 -4
- package/dist/cli/install/file-changes.d.ts +44 -0
- package/dist/cli/install/file-changes.d.ts.map +1 -0
- package/dist/cli/install/file-changes.js +81 -0
- package/dist/cli/install/install-archive.d.ts +3 -1
- package/dist/cli/install/install-archive.d.ts.map +1 -1
- package/dist/cli/install/install-archive.js +80 -33
- package/dist/cli/install/lock.d.ts.map +1 -1
- package/dist/cli/install/lock.js +108 -21
- package/dist/cli/install/media-doctor.d.ts.map +1 -1
- package/dist/cli/install/media-doctor.js +2 -1
- package/dist/cli/install/media-install.d.ts.map +1 -1
- package/dist/cli/install/media-install.js +38 -19
- package/dist/cli/install/media-pins.d.ts +7 -1
- package/dist/cli/install/media-pins.d.ts.map +1 -1
- package/dist/cli/install/media-pins.js +23 -12
- package/dist/cli/install/media-record.d.ts.map +1 -1
- package/dist/cli/install/media-record.js +10 -9
- package/dist/cli/install/media-report.d.ts +3 -1
- package/dist/cli/install/media-report.d.ts.map +1 -1
- package/dist/cli/install/media-report.js +7 -5
- package/dist/cli/install/media-tools.d.ts.map +1 -1
- package/dist/cli/install/media-tools.js +6 -4
- package/dist/cli/install/process-start.d.ts +3 -2
- package/dist/cli/install/process-start.d.ts.map +1 -1
- package/dist/cli/install/process-start.js +4 -0
- package/dist/cli/install/report.d.ts.map +1 -1
- package/dist/cli/install/report.js +11 -7
- package/dist/cli/install/supervisor-install.d.ts +33 -0
- package/dist/cli/install/supervisor-install.d.ts.map +1 -0
- package/dist/cli/install/supervisor-install.js +158 -0
- package/dist/cli/install/supervisor-locate.d.ts +47 -0
- package/dist/cli/install/supervisor-locate.d.ts.map +1 -0
- package/dist/cli/install/supervisor-locate.js +113 -0
- package/dist/cli/install/supervisor-pins.d.ts +21 -0
- package/dist/cli/install/supervisor-pins.d.ts.map +1 -0
- package/dist/cli/install/supervisor-pins.js +28 -0
- package/dist/cli/install/supervisor-record.d.ts +31 -0
- package/dist/cli/install/supervisor-record.d.ts.map +1 -0
- package/dist/cli/install/supervisor-record.js +104 -0
- package/dist/cli/install/supervisor-report.d.ts +11 -0
- package/dist/cli/install/supervisor-report.d.ts.map +1 -0
- package/dist/cli/install/supervisor-report.js +33 -0
- package/dist/cli/install/supervisor-tools.d.ts +43 -0
- package/dist/cli/install/supervisor-tools.d.ts.map +1 -0
- package/dist/cli/install/supervisor-tools.js +114 -0
- package/dist/cli/install/unpack.d.ts +23 -3
- package/dist/cli/install/unpack.d.ts.map +1 -1
- package/dist/cli/install/unpack.js +124 -10
- package/dist/cli/install/zip-member.d.ts +15 -0
- package/dist/cli/install/zip-member.d.ts.map +1 -0
- package/dist/cli/install/zip-member.js +77 -0
- package/dist/cli/interrupt.d.ts +1 -1
- package/dist/cli/interrupt.d.ts.map +1 -1
- package/dist/cli/interrupt.js +15 -10
- package/dist/cli/terminal.d.ts +9 -1
- package/dist/cli/terminal.d.ts.map +1 -1
- package/dist/cli/terminal.js +14 -0
- package/dist/config/define.d.ts +2 -2
- package/dist/config/define.js +2 -2
- package/dist/diagnostics/firefox-collector.d.ts +11 -10
- package/dist/diagnostics/firefox-collector.d.ts.map +1 -1
- package/dist/diagnostics/firefox-collector.js +16 -11
- package/dist/media/client.d.ts +2 -1
- package/dist/media/client.d.ts.map +1 -1
- package/dist/media/client.js +16 -5
- package/dist/media/host.d.ts +32 -0
- package/dist/media/host.d.ts.map +1 -0
- package/dist/media/host.js +50 -0
- package/dist/media/locate.d.ts.map +1 -1
- package/dist/media/locate.js +4 -3
- package/dist/native/processes.d.ts.map +1 -1
- package/dist/native/processes.js +7 -3
- package/dist/protocol/files.d.ts +1 -1
- package/dist/protocol/files.d.ts.map +1 -1
- package/dist/protocol/files.js +10 -5
- package/dist/reporters/commands.d.ts +12 -3
- package/dist/reporters/commands.d.ts.map +1 -1
- package/dist/reporters/commands.js +29 -11
- package/dist/reporters/human-card.js +1 -1
- package/dist/reporters/human.d.ts +3 -1
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +8 -8
- package/dist/reporters/style.d.ts +21 -2
- package/dist/reporters/style.d.ts.map +1 -1
- package/dist/reporters/style.js +11 -2
- package/dist/runner/app-server.d.ts +27 -7
- package/dist/runner/app-server.d.ts.map +1 -1
- package/dist/runner/app-server.js +74 -24
- package/dist/runner/child.js +6 -2
- package/dist/runner/media-leftovers.d.ts +7 -3
- package/dist/runner/media-leftovers.d.ts.map +1 -1
- package/dist/runner/media-leftovers.js +27 -6
- package/dist/runner/resources.d.ts +8 -0
- package/dist/runner/resources.d.ts.map +1 -1
- package/dist/runner/resources.js +12 -1
- package/dist/runner/run-session.d.ts.map +1 -1
- package/dist/runner/run-session.js +9 -4
- package/dist/runner/run.d.ts.map +1 -1
- package/dist/runner/run.js +2 -1
- package/dist/runner/target-drivers.d.ts +5 -2
- package/dist/runner/target-drivers.d.ts.map +1 -1
- package/dist/runner/target-drivers.js +10 -1
- package/dist/runner/test-file-process.d.ts +8 -0
- package/dist/runner/test-file-process.d.ts.map +1 -1
- package/dist/runner/test-file-process.js +26 -3
- package/dist/runner/test-pages.d.ts.map +1 -1
- package/dist/runner/test-pages.js +5 -2
- package/dist/shared/environment.d.ts +17 -0
- package/dist/shared/environment.d.ts.map +1 -0
- package/dist/shared/environment.js +27 -0
- package/dist/shared/metadata-process-worker.js +15 -7
- package/dist/shared/metadata-process.d.ts +5 -0
- package/dist/shared/metadata-process.d.ts.map +1 -1
- package/dist/shared/metadata-process.js +36 -6
- package/dist/shared/posix-path.d.ts +12 -0
- package/dist/shared/posix-path.d.ts.map +1 -1
- package/dist/shared/posix-path.js +16 -0
- package/dist/shared/private-file.d.ts +38 -0
- package/dist/shared/private-file.d.ts.map +1 -0
- package/dist/shared/private-file.js +132 -0
- package/dist/shared/process-ownership.d.ts +52 -2
- package/dist/shared/process-ownership.d.ts.map +1 -1
- package/dist/shared/process-ownership.js +104 -11
- package/dist/shared/process-supervisor.d.ts +309 -0
- package/dist/shared/process-supervisor.d.ts.map +1 -0
- package/dist/shared/process-supervisor.js +737 -0
- package/dist/shared/process-system.d.ts +113 -0
- package/dist/shared/process-system.d.ts.map +1 -0
- package/dist/shared/process-system.js +254 -0
- package/dist/shared/regular-file.d.ts +2 -2
- package/dist/shared/regular-file.d.ts.map +1 -1
- package/dist/shared/regular-file.js +19 -2
- package/dist/store/artifacts.d.ts +5 -4
- package/dist/store/artifacts.d.ts.map +1 -1
- package/dist/store/artifacts.js +18 -5
- package/dist/store/run-store.d.ts +5 -2
- package/dist/store/run-store.d.ts.map +1 -1
- package/dist/store/run-store.js +48 -4
- package/media/Cargo.lock +16 -0
- package/media/Cargo.toml +16 -1
- package/media/src/encoder.rs +56 -12
- package/media/src/frame.rs +7 -1
- package/media/src/main.rs +20 -2
- package/media/src/place.rs +230 -11
- package/media/src/process_ownership_windows.rs +1058 -0
- package/media/src/recording.rs +15 -7
- package/media/src/replies.rs +55 -0
- package/media/src/server.rs +72 -2
- package/media/src/store.rs +71 -2
- package/package.json +9 -4
- package/src/cli/install/media-notices.txt +465 -1
- package/supervisor/Cargo.lock +25 -0
- package/supervisor/Cargo.toml +22 -0
- package/supervisor/NOTICES +464 -0
- package/supervisor/build.rs +44 -0
- package/supervisor/src/command.rs +612 -0
- package/supervisor/src/jobs.rs +1650 -0
- package/supervisor/src/main.rs +71 -0
- package/supervisor/src/reply.rs +367 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
+
import { retestCommand } from "../../reporters/commands.js";
|
|
5
|
+
import { listWords } from "../../shared/list-words.js";
|
|
4
6
|
import { findPin, installedBuild } from "../builds.js";
|
|
5
|
-
|
|
6
|
-
export const defaultFirefoxPath = '/Applications/Firefox.app/Contents/MacOS/firefox';
|
|
7
|
+
import { systemFirefoxPaths, systemHost } from "../executables.js";
|
|
7
8
|
/**
|
|
8
9
|
* The Firefox release the driver is tested with, from the pinned build: the facts the driver encodes (fields its
|
|
9
10
|
* accessibility locator leaves out, role names, what its BiDi lacks) are this release's.
|
|
10
11
|
*
|
|
11
|
-
* @example testedFirefox() // { version: '
|
|
12
|
+
* @example testedFirefox() // { version: '158.0.1', buildId: '20261019185856' }
|
|
12
13
|
*/
|
|
13
14
|
export function testedFirefox() {
|
|
14
15
|
const pin = findPin('firefox', 'mac-arm64');
|
|
@@ -20,7 +21,7 @@ export function testedFirefox() {
|
|
|
20
21
|
* What a Firefox's own files say against the tested release: undefined when it is the tested release, and otherwise a
|
|
21
22
|
* sentence naming both, for `doctor` and a run's record to say of a Firefox a target named itself.
|
|
22
23
|
*
|
|
23
|
-
* @example untestedFirefox({ version: '150.0', buildId: '20260901000000' }) // 'Firefox 150.0 build 20260901000000 is not the tested Firefox
|
|
24
|
+
* @example untestedFirefox({ version: '150.0', buildId: '20260901000000' }) // 'Firefox 150.0 build 20260901000000 is not the tested Firefox 158.0.1 build 20261019185856.'
|
|
24
25
|
*/
|
|
25
26
|
export function untestedFirefox(release, tested = testedFirefox()) {
|
|
26
27
|
if (release !== undefined && release.version === tested.version && (tested.buildId === undefined || release.buildId === tested.buildId))
|
|
@@ -31,22 +32,25 @@ function describeRelease({ version, buildId }) {
|
|
|
31
32
|
return buildId === undefined ? version : `${version} build ${buildId}`;
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
|
-
* Whether this machine is one Retest runs Firefox on: macOS on Apple silicon
|
|
35
|
-
*
|
|
35
|
+
* Whether this machine is one Retest runs Firefox on: macOS on Apple silicon or Windows x64, the platforms Firefox is
|
|
36
|
+
* pinned for. Any other is refused by name rather than tried.
|
|
36
37
|
*
|
|
37
|
-
* @example firefoxPlatformProblem('linux', 'x64') // 'Retest runs Firefox on macOS on Apple silicon, and this machine is linux x64.'
|
|
38
|
+
* @example firefoxPlatformProblem('linux', 'x64') // 'Retest runs Firefox on macOS on Apple silicon and on Windows x64, and this machine is linux x64.'
|
|
38
39
|
*/
|
|
39
40
|
export function firefoxPlatformProblem(platform = process.platform, arch = process.arch) {
|
|
40
41
|
if (platform === 'darwin' && arch === 'arm64')
|
|
41
42
|
return undefined;
|
|
42
|
-
|
|
43
|
+
if (platform === 'win32' && arch === 'x64')
|
|
44
|
+
return undefined;
|
|
45
|
+
return `Retest runs Firefox on macOS on Apple silicon and on Windows x64, and this machine is ${platform} ${arch}.`;
|
|
43
46
|
}
|
|
44
47
|
/**
|
|
45
48
|
* The Firefox a target launches: the path it names, at whatever version it is (its real version and build are what the
|
|
46
|
-
* run records); else the pinned build in this user's Retest cache; else Firefox where
|
|
47
|
-
* files say it is the tested release, since the driver encodes that
|
|
48
|
-
* on an engine nobody exercised. Nothing is downloaded. A machine
|
|
49
|
-
* another release, or no Firefox at all fails setup by name,
|
|
49
|
+
* run records); else the pinned build in this user's Retest cache; else Firefox where the system installs it, the
|
|
50
|
+
* first of its folders that holds one, only when its files say it is the tested release, since the driver encodes that
|
|
51
|
+
* release's facts and another would run every test on an engine nobody exercised. Nothing is downloaded. A machine
|
|
52
|
+
* Retest does not run Firefox on, a system Firefox of another release, or no Firefox at all fails setup by name,
|
|
53
|
+
* saying what it found and the ways forward.
|
|
50
54
|
*
|
|
51
55
|
* @example await findFirefoxExecutable({ name: 'firefox', browser: 'firefox', headless: true }, 'web', process.env)
|
|
52
56
|
*/
|
|
@@ -62,12 +66,15 @@ export async function findFirefoxExecutable(target, app, env, options = {}) {
|
|
|
62
66
|
return { ok: true, path: installed.executablePath };
|
|
63
67
|
if (installed.state === 'damaged')
|
|
64
68
|
return { ok: false, failure: { class: 'setup_failed', message: installed.message, details: { ...details, folder: installed.folder } } };
|
|
65
|
-
const
|
|
69
|
+
const systemPaths = options.systemPath === undefined ? systemFirefoxPaths(systemHost()) : [options.systemPath];
|
|
66
70
|
const tested = testedFirefox();
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
// The pinned build is the one a run takes first, and it stays at the tested release, where a Firefox installed by hand
|
|
72
|
+
// updates itself away from it; so the way named is installing the pin.
|
|
73
|
+
const install = `run ${retestCommand} install firefox`;
|
|
74
|
+
const systemPath = systemPaths.find((path) => existsSync(path));
|
|
75
|
+
if (systemPath === undefined) {
|
|
76
|
+
const nowhere = systemPaths.length === 0 ? 'this system names no folder Firefox installs in' : `none is at ${listWords(systemPaths, 'or')}`;
|
|
77
|
+
const message = `No Firefox for the target ${target.name} of the app ${app}: no pinned build is in Retest's cache, and ${nowhere}. Either ${install}, or give the target an executablePath.`;
|
|
71
78
|
return { ok: false, failure: { class: 'setup_failed', message, details } };
|
|
72
79
|
}
|
|
73
80
|
const release = await readFirefoxRelease(systemPath);
|
|
@@ -80,12 +87,13 @@ export async function findFirefoxExecutable(target, app, env, options = {}) {
|
|
|
80
87
|
}
|
|
81
88
|
/**
|
|
82
89
|
* The version and build a Firefox binary's app states in its `application.ini`, read from the file without starting
|
|
83
|
-
* anything, or undefined when the binary is not inside an app whose file says.
|
|
90
|
+
* anything, or undefined when the binary is not inside an app whose file says. On macOS the file is in the app
|
|
91
|
+
* bundle's resources; on Windows it is beside firefox.exe.
|
|
84
92
|
*
|
|
85
|
-
* @example await readFirefoxRelease('/Applications/Firefox.app/Contents/MacOS/firefox') // { version: '
|
|
93
|
+
* @example await readFirefoxRelease('/Applications/Firefox.app/Contents/MacOS/firefox') // { version: '158.0.1', buildId: '20261019185856' }
|
|
86
94
|
*/
|
|
87
|
-
export async function readFirefoxRelease(executablePath) {
|
|
88
|
-
const file = join(dirname(dirname(executablePath)), 'Resources', 'application.ini');
|
|
95
|
+
export async function readFirefoxRelease(executablePath, platform = process.platform) {
|
|
96
|
+
const file = platform === 'win32' ? join(dirname(executablePath), 'application.ini') : join(dirname(dirname(executablePath)), 'Resources', 'application.ini');
|
|
89
97
|
const text = await readFile(file, 'utf8').catch(() => undefined);
|
|
90
98
|
if (text === undefined)
|
|
91
99
|
return undefined;
|
|
@@ -78,7 +78,7 @@ export async function launchFirefox(options, timeoutMs = launchTimeoutMs, socket
|
|
|
78
78
|
...(socket === undefined ? {} : { socket }),
|
|
79
79
|
});
|
|
80
80
|
const facts = await startSession(client, deadline);
|
|
81
|
-
if (facts.processId
|
|
81
|
+
if (!(await firefox.ends(facts.processId, deadline)))
|
|
82
82
|
throw new LaunchError(`The Firefox at ${address} names process ${facts.processId}, not the launched ${firefox.pid}, so ending ${firefox.pid} would not end the browser it drives.`);
|
|
83
83
|
if (facts.profile !== profile)
|
|
84
84
|
throw new LaunchError(`The Firefox at ${address} uses the profile ${facts.profile}, not ${profile}.`);
|
|
@@ -91,8 +91,11 @@ export declare class NavigationWatcher {
|
|
|
91
91
|
loaded(navigation: string | null, deadline: Deadline): Promise<Outcome>;
|
|
92
92
|
/** Waits for the latest document committed since the watcher was made to load or show an error, or for a move within it. */
|
|
93
93
|
settled(deadline: Deadline): Promise<Outcome>;
|
|
94
|
-
/**
|
|
95
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Waits, for a navigation Firefox refused or gave up, for its error page to commit and show, or for its response to
|
|
96
|
+
* have opened no document; the time it may take is short.
|
|
97
|
+
*/
|
|
98
|
+
refused(deadline: Deadline): Promise<Outcome>;
|
|
96
99
|
stop(): void;
|
|
97
100
|
}
|
|
98
101
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../src/browser/firefox/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAE/D,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAQrD;;;;;GAKG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,UAAU,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,UAAU,EAAE,MAAM,GAAG,GAAG,SAAS,CAAA;IACjC,KAAK,EAAE,MAAM,iBAAiB,CAAA;IAC9B,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;CACtC,CAAA;AAED,KAAK,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../src/browser/firefox/navigation.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,9 +6,11 @@ import { readProtocol } from "../cdp-results.js";
|
|
|
6
6
|
import { connectionEnded } from "../command-failures.js";
|
|
7
7
|
import { originAndPath } from "../page-url.js";
|
|
8
8
|
const startedSchema = s.object({ navigation: s.nullable(s.string()), url: s.string() });
|
|
9
|
-
// What Firefox answers a navigation it refused
|
|
10
|
-
//
|
|
11
|
-
|
|
9
|
+
// What Firefox answers a navigation it refused or gave up: an error that names the reason in its own words, which is all
|
|
10
|
+
// of it Retest keeps. Firefox 133 refuses so only a navigation it never starts, such as one to a blocked port, with a
|
|
11
|
+
// word such as `deniedPortAccess`. Firefox 158 answers every navigation once its document commits, so it also answers
|
|
12
|
+
// so one whose error page committed or whose response opened no document, with a name such as `NS_BINDING_ABORTED`.
|
|
13
|
+
const refusedNavigation = /^unknown error: ([A-Za-z_]+)$/;
|
|
12
14
|
/**
|
|
13
15
|
* Opens a URL in the tab and waits for the `load` event of the document it returned, or of a document that replaced it
|
|
14
16
|
* before it loaded, as a client-side redirect does. A move to another fragment of the document is complete once
|
|
@@ -44,22 +46,28 @@ export async function navigate(page, url, deadline, dispatch) {
|
|
|
44
46
|
throw error;
|
|
45
47
|
}
|
|
46
48
|
if ('refused' in started) {
|
|
47
|
-
// Firefox shows its error page as a document of its own; waiting for it lets a screenshot show it.
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// Firefox shows its error page as a document of its own; waiting for it lets a screenshot show it. A navigation
|
|
50
|
+
// whose response opened no document is told by that response, which came before the answer.
|
|
51
|
+
await watcher.refused(new Deadline(Math.min(errorPageWaitMs, deadline.remainingMs), deadline.signal === undefined ? {} : { signal: deadline.signal }));
|
|
52
|
+
const abandoned = watcher.abandoned;
|
|
53
|
+
if (abandoned !== undefined)
|
|
54
|
+
return { ok: false, failure: gaveUp('open', abandoned, describeAddress(page.currentUrl())) };
|
|
55
|
+
return { ok: false, failure: navigationFailed(address, watcher.unreachable ?? started.refused) };
|
|
50
56
|
}
|
|
51
57
|
if (started.navigation !== null)
|
|
52
58
|
page.opened?.(started.navigation);
|
|
59
|
+
// Firefox answers a move to another fragment of the document once it is made, and Firefox 158 tells no event for it
|
|
60
|
+
// to a subscription bound to the tab, so an answered move that started no navigation of the document is complete.
|
|
61
|
+
if (!watcher.startedAny && sameDocument(address, page.currentUrl()))
|
|
62
|
+
watcher.onMovedWithinDocument();
|
|
53
63
|
const outcome = await watcher.loaded(started.navigation, deadline);
|
|
54
64
|
if (outcome.kind === 'timeout')
|
|
55
65
|
return notLoaded(address, deadline);
|
|
56
66
|
if (outcome.kind === 'stopped')
|
|
57
67
|
return { ok: false, failure: connectionEnded(`open ${originAndPath(address)}`, true, outcome.reason) };
|
|
58
68
|
const abandoned = watcher.abandoned;
|
|
59
|
-
if (abandoned !== undefined)
|
|
60
|
-
|
|
61
|
-
return { ok: false, failure: gaveUp('open', abandoned, current === undefined ? 'the page' : originAndPath(current)) };
|
|
62
|
-
}
|
|
69
|
+
if (abandoned !== undefined)
|
|
70
|
+
return { ok: false, failure: gaveUp('open', abandoned, describeAddress(page.currentUrl())) };
|
|
63
71
|
const unreachable = watcher.unreachable;
|
|
64
72
|
if (unreachable !== undefined)
|
|
65
73
|
return { ok: false, failure: navigationFailed(address, unreachable) };
|
|
@@ -83,7 +91,10 @@ export async function reload(page, deadline, dispatch) {
|
|
|
83
91
|
catch (error) {
|
|
84
92
|
if (error instanceof CdpTimeoutError)
|
|
85
93
|
return notLoaded(current, deadline);
|
|
86
|
-
|
|
94
|
+
const abandoned = givenUp(error, watcher);
|
|
95
|
+
if (abandoned === undefined)
|
|
96
|
+
throw error;
|
|
97
|
+
return { ok: false, failure: gaveUp('reload', abandoned, describeAddress(page.currentUrl() ?? current)) };
|
|
87
98
|
}
|
|
88
99
|
return await opened(page, watcher, { kind: 'reload', verb: 'reload', expected: current }, deadline);
|
|
89
100
|
}
|
|
@@ -118,7 +129,10 @@ export async function traverse(page, direction, deadline, dispatch) {
|
|
|
118
129
|
catch (error) {
|
|
119
130
|
if (error instanceof CdpTimeoutError)
|
|
120
131
|
return notLoaded(page.currentUrl(), deadline);
|
|
121
|
-
|
|
132
|
+
const abandoned = givenUp(error, watcher);
|
|
133
|
+
if (abandoned === undefined)
|
|
134
|
+
throw error;
|
|
135
|
+
return { ok: false, failure: gaveUp(back ? 'go back to' : 'go forward to', abandoned, describeAddress(page.currentUrl())) };
|
|
122
136
|
}
|
|
123
137
|
if (!moved) {
|
|
124
138
|
const where = back ? 'back: the page has no earlier' : 'forward: the page has no later';
|
|
@@ -159,6 +173,21 @@ function gaveUp(verb, asked, stayed) {
|
|
|
159
173
|
const message = `Could not ${verb} ${shown}: the browser sent the request and gave the navigation up without opening a document, as it does for a response with no content or a download. The page stayed on ${stayed}.`;
|
|
160
174
|
return { class: 'not_actionable', message, details: { url: shown, inputSent: true } };
|
|
161
175
|
}
|
|
176
|
+
// Firefox 158 answers a reload or a move through the history with an error once the browser gave its navigation up
|
|
177
|
+
// (`NS_BINDING_ABORTED`, or `timeout` for a move through the history), where Firefox 133 answered as the move began.
|
|
178
|
+
// The response that opened no document came first, so the watcher already holds the address that was given up.
|
|
179
|
+
function givenUp(error, watcher) {
|
|
180
|
+
return error instanceof CdpProtocolError ? watcher.abandoned : undefined;
|
|
181
|
+
}
|
|
182
|
+
function describeAddress(url) {
|
|
183
|
+
return url === undefined ? 'the page' : originAndPath(url);
|
|
184
|
+
}
|
|
185
|
+
// A move to `target` stays in the document at `current`: the same address but for a fragment the target names.
|
|
186
|
+
function sameDocument(target, current) {
|
|
187
|
+
if (current === undefined || target.hash === '')
|
|
188
|
+
return false;
|
|
189
|
+
return target.origin === current.origin && target.pathname === current.pathname && target.search === current.search;
|
|
190
|
+
}
|
|
162
191
|
function resolveTarget(url, baseUrl) {
|
|
163
192
|
const target = URL.parse(url) ?? (baseUrl === undefined ? null : URL.parse(url, baseUrl));
|
|
164
193
|
if (target === null)
|
|
@@ -301,9 +330,14 @@ export class NavigationWatcher {
|
|
|
301
330
|
return latest !== undefined && (this.#loaded.has(latest) || this.#errorPages.has(latest));
|
|
302
331
|
}, deadline);
|
|
303
332
|
}
|
|
304
|
-
/**
|
|
305
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Waits, for a navigation Firefox refused or gave up, for its error page to commit and show, or for its response to
|
|
335
|
+
* have opened no document; the time it may take is short.
|
|
336
|
+
*/
|
|
337
|
+
refused(deadline) {
|
|
306
338
|
return this.#wait(() => {
|
|
339
|
+
if (this.abandoned !== undefined)
|
|
340
|
+
return true;
|
|
307
341
|
const latest = this.#committed.at(-1);
|
|
308
342
|
return latest !== undefined && this.#errorPages.has(latest);
|
|
309
343
|
}, deadline);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orphans.d.ts","sourceRoot":"","sources":["../../../src/browser/firefox/orphans.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAA;AAK7E,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"orphans.d.ts","sourceRoot":"","sources":["../../../src/browser/firefox/orphans.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAA;AAK7E,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAQrD;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEpG,8FAA8F;AAC9F,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,oBAAoB,EAAE,GAAG,OAAO,CAAC,SAAS,oBAAoB,EAAE,CAAC,CAAA;IACrG,WAAW,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,oBAAoB,GAAG,SAAS,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAA;IAC5H,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,CAAA;AAkBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,WAAkB,GAAG,OAAO,CAAC,WAAW,CAAC,CAwB3G"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { lstat, readdir, rm } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
3
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
4
|
import { Deadline } from "../../protocol/deadline.js";
|
|
5
5
|
import { errorMessage } from "../../protocol/failures.js";
|
|
6
6
|
import { errorCode } from "../../shared/error-code.js";
|
|
7
7
|
import { sameProcessIdentity } from "../../shared/process-ownership.js";
|
|
8
|
+
import { launchesHeldByJob, readHeldProcess } from "../../shared/process-system.js";
|
|
8
9
|
import { firefoxFolderPrefix, ownerRecordFile, readOwnerRecord } from "./profile.js";
|
|
9
10
|
import { isRunning, readProcessIdentityAsync, readProcessTableAsync } from "./process-table.js";
|
|
10
11
|
const ownedFolder = new RegExp(`^${firefoxFolderPrefix}(\\d+)-[A-Za-z0-9]+$`);
|
|
@@ -49,6 +50,11 @@ export async function sweepOrphanedFirefoxes(root, system = host) {
|
|
|
49
50
|
const folders = entries.filter((entry) => entry.isDirectory() && ownedFolder.test(entry.name)).map((entry) => join(root, entry.name));
|
|
50
51
|
if (folders.length === 0)
|
|
51
52
|
return report;
|
|
53
|
+
if (launchesHeldByJob && system === host) {
|
|
54
|
+
for (const folder of folders)
|
|
55
|
+
await sweepHeldFolder(folder, report);
|
|
56
|
+
return report;
|
|
57
|
+
}
|
|
52
58
|
let table;
|
|
53
59
|
try {
|
|
54
60
|
table = await system.read();
|
|
@@ -63,7 +69,7 @@ export async function sweepOrphanedFirefoxes(root, system = host) {
|
|
|
63
69
|
}
|
|
64
70
|
async function sweepFolder(folder, table, system, report) {
|
|
65
71
|
const record = await readOwnerRecord(folder);
|
|
66
|
-
const ownerPid = Number(ownedFolder.exec(
|
|
72
|
+
const ownerPid = Number(ownedFolder.exec(basename(folder))?.[1]);
|
|
67
73
|
if (record === undefined) {
|
|
68
74
|
try {
|
|
69
75
|
await lstat(join(folder, 'retest-owner.json'));
|
|
@@ -177,6 +183,32 @@ async function endRecorded(record, system) {
|
|
|
177
183
|
await sleep(Math.min(pollMs, deadline.remainingMs));
|
|
178
184
|
}
|
|
179
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* A folder a launch on Windows left. There the launcher's job ended its Firefox with it, whatever ended the launcher, so
|
|
188
|
+
* nothing is left to end: the folder goes once its launcher is shown gone, by its recorded start, or, before a record
|
|
189
|
+
* was written, by no process holding the pid the folder is named after.
|
|
190
|
+
*/
|
|
191
|
+
async function sweepHeldFolder(folder, report) {
|
|
192
|
+
const record = await readOwnerRecord(folder);
|
|
193
|
+
const ownerPid = Number(ownedFolder.exec(basename(folder))?.[1]);
|
|
194
|
+
const exact = record !== undefined && record.startTimeVersion === 2 && record.owner.pid === ownerPid ? record.owner.startedAt : undefined;
|
|
195
|
+
let alive;
|
|
196
|
+
try {
|
|
197
|
+
alive = exact === undefined ? readHeldProcess(ownerPid) !== undefined : readHeldProcess(ownerPid)?.startedAt === exact;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
report.problems.push(`Could not tell whether the launcher of ${folder} is gone, so the folder was kept: ${errorMessage(error)}`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!alive) {
|
|
204
|
+
await remove(folder, report);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// A record without a start Windows wrote cannot tell its launcher from another process given the same pid since.
|
|
208
|
+
const file = join(folder, ownerRecordFile);
|
|
209
|
+
if (record !== undefined && exact === undefined)
|
|
210
|
+
report.kept.push(`${folder}: cannot confirm the recorded start in ${file}, and its launcher pid is present; the folder was left alone. Remove ${file} once no runner is running.`);
|
|
211
|
+
}
|
|
180
212
|
async function remove(folder, report) {
|
|
181
213
|
try {
|
|
182
214
|
await rm(folder, { recursive: true, force: true, maxRetries: 3 });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../src/browser/firefox/page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAK1L,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAA;AAC7F,OAAO,KAAK,EAAgB,aAAa,EAAmB,MAAM,4BAA4B,CAAA;AAC9F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAA;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAEhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAG9D,OAAO,KAAK,EAAE,YAAY,EAAgB,MAAM,iCAAiC,CAAA;AAMjF,OAAO,EAAE,QAAQ,EAAkB,MAAM,4BAA4B,CAAA;AAsBrE,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAA;AAE/D,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AAIrE,OAAO,EAAuC,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAOpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../src/browser/firefox/page.ts"],"names":[],"mappings":"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"}
|
|
@@ -44,10 +44,11 @@ import { BidiProtocolError } from "./bidi-errors.js";
|
|
|
44
44
|
import { cdpError, FirefoxBridge } from "./bridge.js";
|
|
45
45
|
import { clickAt, moveTo, pressedStrokes, pressKey, selectionProblem, toggleRefusal, typeText, typedStrokes, typingProblem, wheelAt } from "./input.js";
|
|
46
46
|
import { NavigationHold } from "./navigation-hold.js";
|
|
47
|
-
import { navigate, NavigationWatcher, reload, traverse } from "./navigation.js";
|
|
47
|
+
import { errorPageReason, navigate, NavigationWatcher, reload, traverse } from "./navigation.js";
|
|
48
48
|
import { navigationShim, relayScript, sandboxName, wrapped } from "./sandbox.js";
|
|
49
49
|
import { inWindowOrder } from "./window-order.js";
|
|
50
50
|
import { readFirefoxCookies, readFirefoxOriginStorage } from "./storage.js";
|
|
51
|
+
import { subscribedSchema, unsubscribeParameters } from "./subscriptions.js";
|
|
51
52
|
const retryPauseMs = 20;
|
|
52
53
|
const loadedPageReadMs = 1000;
|
|
53
54
|
const firstSelectionPauseMs = 20;
|
|
@@ -70,6 +71,12 @@ const responseSchema = s.object({
|
|
|
70
71
|
request: s.object({ url: s.string() }),
|
|
71
72
|
response: s.object({ status: s.number(), headers: s.optional(s.array(s.object({ name: s.string(), value: s.object({ type: s.string(), value: s.optional(s.string()) }) }))) }),
|
|
72
73
|
});
|
|
74
|
+
const requestSchema = s.object({
|
|
75
|
+
context: s.string(),
|
|
76
|
+
navigation: s.nullable(s.string()),
|
|
77
|
+
redirectCount: s.number(),
|
|
78
|
+
request: s.object({ url: s.string(), destination: s.optional(s.string()) }),
|
|
79
|
+
});
|
|
73
80
|
const messageSchema = s.object({ channel: s.string(), data: s.object({ type: s.string(), value: s.optional(s.string()) }), source: s.object({ context: s.optional(s.string()) }) });
|
|
74
81
|
const treeSchema = s.object({ contexts: s.array(s.object({ url: s.string() })) });
|
|
75
82
|
const scriptSchema = s.object({ script: s.string() });
|
|
@@ -155,6 +162,11 @@ export class FirefoxPage {
|
|
|
155
162
|
#stops = [];
|
|
156
163
|
#channels;
|
|
157
164
|
#subscriptions = new Map();
|
|
165
|
+
// The id a later Firefox gave each counted event's subscription, and how many of each subscription's events are live.
|
|
166
|
+
#subscriptionIds = new Map();
|
|
167
|
+
#liveEvents = new Map();
|
|
168
|
+
// The id of the page's own subscription to its context's events, when the browser gave one.
|
|
169
|
+
#pageSubscription;
|
|
158
170
|
#changes = 0;
|
|
159
171
|
#url;
|
|
160
172
|
#commits = 0;
|
|
@@ -162,6 +174,8 @@ export class FirefoxPage {
|
|
|
162
174
|
#opening = 0;
|
|
163
175
|
// Whether a navigation started since the opening command began, which is then that command's.
|
|
164
176
|
#claimed = false;
|
|
177
|
+
// The navigation under way: its id, its address, and, for one Firefox 158 did not announce, the id Firefox gave its
|
|
178
|
+
// request, which is the document's still showing and names the navigation's response and its end.
|
|
165
179
|
#pending;
|
|
166
180
|
#started = new Set();
|
|
167
181
|
#document;
|
|
@@ -209,7 +223,10 @@ export class FirefoxPage {
|
|
|
209
223
|
this.#listen('browsingContext.domContentLoaded', navigationEventSchema, ({ navigation, url }) => this.#contentLoaded(navigation, url));
|
|
210
224
|
this.#listen('browsingContext.load', navigationEventSchema, ({ navigation, url }) => this.#loaded(navigation, url));
|
|
211
225
|
this.#listen('browsingContext.navigationFailed', navigationEventSchema, ({ navigation }) => this.#navigationFailed(navigation));
|
|
226
|
+
this.#listen('browsingContext.navigationCommitted', navigationEventSchema, ({ navigation, url }) => this.#navigationCommitted(navigation, url));
|
|
227
|
+
this.#listen('network.beforeRequestSent', requestSchema, (params) => this.#requested(params));
|
|
212
228
|
this.#listen('network.responseStarted', responseSchema, (params) => this.#responded(params));
|
|
229
|
+
this.#listen('browsingContext.downloadWillBegin', navigationEventSchema, ({ navigation, url }) => this.#downloading(navigation, url));
|
|
213
230
|
this.#listen('browsingContext.userPromptOpened', promptSchema, ({ type }) => this.#promptOpened(type));
|
|
214
231
|
this.#listen('browsingContext.userPromptClosed', contextEventSchema, () => this.#promptClosed());
|
|
215
232
|
this.#listen('browsingContext.contextDestroyed', contextEventSchema, () => this.#lose('the page was closed'));
|
|
@@ -422,15 +439,16 @@ export class FirefoxPage {
|
|
|
422
439
|
}
|
|
423
440
|
async #start(options, deadline) {
|
|
424
441
|
const send = () => ({ timeoutMs: deadline.commandTimeoutMs, signal: deadline.signal });
|
|
425
|
-
await this.#client.request('session.subscribe', { events: ['browsingContext', 'script'], contexts: [this.#context] },
|
|
442
|
+
this.#pageSubscription = (await this.#client.request('session.subscribe', { events: ['browsingContext', 'script'], contexts: [this.#context] }, subscribedSchema, send())).subscription;
|
|
426
443
|
const channels = [
|
|
427
444
|
{ type: 'channel', value: { channel: this.#channels.changed, ownership: 'none' } },
|
|
428
445
|
{ type: 'channel', value: { channel: this.#channels.documented, ownership: 'none' } },
|
|
429
446
|
];
|
|
430
447
|
// The claim's event type is random, so no page can listen for it or send it.
|
|
431
448
|
const script = documentScript(`retest-claim-${randomUUID()}`);
|
|
432
|
-
// A navigation answered with no document is told by its response alone (`#responded`)
|
|
433
|
-
|
|
449
|
+
// A navigation answered with no document is told by its response alone (`#responded`), and one Firefox 158 does not
|
|
450
|
+
// announce by its document's request (`#requested`).
|
|
451
|
+
await this.#subscribe(['network.beforeRequestSent', 'network.responseStarted'], send());
|
|
434
452
|
const preload = await this.#client.request('script.addPreloadScript', { functionDeclaration: script, arguments: channels, sandbox: sandboxName, contexts: [this.#context] }, scriptSchema, send());
|
|
435
453
|
this.#preload = preload.script;
|
|
436
454
|
const { emulation } = options;
|
|
@@ -728,11 +746,18 @@ export class FirefoxPage {
|
|
|
728
746
|
if (pending !== undefined)
|
|
729
747
|
this.#navigationFailed(pending.id);
|
|
730
748
|
}
|
|
731
|
-
// Subscriptions are counted by event, so the navigation hold and the diagnostics collector never end each other's.
|
|
749
|
+
// Subscriptions are counted by event, so the navigation hold and the diagnostics collector never end each other's. A
|
|
750
|
+
// subscription a later Firefox gave an id is ended by it once none of the events it was made for is wanted.
|
|
732
751
|
async #subscribe(events, send) {
|
|
733
752
|
const fresh = events.filter((event) => (this.#subscriptions.get(event) ?? 0) === 0);
|
|
734
|
-
if (fresh.length > 0)
|
|
735
|
-
await this.#client.request('session.subscribe', { events: fresh, contexts: [this.#context] },
|
|
753
|
+
if (fresh.length > 0) {
|
|
754
|
+
const { subscription } = await this.#client.request('session.subscribe', { events: fresh, contexts: [this.#context] }, subscribedSchema, send);
|
|
755
|
+
if (subscription !== undefined) {
|
|
756
|
+
for (const event of fresh)
|
|
757
|
+
this.#subscriptionIds.set(event, subscription);
|
|
758
|
+
this.#liveEvents.set(subscription, fresh.length);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
736
761
|
for (const event of events)
|
|
737
762
|
this.#subscriptions.set(event, (this.#subscriptions.get(event) ?? 0) + 1);
|
|
738
763
|
}
|
|
@@ -747,9 +772,28 @@ export class FirefoxPage {
|
|
|
747
772
|
else
|
|
748
773
|
this.#subscriptions.set(event, count);
|
|
749
774
|
}
|
|
750
|
-
|
|
751
|
-
|
|
775
|
+
const unnamed = ended.filter((event) => !this.#subscriptionIds.has(event));
|
|
776
|
+
const finished = ended.flatMap((event) => this.#endEvent(event));
|
|
777
|
+
if (this.#client.closeReason !== undefined || this.#lostReason !== undefined)
|
|
778
|
+
return;
|
|
779
|
+
for (const id of finished)
|
|
780
|
+
this.#client.send('session.unsubscribe', { subscriptions: [id] }).catch(() => undefined);
|
|
781
|
+
if (unnamed.length > 0)
|
|
782
|
+
this.#client.send('session.unsubscribe', { events: unnamed, contexts: [this.#context] }).catch(() => undefined);
|
|
783
|
+
}
|
|
784
|
+
// Forgets an ended event's subscription id, and names the subscription once none of its events is live.
|
|
785
|
+
#endEvent(event) {
|
|
786
|
+
const id = this.#subscriptionIds.get(event);
|
|
787
|
+
if (id === undefined)
|
|
788
|
+
return [];
|
|
789
|
+
this.#subscriptionIds.delete(event);
|
|
790
|
+
const live = (this.#liveEvents.get(id) ?? 1) - 1;
|
|
791
|
+
if (live > 0) {
|
|
792
|
+
this.#liveEvents.set(id, live);
|
|
793
|
+
return [];
|
|
752
794
|
}
|
|
795
|
+
this.#liveEvents.delete(id);
|
|
796
|
+
return [id];
|
|
753
797
|
}
|
|
754
798
|
async #pressKey(command, deadline, dispatch, commandToken) {
|
|
755
799
|
const parsed = parseKey(command.key);
|
|
@@ -1056,7 +1100,8 @@ export class FirefoxPage {
|
|
|
1056
1100
|
const preload = this.#preload;
|
|
1057
1101
|
if (preload !== undefined)
|
|
1058
1102
|
await attempt("remove the page's preload script", () => this.#client.send('script.removePreloadScript', { script: preload }, options()));
|
|
1059
|
-
|
|
1103
|
+
const subscription = { id: this.#pageSubscription, events: ['browsingContext', 'script'], contexts: [this.#context] };
|
|
1104
|
+
await attempt("stop following the page's events", () => this.#client.send('session.unsubscribe', unsubscribeParameters(subscription), options()));
|
|
1060
1105
|
await attempt("close the page's user context", () => inWindowOrder(this.#client, deadline, () => this.#client.send('browser.removeUserContext', { userContext: this.#userContext }, options())));
|
|
1061
1106
|
if (problems.length > 0)
|
|
1062
1107
|
throw new BrowserError({ class: 'cleanup_failed', message: problems.join(' ') });
|
|
@@ -1093,7 +1138,7 @@ export class FirefoxPage {
|
|
|
1093
1138
|
// nothing of who asked for a navigation, so the first one to start while such a command opens is the command's, as
|
|
1094
1139
|
// its answer later confirms for a goto or a reload, and any other is the page's own, such as one a script starts
|
|
1095
1140
|
// while the document the goto opened is parsed.
|
|
1096
|
-
#navigationStarted(navigation, url) {
|
|
1141
|
+
#navigationStarted(navigation, url, requestedAs) {
|
|
1097
1142
|
const id = navigation ?? `navigation-${this.#commits}-${url}`;
|
|
1098
1143
|
const commands = this.#opening > 0 && !this.#claimed;
|
|
1099
1144
|
if (commands)
|
|
@@ -1102,7 +1147,7 @@ export class FirefoxPage {
|
|
|
1102
1147
|
this.#causes.requested(url);
|
|
1103
1148
|
this.#causes.started(url, id);
|
|
1104
1149
|
this.#started.add(id);
|
|
1105
|
-
this.#pending = { id, url };
|
|
1150
|
+
this.#pending = requestedAs === undefined ? { id, url } : { id, url, requestedAs };
|
|
1106
1151
|
this.#navigationStarts.emit(url);
|
|
1107
1152
|
for (const watcher of [...this.#watchers])
|
|
1108
1153
|
watcher.onStarted(id);
|
|
@@ -1133,6 +1178,36 @@ export class FirefoxPage {
|
|
|
1133
1178
|
const params = { functionDeclaration: wrapped(installGuardFunction), arguments: [{ type: 'string', value: '[]' }], target: { context: this.#context, sandbox: sandboxName }, awaitPromise: false, resultOwnership: 'none' };
|
|
1134
1179
|
this.#client.send('script.callFunction', params, { timeoutMs: guardInstallMs }).catch(() => undefined);
|
|
1135
1180
|
}
|
|
1181
|
+
// Firefox 158 tells a navigation's commit, Firefox 133 does not; the document itself is heard from Retest's script
|
|
1182
|
+
// either way. What the commit adds is the address of Firefox's error page, which names its reason, before the page's
|
|
1183
|
+
// own events say it.
|
|
1184
|
+
#navigationCommitted(navigation, url) {
|
|
1185
|
+
if (errorPageReason(url) === undefined)
|
|
1186
|
+
return;
|
|
1187
|
+
const id = this.#documentOf(navigation, url);
|
|
1188
|
+
for (const watcher of [...this.#watchers])
|
|
1189
|
+
watcher.onContentLoaded(id, url);
|
|
1190
|
+
}
|
|
1191
|
+
// Firefox 158 announces no navigation a document's own script starts as it loads, such as from a load handler; the
|
|
1192
|
+
// only word of it is the request for the next document, named by the navigation of the document still showing. That
|
|
1193
|
+
// request starts the navigation here, as Firefox 133's announcement did, so nothing is done in a document about to
|
|
1194
|
+
// be replaced. Any other document request is a navigation Firefox announced, or one of a frame.
|
|
1195
|
+
#requested({ navigation, redirectCount, request }) {
|
|
1196
|
+
if (request.destination !== 'document' || redirectCount > 0 || navigation === null)
|
|
1197
|
+
return;
|
|
1198
|
+
if (this.#pending !== undefined || navigation !== this.#document)
|
|
1199
|
+
return;
|
|
1200
|
+
const url = URL.parse(request.url);
|
|
1201
|
+
if (url === null || !isWebUrl(url))
|
|
1202
|
+
return;
|
|
1203
|
+
this.#navigationStarted(null, request.url, navigation);
|
|
1204
|
+
}
|
|
1205
|
+
// The navigation under way, when `navigation` names it: by its own id, or by the id Firefox gave the request of one it
|
|
1206
|
+
// did not announce.
|
|
1207
|
+
#pendingNamed(navigation) {
|
|
1208
|
+
const pending = this.#pending;
|
|
1209
|
+
return pending !== undefined && (pending.id === navigation || pending.requestedAs === navigation) ? pending : undefined;
|
|
1210
|
+
}
|
|
1136
1211
|
#contentLoaded(navigation, url) {
|
|
1137
1212
|
const id = this.#documentOf(navigation, url);
|
|
1138
1213
|
this.#titles.contentLoaded(id);
|
|
@@ -1160,28 +1235,44 @@ export class FirefoxPage {
|
|
|
1160
1235
|
// The response of the navigation under way says it opens no document: no content, or a download. Firefox 133 keeps such
|
|
1161
1236
|
// a navigation as started until another one begins, and only then says it failed, so the response is what ends it.
|
|
1162
1237
|
#responded({ navigation, request, response }) {
|
|
1163
|
-
|
|
1238
|
+
const pending = navigation === null ? undefined : this.#pendingNamed(navigation);
|
|
1239
|
+
if (pending === undefined)
|
|
1164
1240
|
return;
|
|
1165
1241
|
const disposition = response.headers?.find((header) => header.name.toLowerCase() === 'content-disposition')?.value.value;
|
|
1166
1242
|
const download = disposition !== undefined && /^\s*attachment\b/i.test(disposition);
|
|
1167
1243
|
if (response.status !== 204 && response.status !== 205 && !download)
|
|
1168
1244
|
return;
|
|
1245
|
+
this.#abandon(pending, request.url);
|
|
1246
|
+
}
|
|
1247
|
+
// Firefox 158 announces a navigation a load handler sent to a download only once the download's response came, and
|
|
1248
|
+
// then says the download begins, naming that navigation; it never commits, so the download is what ends it.
|
|
1249
|
+
#downloading(navigation, url) {
|
|
1250
|
+
const pending = navigation === null ? undefined : this.#pendingNamed(navigation);
|
|
1251
|
+
if (pending !== undefined)
|
|
1252
|
+
this.#abandon(pending, url);
|
|
1253
|
+
}
|
|
1254
|
+
// The navigation under way opened no document at `url`: the browser gave it up, and the page stayed where it was.
|
|
1255
|
+
#abandon(pending, url) {
|
|
1169
1256
|
this.#pending = undefined;
|
|
1170
1257
|
this.#causes.stoppedLoading();
|
|
1171
|
-
this.#started.delete(
|
|
1258
|
+
this.#started.delete(pending.id);
|
|
1172
1259
|
for (const watcher of [...this.#watchers])
|
|
1173
|
-
watcher.onAbandoned(
|
|
1260
|
+
watcher.onAbandoned(pending.id, url);
|
|
1174
1261
|
}
|
|
1262
|
+
// Firefox gave a navigation up, or Retest failed its request, as a hold that cancels one does with the id Firefox gave
|
|
1263
|
+
// the request: the navigation under way it names ends.
|
|
1175
1264
|
#navigationFailed(navigation) {
|
|
1176
1265
|
if (navigation === null)
|
|
1177
1266
|
return;
|
|
1178
|
-
|
|
1267
|
+
const pending = this.#pendingNamed(navigation);
|
|
1268
|
+
if (pending !== undefined) {
|
|
1179
1269
|
this.#pending = undefined;
|
|
1180
1270
|
this.#causes.stoppedLoading();
|
|
1181
1271
|
}
|
|
1182
|
-
|
|
1272
|
+
const id = pending?.id ?? navigation;
|
|
1273
|
+
this.#started.delete(id);
|
|
1183
1274
|
for (const watcher of [...this.#watchers])
|
|
1184
|
-
watcher.onFailed(
|
|
1275
|
+
watcher.onFailed(id);
|
|
1185
1276
|
}
|
|
1186
1277
|
#movedWithinDocument(href) {
|
|
1187
1278
|
this.#moveTo(href, undefined);
|
|
@@ -1226,7 +1317,10 @@ export class FirefoxPage {
|
|
|
1226
1317
|
return;
|
|
1227
1318
|
const href = read.data.value;
|
|
1228
1319
|
if (read.channel === this.#channels.documented) {
|
|
1229
|
-
|
|
1320
|
+
// Firefox 158 runs the script in a passing document with no address while a navigation moves to another process;
|
|
1321
|
+
// only a document with an address committed.
|
|
1322
|
+
if (href !== '')
|
|
1323
|
+
this.#committed(href);
|
|
1230
1324
|
return;
|
|
1231
1325
|
}
|
|
1232
1326
|
if (read.channel !== this.#channels.changed)
|