puppeteer-bidi 0.0.1 → 0.0.3.beta1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59931b8cfbf15b649c6d4202eed284ab7c5e7f50dfbbbbeae1ec0e15423aa9bf
4
- data.tar.gz: 21eed43df8ece3625be9a701730d5f736a6f4ba02a894631af957bf61b1b5e7c
3
+ metadata.gz: 56fae0e1e155b3fe5bcce854ac7ff615c86af3815b0b9b186bf97fa905421a00
4
+ data.tar.gz: 3b13efc226d2a7d84da91056679707ff3d0b3a5d511f2211c9cfc4da1c0fef23
5
5
  SHA512:
6
- metadata.gz: 4e677ea4db58824f6b25ce4e78c587a1e8cae7dfdb9881a4eb0e47f562fe4f8d9c67ead75e9b93b3a870722856097997000d873067fc6d28e89e114afe225c19
7
- data.tar.gz: 54b4d4dc626bb50f72871e8ff560321c0345f31f9ce60f0409cbb0afe3145f379f863180412497b3e5f830fe758faacdeacf573c58a71e09223def214991652c
6
+ metadata.gz: abd414f479202d7f642c6f3b4b0ce3db308a18c5116bd4545483b88cf03193cdfb49e41931e596b3cb99c7699ac8f6773ba4ffff7bf160dd8f0a165745f6d0cc
7
+ data.tar.gz: 62c840fafb7844a586286eb5776b58763e2d7d3547194233e74e75d997aeb239d875fa263b20dbdfe0a326bc2608748c5c4cc479148299096a6b11f43d116cca
data/API_COVERAGE.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  - Puppeteer commit: `7d750c25cb29764f2fb31cb90b750a8eec350199`
4
4
  - Generated by: `development/generate_api_coverage.rb`
5
- - Coverage: `174/274` (`63.50%`)
5
+ - Coverage: `182/265` (`68.68%`)
6
6
 
7
7
  ## Browser (Puppeteer::Bidi::Browser)
8
8
 
@@ -111,7 +111,7 @@
111
111
  | `Frame.addStyleTag` | `Puppeteer::Bidi::Frame#add_style_tag` | ❌ |
112
112
  | `Frame.childFrames` | `Puppeteer::Bidi::Frame#child_frames` | ✅ |
113
113
  | `Frame.click` | `Puppeteer::Bidi::Frame#click` | ✅ |
114
- | `Frame.content` | `Puppeteer::Bidi::Frame#content` | |
114
+ | `Frame.content` | `Puppeteer::Bidi::Frame#content` | |
115
115
  | `Frame.evaluate` | `Puppeteer::Bidi::Frame#evaluate` | ✅ |
116
116
  | `Frame.evaluateHandle` | `Puppeteer::Bidi::Frame#evaluate_handle` | ✅ |
117
117
  | `Frame.focus` | `Puppeteer::Bidi::Frame#focus` | ❌ |
@@ -254,12 +254,12 @@
254
254
  | `Page.emulateVisionDeficiency` | `Puppeteer::Bidi::Page#emulate_vision_deficiency` | ❌ |
255
255
  | `Page.evaluate` | `Puppeteer::Bidi::Page#evaluate` | ✅ |
256
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` | |
257
+ | `Page.evaluateOnNewDocument` | `Puppeteer::Bidi::Page#evaluate_on_new_document` | |
258
+ | `Page.exposeFunction` | `Puppeteer::Bidi::Page#expose_function` | |
259
259
  | `Page.focus` | `Puppeteer::Bidi::Page#focus` | ✅ |
260
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` | |
261
+ | `Page.getDefaultNavigationTimeout` | `Puppeteer::Bidi::Page#get_default_navigation_timeout` | |
262
+ | `Page.getDefaultTimeout` | `Puppeteer::Bidi::Page#get_default_timeout` | |
263
263
  | `Page.goBack` | `Puppeteer::Bidi::Page#go_back` | ✅ |
264
264
  | `Page.goForward` | `Puppeteer::Bidi::Page#go_forward` | ✅ |
265
265
  | `Page.goto` | `Puppeteer::Bidi::Page#goto` | ✅ |
@@ -275,8 +275,8 @@
275
275
  | `Page.pdf` | `Puppeteer::Bidi::Page#pdf` | ❌ |
276
276
  | `Page.queryObjects` | `Puppeteer::Bidi::Page#query_objects` | ❌ |
277
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` | |
278
+ | `Page.removeExposedFunction` | `Puppeteer::Bidi::Page#remove_exposed_function` | |
279
+ | `Page.removeScriptToEvaluateOnNewDocument` | `Puppeteer::Bidi::Page#remove_script_to_evaluate_on_new_document` | |
280
280
  | `Page.resize` | `Puppeteer::Bidi::Page#resize` | ❌ |
281
281
  | `Page.screencast` | `Puppeteer::Bidi::Page#screencast` | ❌ |
282
282
  | `Page.screenshot` | `Puppeteer::Bidi::Page#screenshot` | ✅ |
@@ -286,7 +286,7 @@
286
286
  | `Page.setCacheEnabled` | `Puppeteer::Bidi::Page#set_cache_enabled` | ✅ |
287
287
  | `Page.setContent` | `Puppeteer::Bidi::Page#set_content` | ✅ |
288
288
  | `Page.setCookie` | `Puppeteer::Bidi::Page#set_cookie` | ✅ |
289
- | `Page.setDefaultNavigationTimeout` | `Puppeteer::Bidi::Page#set_default_navigation_timeout` | |
289
+ | `Page.setDefaultNavigationTimeout` | `Puppeteer::Bidi::Page#set_default_navigation_timeout` | |
290
290
  | `Page.setDefaultTimeout` | `Puppeteer::Bidi::Page#set_default_timeout` | ✅ |
291
291
  | `Page.setDragInterception` | `Puppeteer::Bidi::Page#set_drag_interception` | ❌ |
292
292
  | `Page.setExtraHTTPHeaders` | `Puppeteer::Bidi::Page#set_extra_http_headers` | ✅ |
@@ -329,17 +329,3 @@
329
329
  | `PuppeteerNode.trimCache` | `Puppeteer::Bidi.trim_cache` | ❌ |
330
330
  | `Puppeteer.unregisterCustomQueryHandler` | `Puppeteer::Bidi.unregister_custom_query_handler` | ❌ |
331
331
 
332
- ## Target (Puppeteer::Bidi::Target)
333
-
334
- | Node.js | Ruby | Supported |
335
- | --- | --- | :---: |
336
- | `Target.asPage` | `Puppeteer::Bidi::Target#as_page` | ❌ |
337
- | `Target.browser` | `Puppeteer::Bidi::Target#browser` | ❌ |
338
- | `Target.browserContext` | `Puppeteer::Bidi::Target#browser_context` | ❌ |
339
- | `Target.createCDPSession` | `Puppeteer::Bidi::Target#create_cdp_session` | ❌ |
340
- | `Target.opener` | `Puppeteer::Bidi::Target#opener` | ❌ |
341
- | `Target.page` | `Puppeteer::Bidi::Target#page` | ❌ |
342
- | `Target.type` | `Puppeteer::Bidi::Target#type` | ❌ |
343
- | `Target.url` | `Puppeteer::Bidi::Target#url` | ❌ |
344
- | `Target.worker` | `Puppeteer::Bidi::Target#worker` | ❌ |
345
-
@@ -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)
@@ -0,0 +1,111 @@
1
+ # ReactorRunner - Using Browser Outside Sync Blocks
2
+
3
+ ## Problem
4
+
5
+ The socketry/async library requires all async operations to run inside a `Sync do ... end` block. However, some use cases cannot wrap their entire code in a Sync block:
6
+
7
+ ```ruby
8
+ # This pattern doesn't work with plain async:
9
+ browser = Puppeteer::Bidi.launch_browser_instance(headless: true)
10
+ at_exit { browser.close } # Called outside any Sync block!
11
+
12
+ Sync do
13
+ page = browser.new_page
14
+ page.goto("https://example.com")
15
+ end
16
+ ```
17
+
18
+ The `at_exit` hook runs after the Sync block has finished, so `browser.close` would fail.
19
+
20
+ ## Solution: ReactorRunner
21
+
22
+ `ReactorRunner` creates a dedicated Async reactor in a background thread and provides a way to execute code within that reactor from any thread.
23
+
24
+ ### How It Works
25
+
26
+ 1. **Background Thread with Reactor**: ReactorRunner spawns a new thread that runs `Sync do ... end` with an `Async::Queue` for receiving jobs
27
+ 2. **Proxy Pattern**: Returns a `Proxy` object that wraps the real Browser and forwards all method calls through the ReactorRunner
28
+ 3. **Automatic Detection**: `launch_browser_instance` and `connect_to_browser_instance` check `Async::Task.current` to decide whether to use ReactorRunner
29
+
30
+ ### Architecture
31
+
32
+ ```
33
+ Main Thread Background Thread (ReactorRunner)
34
+ │ │
35
+ │ launch_browser_instance() │
36
+ │ ─────────────────────────────────>│ Sync do
37
+ │ │ Browser.launch()
38
+ │ <─────────────────────────────────│ (browser created)
39
+ │ returns Proxy │
40
+ │ │
41
+ │ proxy.new_page() │
42
+ │ ─────────────────────────────────>│ browser.new_page()
43
+ │ <─────────────────────────────────│ (returns page)
44
+ │ │
45
+ │ at_exit { proxy.close } │
46
+ │ ─────────────────────────────────>│ browser.close()
47
+ │ │ end
48
+ │ │
49
+ ```
50
+
51
+ ### Key Components
52
+
53
+ #### ReactorRunner
54
+
55
+ - Creates background thread with `Sync` reactor
56
+ - Uses `Async::Queue` to receive jobs from other threads
57
+ - `sync(&block)` method executes block in reactor and returns result
58
+ - Handles proper cleanup when closed
59
+
60
+ #### ReactorRunner::Proxy
61
+
62
+ - Extends `SimpleDelegator` for transparent method forwarding
63
+ - Wraps/unwraps return values (e.g., Page becomes Proxy too)
64
+ - `owns_runner: true` means closing browser also closes the ReactorRunner
65
+ - Handles edge cases like calling `close` after runner is already closed
66
+
67
+ ### Usage Patterns
68
+
69
+ #### Pattern 1: Block-based (Recommended)
70
+
71
+ ```ruby
72
+ Puppeteer::Bidi.launch do |browser|
73
+ page = browser.new_page
74
+ # ... use browser
75
+ end # automatically closed
76
+ ```
77
+
78
+ #### Pattern 2: Instance with at_exit
79
+
80
+ ```ruby
81
+ browser = Puppeteer::Bidi.launch_browser_instance(headless: true)
82
+ at_exit { browser.close }
83
+
84
+ Sync do
85
+ page = browser.new_page
86
+ page.goto("https://example.com")
87
+ end
88
+ ```
89
+
90
+ #### Pattern 3: Inside existing Async context
91
+
92
+ ```ruby
93
+ Sync do
94
+ # No ReactorRunner used - browser is returned directly
95
+ browser = Puppeteer::Bidi.launch_browser_instance(headless: true)
96
+ page = browser.new_page
97
+ # ...
98
+ browser.close
99
+ end
100
+ ```
101
+
102
+ ### Implementation Notes
103
+
104
+ 1. **Thread Safety**: `Async::Queue` handles cross-thread communication safely
105
+ 2. **Proxyable Check**: Only `Puppeteer::Bidi::*` objects (excluding Core layer) are wrapped in Proxy
106
+ 3. **Error Handling**: Errors in reactor are propagated back to calling thread via `Async::Promise`
107
+ 4. **Type Annotations**: Return type is `Browser` (Proxy is an internal detail)
108
+
109
+ ### Reference
110
+
111
+ This pattern is inspired by [async-webdriver](https://github.com/socketry/async-webdriver) by Samuel Williams (author of socketry/async).
data/CLAUDE.md CHANGED
@@ -186,6 +186,7 @@ See the [CLAUDE/](CLAUDE/) directory for detailed implementation guides:
186
186
 
187
187
  - **[Two-Layer Architecture](CLAUDE/two_layer_architecture.md)** - Core vs Upper layer, async patterns
188
188
  - **[Async Programming](CLAUDE/async_programming.md)** - Fiber-based concurrency with socketry/async
189
+ - **[ReactorRunner](CLAUDE/reactor_runner.md)** - Using browser outside Sync blocks (at_exit, etc.)
189
190
  - **[Porting Puppeteer](CLAUDE/porting_puppeteer.md)** - Best practices for implementing features
190
191
  - **[Core Layer Gotchas](CLAUDE/core_layer_gotchas.md)** - EventEmitter/Disposable pitfalls, @disposed conflicts
191
192
 
@@ -201,6 +202,7 @@ See the [CLAUDE/](CLAUDE/) directory for detailed implementation guides:
201
202
  - **[Navigation Waiting](CLAUDE/navigation_waiting.md)** - waitForNavigation patterns
202
203
  - **[Frame Architecture](CLAUDE/frame_architecture.md)** - Parent-based frame hierarchy
203
204
  - **[FileChooser](CLAUDE/file_chooser.md)** - File upload and dialog handling (requires Firefox Nightly)
205
+ - **[ExposeFunction](CLAUDE/expose_function_implementation.md)** - `exposeFunction` and `evaluateOnNewDocument` using BiDi script.message
204
206
  - **[Error Handling](CLAUDE/error_handling.md)** - Custom exception types
205
207
 
206
208
  ### Testing
data/README.md CHANGED
@@ -78,9 +78,13 @@ Puppeteer::Bidi.launch(
78
78
  ### Connect to an existing browser
79
79
 
80
80
  ```ruby
81
- browser = Puppeteer::Bidi.launch_browser_instance(headless: true)
82
- ws_endpoint = browser.ws_endpoint
83
- browser.disconnect
81
+ ws_endpoint = nil
82
+
83
+ Sync do
84
+ browser = Puppeteer::Bidi.launch_browser_instance(headless: true)
85
+ ws_endpoint = browser.ws_endpoint
86
+ browser.disconnect
87
+ end
84
88
 
85
89
  Puppeteer::Bidi.connect(ws_endpoint) do |session|
86
90
  puts "Reconnected to browser"
data/Rakefile CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
+ require 'rake/testtask'
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << "test"
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ t.verbose = true
11
+ end
5
12
 
6
13
  RSpec::Core::RakeTask.new(:spec)
7
14
 
@@ -211,9 +211,9 @@ NODE_OWNER_ALIASES = {
211
211
  "HTTPResponse" => "HTTPResponse",
212
212
  "filechooser" => "FileChooser",
213
213
  "fileChooser" => "FileChooser",
214
- "FileChooser" => "FileChooser",
215
- "target" => "Target",
216
- "Target" => "Target"
214
+ "FileChooser" => "FileChooser"
215
+ # Note: Target is excluded from coverage tracking due to significant
216
+ # implementation differences between Node.js and Ruby versions.
217
217
  }.freeze
218
218
 
219
219
  def canonical_node_owner(owner)
@@ -232,8 +232,9 @@ RUBY_OWNER_CONSTANTS = {
232
232
  "Mouse" => "Puppeteer::Bidi::Mouse",
233
233
  "HTTPRequest" => "Puppeteer::Bidi::HTTPRequest",
234
234
  "HTTPResponse" => "Puppeteer::Bidi::HTTPResponse",
235
- "FileChooser" => "Puppeteer::Bidi::FileChooser",
236
- "Target" => "Puppeteer::Bidi::Target"
235
+ "FileChooser" => "Puppeteer::Bidi::FileChooser"
236
+ # Note: Target is excluded from coverage tracking due to significant
237
+ # implementation differences between Node.js and Ruby versions.
237
238
  }.freeze
238
239
 
239
240
  def safe_constantize(name)
@@ -28,29 +28,45 @@ module Puppeteer
28
28
  # @yield [async_task] Execute a task within the timeout, optionally receiving Async::Task
29
29
  # @return [Async::Task] Async task that resolves/rejects once the operation completes
30
30
  def async_timeout(timeout_ms, task = nil, &block)
31
- timeout_seconds = timeout_ms / 1000.0
32
-
33
31
  if task
34
- Async do |async_task|
35
- async_task.with_timeout(timeout_seconds) do
32
+ return Async do |async_task|
33
+ if timeout_ms == 0
36
34
  if task.is_a?(Proc)
37
35
  args = task.arity.positive? ? [async_task] : []
38
36
  task.call(*args)
39
37
  else
40
38
  await(task)
41
39
  end
40
+ else
41
+ timeout_seconds = timeout_ms / 1000.0
42
+ async_task.with_timeout(timeout_seconds) do
43
+ if task.is_a?(Proc)
44
+ args = task.arity.positive? ? [async_task] : []
45
+ task.call(*args)
46
+ else
47
+ await(task)
48
+ end
49
+ end
42
50
  end
43
51
  end
44
- elsif block
45
- Async do |async_task|
46
- async_task.with_timeout(timeout_seconds) do
52
+ end
53
+
54
+ if block
55
+ return Async do |async_task|
56
+ if timeout_ms == 0
47
57
  args = block.arity.positive? ? [async_task] : []
48
58
  await(block.call(*args))
59
+ else
60
+ timeout_seconds = timeout_ms / 1000.0
61
+ async_task.with_timeout(timeout_seconds) do
62
+ args = block.arity.positive? ? [async_task] : []
63
+ await(block.call(*args))
64
+ end
49
65
  end
50
66
  end
51
- else
52
- raise ArgumentError, 'AsyncUtils.async_timeout requires a task or block'
53
67
  end
68
+
69
+ raise ArgumentError, 'AsyncUtils.async_timeout requires a task or block'
54
70
  end
55
71
 
56
72
  def promise_all(*tasks)
@@ -91,7 +91,8 @@ module Puppeteer
91
91
 
92
92
  # Start transport connection in background thread with Sync reactor
93
93
  # Sync is the preferred way to run async code at the top level
94
- AsyncUtils.async_timeout((timeout || 30) * 1000, transport.connect).wait
94
+ timeout_ms = ((timeout || 30) * 1000).to_i
95
+ AsyncUtils.async_timeout(timeout_ms) { transport.connect }.wait
95
96
 
96
97
  connection = Connection.new(transport)
97
98
 
@@ -110,7 +111,7 @@ module Puppeteer
110
111
  transport = Transport.new(ws_endpoint)
111
112
  ws_endpoint = transport.url
112
113
  timeout_ms = ((timeout || 30) * 1000).to_i
113
- AsyncUtils.async_timeout(timeout_ms, transport.connect).wait
114
+ AsyncUtils.async_timeout(timeout_ms) { transport.connect }.wait
114
115
  connection = Connection.new(transport)
115
116
 
116
117
  # Verify that this endpoint speaks WebDriver BiDi (and is ready) before creating a new session.
@@ -81,6 +81,9 @@ module Puppeteer
81
81
  if options[:contexts]
82
82
  params[:contexts] = options[:contexts].map(&:id)
83
83
  end
84
+ if options.key?(:arguments) || options.key?("arguments")
85
+ params[:arguments] = options[:arguments] || options["arguments"]
86
+ end
84
87
  params[:sandbox] = options[:sandbox] if options[:sandbox]
85
88
 
86
89
  Async do
@@ -140,6 +140,7 @@ module Puppeteer
140
140
  'network.authRequired',
141
141
  'script.realmCreated',
142
142
  'script.realmDestroyed',
143
+ 'script.message',
143
144
  'log.entryAdded',
144
145
  'input.fileDialogOpened',
145
146
  ]