mcp-accessibility-scanner 3.4.0 → 3.5.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 +72 -6
- package/config.d.ts +18 -3
- package/lib/browserContextFactory.js +15 -313
- package/lib/browserContextFactory.js.map +1 -1
- package/lib/browserServerBackend.js +5 -0
- package/lib/browserServerBackend.js.map +1 -1
- package/lib/config.js +14 -2
- package/lib/config.js.map +1 -1
- package/lib/context.js +81 -1
- package/lib/context.js.map +1 -1
- package/lib/extension/cdpRelay.js +54 -8
- package/lib/extension/cdpRelay.js.map +1 -1
- package/lib/extension/extensionContextFactory.js +4 -2
- package/lib/extension/extensionContextFactory.js.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/program.js +23 -6
- package/lib/program.js.map +1 -1
- package/lib/response.js +8 -3
- package/lib/response.js.map +1 -1
- package/lib/tools/files.js +5 -2
- package/lib/tools/files.js.map +1 -1
- package/lib/tools/install.js +1 -1
- package/lib/tools/install.js.map +1 -1
- package/lib/tools/screenshot.js +3 -2
- package/lib/tools/screenshot.js.map +1 -1
- package/lib/tools/snapshot.js +5 -1
- package/lib/tools/snapshot.js.map +1 -1
- package/lib/tools.js +3 -1
- package/lib/tools.js.map +1 -1
- package/lib/vscode/browserContextFactory.js +5 -8
- package/lib/vscode/browserContextFactory.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -140,7 +140,9 @@ npx mcp-accessibility-scanner --extension
|
|
|
140
140
|
Set `PLAYWRIGHT_MCP_EXTENSION_TOKEN` to the token shown by the extension to bypass the connection approval dialog. The relay's CDP WebSocket endpoint always requires a separate random token, generated per relay and appended automatically for the server's own connection. This CDP token is never passed in Chrome's launch arguments or extension URL; the extension approval token cannot authenticate a CDP client.
|
|
141
141
|
Token-bypass connections are not background-safe: Chrome focuses the connection tab and window, and client-created tabs remain open after disconnect ([upstream limitation](https://github.com/microsoft/playwright/issues/42343)).
|
|
142
142
|
With a token, the extension must connect and finish setup within 30 seconds after the connection page opens. Failed attempts release the relay so the next tool call can retry. Without a token, manual approval waits until you approve or cancel the call.
|
|
143
|
-
When `--user-data-dir` contains multiple Chrome profiles, the profile with the extension installed is selected automatically, preferring Chrome's last-used profile.
|
|
143
|
+
When `--user-data-dir` contains multiple Chrome profiles, the profile with the extension installed is selected automatically, preferring Chrome's last-used profile. Pass `--profile-dir-name` (or set `PLAYWRIGHT_MCP_PROFILE_DIR_NAME`) to select a profile explicitly; it requires `--user-data-dir` and accepts a Chrome profile directory name such as `Default` or `Profile 1` (see "Profile Path" at `chrome://version`). The `PLAYWRIGHT_MCP_EXTENSION_TOKEN` approval token is specific to the profile, so when selecting a profile explicitly, use the token shown in that profile.
|
|
144
|
+
|
|
145
|
+
Packed extensions require an enabled record in the profile's preferences; a leftover extension directory alone does not count as installed. Profiles whose preferences mark the extension disabled or uninstalled are excluded from automatic selection and rejected for explicit selection. Explicit selection with a custom executable skips local installation checks. A whitespace-only `PLAYWRIGHT_MCP_PROFILE_DIR_NAME` is treated as unset; other string environment variables retain their existing blank-value handling.
|
|
144
146
|
|
|
145
147
|
### Discovering available tools (`list-tools` subcommand)
|
|
146
148
|
|
|
@@ -203,7 +205,8 @@ Create a `config.json` file with the following options:
|
|
|
203
205
|
"timeouts": {
|
|
204
206
|
"navigationTimeout": 60000,
|
|
205
207
|
"defaultTimeout": 5000,
|
|
206
|
-
"settle": 500
|
|
208
|
+
"settle": 500,
|
|
209
|
+
"idle": 0
|
|
207
210
|
},
|
|
208
211
|
"network": {
|
|
209
212
|
"allowedOrigins": ["example.com", "trusted-site.com"],
|
|
@@ -229,18 +232,23 @@ Create a `config.json` file with the following options:
|
|
|
229
232
|
- `browser.cdpTimeout`: Maximum time in milliseconds to wait when connecting to the CDP endpoint (default: `30000`)
|
|
230
233
|
- `browser.cdpLaunch`: Launch a Chromium-family desktop app with CDP enabled, wait for the endpoint, and manage the child process lifecycle
|
|
231
234
|
- CDP attach modes preserve the target browser's existing default-context settings instead of applying Playwright's defaults.
|
|
232
|
-
- `browser.contextOptions.storageState`: Start
|
|
235
|
+
- `browser.contextOptions.storageState`: Start a fresh context from a recorded Playwright storage state. Imports into existing CDP or VS Code contexts are rejected; use `--isolated` for CDP or sign in interactively. The default persistent mode uses a fresh disposable profile. See [Auditing pages behind a login](#auditing-pages-behind-a-login).
|
|
236
|
+
- `browser.profileDirName`: Chrome profile directory name used in extension mode, for example `Default` or `Profile 1` (CLI: `--profile-dir-name`, env: `PLAYWRIGHT_MCP_PROFILE_DIR_NAME`). Requires `--user-data-dir` and extension mode (`--extension` or `--connect-tool`); defaults to the last-used profile that has the extension installed.
|
|
233
237
|
- `timeouts.navigationTimeout`: Maximum time for page navigation in milliseconds (default: `60000`)
|
|
234
238
|
- `timeouts.defaultTimeout`: Default timeout for Playwright operations in milliseconds (default: `5000`)
|
|
235
239
|
- `timeouts.settle`: How long to wait after every action before responding (default: `500`). An action that finishes quietly is first watched for up to 100ms (or the settle delay, whichever is shorter) so scheduled network work can still be awaited before the settle delay.
|
|
240
|
+
- `timeouts.idle`: Release the default browser context after this many idle milliseconds (default: `0`, disabled). Accepts integers from `0` to `2147483647`.
|
|
236
241
|
- `network.allowedOrigins`: List of origins to allow (blocks all others if specified)
|
|
237
242
|
- `network.blockedOrigins`: List of origins to block
|
|
238
243
|
- `snapshot.boxes`: Include each element's viewport-relative bounding box as `[box=x,y,width,height]` in snapshots (default: `false`; CLI: `--snapshot-boxes`, env: `PLAYWRIGHT_MCP_SNAPSHOT_BOXES=1`)
|
|
244
|
+
- `imageResponses`: `allow` (default) returns text and images; `omit` excludes images; `only` omits text from successful responses containing images. Errors, browser lifecycle notices, and responses without images (including full-page screenshots) keep their text. Structured results and resource links are always preserved. In `only` mode, screenshot save-path text, generated code and any accompanying text-only findings are omitted; use `allow` if you need them. Interactive mode rejects `only` because its REPL prints text only; use `allow` or `omit` there. `auto` remains a legacy alias for `allow`. CLI: `--image-responses only`; env: `PLAYWRIGHT_MCP_IMAGE_RESPONSES=only`. Precedence: CLI, then environment, then config file.
|
|
239
245
|
- `server.authToken`: When set, Streamable HTTP requests (`--port`) require `Authorization: Bearer <token>` or return `401` (env: `PLAYWRIGHT_MCP_AUTH_TOKEN`). Blank or malformed tokens fail at startup. The scheme is case-insensitive; the token is exact. Bearer auth does not encrypt traffic: authenticated listeners must bind to loopback, such as `--host 127.0.0.1`; use a TLS reverse proxy for remote access. The printed client config includes a header placeholder to replace locally, without logging the secret. Unset keeps unauthenticated access.
|
|
240
246
|
- `outputDir`: Directory for output files — reports, screenshots, traces, and session logs (CLI: `--output-dir`, env: `PLAYWRIGHT_MCP_OUTPUT_DIR`). Defaults to a fresh directory under the system temp folder, resolved once per server run so all of a run's artifacts land together. The output location is always server configuration; the deprecated MCP roots capability (client workspace folders) is no longer consulted.
|
|
241
247
|
|
|
242
248
|
CLI equivalents are also available: `--cdp-launch-command`, `--cdp-launch-args`, `--cdp-launch-cwd`, `--cdp-launch-port`, `--cdp-launch-startup-timeout`, `--cdp-endpoint`, `--cdp-header` (repeat for multiple headers, e.g. `--cdp-header "Authorization: Bearer <token>"`), and `--cdp-timeout`. The CDP headers and timeout can also be set via the `PLAYWRIGHT_MCP_CDP_HEADERS` (one `Name: Value` entry per line) and `PLAYWRIGHT_MCP_CDP_TIMEOUT` environment variables.
|
|
243
249
|
|
|
250
|
+
If CDP attachment times out after the WebSocket connects, an existing sleeping or unresponsive tab may be blocking Playwright's browser initialization ([upstream report](https://github.com/microsoft/playwright/issues/42730)). Use an explicit positive `--cdp-timeout` to bound the attempt. Inspect or wake the affected tabs yourself, or attach to a separate disposable browser. `noDefaults` and `--isolated` do not skip initialization of existing tabs; the server does not close your tabs or bypass Playwright's initialization to work around this.
|
|
251
|
+
|
|
244
252
|
For remote HTTP access, configure the TLS reverse proxy explicitly. For example, with the MCP server bound using `--host 127.0.0.1 --port 8931` and `PLAYWRIGHT_MCP_AUTH_TOKEN` set:
|
|
245
253
|
|
|
246
254
|
- Accept only your configured public hostname over HTTPS and forward `/mcp` to `http://127.0.0.1:8931/mcp`.
|
|
@@ -254,6 +262,8 @@ Caller-supplied screenshot, PDF, scan-page-matrix, and audit report filenames us
|
|
|
254
262
|
|
|
255
263
|
Use `--timeout-settle` or `PLAYWRIGHT_MCP_TIMEOUT_SETTLE` to override the post-action settle delay. It applies after every action so delayed DOM-only updates are included in the response; a short observation window also catches scheduled requests and waits for them before that delay.
|
|
256
264
|
|
|
265
|
+
Use `--timeout-idle 300000`, `timeouts.idle`, or `PLAYWRIGHT_MCP_TIMEOUT_IDLE` to release the default browser after five idle minutes. Shared contexts stay open while any client is working; the idle window starts after the last tool call or download finishes. Explicit recordings prevent idle release until `browser_stop_recording` finishes; passive `--save-session` capture does not. Cleanup finalizes traces. The next browser tool call reopens the connection and includes a note to navigate again and refresh element references. Attached CDP, extension, and VS Code browsers are disconnected; their external pages remain open. Close and session-management tools do not relaunch an idle browser. Explicit `browser_session_open` handles keep their separate `PLAYWRIGHT_MCP_BROWSER_SESSION_TTL_MS` behavior. Zero disables this feature; blank environment values leave the existing configuration unchanged.
|
|
266
|
+
|
|
257
267
|
The VS Code `browser_connect` tool accepts only `playwright` or `playwright-core` libraries and loopback WebSocket URLs. Set `PLAYWRIGHT_MCP_VSCODE_ALLOW_REMOTE=1` to allow remote endpoints, which must use `wss:`. URL userinfo credentials are rejected.
|
|
258
268
|
|
|
259
269
|
#### HTTP Heartbeat
|
|
@@ -314,10 +324,14 @@ PLAYWRIGHT_MCP_ISOLATED=true PLAYWRIGHT_MCP_STORAGE_STATE=./auth.json npx mcp-ac
|
|
|
314
324
|
|
|
315
325
|
> **Every supported mode handles the state — by applying it or refusing it.**
|
|
316
326
|
>
|
|
327
|
+
> **Playwright 1.63.0 safety restriction:** importing into an existing context is rejected before taking a rollback snapshot or resetting any storage. On this pin, snapshot capture can execute service-worker-served scripts for a previously visited origin whose tab is no longer open ([upstream fix](https://github.com/microsoft/playwright/pull/42664)). Use a fresh context, or omit `--storage-state` and sign in interactively. Service workers are not disabled. A future dependency upgrade must also pass the recorder/shared-client checks in [#218](https://github.com/JustasMonkev/mcp-accessibility-scanner/issues/218) and IndexedDB checks in [#224](https://github.com/JustasMonkev/mcp-accessibility-scanner/issues/224) before this restriction is reconsidered.
|
|
328
|
+
>
|
|
329
|
+
> **IndexedDB snapshot limitation:** on pinned Playwright 1.63.0 with Chromium 153.0.8010.12 and Firefox 155.0, `storageState({ indexedDB: true })` loses `Map` and `Set` contents. Both `newContext({ storageState })` and `setStorageState()` restore them as empty plain objects; ordinary JSON records survive. Fresh contexts protect existing browser data, but cannot recover values already lost during capture. The [upstream fix](https://github.com/microsoft/playwright/pull/42707) is merged but is not in this pin. Before allowing imports into existing contexts again, verify both restore paths preserve Map/Set types and entries on each supported engine, including after a failed import. The real-browser regression in `tests/browser-failures.integration.test.ts` checks that rejecting an import leaves the original Map/Set records intact and that isolated JSON IndexedDB imports still work.
|
|
330
|
+
>
|
|
317
331
|
> - **Fresh-context modes** (`--isolated`, the remote-endpoint mode, or either CDP mode combined with `--isolated`): the context is created with the storage state directly.
|
|
318
332
|
> - **Default persistent-profile mode with `--storage-state`**: the session runs in a fresh, disposable profile — unique to that session and removed when it closes — built from the state, so the recorded state is provably the only session data (without `--storage-state` the regular persistent profile is used and survives restarts, as before). Any page the launch opened (for example from a URL in `browser.launchOptions.args`) is parked on a blank replacement before the state lands, then the replacement is navigated to the same URL, so a still-running anonymous page cannot overwrite the recorded identity and a scan never reads its DOM. This also means `--storage-state` cannot be combined with `--user-data-dir` (a user-supplied profile carries its own session and will not be wiped; the server refuses the combination).
|
|
319
|
-
> - **CDP modes without `--isolated
|
|
320
|
-
> - **`--extension`** (with or without `--isolated`)
|
|
333
|
+
> - **CDP modes without `--isolated` and the VS Code provider**: `--storage-state` is rejected when the browser already has a context. Add `--isolated` in CDP mode to create a fresh context; otherwise omit the state and sign in interactively. If the browser exposes no context, the server creates one with the state. CDP sessions joining that same server-created context inherit its live state without resetting it.
|
|
334
|
+
> - **`--extension`** (with or without `--isolated`) refuses storage imports entirely: it works through the browser you are already running, where wiping cookies to install a recorded state is not an acceptable side effect, so the server refuses to start rather than doing that silently. There, sign in interactively instead — the persistent profile also keeps the session across restarts.
|
|
321
335
|
|
|
322
336
|
### Keep the crawl from destroying its own session
|
|
323
337
|
|
|
@@ -337,6 +351,8 @@ The check compares which cookies the crawled URLs carry, not their values, so a
|
|
|
337
351
|
|
|
338
352
|
## Available Tools
|
|
339
353
|
|
|
354
|
+
Page-registered WebMCP tools are not currently exposed. See the [WebMCP adoption decision](https://github.com/JustasMonkev/mcp-accessibility-scanner/blob/main/docs/decisions/001-webmcp-adoption.md) for the deferral and conditions for revisiting an opt-in capability.
|
|
355
|
+
|
|
340
356
|
The MCP server provides comprehensive browser automation and accessibility scanning tools:
|
|
341
357
|
|
|
342
358
|
### Core Accessibility Tool
|
|
@@ -355,7 +371,7 @@ Performs a comprehensive accessibility scan on the current page using Axe-core.
|
|
|
355
371
|
**Annotated screenshots:**
|
|
356
372
|
When `annotateScreenshot` is `true`, each violating element is outlined and labelled with the rule ids it failed, a full-page PNG is written to the MCP output directory (`scan-page-annotated-{timestamp}-{token}.png`) and returned as a resource link, and the markers are then removed so the page is left exactly as it was. The markers are drawn in an out-of-flow overlay clipped to each element's own box, so they never reflow the page. The overlay uses a fresh id per scan, is placed in the browser's top layer so it stays visible over an open dialog, popover or fullscreen element, and compensates for a CSS `zoom` or a scaled ancestor so markers line up with what is rendered.
|
|
357
373
|
An element that fails several rules gets one box listing every rule id, and elements inside open shadow roots are marked by walking the shadow path Axe reports.
|
|
358
|
-
Running animations are
|
|
374
|
+
Running animations are frozen at their current time before the elements are measured and resumed after the capture, so a moving target keeps its marker. The markers themselves live in a shadow root under an overlay whose own styles are `!important`, so page CSS cannot restyle or hide what the report counts, and each rule label sits outside the clipped box so it stays readable on an element smaller than its own label.
|
|
359
375
|
At most 50 elements are annotated per scan. The result text always reports how many nodes were marked out of the total, plus how many were left out because they exceeded the limit, were hidden, zero-size or off-canvas (a full-page screenshot is clipped to the document box), or were inside an iframe (cross-frame selectors cannot be resolved from the top document).
|
|
360
376
|
|
|
361
377
|
**Supported Violation Tags:**
|
|
@@ -588,6 +604,7 @@ Evaluate a JavaScript expression on the page, or on a specific element when a `r
|
|
|
588
604
|
Take a screenshot of the current page.
|
|
589
605
|
- Parameters: `filename` (optional), `type` (`png`, `jpeg`, or `webp`), `scale` (`css` or `device`, default `css`), `fullPage` (optional), `element`/`ref` pair (for element screenshots)
|
|
590
606
|
- `scale: device` captures a high-resolution screenshot using device pixels (accounts for the device pixel ratio); `scale: css` keeps the image sized in CSS pixels.
|
|
607
|
+
- An empty capture is an error, and its output file is removed, including automatically named files. The requested format is never silently changed. If a WebP capture is empty, reduce its dimensions or explicitly request PNG/JPEG.
|
|
591
608
|
|
|
592
609
|
#### `browser_pdf_save`
|
|
593
610
|
Save page as PDF.
|
|
@@ -599,6 +616,12 @@ This tool requires `--caps pdf` in the CLI.
|
|
|
599
616
|
Install the configured browser engine (use when browser executable is missing).
|
|
600
617
|
- Parameters: none
|
|
601
618
|
|
|
619
|
+
Disabled by default. Enable it at server startup with `--caps install`, `PLAYWRIGHT_MCP_CAPS=install`, or `"capabilities": ["install"]` in the config file. Explicit `core-install` settings remain supported as a deprecated alias; use `install` in new configurations. Without this opt-in, the tool is neither listed nor callable; existing browser installations can still be used.
|
|
620
|
+
|
|
621
|
+
This tool invokes Playwright's installer, which downloads executable code. In [Playwright 1.63.0](https://github.com/microsoft/playwright/blob/v1.63.0/packages/playwright-core/src/server/registry/oopDownloadBrowserMain.ts), browser archives have no checksum or signature verification before extraction; the default download hosts use HTTPS. Only enable installation when you trust the download source and TLS configuration, including any custom `PLAYWRIGHT_DOWNLOAD_HOST`, browser-specific host overrides, or TLS-inspecting proxy. Do not disable TLS certificate validation.
|
|
622
|
+
|
|
623
|
+
For deployments that require independently verified binaries, provision the browser through your trusted deployment process and use `--executable-path` or an existing browser connection. The capability opt-in limits MCP-triggered installation; it does not add archive verification or change manual, CI, or Docker build downloads.
|
|
624
|
+
|
|
602
625
|
### Browser Management
|
|
603
626
|
|
|
604
627
|
#### `browser_close`
|
|
@@ -675,6 +698,7 @@ Handle browser dialogs (alerts, confirms, prompts).
|
|
|
675
698
|
#### `browser_file_upload`
|
|
676
699
|
Upload files to the page.
|
|
677
700
|
- Parameters: `paths` (array of absolute file paths)
|
|
701
|
+
- If `setFiles` fails, the chooser stays available for another upload attempt; `paths: []` clears the selection and completes the chooser. A successful upload clears only that chooser and waits for page activity and the configured settle delay.
|
|
678
702
|
|
|
679
703
|
#### `browser_verify_element_visible`
|
|
680
704
|
Verify an element by ARIA role/name.
|
|
@@ -771,6 +795,48 @@ cd mcp-accessibility-scanner
|
|
|
771
795
|
npm install
|
|
772
796
|
```
|
|
773
797
|
|
|
798
|
+
### Playwright upgrade gate
|
|
799
|
+
|
|
800
|
+
The September 16, 2026 review keeps `playwright` and `playwright-core` paired at
|
|
801
|
+
**1.63.0**, the [latest stable release](https://github.com/microsoft/playwright/releases/tag/v1.63.0)
|
|
802
|
+
on that date. Keep the local `InputRecorder` hub and the existing factory reference
|
|
803
|
+
counts: multiple MCP clients share one client-side browser context, while the hub
|
|
804
|
+
multiplexes session logs and explicit recordings and excludes sibling tool actions.
|
|
805
|
+
A dependency bump alone must not change that ownership model.
|
|
806
|
+
|
|
807
|
+
Before adopting a stable release containing [upstream #42627](https://github.com/microsoft/playwright/pull/42627),
|
|
808
|
+
adapt the hub from `_enableRecorder` / `_disableRecorder` to
|
|
809
|
+
`_startRecording({ language: 'javascript' }, sink)` / `_stopRecording()` and verify
|
|
810
|
+
the per-client event contract against the installed runtime. Do not ship a
|
|
811
|
+
prerelease bump or an untested method-name fallback. Migrating to the separate
|
|
812
|
+
connections in [#42622](https://github.com/microsoft/playwright/pull/42622) is a
|
|
813
|
+
separate ownership change requiring the same lifecycle checks.
|
|
814
|
+
|
|
815
|
+
Install the pinned Chromium browser, then run the real recorder gate alongside
|
|
816
|
+
its failure and concurrency tests:
|
|
817
|
+
|
|
818
|
+
```bash
|
|
819
|
+
npx playwright install chromium
|
|
820
|
+
npx vitest run tests/recorder.integration.test.ts tests/context.test.ts tests/browserSessions.test.ts tests/browserContextFactory.test.ts tests/tools-recorder.test.ts tests/sessionLog.test.ts
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
The real-browser tests cover concurrent starts, duplicate-start rejection,
|
|
824
|
+
shared CDP clients, sibling-action attribution, stop/disconnect/restart,
|
|
825
|
+
`--save-session`, and recording across stateless explicit sessions. Existing unit
|
|
826
|
+
tests also cover failed-start recovery and overlapping start/stop. The recorder
|
|
827
|
+
must receive the final input event before stop; unbuffered input delivered after
|
|
828
|
+
stop begins is excluded, while buffered clicks/navigation get a 500 ms drain.
|
|
829
|
+
|
|
830
|
+
Also recheck the dependency fixes motivating the upgrade. On 1.63.0 Chromium,
|
|
831
|
+
both full-page and oversized element screenshots changed `navigator.maxTouchPoints`
|
|
832
|
+
from 1 to 0 and `(pointer: coarse)` from true to false; navigation restored the
|
|
833
|
+
properties ([#42617](https://github.com/microsoft/playwright/pull/42617)).
|
|
834
|
+
The fixed-header/smooth-scroll retry fixture clicked successfully but emitted 19
|
|
835
|
+
scroll events rather than instant jumps ([#42626](https://github.com/microsoft/playwright/pull/42626)).
|
|
836
|
+
A candidate upgrade must preserve touch properties after full-page and element
|
|
837
|
+
screenshots and navigation, and complete retry scrolling without smooth animation.
|
|
838
|
+
These are dependency limitations; the recorder gate alone does not verify them.
|
|
839
|
+
|
|
774
840
|
### MCP harnesses
|
|
775
841
|
|
|
776
842
|
The npm wrappers build first, then the direct harness calls every exposed MCP
|
package/config.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type ToolCapability =
|
|
|
25
25
|
| 'files'
|
|
26
26
|
| 'install'
|
|
27
27
|
| 'testing'
|
|
28
|
-
| 'core-install'
|
|
28
|
+
| 'core-install' // Deprecated alias for 'install'; requires explicit opt-in.
|
|
29
29
|
| 'core-tabs'
|
|
30
30
|
| 'devtools'
|
|
31
31
|
| 'vision'
|
|
@@ -57,6 +57,13 @@ export type Config = {
|
|
|
57
57
|
*/
|
|
58
58
|
userDataDir?: string;
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Chrome profile directory name used in extension mode (for example
|
|
62
|
+
* "Default" or "Profile 1"); defaults to the last-used profile that
|
|
63
|
+
* has the extension installed.
|
|
64
|
+
*/
|
|
65
|
+
profileDirName?: string;
|
|
66
|
+
|
|
60
67
|
/**
|
|
61
68
|
* Launch options passed to
|
|
62
69
|
* @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context
|
|
@@ -185,9 +192,11 @@ export type Config = {
|
|
|
185
192
|
};
|
|
186
193
|
|
|
187
194
|
/**
|
|
188
|
-
*
|
|
195
|
+
* Image response policy. Defaults to "allow"; "auto" is a legacy alias for "allow".
|
|
196
|
+
* "omit" excludes images. "only" omits text from successful responses containing images,
|
|
197
|
+
* but preserves errors, browser lifecycle notices, structured content and resource links. Responses without images keep text.
|
|
189
198
|
*/
|
|
190
|
-
imageResponses?: 'allow' | 'omit' | 'auto';
|
|
199
|
+
imageResponses?: 'allow' | 'omit' | 'auto' | 'only';
|
|
191
200
|
|
|
192
201
|
snapshot?: {
|
|
193
202
|
/**
|
|
@@ -215,5 +224,11 @@ export type Config = {
|
|
|
215
224
|
* How long to wait after each action for triggered work to settle before responding. Defaults to 500ms.
|
|
216
225
|
*/
|
|
217
226
|
settle?: number;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Release the default browser context after this many idle milliseconds. Zero (the default) disables it.
|
|
230
|
+
* Explicit browser sessions retain their separate idle TTL.
|
|
231
|
+
*/
|
|
232
|
+
idle?: number;
|
|
218
233
|
};
|
|
219
234
|
};
|
|
@@ -25,17 +25,7 @@ import { logUnhandledError, testDebug } from './utils/log.js';
|
|
|
25
25
|
import { createGuid, createHash, createShortGuid } from './utils/guid.js';
|
|
26
26
|
import { outputFile } from './config.js';
|
|
27
27
|
import { ensureNetworkPolicyRoutes } from './networkPolicy.js';
|
|
28
|
-
/**
|
|
29
|
-
* Throws when a storage state is configured but `factory` will not apply it. A
|
|
30
|
-
* factory that neither creates a fresh context with the state nor applies it to
|
|
31
|
-
* the context it reuses would drop it without a word and audit the site as an
|
|
32
|
-
* anonymous user, which looks exactly like a successful run. Every factory in
|
|
33
|
-
* this file applies it one way or the other; the extension factory cannot — it
|
|
34
|
-
* works through the user's own running browser, where clearing every origin's
|
|
35
|
-
* cookies to install the recorded state is not an acceptable side effect.
|
|
36
|
-
* Callers pass the remedy that fits the mode they selected — the factory that
|
|
37
|
-
* creates the context is not always the one `contextFactory()` built.
|
|
38
|
-
*/
|
|
28
|
+
/** Rejects factories that would silently ignore a configured storage state. */
|
|
39
29
|
export function assertStorageStateSupported(config, factory, remedy) {
|
|
40
30
|
if (config.browser.contextOptions?.storageState && !factory.appliesStorageState)
|
|
41
31
|
throw new Error(`Storage state cannot be applied in this mode. ${remedy}`);
|
|
@@ -54,142 +44,10 @@ export function assertStorageStateDoesNotResetUserProfile(config, remedy) {
|
|
|
54
44
|
}
|
|
55
45
|
export function contextFactory(config) {
|
|
56
46
|
const factory = createContextFactory(config);
|
|
57
|
-
//
|
|
58
|
-
// future factory that forgets to declare support rejects the option instead
|
|
59
|
-
// of silently dropping it.
|
|
47
|
+
// Factories must apply the state to a fresh context or reject unsafe reuse.
|
|
60
48
|
assertStorageStateSupported(config, factory, 'Drop the storage state and sign in interactively before auditing.');
|
|
61
49
|
return factory;
|
|
62
50
|
}
|
|
63
|
-
// The rules addCookies enforces client-side (verified against Playwright
|
|
64
|
-
// 1.61.1: empty or missing domain/path without a url, a url combined with a
|
|
65
|
-
// domain or a path, about:blank/data:/unparseable urls, an expires other
|
|
66
|
-
// than -1 or a positive number up to Playwright's ceiling, and sameSite
|
|
67
|
-
// outside Strict/Lax/None are all rejected there — non-http(s) url schemes
|
|
68
|
-
// and malformed origin strings fail browser-side during the apply). Failing
|
|
69
|
-
// them here keeps the failure ahead of the cache clear; anything these
|
|
70
|
-
// checks miss still fails inside setStorageState.
|
|
71
|
-
const cookieUrlProblem = (url) => {
|
|
72
|
-
if (typeof url !== 'string')
|
|
73
|
-
return 'is not a string';
|
|
74
|
-
if (url === 'about:blank')
|
|
75
|
-
return 'cannot be about:blank';
|
|
76
|
-
if (url.startsWith('data:'))
|
|
77
|
-
return 'cannot be a data: URL';
|
|
78
|
-
try {
|
|
79
|
-
const parsed = new URL(url);
|
|
80
|
-
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
81
|
-
return `must be an http(s) URL, not ${parsed.protocol}`;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return 'is not a valid absolute URL';
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
87
|
-
};
|
|
88
|
-
const isValidIndexedDBKey = (value) => typeof value === 'string'
|
|
89
|
-
|| (typeof value === 'number' && Number.isFinite(value))
|
|
90
|
-
|| (value instanceof Date && Number.isFinite(value.getTime()))
|
|
91
|
-
|| value instanceof ArrayBuffer
|
|
92
|
-
|| ArrayBuffer.isView(value)
|
|
93
|
-
|| (Array.isArray(value) && value.every(isValidIndexedDBKey));
|
|
94
|
-
const indexedDBIdentifier = /^[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*$/u;
|
|
95
|
-
const isValidIndexedDBKeyPath = (value) => typeof value === 'string' && (value === '' || value.split('.').every(part => indexedDBIdentifier.test(part)));
|
|
96
|
-
const isValidIndexedDBKeyPathArray = (value) => Array.isArray(value) && !!value.length && value.every(isValidIndexedDBKeyPath);
|
|
97
|
-
function assertValidStorageState(state) {
|
|
98
|
-
for (const value of state.cookies ?? []) {
|
|
99
|
-
const cookie = value;
|
|
100
|
-
const problem = !cookie || typeof cookie !== 'object'
|
|
101
|
-
? 'a cookie entry is not an object'
|
|
102
|
-
: !cookie.url && (!cookie.domain || !cookie.path)
|
|
103
|
-
? `cookie "${String(cookie.name ?? '')}" should have a url or a domain/path pair`
|
|
104
|
-
: cookie.url && cookie.domain
|
|
105
|
-
? `cookie "${String(cookie.name ?? '')}" should have either a url or a domain, not both`
|
|
106
|
-
: cookie.url && cookie.path
|
|
107
|
-
? `cookie "${String(cookie.name ?? '')}" should have either a url or a path, not both`
|
|
108
|
-
: cookie.url !== undefined && cookieUrlProblem(cookie.url)
|
|
109
|
-
? `cookie "${String(cookie.name ?? '')}" has a url that ${cookieUrlProblem(cookie.url)}`
|
|
110
|
-
: cookie.expires !== undefined && (typeof cookie.expires !== 'number' || Number.isNaN(cookie.expires) || (cookie.expires !== -1 && (cookie.expires <= 0 || cookie.expires > 253402300799)))
|
|
111
|
-
? `cookie "${String(cookie.name ?? '')}" should have a valid expires — only -1 or a positive unix timestamp in seconds up to 253402300799 (9999-12-31T23:59:59Z, Playwright's own ceiling) is allowed`
|
|
112
|
-
: cookie.sameSite !== undefined && !['Strict', 'Lax', 'None'].includes(cookie.sameSite)
|
|
113
|
-
? `cookie "${String(cookie.name ?? '')}" has sameSite "${String(cookie.sameSite)}", expected one of Strict|Lax|None`
|
|
114
|
-
: null;
|
|
115
|
-
if (problem)
|
|
116
|
-
throw new Error(`Invalid storage state: ${problem}. Nothing was changed — the state is validated before the apply, because setStorageState() clears the attached context's HTTP cache and cookie jar before it validates, and the cache cannot be restored.`);
|
|
117
|
-
}
|
|
118
|
-
for (const value of state.origins ?? []) {
|
|
119
|
-
const entry = value;
|
|
120
|
-
// Restoring an origin's storage navigates Playwright's temporary page to
|
|
121
|
-
// it — a malformed or non-http(s) origin fails that navigation after the
|
|
122
|
-
// clear.
|
|
123
|
-
const problem = !entry || typeof entry !== 'object'
|
|
124
|
-
? 'an origins entry is not an object'
|
|
125
|
-
: typeof entry.origin !== 'string' || cookieUrlProblem(entry.origin)
|
|
126
|
-
? `origins entry "${String(entry?.origin ?? '')}" is not an absolute http(s) URL`
|
|
127
|
-
: null;
|
|
128
|
-
if (problem)
|
|
129
|
-
throw new Error(`Invalid storage state: ${problem}. Nothing was changed — the state is validated before the apply, because setStorageState() clears the attached context's HTTP cache and cookie jar before it validates, and the cache cannot be restored.`);
|
|
130
|
-
const databaseNames = new Set();
|
|
131
|
-
for (const value of Array.isArray(entry?.indexedDB) ? entry.indexedDB : []) {
|
|
132
|
-
const database = value;
|
|
133
|
-
const stores = Array.isArray(database.stores) ? database.stores : [];
|
|
134
|
-
let indexedDBProblem = !Number.isSafeInteger(database.version) || database.version <= 0
|
|
135
|
-
? `IndexedDB database "${String(database.name ?? '')}" should have a positive integer version`
|
|
136
|
-
: databaseNames.has(String(database.name))
|
|
137
|
-
? `IndexedDB database name "${String(database.name)}" is duplicated`
|
|
138
|
-
: null;
|
|
139
|
-
databaseNames.add(String(database.name));
|
|
140
|
-
const storeNames = new Set();
|
|
141
|
-
for (const store of stores) {
|
|
142
|
-
const storeName = String(store.name);
|
|
143
|
-
indexedDBProblem ??= storeNames.has(storeName)
|
|
144
|
-
? `IndexedDB object store name "${storeName}" is duplicated in database "${String(database.name)}"`
|
|
145
|
-
: store.keyPath !== undefined && !isValidIndexedDBKeyPath(store.keyPath)
|
|
146
|
-
? `IndexedDB object store "${storeName}" has an invalid key path`
|
|
147
|
-
: store.keyPathArray !== undefined && !isValidIndexedDBKeyPathArray(store.keyPathArray)
|
|
148
|
-
? `IndexedDB object store "${storeName}" has an invalid array key path`
|
|
149
|
-
: store.autoIncrement && (store.keyPath === '' || Array.isArray(store.keyPathArray))
|
|
150
|
-
? `IndexedDB object store "${storeName}" cannot combine autoIncrement with an empty or array key path`
|
|
151
|
-
: null;
|
|
152
|
-
storeNames.add(storeName);
|
|
153
|
-
const recordKeys = new Set();
|
|
154
|
-
const hasInlineKey = store.keyPath !== undefined || store.keyPathArray !== undefined;
|
|
155
|
-
for (const value of Array.isArray(store.records) ? store.records : []) {
|
|
156
|
-
const record = value;
|
|
157
|
-
const hasExternalKey = (record.key !== undefined && record.key !== null)
|
|
158
|
-
|| (record.keyEncoded !== undefined && record.keyEncoded !== null);
|
|
159
|
-
const key = record.key ?? record.keyEncoded;
|
|
160
|
-
const serializedKey = hasExternalKey ? JSON.stringify(key) ?? String(key) : '';
|
|
161
|
-
indexedDBProblem ??= record.key !== undefined && record.key !== null && !isValidIndexedDBKey(record.key)
|
|
162
|
-
? `IndexedDB object store "${storeName}" has an invalid external record key`
|
|
163
|
-
: hasInlineKey && hasExternalKey
|
|
164
|
-
? `IndexedDB object store "${storeName}" has an inline key path but record also supplies an external key`
|
|
165
|
-
: !hasInlineKey && !store.autoIncrement && !hasExternalKey
|
|
166
|
-
? `IndexedDB object store "${storeName}" requires an external key for every record`
|
|
167
|
-
: hasExternalKey && recordKeys.has(serializedKey)
|
|
168
|
-
? `IndexedDB object store "${storeName}" has duplicate record key ${serializedKey}`
|
|
169
|
-
: null;
|
|
170
|
-
if (hasExternalKey)
|
|
171
|
-
recordKeys.add(serializedKey);
|
|
172
|
-
}
|
|
173
|
-
const indexNames = new Set();
|
|
174
|
-
for (const index of Array.isArray(store.indexes) ? store.indexes : []) {
|
|
175
|
-
const indexName = String(index.name);
|
|
176
|
-
indexedDBProblem ??= indexNames.has(indexName)
|
|
177
|
-
? `IndexedDB index name "${indexName}" is duplicated in object store "${storeName}"`
|
|
178
|
-
: index.keyPath !== undefined && !isValidIndexedDBKeyPath(index.keyPath)
|
|
179
|
-
? `IndexedDB index "${indexName}" has an invalid key path`
|
|
180
|
-
: index.keyPathArray !== undefined && !isValidIndexedDBKeyPathArray(index.keyPathArray)
|
|
181
|
-
? `IndexedDB index "${indexName}" has an invalid array key path`
|
|
182
|
-
: index.multiEntry && Array.isArray(index.keyPathArray)
|
|
183
|
-
? `IndexedDB index "${indexName}" cannot combine multiEntry with an array key path`
|
|
184
|
-
: null;
|
|
185
|
-
indexNames.add(indexName);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (indexedDBProblem)
|
|
189
|
-
throw new Error(`Invalid storage state: ${indexedDBProblem}. Nothing was changed — the state is validated before the apply, because setStorageState() clears the attached context's HTTP cache and cookie jar before it validates, and the cache cannot be restored.`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
51
|
/**
|
|
194
52
|
* Replaces every open page of `browserContext` with a blank fresh tab and
|
|
195
53
|
* returns the fresh tabs paired with the URL each replaced page showed.
|
|
@@ -283,150 +141,10 @@ async function navigateReplacementPages(replaced) {
|
|
|
283
141
|
}
|
|
284
142
|
}));
|
|
285
143
|
}
|
|
286
|
-
/**
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
* caller asked for, applied to a context `newContext()` never sees: the CDP
|
|
291
|
-
* modes without --isolated reuse the browser's existing context, and
|
|
292
|
-
* launchPersistentContext() silently ignores a storageState option (verified
|
|
293
|
-
* against Playwright 1.61.1).
|
|
294
|
-
*/
|
|
295
|
-
export async function applyStorageStateToReusedContext(config, browserContext) {
|
|
296
|
-
const storageState = config.browser.contextOptions?.storageState;
|
|
297
|
-
if (!storageState)
|
|
298
|
-
return;
|
|
299
|
-
const parsedState = await (async () => {
|
|
300
|
-
try {
|
|
301
|
-
return typeof storageState === 'string'
|
|
302
|
-
? JSON.parse(await fs.promises.readFile(storageState, 'utf-8'))
|
|
303
|
-
: storageState;
|
|
304
|
-
}
|
|
305
|
-
catch (error) {
|
|
306
|
-
// Letting setStorageState() discover the bad file would fail inside
|
|
307
|
-
// the apply block, whose catch answers every failure with a rollback —
|
|
308
|
-
// and the rollback's own setStorageState() clears the attached
|
|
309
|
-
// context's HTTP cache. A config error that changed nothing must not
|
|
310
|
-
// cost the running application its cache.
|
|
311
|
-
throw new Error(`The storage state file could not be read or parsed: ${error instanceof Error ? error.message : String(error)}. Nothing was changed.`);
|
|
312
|
-
}
|
|
313
|
-
})();
|
|
314
|
-
// Playwright validates cookies only while installing them — after the
|
|
315
|
-
// attached context's HTTP cache and cookie jar are already cleared — so a
|
|
316
|
-
// semantically invalid cookie (bad expires, missing domain/path) would
|
|
317
|
-
// fail the apply with the cache unrestorably gone. Checked up front, with
|
|
318
|
-
// the same rules addCookies enforces (verified against 1.61.1).
|
|
319
|
-
assertValidStorageState(parsedState);
|
|
320
|
-
// setStorageState needs a temporary page whenever the state carries origins
|
|
321
|
-
// or the context has visited any — and by the time that page creation fails
|
|
322
|
-
// on a target without Target.createTarget, the HTTP cache is already cleared
|
|
323
|
-
// and cannot be put back. Probe the page creation first, so such targets are
|
|
324
|
-
// rejected before anything is mutated. When neither signal indicates a page
|
|
325
|
-
// will be needed (cookie-only state, no pages open), the probe is skipped so
|
|
326
|
-
// that case keeps working on those targets.
|
|
327
|
-
const stateHasOrigins = (parsedState.origins?.length ?? 0) > 0;
|
|
328
|
-
const hasLoadedPages = browserContext.pages().some(page => page.url() && page.url() !== 'about:blank');
|
|
329
|
-
if (stateHasOrigins || hasLoadedPages) {
|
|
330
|
-
try {
|
|
331
|
-
const probe = await browserContext.newPage();
|
|
332
|
-
await probe.close();
|
|
333
|
-
}
|
|
334
|
-
catch (error) {
|
|
335
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
336
|
-
if (message.includes('Target.createTarget'))
|
|
337
|
-
throw new Error(`The attached browser cannot open the temporary page Playwright needs to apply the storage state's origin data (Electron targets do not support Target.createTarget). Nothing was changed. Drop the storage state and sign in inside the app instead. Original error: ${message}`);
|
|
338
|
-
throw error;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
// setStorageState replaces the cookie jar and then rewrites origin storage
|
|
342
|
-
// one origin at a time, so a failure partway would otherwise leave the
|
|
343
|
-
// attached browser holding a mixture of old and recorded state while the
|
|
344
|
-
// operation reports failure. Both layers are snapshotted first: the cookie
|
|
345
|
-
// jar through pure protocol calls that work everywhere (kept as the
|
|
346
|
-
// fallback for a restore whose own origin phase fails), and origin storage
|
|
347
|
-
// through storageState() below.
|
|
348
|
-
const originalCookies = await browserContext.cookies();
|
|
349
|
-
// The snapshot doubles as a probe for origins this connection has already
|
|
350
|
-
// visited while its pages have since closed or gone blank: for those,
|
|
351
|
-
// storageState() opens the same temporary page the forward apply will need
|
|
352
|
-
// — the newPage probe above cannot see them — but unlike setStorageState()
|
|
353
|
-
// it mutates nothing, so a target that cannot create pages (Electron) is
|
|
354
|
-
// rejected here with everything intact, not after the forward apply has
|
|
355
|
-
// cleared the HTTP cache. Any other snapshot failure also aborts: without
|
|
356
|
-
// the full snapshot, a partial forward apply could only be rolled back to
|
|
357
|
-
// cookies, leaving the attached browser's origin storage part old, part
|
|
358
|
-
// recorded.
|
|
359
|
-
const originalState = await browserContext.storageState({ indexedDB: true }).catch((error) => {
|
|
360
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
361
|
-
if (message.includes('Target.createTarget'))
|
|
362
|
-
throw new Error(`The attached browser cannot open the temporary page Playwright needs to reset origin storage for origins this connection has already visited (Electron targets do not support Target.createTarget). Nothing was changed. Drop the storage state and sign in inside the app instead. Original error: ${message}`);
|
|
363
|
-
throw new Error(`Snapshotting the context's current storage for rollback failed, so the storage state was not applied — a partial apply could not have been undone. Nothing was changed. Retry, or use --isolated for a fresh context. Original error: ${message}`);
|
|
364
|
-
});
|
|
365
|
-
// Pages that were already open still render the previous identity — and
|
|
366
|
-
// their scripts keep running: a page that periodically persists
|
|
367
|
-
// authentication into cookies or localStorage would overwrite the state
|
|
368
|
-
// being installed if it were still alive during setStorageState(), and
|
|
369
|
-
// replacing its tab afterwards cannot undo writes already made into
|
|
370
|
-
// context-wide storage. Every open page is therefore replaced with a blank
|
|
371
|
-
// fresh tab FIRST — the old document closes before the state lands, and
|
|
372
|
-
// only blank replacements (which run no scripts) survive the apply — and
|
|
373
|
-
// the replacements are navigated to the pages they replaced only once the
|
|
374
|
-
// recorded state is in place.
|
|
375
|
-
const replaced = await replaceOpenPagesWithBlankTabs(browserContext);
|
|
376
|
-
const policyRequired = !!(config.network?.allowedOrigins?.length || config.network?.blockedOrigins?.length);
|
|
377
|
-
let replacementNavigationSafe = !policyRequired;
|
|
378
|
-
try {
|
|
379
|
-
// The state validated above is the state applied: handing the path back
|
|
380
|
-
// to Playwright would re-read the file here, and a file replaced since
|
|
381
|
-
// that read would skip the cookie/origin validation and the page-creation
|
|
382
|
-
// probe only to fail after the cache clear those exist to prevent.
|
|
383
|
-
await browserContext.setStorageState(parsedState);
|
|
384
|
-
// The replacement navigations run inside the factory, before Context
|
|
385
|
-
// ensures the configured origin allowlist/blocklist — and the recorded
|
|
386
|
-
// credentials are already in place by now. The policy is installed here
|
|
387
|
-
// (permanently — page scripts can queue requests that fire after the
|
|
388
|
-
// navigation settles, so removing the handlers before Context re-ensures
|
|
389
|
-
// the same policy would open a window to a blocked origin;
|
|
390
|
-
// ensureNetworkPolicyRoutes installs once per context, so Context's later
|
|
391
|
-
// call is a no-op). Installed after setStorageState() so an abort-all
|
|
392
|
-
// route cannot interfere with the temporary page Playwright drives to
|
|
393
|
-
// restore origin storage.
|
|
394
|
-
await ensureNetworkPolicyRoutes(config, browserContext);
|
|
395
|
-
replacementNavigationSafe = true;
|
|
396
|
-
await navigateReplacementPages(replaced);
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
// Prefer the full-state rollback; fall back to cookies-only when its
|
|
400
|
-
// reapplication is itself impossible on this target.
|
|
401
|
-
const restoredFully = await browserContext.setStorageState(originalState).then(() => true, () => false);
|
|
402
|
-
const restoredCookies = restoredFully || await browserContext.clearCookies()
|
|
403
|
-
.then(() => originalCookies.length ? browserContext.addCookies(originalCookies) : undefined)
|
|
404
|
-
.then(() => true, () => false);
|
|
405
|
-
// The old pages were closed before the apply and cannot be handed back.
|
|
406
|
-
// Navigate their replacements only when no policy was required or its
|
|
407
|
-
// installation succeeded; otherwise restored credentials stay offline.
|
|
408
|
-
if (replacementNavigationSafe)
|
|
409
|
-
await navigateReplacementPages(replaced);
|
|
410
|
-
else
|
|
411
|
-
await Promise.all(replaced.map(({ page }) => page.close().catch(() => { })));
|
|
412
|
-
// Restoring origin storage (localStorage/IndexedDB) makes Playwright open a
|
|
413
|
-
// temporary page; a CDP target that cannot create one — Electron has no
|
|
414
|
-
// Target.createTarget — fails here. Cookie-only states need no page and
|
|
415
|
-
// still work on such targets, so name that remedy instead of surfacing the
|
|
416
|
-
// raw protocol error.
|
|
417
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
418
|
-
if (message.includes('Target.createTarget')) {
|
|
419
|
-
const rollbackNote = restoredFully
|
|
420
|
-
? 'The context\'s original storage state was restored.'
|
|
421
|
-
: restoredCookies
|
|
422
|
-
? 'The context\'s original cookies were restored.'
|
|
423
|
-
: 'Restoring the context\'s original cookies also failed; its cookie jar may now hold the recorded state.';
|
|
424
|
-
throw new Error(`The attached browser cannot open the temporary page Playwright needs to reset origin storage (Electron targets do not support Target.createTarget). Drop the storage state and sign in inside the app instead — a cookies-only state helps only while the attached target has no pages open and the connection has visited no origin, because clearing storage for an already-visited origin needs the same temporary page. ${rollbackNote} Original error: ${message}`);
|
|
425
|
-
}
|
|
426
|
-
if (!restoredFully && restoredCookies)
|
|
427
|
-
throw new Error(`${message} The context's original cookies were restored, but origin storage may retain partially applied state.`);
|
|
428
|
-
throw error;
|
|
429
|
-
}
|
|
144
|
+
/** Existing contexts can contain service workers and storage we cannot safely snapshot. */
|
|
145
|
+
export function assertReusedContextStorageStateSupported(config) {
|
|
146
|
+
if (config.browser.contextOptions?.storageState)
|
|
147
|
+
throw new Error('Cannot apply --storage-state to an existing browser context on Playwright 1.63.0: its rollback snapshot can run service-worker-served scripts and alter storage. Use a fresh context (for CDP, add --isolated), or omit --storage-state and sign in interactively. No storage snapshot or reset was attempted.');
|
|
430
148
|
}
|
|
431
149
|
function createContextFactory(config) {
|
|
432
150
|
if (config.browser.remoteEndpoint)
|
|
@@ -615,17 +333,10 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|
|
615
333
|
}
|
|
616
334
|
}
|
|
617
335
|
class CdpContextFactory extends BaseContextFactory {
|
|
618
|
-
//
|
|
619
|
-
// applies it to the browser's existing context via setStorageState().
|
|
336
|
+
// Fresh contexts accept storage state; existing external contexts reject it.
|
|
620
337
|
appliesStorageState = true;
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
// second session would wipe the first session's live cookies and origin
|
|
624
|
-
// storage mid-audit and reload its pages, so the state is applied once per
|
|
625
|
-
// context object: later sessions join the live shared state. Keyed weakly —
|
|
626
|
-
// a reconnect yields a fresh context object, so the slate resets with the
|
|
627
|
-
// connection — and a failed apply is forgotten so the next session retries.
|
|
628
|
-
_storageStateApplied = new WeakMap();
|
|
338
|
+
// Contexts created by this factory already received the configured state.
|
|
339
|
+
_createdContexts = new WeakSet();
|
|
629
340
|
// Serializes the no-context fallback the same way: two sessions arriving at
|
|
630
341
|
// a contextless target must share one created context, not race two.
|
|
631
342
|
_fallbackContext = new WeakMap();
|
|
@@ -725,15 +436,13 @@ class CdpContextFactory extends BaseContextFactory {
|
|
|
725
436
|
const existing = browser.contexts()[0];
|
|
726
437
|
// An attached browser can expose no context at all; a fresh one created
|
|
727
438
|
// with the configured options (storage state included) beats handing an
|
|
728
|
-
// undefined context to the caller.
|
|
729
|
-
//
|
|
730
|
-
// existing context, and must join it rather than reset it — and the
|
|
731
|
-
// creation itself is memoized so concurrent arrivals share one context.
|
|
439
|
+
// undefined context to the caller. Track ownership so later sessions may
|
|
440
|
+
// join that context; memoize creation so concurrent arrivals share it.
|
|
732
441
|
if (!existing) {
|
|
733
442
|
let creating = this._fallbackContext.get(browser);
|
|
734
443
|
if (!creating) {
|
|
735
444
|
creating = browser.newContext(this.config.browser.contextOptions).then(created => {
|
|
736
|
-
this.
|
|
445
|
+
this._createdContexts.add(created);
|
|
737
446
|
// Evict on close, or a context closed externally (while the
|
|
738
447
|
// connection lives on) would keep being handed out of this memo to
|
|
739
448
|
// every later session — contexts() no longer lists it, so only the
|
|
@@ -746,15 +455,8 @@ class CdpContextFactory extends BaseContextFactory {
|
|
|
746
455
|
}
|
|
747
456
|
return await creating;
|
|
748
457
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
let applied = this._storageStateApplied.get(existing);
|
|
752
|
-
if (!applied) {
|
|
753
|
-
applied = applyStorageStateToReusedContext(this.config, existing);
|
|
754
|
-
this._storageStateApplied.set(existing, applied);
|
|
755
|
-
applied.catch(() => this._storageStateApplied.delete(existing));
|
|
756
|
-
}
|
|
757
|
-
await applied;
|
|
458
|
+
if (!this._createdContexts.has(existing))
|
|
459
|
+
assertReusedContextStorageStateSupported(this.config);
|
|
758
460
|
return existing;
|
|
759
461
|
}
|
|
760
462
|
}
|
|
@@ -877,7 +579,7 @@ class CdpLaunchContextFactory {
|
|
|
877
579
|
else {
|
|
878
580
|
const existing = browser.contexts()[0];
|
|
879
581
|
if (existing) {
|
|
880
|
-
|
|
582
|
+
assertReusedContextStorageStateSupported(this.config);
|
|
881
583
|
browserContext = existing;
|
|
882
584
|
}
|
|
883
585
|
else {
|