cdpbrowser 0.1.0__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 (61) hide show
  1. cdpbrowser-0.1.0/PKG-INFO +436 -0
  2. cdpbrowser-0.1.0/README.md +413 -0
  3. cdpbrowser-0.1.0/pyproject.toml +40 -0
  4. cdpbrowser-0.1.0/setup.cfg +4 -0
  5. cdpbrowser-0.1.0/src/CdpBrowser.py +19 -0
  6. cdpbrowser-0.1.0/src/cdpbrowser/__init__.py +68 -0
  7. cdpbrowser-0.1.0/src/cdpbrowser/bridge.js +909 -0
  8. cdpbrowser-0.1.0/src/cdpbrowser/cdp/__init__.py +32 -0
  9. cdpbrowser-0.1.0/src/cdpbrowser/cdp/chrome.py +495 -0
  10. cdpbrowser-0.1.0/src/cdpbrowser/cdp/errors.py +47 -0
  11. cdpbrowser-0.1.0/src/cdpbrowser/cdp/transport.py +384 -0
  12. cdpbrowser-0.1.0/src/cdpbrowser/keys.py +128 -0
  13. cdpbrowser-0.1.0/src/cdpbrowser/library.py +1440 -0
  14. cdpbrowser-0.1.0/src/cdpbrowser/listener.py +112 -0
  15. cdpbrowser-0.1.0/src/cdpbrowser/locators.py +183 -0
  16. cdpbrowser-0.1.0/src/cdpbrowser/page.py +1880 -0
  17. cdpbrowser-0.1.0/src/cdpbrowser/polling.py +133 -0
  18. cdpbrowser-0.1.0/src/cdpbrowser/promises.py +345 -0
  19. cdpbrowser-0.1.0/src/cdpbrowser/recorder.js +258 -0
  20. cdpbrowser-0.1.0/src/cdpbrowser/recorder.py +208 -0
  21. cdpbrowser-0.1.0/src/cdpbrowser/visual.py +286 -0
  22. cdpbrowser-0.1.0/src/cdpbrowser.egg-info/PKG-INFO +436 -0
  23. cdpbrowser-0.1.0/src/cdpbrowser.egg-info/SOURCES.txt +59 -0
  24. cdpbrowser-0.1.0/src/cdpbrowser.egg-info/dependency_links.txt +1 -0
  25. cdpbrowser-0.1.0/src/cdpbrowser.egg-info/requires.txt +10 -0
  26. cdpbrowser-0.1.0/src/cdpbrowser.egg-info/top_level.txt +2 -0
  27. cdpbrowser-0.1.0/tests/test_accessibility.py +134 -0
  28. cdpbrowser-0.1.0/tests/test_chrome.py +353 -0
  29. cdpbrowser-0.1.0/tests/test_cookies.py +117 -0
  30. cdpbrowser-0.1.0/tests/test_dialogs.py +91 -0
  31. cdpbrowser-0.1.0/tests/test_downloads.py +87 -0
  32. cdpbrowser-0.1.0/tests/test_errors.py +94 -0
  33. cdpbrowser-0.1.0/tests/test_escape_hatches.py +86 -0
  34. cdpbrowser-0.1.0/tests/test_ext_locator.py +87 -0
  35. cdpbrowser-0.1.0/tests/test_frames.py +270 -0
  36. cdpbrowser-0.1.0/tests/test_gapfill_core.py +208 -0
  37. cdpbrowser-0.1.0/tests/test_gapfill_library.py +262 -0
  38. cdpbrowser-0.1.0/tests/test_keys.py +135 -0
  39. cdpbrowser-0.1.0/tests/test_known_issues.py +173 -0
  40. cdpbrowser-0.1.0/tests/test_library_polling.py +320 -0
  41. cdpbrowser-0.1.0/tests/test_listener.py +165 -0
  42. cdpbrowser-0.1.0/tests/test_locators.py +245 -0
  43. cdpbrowser-0.1.0/tests/test_packaging.py +110 -0
  44. cdpbrowser-0.1.0/tests/test_page.py +277 -0
  45. cdpbrowser-0.1.0/tests/test_polling.py +178 -0
  46. cdpbrowser-0.1.0/tests/test_promises.py +105 -0
  47. cdpbrowser-0.1.0/tests/test_real_keys.py +94 -0
  48. cdpbrowser-0.1.0/tests/test_real_mouse.py +104 -0
  49. cdpbrowser-0.1.0/tests/test_recorder_flows.py +119 -0
  50. cdpbrowser-0.1.0/tests/test_recorder_integration.py +128 -0
  51. cdpbrowser-0.1.0/tests/test_recorder_unit.py +89 -0
  52. cdpbrowser-0.1.0/tests/test_shadow_dom.py +100 -0
  53. cdpbrowser-0.1.0/tests/test_state_keywords.py +79 -0
  54. cdpbrowser-0.1.0/tests/test_tabs.py +91 -0
  55. cdpbrowser-0.1.0/tests/test_transport.py +173 -0
  56. cdpbrowser-0.1.0/tests/test_transport_real_chrome.py +163 -0
  57. cdpbrowser-0.1.0/tests/test_upload_scroll.py +76 -0
  58. cdpbrowser-0.1.0/tests/test_viewport.py +100 -0
  59. cdpbrowser-0.1.0/tests/test_visual_advanced.py +157 -0
  60. cdpbrowser-0.1.0/tests/test_visual_engine.py +122 -0
  61. cdpbrowser-0.1.0/tests/test_visual_keyword.py +90 -0
@@ -0,0 +1,436 @@
1
+ Metadata-Version: 2.4
2
+ Name: cdpbrowser
3
+ Version: 0.1.0
4
+ Summary: Framework-independent pure-CDP browser automation core for Python, with an optional Robot Framework adapter
5
+ Project-URL: Homepage, https://github.com/lhjnano/cdpbrowser
6
+ Classifier: Development Status :: 4 - Beta
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Classifier: Topic :: Software Development :: Quality Assurance
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: websockets>=17.0
17
+ Provides-Extra: robot
18
+ Requires-Dist: robotframework>=7.0; extra == "robot"
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Provides-Extra: visual
22
+ Requires-Dist: Pillow>=10; extra == "visual"
23
+
24
+ # CdpBrowser
25
+
26
+ CdpBrowser is a browser automation library that speaks the Chrome DevTools
27
+ Protocol directly from Python. It has no driver binaries, no Node runtime,
28
+ and no Selenium. The core is framework independent; a Robot Framework
29
+ keyword adapter is available as an optional extra.
30
+
31
+ ```bash
32
+ pip install cdpbrowser # core only
33
+ pip install "cdpbrowser[robot]" # Robot Framework keywords
34
+ pip install "cdpbrowser[visual]" # Pillow, for visual regression
35
+ pip install "cdpbrowser[dev,robot,visual]" # development
36
+ ```
37
+
38
+ Without robotframework installed, `cdpbrowser.CdpBrowser` raises an
39
+ `ImportError` that names the `[robot]` extra. The core (`PageSession` and
40
+ the modules under `cdpbrowser.*`) needs no extras.
41
+
42
+ ## Design
43
+
44
+ - **Direct CDP.** One background asyncio loop per library, one WebSocket to
45
+ Chrome. Nothing in between.
46
+ - **Poll by default.** Interaction and assertion keywords retry their
47
+ condition until the deadline set by `Set Timeout`. A missing element is a
48
+ retry, not an error.
49
+ - **Atomic bridge.** bridge.js runs find, visibility, and enabled checks in
50
+ a single step inside the browser. Python receives plain JSON.
51
+ - **Evidence.** Screenshots accumulate under
52
+ `{outputdir}/evidence/{suite}/{test}/` with per-test sequence numbers. A
53
+ listener saves the last screen on failure.
54
+ - **Optional adapter.** The core imports nothing from Robot. Only
55
+ `library.py` and `listener.py` do.
56
+
57
+ ## Quick start, Robot Framework
58
+
59
+ ```robotframework
60
+ *** Settings ***
61
+ Library CdpBrowser
62
+
63
+ *** Test Cases ***
64
+ Hello CdpBrowser
65
+ Go To file:///tmp/demo.html
66
+ ${title}= Get Title
67
+ Should Be Equal ${title} Demo
68
+ Click testid:submit-button
69
+ Element Should Be Visible testid:done-panel
70
+ [Teardown] Close Browser
71
+ ```
72
+
73
+ Chrome launches lazily on the first keyword. `headless` defaults to `auto`:
74
+ headless when `CI` is set or `DISPLAY` is missing. `--no-sandbox` is added
75
+ under root, in detected CI environments, or when
76
+ `CDPBROWSER_NO_SANDBOX=1`; `--disable-dev-shm-usage` is added when
77
+ `/dev/shm` is under 1 GiB.
78
+
79
+ ### Import arguments
80
+
81
+ | Argument | Default | Description |
82
+ | --- | --- | --- |
83
+ | `chrome_path` | auto-detected | Chrome executable path |
84
+ | `headless` | `auto` | `True`, `False`, or `auto` |
85
+ | `timeout` | `5s` | Default polling deadline (Robot time string) |
86
+ | `poll_interval` | `0.1s` | Polling retry interval |
87
+ | `baseline_dir` | `visual-baselines/` | Root for visual-regression baselines |
88
+
89
+ ## Using the core from Python
90
+
91
+ The core is a synchronous Python API. The test suite drives it without
92
+ Robot everywhere.
93
+
94
+ ```python
95
+ from cdpbrowser import ChromeProcess, CdpConnection, PageSession, find_chrome
96
+
97
+ chrome = ChromeProcess(find_chrome(), headless=True)
98
+ chrome.start()
99
+ try:
100
+ session = PageSession(CdpConnection(chrome.ws_url)).attach()
101
+ session.navigate("https://example.com")
102
+
103
+ session.call("click", {"s": "id", "v": "submit"}) # testid: selector
104
+ text = session.call("getText", {"s": "id", "v": "result"})
105
+
106
+ handle = session.arm_download()
107
+ session.call("click", {"s": "id", "v": "download-link"})
108
+ saved = session.wait_download(handle, 10.0)
109
+
110
+ # Frames, viewport, cookies, and real key/mouse input are also plain
111
+ # methods. See the PageSession docstrings.
112
+ finally:
113
+ session.close()
114
+ chrome.stop()
115
+ ```
116
+
117
+ For waits, `cdpbrowser.polling.poll_until` retries any callable until a
118
+ deadline. It is the same primitive the Robot adapter uses.
119
+
120
+ ## Concepts
121
+
122
+ ### Dialogs: arm before the trigger
123
+
124
+ While a JS dialog (alert, confirm, prompt) is open, the CDP session blocks
125
+ other commands, including the evaluation inside `Click`. Reserve the dialog
126
+ first, then trigger it, then collect the result:
127
+
128
+ ```robotframework
129
+ ${handle}= Promise Next Alert action=ACCEPT
130
+ Click testid:submit
131
+ ${text}= Wait For ${handle}
132
+ Should Be Equal As Strings ${text} Passwords don't match
133
+ ```
134
+
135
+ A dialog opened without an arm is dismissed automatically after
136
+ `Set Timeout` elapses, with a warning. This prevents deadlocks.
137
+
138
+ `Handle Alert` combines the three steps for clicks:
139
+ `Handle Alert ACCEPT Click testid:delete`.
140
+
141
+ ### Downloads
142
+
143
+ Downloads do not block the session, so the order matters less, but the
144
+ convention is the same:
145
+
146
+ ```robotframework
147
+ ${promise}= Promise Next Download
148
+ Click testid:download-link
149
+ ${file}= Wait For ${promise}
150
+ File Should Exist ${file}
151
+ ```
152
+
153
+ The first `Promise Next Download` configures Chrome to save under
154
+ `results/downloads/{suite}/{test}/`. `Wait For` returns the saved file's
155
+ full path.
156
+
157
+ ### Fast and real input tracks
158
+
159
+ `Click` and `Fill Text` simulate input in JavaScript. `Type Text`,
160
+ `Press Keys`, and the mouse keywords send real input events through CDP.
161
+ Use the real track when the application checks `isTrusted` or listens for
162
+ keydown. ASCII goes through per-key events; non-ASCII such as Hangul falls
163
+ back to `Input.insertText` and is documented as such.
164
+
165
+ ### Visual regression
166
+
167
+ `Page Should Match Baseline` compares the viewport against a committed PNG.
168
+ Tolerances are a per-channel delta (`pixel_delta`) and a mismatch-ratio cap
169
+ (`max_mismatch_ratio`). Failures write `.actual.png` and `.diff.png`
170
+ artifacts and describe the change in words: one localized region, a whole
171
+ image shift, or scattered changes. Fix the viewport with
172
+ `Set Viewport Size` before comparing. Without Pillow the comparison is
173
+ exact-match only.
174
+
175
+ ### Recording and replay
176
+
177
+ `Start Recording` captures real input on the current tab: manual driving,
178
+ real-mouse keywords, or any keyword that produces DOM events. Typing
179
+ collapses into one fill per element. Observed dialogs and downloads are
180
+ attached to the action that triggered them.
181
+
182
+ ```robotframework
183
+ Start Recording
184
+ Go To https://app.example.com/
185
+ # interact, by hand or with keywords
186
+ Save Recording ${CURDIR}${/}recorded.robot
187
+ ```
188
+
189
+ The generated Robot test uses poll-by-default keywords, so replays absorb
190
+ timing differences. `Save Recording` with `format=Python` writes a
191
+ plain-Python replay against the core instead. Recording covers the main
192
+ frame only.
193
+
194
+ ## Keyword reference
195
+
196
+ Unless noted, interaction and assertion keywords poll until the deadline.
197
+
198
+ ### Session
199
+
200
+ | Keyword | Arguments | Description |
201
+ | --- | --- | --- |
202
+ | `Open Browser` | | Explicit launch. No-op when already open. |
203
+ | `Close Browser` | | Closes all tabs, the connection, and Chrome. Idempotent. |
204
+ | `Go To` | `url` | Navigates, waits for load, returns the final URL. |
205
+ | `Get Title` | | `document.title` |
206
+ | `Get Current Url` | | `location.href` |
207
+
208
+ ### Interaction
209
+
210
+ | Keyword | Arguments | Description |
211
+ | --- | --- | --- |
212
+ | `Click` | `selector` | Atomic find, visibility, and enabled checks, then click. |
213
+ | `Fill Text` | `selector`, `text` | Native setter plus input and change events. |
214
+ | `Type Text` | `selector`, `text` | Real key events. Appends; does not clear. Non-ASCII uses `insertText`. |
215
+ | `Press Keys` | `*keys` | Special keys (`ENTER`, `TAB`, `ARROW_*`, ...) as real events. |
216
+ | `Click With Real Mouse` | `selector` | Scrolls into view, clicks the center with real events. Returns `"x,y"`. |
217
+ | `Click At Coordinates` | `x`, `y` | Real click at viewport coordinates. For canvas. |
218
+ | `Hover` | `selector` | Real pointer move. Triggers CSS `:hover`. |
219
+ | `Drag From To` | `source`, `target` | Real press, move, release. No HTML5 drag events. |
220
+ | `Scroll By` | `x=0`, `y=0` | Real wheel scroll in pixels. Returns the final scrollY. |
221
+ | `Scroll To Element` | `selector` | Scrolls the element into the viewport. |
222
+ | `Upload File` | `selector`, `*paths` | Sets files directly with `DOM.setFileInputFiles`. Absolute paths; `css=` or `testid:` selectors only. |
223
+
224
+ ### Assertions
225
+
226
+ | Keyword | Arguments | Description |
227
+ | --- | --- | --- |
228
+ | `Element Should Be Visible` | `selector` | Waits for visibility. |
229
+ | `Element Should Exist` | `selector` | Waits for DOM presence. |
230
+ | `Element Should Not Exist` | `selector` | Confirms presence first, then waits for removal. |
231
+
232
+ ### Reading state
233
+
234
+ | Keyword | Arguments | Returns | Description |
235
+ | --- | --- | --- | --- |
236
+ | `Get Text` | `selector` | `str` | `textContent`. Does not read input values. |
237
+ | `Get Attribute` | `selector`, `name` | `str` or `None` | A missing attribute is `None`, not a failure. |
238
+ | `Get Element Count` | `selector` | `int` | Snapshot. Zero is valid; precede with `Element Should Exist` to wait. |
239
+
240
+ ### Frames
241
+
242
+ | Keyword | Arguments | Description |
243
+ | --- | --- | --- |
244
+ | `Switch To Frame` | `frame` | Numeric index, `name`, `id`, or a CSS selector for the iframe. Calls stack. |
245
+ | `Reset Frame` | `scope=` | Pops one level, or all with `scope=ALL`. |
246
+
247
+ Same-site frames only. Cross-site iframes (OOPIF) run in a separate process
248
+ and have no execution context in this session.
249
+
250
+ ### Dialogs and downloads
251
+
252
+ | Keyword | Arguments | Description |
253
+ | --- | --- | --- |
254
+ | `Promise Next Alert` | `action`, `prompt_text=` | Arms the next dialog. Returns a handle. |
255
+ | `Promise Next Download` | | Arms the next download completion. Returns a handle. |
256
+ | `Wait For` | `handle`, `timeout=` | Waits for an armed promise. Returns the dialog text or the saved file path. |
257
+ | `Handle Alert` | `action`, `*trigger`, `text=` | Arm, run trigger keyword, wait, and optionally assert the text. |
258
+
259
+ ### Tabs
260
+
261
+ | Keyword | Arguments | Description |
262
+ | --- | --- | --- |
263
+ | `New Tab` | `url=` | Creates a tab, switches to it, returns the 0-based index. |
264
+ | `Switch To Tab` | `tab` | 0-based index, or a title or URL substring. |
265
+ | `Close Tab` | `tab=` | Default current. Refuses the last tab; use `Close Browser`. |
266
+
267
+ Dialog arms, downloads, and frame scope are isolated per tab.
268
+
269
+ ### Cookies
270
+
271
+ | Keyword | Arguments | Description |
272
+ | --- | --- | --- |
273
+ | `Get Cookies` | `urls=` | Comma-separated URLs narrow the result. Use http(s) origins. |
274
+ | `Set Cookie` | `url`, `name`, `value`, ... | Optional `expires`, `http_only`, `secure`, `same_site`. |
275
+ | `Delete Cookie` | `name`, `url=` | Deletes by name, optionally per origin. |
276
+ | `Delete All Cookies` | | Browser-global. |
277
+
278
+ ### Viewport
279
+
280
+ | Keyword | Arguments | Description |
281
+ | --- | --- | --- |
282
+ | `Set Viewport Size` | `width`, `height`, `device_scale_factor=1.0`, `mobile=False` | Per tab. `mobile=True` enables touch emulation. |
283
+ | `Reset Viewport` | | Clears the override. |
284
+
285
+ ### Visual regression
286
+
287
+ | Keyword | Arguments | Description |
288
+ | --- | --- | --- |
289
+ | `Page Should Match Baseline` | `name`, `pixel_delta=0`, `max_mismatch_ratio=0.0`, `mask=` | Compares against `{baseline_dir}/{name}.png`. |
290
+ | `Update Baseline` | `name`, `mask=` | Recreates a baseline. Use the same mask as the comparison. |
291
+
292
+ `mask` takes comma-separated selectors and blacks out matching elements
293
+ before capture, for volatile areas such as timestamps. Mask boxes follow
294
+ element rects, so keep length-changing content in a fixed-width container.
295
+ Set `CDPBROWSER_UPDATE_BASELINES=1` to rewrite baselines instead of
296
+ failing.
297
+
298
+ ### Recording
299
+
300
+ | Keyword | Arguments | Description |
301
+ | --- | --- | --- |
302
+ | `Start Recording` | | Records real input on the current tab. |
303
+ | `Stop Recording` | `name=` | Stops and returns a Robot replay script. |
304
+ | `Save Recording` | `path`, `name=`, `format=Robot` | Stops and writes the script. `format=Python` for a core script. |
305
+
306
+ ### Escape hatches
307
+
308
+ | Keyword | Arguments | Description |
309
+ | --- | --- | --- |
310
+ | `Run Javascript` | `expression` | Evaluation with JSON return and awaited promises. Respects frame scope. |
311
+ | `Execute Javascript` | `script` | Same execution, but the return value is discarded before serialization. For jQuery-style chains whose result fails CDP serialization. |
312
+ | `Execute CDP Command` | `method`, `params_json=` | Raw CDP. `Browser.*` and `Target.*` route at browser level. |
313
+ | `Insert Text` | `text` | `Input.insertText` into the focused element. The IME bypass. |
314
+
315
+ ## Selectors
316
+
317
+ | Syntax | Strategy | Description |
318
+ | --- | --- | --- |
319
+ | `.nav > a` | css | Default when unprefixed. |
320
+ | `testid:submit` | testid | Matches `data-testid`. |
321
+ | `text:Log in` | text | Exact match first, then case-insensitive partial. |
322
+ | `x://button[1]` | xpath | XPath. |
323
+ | `css:#id` | css | Explicit prefix, for values that collide with the reserved ones. |
324
+ | `role:button Save` | role | Explicit `role` attribute, then implicit roles. Optional accessible name after the first space. Case-insensitive. |
325
+ | `label:User name` | label | Accessible name, simplified accname. Case-insensitive. |
326
+ | `deep:...` | modifier | Pierces open shadow DOM. Opt-in. Closed roots are inaccessible. |
327
+ | `ext:...` | ext | Ext JS ComponentQuery when `window.Ext` is exposed. DOM-independent. |
328
+
329
+ Selector stability, best to worst: explicit `data-testid`, accessibility
330
+ anchors, explicit id or form name, stable text, structural CSS or XPath.
331
+ Never anchor on generated ids such as `ext-gen123` or `:r1:`.
332
+
333
+ ## Scope
334
+
335
+ Controllable: what the browser exposes about the page, meaning the DOM,
336
+ accessibility tree, events, downloads, uploads, tabs, and network. Not
337
+ controllable: the OS, browser chrome, and physical devices.
338
+
339
+ | Level | Scope |
340
+ | --- | --- |
341
+ | Full | Apps whose elements live in the DOM or accessibility tree, including shadow DOM via `deep:` and same-site iframes. |
342
+ | Partial | Canvas and WebGL. No element anchors; use `Click At Coordinates` with visual assertions. |
343
+ | Unsupported | OS-native dialogs, desktop-to-browser drags, real IME composition (use `Insert Text`), anti-bot evasion. |
344
+
345
+ ## Parallel and multi-agent use
346
+
347
+ Several agents or test runners can drive CdpBrowser at the same time.
348
+
349
+ **Separate processes, separate browsers.** Every launch uses
350
+ `--remote-debugging-port=0`, so the OS assigns a free port, and a fresh
351
+ temporary `--user-data-dir`, so profiles never collide. Nothing is shared
352
+ at module level: each process gets its own Chrome, connection, and state.
353
+ Parallel `pabot` runs work the same way. When several processes share one
354
+ working directory, give each its own `CDPBROWSER_OUTPUT_DIR` so evidence
355
+ and download paths do not overlap.
356
+
357
+ **Several browsers inside one process.** The core is instance based, so
358
+ nothing stops a single script from running two independent browsers:
359
+
360
+ ```python
361
+ chrome_a = ChromeProcess(find_chrome(), headless=True).start()
362
+ chrome_b = ChromeProcess(find_chrome(), headless=True).start()
363
+ session_a = PageSession(CdpConnection(chrome_a.ws_url)).attach()
364
+ session_b = PageSession(CdpConnection(chrome_b.ws_url)).attach()
365
+ ```
366
+
367
+ Each `PageSession` owns its target, dialog arms, downloads, and frame
368
+ scope. Tabs within one browser are cheaper still: see `New Tab`.
369
+
370
+ **Not supported: sharing one Chrome across processes.** There is no
371
+ keyword to attach to an externally running browser. Ownership of the
372
+ lifecycle and the browser-global download path would need an explicit
373
+ policy first.
374
+
375
+ The Robot keyword layer drives one browser per library instance; a single
376
+ suite that needs two browsers should either use `New Tab`, or run the
377
+ second browser through the core from a keyword.
378
+
379
+ ## Evidence
380
+
381
+ Captures land in `{outputdir}/evidence/{suite}/{test}/NNNN-{label}.png`.
382
+ Numbering resets per test and is shared between `Numbered Screenshot` and
383
+ automatic captures. `Set Evidence Mode` selects `off`, `on-failure`
384
+ (default), or `every-step`.
385
+
386
+ ## Environment variables
387
+
388
+ | Variable | Effect |
389
+ | --- | --- |
390
+ | `CDPBROWSER_CHROME_PATH` | Chrome path, highest detection priority. |
391
+ | `CHROME_PATH` | Fallback path. |
392
+ | `CDPBROWSER_NO_SANDBOX` | `1` forces `--no-sandbox`. |
393
+ | `CDPBROWSER_DISABLE_DEV_SHM` | `1` forces `--disable-dev-shm-usage`. |
394
+ | `CDPBROWSER_UPDATE_BASELINES` | `1` rewrites visual baselines instead of failing. |
395
+ | `CDPBROWSER_SEND_RETRIES` | How many times a timed-out CDP command is retried. Default 1. |
396
+ | `CDPBROWSER_OUTPUT_DIR` | Overrides the artifact outputdir. |
397
+ | `CI` | Set means headless by default. |
398
+
399
+ ## Development
400
+
401
+ ```bash
402
+ python3 -m venv .venv
403
+ .venv/bin/pip install -e ".[dev,robot,visual]"
404
+
405
+ make test # pytest
406
+ make atest # robot acceptance suite
407
+ make coverage # combined branch coverage plus docs/coverage.md
408
+ ```
409
+
410
+ Real-Chrome tests find the binary through `find_chrome()` and skip when
411
+ absent. Unit tests use a mock CDP WebSocket server (`tests/cdp_mock.py`).
412
+ Packaging guards run the core with robotframework import-blocked. Robot
413
+ fixtures are plain HTML files under `atest/fixtures/`.
414
+
415
+ ## Status and coverage
416
+
417
+ Implemented: the full keyword set above, including recording and replay,
418
+ visual regression, and the `ext:` registry locator. Planned: an external
419
+ locator-registry plugin API, element-scoped visual assertions, OOPIF
420
+ support via child-target attach, iframe-interior recording.
421
+
422
+ The suite is 403 pytest tests plus 28 Robot acceptance tests, measured
423
+ together at 87% branch coverage. Details, including the gap classification
424
+ and a ledger of bugs found during development, are in
425
+ [docs/coverage.md](docs/coverage.md). An HTML rendering of the same report
426
+ sits at [docs/coverage.html](docs/coverage.html).
427
+
428
+ ## Verified environments
429
+
430
+ | Component | Version |
431
+ | --- | --- |
432
+ | Python | 3.10 or newer; 3.12 verified |
433
+ | Robot Framework | 7.4.2 |
434
+ | websockets | 17.1 |
435
+ | Chrome | Chrome for Testing 131 locally, Chrome stable 152 on CI |
436
+ | Pillow, optional | 12.x |