puppeteer-bidi 0.0.1.beta10 → 0.0.2

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +44 -0
  3. data/API_COVERAGE.md +331 -0
  4. data/CLAUDE/expose_function_implementation.md +271 -0
  5. data/CLAUDE/porting_puppeteer.md +20 -0
  6. data/CLAUDE.md +3 -1
  7. data/DEVELOPMENT.md +14 -0
  8. data/README.md +47 -415
  9. data/development/generate_api_coverage.rb +412 -0
  10. data/development/puppeteer_revision.txt +1 -0
  11. data/lib/puppeteer/bidi/async_utils.rb +25 -9
  12. data/lib/puppeteer/bidi/browser.rb +120 -23
  13. data/lib/puppeteer/bidi/browser_context.rb +185 -2
  14. data/lib/puppeteer/bidi/connection.rb +16 -5
  15. data/lib/puppeteer/bidi/cookie_utils.rb +192 -0
  16. data/lib/puppeteer/bidi/core/browser.rb +3 -0
  17. data/lib/puppeteer/bidi/core/browsing_context.rb +83 -40
  18. data/lib/puppeteer/bidi/core/realm.rb +6 -0
  19. data/lib/puppeteer/bidi/core/request.rb +79 -35
  20. data/lib/puppeteer/bidi/core/session.rb +1 -0
  21. data/lib/puppeteer/bidi/core/user_context.rb +5 -3
  22. data/lib/puppeteer/bidi/element_handle.rb +200 -8
  23. data/lib/puppeteer/bidi/errors.rb +4 -0
  24. data/lib/puppeteer/bidi/exposed_function.rb +390 -0
  25. data/lib/puppeteer/bidi/frame.rb +197 -20
  26. data/lib/puppeteer/bidi/http_request.rb +577 -0
  27. data/lib/puppeteer/bidi/http_response.rb +161 -10
  28. data/lib/puppeteer/bidi/locator.rb +792 -0
  29. data/lib/puppeteer/bidi/page.rb +783 -22
  30. data/lib/puppeteer/bidi/query_handler.rb +1 -1
  31. data/lib/puppeteer/bidi/timeout_settings.rb +23 -6
  32. data/lib/puppeteer/bidi/version.rb +1 -1
  33. data/lib/puppeteer/bidi.rb +40 -6
  34. data/sig/puppeteer/bidi/browser.rbs +53 -6
  35. data/sig/puppeteer/bidi/browser_context.rbs +36 -0
  36. data/sig/puppeteer/bidi/cookie_utils.rbs +64 -0
  37. data/sig/puppeteer/bidi/core/browsing_context.rbs +16 -6
  38. data/sig/puppeteer/bidi/core/request.rbs +14 -11
  39. data/sig/puppeteer/bidi/core/user_context.rbs +2 -2
  40. data/sig/puppeteer/bidi/element_handle.rbs +28 -0
  41. data/sig/puppeteer/bidi/errors.rbs +4 -0
  42. data/sig/puppeteer/bidi/exposed_function.rbs +127 -0
  43. data/sig/puppeteer/bidi/frame.rbs +38 -4
  44. data/sig/puppeteer/bidi/http_request.rbs +162 -0
  45. data/sig/puppeteer/bidi/http_response.rbs +67 -8
  46. data/sig/puppeteer/bidi/locator.rbs +267 -0
  47. data/sig/puppeteer/bidi/page.rbs +209 -8
  48. data/sig/puppeteer/bidi/timeout_settings.rbs +16 -4
  49. data/sig/puppeteer/bidi.rbs +15 -3
  50. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a52c1dcb0d3a791f2754c968a543411c98051380796455da1f0e24404923aa8
4
- data.tar.gz: cd9c3880fe2f8ab49201c681efd39a95a77b477e6445cd620e3cdb3e7a7014d3
3
+ metadata.gz: 7242d594b90af9ed2739ef56e234d3d26805cf19d688212e9b0b68ad823a96c2
4
+ data.tar.gz: 6bca1fe1f82165851376466367cee72ec59f10fabb6b2970a70bcffde5ef42b6
5
5
  SHA512:
6
- metadata.gz: 516d9e6b86a99902e20db1fd43d7d8521fcf8ce6850d72cf253cc6ae5af97f5bd591e2138f0c733d2d96f89f606e45f3d8b6f015ca4730e7b8f75b306dcbd0df
7
- data.tar.gz: a2314ab02834c9ded0abdf5ba85bbae5dcf2252c9dbeaaf81c9f2d6c332d16fc291a6b18d6e36bc470d0585f960fb4cac8aa090467329fa816b47e693df7b024
6
+ metadata.gz: 1c7f73fa6f5351d1e1e12a87388e56c05faa11286bf724e400ec97e19092a81a6e22c6718970c028305717afdaf560ddd16dbfa6f499b5dbc28313b8b55d3347
7
+ data.tar.gz: ad2cc2f58a6da1d6bff87bde5eab283536f46aa160bbc3ac197c6574f9fd5f35950ca47331c9363feea4a93eaa8af76322f38b123e31d9212206d01e01d2d9d4
data/AGENTS.md ADDED
@@ -0,0 +1,44 @@
1
+ # Repository Guidelines
2
+
3
+ ## Start Here (Project-Specific Guidance)
4
+
5
+ - Read `CLAUDE.md` and `CLAUDE/` first; they define the core async architecture, porting workflow, and testing strategy.
6
+ - Ruby: requires `>= 3.2` (CI covers 3.2–3.4). Prefer a version manager (`rbenv`, `asdf`, etc.) over system Ruby.
7
+
8
+ ## Project Structure & Module Organization
9
+
10
+ - `lib/puppeteer/bidi/`: user-facing API (sync, calls `.wait`).
11
+ - `lib/puppeteer/bidi/core/`: low-level BiDi core (async, returns `Async::Task`).
12
+ - `spec/integration/`: browser-driven specs; fixtures in `spec/assets/`.
13
+
14
+ ## Build, Test, and Development Commands
15
+
16
+ - See `DEVELOPMENT.md` for the full command list and environment variables.
17
+ - Run RSpec via `rbenv exec bundle exec rspec ...` when using rbenv.
18
+
19
+ ## Coding Style & Naming Conventions
20
+
21
+ - Ruby: 2-space indent, double-quoted strings, ~120 char lines; follow RuboCop (`.rubocop.yml`).
22
+ - BiDi-only: do not introduce CDP-related ports.
23
+ - Async: core returns `Async::Task`; upper layer must call `.wait` on every core call (see `CLAUDE/two_layer_architecture.md`).
24
+ - Reviews to watch: WS messages can be handled out-of-order; “wait for event” code must not hang (register listeners before commands, handle “already happened”, cancel on errors).
25
+
26
+ ## Testing Guidelines
27
+
28
+ - Prefer `spec/integration/` and the shared-browser `with_test_state` pattern (see `CLAUDE/testing_strategy.md`).
29
+ - Use `pending` for browser limitations vs `skip` for unimplemented features.
30
+
31
+ ## Commit & Pull Request Guidelines
32
+
33
+ - See `DEVELOPMENT.md` for commit, PR, and release conventions.
34
+
35
+ ## Agent Notes (Porting/Review)
36
+
37
+ - When porting from upstream TS, mirror optional vs default fields: defaults are for validation, and optional keys should be omitted from payloads unless explicitly provided.
38
+ - Match upstream error messages as closely as possible (including interpolated values) so tests align with Puppeteer.
39
+ - In core layer option checks, use key presence (`options.key?`) when upstream uses `'in'` to distinguish "missing" from `nil`.
40
+ - During reviews, compare both implementation (`packages/puppeteer-core/src/bidi/*.ts`) and tests (`test/src/page.spec.ts`) to catch behavior parity gaps.
41
+
42
+ ## Security & Configuration Tips
43
+
44
+ - Do not commit credentials; review network-fetched changes (e.g., `scripts/update_injected_source.rb` → `lib/puppeteer/bidi/injected.js`) carefully.
data/API_COVERAGE.md ADDED
@@ -0,0 +1,331 @@
1
+ # API Coverage
2
+
3
+ - Puppeteer commit: `7d750c25cb29764f2fb31cb90b750a8eec350199`
4
+ - Generated by: `development/generate_api_coverage.rb`
5
+ - Coverage: `182/265` (`68.68%`)
6
+
7
+ ## Browser (Puppeteer::Bidi::Browser)
8
+
9
+ | Node.js | Ruby | Supported |
10
+ | --- | --- | :---: |
11
+ | `Browser.addScreen` | `Puppeteer::Bidi::Browser#add_screen` | ❌ |
12
+ | `Browser.browserContexts` | `Puppeteer::Bidi::Browser#browser_contexts` | ❌ |
13
+ | `Browser.close` | `Puppeteer::Bidi::Browser#close` | ✅ |
14
+ | `Browser.cookies` | `Puppeteer::Bidi::Browser#cookies` | ✅ |
15
+ | `Browser.createBrowserContext` | `Puppeteer::Bidi::Browser#create_browser_context` | ✅ |
16
+ | `Browser.defaultBrowserContext` | `Puppeteer::Bidi::Browser#default_browser_context` | ✅ |
17
+ | `Browser.deleteCookie` | `Puppeteer::Bidi::Browser#delete_cookie` | ✅ |
18
+ | `Browser.deleteMatchingCookies` | `Puppeteer::Bidi::Browser#delete_matching_cookies` | ✅ |
19
+ | `Browser.disconnect` | `Puppeteer::Bidi::Browser#disconnect` | ✅ |
20
+ | `Browser.getWindowBounds` | `Puppeteer::Bidi::Browser#get_window_bounds` | ❌ |
21
+ | `Browser.installExtension` | `Puppeteer::Bidi::Browser#install_extension` | ❌ |
22
+ | `Browser.isConnected` | `Puppeteer::Bidi::Browser#is_connected` | ❌ |
23
+ | `Browser.newPage` | `Puppeteer::Bidi::Browser#new_page` | ✅ |
24
+ | `Browser.pages` | `Puppeteer::Bidi::Browser#pages` | ✅ |
25
+ | `Browser.process` | `Puppeteer::Bidi::Browser#process` | ✅ |
26
+ | `Browser.removeScreen` | `Puppeteer::Bidi::Browser#remove_screen` | ❌ |
27
+ | `Browser.screens` | `Puppeteer::Bidi::Browser#screens` | ❌ |
28
+ | `Browser.setCookie` | `Puppeteer::Bidi::Browser#set_cookie` | ✅ |
29
+ | `Browser.setWindowBounds` | `Puppeteer::Bidi::Browser#set_window_bounds` | ❌ |
30
+ | `Browser.target` | `Puppeteer::Bidi::Browser#target` | ❌ |
31
+ | `Browser.targets` | `Puppeteer::Bidi::Browser#targets` | ❌ |
32
+ | `Browser.uninstallExtension` | `Puppeteer::Bidi::Browser#uninstall_extension` | ❌ |
33
+ | `Browser.userAgent` | `Puppeteer::Bidi::Browser#user_agent` | ✅ |
34
+ | `Browser.version` | `Puppeteer::Bidi::Browser#version` | ❌ |
35
+ | `Browser.waitForTarget` | `Puppeteer::Bidi::Browser#wait_for_target` | ✅ |
36
+ | `Browser.wsEndpoint` | `Puppeteer::Bidi::Browser#ws_endpoint` | ✅ |
37
+
38
+ ## BrowserContext (Puppeteer::Bidi::BrowserContext)
39
+
40
+ | Node.js | Ruby | Supported |
41
+ | --- | --- | :---: |
42
+ | `BrowserContext.browser` | `Puppeteer::Bidi::BrowserContext#browser` | ✅ |
43
+ | `BrowserContext.clearPermissionOverrides` | `Puppeteer::Bidi::BrowserContext#clear_permission_overrides` | ❌ |
44
+ | `BrowserContext.close` | `Puppeteer::Bidi::BrowserContext#close` | ✅ |
45
+ | `BrowserContext.cookies` | `Puppeteer::Bidi::BrowserContext#cookies` | ✅ |
46
+ | `BrowserContext.deleteCookie` | `Puppeteer::Bidi::BrowserContext#delete_cookie` | ✅ |
47
+ | `BrowserContext.deleteMatchingCookies` | `Puppeteer::Bidi::BrowserContext#delete_matching_cookies` | ✅ |
48
+ | `BrowserContext.newPage` | `Puppeteer::Bidi::BrowserContext#new_page` | ✅ |
49
+ | `BrowserContext.overridePermissions` | `Puppeteer::Bidi::BrowserContext#override_permissions` | ✅ |
50
+ | `BrowserContext.pages` | `Puppeteer::Bidi::BrowserContext#pages` | ✅ |
51
+ | `BrowserContext.setCookie` | `Puppeteer::Bidi::BrowserContext#set_cookie` | ✅ |
52
+ | `BrowserContext.targets` | `Puppeteer::Bidi::BrowserContext#targets` | ❌ |
53
+ | `BrowserContext.waitForTarget` | `Puppeteer::Bidi::BrowserContext#wait_for_target` | ❌ |
54
+
55
+ ## ElementHandle (Puppeteer::Bidi::ElementHandle)
56
+
57
+ | Node.js | Ruby | Supported |
58
+ | --- | --- | :---: |
59
+ | `ElementHandle.$` | `Puppeteer::Bidi::ElementHandle#query_selector` | ✅ |
60
+ | `ElementHandle.$$` | `Puppeteer::Bidi::ElementHandle#query_selector_all` | ✅ |
61
+ | `ElementHandle.$$eval` | `Puppeteer::Bidi::ElementHandle#eval_on_selector_all` | ✅ |
62
+ | `ElementHandle.$eval` | `Puppeteer::Bidi::ElementHandle#eval_on_selector` | ✅ |
63
+ | `ElementHandle.asLocator` | `Puppeteer::Bidi::ElementHandle#as_locator` | ✅ |
64
+ | `ElementHandle.autofill` | `Puppeteer::Bidi::ElementHandle#autofill` | ❌ |
65
+ | `ElementHandle.backendNodeId` | `Puppeteer::Bidi::ElementHandle#backend_node_id` | ❌ |
66
+ | `ElementHandle.boundingBox` | `Puppeteer::Bidi::ElementHandle#bounding_box` | ✅ |
67
+ | `ElementHandle.boxModel` | `Puppeteer::Bidi::ElementHandle#box_model` | ✅ |
68
+ | `ElementHandle.click` | `Puppeteer::Bidi::ElementHandle#click` | ✅ |
69
+ | `ElementHandle.clickablePoint` | `Puppeteer::Bidi::ElementHandle#clickable_point` | ✅ |
70
+ | `ElementHandle.contentFrame` | `Puppeteer::Bidi::ElementHandle#content_frame` | ✅ |
71
+ | `ElementHandle.drag` | `Puppeteer::Bidi::ElementHandle#drag` | ❌ |
72
+ | `ElementHandle.dragAndDrop` | `Puppeteer::Bidi::ElementHandle#drag_and_drop` | ❌ |
73
+ | `ElementHandle.dragEnter` | `Puppeteer::Bidi::ElementHandle#drag_enter` | ❌ |
74
+ | `ElementHandle.dragOver` | `Puppeteer::Bidi::ElementHandle#drag_over` | ❌ |
75
+ | `ElementHandle.drop` | `Puppeteer::Bidi::ElementHandle#drop` | ❌ |
76
+ | `ElementHandle.focus` | `Puppeteer::Bidi::ElementHandle#focus` | ✅ |
77
+ | `ElementHandle.hover` | `Puppeteer::Bidi::ElementHandle#hover` | ✅ |
78
+ | `ElementHandle.isHidden` | `Puppeteer::Bidi::ElementHandle#hidden?` | ✅ |
79
+ | `ElementHandle.isIntersectingViewport` | `Puppeteer::Bidi::ElementHandle#intersecting_viewport?` | ✅ |
80
+ | `ElementHandle.isVisible` | `Puppeteer::Bidi::ElementHandle#visible?` | ✅ |
81
+ | `ElementHandle.press` | `Puppeteer::Bidi::ElementHandle#press` | ✅ |
82
+ | `ElementHandle.screenshot` | `Puppeteer::Bidi::ElementHandle#screenshot` | ✅ |
83
+ | `ElementHandle.scrollIntoView` | `Puppeteer::Bidi::ElementHandle#scroll_into_view` | ✅ |
84
+ | `ElementHandle.select` | `Puppeteer::Bidi::ElementHandle#select` | ✅ |
85
+ | `ElementHandle.tap` | `Puppeteer::Bidi::ElementHandle#tap` | ❌ |
86
+ | `ElementHandle.toElement` | `Puppeteer::Bidi::ElementHandle#to_element` | ✅ |
87
+ | `ElementHandle.touchEnd` | `Puppeteer::Bidi::ElementHandle#touch_end` | ❌ |
88
+ | `ElementHandle.touchMove` | `Puppeteer::Bidi::ElementHandle#touch_move` | ❌ |
89
+ | `ElementHandle.touchStart` | `Puppeteer::Bidi::ElementHandle#touch_start` | ❌ |
90
+ | `ElementHandle.type` | `Puppeteer::Bidi::ElementHandle#type` | ✅ |
91
+ | `ElementHandle.uploadFile` | `Puppeteer::Bidi::ElementHandle#upload_file` | ✅ |
92
+ | `ElementHandle.waitForSelector` | `Puppeteer::Bidi::ElementHandle#wait_for_selector` | ✅ |
93
+
94
+ ## FileChooser (Puppeteer::Bidi::FileChooser)
95
+
96
+ | Node.js | Ruby | Supported |
97
+ | --- | --- | :---: |
98
+ | `FileChooser.accept` | `Puppeteer::Bidi::FileChooser#accept` | ✅ |
99
+ | `FileChooser.cancel` | `Puppeteer::Bidi::FileChooser#cancel` | ✅ |
100
+ | `FileChooser.isMultiple` | `Puppeteer::Bidi::FileChooser#multiple?` | ✅ |
101
+
102
+ ## Frame (Puppeteer::Bidi::Frame)
103
+
104
+ | Node.js | Ruby | Supported |
105
+ | --- | --- | :---: |
106
+ | `Frame.$` | `Puppeteer::Bidi::Frame#query_selector` | ✅ |
107
+ | `Frame.$$` | `Puppeteer::Bidi::Frame#query_selector_all` | ✅ |
108
+ | `Frame.$$eval` | `Puppeteer::Bidi::Frame#eval_on_selector_all` | ✅ |
109
+ | `Frame.$eval` | `Puppeteer::Bidi::Frame#eval_on_selector` | ✅ |
110
+ | `Frame.addScriptTag` | `Puppeteer::Bidi::Frame#add_script_tag` | ❌ |
111
+ | `Frame.addStyleTag` | `Puppeteer::Bidi::Frame#add_style_tag` | ❌ |
112
+ | `Frame.childFrames` | `Puppeteer::Bidi::Frame#child_frames` | ✅ |
113
+ | `Frame.click` | `Puppeteer::Bidi::Frame#click` | ✅ |
114
+ | `Frame.content` | `Puppeteer::Bidi::Frame#content` | ✅ |
115
+ | `Frame.evaluate` | `Puppeteer::Bidi::Frame#evaluate` | ✅ |
116
+ | `Frame.evaluateHandle` | `Puppeteer::Bidi::Frame#evaluate_handle` | ✅ |
117
+ | `Frame.focus` | `Puppeteer::Bidi::Frame#focus` | ❌ |
118
+ | `Frame.frameElement` | `Puppeteer::Bidi::Frame#frame_element` | ✅ |
119
+ | `Frame.goto` | `Puppeteer::Bidi::Frame#goto` | ✅ |
120
+ | `Frame.hover` | `Puppeteer::Bidi::Frame#hover` | ✅ |
121
+ | `Frame.isDetached` | `Puppeteer::Bidi::Frame#detached?` | ✅ |
122
+ | `Frame.locator` | `Puppeteer::Bidi::Frame#locator` | ✅ |
123
+ | `Frame.name` | `Puppeteer::Bidi::Frame#name` | ✅ |
124
+ | `Frame.page` | `Puppeteer::Bidi::Frame#page` | ✅ |
125
+ | `Frame.parentFrame` | `Puppeteer::Bidi::Frame#parent_frame` | ✅ |
126
+ | `Frame.select` | `Puppeteer::Bidi::Frame#select` | ✅ |
127
+ | `Frame.setContent` | `Puppeteer::Bidi::Frame#set_content` | ✅ |
128
+ | `Frame.tap` | `Puppeteer::Bidi::Frame#tap` | ❌ |
129
+ | `Frame.title` | `Puppeteer::Bidi::Frame#title` | ❌ |
130
+ | `Frame.type` | `Puppeteer::Bidi::Frame#type` | ✅ |
131
+ | `Frame.url` | `Puppeteer::Bidi::Frame#url` | ✅ |
132
+ | `Frame.waitForFunction` | `Puppeteer::Bidi::Frame#wait_for_function` | ✅ |
133
+ | `Frame.waitForNavigation` | `Puppeteer::Bidi::Frame#wait_for_navigation` | ✅ |
134
+ | `Frame.waitForSelector` | `Puppeteer::Bidi::Frame#wait_for_selector` | ✅ |
135
+
136
+ ## HTTPRequest (Puppeteer::Bidi::HTTPRequest)
137
+
138
+ | Node.js | Ruby | Supported |
139
+ | --- | --- | :---: |
140
+ | `HTTPRequest.abort` | `Puppeteer::Bidi::HTTPRequest#abort` | ✅ |
141
+ | `HTTPRequest.abortErrorReason` | `Puppeteer::Bidi::HTTPRequest#abort_error_reason` | ✅ |
142
+ | `HTTPRequest.continue` | `Puppeteer::Bidi::HTTPRequest#continue` | ✅ |
143
+ | `HTTPRequest.continueRequestOverrides` | `Puppeteer::Bidi::HTTPRequest#continue_request_overrides` | ✅ |
144
+ | `HTTPRequest.enqueueInterceptAction` | `Puppeteer::Bidi::HTTPRequest#enqueue_intercept_action` | ✅ |
145
+ | `HTTPRequest.failure` | `Puppeteer::Bidi::HTTPRequest#failure` | ✅ |
146
+ | `HTTPRequest.fetchPostData` | `Puppeteer::Bidi::HTTPRequest#fetch_post_data` | ✅ |
147
+ | `HTTPRequest.finalizeInterceptions` | `Puppeteer::Bidi::HTTPRequest#finalize_interceptions` | ✅ |
148
+ | `HTTPRequest.frame` | `Puppeteer::Bidi::HTTPRequest#frame` | ✅ |
149
+ | `HTTPRequest.hasPostData` | `Puppeteer::Bidi::HTTPRequest#has_post_data` | ❌ |
150
+ | `HTTPRequest.headers` | `Puppeteer::Bidi::HTTPRequest#headers` | ✅ |
151
+ | `HTTPRequest.initiator` | `Puppeteer::Bidi::HTTPRequest#initiator` | ✅ |
152
+ | `HTTPRequest.interceptResolutionState` | `Puppeteer::Bidi::HTTPRequest#intercept_resolution_state` | ✅ |
153
+ | `HTTPRequest.isInterceptResolutionHandled` | `Puppeteer::Bidi::HTTPRequest#intercept_resolution_handled?` | ✅ |
154
+ | `HTTPRequest.isNavigationRequest` | `Puppeteer::Bidi::HTTPRequest#navigation_request?` | ✅ |
155
+ | `HTTPRequest.method` | `Puppeteer::Bidi::HTTPRequest#method` | ✅ |
156
+ | `HTTPRequest.postData` | `Puppeteer::Bidi::HTTPRequest#post_data` | ✅ |
157
+ | `HTTPRequest.redirectChain` | `Puppeteer::Bidi::HTTPRequest#redirect_chain` | ✅ |
158
+ | `HTTPRequest.resourceType` | `Puppeteer::Bidi::HTTPRequest#resource_type` | ✅ |
159
+ | `HTTPRequest.respond` | `Puppeteer::Bidi::HTTPRequest#respond` | ✅ |
160
+ | `HTTPRequest.response` | `Puppeteer::Bidi::HTTPRequest#response` | ✅ |
161
+ | `HTTPRequest.responseForRequest` | `Puppeteer::Bidi::HTTPRequest#response_for_request` | ✅ |
162
+ | `HTTPRequest.url` | `Puppeteer::Bidi::HTTPRequest#url` | ✅ |
163
+
164
+ ## HTTPResponse (Puppeteer::Bidi::HTTPResponse)
165
+
166
+ | Node.js | Ruby | Supported |
167
+ | --- | --- | :---: |
168
+ | `HTTPResponse.buffer` | `Puppeteer::Bidi::HTTPResponse#buffer` | ✅ |
169
+ | `HTTPResponse.content` | `Puppeteer::Bidi::HTTPResponse#content` | ✅ |
170
+ | `HTTPResponse.frame` | `Puppeteer::Bidi::HTTPResponse#frame` | ✅ |
171
+ | `HTTPResponse.fromCache` | `Puppeteer::Bidi::HTTPResponse#from_cache` | ❌ |
172
+ | `HTTPResponse.fromServiceWorker` | `Puppeteer::Bidi::HTTPResponse#from_service_worker` | ❌ |
173
+ | `HTTPResponse.headers` | `Puppeteer::Bidi::HTTPResponse#headers` | ✅ |
174
+ | `HTTPResponse.json` | `Puppeteer::Bidi::HTTPResponse#json` | ✅ |
175
+ | `HTTPResponse.ok` | `Puppeteer::Bidi::HTTPResponse#ok` | ❌ |
176
+ | `HTTPResponse.remoteAddress` | `Puppeteer::Bidi::HTTPResponse#remote_address` | ✅ |
177
+ | `HTTPResponse.request` | `Puppeteer::Bidi::HTTPResponse#request` | ✅ |
178
+ | `HTTPResponse.securityDetails` | `Puppeteer::Bidi::HTTPResponse#security_details` | ✅ |
179
+ | `HTTPResponse.status` | `Puppeteer::Bidi::HTTPResponse#status` | ✅ |
180
+ | `HTTPResponse.statusText` | `Puppeteer::Bidi::HTTPResponse#status_text` | ✅ |
181
+ | `HTTPResponse.text` | `Puppeteer::Bidi::HTTPResponse#text` | ✅ |
182
+ | `HTTPResponse.timing` | `Puppeteer::Bidi::HTTPResponse#timing` | ✅ |
183
+ | `HTTPResponse.url` | `Puppeteer::Bidi::HTTPResponse#url` | ✅ |
184
+
185
+ ## JSHandle (Puppeteer::Bidi::JSHandle)
186
+
187
+ | Node.js | Ruby | Supported |
188
+ | --- | --- | :---: |
189
+ | `JSHandle.asElement` | `Puppeteer::Bidi::JSHandle#as_element` | ✅ |
190
+ | `JSHandle.dispose` | `Puppeteer::Bidi::JSHandle#dispose` | ✅ |
191
+ | `JSHandle.evaluate` | `Puppeteer::Bidi::JSHandle#evaluate` | ✅ |
192
+ | `JSHandle.evaluateHandle` | `Puppeteer::Bidi::JSHandle#evaluate_handle` | ✅ |
193
+ | `JSHandle.getProperties` | `Puppeteer::Bidi::JSHandle#get_properties` | ✅ |
194
+ | `JSHandle.getProperty` | `Puppeteer::Bidi::JSHandle#get_property` | ✅ |
195
+ | `JSHandle.jsonValue` | `Puppeteer::Bidi::JSHandle#json_value` | ✅ |
196
+ | `JSHandle.remoteObject` | `Puppeteer::Bidi::JSHandle#remote_object` | ✅ |
197
+ | `JSHandle.toString` | `Puppeteer::Bidi::JSHandle#to_string` | ❌ |
198
+
199
+ ## Keyboard (Puppeteer::Bidi::Keyboard)
200
+
201
+ | Node.js | Ruby | Supported |
202
+ | --- | --- | :---: |
203
+ | `Keyboard.down` | `Puppeteer::Bidi::Keyboard#down` | ✅ |
204
+ | `Keyboard.press` | `Puppeteer::Bidi::Keyboard#press` | ✅ |
205
+ | `Keyboard.sendCharacter` | `Puppeteer::Bidi::Keyboard#send_character` | ✅ |
206
+ | `Keyboard.type` | `Puppeteer::Bidi::Keyboard#type` | ✅ |
207
+ | `Keyboard.up` | `Puppeteer::Bidi::Keyboard#up` | ✅ |
208
+
209
+ ## Mouse (Puppeteer::Bidi::Mouse)
210
+
211
+ | Node.js | Ruby | Supported |
212
+ | --- | --- | :---: |
213
+ | `Mouse.click` | `Puppeteer::Bidi::Mouse#click` | ✅ |
214
+ | `Mouse.down` | `Puppeteer::Bidi::Mouse#down` | ✅ |
215
+ | `Mouse.drag` | `Puppeteer::Bidi::Mouse#drag` | ❌ |
216
+ | `Mouse.dragAndDrop` | `Puppeteer::Bidi::Mouse#drag_and_drop` | ❌ |
217
+ | `Mouse.dragEnter` | `Puppeteer::Bidi::Mouse#drag_enter` | ❌ |
218
+ | `Mouse.dragOver` | `Puppeteer::Bidi::Mouse#drag_over` | ❌ |
219
+ | `Mouse.drop` | `Puppeteer::Bidi::Mouse#drop` | ❌ |
220
+ | `Mouse.move` | `Puppeteer::Bidi::Mouse#move` | ✅ |
221
+ | `Mouse.reset` | `Puppeteer::Bidi::Mouse#reset` | ✅ |
222
+ | `Mouse.up` | `Puppeteer::Bidi::Mouse#up` | ✅ |
223
+ | `Mouse.wheel` | `Puppeteer::Bidi::Mouse#wheel` | ✅ |
224
+
225
+ ## Page (Puppeteer::Bidi::Page)
226
+
227
+ | Node.js | Ruby | Supported |
228
+ | --- | --- | :---: |
229
+ | `Page.$` | `Puppeteer::Bidi::Page#query_selector` | ✅ |
230
+ | `Page.$$` | `Puppeteer::Bidi::Page#query_selector_all` | ✅ |
231
+ | `Page.$$eval` | `Puppeteer::Bidi::Page#eval_on_selector_all` | ✅ |
232
+ | `Page.$eval` | `Puppeteer::Bidi::Page#eval_on_selector` | ✅ |
233
+ | `Page.addScriptTag` | `Puppeteer::Bidi::Page#add_script_tag` | ❌ |
234
+ | `Page.addStyleTag` | `Puppeteer::Bidi::Page#add_style_tag` | ❌ |
235
+ | `Page.authenticate` | `Puppeteer::Bidi::Page#authenticate` | ✅ |
236
+ | `Page.bringToFront` | `Puppeteer::Bidi::Page#bring_to_front` | ❌ |
237
+ | `Page.browser` | `Puppeteer::Bidi::Page#browser` | ❌ |
238
+ | `Page.browserContext` | `Puppeteer::Bidi::Page#browser_context` | ✅ |
239
+ | `Page.click` | `Puppeteer::Bidi::Page#click` | ✅ |
240
+ | `Page.close` | `Puppeteer::Bidi::Page#close` | ✅ |
241
+ | `Page.content` | `Puppeteer::Bidi::Page#content` | ✅ |
242
+ | `Page.cookies` | `Puppeteer::Bidi::Page#cookies` | ✅ |
243
+ | `Page.createCDPSession` | `Puppeteer::Bidi::Page#create_cdp_session` | ❌ |
244
+ | `Page.createPDFStream` | `Puppeteer::Bidi::Page#create_pdf_stream` | ❌ |
245
+ | `Page.deleteCookie` | `Puppeteer::Bidi::Page#delete_cookie` | ✅ |
246
+ | `Page.emulate` | `Puppeteer::Bidi::Page#emulate` | ❌ |
247
+ | `Page.emulateCPUThrottling` | `Puppeteer::Bidi::Page#emulate_cpu_throttling` | ❌ |
248
+ | `Page.emulateFocusedPage` | `Puppeteer::Bidi::Page#emulate_focused_page` | ❌ |
249
+ | `Page.emulateIdleState` | `Puppeteer::Bidi::Page#emulate_idle_state` | ❌ |
250
+ | `Page.emulateMediaFeatures` | `Puppeteer::Bidi::Page#emulate_media_features` | ❌ |
251
+ | `Page.emulateMediaType` | `Puppeteer::Bidi::Page#emulate_media_type` | ❌ |
252
+ | `Page.emulateNetworkConditions` | `Puppeteer::Bidi::Page#emulate_network_conditions` | ❌ |
253
+ | `Page.emulateTimezone` | `Puppeteer::Bidi::Page#emulate_timezone` | ❌ |
254
+ | `Page.emulateVisionDeficiency` | `Puppeteer::Bidi::Page#emulate_vision_deficiency` | ❌ |
255
+ | `Page.evaluate` | `Puppeteer::Bidi::Page#evaluate` | ✅ |
256
+ | `Page.evaluateHandle` | `Puppeteer::Bidi::Page#evaluate_handle` | ✅ |
257
+ | `Page.evaluateOnNewDocument` | `Puppeteer::Bidi::Page#evaluate_on_new_document` | ✅ |
258
+ | `Page.exposeFunction` | `Puppeteer::Bidi::Page#expose_function` | ✅ |
259
+ | `Page.focus` | `Puppeteer::Bidi::Page#focus` | ✅ |
260
+ | `Page.frames` | `Puppeteer::Bidi::Page#frames` | ✅ |
261
+ | `Page.getDefaultNavigationTimeout` | `Puppeteer::Bidi::Page#get_default_navigation_timeout` | ✅ |
262
+ | `Page.getDefaultTimeout` | `Puppeteer::Bidi::Page#get_default_timeout` | ✅ |
263
+ | `Page.goBack` | `Puppeteer::Bidi::Page#go_back` | ✅ |
264
+ | `Page.goForward` | `Puppeteer::Bidi::Page#go_forward` | ✅ |
265
+ | `Page.goto` | `Puppeteer::Bidi::Page#goto` | ✅ |
266
+ | `Page.hover` | `Puppeteer::Bidi::Page#hover` | ✅ |
267
+ | `Page.isClosed` | `Puppeteer::Bidi::Page#closed?` | ✅ |
268
+ | `Page.isDragInterceptionEnabled` | `Puppeteer::Bidi::Page#is_drag_interception_enabled` | ❌ |
269
+ | `Page.isJavaScriptEnabled` | `Puppeteer::Bidi::Page#is_java_script_enabled` | ❌ |
270
+ | `Page.isServiceWorkerBypassed` | `Puppeteer::Bidi::Page#is_service_worker_bypassed` | ❌ |
271
+ | `Page.locator` | `Puppeteer::Bidi::Page#locator` | ✅ |
272
+ | `Page.mainFrame` | `Puppeteer::Bidi::Page#main_frame` | ✅ |
273
+ | `Page.metrics` | `Puppeteer::Bidi::Page#metrics` | ❌ |
274
+ | `Page.openDevTools` | `Puppeteer::Bidi::Page#open_dev_tools` | ❌ |
275
+ | `Page.pdf` | `Puppeteer::Bidi::Page#pdf` | ❌ |
276
+ | `Page.queryObjects` | `Puppeteer::Bidi::Page#query_objects` | ❌ |
277
+ | `Page.reload` | `Puppeteer::Bidi::Page#reload` | ✅ |
278
+ | `Page.removeExposedFunction` | `Puppeteer::Bidi::Page#remove_exposed_function` | ✅ |
279
+ | `Page.removeScriptToEvaluateOnNewDocument` | `Puppeteer::Bidi::Page#remove_script_to_evaluate_on_new_document` | ✅ |
280
+ | `Page.resize` | `Puppeteer::Bidi::Page#resize` | ❌ |
281
+ | `Page.screencast` | `Puppeteer::Bidi::Page#screencast` | ❌ |
282
+ | `Page.screenshot` | `Puppeteer::Bidi::Page#screenshot` | ✅ |
283
+ | `Page.select` | `Puppeteer::Bidi::Page#select` | ✅ |
284
+ | `Page.setBypassCSP` | `Puppeteer::Bidi::Page#set_bypass_csp` | ❌ |
285
+ | `Page.setBypassServiceWorker` | `Puppeteer::Bidi::Page#set_bypass_service_worker` | ❌ |
286
+ | `Page.setCacheEnabled` | `Puppeteer::Bidi::Page#set_cache_enabled` | ✅ |
287
+ | `Page.setContent` | `Puppeteer::Bidi::Page#set_content` | ✅ |
288
+ | `Page.setCookie` | `Puppeteer::Bidi::Page#set_cookie` | ✅ |
289
+ | `Page.setDefaultNavigationTimeout` | `Puppeteer::Bidi::Page#set_default_navigation_timeout` | ✅ |
290
+ | `Page.setDefaultTimeout` | `Puppeteer::Bidi::Page#set_default_timeout` | ✅ |
291
+ | `Page.setDragInterception` | `Puppeteer::Bidi::Page#set_drag_interception` | ❌ |
292
+ | `Page.setExtraHTTPHeaders` | `Puppeteer::Bidi::Page#set_extra_http_headers` | ✅ |
293
+ | `Page.setGeolocation` | `Puppeteer::Bidi::Page#set_geolocation` | ✅ |
294
+ | `Page.setJavaScriptEnabled` | `Puppeteer::Bidi::Page#set_java_script_enabled` | ❌ |
295
+ | `Page.setOfflineMode` | `Puppeteer::Bidi::Page#set_offline_mode` | ❌ |
296
+ | `Page.setRequestInterception` | `Puppeteer::Bidi::Page#set_request_interception` | ✅ |
297
+ | `Page.setUserAgent` | `Puppeteer::Bidi::Page#set_user_agent` | ✅ |
298
+ | `Page.setViewport` | `Puppeteer::Bidi::Page#set_viewport` | ✅ |
299
+ | `Page.tap` | `Puppeteer::Bidi::Page#tap` | ❌ |
300
+ | `Page.target` | `Puppeteer::Bidi::Page#target` | ❌ |
301
+ | `Page.title` | `Puppeteer::Bidi::Page#title` | ✅ |
302
+ | `Page.type` | `Puppeteer::Bidi::Page#type` | ✅ |
303
+ | `Page.url` | `Puppeteer::Bidi::Page#url` | ✅ |
304
+ | `Page.viewport` | `Puppeteer::Bidi::Page#viewport` | ✅ |
305
+ | `Page.waitForDevicePrompt` | `Puppeteer::Bidi::Page#wait_for_device_prompt` | ❌ |
306
+ | `Page.waitForFileChooser` | `Puppeteer::Bidi::Page#wait_for_file_chooser` | ✅ |
307
+ | `Page.waitForFrame` | `Puppeteer::Bidi::Page#wait_for_frame` | ❌ |
308
+ | `Page.waitForFunction` | `Puppeteer::Bidi::Page#wait_for_function` | ✅ |
309
+ | `Page.waitForNavigation` | `Puppeteer::Bidi::Page#wait_for_navigation` | ✅ |
310
+ | `Page.waitForNetworkIdle` | `Puppeteer::Bidi::Page#wait_for_network_idle` | ✅ |
311
+ | `Page.waitForRequest` | `Puppeteer::Bidi::Page#wait_for_request` | ✅ |
312
+ | `Page.waitForResponse` | `Puppeteer::Bidi::Page#wait_for_response` | ✅ |
313
+ | `Page.waitForSelector` | `Puppeteer::Bidi::Page#wait_for_selector` | ✅ |
314
+ | `Page.windowId` | `Puppeteer::Bidi::Page#window_id` | ❌ |
315
+ | `Page.workers` | `Puppeteer::Bidi::Page#workers` | ❌ |
316
+
317
+ ## Puppeteer (Puppeteer::Bidi)
318
+
319
+ | Node.js | Ruby | Supported |
320
+ | --- | --- | :---: |
321
+ | `Puppeteer.clearCustomQueryHandlers` | `Puppeteer::Bidi.clear_custom_query_handlers` | ❌ |
322
+ | `Puppeteer.connect` | `Puppeteer::Bidi.connect` | ✅ |
323
+ | `PuppeteerNode.connect` | `Puppeteer::Bidi.connect` | ✅ |
324
+ | `Puppeteer.customQueryHandlerNames` | `Puppeteer::Bidi.custom_query_handler_names` | ❌ |
325
+ | `PuppeteerNode.defaultArgs` | `Puppeteer::Bidi.default_args` | ❌ |
326
+ | `PuppeteerNode.executablePath` | `Puppeteer::Bidi.executable_path` | ❌ |
327
+ | `PuppeteerNode.launch` | `Puppeteer::Bidi.launch` | ✅ |
328
+ | `Puppeteer.registerCustomQueryHandler` | `Puppeteer::Bidi.register_custom_query_handler` | ❌ |
329
+ | `PuppeteerNode.trimCache` | `Puppeteer::Bidi.trim_cache` | ❌ |
330
+ | `Puppeteer.unregisterCustomQueryHandler` | `Puppeteer::Bidi.unregister_custom_query_handler` | ❌ |
331
+
@@ -0,0 +1,271 @@
1
+ # ExposeFunction and EvaluateOnNewDocument Implementation
2
+
3
+ This document details the implementation of `Page.evaluateOnNewDocument` and `Page.exposeFunction`, which use BiDi preload scripts and `script.message` channel for communication.
4
+
5
+ ## Page.evaluateOnNewDocument
6
+
7
+ Injects JavaScript to be evaluated before any page scripts run.
8
+
9
+ ### BiDi Implementation
10
+
11
+ Uses `script.addPreloadScript` command:
12
+
13
+ ```ruby
14
+ def evaluate_on_new_document(page_function, *args)
15
+ expression = build_evaluation_expression(page_function, *args)
16
+ script_id = @browsing_context.add_preload_script(expression).wait
17
+ NewDocumentScriptEvaluation.new(script_id)
18
+ end
19
+ ```
20
+
21
+ ### Key Points
22
+
23
+ 1. **Preload Scripts Persist**: Scripts added via `addPreloadScript` run on every navigation
24
+ 2. **Argument Serialization**: Arguments are serialized into the script as JSON literals
25
+ 3. **Return Value**: Returns a `NewDocumentScriptEvaluation` with the script ID for later removal
26
+
27
+ ### Example
28
+
29
+ ```ruby
30
+ # Inject code that runs before any page scripts
31
+ script = page.evaluate_on_new_document("window.injected = 123")
32
+ page.goto(server.empty_page)
33
+ result = page.evaluate("window.injected") # => 123
34
+
35
+ # Remove when done
36
+ page.remove_script_to_evaluate_on_new_document(script.identifier)
37
+ ```
38
+
39
+ ## Page.exposeFunction
40
+
41
+ Exposes a Ruby callable as a JavaScript function on the page.
42
+
43
+ ### BiDi Implementation
44
+
45
+ Uses `script.message` channel for bidirectional communication:
46
+
47
+ ```
48
+ Page (JS) Ruby (ExposedFunction)
49
+ | |
50
+ |-- callback([resolve,reject,args]) -->|
51
+ | |
52
+ |<-- resolve(result) or reject(error) -|
53
+ ```
54
+
55
+ ### Key Components
56
+
57
+ #### 1. Channel Argument Pattern
58
+
59
+ BiDi uses a special `channel` argument type:
60
+
61
+ ```ruby
62
+ def channel_argument
63
+ {
64
+ type: "channel",
65
+ value: {
66
+ channel: @channel, # Unique channel ID
67
+ ownership: "root" # Keep handles alive
68
+ }
69
+ }
70
+ end
71
+ ```
72
+
73
+ #### 2. Function Declaration
74
+
75
+ The exposed function creates a Promise that waits for the Ruby callback:
76
+
77
+ ```javascript
78
+ (callback) => {
79
+ Object.assign(globalThis, {
80
+ [name]: function (...args) {
81
+ return new Promise((resolve, reject) => {
82
+ callback([resolve, reject, args]);
83
+ });
84
+ },
85
+ });
86
+ }
87
+ ```
88
+
89
+ #### 3. Message Handling
90
+
91
+ Ruby listens for `script.message` events and processes calls:
92
+
93
+ ```ruby
94
+ def handle_message(params)
95
+ return unless params["channel"] == @channel
96
+
97
+ # Extract data handle with [resolve, reject, args]
98
+ data_handle = JSHandle.from(params["data"], realm.core_realm)
99
+
100
+ # Call Ruby function and send result back
101
+ result = @apply.call(*args)
102
+ send_result(data_handle, result)
103
+ end
104
+ ```
105
+
106
+ ### Session Event Subscription
107
+
108
+ **Important**: `script.message` must be subscribed in the session:
109
+
110
+ ```ruby
111
+ # In Core::Session
112
+ def subscribe_to_events
113
+ subscribe([
114
+ "browsingContext.load",
115
+ "browsingContext.domContentLoaded",
116
+ # ... other events
117
+ "script.message", # Required for exposeFunction
118
+ ]).wait
119
+ end
120
+ ```
121
+
122
+ ### Frame Handling
123
+
124
+ ExposedFunction handles dynamic frames by:
125
+
126
+ 1. **Listening to frameattached**: Injects into new frames
127
+ 2. **Using preload scripts**: For top-level browsing contexts (not iframes)
128
+ 3. **Using callFunction**: For immediate injection into current context
129
+
130
+ ```ruby
131
+ def inject_into_frame(frame)
132
+ # Add preload script for top-level contexts only
133
+ if frame.browsing_context.parent.nil?
134
+ script_id = frame.browsing_context.add_preload_script(
135
+ function_declaration,
136
+ arguments: [channel]
137
+ ).wait
138
+ end
139
+
140
+ # Always call function for immediate availability
141
+ realm.core_realm.call_function(
142
+ function_declaration,
143
+ false,
144
+ arguments: [channel]
145
+ ).wait
146
+ end
147
+ ```
148
+
149
+ ### Error Handling
150
+
151
+ #### Standard Errors
152
+
153
+ Errors are serialized with name, message, and stack trace:
154
+
155
+ ```ruby
156
+ def send_error(data_handle, error)
157
+ name = error.class.name
158
+ message = error.message
159
+ stack = error.backtrace&.join("\n")
160
+
161
+ data_handle.evaluate(<<~JS, name, message, stack)
162
+ ([, reject], name, message, stack) => {
163
+ const error = new Error(message);
164
+ error.name = name;
165
+ if (stack) { error.stack = stack; }
166
+ reject(error);
167
+ }
168
+ JS
169
+ end
170
+ ```
171
+
172
+ #### Non-Error Values (ThrownValue)
173
+
174
+ Ruby doesn't support `throw "string"` syntax. Use `ThrownValue`:
175
+
176
+ ```ruby
177
+ class ThrownValue < StandardError
178
+ attr_reader :value
179
+
180
+ def initialize(value)
181
+ @value = value
182
+ super("Thrown value")
183
+ end
184
+ end
185
+
186
+ # Usage
187
+ page.expose_function("throwValue") do |value|
188
+ raise ExposedFunction::ThrownValue.new(value)
189
+ end
190
+ ```
191
+
192
+ ### Cleanup
193
+
194
+ Disposal removes the function from all frames and cleans up resources:
195
+
196
+ ```ruby
197
+ def dispose
198
+ session.off("script.message", &@listener)
199
+ page.off(:frameattached, &@frame_listener)
200
+
201
+ # Remove from globalThis
202
+ remove_binding_from_frame(@frame)
203
+
204
+ # Remove preload scripts
205
+ @scripts.each do |frame, script_id|
206
+ frame.browsing_context.remove_preload_script(script_id).wait
207
+ end
208
+ end
209
+ ```
210
+
211
+ ## Testing Considerations
212
+
213
+ ### CSP Headers
214
+
215
+ Some tests require Content-Security-Policy headers. Use `TestServer#set_csp`:
216
+
217
+ ```ruby
218
+ server.set_csp("/empty.html", "script-src 'self'")
219
+ ```
220
+
221
+ ### Test Asset
222
+
223
+ `spec/assets/tamperable.html` captures `window.injected` before page scripts run:
224
+
225
+ ```html
226
+ <script>
227
+ window.result = window.injected;
228
+ </script>
229
+ ```
230
+
231
+ ## Common Pitfalls
232
+
233
+ ### 1. Missing script.message Subscription
234
+
235
+ **Problem**: `exposeFunction` doesn't receive callbacks
236
+
237
+ **Solution**: Ensure `script.message` is in session event subscriptions
238
+
239
+ ### 2. Ownership: "root" Required
240
+
241
+ **Problem**: JSHandle becomes invalid before processing
242
+
243
+ **Solution**: Use `ownership: "root"` in channel argument to keep handles alive
244
+
245
+ ### 3. Preload Scripts for Iframes
246
+
247
+ **Problem**: `addPreloadScript` not supported for iframe contexts
248
+
249
+ **Solution**: Only use preload scripts for top-level contexts; use `callFunction` for iframes
250
+
251
+ ### 4. TypeError on raise nil
252
+
253
+ **Problem**: Ruby's `raise nil` throws `TypeError: exception class/object expected`
254
+
255
+ **Solution**: Catch and convert to `send_thrown_value`:
256
+
257
+ ```ruby
258
+ rescue TypeError => e
259
+ if e.message.include?("exception class/object expected")
260
+ send_thrown_value(data_handle, nil)
261
+ else
262
+ send_error(data_handle, e)
263
+ end
264
+ end
265
+ ```
266
+
267
+ ## References
268
+
269
+ - [WebDriver BiDi script.message](https://w3c.github.io/webdriver-bidi/#event-script-message)
270
+ - [WebDriver BiDi addPreloadScript](https://w3c.github.io/webdriver-bidi/#command-script-addPreloadScript)
271
+ - [Puppeteer ExposedFunction.ts](https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/bidi/ExposedFunction.ts)
@@ -212,3 +212,23 @@ curl -sL https://raw.githubusercontent.com/puppeteer/puppeteer/main/test/assets/
212
212
  ```
213
213
 
214
214
  **Why this matters**: Test assets are designed to test specific edge cases (rotated elements, complex layouts, etc.). Using simplified versions defeats the purpose of these tests.
215
+
216
+ ## 9. API Coverage Update
217
+
218
+ **IMPORTANT**: When implementing new Puppeteer API methods, update `API_COVERAGE.md`:
219
+
220
+ 1. Find the corresponding entry in the table (e.g., `Browser.userAgent`, `Page.setUserAgent`)
221
+ 2. Change the status from `❌` to `✅`
222
+ 3. Update the coverage count at the top of the file
223
+
224
+ ```markdown
225
+ # Before
226
+ - Coverage: `156/274` (`56.93%`)
227
+ | `Browser.userAgent` | `Puppeteer::Bidi::Browser#user_agent` | ❌ |
228
+
229
+ # After (implemented 2 new methods: 156 + 2 = 158)
230
+ - Coverage: `158/274` (`57.66%`)
231
+ | `Browser.userAgent` | `Puppeteer::Bidi::Browser#user_agent` | ✅ |
232
+ ```
233
+
234
+ **CI will fail** if API_COVERAGE.md is not updated when new methods are implemented. The API Coverage check compares implemented methods against the coverage file.