@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.
Files changed (218) hide show
  1. package/README.md +6 -5
  2. package/dist/api/source-location.d.ts.map +1 -1
  3. package/dist/api/source-location.js +3 -1
  4. package/dist/browser/builds.d.ts +35 -6
  5. package/dist/browser/builds.d.ts.map +1 -1
  6. package/dist/browser/builds.js +115 -16
  7. package/dist/browser/cdp/transport.d.ts.map +1 -1
  8. package/dist/browser/cdp/transport.js +4 -4
  9. package/dist/browser/chromium-process.d.ts +23 -0
  10. package/dist/browser/chromium-process.d.ts.map +1 -1
  11. package/dist/browser/chromium-process.js +78 -13
  12. package/dist/browser/electron.d.ts +1 -1
  13. package/dist/browser/electron.d.ts.map +1 -1
  14. package/dist/browser/electron.js +6 -3
  15. package/dist/browser/executables.d.ts +16 -1
  16. package/dist/browser/executables.d.ts.map +1 -1
  17. package/dist/browser/executables.js +50 -18
  18. package/dist/browser/firefox/accessible-names.d.ts +5 -4
  19. package/dist/browser/firefox/accessible-names.d.ts.map +1 -1
  20. package/dist/browser/firefox/accessible-names.js +11 -6
  21. package/dist/browser/firefox/bridge.js +3 -2
  22. package/dist/browser/firefox/executable.d.ts +14 -14
  23. package/dist/browser/firefox/executable.d.ts.map +1 -1
  24. package/dist/browser/firefox/executable.js +29 -21
  25. package/dist/browser/firefox/launch.js +1 -1
  26. package/dist/browser/firefox/navigation.d.ts +5 -2
  27. package/dist/browser/firefox/navigation.d.ts.map +1 -1
  28. package/dist/browser/firefox/navigation.js +48 -14
  29. package/dist/browser/firefox/orphans.d.ts.map +1 -1
  30. package/dist/browser/firefox/orphans.js +34 -2
  31. package/dist/browser/firefox/page.d.ts.map +1 -1
  32. package/dist/browser/firefox/page.js +113 -19
  33. package/dist/browser/firefox/process.d.ts +20 -1
  34. package/dist/browser/firefox/process.d.ts.map +1 -1
  35. package/dist/browser/firefox/process.js +124 -22
  36. package/dist/browser/firefox/profile.d.ts +5 -0
  37. package/dist/browser/firefox/profile.d.ts.map +1 -1
  38. package/dist/browser/firefox/profile.js +17 -1
  39. package/dist/browser/firefox/storage.d.ts.map +1 -1
  40. package/dist/browser/firefox/storage.js +4 -2
  41. package/dist/browser/firefox/subscriptions.d.ts +26 -0
  42. package/dist/browser/firefox/subscriptions.d.ts.map +1 -0
  43. package/dist/browser/firefox/subscriptions.js +10 -0
  44. package/dist/browser/launch.d.ts +6 -0
  45. package/dist/browser/launch.d.ts.map +1 -1
  46. package/dist/browser/launch.js +14 -1
  47. package/dist/cli/commands/doctor.js +3 -3
  48. package/dist/cli/commands/inspect.js +3 -2
  49. package/dist/cli/commands/install.d.ts.map +1 -1
  50. package/dist/cli/commands/install.js +26 -13
  51. package/dist/cli/commands/licences.d.ts +22 -0
  52. package/dist/cli/commands/licences.d.ts.map +1 -1
  53. package/dist/cli/commands/licences.js +62 -20
  54. package/dist/cli/commands/run.js +2 -2
  55. package/dist/cli/doctor/checks.js +4 -1
  56. package/dist/cli/inspect/diagnostics.js +3 -3
  57. package/dist/cli/inspect/test-timeline.js +10 -10
  58. package/dist/cli/install/doctor-rows.d.ts.map +1 -1
  59. package/dist/cli/install/doctor-rows.js +5 -4
  60. package/dist/cli/install/file-changes.d.ts +44 -0
  61. package/dist/cli/install/file-changes.d.ts.map +1 -0
  62. package/dist/cli/install/file-changes.js +81 -0
  63. package/dist/cli/install/install-archive.d.ts +3 -1
  64. package/dist/cli/install/install-archive.d.ts.map +1 -1
  65. package/dist/cli/install/install-archive.js +80 -33
  66. package/dist/cli/install/lock.d.ts.map +1 -1
  67. package/dist/cli/install/lock.js +108 -21
  68. package/dist/cli/install/media-doctor.d.ts.map +1 -1
  69. package/dist/cli/install/media-doctor.js +2 -1
  70. package/dist/cli/install/media-install.d.ts.map +1 -1
  71. package/dist/cli/install/media-install.js +38 -19
  72. package/dist/cli/install/media-pins.d.ts +7 -1
  73. package/dist/cli/install/media-pins.d.ts.map +1 -1
  74. package/dist/cli/install/media-pins.js +23 -12
  75. package/dist/cli/install/media-record.d.ts.map +1 -1
  76. package/dist/cli/install/media-record.js +10 -9
  77. package/dist/cli/install/media-report.d.ts +3 -1
  78. package/dist/cli/install/media-report.d.ts.map +1 -1
  79. package/dist/cli/install/media-report.js +7 -5
  80. package/dist/cli/install/media-tools.d.ts.map +1 -1
  81. package/dist/cli/install/media-tools.js +6 -4
  82. package/dist/cli/install/process-start.d.ts +3 -2
  83. package/dist/cli/install/process-start.d.ts.map +1 -1
  84. package/dist/cli/install/process-start.js +4 -0
  85. package/dist/cli/install/report.d.ts.map +1 -1
  86. package/dist/cli/install/report.js +11 -7
  87. package/dist/cli/install/supervisor-install.d.ts +33 -0
  88. package/dist/cli/install/supervisor-install.d.ts.map +1 -0
  89. package/dist/cli/install/supervisor-install.js +158 -0
  90. package/dist/cli/install/supervisor-locate.d.ts +47 -0
  91. package/dist/cli/install/supervisor-locate.d.ts.map +1 -0
  92. package/dist/cli/install/supervisor-locate.js +113 -0
  93. package/dist/cli/install/supervisor-pins.d.ts +21 -0
  94. package/dist/cli/install/supervisor-pins.d.ts.map +1 -0
  95. package/dist/cli/install/supervisor-pins.js +28 -0
  96. package/dist/cli/install/supervisor-record.d.ts +31 -0
  97. package/dist/cli/install/supervisor-record.d.ts.map +1 -0
  98. package/dist/cli/install/supervisor-record.js +104 -0
  99. package/dist/cli/install/supervisor-report.d.ts +11 -0
  100. package/dist/cli/install/supervisor-report.d.ts.map +1 -0
  101. package/dist/cli/install/supervisor-report.js +33 -0
  102. package/dist/cli/install/supervisor-tools.d.ts +43 -0
  103. package/dist/cli/install/supervisor-tools.d.ts.map +1 -0
  104. package/dist/cli/install/supervisor-tools.js +114 -0
  105. package/dist/cli/install/unpack.d.ts +23 -3
  106. package/dist/cli/install/unpack.d.ts.map +1 -1
  107. package/dist/cli/install/unpack.js +124 -10
  108. package/dist/cli/install/zip-member.d.ts +15 -0
  109. package/dist/cli/install/zip-member.d.ts.map +1 -0
  110. package/dist/cli/install/zip-member.js +77 -0
  111. package/dist/cli/interrupt.d.ts +1 -1
  112. package/dist/cli/interrupt.d.ts.map +1 -1
  113. package/dist/cli/interrupt.js +15 -10
  114. package/dist/cli/terminal.d.ts +9 -1
  115. package/dist/cli/terminal.d.ts.map +1 -1
  116. package/dist/cli/terminal.js +14 -0
  117. package/dist/config/define.d.ts +2 -2
  118. package/dist/config/define.js +2 -2
  119. package/dist/diagnostics/firefox-collector.d.ts +11 -10
  120. package/dist/diagnostics/firefox-collector.d.ts.map +1 -1
  121. package/dist/diagnostics/firefox-collector.js +16 -11
  122. package/dist/media/client.d.ts +2 -1
  123. package/dist/media/client.d.ts.map +1 -1
  124. package/dist/media/client.js +16 -5
  125. package/dist/media/host.d.ts +32 -0
  126. package/dist/media/host.d.ts.map +1 -0
  127. package/dist/media/host.js +50 -0
  128. package/dist/media/locate.d.ts.map +1 -1
  129. package/dist/media/locate.js +4 -3
  130. package/dist/native/processes.d.ts.map +1 -1
  131. package/dist/native/processes.js +7 -3
  132. package/dist/protocol/files.d.ts +1 -1
  133. package/dist/protocol/files.d.ts.map +1 -1
  134. package/dist/protocol/files.js +10 -5
  135. package/dist/reporters/commands.d.ts +12 -3
  136. package/dist/reporters/commands.d.ts.map +1 -1
  137. package/dist/reporters/commands.js +29 -11
  138. package/dist/reporters/human-card.js +1 -1
  139. package/dist/reporters/human.d.ts +3 -1
  140. package/dist/reporters/human.d.ts.map +1 -1
  141. package/dist/reporters/human.js +8 -8
  142. package/dist/reporters/style.d.ts +21 -2
  143. package/dist/reporters/style.d.ts.map +1 -1
  144. package/dist/reporters/style.js +11 -2
  145. package/dist/runner/app-server.d.ts +27 -7
  146. package/dist/runner/app-server.d.ts.map +1 -1
  147. package/dist/runner/app-server.js +74 -24
  148. package/dist/runner/child.js +6 -2
  149. package/dist/runner/media-leftovers.d.ts +7 -3
  150. package/dist/runner/media-leftovers.d.ts.map +1 -1
  151. package/dist/runner/media-leftovers.js +27 -6
  152. package/dist/runner/resources.d.ts +8 -0
  153. package/dist/runner/resources.d.ts.map +1 -1
  154. package/dist/runner/resources.js +12 -1
  155. package/dist/runner/run-session.d.ts.map +1 -1
  156. package/dist/runner/run-session.js +9 -4
  157. package/dist/runner/run.d.ts.map +1 -1
  158. package/dist/runner/run.js +2 -1
  159. package/dist/runner/target-drivers.d.ts +5 -2
  160. package/dist/runner/target-drivers.d.ts.map +1 -1
  161. package/dist/runner/target-drivers.js +10 -1
  162. package/dist/runner/test-file-process.d.ts +8 -0
  163. package/dist/runner/test-file-process.d.ts.map +1 -1
  164. package/dist/runner/test-file-process.js +26 -3
  165. package/dist/runner/test-pages.d.ts.map +1 -1
  166. package/dist/runner/test-pages.js +5 -2
  167. package/dist/shared/environment.d.ts +17 -0
  168. package/dist/shared/environment.d.ts.map +1 -0
  169. package/dist/shared/environment.js +27 -0
  170. package/dist/shared/metadata-process-worker.js +15 -7
  171. package/dist/shared/metadata-process.d.ts +5 -0
  172. package/dist/shared/metadata-process.d.ts.map +1 -1
  173. package/dist/shared/metadata-process.js +36 -6
  174. package/dist/shared/posix-path.d.ts +12 -0
  175. package/dist/shared/posix-path.d.ts.map +1 -1
  176. package/dist/shared/posix-path.js +16 -0
  177. package/dist/shared/private-file.d.ts +38 -0
  178. package/dist/shared/private-file.d.ts.map +1 -0
  179. package/dist/shared/private-file.js +132 -0
  180. package/dist/shared/process-ownership.d.ts +52 -2
  181. package/dist/shared/process-ownership.d.ts.map +1 -1
  182. package/dist/shared/process-ownership.js +104 -11
  183. package/dist/shared/process-supervisor.d.ts +309 -0
  184. package/dist/shared/process-supervisor.d.ts.map +1 -0
  185. package/dist/shared/process-supervisor.js +737 -0
  186. package/dist/shared/process-system.d.ts +113 -0
  187. package/dist/shared/process-system.d.ts.map +1 -0
  188. package/dist/shared/process-system.js +254 -0
  189. package/dist/shared/regular-file.d.ts +2 -2
  190. package/dist/shared/regular-file.d.ts.map +1 -1
  191. package/dist/shared/regular-file.js +19 -2
  192. package/dist/store/artifacts.d.ts +5 -4
  193. package/dist/store/artifacts.d.ts.map +1 -1
  194. package/dist/store/artifacts.js +18 -5
  195. package/dist/store/run-store.d.ts +5 -2
  196. package/dist/store/run-store.d.ts.map +1 -1
  197. package/dist/store/run-store.js +48 -4
  198. package/media/Cargo.lock +16 -0
  199. package/media/Cargo.toml +16 -1
  200. package/media/src/encoder.rs +56 -12
  201. package/media/src/frame.rs +7 -1
  202. package/media/src/main.rs +20 -2
  203. package/media/src/place.rs +230 -11
  204. package/media/src/process_ownership_windows.rs +1058 -0
  205. package/media/src/recording.rs +15 -7
  206. package/media/src/replies.rs +55 -0
  207. package/media/src/server.rs +72 -2
  208. package/media/src/store.rs +71 -2
  209. package/package.json +9 -4
  210. package/src/cli/install/media-notices.txt +465 -1
  211. package/supervisor/Cargo.lock +25 -0
  212. package/supervisor/Cargo.toml +22 -0
  213. package/supervisor/NOTICES +464 -0
  214. package/supervisor/build.rs +44 -0
  215. package/supervisor/src/command.rs +612 -0
  216. package/supervisor/src/jobs.rs +1650 -0
  217. package/supervisor/src/main.rs +71 -0
  218. 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
- /** Where Firefox installs on macOS, which a target without an executable path, and no installed pinned build, uses. */
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: '133.0.3', buildId: '20241209150345' }
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 133.0.3 build 20241209150345.'
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, the one platform the Firefox driver was
35
- * exercised on. Any other is refused by name rather than tried.
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
- return `Retest runs Firefox on macOS on Apple silicon, and this machine is ${platform} ${arch}.`;
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 macOS installs it, only when its
47
- * files say it is the tested release, since the driver encodes that release's facts and another would run every test
48
- * on an engine nobody exercised. Nothing is downloaded. A machine Retest does not run Firefox on, a system Firefox of
49
- * another release, or no Firefox at all fails setup by name, saying what it found and the ways forward.
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 systemPath = options.systemPath ?? defaultFirefoxPath;
69
+ const systemPaths = options.systemPath === undefined ? systemFirefoxPaths(systemHost()) : [options.systemPath];
66
70
  const tested = testedFirefox();
67
- // `retest install firefox` refuses the pin until its archive has a checksum, so the way named is the app itself.
68
- const install = `install the tested Firefox ${tested.version} at ${systemPath}`;
69
- if (!existsSync(systemPath)) {
70
- const message = `No Firefox for the target ${target.name} of the app ${app}: no pinned build is in Retest's cache, and none is at ${systemPath}. Either ${install}, or give the target an executablePath.`;
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: '133.0.3', buildId: '20241209150345' }
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 !== firefox.pid)
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
- /** Waits for an error page to commit and show, for a navigation Firefox refused at once; the time it may take is short. */
95
- errorPage(deadline: Deadline): Promise<Outcome>;
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":"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"}
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 at once, such as one to a blocked port: an error that names the reason
10
- // in its own words, which is all of it Retest keeps.
11
- const refusedNavigation = /^unknown error: ([A-Za-z]+)$/;
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
- await watcher.errorPage(new Deadline(Math.min(errorPageWaitMs, deadline.remainingMs), deadline.signal === undefined ? {} : { signal: deadline.signal }));
49
- return { ok: false, failure: navigationFailed(address, started.refused) };
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
- const current = page.currentUrl();
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
- throw error;
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
- throw error;
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
- /** Waits for an error page to commit and show, for a navigation Firefox refused at once; the time it may take is short. */
305
- errorPage(deadline) {
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;AAOrD;;;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,CAoB3G"}
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(folder.slice(folder.lastIndexOf('/') + 1))?.[1]);
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":"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"}
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] }, s.object({}), send());
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
- await this.#subscribe(['network.responseStarted'], send());
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] }, s.object({}), send);
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
- if (ended.length > 0 && this.#client.closeReason === undefined && this.#lostReason === undefined) {
751
- this.#client.send('session.unsubscribe', { events: ended, contexts: [this.#context] }).catch(() => undefined);
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
- await attempt("stop following the page's events", () => this.#client.send('session.unsubscribe', { events: ['browsingContext', 'script'], contexts: [this.#context] }, options()));
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
- if (navigation === null || navigation !== this.#pending?.id)
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(navigation);
1258
+ this.#started.delete(pending.id);
1172
1259
  for (const watcher of [...this.#watchers])
1173
- watcher.onAbandoned(navigation, request.url);
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
- if (this.#pending?.id === navigation) {
1267
+ const pending = this.#pendingNamed(navigation);
1268
+ if (pending !== undefined) {
1179
1269
  this.#pending = undefined;
1180
1270
  this.#causes.stoppedLoading();
1181
1271
  }
1182
- this.#started.delete(navigation);
1272
+ const id = pending?.id ?? navigation;
1273
+ this.#started.delete(id);
1183
1274
  for (const watcher of [...this.#watchers])
1184
- watcher.onFailed(navigation);
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
- this.#committed(href);
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)