patchright-cli 0.10.0__tar.gz → 0.11.1__tar.gz
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.
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.claude-plugin/marketplace.json +1 -1
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.github/workflows/ci.yml +2 -2
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/CLAUDE.md +4 -3
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/CONTRIBUTING.md +1 -1
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/PKG-INFO +38 -9
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/README.md +35 -4
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/docs/daemon-protocol.md +35 -8
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/docs/installation.md +1 -1
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/pyproject.toml +6 -6
- patchright_cli-0.11.1/scripts/ref_sweep/classify.py +207 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/common.js +9 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/controls.html +46 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/delayed.html +18 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/dialog.html +12 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/iframe.html +9 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/iframe_child.html +9 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/large.html +161 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/nav.html +31 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/nav2.html +10 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/nested.html +21 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/offscreen.html +10 -0
- patchright_cli-0.11.1/scripts/ref_sweep/fixtures/shadow.html +17 -0
- patchright_cli-0.11.1/scripts/ref_sweep/results/after.json +3920 -0
- patchright_cli-0.11.1/scripts/ref_sweep/results/before.json +3950 -0
- patchright_cli-0.11.1/scripts/ref_sweep/results/diff-report.md +187 -0
- patchright_cli-0.11.1/scripts/ref_sweep/results/diff.txt +5247 -0
- patchright_cli-0.11.1/scripts/ref_sweep/sweep.py +627 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/SKILL.md +88 -19
- patchright_cli-0.11.1/skills/patchright-cli/references/snapshot-refs.md +155 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/storage-state.md +1 -1
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/__init__.py +1 -1
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/cli.py +382 -32
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/daemon.py +1689 -387
- patchright_cli-0.11.1/src/patchright_cli/debugger.py +240 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/mcp_server.py +287 -16
- patchright_cli-0.11.1/src/patchright_cli/snapshot.py +67 -0
- patchright_cli-0.11.1/src/patchright_cli/snapshot_text.py +535 -0
- patchright_cli-0.11.1/src/patchright_cli/values.py +316 -0
- patchright_cli-0.11.1/tests/detect_fixture.html +39 -0
- patchright_cli-0.11.1/tests/ref_helpers.py +24 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_agent_hooks.py +2 -4
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_annotate.py +1 -3
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_attach.py +1 -0
- patchright_cli-0.11.1/tests/test_cli_audit.py +280 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_cli_axi.py +33 -2
- patchright_cli-0.11.1/tests/test_daemon_audit.py +352 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_axi.py +65 -35
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_handlers.py +632 -137
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_token.py +3 -1
- patchright_cli-0.11.1/tests/test_debugger.py +389 -0
- patchright_cli-0.11.1/tests/test_e2e.py +940 -0
- patchright_cli-0.11.1/tests/test_external_close.py +338 -0
- patchright_cli-0.11.1/tests/test_i41_extras.py +494 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_mcp.py +82 -2
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_navigation_policy.py +6 -9
- patchright_cli-0.11.1/tests/test_passkeys.py +304 -0
- patchright_cli-0.11.1/tests/test_pick_locator.py +368 -0
- patchright_cli-0.11.1/tests/test_snapshot_refs.py +236 -0
- patchright_cli-0.11.1/tests/test_snapshot_text.py +340 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_tabs.py +19 -16
- patchright_cli-0.11.1/tests/test_webmcp.py +266 -0
- patchright_cli-0.11.1/tests/webmcp_fixture.html +21 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/uv.lock +29 -846
- patchright_cli-0.10.0/skills/patchright-cli/references/snapshot-refs.md +0 -142
- patchright_cli-0.10.0/src/patchright_cli/ref_registry.py +0 -337
- patchright_cli-0.10.0/src/patchright_cli/snapshot.py +0 -48
- patchright_cli-0.10.0/tests/test_e2e.py +0 -238
- patchright_cli-0.10.0/tests/test_ref_registry.py +0 -634
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.claude-plugin/skills/browser-automation.json +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.github/dependabot.yml +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.github/workflows/publish-docker.yml +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.github/workflows/publish.yml +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.gitignore +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.pre-commit-config.yaml +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/Dockerfile +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/GLOSSARY.md +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/LICENSE +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/Makefile +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/compose.yml +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/docs/mcp-architecture.svg +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/patchright-cli +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/patchright-cli.cmd +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/scripts/gen_skill.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/request-mocking.md +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/running-code.md +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/session-management.md +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/tracing.md +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/video-recording.md +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/__main__.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/_entry.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/agent_hooks.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/daemon_pid.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/dashboard.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/toon.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/__init__.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/fixture.html +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_cli_config.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_cli_home.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_info.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_pid.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_read.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_docker.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_emulation.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_network_guard.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_packaging.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_profiles.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_skill_sync.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_supply_chain.py +0 -0
- {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_toon.py +0 -0
|
@@ -22,10 +22,10 @@ jobs:
|
|
|
22
22
|
strategy:
|
|
23
23
|
matrix:
|
|
24
24
|
os: [ubuntu-latest, windows-latest]
|
|
25
|
-
python-version: ["3.
|
|
25
|
+
python-version: ["3.12", "3.13"]
|
|
26
26
|
include:
|
|
27
27
|
- os: macos-latest
|
|
28
|
-
python-version: "3.
|
|
28
|
+
python-version: "3.13"
|
|
29
29
|
runs-on: ${{ matrix.os }}
|
|
30
30
|
steps:
|
|
31
31
|
- uses: actions/checkout@v7
|
|
@@ -50,11 +50,11 @@ Three-component client-daemon-browser design:
|
|
|
50
50
|
CLI (cli.py) --TCP:9321--> Daemon (daemon.py) --Patchright--> Chrome
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
- **CLI (`src/patchright_cli/cli.py`)**: Stateless thin client. Parses argv manually (not click subcommands), builds a JSON `{command, args, options, cwd}` message, sends it over a length-prefixed TCP socket to the daemon, prints the response, and exits. All commands are defined in the `COMMAND_SPECS` dict (usage, summary, flags, examples; `COMMANDS_HELP` and `ALL_COMMANDS` derive from it) and grouped for the manual in `COMMAND_CATEGORIES`. With no arguments it prints the home view (`_print_home`): live daemon state from an already-running daemon only, never starting one. `--toon` renders a reply's `data` field with `toon.py`.
|
|
53
|
+
- **CLI (`src/patchright_cli/cli.py`)**: Stateless thin client. Parses argv manually (not click subcommands), builds a JSON `{command, args, options, cwd}` message, sends it over a length-prefixed TCP socket to the daemon, prints the response, and exits. All commands are defined in the `COMMAND_SPECS` dict (usage, summary, flags, examples; `COMMANDS_HELP` and `ALL_COMMANDS` derive from it) and grouped for the manual in `COMMAND_CATEGORIES`. With no arguments it prints the home view (`_print_home`): live daemon state from an already-running daemon only, never starting one. `--toon` renders a reply's `data` field with `toon.py`. A failed reply, whatever the daemon's wording, prints through `_failure_lines()`: `error: <message>`, detail lines, then `help: <next step>` lines (exit 1, or 2 when the reply carries `exit_code: 2`); `--json` adds `error`/`help` fields.
|
|
54
54
|
|
|
55
55
|
- **Daemon (`src/patchright_cli/daemon.py`)**: Long-running async process managing browser sessions. Auto-spawned on `open` via `ensure_daemon_running()` which launches a subprocess. Uses `DaemonState` → multiple `Session` objects, each owning a Patchright persistent browser context. Command dispatch uses `@register()` decorated handlers in `COMMAND_HANDLERS` dict, dispatched by `handle_command()`. Console capture uses CDP sessions (not Playwright's `page.on('console')`) because Patchright suppresses it.
|
|
56
56
|
|
|
57
|
-
- **Snapshot (`src/patchright_cli/snapshot.py`)**:
|
|
57
|
+
- **Snapshot (`src/patchright_cli/snapshot.py`, `snapshot_text.py`)**: One full-page `page.aria_snapshot(mode="ai")` per tab (kept as `Tab.last_raw`) gives Playwright's own refs (`e5`, `f3e5` in iframes and after navigation); `page.get_by_ref` resolves them. Any other `aria_snapshot()` call replaces the page's ref map, so `snapshot.py` is the only caller (a unit test greps for it) and scoping, `--depth`, `-i` and `find` are text operations in `snapshot_text.py`. `scripts/ref_sweep/` is the before/after sweep of every ref-taking command; rerun it on a patchright bump. Snapshots are saved as YAML to `.patchright-cli/` in the caller's cwd.
|
|
58
58
|
|
|
59
59
|
## Key Design Decisions
|
|
60
60
|
|
|
@@ -63,7 +63,8 @@ CLI (cli.py) --TCP:9321--> Daemon (daemon.py) --Patchright--> Chrome
|
|
|
63
63
|
- Default daemon port: 9321, profiles stored at `~/.patchright-cli/profiles/<profile-name>`; a session's profile defaults to its session name (`--profile=<name>` overrides, a value with `/` or `\` is a directory). One session per profile at a time
|
|
64
64
|
- CLI-daemon protocol: length-prefixed (4-byte big-endian `!I`) JSON over TCP; every message carries a `token` field read from `~/.patchright-cli/daemon-<port>.token` (written 0600 by the daemon before it listens), and the daemon rejects messages without the right one
|
|
65
65
|
- CLI parses argv manually in `main()` — click is only used for `click.echo()`, not subcommands. New commands need a `COMMAND_SPECS` entry, a place in `COMMAND_CATEGORIES`, and a `@register("<name>")` handler in daemon.py, then `python scripts/gen_skill.py` to refresh the generated command block in `skills/patchright-cli/SKILL.md` (`--check` exits 1 when stale; `tests/test_skill_sync.py` enforces it); only session and lifecycle commands (open, attach, close, kill-all, ...) live in `handle_command()` itself
|
|
66
|
+
- Flag and positional value shapes (numbers, choices, flags that need a value) live in `src/patchright_cli/values.py` (`FLAG_KINDS`, `ARG_KINDS`); the CLI checks them before contacting the daemon (exit 2) and the daemon checks them again for clients that skip the CLI. A new numeric or enum flag needs an entry there (`tests/test_cli_audit.py` enforces it)
|
|
66
67
|
- Version must be updated in `pyproject.toml`, `src/patchright_cli/__init__.py` and `.claude-plugin/marketplace.json` (then run `uv lock` to sync the lock file; `tests/test_packaging.py` checks they match)
|
|
67
68
|
- `python -m patchright_cli` works via `__main__.py`
|
|
68
|
-
- Uses hatchling as build backend, ruff for linting (line-length=120, target
|
|
69
|
+
- Uses hatchling as build backend, ruff for linting (line-length=120, target py312)
|
|
69
70
|
- Ruff rules: E, F, I, W, UP, B, SIM (ignores E501, SIM105, E402)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: patchright-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Summary: Anti-detect browser automation CLI using Patchright (undetected Playwright fork)
|
|
5
5
|
Project-URL: Homepage, https://github.com/AhaiMk01/patchright-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/AhaiMk01/patchright-cli
|
|
@@ -14,16 +14,14 @@ Classifier: Environment :: Console
|
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
19
|
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
22
20
|
Classifier: Topic :: Software Development :: Testing
|
|
23
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.12
|
|
24
22
|
Requires-Dist: aiohttp>=3.9
|
|
25
23
|
Requires-Dist: click
|
|
26
|
-
Requires-Dist: patchright
|
|
24
|
+
Requires-Dist: patchright<1.65,>=1.64
|
|
27
25
|
Requires-Dist: pywin32>=306; sys_platform == 'win32'
|
|
28
26
|
Requires-Dist: pyyaml
|
|
29
27
|
Provides-Extra: dev
|
|
@@ -85,7 +83,7 @@ curl -s https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/docs/inst
|
|
|
85
83
|
</details>
|
|
86
84
|
|
|
87
85
|
> [!IMPORTANT]
|
|
88
|
-
> **Requirements:** Python 3.
|
|
86
|
+
> **Requirements:** Python 3.12+ and Google Chrome
|
|
89
87
|
|
|
90
88
|
```bash
|
|
91
89
|
# Recommended — always runs latest version, no install needed
|
|
@@ -235,7 +233,7 @@ graph LR
|
|
|
235
233
|
|-----------|------|
|
|
236
234
|
| **Daemon** (`daemon.py`) | Long-running process managing browser sessions via Patchright. Auto-starts on first `open`. |
|
|
237
235
|
| **CLI** (`cli.py`) | Thin client — connects, sends command, prints result, disconnects. Browser stays open. |
|
|
238
|
-
| **Snapshot** (`snapshot.py`) | Playwright `aria_snapshot()` accessibility tree,
|
|
236
|
+
| **Snapshot** (`snapshot.py`) | Playwright's AI-mode `aria_snapshot()` accessibility tree, iframes included, with Playwright's own `[ref=e5]` handles, resolved with `get_by_ref`. |
|
|
239
237
|
|
|
240
238
|
### Daemon token
|
|
241
239
|
|
|
@@ -295,6 +293,10 @@ patchright-cli find "text" # Search snapshot for text
|
|
|
295
293
|
patchright-cli find --regex "a|b" # Search by regex
|
|
296
294
|
patchright-cli find "text" --all # Include text/paragraph nodes
|
|
297
295
|
patchright-cli find "text" --limit=50 # Cap hits (default 20)
|
|
296
|
+
patchright-cli find --role=button --description="Deletes" # By role + accessible name/description
|
|
297
|
+
patchright-cli html # Page HTML (first 8000 chars; --max-chars=0 for all)
|
|
298
|
+
patchright-cli html --include-shadow --filename=page.html # With open shadow roots, to a file
|
|
299
|
+
patchright-cli html e5 # One element's outerHTML (or --selector=CSS)
|
|
298
300
|
patchright-cli eval <expr> # Run JavaScript
|
|
299
301
|
patchright-cli eval --file=script.js # Run JS from file
|
|
300
302
|
patchright-cli run-code <code> # Run JS with return value
|
|
@@ -400,6 +402,19 @@ patchright-cli sessionstorage-delete <key>
|
|
|
400
402
|
patchright-cli sessionstorage-clear
|
|
401
403
|
```
|
|
402
404
|
|
|
405
|
+
### Passkeys
|
|
406
|
+
A virtual WebAuthn authenticator for testing passkey sign-ups and logins. The first `passkey-*` command installs it in the session, overriding `navigator.credentials.create()` / `.get()` in every page (a page can notice the override, so it stays off until asked for). **Run a `passkey-*` command (e.g. `passkey-list`) before the page calls WebAuthn**: until then the page gets Chrome's native OS passkey dialog and hangs. Passkeys live in the session only; use `passkey-export` / `passkey-import` to keep them (the export holds private keys and is written owner-only; keep it out of version control).
|
|
407
|
+
```bash
|
|
408
|
+
patchright-cli passkey-list # Installs the authenticator, lists passkeys (no keys shown)
|
|
409
|
+
patchright-cli passkey-list --rp-id=example.com
|
|
410
|
+
patchright-cli passkey-add example.com # Seed a new P-256 passkey
|
|
411
|
+
patchright-cli passkey-add example.com --id=<b64url> --user-handle=<b64url> --private-key=<b64url> --public-key=<b64url> --sign-count=5
|
|
412
|
+
patchright-cli passkey-delete <credential-id>
|
|
413
|
+
patchright-cli passkey-clear [--rp-id=example.com]
|
|
414
|
+
patchright-cli passkey-export passkeys.json # Includes private keys
|
|
415
|
+
patchright-cli passkey-import passkeys.json
|
|
416
|
+
```
|
|
417
|
+
|
|
403
418
|
### Request Mocking
|
|
404
419
|
```bash
|
|
405
420
|
patchright-cli route "**/*.jpg" --status=404
|
|
@@ -416,7 +431,8 @@ patchright-cli unroute # Remove all routes
|
|
|
416
431
|
```bash
|
|
417
432
|
patchright-cli tracing-start
|
|
418
433
|
patchright-cli tracing-stop # Saves .zip trace file
|
|
419
|
-
patchright-cli video-start # Start video recording (CDP screencast)
|
|
434
|
+
patchright-cli video-start # Start real-time video recording at 25 fps (CDP screencast)
|
|
435
|
+
patchright-cli video-start --fps=60 # Another frame rate, 1-60
|
|
420
436
|
patchright-cli video-stop # Stop and save video (requires ffmpeg for .webm)
|
|
421
437
|
patchright-cli video-stop --filename=recording.webm
|
|
422
438
|
patchright-cli show --annotate # Ask a human to mark up the page, wait for it
|
|
@@ -436,11 +452,24 @@ patchright-cli network-state-set online # Restore connectivity
|
|
|
436
452
|
|
|
437
453
|
### DevTools
|
|
438
454
|
```bash
|
|
439
|
-
patchright-cli console #
|
|
455
|
+
patchright-cli console # Start capture / show captured console messages
|
|
440
456
|
patchright-cli console warning # Filter by level
|
|
441
457
|
patchright-cli console --clear # Clear after printing
|
|
442
458
|
```
|
|
443
459
|
|
|
460
|
+
The first `console` call enables CDP Runtime capture for every page in the session,
|
|
461
|
+
including tabs and popups opened later. Messages from before capture starts are
|
|
462
|
+
unavailable: run `console` before reproducing an issue, then call it again to read
|
|
463
|
+
the messages. Capture can make automation detectable and stays enabled until the
|
|
464
|
+
session closes; `--clear` only clears the message buffer. A new session starts
|
|
465
|
+
with capture disabled.
|
|
466
|
+
|
|
467
|
+
Two DevTools commands leave traces a page can see. `highlight` draws inside an
|
|
468
|
+
`<x-pw-glass>` element under `<html>`; `highlight --hide` removes it once no
|
|
469
|
+
highlight is left. `pick-locator` runs only with `--detectable`: it installs
|
|
470
|
+
Playwright's recorder, which puts `window.playwright`, `__pw_*` globals and an
|
|
471
|
+
`<x-pw-glass>` element into every page of the session until the session closes.
|
|
472
|
+
|
|
444
473
|
### Output
|
|
445
474
|
```bash
|
|
446
475
|
patchright-cli --raw eval --file=s.js # Raw output (no page/snapshot decoration)
|
|
@@ -48,7 +48,7 @@ curl -s https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/docs/inst
|
|
|
48
48
|
</details>
|
|
49
49
|
|
|
50
50
|
> [!IMPORTANT]
|
|
51
|
-
> **Requirements:** Python 3.
|
|
51
|
+
> **Requirements:** Python 3.12+ and Google Chrome
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
# Recommended — always runs latest version, no install needed
|
|
@@ -198,7 +198,7 @@ graph LR
|
|
|
198
198
|
|-----------|------|
|
|
199
199
|
| **Daemon** (`daemon.py`) | Long-running process managing browser sessions via Patchright. Auto-starts on first `open`. |
|
|
200
200
|
| **CLI** (`cli.py`) | Thin client — connects, sends command, prints result, disconnects. Browser stays open. |
|
|
201
|
-
| **Snapshot** (`snapshot.py`) | Playwright `aria_snapshot()` accessibility tree,
|
|
201
|
+
| **Snapshot** (`snapshot.py`) | Playwright's AI-mode `aria_snapshot()` accessibility tree, iframes included, with Playwright's own `[ref=e5]` handles, resolved with `get_by_ref`. |
|
|
202
202
|
|
|
203
203
|
### Daemon token
|
|
204
204
|
|
|
@@ -258,6 +258,10 @@ patchright-cli find "text" # Search snapshot for text
|
|
|
258
258
|
patchright-cli find --regex "a|b" # Search by regex
|
|
259
259
|
patchright-cli find "text" --all # Include text/paragraph nodes
|
|
260
260
|
patchright-cli find "text" --limit=50 # Cap hits (default 20)
|
|
261
|
+
patchright-cli find --role=button --description="Deletes" # By role + accessible name/description
|
|
262
|
+
patchright-cli html # Page HTML (first 8000 chars; --max-chars=0 for all)
|
|
263
|
+
patchright-cli html --include-shadow --filename=page.html # With open shadow roots, to a file
|
|
264
|
+
patchright-cli html e5 # One element's outerHTML (or --selector=CSS)
|
|
261
265
|
patchright-cli eval <expr> # Run JavaScript
|
|
262
266
|
patchright-cli eval --file=script.js # Run JS from file
|
|
263
267
|
patchright-cli run-code <code> # Run JS with return value
|
|
@@ -363,6 +367,19 @@ patchright-cli sessionstorage-delete <key>
|
|
|
363
367
|
patchright-cli sessionstorage-clear
|
|
364
368
|
```
|
|
365
369
|
|
|
370
|
+
### Passkeys
|
|
371
|
+
A virtual WebAuthn authenticator for testing passkey sign-ups and logins. The first `passkey-*` command installs it in the session, overriding `navigator.credentials.create()` / `.get()` in every page (a page can notice the override, so it stays off until asked for). **Run a `passkey-*` command (e.g. `passkey-list`) before the page calls WebAuthn**: until then the page gets Chrome's native OS passkey dialog and hangs. Passkeys live in the session only; use `passkey-export` / `passkey-import` to keep them (the export holds private keys and is written owner-only; keep it out of version control).
|
|
372
|
+
```bash
|
|
373
|
+
patchright-cli passkey-list # Installs the authenticator, lists passkeys (no keys shown)
|
|
374
|
+
patchright-cli passkey-list --rp-id=example.com
|
|
375
|
+
patchright-cli passkey-add example.com # Seed a new P-256 passkey
|
|
376
|
+
patchright-cli passkey-add example.com --id=<b64url> --user-handle=<b64url> --private-key=<b64url> --public-key=<b64url> --sign-count=5
|
|
377
|
+
patchright-cli passkey-delete <credential-id>
|
|
378
|
+
patchright-cli passkey-clear [--rp-id=example.com]
|
|
379
|
+
patchright-cli passkey-export passkeys.json # Includes private keys
|
|
380
|
+
patchright-cli passkey-import passkeys.json
|
|
381
|
+
```
|
|
382
|
+
|
|
366
383
|
### Request Mocking
|
|
367
384
|
```bash
|
|
368
385
|
patchright-cli route "**/*.jpg" --status=404
|
|
@@ -379,7 +396,8 @@ patchright-cli unroute # Remove all routes
|
|
|
379
396
|
```bash
|
|
380
397
|
patchright-cli tracing-start
|
|
381
398
|
patchright-cli tracing-stop # Saves .zip trace file
|
|
382
|
-
patchright-cli video-start # Start video recording (CDP screencast)
|
|
399
|
+
patchright-cli video-start # Start real-time video recording at 25 fps (CDP screencast)
|
|
400
|
+
patchright-cli video-start --fps=60 # Another frame rate, 1-60
|
|
383
401
|
patchright-cli video-stop # Stop and save video (requires ffmpeg for .webm)
|
|
384
402
|
patchright-cli video-stop --filename=recording.webm
|
|
385
403
|
patchright-cli show --annotate # Ask a human to mark up the page, wait for it
|
|
@@ -399,11 +417,24 @@ patchright-cli network-state-set online # Restore connectivity
|
|
|
399
417
|
|
|
400
418
|
### DevTools
|
|
401
419
|
```bash
|
|
402
|
-
patchright-cli console #
|
|
420
|
+
patchright-cli console # Start capture / show captured console messages
|
|
403
421
|
patchright-cli console warning # Filter by level
|
|
404
422
|
patchright-cli console --clear # Clear after printing
|
|
405
423
|
```
|
|
406
424
|
|
|
425
|
+
The first `console` call enables CDP Runtime capture for every page in the session,
|
|
426
|
+
including tabs and popups opened later. Messages from before capture starts are
|
|
427
|
+
unavailable: run `console` before reproducing an issue, then call it again to read
|
|
428
|
+
the messages. Capture can make automation detectable and stays enabled until the
|
|
429
|
+
session closes; `--clear` only clears the message buffer. A new session starts
|
|
430
|
+
with capture disabled.
|
|
431
|
+
|
|
432
|
+
Two DevTools commands leave traces a page can see. `highlight` draws inside an
|
|
433
|
+
`<x-pw-glass>` element under `<html>`; `highlight --hide` removes it once no
|
|
434
|
+
highlight is left. `pick-locator` runs only with `--detectable`: it installs
|
|
435
|
+
Playwright's recorder, which puts `window.playwright`, `__pw_*` globals and an
|
|
436
|
+
`<x-pw-glass>` element into every page of the session until the session closes.
|
|
437
|
+
|
|
407
438
|
### Output
|
|
408
439
|
```bash
|
|
409
440
|
patchright-cli --raw eval --file=s.js # Raw output (no page/snapshot decoration)
|
|
@@ -72,9 +72,29 @@ Every reply carries an explicit boolean `success` and a string `output`, on
|
|
|
72
72
|
both success and failure. `success: false` indicates authentication, command,
|
|
73
73
|
or daemon errors. `output` contains the result or a human-readable error.
|
|
74
74
|
Additional fields, such as `snapshot_path`, are command-specific. Clients must
|
|
75
|
-
ignore unknown fields.
|
|
75
|
+
ignore unknown fields.
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
### Error codes
|
|
78
|
+
|
|
79
|
+
A failed reply in one of these states carries a string `code`. Branch on it
|
|
80
|
+
rather than on `output`, whose wording can change in any release:
|
|
81
|
+
|
|
82
|
+
| `code` | State |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `session_not_open` | The requested session is absent. |
|
|
85
|
+
| `tab_not_open` | The requested named tab is absent from the session, including one whose page closed or crashed outside the daemon. `open --tab <name>` opens it again. |
|
|
86
|
+
| `target_closed` | The page, context or browser closed under the command. Also the default tab's page closed or crashed outside the daemon (a person, the page, the Host, or a renderer crash): the default tab does not move to another page on its own, so pick one with `tab-select <index>` or open one with `tab-new`. An `open` or `attach` that fails while starting a new session never carries it. A command whose own action closes its page (a popup's "Done" button) succeeds and says the page closed. |
|
|
87
|
+
| `no_page` | The session is open but has no pages left, whether they were closed with `tab-close`, closed outside the daemon, or crashed. The session stays open; `tab-new` opens a page. |
|
|
88
|
+
|
|
89
|
+
A page closed or crashed outside the daemon leaves the session as soon as Chrome reports it: `tab-list` stops listing it. A crashed page cannot be reloaded, since Patchright fails every later call on it, so the session closes it (in a Host Context, only a page the session opened). While the default tab has no page, `tab-close <index>` still works; `tab-close` without an index is refused rather than closing a neighbour.
|
|
90
|
+
|
|
91
|
+
Other failures carry no `code`; more codes can be added later, so treat an
|
|
92
|
+
unknown one like a missing one. Successful replies, including the no-ops below,
|
|
93
|
+
never carry `code`. Daemons before 0.11.1 send no codes under the same
|
|
94
|
+
`protocol`, so a client that may meet one keeps the output fragments below as
|
|
95
|
+
its fallback.
|
|
96
|
+
|
|
97
|
+
Clients can also detect these states in failed replies by matching these output
|
|
78
98
|
fragments. Surrounding context and recovery advice can vary. The fragments in
|
|
79
99
|
this table are part of the protocol:
|
|
80
100
|
|
|
@@ -120,21 +140,28 @@ fewer items than exist, `total` holds the full count.
|
|
|
120
140
|
| `profile-list` | `[{"name", "size", "session"}]`, where `size` is bytes and `session` is the holding session or `null` |
|
|
121
141
|
| `find` | `[{"ref", "role", "name"}]`, plus `total` when `--limit` cut the hits |
|
|
122
142
|
|
|
143
|
+
`console` opts the session into CDP Runtime capture on existing and future pages.
|
|
144
|
+
Earlier messages are unavailable; the first response explains this and the CDP
|
|
145
|
+
detection risk. Capture lasts until the session closes; `--clear` only clears the
|
|
146
|
+
buffer. If capture fails on a live page, the response has `success: false` and any
|
|
147
|
+
available messages; another `console` call retries pages that failed.
|
|
148
|
+
|
|
123
149
|
An empty list is `[]`, and its `output` states the zero explicitly, for example
|
|
124
150
|
`0 cookies in this session`. Commands not listed here send no `data`. More
|
|
125
151
|
fields or commands can gain `data` later, so read only the keys you need.
|
|
126
152
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
containing `has been closed
|
|
130
|
-
|
|
153
|
+
A command on a page, context or browser that has closed fails with
|
|
154
|
+
`code: "target_closed"`. Its `output` is Playwright's own message, usually
|
|
155
|
+
containing `has been closed`; that text can change with Playwright's version
|
|
156
|
+
without a protocol increment, so match the code instead.
|
|
131
157
|
|
|
132
158
|
## Compatibility
|
|
133
159
|
|
|
134
160
|
`protocol` is independent of the package version. Any incompatible change to
|
|
135
161
|
framing, request limits, request fields (`command`, `args`, `options`, `cwd`,
|
|
136
|
-
`token`), reply fields (`success`, `output`), authentication,
|
|
137
|
-
state-detection fragments in the
|
|
162
|
+
`token`), reply fields (`success`, `output`), authentication, the error
|
|
163
|
+
codes, or the state-detection fragments in the tables above requires a
|
|
164
|
+
protocol increment. Adding an error code does not.
|
|
138
165
|
Raising a request limit is not incompatible. Optional additive fields and new
|
|
139
166
|
commands do not require an increment. Clients can reject unsupported protocol
|
|
140
167
|
versions before sending requests.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "patchright-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.11.1"
|
|
4
4
|
description = "Anti-detect browser automation CLI using Patchright (undetected Playwright fork)"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "zpahai" },
|
|
@@ -15,15 +15,15 @@ classifiers = [
|
|
|
15
15
|
"Intended Audience :: Developers",
|
|
16
16
|
"License :: OSI Approved :: Apache Software License",
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
|
-
"Programming Language :: Python :: 3.10",
|
|
19
|
-
"Programming Language :: Python :: 3.11",
|
|
20
18
|
"Programming Language :: Python :: 3.12",
|
|
21
19
|
"Programming Language :: Python :: 3.13",
|
|
22
20
|
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
23
21
|
"Topic :: Software Development :: Testing",
|
|
24
22
|
]
|
|
25
23
|
dependencies = [
|
|
26
|
-
|
|
24
|
+
# Refs come from patchright 1.64's aria_snapshot(mode="ai") and get_by_ref; re-run
|
|
25
|
+
# scripts/ref_sweep before widening this.
|
|
26
|
+
"patchright>=1.64,<1.65",
|
|
27
27
|
"pyyaml",
|
|
28
28
|
"click",
|
|
29
29
|
"aiohttp>=3.9",
|
|
@@ -74,7 +74,7 @@ dev = [
|
|
|
74
74
|
]
|
|
75
75
|
|
|
76
76
|
[tool.ruff]
|
|
77
|
-
target-version = "
|
|
77
|
+
target-version = "py312"
|
|
78
78
|
line-length = 120
|
|
79
79
|
src = ["src"]
|
|
80
80
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Classify every step that differs between two sweep results; fail on any step no rule explains.
|
|
2
|
+
|
|
3
|
+
python scripts/ref_sweep/classify.py results/before.json results/after.json > results/diff-report.md
|
|
4
|
+
|
|
5
|
+
Each rule names the steps it covers and why the difference is intended. A step
|
|
6
|
+
the rules do not cover is printed as UNEXPLAINED and the exit code is 1.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
COMPARED = ("exit", "out", "state", "snapshot", "missing_targets")
|
|
17
|
+
|
|
18
|
+
# (category, reason, predicate over (step id, before, after))
|
|
19
|
+
RULES = [
|
|
20
|
+
(
|
|
21
|
+
"snapshot format",
|
|
22
|
+
"Native AI-mode YAML: Playwright's own refs printed verbatim (`f1e5` after a navigation), bare `generic` "
|
|
23
|
+
"wrappers hoisted, `[active]`/`[cursor=pointer]` tags, refs after a node's key instead of after its colon, "
|
|
24
|
+
"iframe content included, options and `text:` lines without refs. `--depth` now cuts lines (with `# +N refs`) "
|
|
25
|
+
"where it used to only hide tags; `-i` drops non-interactive lines where it used to only hide tags; scoped "
|
|
26
|
+
"snapshots show page refs instead of renumbering from e1.",
|
|
27
|
+
lambda sid, b, a: (
|
|
28
|
+
"snapshot" in b["argv"][:2]
|
|
29
|
+
and b["exit"] == a["exit"]
|
|
30
|
+
and b.get("missing_targets") == a.get("missing_targets")
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
(
|
|
34
|
+
"find output",
|
|
35
|
+
"Same search over the new YAML: refs are the page's refs (`f1e28`), ref-less hits show `[in=<ref>]`, and "
|
|
36
|
+
"iframe content is searchable. find --role maps matches through a depth-0 probe (same matches, native refs).",
|
|
37
|
+
lambda sid, b, a: b["argv"][0] == "find" and b["exit"] == a["exit"],
|
|
38
|
+
),
|
|
39
|
+
(
|
|
40
|
+
"--boxes no longer times out",
|
|
41
|
+
"Old --boxes resolved every ref with a 30 s bounding_box and timed out (controls page > 90 s); native "
|
|
42
|
+
"boxes=True takes ~0.2 s, iframe boxes shifted to page coordinates (iframe:snapshot-boxes f4e4 "
|
|
43
|
+
"[box=332,91,58,21] equals the page box measured in iframe:inner-submit-page-box).",
|
|
44
|
+
lambda sid, b, a: sid == "controls:snapshot-boxes" and b["exit"] == -1 and a["exit"] == 0,
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"iframe elements are actionable",
|
|
48
|
+
"The old snapshot had no iframe content, so no ref existed (`missing_targets`); native refs (`f4e3`) reach "
|
|
49
|
+
"into the same-origin iframe.",
|
|
50
|
+
lambda sid, b, a: (
|
|
51
|
+
sid.startswith("iframe:") and b.get("missing_targets") and not a.get("missing_targets") and a["exit"] == 0
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"refs outside a scoped view keep working",
|
|
56
|
+
"A scoped snapshot used to replace the registry with subtree-local refs, so a page ref then failed; every "
|
|
57
|
+
"view is now cut from one full snapshot.",
|
|
58
|
+
lambda sid, b, a: (
|
|
59
|
+
sid in ("controls:outside-ref-after-selector", "controls:outside-ref-after-ref-scope")
|
|
60
|
+
and b["exit"] == 1
|
|
61
|
+
and a["exit"] == 0
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
"stale ref refused instead of acting on another element",
|
|
66
|
+
"Old role/name/nth lookup silently hit a different element after a DOM change or navigation (state log shows "
|
|
67
|
+
"item-a2 / stay2 / rerender clicked); native refs fail fast with `no longer matches an element`, `is from a "
|
|
68
|
+
"page this tab has left`, or the tab guard.",
|
|
69
|
+
lambda sid, b, a: sid.startswith("nav:stale-") and a["exit"] == 1,
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
"new ref error wording",
|
|
73
|
+
"Unknown/stale ref errors name the cause: tab guard `is not from a snapshot of this tab; run snapshot (...)`, "
|
|
74
|
+
"`is from a page this tab has left`. Same exit code.",
|
|
75
|
+
lambda sid, b, a: (
|
|
76
|
+
b["exit"] == a["exit"] == 1
|
|
77
|
+
and re.search(r"not found|Re-run", b["out"])
|
|
78
|
+
and re.search(r"is not from a snapshot of this tab|is from a page this tab has left", a["out"])
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
(
|
|
82
|
+
"malformed ref is a usage error",
|
|
83
|
+
'`click nonsense` exits 2 with `"nonsense" is not a ref (refs look like e5 or f3e5, from snapshot)`.',
|
|
84
|
+
lambda sid, b, a: sid == "controls:bad-ref-word" and a["exit"] == 2,
|
|
85
|
+
),
|
|
86
|
+
(
|
|
87
|
+
"ref from the previous document refused",
|
|
88
|
+
"`@e1` named the first document's root; after the navigations the page's refs are `f1e*`, and the old code "
|
|
89
|
+
"resolved `e1` by role/name on the new page. The tab guard refuses it.",
|
|
90
|
+
lambda sid, b, a: sid == "controls:bad-ref-at" and b["exit"] == 0 and a["exit"] == 1,
|
|
91
|
+
),
|
|
92
|
+
(
|
|
93
|
+
"select names the mistake",
|
|
94
|
+
"A ref given as the option fails at once with the label hint instead of a 30 s select_option timeout; a "
|
|
95
|
+
"missing option's timeout lists the combobox's options.",
|
|
96
|
+
lambda sid, b, a: (
|
|
97
|
+
sid in ("controls:select-ref-as-option", "controls:select-missing-option") and b["exit"] == a["exit"]
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
(
|
|
101
|
+
"ref names in output",
|
|
102
|
+
"Same result, with the native ref in the message (`Highlighted f1e18.`, pick-locator `### Ref - f1e5`) or "
|
|
103
|
+
'the locator in Playwright\'s call log (`locator("aria-ref=f8e4")` instead of `get_by_role(...)`).',
|
|
104
|
+
lambda sid, b, a: (
|
|
105
|
+
b["exit"] == a["exit"]
|
|
106
|
+
and (re.search(r"\b(f\d+)?e\d+\b", a["out"]) or "aria-ref=" in a["out"])
|
|
107
|
+
and a.get("state") == b.get("state")
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
(
|
|
111
|
+
"pick-locator now finds the ref",
|
|
112
|
+
"Old matching only tried role-based picks (an id-based `#save2` pick got no ref) and capped candidates; the "
|
|
113
|
+
"depth-0 probe maps any picked element, including inside the iframe.",
|
|
114
|
+
lambda sid, b, a: "pick-" in sid and "### Ref" in a["out"] and b["exit"] == a["exit"] == 0,
|
|
115
|
+
),
|
|
116
|
+
(
|
|
117
|
+
"cascade: page state from earlier steps",
|
|
118
|
+
"Same step result; the fixture's click log / field values differ only because earlier steps now succeed "
|
|
119
|
+
"(iframe fills, refs outside a scoped view) or no longer act on a wrong element.",
|
|
120
|
+
lambda sid, b, a: (
|
|
121
|
+
b["exit"] == a["exit"] and _same_shape(b["out"], a["out"]) and b.get("log_delta") == a.get("log_delta")
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _add_log_deltas(steps: list[dict]) -> None:
|
|
128
|
+
"""What each step itself appended to the fixture's click log, so a cascade cannot hide a different action."""
|
|
129
|
+
prev = ""
|
|
130
|
+
for step in steps:
|
|
131
|
+
state = step.get("state")
|
|
132
|
+
log = state.get("log", "") if isinstance(state, dict) else None
|
|
133
|
+
if log is None:
|
|
134
|
+
continue
|
|
135
|
+
step["log_delta"] = log[len(prev) :] if log.startswith(prev) else log
|
|
136
|
+
prev = log
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
NOTES = """## Notes
|
|
140
|
+
|
|
141
|
+
- Known residual, identical before and after: `tabs:a-ref-in-b-same-number`. Two tabs that are both on
|
|
142
|
+
their first document can print the same ref (`e3`); the tab guard cannot tell them apart, so tab b's
|
|
143
|
+
`e3` is clicked. Both versions do this; SKILL.md says refs are per tab and to use the snapshot that
|
|
144
|
+
`tab-select` prints.
|
|
145
|
+
- Ref numbers are Playwright's: the page is on a later document by the time most steps run, so refs read
|
|
146
|
+
`f1e18` where the old registry printed `e19`. The sweep's argv names targets by role and name, so the
|
|
147
|
+
steps still line up.
|
|
148
|
+
- Timing (ms, CLI round trip): controls `snapshot --boxes` 90016 (timeout) -> ~210; offscreen `--boxes`
|
|
149
|
+
~4250 -> ~215; large `--boxes` ~2700 -> ~250; plain large `snapshot` unchanged (~220).
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _same_shape(x: str, y: str) -> bool:
|
|
154
|
+
strip = lambda s: re.sub(r"\b(f\d+)?e\d+\b", "<ref>", s) # noqa: E731
|
|
155
|
+
return strip(x) == strip(y)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main() -> int:
|
|
159
|
+
runs = []
|
|
160
|
+
for path in sys.argv[1:3]:
|
|
161
|
+
steps = json.loads(Path(path).read_text(encoding="utf-8"))["steps"]
|
|
162
|
+
_add_log_deltas(steps)
|
|
163
|
+
keyed: dict[str, dict] = {}
|
|
164
|
+
for step in steps: # repeated ids get #2, #3 like sweep.py diff
|
|
165
|
+
key, n = step["id"], 2
|
|
166
|
+
while key in keyed:
|
|
167
|
+
key, n = f"{step['id']}#{n}", n + 1
|
|
168
|
+
keyed[key] = step
|
|
169
|
+
runs.append(keyed)
|
|
170
|
+
before, after = runs
|
|
171
|
+
by_rule: dict[str, list[str]] = {name: [] for name, _, _ in RULES}
|
|
172
|
+
unexplained = []
|
|
173
|
+
for sid, b in before.items():
|
|
174
|
+
a = after.get(sid)
|
|
175
|
+
if a is None:
|
|
176
|
+
unexplained.append(f"{sid} (missing after)")
|
|
177
|
+
continue
|
|
178
|
+
if not [f for f in COMPARED if b.get(f) != a.get(f)]:
|
|
179
|
+
continue
|
|
180
|
+
for name, _, match in RULES:
|
|
181
|
+
if match(sid, b, a):
|
|
182
|
+
by_rule[name].append(f"{sid} (exit {b['exit']} -> {a['exit']})")
|
|
183
|
+
break
|
|
184
|
+
else:
|
|
185
|
+
unexplained.append(sid)
|
|
186
|
+
|
|
187
|
+
differing = sum(len(v) for v in by_rule.values()) + len(unexplained)
|
|
188
|
+
print("# Ref sweep: before (RefRegistry) vs after (native aria refs)\n")
|
|
189
|
+
print(
|
|
190
|
+
f"{len(before)} steps; {len(before) - differing} identical; {differing} differ; {len(unexplained)} unexplained.\n"
|
|
191
|
+
)
|
|
192
|
+
for name, reason, _ in RULES:
|
|
193
|
+
if by_rule[name]:
|
|
194
|
+
print(f"## Intended: {name} ({len(by_rule[name])})\n\n{reason}\n")
|
|
195
|
+
for line in by_rule[name]:
|
|
196
|
+
print(f"- {line}")
|
|
197
|
+
print()
|
|
198
|
+
print(NOTES)
|
|
199
|
+
if unexplained:
|
|
200
|
+
print(f"## UNEXPLAINED ({len(unexplained)})\n")
|
|
201
|
+
for sid in unexplained:
|
|
202
|
+
print(f"- {sid}")
|
|
203
|
+
return 1 if unexplained else 0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
sys.exit(main())
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Sweep harness: fixtures record what happened to them in a data attribute on
|
|
2
|
+
// <html>. `eval` runs in an isolated world that cannot see page globals, but it
|
|
3
|
+
// can read the DOM, so the log lives there (and stays out of the aria tree).
|
|
4
|
+
function log(msg) {
|
|
5
|
+
const root = document.documentElement;
|
|
6
|
+
root.dataset.log = (root.dataset.log ? root.dataset.log + ' ' : '') + msg;
|
|
7
|
+
}
|
|
8
|
+
document.addEventListener('click', e => log('click:' + (e.target.id || e.target.tagName)), true);
|
|
9
|
+
document.addEventListener('dblclick', e => log('dblclick:' + (e.target.id || e.target.tagName)), true);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head><meta charset="UTF-8"><title>Controls</title><script src="common.js"></script></head>
|
|
4
|
+
<body>
|
|
5
|
+
<h1>Controls</h1>
|
|
6
|
+
<section aria-label="Form">
|
|
7
|
+
<label>Name <input id="name" type="text"></label>
|
|
8
|
+
<label>Bio <textarea id="bio"></textarea></label>
|
|
9
|
+
<label><input id="agree" type="checkbox"> I agree</label>
|
|
10
|
+
<label><input id="r1" type="radio" name="size" value="s"> Small</label>
|
|
11
|
+
<label><input id="r2" type="radio" name="size" value="l"> Large</label>
|
|
12
|
+
<label>Color
|
|
13
|
+
<select id="color"><option value="red">Red</option><option value="green">Green</option><option value="blue">Blue</option></select>
|
|
14
|
+
</label>
|
|
15
|
+
<label>Avatar <input id="avatar" type="file"></label>
|
|
16
|
+
<button id="go" type="button" onclick="log('go')">Go</button>
|
|
17
|
+
<button id="dbl" type="button" ondblclick="log('dbl')">Double me</button>
|
|
18
|
+
<button id="hov" type="button" onmouseenter="log('hover')">Hover me</button>
|
|
19
|
+
<a id="anchor" href="#section-two">Jump to two</a>
|
|
20
|
+
<button id="star" type="button" aria-label="">★</button>
|
|
21
|
+
<button id="say" type="button">Say "hi"</button>
|
|
22
|
+
</section>
|
|
23
|
+
<section aria-label="Duplicates">
|
|
24
|
+
<h2>Duplicates</h2>
|
|
25
|
+
<ul>
|
|
26
|
+
<li><button id="save1" type="button">Save</button></li>
|
|
27
|
+
<li><button id="save2" type="button">Save</button></li>
|
|
28
|
+
<li><button id="save3" type="button">Save</button></li>
|
|
29
|
+
</ul>
|
|
30
|
+
<a id="dup-link-1" href="#a">More</a> <a id="dup-link-2" href="#b">More</a>
|
|
31
|
+
</section>
|
|
32
|
+
<section aria-label="Drag">
|
|
33
|
+
<h2 id="section-two">Drag</h2>
|
|
34
|
+
<div id="src" draggable="true" role="button" aria-label="Drag source"
|
|
35
|
+
ondragstart="event.dataTransfer.setData('text/plain','src')">Drag source</div>
|
|
36
|
+
<div id="dst" role="region" aria-label="Drop target" style="width:200px;height:60px;border:1px solid #888"
|
|
37
|
+
ondragover="event.preventDefault()"
|
|
38
|
+
ondrop="event.preventDefault(); log('drop:' + event.dataTransfer.getData('text/plain') + ':' + Array.from(event.dataTransfer.files).map(f => f.name).join(','))">Drop here</div>
|
|
39
|
+
</section>
|
|
40
|
+
<section aria-label="Pick">
|
|
41
|
+
<button type="button">Pick me</button>
|
|
42
|
+
<button type="button">Pick me</button>
|
|
43
|
+
</section>
|
|
44
|
+
<p id="para">Plain paragraph text for find --all.</p>
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|