@itmetsam/samurai-framework 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/bin/samurai.mjs +31 -0
- package/dist/api.js +3 -0
- package/dist/assert/assertion-error.d.ts +23 -0
- package/dist/assert/assertion-error.js +45 -0
- package/dist/assert/expect.d.ts +55 -0
- package/dist/assert/expect.js +188 -0
- package/dist/browser/browser-context.d.ts +91 -0
- package/dist/browser/browser-context.js +146 -0
- package/dist/browser/browser-finder.d.ts +2 -0
- package/dist/browser/browser-finder.js +95 -0
- package/dist/browser/browser.d.ts +95 -0
- package/dist/browser/browser.js +372 -0
- package/dist/browser/context-tree.d.ts +23 -0
- package/dist/browser/context-tree.js +60 -0
- package/dist/browser/dialog.d.ts +37 -0
- package/dist/browser/dialog.js +61 -0
- package/dist/browser/download.d.ts +71 -0
- package/dist/browser/download.js +137 -0
- package/dist/browser/downloads-dir.d.ts +8 -0
- package/dist/browser/downloads-dir.js +38 -0
- package/dist/browser/emulation.d.ts +61 -0
- package/dist/browser/emulation.js +144 -0
- package/dist/browser/file-chooser.d.ts +39 -0
- package/dist/browser/file-chooser.js +86 -0
- package/dist/browser/file-paths.d.ts +4 -0
- package/dist/browser/file-paths.js +18 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/navigation-error.d.ts +13 -0
- package/dist/browser/navigation-error.js +19 -0
- package/dist/browser/page-logs.d.ts +86 -0
- package/dist/browser/page-logs.js +149 -0
- package/dist/browser/page-wait.d.ts +9 -0
- package/dist/browser/page-wait.js +17 -0
- package/dist/browser/page.d.ts +370 -0
- package/dist/browser/page.js +1038 -0
- package/dist/browser/pdf.d.ts +72 -0
- package/dist/browser/pdf.js +37 -0
- package/dist/browser/screenshot.d.ts +29 -0
- package/dist/browser/screenshot.js +42 -0
- package/dist/cli/args.d.ts +35 -0
- package/dist/cli/args.js +69 -0
- package/dist/cli/cli.cli-test.d.ts +1 -0
- package/dist/cli/cli.cli-test.js +170 -0
- package/dist/cli/console-reporter.d.ts +15 -0
- package/dist/cli/console-reporter.js +52 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +61 -0
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.js +156 -0
- package/dist/config/config.d.ts +24 -0
- package/dist/config/config.js +42 -0
- package/dist/config/default.d.ts +2 -0
- package/dist/config/default.js +3 -0
- package/dist/config/mask.d.ts +11 -0
- package/dist/config/mask.js +85 -0
- package/dist/config/run-settings.d.ts +28 -0
- package/dist/config/run-settings.js +61 -0
- package/dist/config/secrets.d.ts +10 -0
- package/dist/config/secrets.js +67 -0
- package/dist/config/variables.d.ts +11 -0
- package/dist/config/variables.js +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -0
- package/dist/locator/action-timeout-error.d.ts +25 -0
- package/dist/locator/action-timeout-error.js +35 -0
- package/dist/locator/element-state.d.ts +44 -0
- package/dist/locator/element-state.js +109 -0
- package/dist/locator/label-locator.d.ts +13 -0
- package/dist/locator/label-locator.js +49 -0
- package/dist/locator/locator.d.ts +168 -0
- package/dist/locator/locator.js +591 -0
- package/dist/locator/selector-errors.d.ts +9 -0
- package/dist/locator/selector-errors.js +15 -0
- package/dist/locator/selector.d.ts +60 -0
- package/dist/locator/selector.js +113 -0
- package/dist/locator/text-locator.d.ts +13 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +30 -0
- package/dist/network/data-collector.d.ts +26 -0
- package/dist/network/data-collector.js +77 -0
- package/dist/network/network-tracker.d.ts +94 -0
- package/dist/network/network-tracker.js +198 -0
- package/dist/network/network-wait.d.ts +16 -0
- package/dist/network/network-wait.js +24 -0
- package/dist/network/response.d.ts +22 -0
- package/dist/network/response.js +42 -0
- package/dist/network/router.d.ts +120 -0
- package/dist/network/router.js +343 -0
- package/dist/network/url-match.d.ts +8 -0
- package/dist/network/url-match.js +38 -0
- package/dist/recorder/apply.d.ts +3 -0
- package/dist/recorder/apply.js +10 -0
- package/dist/recorder/capture.d.ts +18 -0
- package/dist/recorder/capture.js +198 -0
- package/dist/recorder/index.d.ts +3 -0
- package/dist/recorder/index.js +3 -0
- package/dist/recorder/normalise.d.ts +68 -0
- package/dist/recorder/normalise.js +104 -0
- package/dist/recorder/ranking.d.ts +9 -0
- package/dist/recorder/ranking.js +21 -0
- package/dist/recorder/recorder.d.ts +52 -0
- package/dist/recorder/recorder.js +143 -0
- package/dist/recorder/session.d.ts +49 -0
- package/dist/recorder/session.js +113 -0
- package/dist/runner/module-type.d.ts +6 -0
- package/dist/runner/module-type.js +29 -0
- package/dist/runner/page-logs-report.d.ts +40 -0
- package/dist/runner/page-logs-report.js +44 -0
- package/dist/runner/prepare-run.d.ts +14 -0
- package/dist/runner/prepare-run.js +20 -0
- package/dist/runner/project.d.ts +17 -0
- package/dist/runner/project.js +33 -0
- package/dist/runner/registry.d.ts +7 -0
- package/dist/runner/registry.js +40 -0
- package/dist/runner/reporter.d.ts +43 -0
- package/dist/runner/reporter.js +83 -0
- package/dist/runner/run.d.ts +27 -0
- package/dist/runner/run.js +34 -0
- package/dist/runner/test-error.d.ts +3 -0
- package/dist/runner/test-error.js +18 -0
- package/dist/runner/test-runner.d.ts +44 -0
- package/dist/runner/test-runner.js +205 -0
- package/dist/script/call-function.d.ts +34 -0
- package/dist/script/call-function.js +71 -0
- package/dist/script/element-handle.d.ts +10 -0
- package/{src/script/element-handle.ts → dist/script/element-handle.js} +4 -4
- package/dist/script/helpers.d.ts +28 -0
- package/dist/script/helpers.js +50 -0
- package/dist/script/preload.d.ts +31 -0
- package/dist/script/preload.js +71 -0
- package/dist/script/serialize.d.ts +29 -0
- package/dist/script/serialize.js +207 -0
- package/dist/steps/build.d.ts +5 -0
- package/dist/steps/build.js +38 -0
- package/dist/steps/edit.d.ts +30 -0
- package/dist/steps/edit.js +80 -0
- package/dist/steps/emit.d.ts +6 -0
- package/dist/steps/emit.js +73 -0
- package/dist/steps/index.d.ts +5 -0
- package/dist/steps/index.js +4 -0
- package/dist/steps/model.d.ts +103 -0
- package/dist/steps/model.js +1 -0
- package/dist/steps/parse.d.ts +3 -0
- package/dist/steps/parse.js +272 -0
- package/dist/transport/bidi-connection.d.ts +46 -0
- package/dist/transport/bidi-connection.js +176 -0
- package/dist/transport/bidi-error.d.ts +10 -0
- package/dist/transport/bidi-error.js +10 -0
- package/dist/transport/subscriptions.d.ts +36 -0
- package/dist/transport/subscriptions.js +81 -0
- package/{src → dist}/types/test.d.ts +10 -4
- package/dist/wait/wait-until.d.ts +20 -0
- package/dist/wait/wait-until.js +63 -0
- package/package.json +44 -7
- package/.claude/settings.local.json +0 -7
- package/.env.example +0 -3
- package/.github/actions/setup/action.yml +0 -16
- package/.github/workflows/ci.yml +0 -71
- package/.trunk/configs/.markdownlint.yaml +0 -2
- package/.trunk/configs/.yamllint.yaml +0 -7
- package/.trunk/configs/svgo.config.mjs +0 -14
- package/.trunk/trunk.yaml +0 -34
- package/docs/superpowers/bidi-roadmap.md +0 -60
- package/docs/superpowers/plans/2026-09-29-auto-wait.md +0 -1827
- package/docs/superpowers/plans/2026-09-29-expect.md +0 -1350
- package/docs/superpowers/plans/2026-10-01-ci.md +0 -350
- package/docs/superpowers/plans/2026-10-01-test-api-alignment.md +0 -1821
- package/docs/superpowers/recorder-roadmap.md +0 -52
- package/docs/superpowers/specs/2026-09-29-auto-wait-design.md +0 -185
- package/docs/superpowers/specs/2026-09-29-bidi-foundation-design.md +0 -130
- package/docs/superpowers/specs/2026-09-29-contexts-and-storage-design.md +0 -88
- package/docs/superpowers/specs/2026-09-29-downloads-and-file-dialogs-design.md +0 -94
- package/docs/superpowers/specs/2026-09-29-emulation-design.md +0 -88
- package/docs/superpowers/specs/2026-09-29-expect-design.md +0 -130
- package/docs/superpowers/specs/2026-09-29-locate-nodes-design.md +0 -90
- package/docs/superpowers/specs/2026-09-29-logs-and-errors-design.md +0 -87
- package/docs/superpowers/specs/2026-09-29-navigation-waits-design.md +0 -83
- package/docs/superpowers/specs/2026-09-29-network-data-design.md +0 -78
- package/docs/superpowers/specs/2026-09-29-network-interception-design.md +0 -91
- package/docs/superpowers/specs/2026-09-29-network-tracking-design.md +0 -86
- package/docs/superpowers/specs/2026-09-29-page-api-design.md +0 -90
- package/docs/superpowers/specs/2026-09-29-preload-scripts-design.md +0 -82
- package/docs/superpowers/specs/2026-09-29-script-call-function-design.md +0 -107
- package/docs/superpowers/specs/2026-09-29-set-files-design.md +0 -62
- package/docs/superpowers/specs/2026-09-29-user-prompts-design.md +0 -75
- package/docs/superpowers/specs/2026-10-01-ci-design.md +0 -106
- package/docs/superpowers/specs/2026-10-01-test-api-alignment-design.md +0 -241
- package/public/logo-dark.svg +0 -236
- package/public/logo-light.svg +0 -262
- package/samurai.config.ts +0 -7
- package/src/assert/assertion-error.test.ts +0 -55
- package/src/assert/assertion-error.ts +0 -62
- package/src/assert/expect-locator.test.ts +0 -217
- package/src/assert/expect.test.ts +0 -73
- package/src/assert/expect.ts +0 -299
- package/src/browser/browser-context.browser-test.ts +0 -201
- package/src/browser/browser-context.test.ts +0 -407
- package/src/browser/browser-context.ts +0 -208
- package/src/browser/browser-finder.ts +0 -110
- package/src/browser/browser.test.ts +0 -88
- package/src/browser/browser.ts +0 -515
- package/src/browser/context-tree.test.ts +0 -86
- package/src/browser/context-tree.ts +0 -70
- package/src/browser/dialog.browser-test.ts +0 -144
- package/src/browser/dialog.test.ts +0 -322
- package/src/browser/dialog.ts +0 -83
- package/src/browser/download.test.ts +0 -161
- package/src/browser/download.ts +0 -172
- package/src/browser/downloads-dir.test.ts +0 -75
- package/src/browser/downloads-dir.ts +0 -39
- package/src/browser/emulation.browser-test.ts +0 -186
- package/src/browser/emulation.test.ts +0 -200
- package/src/browser/emulation.ts +0 -204
- package/src/browser/file-chooser.test.ts +0 -142
- package/src/browser/file-chooser.ts +0 -99
- package/src/browser/file-paths.ts +0 -18
- package/src/browser/goto.browser-test.ts +0 -44
- package/src/browser/navigation-error.ts +0 -26
- package/src/browser/navigation.browser-test.ts +0 -172
- package/src/browser/page-api.browser-test.ts +0 -228
- package/src/browser/page-logs.browser-test.ts +0 -119
- package/src/browser/page-logs.test.ts +0 -212
- package/src/browser/page-logs.ts +0 -208
- package/src/browser/page-wait.ts +0 -19
- package/src/browser/page.test.ts +0 -1567
- package/src/browser/page.ts +0 -1176
- package/src/browser/pdf.ts +0 -62
- package/src/browser/screenshot.ts +0 -65
- package/src/browser/transfers.browser-test.ts +0 -180
- package/src/config/config.ts +0 -33
- package/src/config/default.ts +0 -5
- package/src/config/fixtures.test.ts +0 -137
- package/src/config/mask.test.ts +0 -143
- package/src/config/mask.ts +0 -87
- package/src/config/run-settings.test.ts +0 -163
- package/src/config/run-settings.ts +0 -114
- package/src/config/secrets.ts +0 -70
- package/src/config/variables.ts +0 -31
- package/src/index.ts +0 -10
- package/src/locator/action-timeout-error.test.ts +0 -83
- package/src/locator/action-timeout-error.ts +0 -72
- package/src/locator/element-state.test.ts +0 -181
- package/src/locator/element-state.ts +0 -159
- package/src/locator/locate-nodes.browser-test.ts +0 -169
- package/src/locator/locator-actions.test.ts +0 -355
- package/src/locator/locator-chain.test.ts +0 -292
- package/src/locator/locator.browser-test.ts +0 -137
- package/src/locator/locator.test.ts +0 -179
- package/src/locator/locator.ts +0 -656
- package/src/locator/selector-errors.ts +0 -18
- package/src/locator/selector.test.ts +0 -124
- package/src/locator/selector.ts +0 -107
- package/src/locator/set-files.browser-test.ts +0 -141
- package/src/locator/set-files.test.ts +0 -191
- package/src/locator/text-locator.test.ts +0 -118
- package/src/logger/index.test.ts +0 -28
- package/src/logger/index.ts +0 -40
- package/src/network/data-collector.test.ts +0 -111
- package/src/network/data-collector.ts +0 -92
- package/src/network/network-data.browser-test.ts +0 -192
- package/src/network/network-tracker.browser-test.ts +0 -151
- package/src/network/network-tracker.test.ts +0 -307
- package/src/network/network-tracker.ts +0 -286
- package/src/network/network-wait.ts +0 -28
- package/src/network/response.test.ts +0 -93
- package/src/network/response.ts +0 -58
- package/src/network/router.browser-test.ts +0 -260
- package/src/network/router.test.ts +0 -528
- package/src/network/router.ts +0 -420
- package/src/network/url-match.test.ts +0 -69
- package/src/network/url-match.ts +0 -41
- package/src/runner/page-logs-report.test.ts +0 -144
- package/src/runner/page-logs-report.ts +0 -82
- package/src/runner/prepare-run.test.ts +0 -33
- package/src/runner/prepare-run.ts +0 -36
- package/src/runner/registry.test.ts +0 -59
- package/src/runner/registry.ts +0 -46
- package/src/runner/reporter.test.ts +0 -73
- package/src/runner/reporter.ts +0 -109
- package/src/runner/test-error.test.ts +0 -39
- package/src/runner/test-error.ts +0 -20
- package/src/runner/test-runner.ts +0 -184
- package/src/script/call-function.browser-test.ts +0 -112
- package/src/script/call-function.test.ts +0 -174
- package/src/script/call-function.ts +0 -113
- package/src/script/helpers.test.ts +0 -116
- package/src/script/helpers.ts +0 -68
- package/src/script/preload.browser-test.ts +0 -144
- package/src/script/preload.test.ts +0 -180
- package/src/script/preload.ts +0 -102
- package/src/script/serialize.test.ts +0 -182
- package/src/script/serialize.ts +0 -203
- package/src/testing/browser-fixture.ts +0 -18
- package/src/testing/fake-websocket.ts +0 -72
- package/src/testing/stub-connector.ts +0 -154
- package/src/tests/index.spec.ts +0 -30
- package/src/transport/bidi-connection.test.ts +0 -155
- package/src/transport/bidi-connection.ts +0 -256
- package/src/transport/bidi-error.ts +0 -23
- package/src/transport/foundation.browser-test.ts +0 -73
- package/src/transport/subscriptions.test.ts +0 -140
- package/src/transport/subscriptions.ts +0 -110
- package/src/types/bidi-commands.test-d.ts +0 -55
- package/src/wait/wait-until.test.ts +0 -117
- package/src/wait/wait-until.ts +0 -86
- package/tsconfig.json +0 -44
- /package/{src/api.ts → dist/api.d.ts} +0 -0
- /package/{src/locator/text-locator.ts → dist/locator/text-locator.js} +0 -0
- /package/{src → dist}/types/bidi-modules/browser.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/browsing-context.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/emulation.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/input.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/log.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/network.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/permissions.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/script.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/session.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/storage.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/web-extensions.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/command.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/error.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/event.d.ts +0 -0
- /package/{src → dist}/types/bidi.d.ts +0 -0
- /package/{src → dist}/types/browser.d.ts +0 -0
- /package/{src → dist}/types/config.d.ts +0 -0
- /package/{src → dist}/types/env.d.ts +0 -0
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# Network interception — design
|
|
2
|
-
|
|
3
|
-
Date: 2026-09-29
|
|
4
|
-
Status: implemented (see "Implementation notes")
|
|
5
|
-
Roadmap item: 9
|
|
6
|
-
Depends on: bidi-foundation, network-tracking
|
|
7
|
-
|
|
8
|
-
## Goal
|
|
9
|
-
|
|
10
|
-
Mock, block and modify requests from tests.
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
await page.route("**/api/users", (route) =>
|
|
14
|
-
route.fulfill({ status: 200, json: [{ id: 1 }] }));
|
|
15
|
-
await page.route(/\.(png|jpg)$/, (route) => route.abort());
|
|
16
|
-
await page.route("**/api/**", (route) =>
|
|
17
|
-
route.continue({ headers: { ...route.request().headers, "x-test": "1" } }));
|
|
18
|
-
await page.unroute("**/api/users");
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Non-goals
|
|
22
|
-
|
|
23
|
-
- Modifying responses of real requests (`network.continueResponse`, phase `responseStarted`); a later addition.
|
|
24
|
-
- `route.fetch()` (BiDi has no way to perform a request and hand back the response); use `continue` or `fulfill`.
|
|
25
|
-
- HTTP auth (`authRequired` + `continueWithAuth`); planned follow-up `page.setHttpCredentials`.
|
|
26
|
-
- Service worker requests.
|
|
27
|
-
|
|
28
|
-
## API
|
|
29
|
-
|
|
30
|
-
```ts
|
|
31
|
-
page.route(url: string | RegExp, handler: (route: Route) => void | Promise<void>): Promise<void>;
|
|
32
|
-
page.unroute(url: string | RegExp, handler?): Promise<void>;
|
|
33
|
-
page.unrouteAll(): Promise<void>;
|
|
34
|
-
|
|
35
|
-
class Route {
|
|
36
|
-
request(): Request;
|
|
37
|
-
fulfill(o: { status?: number; headers?: Record<string,string>; contentType?: string; body?: string | Buffer; json?: unknown }): Promise<void>;
|
|
38
|
-
continue(o?: { url?: string; method?: string; headers?: Record<string,string>; postData?: string | Buffer }): Promise<void>;
|
|
39
|
-
abort(): Promise<void>; // BiDi failRequest has no error code
|
|
40
|
-
fallback(): Promise<void>; // pass to the next matching route
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
URL matching: a string without `*` is an exact match; with `*`/`**` a glob (`*` = no `/`, `**` = anything); a `RegExp` tests the full URL.
|
|
45
|
-
|
|
46
|
-
## Components — `src/network/router.ts` (new)
|
|
47
|
-
|
|
48
|
-
- **One intercept per page**, not per route: on the first `page.route`, send `network.addIntercept { phases: ["beforeRequestSent"], contexts: [pageContext] }` (no `urlPatterns`); matching happens in Node. This avoids translating globs/RegExps to BiDi URL patterns and keeps ordering logic in one place. Unmatched blocked requests are released immediately with `continueRequest { request }`.
|
|
49
|
-
- Optimisation: when every registered route is a plain exact string, pass `urlPatterns: [{ type: "string", pattern }]` so unrelated traffic is never blocked. Recomputed (remove + add intercept) when routes change.
|
|
50
|
-
- On `network.beforeRequestSent` with `isBlocked: true` for this page (`ContextTree`): find matching routes, newest first; create a `Route`; call the handler. `fallback()` moves to the next match; when none remain (or none matched) the request continues unchanged.
|
|
51
|
-
- Resolution mapping:
|
|
52
|
-
- `fulfill` → `network.provideResponse { request, statusCode, reasonPhrase, headers, body }`. `json` sets body `JSON.stringify` and content type `application/json`; `Buffer` body is `{ type: "base64" }`, string is `{ type: "string" }`. `Content-Length` computed unless supplied. Default status 200.
|
|
53
|
-
- `continue` → `network.continueRequest { request, url, method, headers, body }` (headers as `{ name, value: { type: "string", value } }`).
|
|
54
|
-
- `abort` → `network.failRequest { request }`.
|
|
55
|
-
- **Every blocked request must be resolved exactly once.** Guarantees:
|
|
56
|
-
- a second resolution call throws `Error("route already handled")`;
|
|
57
|
-
- a handler that throws → the request is failed (`failRequest`) and the error is stored and rethrown to the test after the next `await` boundary via `page.routeErrors` surfaced in the runner (same channel as page errors);
|
|
58
|
-
- a handler that returns without resolving → after the handler's promise settles the route auto-`continue`s and logs a warn (safer than a hang);
|
|
59
|
-
- `unroute*` and `dispose` resolve pending routes by continuing them, then `removeIntercept`.
|
|
60
|
-
- a route pending longer than `network.routeTimeout` (default 30000) logs an error and is continued.
|
|
61
|
-
- Blocked requests count as in flight for `waitForNetworkIdle` (tracking spec).
|
|
62
|
-
- Requests from other contexts are untouched: the intercept is scoped with `contexts`.
|
|
63
|
-
|
|
64
|
-
## Errors
|
|
65
|
-
|
|
66
|
-
| Situation | Behaviour |
|
|
67
|
-
|---|---|
|
|
68
|
-
| Resolve twice | `Error("route already handled")` |
|
|
69
|
-
| Handler throws | request failed, error reported to the test |
|
|
70
|
-
| Browser rejects (`no such request`, request already finished) | `BiDiError` rethrown from the Route method |
|
|
71
|
-
| Redirect to a URL matching another route | the new hop is a new blocked request, routed independently |
|
|
72
|
-
|
|
73
|
-
## Testing
|
|
74
|
-
|
|
75
|
-
- Unit (stub connector): single intercept for multiple routes; exact-only optimisation and re-registration; glob and RegExp matching; newest-first ordering and `fallback`; each resolution's payload (fulfill json/Buffer/headers/default status, continue overrides, abort); unmatched blocked request continued; double resolve; throwing handler → `failRequest`; non-resolving handler → auto-continue + warning; `unroute` continues pending then removes the intercept; foreign-context request ignored.
|
|
76
|
-
- Browser (local server): fulfil an XHR with JSON and assert the page sees it; abort an image and see `onerror`; continue with an added header and echo it from the server; a redirect chain; a route that never resolves does not hang the page.
|
|
77
|
-
- TDD: tests first.
|
|
78
|
-
|
|
79
|
-
## Implementation notes
|
|
80
|
-
|
|
81
|
-
- **Where:** `network/url-match.ts` (`matchUrl`, `isExactString`, `samePattern`), `network/router.ts` (`Router`, `Route`), `Page.route/unroute/unrouteAll/routeErrors`, config `network.routeTimeout` (default 30000), and `RouteErrorsError` in `runner/page-logs-report.ts`. `network-tracker.ts` now exports `toRequest`.
|
|
82
|
-
- **One intercept per page, matching here**, as designed. The router listens to `network.beforeRequestSent` itself (not through the tracker, so it works when `network.track` is false) and only acts on requests held by one of its own intercept ids, in this page or its frames. Adding a route that widens what must be blocked adds the new intercept first and removes the old one after, so nothing slips through.
|
|
83
|
-
- **Exact-URL optimisation is conservative.** `urlPatterns` are sent only when every route is an exact string already in `new URL(x).href` form (so the browser reads it as we do); anything else blocks everything and matches in Node. If a browser rejects `urlPatterns` (`invalid argument`) the router falls back to blocking everything and remembers that per connection.
|
|
84
|
-
- **Every held request is answered exactly once:** a second answer throws `route already handled`; a handler that throws fails the request (`failRequest`) and the error is kept in `page.routeErrors()` (message `route handler for <url> threw: <message>`, cause attached); a handler that returns without answering lets the request continue with a `warn` (so a handler must `await` its answer; answering later from a callback is too late); a request pending longer than `routeTimeout` continues with an `error` log; `unroute*` and `dispose` continue the requests the removed routes still hold before the intercept goes; `fallback()` hands the request to the next older matching route, and past the last one it continues unchanged.
|
|
85
|
-
- **Runner:** route handler errors fail a passing test (`RouteErrorsError`, `<n> route handler error(s): …`), regardless of `logs.failOnPageError`; they take precedence over page errors, and a test that already failed keeps its own error.
|
|
86
|
-
- **`fulfill`:** status defaults to 200 with the standard reason phrase; `json` implies `application/json`; `content-type` and `content-length` (bytes) are added only when the headers you pass have none, compared case-insensitively; `json` together with `body` is a `TypeError`.
|
|
87
|
-
- **`continue`:** only what you pass is sent. A new `postData` always comes with a matching `content-length` (dropping the request's old one): otherwise Firefox keeps the old header and cuts the new body off (or waits for missing bytes). `headers` replaces all request headers, so spread `route.request().headers` to add one.
|
|
88
|
-
- **Firefox 153 facts** (browser tests): `fulfill`, `abort`, `fallback`, header changes, method changes and body replacement work for `fetch`, images and documents, iframes included; mocking a page's document by routing its URL works. **Changing the URL only works for navigations:** for `fetch`, XHR and images Firefox fails the request (`AbortError` / error event) although the rewritten request reaches the server, hence the note on `ContinueOptions.url`. A `fetch` from a page that is itself a JSON document (Firefox's JSON viewer) fails whatever the routes: use an HTML page in tests.
|
|
89
|
-
- Blocked requests stay in flight for `waitForNetworkIdle` (browser test: idle waits for a request a handler holds for 400 ms).
|
|
90
|
-
- **Not done:** `route.fetch()`, modifying real responses, HTTP auth; unchanged from the non-goals.
|
|
91
|
-
- Tests: `network/url-match.test.ts`, `network/router.test.ts` (38), "Page routing" block in `browser/page.test.ts`, route errors in `runner/page-logs-report.test.ts`, `network/router.browser-test.ts` (port 9241, local server, 20 tests).
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# Network tracking and real network idle — design
|
|
2
|
-
|
|
3
|
-
Date: 2026-09-29
|
|
4
|
-
Status: implemented (see "Implementation notes")
|
|
5
|
-
Roadmap item: 5
|
|
6
|
-
Depends on: bidi-foundation
|
|
7
|
-
|
|
8
|
-
## Goal
|
|
9
|
-
|
|
10
|
-
Replace `Page.waitForNetworkIdle` (today it only waits for the next `browsingContext.load` event, leaks its listener, and never resolves if `load` already fired) with a tracker built on `network.beforeRequestSent`, `network.responseCompleted` and `network.fetchError`. The same tracker later feeds interception and response data.
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
await page.navigateTo("example.com");
|
|
14
|
-
await page.waitForNetworkIdle(); // no requests in flight for 500ms
|
|
15
|
-
await page.waitForNetworkIdle({ idleTime: 200, timeout: 10000 });
|
|
16
|
-
page.on("request", (r) => …); page.on("response", (r) => …); page.on("requestfailed", (r) => …);
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Non-goals
|
|
20
|
-
|
|
21
|
-
- Blocking or altering requests (interception spec), response bodies (network-data spec).
|
|
22
|
-
- Tracking requests of other tabs.
|
|
23
|
-
- WebSocket / SSE frames (BiDi has no events for them).
|
|
24
|
-
|
|
25
|
-
## Components
|
|
26
|
-
|
|
27
|
-
### `src/network/network-tracker.ts` (new)
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
class NetworkTracker {
|
|
31
|
-
static async start(connector, tree: ContextTree, contextId: string, options?: TrackerOptions): Promise<NetworkTracker>;
|
|
32
|
-
readonly inflight: number;
|
|
33
|
-
waitForIdle(options: { idleTime: number; timeout: number }): Promise<void>;
|
|
34
|
-
on(event: "request" | "response" | "requestfailed", listener): void;
|
|
35
|
-
off(…): void;
|
|
36
|
-
dispose(): Promise<void>;
|
|
37
|
-
}
|
|
38
|
-
interface TrackerOptions { ignore?: (request: NetworkRequest) => boolean } // default: ignore none
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
- Subscribes (refcounted) to the three network events. Events are kept when `tree.isWithin(event.context, contextId)`.
|
|
42
|
-
- In-flight key: `${request.request}:${request.redirectCount}`. `beforeRequestSent` adds; `responseCompleted` and `fetchError` with the same key remove. A redirect is a new key, so a redirect chain counts as one live request at a time. Events with unknown keys (tracker started mid-request) are ignored on removal.
|
|
43
|
-
- Blocked requests (`isBlocked: true`, interception spec) stay in flight until resolved, so idle never fires while a route handler is pending.
|
|
44
|
-
- Never-completing requests (long-poll, SSE) would block idle forever: `ignore` lets callers exclude them, and `waitForIdle` also stops at its timeout.
|
|
45
|
-
- `waitForIdle`: resolves once `inflight === 0` has held for `idleTime` (a timer restarts on every new request). Checks the current state first, so it resolves immediately on an already-idle page (after `idleTime`). Rejects with `WaitTimeoutError` including the URLs still in flight (max 5) at the deadline.
|
|
46
|
-
- Started at `Page` creation, always on. Cost: three event listeners and a `Map`. Config `network.track: false` disables it; `waitForNetworkIdle` then throws `Error("network tracking is disabled")`.
|
|
47
|
-
- Must exist before the first navigation, otherwise requests started earlier are missed and idle can fire early; `Page` creation awaits `NetworkTracker.start`.
|
|
48
|
-
|
|
49
|
-
### `Page` changes
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
waitForNetworkIdle(options?: { idleTime?: number; timeout?: number }): Promise<void>; // 500ms / navigation timeout
|
|
53
|
-
on/off("request" | "response" | "requestfailed", …)
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
- `idleTime` default 500 (config `network.idleTime`), `timeout` default from navigation spec (30000).
|
|
57
|
-
- The old zero-argument call keeps working. The old behaviour (resolve on `load`) is gone; `navigateTo` with `wait: "complete"` covers it (navigation-waits spec).
|
|
58
|
-
- `Request` wrapper: `url`, `method`, `headers` (plain object, lower-cased names), `resourceType` (from `initiatorType`/`destination`), `navigation`, `redirectedFrom`. `Response`: `url`, `status`, `statusText`, `headers`, `fromCache`, `request`. Bodies are not included here.
|
|
59
|
-
|
|
60
|
-
## Errors
|
|
61
|
-
|
|
62
|
-
| Situation | Error |
|
|
63
|
-
|---|---|
|
|
64
|
-
| Idle not reached | `WaitTimeoutError` → message `waitForNetworkIdle(): still <n> request(s) in flight after <t>ms: <urls>` |
|
|
65
|
-
| Tracker disabled | `Error("network tracking is disabled")` |
|
|
66
|
-
| Page closed while waiting | rejects with `Error("page closed")` |
|
|
67
|
-
|
|
68
|
-
## Testing
|
|
69
|
-
|
|
70
|
-
- Unit (stub connector emitting scripted events): add/remove keys; redirect chain (same id, incrementing `redirectCount`) counts as one at a time; `fetchError` removes; event from an iframe context counted, from an unrelated context ignored; idle timer restarts on a new request; immediate idle on quiet page; timeout message lists URLs; `ignore` predicate; `dispose` unsubscribes and removes listeners; disabled config.
|
|
71
|
-
- Browser: local `node:http` server with `/slow` (300ms) and a redirect route; page fires three fetches on a timer; `waitForNetworkIdle` resolves after the last one plus `idleTime`, not before; a hanging route is reported on timeout.
|
|
72
|
-
- TDD: tests first.
|
|
73
|
-
|
|
74
|
-
## Implementation notes
|
|
75
|
-
|
|
76
|
-
- **Where it lives:** `src/network/network-tracker.ts` (`NetworkTracker`, `NetworkIdleTimeoutError`, `toHeaders`), `Page.waitForNetworkIdle/on/once/off/startNetworkTracking`, config `network: { track, idleTime }` (in `types/config.d.ts`), `PageSettings` (3rd `Page` constructor argument) to override config in tests.
|
|
77
|
-
- **Start:** the tracker is created lazily by `Page` (its constructor stays synchronous), and eagerly by `Browser.launch` and `Browser.newPage` through `page.startNetworkTracking()`, so requests made before the first navigation are seen. A `Page` built by hand starts tracking on first use and misses earlier requests.
|
|
78
|
-
- **`NetworkIdleTimeoutError extends WaitTimeoutError<string[]>`**: `last` holds up to five in-flight URLs and the message is the one from the spec.
|
|
79
|
-
- **`page.on/once/off`** are typed for `request` / `response` / `requestfailed` and start tracking on first use. Listener exceptions are logged, never break tracking.
|
|
80
|
-
- **Redirects:** a hop is linked to the previous one (`redirectedFrom`) using the `request id:redirect count` key. A redirect response keeps its request for the next hop; other responses and failures drop it.
|
|
81
|
-
- **`requestfailed`** payload is the request plus `errorText`.
|
|
82
|
-
- **Disabled tracking:** `waitForNetworkIdle()` rejects with `network tracking is disabled`; nothing is subscribed.
|
|
83
|
-
- **`dispose()`** rejects pending waits with `page closed`, removes listeners and unsubscribes; `Page.dispose()` disposes the tracker before the context tree it reads.
|
|
84
|
-
- **The old `waitForNetworkIdle`** (wait for the next `load`) is gone; `navigateTo(…, "complete")` covers that.
|
|
85
|
-
- **Test runner:** `npm run test:browser` now passes `--test-concurrency=1`. All browser tests launch Firefox with the same profile directory, and launching six at once made some fail to start.
|
|
86
|
-
- Tests: `network/network-tracker.test.ts`, the "Page network tracking" block in `browser/page.test.ts`, `network/network-tracker.browser-test.ts` (port 9237, local `node:http` server with slow, redirect, failing, hanging and iframe routes).
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# Page API completeness — design
|
|
2
|
-
|
|
3
|
-
Date: 2026-09-29
|
|
4
|
-
Status: implemented (see "Implementation notes")
|
|
5
|
-
Roadmap item: 11
|
|
6
|
-
Depends on: bidi-foundation, script-call-function
|
|
7
|
-
|
|
8
|
-
## Goal
|
|
9
|
-
|
|
10
|
-
Finish the everyday page surface with BiDi commands that have no framework wrapper yet, and fix `captureScreenshot`, which currently throws its result away (`// TODO: Store screenshot`).
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
await page.setViewport({ width: 1280, height: 720, devicePixelRatio: 2 });
|
|
14
|
-
await page.reload({ wait: "complete", ignoreCache: true });
|
|
15
|
-
await page.goBack(); await page.goForward();
|
|
16
|
-
const png = await page.screenshot({ path: "out/home.png", fullPage: true });
|
|
17
|
-
await page.locator("//header").screenshot();
|
|
18
|
-
await page.pdf({ path: "out/page.pdf", format: "A4" });
|
|
19
|
-
page.url(); await page.title();
|
|
20
|
-
await page.close({ runBeforeUnload: true });
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Non-goals
|
|
24
|
-
|
|
25
|
-
- Window management (`browser.setClientWindowState`).
|
|
26
|
-
- PDF header/footer templates (not in BiDi).
|
|
27
|
-
- Multi-page tab APIs (contexts spec).
|
|
28
|
-
|
|
29
|
-
## API and mapping
|
|
30
|
-
|
|
31
|
-
| Method | BiDi | Notes |
|
|
32
|
-
|---|---|---|
|
|
33
|
-
| `setViewport({ width, height, devicePixelRatio? })` | `browsingContext.setViewport { context, viewport, devicePixelRatio }` | `null` resets. Validates positive integers. |
|
|
34
|
-
| `reload({ wait?, ignoreCache? })` | `browsingContext.reload` | same wait/timeout/`NavigationError` semantics as `navigateTo` (navigation-waits spec) |
|
|
35
|
-
| `goBack()` / `goForward()` | `browsingContext.traverseHistory { delta: ∓1 }` | resolves `true`; on `no such history entry` resolves `false` (not an error, matches Playwright's null) |
|
|
36
|
-
| `screenshot(options?)` | `browsingContext.captureScreenshot` | returns `Buffer`; see below |
|
|
37
|
-
| `Locator.screenshot(options?)` | `captureScreenshot` with `clip: { type: "element", element: SharedReference }` | scrolls into view first (`attached`+`visible` wait) |
|
|
38
|
-
| `pdf(options?)` | `browsingContext.print` | returns `Buffer`; `path`, `landscape`, `scale`, `pageRanges`, `margin`, `background`, `format` (`A4`/`Letter` → page size in cm) |
|
|
39
|
-
| `url()` | `callFunction(() => location.href)` | |
|
|
40
|
-
| `title()` | `callFunction(() => document.title)` | |
|
|
41
|
-
| `close({ runBeforeUnload? })` | `browsingContext.close { promptUnload }` | further use of the page throws `Error("page closed")`; disposes trackers/subscriptions |
|
|
42
|
-
|
|
43
|
-
### Screenshot
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
interface ScreenshotOptions {
|
|
47
|
-
path?: string; // written (dirs created) in addition to being returned
|
|
48
|
-
fullPage?: boolean; // origin: "document"; default "viewport"
|
|
49
|
-
clip?: { x: number; y: number; width: number; height: number };
|
|
50
|
-
type?: "png" | "jpeg"; // format.type
|
|
51
|
-
quality?: number; // 0–100 → format.quality 0–1, jpeg only
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
- Base64 result decoded to a `Buffer`.
|
|
56
|
-
- Default output dir for auto-named screenshots (test failure screenshots, next step of the runner) from config `screenshotDir` (default `result/screenshots`); this spec only provides the mechanism.
|
|
57
|
-
- `clip` and `fullPage` combine as BiDi `clip: { type: "box", x, y, width, height }` with `origin: "document"`.
|
|
58
|
-
|
|
59
|
-
### Default viewport
|
|
60
|
-
|
|
61
|
-
`use: { viewport: { width: 1280, height: 720 } }` in config applied when a `Page` is created, so headless screenshots are reproducible. `viewport: null` keeps the browser default.
|
|
62
|
-
|
|
63
|
-
## Errors
|
|
64
|
-
|
|
65
|
-
| Situation | Error |
|
|
66
|
-
|---|---|
|
|
67
|
-
| Invalid viewport numbers | `RangeError` before sending |
|
|
68
|
-
| `jpeg` quality out of range or with png | `RangeError` |
|
|
69
|
-
| Locator not attached/visible within timeout | `ActionTimeoutError` (`action: "screenshot"`) |
|
|
70
|
-
| Use after `close()` | `Error("page closed")` |
|
|
71
|
-
| Screenshot/print rejected (`unable to capture screen`) | `BiDiError` |
|
|
72
|
-
|
|
73
|
-
## Testing
|
|
74
|
-
|
|
75
|
-
- Unit (stub connector): payload for each method; base64 → Buffer and `path` write (temp dir); clip/fullPage/jpeg mapping; `goBack` false on `no such history entry`; viewport validation; `close` sets closed state and disposes trackers; default viewport applied at creation; `Locator.screenshot` uses element clip with the resolved reference.
|
|
76
|
-
- Browser: viewport 400×300 reflected by `innerWidth`; PNG magic bytes; full-page taller than viewport; element screenshot dimensions ≈ bounding box; history back/forward across two `about:blank`-style data pages; PDF starts with `%PDF`.
|
|
77
|
-
- TDD: tests first.
|
|
78
|
-
|
|
79
|
-
## Implementation notes
|
|
80
|
-
|
|
81
|
-
- **Where:** `browser/screenshot.ts` (`takeScreenshot`, `writeOutput`), `browser/pdf.ts` (`printPdf`, paper sizes), and on `Page`: `setViewport`, `applyDefaultViewport`, `reload`, `goBack`, `goForward`, `url`, `title`, `screenshot`, `pdf`, `close`, `closed`; `Locator.screenshot`. Config `use.viewport` (default 1280x720; `null` keeps the browser's own size).
|
|
82
|
-
- **Image types go out as MIME types** (`image/png`, `image/jpeg`): Firefox silently answers a PNG for anything else (`"jpeg"` included). `quality` (0–100, jpeg only) is sent as a 0–1 fraction; the local `ImageFormat` typing was narrowed to five literal qualities and is now a `number`. `captureScreenshot`'s result type is `{ data: string }` (was `{}`).
|
|
83
|
-
- **`screenshot`:** nothing but the context is sent by default; `fullPage` adds `origin: "document"`, `clip` a box (viewport coordinates, or document coordinates together with `fullPage`), `path` writes the file after creating folders. A quality without `type: "jpeg"`, or outside 0–100, is a `RangeError` before anything is sent. `Locator.screenshot` waits for attached + visible (with a scroll into view) and clips to the element (`ActionTimeoutError` with `action: "screenshot"`).
|
|
84
|
-
- **The old `Page.captureScreenshot(clip, format, origin)` is left as it was;** `screenshot()` is the API to use. The auto-named failure screenshot and a `screenshotDir` config were not added ("this spec only provides the mechanism").
|
|
85
|
-
- **`reload`** shares `navigateTo`'s handling (default wait `complete` or config, `NavigationError` named `reload(): <url> failed: …`, the navigation timeout, progress tracking for `wait: "none"`; the code moved into a private `Page.navigation()`). **Firefox 153 does not support `ignoreCache`** (`unsupported operation`), so asking for it throws `UnsupportedOperationError`.
|
|
86
|
-
- **`goBack` / `goForward`** answer false on `no such history entry`. A new tab's initial `about:blank` is not a history entry. Walking a tab back past its first entry lands on Firefox's own start page, and a later `reload` there hung until the navigation timeout in one run; keep tests inside the entries they created.
|
|
87
|
-
- **`setViewport`** validates positive integers (`RangeError`); `null` resets size and ratio. An omitted `devicePixelRatio` keeps the current one (BiDi's behaviour), so reset with `null` when a test set one. Firefox headless opens at 1227x682, hence the 1280x720 default, applied by `Browser.launch` / `newPage`.
|
|
88
|
-
- **`pdf`:** `landscape`, `scale` (0.1–2, `RangeError`), `pageRanges`, `margin` (centimetres), `printBackground`, `format` (A0–A6, Letter, Legal, Tabloid, Ledger; unknown is a `RangeError`) and `path`. Browser test: A4 is about 595x842 pt, landscape swaps them, Letter is about 612x792 (found by inflating the PDF's compressed streams).
|
|
89
|
-
- **`close({ runBeforeUnload })`** sends `browsingContext.close` (`promptUnload`), treats `no such frame` as already closed, then disposes what the page registered (errors from that are only logged). After it `page.closed` is true and `navigateTo`, `reload`, `goBack`, `goForward`, `screenshot`, `pdf`, `url`, `title`, `setViewport`, `waitForLoadState`, `addInitScript`, `route`, `locator` and the `getBy*` methods throw `page closed`; a second `close()` does nothing. Closing the browser's last tab may end the browser.
|
|
90
|
-
- Tests: the "Page API" block in `browser/page.test.ts`, `browser/page-api.browser-test.ts` (port 9244; PNG dimensions read from the header, JPEG quality vs size, PDF page boxes, history, reload, close).
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# `script.addPreloadScript` — design
|
|
2
|
-
|
|
3
|
-
Date: 2026-09-29
|
|
4
|
-
Status: implemented (see "Implementation notes")
|
|
5
|
-
Roadmap item: 2
|
|
6
|
-
Depends on: bidi-foundation, script-call-function
|
|
7
|
-
|
|
8
|
-
## Goal
|
|
9
|
-
|
|
10
|
-
Run code before any page script in every new document, so (a) the framework installs its own helper library once per document instead of re-sending probe source every poll, and (b) users get `page.addInitScript`.
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
await page.addInitScript(() => { window.__testMode = true }); // page's own world
|
|
14
|
-
await page.addInitScript((flag) => { … }, { flag: 1 });
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Non-goals
|
|
18
|
-
|
|
19
|
-
- Exposing binding functions (`exposeFunction`); `script.message` channels are used internally only (logs spec).
|
|
20
|
-
- Removing scripts by handle from user code beyond the returned `dispose()`.
|
|
21
|
-
|
|
22
|
-
## Components
|
|
23
|
-
|
|
24
|
-
### `src/script/preload.ts` (new)
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
export interface PreloadHandle { readonly id: string; dispose(): Promise<void> }
|
|
28
|
-
export async function addPreload(
|
|
29
|
-
connector: BiDiConnector,
|
|
30
|
-
options: { source: string; contexts?: string[]; sandbox?: string; args?: ChannelValue[] },
|
|
31
|
-
): Promise<PreloadHandle>;
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
- Sends `script.addPreloadScript { functionDeclaration, contexts, sandbox, arguments }`; result `{ script }` is the id.
|
|
35
|
-
- Preload scripts only affect documents created **after** registration. `addPreload` therefore also runs the same function once in every already-loaded context in scope (`browsingContext.getTree`, via `callFunction` in the same sandbox), so registration order never matters.
|
|
36
|
-
- `dispose()` sends `script.removePreloadScript`; idempotent. Already-loaded documents keep the effect until they navigate; documented.
|
|
37
|
-
|
|
38
|
-
### Framework helper realm
|
|
39
|
-
|
|
40
|
-
- Name: sandbox `"samurai"`. A sandbox realm shares the DOM with the page but has its own JS globals, so page code cannot tamper with the helpers and helpers do not leak into `window`.
|
|
41
|
-
- `src/script/helpers.ts` exports a function declaration that installs `globalThis.__samurai = { probeElement, resolveXpath, … }` in the sandbox realm.
|
|
42
|
-
- `Page` registers it once at creation (`contexts: [rootContext]` includes its iframes automatically since preload scripts apply to child navigables of the listed top-level context).
|
|
43
|
-
- Locator `callFunction` calls pass `sandbox: "samurai"` and call `(…a) => globalThis.__samurai.probeElement(…a)`. If the helper is missing (registration lost), the wrapper reinstalls it once and retries; a second miss throws `ScriptError`.
|
|
44
|
-
|
|
45
|
-
### `page.addInitScript`
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
addInitScript(script: string | ((arg?: any) => void), arg?: unknown): Promise<PreloadHandle>;
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
- Runs in the page's main realm (no sandbox): it is meant to change what page scripts see.
|
|
52
|
-
- Function form is serialized like `callFunction` (same `__name(` guard). `arg` goes through `toLocalValue` as the function's first argument.
|
|
53
|
-
- All init scripts registered on a page dispose with it.
|
|
54
|
-
|
|
55
|
-
## Errors
|
|
56
|
-
|
|
57
|
-
| Situation | Error |
|
|
58
|
-
|---|---|
|
|
59
|
-
| Init script throws | Browser reports through `log.entryAdded` (javascript error); the logs spec surfaces it. Not a test failure by itself. |
|
|
60
|
-
| Registration fails (`invalid argument`) | `BiDiError` rethrown from `addInitScript` |
|
|
61
|
-
| Helper missing during a locator call | one reinstall + retry, then `ScriptError` |
|
|
62
|
-
|
|
63
|
-
## Testing
|
|
64
|
-
|
|
65
|
-
- Unit: payload shape (sandbox, contexts, arguments); the "also run in existing contexts" step calls `getTree` and one `callFunction` per context; `dispose` is idempotent; helper-missing retry path with a stub returning the missing-helper error once.
|
|
66
|
-
- Browser: an init script sets `window.__x = 1`, then after `navigateTo` a locator-independent `evaluate` reads it; a script registered *after* load still applies to the current document (via the immediate run) and to the next navigation; the helper realm is invisible: `typeof window.__samurai === "undefined"` from the main realm; `dispose()` stops the effect on the next navigation.
|
|
67
|
-
- TDD: tests first.
|
|
68
|
-
|
|
69
|
-
## Implementation notes
|
|
70
|
-
|
|
71
|
-
Deviations and details settled while implementing:
|
|
72
|
-
|
|
73
|
-
- **Lazy install.** `Page` is constructed synchronously (tests and `Browser` build pages without I/O), so the helpers are not registered at creation. `Locator` awaits `HelperRealm.ensureInstalled()` before every probe; the first call registers, later calls reuse the cached promise (a failed install is retried). The immediate run covers any document loaded before the first probe.
|
|
74
|
-
- **Missing helpers are detected with a marker, not error text.** The call wrapper is `(...args) => globalThis.__samurai ? probeElement(...args) : "samurai:helpers-missing"`. On the marker the locator calls `reinstall()` (runs the helpers again in the loaded documents, no new registration) and retries once; a second marker throws `ScriptError`.
|
|
75
|
-
- **Only `probeElement` is installed.** `resolveXpath` from the draft is not added: nothing calls it yet, and the other element reads keep resolving the xpath inline.
|
|
76
|
-
- **A `Locator` built by hand (no helper realm) still works:** it sends the probe source to the page realm on every poll, as before. `Page.locator()` always passes its realm.
|
|
77
|
-
- **Channels are not supported yet.** `addPreload` has no `args`; BiDi preload arguments can only be channels, and the logs spec will add them when it needs `script.message`.
|
|
78
|
-
- **`addInitScript` argument.** Preload scripts cannot take arguments, so the function is wrapped as `() => (<function>)(<JSON of arg>)`; `arg` must be JSON-serializable (`TypeError` otherwise, and for a `bigint` or a cycle). A string script is wrapped as `() => { <script> }` and takes no argument.
|
|
79
|
-
- **Errors while running in an already-loaded document.** For `addInitScript` a throwing script is logged (`warn`) and the registration stays; for the framework helpers it fails the install. If the immediate run throws, the fresh registration is removed again.
|
|
80
|
-
- **`page.dispose()`** (new) removes init scripts, the helper registration and the context tree; the page-api spec's `close()` should call it.
|
|
81
|
-
- **Verified in Firefox:** preload scripts with a `sandbox` create the sandbox realm before page scripts; init scripts also apply to iframes created later; the page cannot reach or overwrite `__samurai` (it lives only in the sandbox realm). Firefox prints `TypeError: WeakMap key null …` from its own `browsingContext.sys.mjs` on stderr when an iframe loads under a preload script; it is harmless and unrelated to our code.
|
|
82
|
-
- Tests: `script/{preload,helpers}.test.ts`, additions to `browser/page.test.ts` and `locator/locator-actions.test.ts`, `script/preload.browser-test.ts` (port 9234). The reply pumps in the fake-WebSocket tests are `unref`'d so a failing test can no longer hang the run.
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# `script.callFunction` — design
|
|
2
|
-
|
|
3
|
-
Date: 2026-09-29
|
|
4
|
-
Status: implemented (see "Implementation notes")
|
|
5
|
-
Roadmap item: 1 (with `script.disown`)
|
|
6
|
-
Depends on: bidi-foundation
|
|
7
|
-
|
|
8
|
-
## Goal
|
|
9
|
-
|
|
10
|
-
Replace string-built `script.evaluate` expressions with typed function calls. Arguments are serialized by the protocol (no JSON-in-JS-string escaping), elements travel as `SharedReference`s, and return values are deserialized to plain JS.
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
// before (locator.ts): xpath embedded via JSON.stringify inside a source string
|
|
14
|
-
await this.evaluate(`document.evaluate(${JSON.stringify(xpath)}, …).singleNodeValue.focus()`);
|
|
15
|
-
|
|
16
|
-
// after
|
|
17
|
-
await script.call(target, (xpath) => { … }, [xpath]);
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Non-goals
|
|
21
|
-
|
|
22
|
-
- Public `page.evaluate` (a later, tiny follow-up on top of this module).
|
|
23
|
-
- Changing what the actionability probe checks.
|
|
24
|
-
- Sandboxed realms (preload-scripts spec).
|
|
25
|
-
|
|
26
|
-
## Components
|
|
27
|
-
|
|
28
|
-
### `src/script/serialize.ts` (new)
|
|
29
|
-
|
|
30
|
-
```ts
|
|
31
|
-
export type Arg = unknown | ElementHandle;
|
|
32
|
-
export function toLocalValue(value: Arg): LocalValue;
|
|
33
|
-
export function fromRemoteValue(value: RemoteValue): unknown;
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
`toLocalValue` mapping: `undefined`/`null`/boolean/string → primitive types; number incl. `NaN`, `Infinity`, `-Infinity`, `-0` via the special-number form; `bigint`; `Date` → `date`; `RegExp` → `regexp`; `Array` → `array`; `Map` → `map`; `Set` → `set`; plain object → `object` (entries); `ElementHandle` → `{ sharedId, handle? }` reference; functions and symbols throw `TypeError("cannot serialize <type>")`. Cycles throw `TypeError` (depth cap 100 as a second guard).
|
|
37
|
-
|
|
38
|
-
`fromRemoteValue` is the inverse for `RemoteValue` (`node` → `ElementHandle`, `window`/`function`/`error`/`promise` etc. → an opaque `RemoteObject` with `type` and `handle`; `error` → `Error` with the remote message).
|
|
39
|
-
|
|
40
|
-
### `src/script/element-handle.ts` (new)
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
export class ElementHandle { readonly sharedId: string; readonly handle?: string; }
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Node values returned by `callFunction` become `ElementHandle`s. A `sharedId` stays valid across scripts within the same document, also after the node was removed from the DOM (Firefox keeps a detached node resolvable while it is alive); after a navigation to another document the browser answers `no such node`. `ElementHandle` is internal; user code keeps using `Locator`.
|
|
47
|
-
|
|
48
|
-
### `src/script/call-function.ts` (new)
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
export interface CallOptions {
|
|
52
|
-
awaitPromise?: boolean; // default true
|
|
53
|
-
ownership?: "none" | "root"; // default "none"; "root" keeps a handle alive until disown
|
|
54
|
-
sandbox?: string; // realm sandbox name (preload-scripts spec)
|
|
55
|
-
thisArg?: Arg;
|
|
56
|
-
}
|
|
57
|
-
export async function callFunction<T = unknown>(
|
|
58
|
-
connector: BiDiConnector,
|
|
59
|
-
context: string,
|
|
60
|
-
fn: string | ((...args: any[]) => unknown),
|
|
61
|
-
args?: Arg[],
|
|
62
|
-
options?: CallOptions,
|
|
63
|
-
): Promise<T>;
|
|
64
|
-
export async function disown(connector, context, handles: string[]): Promise<void>;
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
- `fn` string is used as-is as `functionDeclaration`. `fn` function is `fn.toString()`.
|
|
68
|
-
- Function-form caveat: `tsx`/esbuild may wrap named functions with a `__name(...)` helper that does not exist in the page. `callFunction` throws `ScriptSerializationError` if the source contains `__name(`, telling the caller to use an arrow function without a name or the string form. A unit test runs under `tsx` to lock this behaviour.
|
|
69
|
-
- `exception` results throw `ScriptError` (`message`, `text`, `stackTrace`, `functionSource` trimmed to 200 chars).
|
|
70
|
-
- `type: "success"` returns `fromRemoteValue(result)`.
|
|
71
|
-
- `awaitPromise: true` is safe for sync functions and required for async ones; the auto-wait probes pass `false`.
|
|
72
|
-
- With `ownership: "root"` the caller receives `RemoteObject.handle` and is responsible for `disown`; `disown` swallows `no such handle`.
|
|
73
|
-
|
|
74
|
-
### Locator migration (`src/locator/*`)
|
|
75
|
-
|
|
76
|
-
- `elementStateScript(expression, options)` becomes `probeElement(xpath: string, options: ProbeOptions)`, a function declaration executed via `callFunction`. The xpath is an argument, so `JSON.stringify` embedding disappears. `parseElementState` becomes a type guard over the returned object (no JSON string round trip).
|
|
77
|
-
- `Locator.evaluate(expression)` is replaced by `Locator.call(fn, args)`, which resolves the element inside the function (`document.evaluate(xpath…)`) until locate-nodes lands.
|
|
78
|
-
- Every existing action keeps its observable behaviour and error messages. `ScriptError` from the probe becomes the existing `Failed to locate element <xpath>. Details: …` message.
|
|
79
|
-
|
|
80
|
-
## Errors
|
|
81
|
-
|
|
82
|
-
| Situation | Error |
|
|
83
|
-
|---|---|
|
|
84
|
-
| Un-serializable argument | `TypeError` before anything is sent |
|
|
85
|
-
| Function throws in page | `ScriptError` |
|
|
86
|
-
| `no such node` from a `sharedId` of a previous document | `BiDiError` (callers such as `Locator` catch and re-resolve) |
|
|
87
|
-
| `__name(` in function source | `ScriptSerializationError` |
|
|
88
|
-
|
|
89
|
-
## Testing
|
|
90
|
-
|
|
91
|
-
- Unit: `toLocalValue`/`fromRemoteValue` round trip for every mapped type, special numbers, nested structures, cycles, unsupported types. `callFunction` with the stub connector: payload shape (`functionDeclaration`, `arguments`, `target`, `awaitPromise`, `resultOwnership`, `this`), exception mapping, `__name(` guard.
|
|
92
|
-
- The full existing locator/expect unit suite passes unchanged (regression net for the migration).
|
|
93
|
-
- Browser: xpath containing `"`, `'`, `\` and a newline is located without escaping issues (previously a risk class); a `Map`/`Date` argument round trips; `ownership: "root"` handle then `disown` succeeds, second `disown` is silent; a removed node stays usable, and a `sharedId` from before a navigation yields `no such node`.
|
|
94
|
-
- TDD: tests first.
|
|
95
|
-
|
|
96
|
-
## Implementation notes
|
|
97
|
-
|
|
98
|
-
Deviations and details settled while implementing:
|
|
99
|
-
|
|
100
|
-
- `fromRemoteValue` maps `error` to an opaque `RemoteObject` (the protocol's `ErrorRemoteValue` carries no message); `internalId` back references are resolved, so cyclic remote objects come back as cyclic JS objects. Values cut off by the serialization depth also become `RemoteObject`s.
|
|
101
|
-
- With `ownership: "root"` results that carry a handle come back as `RemoteObject { type, handle, value }` (`value` = the deserialized copy), so the caller can `disown(handle)`. With `"none"` the plain value is returned.
|
|
102
|
-
- `toLocalValue` accepts only plain objects (prototype `Object.prototype` or `null`), arrays, `Map`, `Set`, `Date`, `RegExp`, primitives and `ElementHandle`; class instances, `Buffer`, functions and symbols throw `TypeError`.
|
|
103
|
-
- The actionability probe is one constant `PROBE_ELEMENT` (a function declaration string) called as `(xpath, options)`; both xpath and `{ scroll, hitTest }` are arguments. It returns a plain object, so `parseElementState` is a check over `unknown` (JSON round trip gone).
|
|
104
|
-
- `Locator` has `call` / `callOnElement`; other reads pass what they need (`getAttribute` passes the attribute name, `count` passes `count(<xpath>)`) as arguments too. A `ScriptError` becomes the existing `Failed to locate element <xpath>. Details: <text>` message.
|
|
105
|
-
- `getBoundingClientRect` now returns the object directly (was a JSON string).
|
|
106
|
-
- The stub connector answers `script.callFunction` like `script.evaluate`, records `calls` (declaration + decoded arguments) and exposes `remote()` to build object results. Tests that looked at expression text for `scrollIntoView` / `elementFromPoint` / `getAttribute("href")` now assert on the arguments instead; all other locator and expect tests are unchanged.
|
|
107
|
-
- Tests: `script/{serialize,call-function}.test.ts`, `script/call-function.browser-test.ts` (real Firefox, port 9233).
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# `input.setFiles` — design
|
|
2
|
-
|
|
3
|
-
Date: 2026-09-29
|
|
4
|
-
Status: implemented (see "Implementation notes")
|
|
5
|
-
Roadmap item: 4
|
|
6
|
-
Depends on: script-call-function, locate-nodes
|
|
7
|
-
|
|
8
|
-
## Goal
|
|
9
|
-
|
|
10
|
-
Real file upload for `<input type="file">`, which pointer/keyboard input cannot do.
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
await page.locator("input[@type='file']").setInputFiles("./fixtures/avatar.png");
|
|
14
|
-
await page.locator("input[@type='file']").setInputFiles(["a.csv", "b.csv"]);
|
|
15
|
-
await page.locator("input[@type='file']").setInputFiles([]); // clear
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Non-goals
|
|
19
|
-
|
|
20
|
-
- In-memory files (`{ name, mimeType, buffer }`); BiDi takes paths on the browser's machine only.
|
|
21
|
-
- Drag-and-drop uploads.
|
|
22
|
-
- The file chooser popup flow (downloads-and-file-dialogs spec).
|
|
23
|
-
|
|
24
|
-
## API
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
setInputFiles(files: string | string[], options?: { timeout?: number }): Promise<void>;
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Behaviour
|
|
31
|
-
|
|
32
|
-
1. Normalise `files` to an array; resolve each with `path.resolve` (relative to `process.cwd()`).
|
|
33
|
-
2. Before touching the browser, `fs.stat` every path: missing or a directory throws `Error("setInputFiles(): file not found: <path>")` (a local-runner assumption: browser and tests share a filesystem, true for the spawned Firefox; documented).
|
|
34
|
-
3. Wait (auto-wait mechanism, `waitUntil`) for checks: **attached** and **enabled**. Not *visible*: file inputs are commonly hidden behind a styled label, so visibility and hit-testing are skipped.
|
|
35
|
-
4. One `callFunction` reads `{ tag, type, multiple }`. Not `input[type=file]` → `ActionTimeoutError`-free immediate `Error("setInputFiles(): <selector> is not an <input type=file> (found <tag type=…>)")`. More than one file and `!multiple` → `Error("… does not accept multiple files")`.
|
|
36
|
-
5. `input.setFiles { context, element: SharedReference, files }`. The browser fires `input` and `change` events itself; no synthetic dispatch.
|
|
37
|
-
6. `[]` clears the selection and fires the events (browser behaviour; asserted in the browser test).
|
|
38
|
-
|
|
39
|
-
## Errors
|
|
40
|
-
|
|
41
|
-
| Situation | Error |
|
|
42
|
-
|---|---|
|
|
43
|
-
| Path missing | plain `Error`, before any BiDi call |
|
|
44
|
-
| Wrong element / multiple | plain `Error` |
|
|
45
|
-
| Never attached/enabled within timeout | `ActionTimeoutError` (`action: "setInputFiles"`, standard checks line) |
|
|
46
|
-
| Browser rejects (`no such element`, `invalid argument`) | `BiDiError` |
|
|
47
|
-
|
|
48
|
-
`ActionTimeoutError.action` union gains `"setInputFiles"`; `expect.timeout` config docs mention it.
|
|
49
|
-
|
|
50
|
-
## Testing
|
|
51
|
-
|
|
52
|
-
- Unit (stub connector): step order (stat → wait → type check → `setFiles`), `element` is the resolved `SharedReference`, absolute paths sent, multiple-files guard, wrong-element message, timeout error text, `[]` sends an empty list.
|
|
53
|
-
- Browser: fixture page with a file input and a listener writing `input.files[0].name` and event counts into the DOM; single upload, multiple upload, clear, hidden input behind a label, `multiple`-less input with two files errors.
|
|
54
|
-
- TDD: tests first.
|
|
55
|
-
|
|
56
|
-
## Implementation notes
|
|
57
|
-
|
|
58
|
-
- A directory gets its own message, `setInputFiles(): not a file: <path>` (the draft used "file not found" for both); a missing path is `setInputFiles(): file not found: <path>`. Both are checked for every path, in order, before any BiDi command is sent.
|
|
59
|
-
- Sequence: stat → wait (`attached`, `enabled`; no scroll, no hit test) → resolve the element again → one script reads `{ tag, type, multiple }` → `input.setFiles`. If the element vanished between the wait and the second search, the error is `Failed to locate element … no element matches`.
|
|
60
|
-
- `ActionTimeoutError.action` is a plain string, so `"setInputFiles"` needed no type change. The `expect.timeout` doc comment in `types/config.d.ts` was left alone.
|
|
61
|
-
- Verified in Firefox: the browser fires `input` and `change` exactly once per call, `[]` clears the selection, hidden inputs work, the content is readable by the page.
|
|
62
|
-
- Tests: `locator/set-files.test.ts`, `locator/set-files.browser-test.ts` (port 9236). The stub connector gained `input.setFiles` and `failCommand(method, error)`.
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# User prompts (`browsingContext.handleUserPrompt`) — design
|
|
2
|
-
|
|
3
|
-
Date: 2026-09-29
|
|
4
|
-
Status: implemented (see "Implementation notes")
|
|
5
|
-
Roadmap item: 7
|
|
6
|
-
Depends on: bidi-foundation
|
|
7
|
-
|
|
8
|
-
## Goal
|
|
9
|
-
|
|
10
|
-
`alert`, `confirm`, `prompt` and `beforeunload` dialogs must never hang a test. While a prompt is open the page's script commands can stall, which today would freeze an action until its timeout.
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
page.on("dialog", async (dialog) => {
|
|
14
|
-
console.log(dialog.type(), dialog.message());
|
|
15
|
-
await dialog.accept("Sam"); // prompt text; or dialog.dismiss()
|
|
16
|
-
});
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Non-goals
|
|
20
|
-
|
|
21
|
-
- Custom dialogs (HTML `<dialog>`), which are just DOM.
|
|
22
|
-
- Permission prompts and HTTP auth dialogs (interception spec handles `authRequired`).
|
|
23
|
-
|
|
24
|
-
## Components
|
|
25
|
-
|
|
26
|
-
### Session capability
|
|
27
|
-
|
|
28
|
-
`Browser.launch` sends `session.new { capabilities: { alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } } } }`. With `ignore` the browser leaves the prompt open, fires `browsingContext.userPromptOpened`, and waits for `handleUserPrompt`. That is the only setting that lets the framework decide per prompt. (Verified against Firefox in the browser test; if Firefox ignores the capability the test fails, and the spec falls back to per-user-context `createUserContext { unhandledPromptBehavior }`.)
|
|
29
|
-
|
|
30
|
-
### `src/browser/dialog.ts` (new)
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
class Dialog {
|
|
34
|
-
type(): "alert" | "confirm" | "prompt" | "beforeunload";
|
|
35
|
-
message(): string;
|
|
36
|
-
defaultValue(): string; // "" unless a prompt
|
|
37
|
-
accept(promptText?: string): Promise<void>; // handleUserPrompt { accept: true, userText }
|
|
38
|
-
dismiss(): Promise<void>; // handleUserPrompt { accept: false }
|
|
39
|
-
readonly closed: Promise<{ accepted: boolean; userText?: string }>; // resolves on userPromptClosed
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
`accept`/`dismiss` may be called once; a second call throws `Error("dialog already handled")`. `no such alert` from the browser (closed by navigation or another handler) is swallowed with a debug log because the outcome the caller wanted (dialog gone) is true.
|
|
44
|
-
|
|
45
|
-
### `Page` integration
|
|
46
|
-
|
|
47
|
-
- `page.on("dialog", handler)` / `page.off` / `page.once`. Subscribes (refcounted) to `userPromptOpened` and `userPromptClosed`, filtered by `ContextTree.isWithin`.
|
|
48
|
-
- Always subscribed from page creation (as a `Page` internal listener) so the default policy below works even with no user handler.
|
|
49
|
-
- Dispatch: on `userPromptOpened`, call every registered handler (awaited concurrently). After they settle, if the prompt is still open (`Dialog` not handled), apply the default policy.
|
|
50
|
-
- **Default policy (no listener, or listeners left it open):** `beforeunload` → accept (so navigation proceeds), everything else → dismiss. Log a `warn`: `Dialog "<message>" was dismissed automatically; add page.on("dialog") to handle it`. Handler exceptions are logged and also trigger the default policy so the test does not hang.
|
|
51
|
-
- While a prompt is open, `waitUntil` probes still race their deadline (auto-wait spec), so a stalled `callFunction` fails at its own timeout instead of hanging; the default policy normally closes the prompt within a tick, unblocking it.
|
|
52
|
-
|
|
53
|
-
## Errors
|
|
54
|
-
|
|
55
|
-
| Situation | Behaviour |
|
|
56
|
-
|---|---|
|
|
57
|
-
| Handler throws | logged; default policy applied; the test does not fail by itself |
|
|
58
|
-
| `accept()` after handled | `Error("dialog already handled")` |
|
|
59
|
-
| Prompt disappeared | swallowed (`no such alert`) |
|
|
60
|
-
|
|
61
|
-
## Testing
|
|
62
|
-
|
|
63
|
-
- Unit (stub connector): capability sent in `session.new`; `alert` with no listener dismissed + warning; `beforeunload` accepted; listener `accept("x")` sends `userText`; double-handle throws; handler throws → default policy; prompt from an iframe context handled, from an unrelated context ignored; `closed` resolves from `userPromptClosed`.
|
|
64
|
-
- Browser: page with buttons for `alert`, `confirm`, `prompt` and a `beforeunload` handler; `confirm` accepted returns `true` to the page, dismissed returns `false`; `prompt` text reaches the page; unhandled `alert` on click does not hang the click; navigating away from a `beforeunload` page completes.
|
|
65
|
-
- TDD: tests first.
|
|
66
|
-
|
|
67
|
-
## Implementation notes
|
|
68
|
-
|
|
69
|
-
- **Where:** `browser/dialog.ts` (`Dialog`), `Page.on/once/off("dialog")` (the event methods now cover `PageEventName`: network events and `dialog`), `Page.startDialogHandling()`, `SESSION_CAPABILITIES` exported from `browser/browser.ts` and sent with `session.new`. `Browser.launch` and `Browser.newPage` start dialog handling next to network tracking; `page.on("dialog")` starts it for a page built by hand.
|
|
70
|
-
- **Verified in Firefox 153:** `unhandledPromptBehavior: { default: "ignore" }` leaves the dialog open and the browser reports `userPromptOpened` (with `handler: "ignore"`) for `alert`, `confirm`, `prompt` and, after a user gesture, `beforeunload`, so the per-user-context fallback from the draft was not needed. The page stays blocked in `alert()` until the dialog is answered.
|
|
71
|
-
- **Answering:** always through the page's top-level context (`handleUserPrompt` needs it), also for a prompt an iframe opened. Prompts of other pages are ignored (`ContextTree`).
|
|
72
|
-
- **`Dialog`:** `type()`, `message()`, `defaultValue()` (empty unless a prompt), `accept(text?)`, `dismiss()`, `handled`, `closed`. One answer only, a second call throws `dialog already handled`; `no such alert` is swallowed, other errors reach the caller. When the browser has already answered (`handler` is not `ignore`) the dialog starts as handled and listeners are still told.
|
|
73
|
-
- **Default policy:** after all `dialog` listeners settled, a dialog still open is answered: `beforeunload` accepted, everything else dismissed, with a `warn` log `Dialog "<message>" was dismissed automatically; add page.on("dialog") to handle it`. A listener that throws is logged and the default applies. Listeners run concurrently and are awaited, so an async listener may answer late.
|
|
74
|
-
- **`once` listeners** are called through `EventEmitter.rawListeners()`: `listeners()` returns the unwrapped function and a `once` listener would never remove itself.
|
|
75
|
-
- Tests: `browser/dialog.test.ts`, `browser/dialog.browser-test.ts` (port 9239: alert holds the page, confirm true/false, prompt text and default, dismissed prompt is null, `closed`, an unhandled alert on a click does not hang, iframe alert, `beforeunload` accepted).
|