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.
Files changed (109) hide show
  1. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.claude-plugin/marketplace.json +1 -1
  2. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.github/workflows/ci.yml +2 -2
  3. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/CLAUDE.md +4 -3
  4. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/CONTRIBUTING.md +1 -1
  5. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/PKG-INFO +38 -9
  6. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/README.md +35 -4
  7. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/docs/daemon-protocol.md +35 -8
  8. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/docs/installation.md +1 -1
  9. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/pyproject.toml +6 -6
  10. patchright_cli-0.11.1/scripts/ref_sweep/classify.py +207 -0
  11. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/common.js +9 -0
  12. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/controls.html +46 -0
  13. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/delayed.html +18 -0
  14. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/dialog.html +12 -0
  15. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/iframe.html +9 -0
  16. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/iframe_child.html +9 -0
  17. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/large.html +161 -0
  18. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/nav.html +31 -0
  19. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/nav2.html +10 -0
  20. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/nested.html +21 -0
  21. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/offscreen.html +10 -0
  22. patchright_cli-0.11.1/scripts/ref_sweep/fixtures/shadow.html +17 -0
  23. patchright_cli-0.11.1/scripts/ref_sweep/results/after.json +3920 -0
  24. patchright_cli-0.11.1/scripts/ref_sweep/results/before.json +3950 -0
  25. patchright_cli-0.11.1/scripts/ref_sweep/results/diff-report.md +187 -0
  26. patchright_cli-0.11.1/scripts/ref_sweep/results/diff.txt +5247 -0
  27. patchright_cli-0.11.1/scripts/ref_sweep/sweep.py +627 -0
  28. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/SKILL.md +88 -19
  29. patchright_cli-0.11.1/skills/patchright-cli/references/snapshot-refs.md +155 -0
  30. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/storage-state.md +1 -1
  31. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/__init__.py +1 -1
  32. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/cli.py +382 -32
  33. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/daemon.py +1689 -387
  34. patchright_cli-0.11.1/src/patchright_cli/debugger.py +240 -0
  35. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/mcp_server.py +287 -16
  36. patchright_cli-0.11.1/src/patchright_cli/snapshot.py +67 -0
  37. patchright_cli-0.11.1/src/patchright_cli/snapshot_text.py +535 -0
  38. patchright_cli-0.11.1/src/patchright_cli/values.py +316 -0
  39. patchright_cli-0.11.1/tests/detect_fixture.html +39 -0
  40. patchright_cli-0.11.1/tests/ref_helpers.py +24 -0
  41. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_agent_hooks.py +2 -4
  42. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_annotate.py +1 -3
  43. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_attach.py +1 -0
  44. patchright_cli-0.11.1/tests/test_cli_audit.py +280 -0
  45. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_cli_axi.py +33 -2
  46. patchright_cli-0.11.1/tests/test_daemon_audit.py +352 -0
  47. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_axi.py +65 -35
  48. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_handlers.py +632 -137
  49. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_token.py +3 -1
  50. patchright_cli-0.11.1/tests/test_debugger.py +389 -0
  51. patchright_cli-0.11.1/tests/test_e2e.py +940 -0
  52. patchright_cli-0.11.1/tests/test_external_close.py +338 -0
  53. patchright_cli-0.11.1/tests/test_i41_extras.py +494 -0
  54. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_mcp.py +82 -2
  55. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_navigation_policy.py +6 -9
  56. patchright_cli-0.11.1/tests/test_passkeys.py +304 -0
  57. patchright_cli-0.11.1/tests/test_pick_locator.py +368 -0
  58. patchright_cli-0.11.1/tests/test_snapshot_refs.py +236 -0
  59. patchright_cli-0.11.1/tests/test_snapshot_text.py +340 -0
  60. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_tabs.py +19 -16
  61. patchright_cli-0.11.1/tests/test_webmcp.py +266 -0
  62. patchright_cli-0.11.1/tests/webmcp_fixture.html +21 -0
  63. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/uv.lock +29 -846
  64. patchright_cli-0.10.0/skills/patchright-cli/references/snapshot-refs.md +0 -142
  65. patchright_cli-0.10.0/src/patchright_cli/ref_registry.py +0 -337
  66. patchright_cli-0.10.0/src/patchright_cli/snapshot.py +0 -48
  67. patchright_cli-0.10.0/tests/test_e2e.py +0 -238
  68. patchright_cli-0.10.0/tests/test_ref_registry.py +0 -634
  69. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.claude-plugin/skills/browser-automation.json +0 -0
  70. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.github/dependabot.yml +0 -0
  71. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.github/workflows/publish-docker.yml +0 -0
  72. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.github/workflows/publish.yml +0 -0
  73. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.gitignore +0 -0
  74. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/.pre-commit-config.yaml +0 -0
  75. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/Dockerfile +0 -0
  76. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/GLOSSARY.md +0 -0
  77. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/LICENSE +0 -0
  78. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/Makefile +0 -0
  79. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/compose.yml +0 -0
  80. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/docs/mcp-architecture.svg +0 -0
  81. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/patchright-cli +0 -0
  82. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/patchright-cli.cmd +0 -0
  83. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/scripts/gen_skill.py +0 -0
  84. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/request-mocking.md +0 -0
  85. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/running-code.md +0 -0
  86. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/session-management.md +0 -0
  87. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/tracing.md +0 -0
  88. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/skills/patchright-cli/references/video-recording.md +0 -0
  89. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/__main__.py +0 -0
  90. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/_entry.py +0 -0
  91. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/agent_hooks.py +0 -0
  92. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/daemon_pid.py +0 -0
  93. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/dashboard.py +0 -0
  94. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/src/patchright_cli/toon.py +0 -0
  95. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/__init__.py +0 -0
  96. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/fixture.html +0 -0
  97. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_cli_config.py +0 -0
  98. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_cli_home.py +0 -0
  99. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_info.py +0 -0
  100. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_pid.py +0 -0
  101. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_daemon_read.py +0 -0
  102. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_docker.py +0 -0
  103. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_emulation.py +0 -0
  104. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_network_guard.py +0 -0
  105. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_packaging.py +0 -0
  106. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_profiles.py +0 -0
  107. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_skill_sync.py +0 -0
  108. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_supply_chain.py +0 -0
  109. {patchright_cli-0.10.0 → patchright_cli-0.11.1}/tests/test_toon.py +0 -0
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Anti-detect browser automation CLI & Skills for AI agents, powered by Patchright",
8
- "version": "0.10.0"
8
+ "version": "0.11.1"
9
9
  },
10
10
  "plugins": [
11
11
  {
@@ -22,10 +22,10 @@ jobs:
22
22
  strategy:
23
23
  matrix:
24
24
  os: [ubuntu-latest, windows-latest]
25
- python-version: ["3.10", "3.12"]
25
+ python-version: ["3.12", "3.13"]
26
26
  include:
27
27
  - os: macos-latest
28
- python-version: "3.12"
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`)**: Uses Playwright's native `aria_snapshot()` to get the accessibility tree. `RefRegistry` annotates each node with sequential refs (`e1`, `e2`, ...). Snapshots are saved as YAML to `.patchright-cli/` in the caller's cwd.
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 py310)
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)
@@ -31,7 +31,7 @@ pre-commit install
31
31
 
32
32
  ## Code Style
33
33
 
34
- - Python 3.10+
34
+ - Python 3.12+
35
35
  - Formatted with [Ruff](https://github.com/astral-sh/ruff) (enforced via pre-commit)
36
36
  - Keep the daemon lightweight — avoid heavy dependencies
37
37
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchright-cli
3
- Version: 0.10.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.10
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.10+ and Google Chrome
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, annotated with sequential `[ref=eN]` handles for element targeting. |
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 # All console messages
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.10+ and Google Chrome
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, annotated with sequential `[ref=eN]` handles for element targeting. |
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 # All console messages
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. No structured error codes are currently provided.
75
+ ignore unknown fields.
76
76
 
77
- Clients can detect these states in failed replies by matching these output
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
- Best effort, not covered by the compatibility rules below: a command on a page,
128
- context or browser that has closed usually fails with Playwright's own message
129
- containing `has been closed`. That text comes from Playwright and can change
130
- with its version without a protocol increment.
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, or the
137
- state-detection fragments in the table above requires a protocol increment.
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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Prerequisites
4
4
 
5
- - Python 3.10+
5
+ - Python 3.12+
6
6
  - Google Chrome installed on the system
7
7
 
8
8
  ## Install
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "patchright-cli"
3
- version = "0.10.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.10"
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
- "patchright",
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 = "py310"
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="">&#9733;</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>