@itmetsam/samurai-framework 0.10.0 → 0.12.1

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 (326) hide show
  1. package/README.md +3 -4
  2. package/dist/assert/assertion-error.d.ts +1 -1
  3. package/dist/assert/expect.d.ts +5 -50
  4. package/dist/assert/expect.js +5 -182
  5. package/dist/assert/locator-assertions.d.ts +22 -0
  6. package/dist/assert/locator-assertions.js +61 -0
  7. package/dist/assert/pending-assertions.d.ts +7 -0
  8. package/dist/assert/pending-assertions.js +40 -0
  9. package/dist/assert/text-match.d.ts +4 -0
  10. package/dist/assert/text-match.js +11 -0
  11. package/dist/assert/value-assertions.d.ts +23 -0
  12. package/dist/assert/value-assertions.js +82 -0
  13. package/dist/browser/browser-context.d.ts +0 -1
  14. package/dist/browser/browser-context.js +13 -5
  15. package/dist/browser/browser-finder.js +5 -9
  16. package/dist/browser/browser.d.ts +3 -31
  17. package/dist/browser/browser.js +36 -219
  18. package/dist/browser/chrome-launch.d.ts +2 -1
  19. package/dist/browser/dialog-handling.d.ts +32 -0
  20. package/dist/browser/dialog-handling.js +85 -0
  21. package/dist/browser/document-watcher.d.ts +44 -0
  22. package/dist/browser/document-watcher.js +160 -0
  23. package/dist/browser/download-folders.d.ts +0 -1
  24. package/dist/browser/download-folders.js +7 -15
  25. package/dist/browser/download.js +5 -10
  26. package/dist/browser/downloads-dir.js +2 -2
  27. package/dist/browser/emit.d.ts +7 -0
  28. package/dist/browser/emit.js +16 -0
  29. package/dist/browser/emulation.d.ts +1 -1
  30. package/dist/browser/emulation.js +54 -18
  31. package/dist/browser/file-chooser.js +4 -11
  32. package/dist/browser/history.d.ts +6 -0
  33. package/dist/browser/history.js +89 -0
  34. package/dist/browser/index.d.ts +2 -0
  35. package/dist/browser/index.js +2 -0
  36. package/dist/browser/init-script.d.ts +2 -0
  37. package/dist/browser/init-script.js +16 -0
  38. package/dist/browser/launch-firefox.d.ts +20 -0
  39. package/dist/browser/launch-firefox.js +66 -0
  40. package/dist/browser/launch-flags.d.ts +18 -0
  41. package/dist/browser/launch-flags.js +100 -0
  42. package/dist/browser/lazy.d.ts +21 -0
  43. package/dist/browser/lazy.js +44 -0
  44. package/dist/browser/page-events.d.ts +44 -0
  45. package/dist/browser/page-events.js +58 -0
  46. package/dist/browser/page-logs.js +2 -9
  47. package/dist/browser/page-navigation.d.ts +58 -0
  48. package/dist/browser/page-navigation.js +108 -0
  49. package/dist/browser/page-network.d.ts +69 -0
  50. package/dist/browser/page-network.js +163 -0
  51. package/dist/browser/page-settings.d.ts +35 -0
  52. package/dist/browser/page-settings.js +51 -0
  53. package/dist/browser/page-transfers.d.ts +36 -0
  54. package/dist/browser/page-transfers.js +50 -0
  55. package/dist/browser/page.d.ts +39 -154
  56. package/dist/browser/page.js +134 -758
  57. package/dist/browser/pdf.d.ts +1 -1
  58. package/dist/browser/session.d.ts +16 -0
  59. package/dist/browser/session.js +32 -0
  60. package/dist/browser/viewport.d.ts +19 -0
  61. package/dist/browser/viewport.js +72 -0
  62. package/dist/browser/warm-up.d.ts +1 -1
  63. package/dist/browser/warm-up.js +2 -1
  64. package/dist/browsers/index.d.ts +1 -1
  65. package/dist/browsers/index.js +1 -1
  66. package/dist/browsers/pins.d.ts +0 -2
  67. package/dist/browsers/pins.js +0 -2
  68. package/dist/browsers/store.d.ts +0 -2
  69. package/dist/browsers/store.js +1 -2
  70. package/dist/cli/args.d.ts +13 -6
  71. package/dist/cli/args.js +13 -10
  72. package/dist/cli/browsers.d.ts +1 -2
  73. package/dist/cli/browsers.js +2 -2
  74. package/dist/cli/console-reporter.js +3 -2
  75. package/dist/cli/flow-reporter.js +2 -1
  76. package/dist/cli/flow.d.ts +1 -2
  77. package/dist/cli/flow.js +25 -22
  78. package/dist/cli/init.js +2 -7
  79. package/dist/cli/io.d.ts +10 -0
  80. package/dist/cli/io.js +1 -0
  81. package/dist/cli/main.d.ts +3 -10
  82. package/dist/cli/main.js +10 -143
  83. package/dist/cli/paint.d.ts +2 -0
  84. package/dist/cli/paint.js +4 -0
  85. package/dist/cli/record.d.ts +4 -0
  86. package/dist/cli/record.js +93 -0
  87. package/dist/cli/usage.d.ts +1 -0
  88. package/dist/cli/usage.js +58 -0
  89. package/dist/cli/version.d.ts +2 -0
  90. package/dist/cli/version.js +7 -0
  91. package/dist/config/browsers.d.ts +11 -0
  92. package/dist/config/browsers.js +32 -0
  93. package/dist/config/config.js +4 -1
  94. package/dist/config/mask.d.ts +1 -1
  95. package/dist/config/mask.js +7 -2
  96. package/dist/config/run-settings.d.ts +1 -2
  97. package/dist/config/run-settings.js +1 -6
  98. package/dist/config/timeouts.d.ts +2 -0
  99. package/dist/config/timeouts.js +13 -0
  100. package/dist/config/workers.d.ts +14 -0
  101. package/dist/config/workers.js +29 -0
  102. package/dist/flows/run/branch-context.d.ts +14 -0
  103. package/dist/flows/run/branch-context.js +35 -0
  104. package/dist/flows/run/handlers/condition.d.ts +1 -1
  105. package/dist/flows/run/handlers/end.d.ts +1 -1
  106. package/dist/flows/run/handlers/group.d.ts +1 -1
  107. package/dist/flows/run/handlers/index.d.ts +2 -2
  108. package/dist/flows/run/handlers/parallel.d.ts +1 -1
  109. package/dist/flows/run/handlers/start.d.ts +1 -1
  110. package/dist/flows/run/handlers/support.d.ts +3 -27
  111. package/dist/flows/run/handlers/test.d.ts +1 -1
  112. package/dist/flows/run/handlers/types.d.ts +27 -0
  113. package/dist/flows/run/handlers/types.js +1 -0
  114. package/dist/flows/run/index.d.ts +1 -1
  115. package/dist/flows/run/prepare-flow.d.ts +14 -0
  116. package/dist/flows/run/prepare-flow.js +44 -0
  117. package/dist/flows/run/project.d.ts +33 -2
  118. package/dist/flows/run/project.js +139 -57
  119. package/dist/flows/run/run.d.ts +3 -4
  120. package/dist/flows/run/run.js +23 -193
  121. package/dist/flows/run/walker.d.ts +56 -0
  122. package/dist/flows/run/walker.js +225 -0
  123. package/dist/flows/steps/catalog.d.ts +1 -1
  124. package/dist/flows/steps/handler.d.ts +1 -1
  125. package/dist/flows/steps/set.d.ts +1 -1
  126. package/dist/index.js +1 -1
  127. package/dist/locator/actionability.d.ts +33 -0
  128. package/dist/locator/actionability.js +104 -0
  129. package/dist/locator/chain-resolver.d.ts +20 -0
  130. package/dist/locator/chain-resolver.js +102 -0
  131. package/dist/locator/document-swap.d.ts +1 -0
  132. package/dist/locator/document-swap.js +11 -0
  133. package/dist/locator/element-state.d.ts +3 -1
  134. package/dist/locator/element-state.js +11 -3
  135. package/dist/locator/file-actions.d.ts +35 -0
  136. package/dist/locator/file-actions.js +55 -0
  137. package/dist/{script/helpers.js → locator/helper-realm.js} +2 -2
  138. package/dist/locator/input-actions.d.ts +18 -0
  139. package/dist/locator/input-actions.js +80 -0
  140. package/dist/locator/label-locator.d.ts +1 -1
  141. package/dist/locator/label-locator.js +5 -12
  142. package/dist/locator/locator.d.ts +14 -37
  143. package/dist/locator/locator.js +68 -424
  144. package/dist/locator/page-scripts.d.ts +16 -0
  145. package/dist/locator/page-scripts.js +54 -0
  146. package/dist/locator/page-search.d.ts +10 -0
  147. package/dist/locator/page-search.js +19 -0
  148. package/dist/locator/selector-errors.d.ts +0 -4
  149. package/dist/locator/selector-errors.js +0 -7
  150. package/dist/locator/selector.d.ts +14 -13
  151. package/dist/locator/selector.js +10 -16
  152. package/dist/locator/text-locator.d.ts +1 -1
  153. package/dist/locator/text-locator.js +5 -9
  154. package/dist/logger/index.d.ts +6 -0
  155. package/dist/logger/index.js +14 -7
  156. package/dist/network/bidi-wire.d.ts +10 -0
  157. package/dist/network/bidi-wire.js +38 -0
  158. package/dist/network/intercept.d.ts +30 -0
  159. package/dist/network/intercept.js +130 -0
  160. package/dist/network/network-tracker.d.ts +10 -47
  161. package/dist/network/network-tracker.js +34 -68
  162. package/dist/network/network-types.d.ts +33 -0
  163. package/dist/network/network-types.js +1 -0
  164. package/dist/network/network-wait.d.ts +7 -2
  165. package/dist/network/network-wait.js +14 -4
  166. package/dist/network/response.d.ts +1 -1
  167. package/dist/network/response.js +1 -1
  168. package/dist/network/route.d.ts +92 -0
  169. package/dist/network/route.js +142 -0
  170. package/dist/network/router.d.ts +6 -91
  171. package/dist/network/router.js +25 -258
  172. package/dist/recorder/expectation.d.ts +5 -0
  173. package/dist/recorder/expectation.js +20 -0
  174. package/dist/recorder/files-recorder.d.ts +30 -0
  175. package/dist/recorder/files-recorder.js +88 -0
  176. package/dist/recorder/normalise.d.ts +17 -6
  177. package/dist/recorder/normalise.js +96 -91
  178. package/dist/recorder/options.d.ts +56 -0
  179. package/dist/recorder/options.js +1 -0
  180. package/dist/recorder/page-messages.d.ts +35 -0
  181. package/dist/recorder/page-messages.js +26 -0
  182. package/dist/recorder/ranking.js +1 -0
  183. package/dist/recorder/realms.d.ts +6 -0
  184. package/dist/recorder/realms.js +13 -0
  185. package/dist/recorder/recorder.d.ts +10 -74
  186. package/dist/recorder/recorder.js +39 -199
  187. package/dist/recorder/session-browser.d.ts +22 -0
  188. package/dist/recorder/session-browser.js +21 -0
  189. package/dist/recorder/session-secrets.d.ts +8 -0
  190. package/dist/recorder/session-secrets.js +45 -0
  191. package/dist/recorder/session.d.ts +5 -14
  192. package/dist/recorder/session.js +22 -120
  193. package/dist/recorder/spec-source.d.ts +39 -0
  194. package/dist/recorder/spec-source.js +84 -0
  195. package/dist/recorder/toolbar/action-guard.d.ts +21 -0
  196. package/dist/recorder/toolbar/action-guard.js +43 -0
  197. package/dist/recorder/toolbar/actions/assert.d.ts +4 -0
  198. package/dist/recorder/toolbar/actions/assert.js +52 -0
  199. package/dist/recorder/toolbar/actions/insert.d.ts +6 -0
  200. package/dist/recorder/toolbar/actions/insert.js +93 -0
  201. package/dist/recorder/toolbar/actions/pick.d.ts +8 -0
  202. package/dist/recorder/toolbar/actions/pick.js +26 -0
  203. package/dist/recorder/toolbar/actions/reply.d.ts +7 -0
  204. package/dist/recorder/toolbar/actions/reply.js +10 -0
  205. package/dist/recorder/toolbar/controller-fixture.d.ts +47 -0
  206. package/dist/recorder/toolbar/controller-fixture.js +129 -0
  207. package/dist/recorder/toolbar/controller.d.ts +12 -78
  208. package/dist/recorder/toolbar/controller.js +35 -247
  209. package/dist/recorder/toolbar/inspect.d.ts +1 -3
  210. package/dist/recorder/toolbar/inspect.js +2 -2
  211. package/dist/recorder/toolbar/page-scope.d.ts +19 -0
  212. package/dist/recorder/toolbar/page-scope.js +43 -0
  213. package/dist/recorder/toolbar/protocol.d.ts +2 -1
  214. package/dist/recorder/toolbar/source/attach.d.ts +2 -0
  215. package/dist/recorder/toolbar/source/attach.js +41 -0
  216. package/dist/recorder/toolbar/source/dom.d.ts +2 -0
  217. package/dist/recorder/toolbar/source/dom.js +47 -0
  218. package/dist/recorder/toolbar/source/drag.d.ts +2 -0
  219. package/dist/recorder/toolbar/source/drag.js +32 -0
  220. package/dist/recorder/toolbar/source/events-page.d.ts +2 -0
  221. package/dist/recorder/toolbar/source/events-page.js +67 -0
  222. package/dist/recorder/toolbar/source/events-toolbar.d.ts +2 -0
  223. package/dist/recorder/toolbar/source/events-toolbar.js +35 -0
  224. package/dist/recorder/toolbar/source/fields.d.ts +2 -0
  225. package/dist/recorder/toolbar/source/fields.js +44 -0
  226. package/dist/recorder/toolbar/source/handlers.d.ts +2 -0
  227. package/dist/recorder/toolbar/source/handlers.js +134 -0
  228. package/dist/recorder/toolbar/source/names.d.ts +2 -0
  229. package/dist/recorder/toolbar/source/names.js +12 -0
  230. package/dist/recorder/toolbar/source/picking.d.ts +2 -0
  231. package/dist/recorder/toolbar/source/picking.js +72 -0
  232. package/dist/recorder/toolbar/source/placement.d.ts +2 -0
  233. package/dist/recorder/toolbar/source/placement.js +45 -0
  234. package/dist/recorder/toolbar/source/recorder-side.d.ts +2 -0
  235. package/dist/recorder/toolbar/source/recorder-side.js +54 -0
  236. package/dist/recorder/toolbar/source/setup.d.ts +2 -0
  237. package/dist/recorder/toolbar/source/setup.js +50 -0
  238. package/dist/recorder/toolbar/source/views-panels.d.ts +2 -0
  239. package/dist/recorder/toolbar/source/views-panels.js +150 -0
  240. package/dist/recorder/toolbar/source/views-rail.d.ts +2 -0
  241. package/dist/recorder/toolbar/source/views-rail.js +40 -0
  242. package/dist/recorder/toolbar/source/views-render.d.ts +2 -0
  243. package/dist/recorder/toolbar/source/views-render.js +48 -0
  244. package/dist/recorder/toolbar/source.js +30 -856
  245. package/dist/recorder/toolbar/types.d.ts +70 -0
  246. package/dist/recorder/toolbar/types.js +1 -0
  247. package/dist/recorder/toolbar-bridge.d.ts +43 -0
  248. package/dist/recorder/toolbar-bridge.js +106 -0
  249. package/dist/runner/browsers.d.ts +0 -12
  250. package/dist/runner/browsers.js +1 -33
  251. package/dist/runner/options.d.ts +31 -0
  252. package/dist/runner/options.js +1 -0
  253. package/dist/runner/pooled-run.d.ts +43 -0
  254. package/dist/runner/pooled-run.js +75 -0
  255. package/dist/runner/ports.d.ts +2 -0
  256. package/dist/runner/ports.js +2 -0
  257. package/dist/runner/registry.d.ts +1 -1
  258. package/dist/runner/registry.js +1 -1
  259. package/dist/runner/reporter.d.ts +31 -10
  260. package/dist/runner/reporter.js +39 -6
  261. package/dist/runner/run-test.d.ts +16 -0
  262. package/dist/runner/run-test.js +178 -0
  263. package/dist/runner/run.d.ts +5 -3
  264. package/dist/runner/run.js +28 -29
  265. package/dist/runner/select.d.ts +24 -0
  266. package/dist/runner/select.js +77 -0
  267. package/dist/runner/test-dir.d.ts +13 -0
  268. package/dist/runner/test-dir.js +27 -0
  269. package/dist/runner/test-runner.d.ts +25 -56
  270. package/dist/runner/test-runner.js +81 -270
  271. package/dist/runner/worker-pool.d.ts +67 -0
  272. package/dist/runner/worker-pool.js +275 -0
  273. package/dist/runner/worker-process.d.ts +6 -0
  274. package/dist/runner/worker-process.js +54 -0
  275. package/dist/runner/worker-protocol.d.ts +67 -0
  276. package/dist/runner/worker-protocol.js +1 -0
  277. package/dist/runner/worker-session.d.ts +7 -0
  278. package/dist/runner/worker-session.js +60 -0
  279. package/dist/runner/worker.d.ts +26 -0
  280. package/dist/runner/worker.js +121 -0
  281. package/dist/runner/workers-option.d.ts +15 -0
  282. package/dist/runner/workers-option.js +22 -0
  283. package/dist/script/call-function.d.ts +1 -9
  284. package/dist/script/call-function.js +2 -19
  285. package/dist/script/element-handle.d.ts +1 -2
  286. package/dist/script/element-handle.js +1 -2
  287. package/dist/script/serialize.d.ts +3 -12
  288. package/dist/script/serialize.js +53 -62
  289. package/dist/steps/parse/call.d.ts +3 -0
  290. package/dist/steps/parse/call.js +81 -0
  291. package/dist/steps/parse/expectation.d.ts +5 -0
  292. package/dist/steps/parse/expectation.js +71 -0
  293. package/dist/steps/parse/file-entry.d.ts +4 -0
  294. package/dist/steps/parse/file-entry.js +74 -0
  295. package/dist/steps/parse/literals.d.ts +4 -0
  296. package/dist/steps/parse/literals.js +28 -0
  297. package/dist/steps/parse/step.d.ts +4 -0
  298. package/dist/steps/parse/step.js +81 -0
  299. package/dist/steps/parse.js +2 -323
  300. package/dist/transport/bidi-connection.d.ts +4 -15
  301. package/dist/transport/bidi-connection.js +9 -63
  302. package/dist/transport/log-redaction.d.ts +13 -0
  303. package/dist/transport/log-redaction.js +51 -0
  304. package/dist/transport/subscriptions.js +18 -13
  305. package/dist/transport/unsupported.d.ts +7 -0
  306. package/dist/transport/unsupported.js +13 -0
  307. package/dist/types/bidi-modules/browser.d.ts +1 -12
  308. package/dist/types/bidi-modules/browsing-context.d.ts +8 -18
  309. package/dist/types/bidi-modules/emulation.d.ts +29 -21
  310. package/dist/types/bidi-modules/input.d.ts +1 -9
  311. package/dist/types/bidi-modules/network.d.ts +29 -24
  312. package/dist/types/bidi-modules/script.d.ts +0 -9
  313. package/dist/types/bidi-modules/session.d.ts +0 -8
  314. package/dist/types/bidi-modules/storage.d.ts +0 -5
  315. package/dist/types/bidi.d.ts +7 -37
  316. package/dist/types/config.d.ts +12 -0
  317. package/dist/types/test.d.ts +8 -3
  318. package/dist/wait/wait-until.d.ts +1 -5
  319. package/dist/wait/wait-until.js +7 -16
  320. package/package.json +3 -2
  321. package/dist/config/default.d.ts +0 -2
  322. package/dist/config/default.js +0 -3
  323. package/dist/types/bidi-modules/web-extensions.d.ts +0 -9
  324. package/dist/types/bidi-protocols/command.d.ts +0 -7
  325. package/dist/types/bidi-protocols/event.d.ts +0 -17
  326. /package/dist/{script/helpers.d.ts → locator/helper-realm.d.ts} +0 -0
@@ -1,105 +1,65 @@
1
- import Locator, {} from "../locator/locator.js";
1
+ import Locator from "../locator/locator.js";
2
2
  import { cssSelector, labelSelector, roleSelector, testIdSelector, textSelector, } from "../locator/selector.js";
3
3
  import EventEmitter from "node:events";
4
4
  import path from "node:path";
5
- import { projectDir, readConfig } from "../config/config.js";
6
- import { peekRunSettings } from "../config/run-settings.js";
5
+ import { projectDir } from "../config/config.js";
7
6
  import logger from "../logger/index.js";
8
- import { NetworkTracker, } from "../network/network-tracker.js";
9
- import { Recorder } from "../recorder/recorder.js";
10
7
  import { ContextTree } from "./context-tree.js";
11
- import { Dialog } from "./dialog.js";
12
8
  import { printPdf } from "./pdf.js";
13
9
  import { applyEmulation } from "./emulation.js";
14
- import { Download, DownloadTracker } from "./download.js";
15
- import { FileChooser, FileChooserTracker } from "./file-chooser.js";
16
- import { DownloadWaitTimeoutError, FileChooserTimeoutError, } from "./page-wait.js";
17
10
  import { takeScreenshot, writeOutput, } from "./screenshot.js";
18
11
  import { Evidence } from "./evidence.js";
19
- import { PageLogs, } from "./page-logs.js";
20
- import { Router } from "../network/router.js";
21
- import { DataCollector, ResponseBodyUnavailableError, } from "../network/data-collector.js";
12
+ import { PageLogs } from "./page-logs.js";
13
+ import { Lazy } from "./lazy.js";
22
14
  import { Response } from "../network/response.js";
23
- import { RequestTimeoutError, ResponseTimeoutError, toPredicate, } from "../network/network-wait.js";
24
- import { UnsupportedOperationError } from "../locator/selector-errors.js";
25
- import { toFunctionDeclaration } from "../script/call-function.js";
26
- import { HelperRealm } from "../script/helpers.js";
15
+ import { traverseHistory } from "./history.js";
16
+ import { DialogHandling } from "./dialog-handling.js";
17
+ import { DocumentWatcher } from "./document-watcher.js";
18
+ import { PageNavigation } from "./page-navigation.js";
19
+ import { PageNetwork } from "./page-network.js";
20
+ import { PageTransfers } from "./page-transfers.js";
21
+ import { initScriptSource } from "./init-script.js";
22
+ import { sourcesFor, waitForPageEvent, } from "./page-events.js";
23
+ import { PageConfig, } from "./page-settings.js";
24
+ import { applyViewport, DEFAULT_VIEWPORT } from "./viewport.js";
25
+ import { HelperRealm } from "../locator/helper-realm.js";
27
26
  import { addPreload } from "../script/preload.js";
28
27
  import { BiDiError } from "../transport/bidi-error.js";
29
- import { WaitTimeoutError } from "../wait/wait-until.js";
30
- import { LoadStateTimeoutError, NavigationError } from "./navigation-error.js";
31
28
  import { callFunction } from "../script/call-function.js";
32
- /** Time (ms) a navigation or a wait for a load state may take */
33
- const DEFAULT_NAVIGATION_TIMEOUT = 30000;
34
- /** Time (ms) a history traversal may go without a sign that the entry is shown before it is taken as done */
35
- const HISTORY_SETTLE_TIMEOUT = 1000;
36
- /** Time (ms) without requests in flight that counts as idle */
37
- const DEFAULT_IDLE_TIME = 500;
38
- /** The size a page gets unless config `use.viewport` says otherwise */
39
- const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
40
- /** Events a connection's browser rejected as unknown, so nothing asks for them again */
41
- const unsupportedEventsByConnector = new WeakMap();
42
- function unsupportedEvents(connector) {
43
- let events = unsupportedEventsByConnector.get(connector);
44
- if (!events)
45
- unsupportedEventsByConnector.set(connector, (events = new Set()));
46
- return events;
47
- }
48
- class NetworkTrackingDisabledError extends Error {
49
- constructor() {
50
- super("network tracking is disabled");
51
- this.name = "NetworkTrackingDisabledError";
52
- }
53
- }
54
- async function readSection(key) {
55
- try {
56
- return ((await readConfig(key)) ?? {});
57
- }
58
- catch (err) {
59
- logger.debug("Could not read %s config, using defaults", key, { err });
60
- return {};
61
- }
62
- }
63
29
  export default class Page {
64
30
  /** `settings` override what `samurai.config.ts` says; without them the config is read when needed */
65
31
  constructor(biDiConnector, contextId, settings) {
32
+ this.lazyTree = new Lazy();
66
33
  this.initScripts = new Set();
67
34
  this.pageEvents = new EventEmitter();
68
- /** How far navigations that did not wait for the load got, by navigation id (the latest few) */
69
- this.navigationProgress = new Map();
35
+ this.lazyLogs = new Lazy();
70
36
  this.errorsAllowed = false;
71
37
  this.isClosed = false;
72
- this.responses = new WeakMap();
73
- this.openDialogs = new Map();
74
- this.onDocumentEvent = (kind) => (info) => {
75
- if (info.context !== this.id || info.navigation === null)
76
- return;
77
- const progress = this.progressOf(info.navigation);
78
- progress.domContentLoaded = true;
79
- if (kind === "load")
80
- progress.load = true;
81
- };
82
- this.onDomContentLoaded = this.onDocumentEvent("domContentLoaded");
83
- this.onLoad = this.onDocumentEvent("load");
84
- this.onPromptOpened = (params) => {
85
- if (!this.ownsContext(params.context))
86
- return;
87
- this.dispatchDialog(new Dialog(this.biDiConnector, this.id, params), params).catch((err) => logger.error("Handling a dialog failed", { err }));
88
- };
89
- this.onPromptClosed = (params) => {
90
- if (!this.ownsContext(params.context))
91
- return;
92
- const dialog = this.openDialogs.get(params.context);
93
- this.openDialogs.delete(params.context);
94
- dialog?.markClosed({
95
- accepted: params.accepted,
96
- ...(params.userText !== undefined && { userText: params.userText }),
97
- });
98
- };
99
38
  this.biDiConnector = biDiConnector;
100
39
  this.id = contextId;
101
- this.settings = settings;
40
+ this.pageConfig = new PageConfig(settings);
102
41
  this.helperRealm = new HelperRealm(biDiConnector, contextId);
42
+ this.documents = new DocumentWatcher(biDiConnector, contextId);
43
+ this.net = new PageNetwork(biDiConnector, contextId, {
44
+ config: this.pageConfig,
45
+ tree: { ensure: () => this.tree(), peek: () => this.lazyTree.peek() },
46
+ emitter: this.pageEvents,
47
+ waitForEvent: (event, matches, options, onTimeout) => this.waitForPageEvent(event, matches, options, onTimeout),
48
+ });
49
+ this.nav = new PageNavigation(biDiConnector, contextId, {
50
+ config: this.pageConfig,
51
+ documents: this.documents,
52
+ assertOpen: () => this.assertOpen(),
53
+ url: () => this.url(),
54
+ waitForNetworkIdle: (options) => this.net.waitForIdle(options),
55
+ });
56
+ this.transfers = new PageTransfers(biDiConnector, contextId, {
57
+ config: this.pageConfig,
58
+ tree: { ensure: () => this.tree(), peek: () => this.lazyTree.peek() },
59
+ emitter: this.pageEvents,
60
+ waitForEvent: (event, matches, options, onTimeout) => this.waitForPageEvent(event, matches, options, onTimeout),
61
+ });
62
+ this.dialogs = new DialogHandling(biDiConnector, contextId, { ensure: () => this.tree(), peek: () => this.lazyTree.peek() }, this.pageEvents);
103
63
  }
104
64
  /** The BiDi browsing context id of this page's top-level context */
105
65
  get contextId() {
@@ -107,41 +67,35 @@ export default class Page {
107
67
  }
108
68
  /** The context tree, created on first use and shared by everything that needs to attribute frame events to this page */
109
69
  tree() {
110
- this.contextTree ??= ContextTree.create(this.biDiConnector).then((tree) => (this.loadedTree = tree), (err) => {
111
- this.contextTree = undefined;
112
- throw err;
113
- });
114
- return this.contextTree;
70
+ return this.lazyTree.get(() => ContextTree.create(this.biDiConnector));
115
71
  }
116
72
  /** Locates an element on the page and returns an interactable object */
117
73
  locator(xpath) {
74
+ return this.locate(xpath);
75
+ }
76
+ locate(selector) {
118
77
  this.assertOpen();
119
- return new Locator(xpath, this.biDiConnector, this.id, this.helperRealm);
78
+ return new Locator(selector, this.biDiConnector, this.id, this.helperRealm);
120
79
  }
121
80
  /** Locates elements with a CSS selector */
122
81
  getByCss(css) {
123
- this.assertOpen();
124
- return new Locator([cssSelector(css)], this.biDiConnector, this.id, this.helperRealm);
82
+ return this.locate([cssSelector(css)]);
125
83
  }
126
84
  /** Locates elements by their text: an exact match unless `match: "partial"` */
127
85
  getByText(text, options) {
128
- this.assertOpen();
129
- return new Locator([textSelector(text, options)], this.biDiConnector, this.id, this.helperRealm);
86
+ return this.locate([textSelector(text, options)]);
130
87
  }
131
88
  /** Locates form controls by their label text: an exact match unless `match: "partial"` */
132
89
  getByLabel(text, options) {
133
- this.assertOpen();
134
- return new Locator([labelSelector(text, options)], this.biDiConnector, this.id, this.helperRealm);
90
+ return this.locate([labelSelector(text, options)]);
135
91
  }
136
92
  /** Locates elements by their `data-testid` attribute */
137
93
  getByTestId(testId) {
138
- this.assertOpen();
139
- return new Locator([testIdSelector(testId)], this.biDiConnector, this.id, this.helperRealm);
94
+ return this.locate([testIdSelector(testId)]);
140
95
  }
141
96
  /** Locates elements by ARIA role and, optionally, accessible name */
142
97
  getByRole(role, options) {
143
- this.assertOpen();
144
- return new Locator([roleSelector(role, options)], this.biDiConnector, this.id, this.helperRealm);
98
+ return this.locate([roleSelector(role, options)]);
145
99
  }
146
100
  /**
147
101
  * Runs `script` before any page script in every document this page loads from now on, and once in
@@ -174,53 +128,30 @@ export default class Page {
174
128
  * Starts recording what a person does in the page as steps (see `Recorder`). The page must be driven by
175
129
  * a person; stop with `recorder.stop()`.
176
130
  */
177
- record(options) {
131
+ async record(options) {
178
132
  this.assertOpen();
133
+ // Loaded when asked for: the recorder is built on Page, so a static import would be a loop
134
+ const { Recorder } = await import("../recorder/recorder.js");
179
135
  return Recorder.start({ page: this, connector: this.biDiConnector, context: this.id }, options);
180
136
  }
181
137
  /** Removes everything the page registered in the browser: init scripts, framework helpers and event subscriptions */
182
138
  async dispose() {
183
139
  const scripts = [...this.initScripts];
184
- const tracker = this.networkTracker;
185
- this.networkTracker = undefined;
186
140
  const results = await Promise.allSettled([
187
141
  ...scripts.map((handle) => handle.dispose()),
188
142
  this.helperRealm.dispose(),
189
143
  // The tracker goes before the tree it reads
190
- tracker?.then((resolved) => resolved.dispose()).catch(() => undefined),
144
+ this.net.disposeTracker(),
191
145
  ]);
192
- const tree = this.contextTree;
193
- this.contextTree = undefined;
194
- const documentEvents = this.documentEvents;
195
- this.documentEvents = undefined;
196
- const dialogHandling = this.dialogHandling;
197
- this.dialogHandling = undefined;
198
- const pageLogs = this.pageLogs;
199
- this.pageLogs = undefined;
200
- this.loadedLogs = undefined;
201
- const downloads = this.downloadTracker;
202
- this.downloadTracker = undefined;
203
- const fileChoosers = this.fileChooserTracker;
204
- this.fileChooserTracker = undefined;
205
- const collector = this.collectorPromise;
206
- this.collectorPromise = undefined;
207
- const router = this.routerPromise;
208
- this.routerPromise = undefined;
209
- this.loadedRouter = undefined;
210
- this.biDiConnector.offEvent("browsingContext.userPromptOpened", this.onPromptOpened);
211
- this.biDiConnector.offEvent("browsingContext.userPromptClosed", this.onPromptClosed);
212
- this.openDialogs.clear();
213
- this.biDiConnector.offEvent("browsingContext.domContentLoaded", this.onDomContentLoaded);
214
- this.biDiConnector.offEvent("browsingContext.load", this.onLoad);
146
+ const tree = this.lazyTree.take();
147
+ const pageLogs = this.lazyLogs.take();
215
148
  results.push(...(await Promise.allSettled([
216
149
  tree?.then((resolved) => resolved.dispose()),
217
- documentEvents?.then((subscription) => subscription.unsubscribe()),
218
- dialogHandling?.then((subscription) => subscription.unsubscribe()),
150
+ this.documents.dispose(),
151
+ this.dialogs.dispose(),
219
152
  pageLogs?.then((logs) => logs.dispose()),
220
- router?.then((resolved) => resolved.dispose()),
221
- collector?.then((resolved) => resolved.dispose()),
222
- downloads?.then((resolved) => resolved.dispose()),
223
- fileChoosers?.then((resolved) => resolved.dispose()),
153
+ ...this.net.disposeRest(),
154
+ ...this.transfers.dispose(),
224
155
  ])));
225
156
  const failed = results.find((result) => result.status === "rejected");
226
157
  if (failed)
@@ -228,7 +159,7 @@ export default class Page {
228
159
  }
229
160
  /** Sets the base that relative `goto` URLs resolve against; `undefined` falls back to the run's baseURL */
230
161
  setBaseURL(url) {
231
- this.baseURL = url;
162
+ this.nav.setBaseURL(url);
232
163
  }
233
164
  /**
234
165
  * Navigates like `navigateTo`, but a URL starting with "/", "./" or "../" resolves against the base URL
@@ -237,14 +168,8 @@ export default class Page {
237
168
  * await page.goto("/products");
238
169
  * await page.goto("https://example.com", { wait: "interactive" });
239
170
  */
240
- async goto(url, options) {
241
- if (!/^\.{0,2}\//.test(url))
242
- return this.navigateTo(url, options);
243
- const base = this.baseURL ?? peekRunSettings()?.baseURL;
244
- if (!base) {
245
- throw new Error(`page.goto("${url}") needs a baseURL; set one in samurai.config.ts or the environment`);
246
- }
247
- return this.navigateTo(new URL(url, base).href, options);
171
+ goto(url, options) {
172
+ return this.nav.goto(url, options);
248
173
  }
249
174
  /**
250
175
  * Navigates to `url` and resolves when the page reached `wait` (default "complete", the load event).
@@ -254,168 +179,25 @@ export default class Page {
254
179
  * await page.navigateTo("example.com");
255
180
  * await page.navigateTo("localhost:3000", { wait: "interactive", protocol: "http" });
256
181
  */
257
- async navigateTo(url, options, protocol) {
258
- const given = typeof options === "string" ? { wait: options } : (options ?? {});
259
- const settings = await this.navigationSettings();
260
- const wait = given.wait ?? settings.waitUntil ?? "complete";
261
- const timeout = given.timeout ?? settings.timeout ?? DEFAULT_NAVIGATION_TIMEOUT;
262
- // A scheme is letters followed by ":" not directly followed by a digit, so "localhost:3000" is a host
263
- const hasScheme = /^[a-z][a-z\d+.-]*:(?!\d)/i.test(url);
264
- const parsedURL = hasScheme
265
- ? url
266
- : `${given.protocol ?? protocol ?? "https"}://${url}`;
267
- this.assertOpen();
268
- return this.navigation("navigateTo", async () => parsedURL, wait, timeout, async (sendOptions) => {
269
- const result = await this.biDiConnector.send("browsingContext.navigate", { context: this.id, url: parsedURL, wait }, sendOptions);
270
- return {
271
- navigation: result.navigation ?? null,
272
- url: result.url ?? parsedURL,
273
- };
274
- });
275
- }
276
- /**
277
- * Runs a navigation command and settles what waiting for it means: failures become NavigationErrors, and a
278
- * navigation that did not wait for the load is watched so `waitForLoadState` can tell it from the old page.
279
- */
280
- async navigation(operation, describe, wait, timeout, send) {
281
- // A navigation that does not wait for the load needs watching, or waitForLoadState cannot tell it from the old page
282
- if (wait !== "complete")
283
- await this.watchDocumentEvents();
284
- try {
285
- const result = await send({ timeout });
286
- this.lastNavigation = result.navigation ?? undefined;
287
- if (result.navigation && wait !== "complete") {
288
- // Tracked from now on: the document still showing is the old one until this navigation commits
289
- const progress = this.progressOf(result.navigation);
290
- if (wait === "interactive")
291
- progress.domContentLoaded = true;
292
- }
293
- return result;
294
- }
295
- catch (err) {
296
- if (err instanceof BiDiError) {
297
- const reason = err.code === "timeout" ? `timeout after ${timeout}ms` : err.message;
298
- throw new NavigationError(await describe(), reason, err.code, operation);
299
- }
300
- throw err;
301
- }
182
+ navigateTo(url, options, protocol) {
183
+ return this.nav.navigateTo(url, options, protocol);
302
184
  }
303
185
  /**
304
186
  * Reloads the page and resolves when it reached `wait` (default like `navigateTo`). Firefox does not support
305
187
  * `ignoreCache`; asking for it throws UnsupportedOperationError.
306
188
  */
307
- async reload(options = {}) {
308
- this.assertOpen();
309
- const settings = await this.navigationSettings();
310
- const wait = options.wait ?? settings.waitUntil ?? "complete";
311
- const timeout = options.timeout ?? settings.timeout ?? DEFAULT_NAVIGATION_TIMEOUT;
312
- return this.navigation("reload", () => this.url().catch(() => "the page"), wait, timeout, async (sendOptions) => {
313
- try {
314
- const result = await this.biDiConnector.send("browsingContext.reload", {
315
- context: this.id,
316
- wait,
317
- ...(options.ignoreCache && { ignoreCache: true }),
318
- }, sendOptions);
319
- return { navigation: result.navigation ?? null, url: result.url };
320
- }
321
- catch (err) {
322
- if (options.ignoreCache &&
323
- err instanceof BiDiError &&
324
- err.code === "unsupported operation") {
325
- throw new UnsupportedOperationError("reload({ ignoreCache }) is not supported by the browser");
326
- }
327
- throw err;
328
- }
329
- });
189
+ reload(options = {}) {
190
+ return this.nav.reload(options);
330
191
  }
331
192
  /** Goes back one entry in the history. False when there is none to go back to */
332
- goBack() {
333
- return this.traverse(-1);
193
+ async goBack() {
194
+ this.assertOpen();
195
+ return traverseHistory(this.biDiConnector, this.id, -1);
334
196
  }
335
197
  /** Goes forward one entry in the history. False when there is none */
336
- goForward() {
337
- return this.traverse(1);
338
- }
339
- async traverse(delta) {
198
+ async goForward() {
340
199
  this.assertOpen();
341
- if (delta === -1 && (await this.atFirstEntry()))
342
- return false;
343
- const settled = await this.watchHistoryTraversal();
344
- try {
345
- await this.biDiConnector.send("browsingContext.traverseHistory", {
346
- context: this.id,
347
- delta,
348
- });
349
- // Firefox answers once the entry is shown; Chrome answers before, and a script call in between can hit the
350
- // document that is going away
351
- await settled.done;
352
- return true;
353
- }
354
- catch (err) {
355
- if (err instanceof BiDiError && err.code === "no such history entry")
356
- return false;
357
- throw err;
358
- }
359
- finally {
360
- await settled.stop();
361
- }
362
- }
363
- /**
364
- * Whether going back would only reach the blank page a tab starts with. Firefox does not keep that page in the
365
- * history, Chrome does; the Navigation API (Chrome only) lets Chrome say the same as Firefox.
366
- */
367
- async atFirstEntry() {
368
- try {
369
- return await callFunction(this.biDiConnector, this.id, `() => {
370
- const nav = globalThis.navigation;
371
- if (!nav) return false;
372
- const entries = nav.entries();
373
- const index = nav.currentEntry.index;
374
- return index === 0 || (index === 1 && entries[0].url === "about:blank");
375
- }`, [], { awaitPromise: false });
376
- }
377
- catch (err) {
378
- logger.debug("Could not read the history position", { err });
379
- return false;
380
- }
381
- }
382
- /**
383
- * Starts listening for the end of a history traversal (the new entry committed, or the history or fragment changed
384
- * within the document). `done` resolves at the first such event, or after a second without one (a page restored from
385
- * the back-forward cache may say nothing); it never rejects.
386
- */
387
- async watchHistoryTraversal() {
388
- const events = [
389
- "browsingContext.navigationCommitted",
390
- "browsingContext.historyUpdated",
391
- "browsingContext.fragmentNavigated",
392
- ];
393
- let subscription;
394
- try {
395
- subscription = await this.biDiConnector.subscribe([...events]);
396
- }
397
- catch (err) {
398
- logger.debug("Could not watch a history traversal", { err });
399
- return { done: Promise.resolve(), stop: async () => { } };
400
- }
401
- let finish;
402
- const done = new Promise((resolve) => (finish = resolve));
403
- const timer = setTimeout(finish, HISTORY_SETTLE_TIMEOUT);
404
- const listener = (info) => {
405
- if (info.context === this.id)
406
- finish();
407
- };
408
- for (const event of events)
409
- this.biDiConnector.onEvent(event, listener);
410
- return {
411
- done,
412
- stop: async () => {
413
- clearTimeout(timer);
414
- for (const event of events)
415
- this.biDiConnector.offEvent(event, listener);
416
- await subscription?.unsubscribe().catch(() => undefined);
417
- },
418
- };
200
+ return traverseHistory(this.biDiConnector, this.id, 1);
419
201
  }
420
202
  /** The URL of the page now */
421
203
  async url() {
@@ -434,69 +216,27 @@ export default class Page {
434
216
  */
435
217
  async setViewport(viewport) {
436
218
  this.assertOpen();
437
- if (viewport === null) {
438
- await this.untilResized(() => this.biDiConnector.send("browsingContext.setViewport", {
439
- context: this.id,
440
- viewport: null,
441
- devicePixelRatio: null,
442
- }));
443
- return;
444
- }
445
- const { width, height, devicePixelRatio } = viewport;
446
- for (const [name, value] of [
447
- ["width", width],
448
- ["height", height],
449
- ]) {
450
- if (!Number.isInteger(value) || value <= 0) {
451
- throw new RangeError(`setViewport(): ${name} must be a positive integer, got ${value}`);
452
- }
453
- }
454
- if (devicePixelRatio !== undefined &&
455
- !(devicePixelRatio > 0 && Number.isFinite(devicePixelRatio))) {
456
- throw new RangeError(`setViewport(): devicePixelRatio must be a positive number, got ${devicePixelRatio}`);
457
- }
458
- await this.biDiConnector.send("browsingContext.setViewport", {
459
- context: this.id,
460
- viewport: { width, height },
461
- ...(devicePixelRatio !== undefined && { devicePixelRatio }),
462
- });
463
- }
464
- /**
465
- * Runs `change`, then waits until the page has stopped resizing. Chrome answers a viewport reset before the window
466
- * has its own size back, so a read right after it can still show the old size. The wait ends 50 ms after the last
467
- * `resize` event, and after 300 ms when the size does not change at all (no event comes). Never fails: a page that
468
- * cannot be watched is simply not waited for.
469
- */
470
- async untilResized(change) {
471
- const sandbox = { sandbox: "samurai-viewport" };
472
- const watching = await callFunction(this.biDiConnector, this.id, `() => {
473
- globalThis.__resized = new Promise((resolve) => {
474
- let timer;
475
- const finish = () => {
476
- clearTimeout(timer);
477
- removeEventListener("resize", onResize);
478
- resolve();
479
- };
480
- const onResize = () => {
481
- clearTimeout(timer);
482
- timer = setTimeout(finish, 50);
483
- };
484
- addEventListener("resize", onResize);
485
- timer = setTimeout(finish, 300);
486
- });
487
- }`, [], { ...sandbox, awaitPromise: false }).then(() => true, () => false);
488
- await change();
489
- if (!watching)
490
- return;
491
- await callFunction(this.biDiConnector, this.id, "() => globalThis.__resized", [], sandbox).catch(() => undefined);
219
+ await applyViewport(this.biDiConnector, this.id, viewport);
492
220
  }
493
221
  /** Gives the page the size from config `use.viewport` (1280x720 by default); the browser does this when it opens a page */
494
222
  async applyDefaultViewport() {
495
- const { viewport } = this.settings?.use ?? (await readSection("use"));
223
+ const { viewport } = await this.pageConfig.use();
496
224
  if (viewport === null)
497
225
  return;
498
226
  await this.setViewport(viewport ?? DEFAULT_VIEWPORT);
499
227
  }
228
+ /**
229
+ * What the browser sets up on every page it opens: the default size, then everything that has to be listening
230
+ * before the page does anything (requests, dialogs, logs, downloads, file pickers). Safe to call again.
231
+ */
232
+ async startTracking() {
233
+ await this.applyDefaultViewport();
234
+ await this.startNetworkTracking();
235
+ await this.startDialogHandling();
236
+ await this.startLogging();
237
+ await this.startDownloadTracking();
238
+ await this.startFileChooserTracking();
239
+ }
500
240
  /**
501
241
  * Takes a screenshot and returns the image (also written to `options.path`).
502
242
  * @example
@@ -518,7 +258,7 @@ export default class Page {
518
258
  /** Waits `ms` milliseconds. Prefer waiting for something (an assertion, `waitForNetworkIdle`) when you can */
519
259
  async waitForTimeout(ms) {
520
260
  if (!Number.isFinite(ms) || ms < 0)
521
- throw new RangeError(`waitForTimeout needs 0 or more milliseconds, got ${ms}`);
261
+ throw new RangeError(`waitForTimeout(): ms must be 0 or more, got ${ms}`);
522
262
  await new Promise((resolve) => setTimeout(resolve, ms));
523
263
  }
524
264
  /** Renders the page as a PDF and returns it (also written to `options.path`) */
@@ -560,7 +300,7 @@ export default class Page {
560
300
  const origin = options.origin ?? new URL(await this.url()).origin;
561
301
  // about:blank and data: pages have no origin to grant to
562
302
  if (origin === "null")
563
- throw new Error("grantPermission() needs a page on a web origin, or an explicit origin option");
303
+ throw new Error("grantPermission(): needs a page on a web origin, or an explicit origin option");
564
304
  const context = this.browserContext;
565
305
  await this.biDiConnector.send("permissions.setPermission", {
566
306
  descriptor: { name },
@@ -606,192 +346,23 @@ export default class Page {
606
346
  * await page.locator("a[@id='next']").click();
607
347
  * await page.waitForLoadState("domcontentloaded");
608
348
  */
609
- async waitForLoadState(state = "load", options = {}) {
610
- this.assertOpen();
611
- const timeout = options.timeout ??
612
- (await this.navigationSettings()).timeout ??
613
- DEFAULT_NAVIGATION_TIMEOUT;
614
- try {
615
- await this.waitForDocumentState(state === "domcontentloaded" ? "domcontentloaded" : "load", timeout);
616
- }
617
- catch (err) {
618
- if (err instanceof WaitTimeoutError)
619
- throw new LoadStateTimeoutError(state, timeout);
620
- throw err;
621
- }
622
- if (state === "networkidle") {
623
- const tracker = await this.network();
624
- const idleTime = (await this.networkSettings()).idleTime ?? DEFAULT_IDLE_TIME;
625
- await tracker.waitForIdle({ idleTime, timeout });
626
- }
627
- }
628
- progressOf(navigation) {
629
- let progress = this.navigationProgress.get(navigation);
630
- if (!progress) {
631
- progress = { domContentLoaded: false, load: false };
632
- this.navigationProgress.set(navigation, progress);
633
- // Only the latest few navigations matter
634
- if (this.navigationProgress.size > 20) {
635
- this.navigationProgress.delete(this.navigationProgress.keys().next().value);
636
- }
637
- }
638
- return progress;
639
- }
640
- /** Starts recording which navigations reached DOMContentLoaded and load; kept until dispose */
641
- watchDocumentEvents() {
642
- this.documentEvents ??= (async () => {
643
- this.biDiConnector.onEvent("browsingContext.domContentLoaded", this.onDomContentLoaded);
644
- this.biDiConnector.onEvent("browsingContext.load", this.onLoad);
645
- try {
646
- return await this.biDiConnector.subscribe([
647
- "browsingContext.domContentLoaded",
648
- "browsingContext.load",
649
- ]);
650
- }
651
- catch (err) {
652
- this.biDiConnector.offEvent("browsingContext.domContentLoaded", this.onDomContentLoaded);
653
- this.biDiConnector.offEvent("browsingContext.load", this.onLoad);
654
- this.documentEvents = undefined;
655
- throw err;
656
- }
657
- })();
658
- return this.documentEvents;
659
- }
660
- canBeAborted() {
661
- return !unsupportedEvents(this.biDiConnector).has("browsingContext.navigationAborted");
662
- }
663
- /** Subscribes to what a wait listens for; a browser that lacks the navigationAborted event (Firefox) is asked only once */
664
- async subscribeToDocumentEvents(reachedEvent) {
665
- const required = [
666
- reachedEvent,
667
- "browsingContext.navigationFailed",
668
- ];
669
- if (!this.canBeAborted())
670
- return this.biDiConnector.subscribe([...required]);
671
- try {
672
- return await this.biDiConnector.subscribe([
673
- ...required,
674
- "browsingContext.navigationAborted",
675
- ]);
676
- }
677
- catch (err) {
678
- if (!(err instanceof BiDiError) || err.code !== "invalid argument")
679
- throw err;
680
- unsupportedEvents(this.biDiConnector).add("browsingContext.navigationAborted");
681
- return this.biDiConnector.subscribe([...required]);
682
- }
683
- }
684
- async waitForDocumentState(target, timeout) {
685
- const connector = this.biDiConnector;
686
- const reachedEvent = target === "load"
687
- ? "browsingContext.load"
688
- : "browsingContext.domContentLoaded";
689
- const subscription = await this.subscribeToDocumentEvents(reachedEvent);
690
- const abort = new AbortController();
691
- const last = this.lastNavigation;
692
- const listen = { timeout, signal: abort.signal };
693
- const failure = (info, what) => {
694
- throw new NavigationError(info.url, what, undefined, "waitForLoadState");
695
- };
696
- try {
697
- // Registered before the state is read, so an event in between is not lost
698
- const events = Promise.race([
699
- connector.waitForEvent(reachedEvent, (p) => p.context === this.id, listen),
700
- connector
701
- .waitForEvent("browsingContext.navigationFailed", (p) => p.context === this.id &&
702
- (last === undefined || p.navigation === last), listen)
703
- .then((p) => failure(p, "navigation failed")),
704
- ...(this.canBeAborted()
705
- ? [
706
- connector
707
- .waitForEvent("browsingContext.navigationAborted", (p) => p.context === this.id &&
708
- last !== undefined &&
709
- p.navigation === last, listen)
710
- .then((p) => failure(p, "navigation aborted")),
711
- ]
712
- : []),
713
- ]);
714
- events.catch(() => { }); // settled below, or aborted in finally
715
- const known = last === undefined ? undefined : this.navigationProgress.get(last);
716
- // A navigation we started and saw fall short of the state beats the document that is showing
717
- let reached;
718
- if (known && !(target === "load" ? known.load : known.domContentLoaded)) {
719
- reached = false;
720
- }
721
- else {
722
- const readyState = await callFunction(connector, this.id, "() => document.readyState", [], {
723
- awaitPromise: false,
724
- });
725
- reached =
726
- target === "load"
727
- ? readyState === "complete"
728
- : readyState === "interactive" || readyState === "complete";
729
- }
730
- if (!reached)
731
- await events;
732
- }
733
- finally {
734
- abort.abort();
735
- await subscription.unsubscribe();
736
- }
737
- }
738
- networkSettings() {
739
- return this.settings?.network
740
- ? Promise.resolve(this.settings.network)
741
- : readSection("network");
742
- }
743
- navigationSettings() {
744
- return this.settings?.navigation
745
- ? Promise.resolve(this.settings.navigation)
746
- : readSection("navigation");
747
- }
748
- /** The network tracker, created on first use. Rejects with NetworkTrackingDisabledError when config turned tracking off */
749
- network() {
750
- this.networkTracker ??= (async () => {
751
- const settings = await this.networkSettings();
752
- if (settings.track === false)
753
- throw new NetworkTrackingDisabledError();
754
- const tracker = await NetworkTracker.start(this.biDiConnector, await this.tree(), this.id);
755
- tracker.on("request", (request) => this.pageEvents.emit("request", request));
756
- tracker.on("response", (response) => this.pageEvents.emit("response", this.responseFor(response)));
757
- tracker.on("requestfailed", (failed) => this.pageEvents.emit("requestfailed", failed));
758
- if (settings.collectBodies)
759
- await this.startBodyCollection();
760
- return tracker;
761
- })().catch((err) => {
762
- if (!(err instanceof NetworkTrackingDisabledError))
763
- this.networkTracker = undefined;
764
- throw err;
765
- });
766
- return this.networkTracker;
349
+ waitForLoadState(state = "load", options = {}) {
350
+ return this.nav.waitForLoadState(state, options);
767
351
  }
768
352
  /**
769
353
  * Starts following this page's requests now. Tracking only sees requests made after it started, so the
770
354
  * browser calls this when it opens a page; without it, tracking starts on first use. Does nothing when
771
355
  * config `network.track` is false.
772
356
  */
773
- async startNetworkTracking() {
774
- try {
775
- await this.network();
776
- }
777
- catch (err) {
778
- if (!(err instanceof NetworkTrackingDisabledError))
779
- throw err;
780
- }
357
+ startNetworkTracking() {
358
+ return this.net.startTracking();
781
359
  }
782
360
  /**
783
361
  * Waits until no request has been in flight for `idleTime` ms (default config `network.idleTime`, then 500).
784
362
  * Long-lived requests (server-sent events, long polling) keep the page busy until `timeout`.
785
363
  */
786
- async waitForNetworkIdle(options = {}) {
787
- const tracker = await this.network();
788
- const idleTime = options.idleTime ??
789
- (await this.networkSettings()).idleTime ??
790
- DEFAULT_IDLE_TIME;
791
- const timeout = options.timeout ??
792
- (await this.navigationSettings()).timeout ??
793
- DEFAULT_NAVIGATION_TIMEOUT;
794
- await tracker.waitForIdle({ idleTime, timeout });
364
+ waitForNetworkIdle(options = {}) {
365
+ return this.net.waitForIdle(options);
795
366
  }
796
367
  /**
797
368
  * Listens for page events: `request`, `response`, `requestfailed` (network), `dialog` (a JavaScript dialog or
@@ -816,46 +387,23 @@ export default class Page {
816
387
  return this;
817
388
  }
818
389
  trackInBackground(event) {
819
- const started = event === "dialog"
820
- ? this.startDialogHandling()
821
- : event === "download"
822
- ? this.startDownloadTracking()
823
- : event === "filechooser"
824
- ? this.startFileChooserTracking()
825
- : event === "console" || event === "pageerror"
826
- ? this.startLogging()
827
- : event === "response"
828
- ? Promise.all([
829
- this.startNetworkTracking(),
830
- this.startBodyCollection(),
831
- ])
832
- : this.startNetworkTracking();
833
- started.catch((err) => logger.error("Could not start listening for %s", event, { err }));
390
+ const start = {
391
+ dialogs: () => this.startDialogHandling(),
392
+ downloads: () => this.startDownloadTracking(),
393
+ filechoosers: () => this.startFileChooserTracking(),
394
+ logs: () => this.startLogging(),
395
+ network: () => this.startNetworkTracking(),
396
+ bodies: () => this.startBodyCollection(),
397
+ };
398
+ Promise.all(sourcesFor(event).map((source) => start[source]())).catch((err) => logger.error("Could not start listening for %s", event, { err }));
834
399
  }
835
400
  /** Starts following downloads; the browser does this when it opens a page, `page.on("download")` and `waitForDownload` when needed */
836
- async startDownloadTracking() {
837
- this.downloadTracker ??= (async () => {
838
- const timeout = (await this.navigationSettings()).timeout ?? DEFAULT_NAVIGATION_TIMEOUT;
839
- const tracker = await DownloadTracker.start(this.biDiConnector, await this.tree(), this.id, { timeout });
840
- tracker.on("download", (download) => this.pageEvents.emit("download", download));
841
- return tracker;
842
- })().catch((err) => {
843
- this.downloadTracker = undefined;
844
- throw err;
845
- });
846
- await this.downloadTracker;
401
+ startDownloadTracking() {
402
+ return this.transfers.startDownloadTracking();
847
403
  }
848
404
  /** Starts following file pickers; the browser does this when it opens a page, `page.on("filechooser")` and `waitForFileChooser` when needed */
849
- async startFileChooserTracking() {
850
- this.fileChooserTracker ??= (async () => {
851
- const tracker = await FileChooserTracker.start(this.biDiConnector, await this.tree(), this.id);
852
- tracker.on("filechooser", (chooser) => this.pageEvents.emit("filechooser", chooser));
853
- return tracker;
854
- })().catch((err) => {
855
- this.fileChooserTracker = undefined;
856
- throw err;
857
- });
858
- await this.fileChooserTracker;
405
+ startFileChooserTracking() {
406
+ return this.transfers.startFileChooserTracking();
859
407
  }
860
408
  /**
861
409
  * Resolves with the next download this page starts (when it starts, not when it ends: use `download.path()` for that).
@@ -866,8 +414,7 @@ export default class Page {
866
414
  */
867
415
  async waitForDownload(options = {}) {
868
416
  this.assertOpen();
869
- await this.startDownloadTracking();
870
- return this.waitForPageEvent("download", () => true, options, (timeout) => new DownloadWaitTimeoutError(timeout));
417
+ return this.transfers.waitForDownload(options);
871
418
  }
872
419
  /**
873
420
  * Resolves with the next file picker the page opens (a click on an `<input type=file>` or its label). Nothing shows
@@ -878,8 +425,7 @@ export default class Page {
878
425
  */
879
426
  async waitForFileChooser(options = {}) {
880
427
  this.assertOpen();
881
- await this.startFileChooserTracking();
882
- return this.waitForPageEvent("filechooser", () => true, options, (timeout) => new FileChooserTimeoutError(timeout));
428
+ return this.transfers.waitForFileChooser(options);
883
429
  }
884
430
  /**
885
431
  * Playwright-style wait for one page event: `download`, `filechooser`, `request` or `response`. Same as the named
@@ -901,14 +447,12 @@ export default class Page {
901
447
  return this.waitForResponse(accepts, options);
902
448
  case "download":
903
449
  this.assertOpen();
904
- await this.startDownloadTracking();
905
- return this.waitForPageEvent("download", accepts, options, (timeout) => new DownloadWaitTimeoutError(timeout));
450
+ return this.transfers.waitForDownload(options, accepts);
906
451
  case "filechooser":
907
452
  this.assertOpen();
908
- await this.startFileChooserTracking();
909
- return this.waitForPageEvent("filechooser", accepts, options, (timeout) => new FileChooserTimeoutError(timeout));
453
+ return this.transfers.waitForFileChooser(options, accepts);
910
454
  default:
911
- throw new Error(`page.waitForEvent() does not support "${String(event)}": use page.on()`);
455
+ throw new Error(`waitForEvent(): "${String(event)}" is not supported; use page.on()`);
912
456
  }
913
457
  }
914
458
  /**
@@ -916,35 +460,8 @@ export default class Page {
916
460
  * when this ran before the request started. `page.on("response")` and `waitForResponse` call it; config
917
461
  * `network.collectBodies` calls it when the browser opens the page.
918
462
  */
919
- async startBodyCollection() {
920
- this.collectorPromise ??= (async () => {
921
- const { maxBodySize } = await this.networkSettings();
922
- return DataCollector.start(this.biDiConnector, this.id, {
923
- ...(maxBodySize !== undefined && { maxBodySize }),
924
- });
925
- })().catch((err) => {
926
- this.collectorPromise = undefined;
927
- throw err;
928
- });
929
- await this.collectorPromise;
930
- }
931
- /** One Response per network response, whoever asks, so a body is read from the browser once */
932
- responseFor(data) {
933
- let response = this.responses.get(data);
934
- if (!response) {
935
- response = new Response(data, (loaded) => this.loadBody(loaded));
936
- this.responses.set(data, response);
937
- }
938
- return response;
939
- }
940
- async loadBody(response) {
941
- if (!this.collectorPromise) {
942
- throw new ResponseBodyUnavailableError(response.url, "no body collection was running: call page.startBodyCollection() before the request, or listen with page.on('response')");
943
- }
944
- const collector = await this.collectorPromise;
945
- const body = await collector.read(response.id, response.url);
946
- void collector.release(response.id);
947
- return body;
463
+ startBodyCollection() {
464
+ return this.net.startBodyCollection();
948
465
  }
949
466
  /**
950
467
  * Resolves with the next response of this page whose URL matches (a pattern, see `route`) or that the function
@@ -953,91 +470,24 @@ export default class Page {
953
470
  * const response = await page.waitForResponse("**\/api/users", { trigger: () => button.click() });
954
471
  * const users = await response.json();
955
472
  */
956
- async waitForResponse(match, options = {}) {
957
- await Promise.all([this.network(), this.startBodyCollection()]);
958
- return this.waitForNetworkEvent("response", match, options);
473
+ waitForResponse(match, options = {}) {
474
+ return this.net.waitForResponse(match, options);
959
475
  }
960
476
  /** Like `waitForResponse`, for requests */
961
- async waitForRequest(match, options = {}) {
962
- await this.network();
963
- return this.waitForNetworkEvent("request", match, options);
964
- }
965
- waitForNetworkEvent(event, match, options) {
966
- const matches = toPredicate(match);
967
- const Timeout = event === "request" ? RequestTimeoutError : ResponseTimeoutError;
968
- return this.waitForPageEvent(event, matches, options, (timeout) => new Timeout(event === "request" ? "waitForRequest" : "waitForResponse", match, timeout));
477
+ waitForRequest(match, options = {}) {
478
+ return this.net.waitForRequest(match, options);
969
479
  }
970
480
  /**
971
481
  * Resolves with the first `event` `matches` accepts. Listens before `trigger` runs, so what the trigger causes cannot
972
482
  * be missed; a trigger that throws rejects the wait with its error. `onTimeout` builds the error for a wait that ran out.
973
483
  */
974
484
  async waitForPageEvent(event, matches, options, onTimeout) {
975
- const timeout = options.timeout ??
976
- (await this.navigationSettings()).timeout ??
977
- DEFAULT_NAVIGATION_TIMEOUT;
978
- return new Promise((resolve, reject) => {
979
- const done = () => {
980
- clearTimeout(timer);
981
- this.pageEvents.off(event, listener);
982
- };
983
- const listener = (item) => {
984
- try {
985
- if (!matches(item))
986
- return;
987
- }
988
- catch (err) {
989
- done();
990
- reject(err);
991
- return;
992
- }
993
- done();
994
- resolve(item);
995
- };
996
- const timer = setTimeout(() => {
997
- done();
998
- reject(onTimeout(timeout));
999
- }, timeout);
1000
- this.pageEvents.on(event, listener);
1001
- const { trigger } = options;
1002
- if (trigger) {
1003
- Promise.resolve()
1004
- .then(trigger)
1005
- .catch((err) => {
1006
- done();
1007
- reject(err);
1008
- });
1009
- }
1010
- });
485
+ const timeout = await this.pageConfig.navigationTimeout(options.timeout);
486
+ return waitForPageEvent(this.pageEvents, event, matches, options, timeout, onTimeout);
1011
487
  }
1012
488
  /** Turns the browser's HTTP cache off (`true`) or back on for this page: every request then goes to the server */
1013
- async setCacheDisabled(disabled) {
1014
- try {
1015
- await this.biDiConnector.send("network.setCacheBehavior", {
1016
- cacheBehavior: disabled ? "bypass" : "default",
1017
- contexts: [this.id],
1018
- });
1019
- }
1020
- catch (err) {
1021
- if (err instanceof BiDiError &&
1022
- (err.code === "unsupported operation" || err.code === "unknown command")) {
1023
- throw new UnsupportedOperationError("page.setCacheDisabled() is not supported by the browser");
1024
- }
1025
- throw err;
1026
- }
1027
- }
1028
- router() {
1029
- this.routerPromise ??= (async () => {
1030
- const routeTimeout = (await this.networkSettings()).routeTimeout;
1031
- const router = new Router(this.biDiConnector, await this.tree(), this.id, {
1032
- ...(routeTimeout !== undefined && { routeTimeout }),
1033
- });
1034
- this.loadedRouter = router;
1035
- return router;
1036
- })().catch((err) => {
1037
- this.routerPromise = undefined;
1038
- throw err;
1039
- });
1040
- return this.routerPromise;
489
+ setCacheDisabled(disabled) {
490
+ return this.net.setCacheDisabled(disabled);
1041
491
  }
1042
492
  /**
1043
493
  * Sends requests whose URL matches to `handler`, which must answer with `route.fulfill()`, `route.continue()`,
@@ -1050,54 +500,45 @@ export default class Page {
1050
500
  */
1051
501
  async route(url, handler) {
1052
502
  this.assertOpen();
1053
- await (await this.router()).route(url, handler);
503
+ await this.net.route(url, handler);
1054
504
  }
1055
505
  /** Removes the routes for `url` (only the one with `handler`, when given) */
1056
- async unroute(url, handler) {
1057
- if (!this.routerPromise)
1058
- return;
1059
- await (await this.router()).unroute(url, handler);
506
+ unroute(url, handler) {
507
+ return this.net.unroute(url, handler);
1060
508
  }
1061
- async unrouteAll() {
1062
- if (!this.routerPromise)
1063
- return;
1064
- await (await this.router()).unrouteAll();
509
+ unrouteAll() {
510
+ return this.net.unrouteAll();
1065
511
  }
1066
512
  /** Errors thrown by route handlers so far */
1067
513
  routeErrors() {
1068
- return this.loadedRouter?.errors ?? [];
514
+ return this.net.routeErrors();
1069
515
  }
1070
516
  /**
1071
517
  * Starts collecting the page's console output and uncaught exceptions. The browser does this when it opens a page so
1072
518
  * early output is kept; `page.on("console" | "pageerror")` starts it when needed.
1073
519
  */
1074
520
  async startLogging() {
1075
- this.pageLogs ??= (async () => {
521
+ await this.lazyLogs.get(async () => {
1076
522
  const logs = await PageLogs.start(this.biDiConnector, await this.tree(), this.id);
1077
523
  logs.on("console", (message) => this.pageEvents.emit("console", message));
1078
524
  logs.on("pageerror", (error) => this.pageEvents.emit("pageerror", error));
1079
- this.loadedLogs = logs;
1080
525
  return logs;
1081
- })().catch((err) => {
1082
- this.pageLogs = undefined;
1083
- throw err;
1084
526
  });
1085
- await this.pageLogs;
1086
527
  }
1087
528
  /** The newest console and error entries of this page (empty until logging started), oldest first */
1088
529
  getLogs() {
1089
- return this.loadedLogs?.entries ?? [];
530
+ return this.lazyLogs.peek()?.entries ?? [];
1090
531
  }
1091
532
  /** Uncaught exceptions of the page since logging started or `clearLogs()` */
1092
533
  pageErrors() {
1093
- return this.loadedLogs?.errors ?? [];
534
+ return this.lazyLogs.peek()?.errors ?? [];
1094
535
  }
1095
536
  /** How many entries fell out of the page's log buffer */
1096
537
  get logsDropped() {
1097
- return this.loadedLogs?.dropped ?? 0;
538
+ return this.lazyLogs.peek()?.dropped ?? 0;
1098
539
  }
1099
540
  clearLogs() {
1100
- this.loadedLogs?.clear();
541
+ this.lazyLogs.peek()?.clear();
1101
542
  }
1102
543
  /** Resolves once everything the page logged before the call has arrived: events and replies share one ordered connection */
1103
544
  async syncLogs() {
@@ -1111,72 +552,7 @@ export default class Page {
1111
552
  return this.errorsAllowed;
1112
553
  }
1113
554
  /** Starts watching for dialogs; the browser does this when it opens a page, `page.on("dialog")` when needed */
1114
- async startDialogHandling() {
1115
- this.dialogHandling ??= (async () => {
1116
- await this.tree();
1117
- this.biDiConnector.onEvent("browsingContext.userPromptOpened", this.onPromptOpened);
1118
- this.biDiConnector.onEvent("browsingContext.userPromptClosed", this.onPromptClosed);
1119
- try {
1120
- return await this.biDiConnector.subscribe([
1121
- "browsingContext.userPromptOpened",
1122
- "browsingContext.userPromptClosed",
1123
- ]);
1124
- }
1125
- catch (err) {
1126
- this.biDiConnector.offEvent("browsingContext.userPromptOpened", this.onPromptOpened);
1127
- this.biDiConnector.offEvent("browsingContext.userPromptClosed", this.onPromptClosed);
1128
- this.dialogHandling = undefined;
1129
- throw err;
1130
- }
1131
- })();
1132
- await this.dialogHandling;
1133
- }
1134
- ownsContext(context) {
1135
- return this.loadedTree?.isWithin(context, this.id) ?? context === this.id;
1136
- }
1137
- /** Lets the listeners answer; a dialog still open afterwards gets the default answer, so nothing hangs */
1138
- async dispatchDialog(dialog, params) {
1139
- this.openDialogs.set(params.context, dialog);
1140
- // rawListeners: the wrappers that make a `once` listener remove itself when called
1141
- const listeners = this.pageEvents.rawListeners("dialog");
1142
- await Promise.all(listeners.map(async (listener) => {
1143
- try {
1144
- await listener(dialog);
1145
- }
1146
- catch (err) {
1147
- logger.error("A dialog listener threw", { err });
1148
- }
1149
- }));
1150
- if (dialog.handled)
1151
- return;
1152
- // Staying on a page is not what a test that navigates wants: leave it, dismiss everything else
1153
- const accept = params.type === "beforeunload";
1154
- logger.warn('Dialog "%s" was %s automatically; add page.on("dialog") to handle it', params.message, accept ? "accepted" : "dismissed");
1155
- await (accept ? dialog.accept() : dialog.dismiss());
1156
- }
1157
- /** Captures an image of the given navigable, and returns it as a Base64-encoded string */
1158
- async captureScreenshot(clip, format, origin) {
1159
- const screenshot = await this.biDiConnector.send("browsingContext.captureScreenshot", {
1160
- context: this.id,
1161
- clip,
1162
- format,
1163
- origin,
1164
- });
1165
- // TODO: Store screenshot according to config specs
1166
- }
1167
- }
1168
- /** Wraps an init script as one function declaration; the argument is embedded as JSON because preload scripts take no arguments */
1169
- function initScriptSource(script, arg) {
1170
- if (typeof script === "string") {
1171
- if (arg !== undefined) {
1172
- throw new TypeError("addInitScript(): a string script does not take an argument; pass a function instead");
1173
- }
1174
- return `() => {\n${script}\n}`;
1175
- }
1176
- const declaration = toFunctionDeclaration(script);
1177
- const argument = arg === undefined ? "undefined" : JSON.stringify(arg);
1178
- if (argument === undefined) {
1179
- throw new TypeError("addInitScript(): the argument must be JSON-serializable");
555
+ startDialogHandling() {
556
+ return this.dialogs.start();
1180
557
  }
1181
- return `() => (${declaration})(${argument})`;
1182
558
  }