puppeteer-bidi 0.0.3.beta1 → 0.0.3.beta2

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.
@@ -1,263 +0,0 @@
1
- # Test Server Dynamic Routes
2
-
3
- This document describes the dynamic route handling functionality added to the test server infrastructure.
4
-
5
- ## Overview
6
-
7
- The test server (`spec/support/test_server.rb`) has been extended to support dynamic route interception and request synchronization, matching Puppeteer's test server capabilities.
8
-
9
- ## Features
10
-
11
- ### 1. Dynamic Route Interception
12
-
13
- **Purpose**: Intercept specific routes and control the response timing/content.
14
-
15
- **API:**
16
-
17
- ```ruby
18
- server.set_route(path) do |request, response|
19
- # Control when/how to respond
20
- response_holder[:response] = response
21
- end
22
- ```
23
-
24
- **Usage Example:**
25
-
26
- ```ruby
27
- it 'should intercept CSS loading' do
28
- with_test_state do |page:, server:, **|
29
- response_holder = {}
30
-
31
- # Intercept CSS file to control when it loads
32
- server.set_route('/one-style.css') do |_req, res|
33
- response_holder[:response] = res
34
- end
35
-
36
- # Navigate to page (will wait for CSS)
37
- page.goto("#{server.prefix}/one-style.html", wait: 'none')
38
-
39
- # Do something while CSS is pending...
40
-
41
- # Release the CSS response
42
- response_holder[:response].finish
43
- end
44
- end
45
- ```
46
-
47
- ### 2. Request Synchronization
48
-
49
- **Purpose**: Wait for a specific request to arrive at the server before proceeding.
50
-
51
- **API:**
52
-
53
- ```ruby
54
- # Returns an Async task that resolves when request is received
55
- task = server.wait_for_request(path)
56
- task.wait # Block until request arrives (with 5s timeout)
57
- ```
58
-
59
- **Usage Example:**
60
-
61
- ```ruby
62
- it 'should wait for image request' do
63
- with_test_state do |page:, server:, **|
64
- # Start navigation
65
- page.goto("#{server.prefix}/page-with-image.html", wait: 'none')
66
-
67
- # Wait for the image to be requested
68
- begin
69
- server.wait_for_request('/image.png').wait
70
- puts "Image was requested!"
71
- rescue Async::TimeoutError
72
- puts "Image request never arrived"
73
- end
74
- end
75
- end
76
- ```
77
-
78
- ## Implementation Details
79
-
80
- ### Async HTTP Server
81
-
82
- `TestServer::Server` now runs an `Async::HTTP::Server` inside a dedicated thread. The server keeps two shared hashes guarded by mutexes:
83
-
84
- - `@routes` maps request paths to custom handlers.
85
- - `@request_promises` stores waiters created via `wait_for_request`.
86
-
87
- Incoming requests execute the following flow:
88
-
89
- ```ruby
90
- server = Async::HTTP::Server.for(endpoint) do |request|
91
- if handler = lookup_route(request.path)
92
- notify_request(request.path)
93
- respond_with_handler(handler, request)
94
- else
95
- serve_static_asset(request)
96
- end
97
- end
98
- ```
99
-
100
- Static assets are served from `spec/assets`, while dynamic route handlers receive a lightweight wrapper (`RouteRequest`) exposing `path`, `headers`, `params`, and optional `body` accessors.
101
-
102
- ### Response Writer
103
-
104
- Dynamic handlers interact with a `ResponseWriter` instance that buffers data until `finish` is invoked:
105
-
106
- ```ruby
107
- server.set_route('/slow.css') do |_request, writer|
108
- writer.add_header('content-type', 'text/css; charset=utf-8')
109
- writer.write("body { background: red; }")
110
- writer.finish
111
- end
112
- ```
113
-
114
- The server task waits asynchronously for `writer.finish` before constructing the final `Protocol::HTTP::Response`. Handlers may capture the writer and complete it later from other tasks or threads, enabling Puppeteer-style resource gating.
115
-
116
- ## Testing Navigation Events
117
-
118
- ### Puppeteer Test Pattern
119
-
120
- A common Puppeteer test pattern tests the timing of `domcontentloaded` vs `load` events:
121
-
122
- ```typescript
123
- it("should work with both domcontentloaded and load", async () => {
124
- let response!: ServerResponse;
125
- server.setRoute("/one-style.css", (_req, res) => {
126
- return (response = res);
127
- });
128
-
129
- let bothFired = false;
130
-
131
- const navigationPromise = page.goto(server.PREFIX + "/one-style.html");
132
- const domContentLoadedPromise = page.waitForNavigation({
133
- waitUntil: "domcontentloaded",
134
- });
135
- const loadFiredPromise = page
136
- .waitForNavigation({ waitUntil: "load" })
137
- .then(() => {
138
- bothFired = true;
139
- });
140
-
141
- await server.waitForRequest("/one-style.css");
142
- await domContentLoadedPromise;
143
- expect(bothFired).toBe(false); // load hasn't fired yet
144
-
145
- response.end(); // Release CSS
146
- await loadFiredPromise; // Now load fires
147
- });
148
- ```
149
-
150
- ## Known Limitations and Challenges
151
-
152
- ### 1. Navigation Timing Coordination
153
-
154
- **Challenge**: Testing `domcontentloaded` vs `load` timing requires careful coordination of:
155
-
156
- - Navigation start (must not wait for load)
157
- - Event listeners (must be registered before events fire)
158
- - Resource loading (must control when resources complete)
159
-
160
- **Issue**: In Ruby implementation, using `page.goto()` causes problems because:
161
-
162
- - `goto(url)` internally calls `navigate(url, wait: 'complete')` by default
163
- - This blocks until the `load` event fires
164
- - Cannot register `wait_for_navigation` listeners after navigation completes
165
-
166
- **Attempted Solutions:**
167
-
168
- 1. **Using `wait: 'none'`**:
169
-
170
- ```ruby
171
- page.browsing_context.navigate(url, wait: 'none')
172
- ```
173
-
174
- - Doesn't block on navigation
175
- - But bypasses high-level `Page` API
176
- - Still has timing issues with event listener registration
177
-
178
- 2. **Parallel Async tasks**:
179
- ```ruby
180
- navigation_task = Async { page.goto(url) }
181
- dom_loaded_task = Async { page.wait_for_navigation(wait_until: 'domcontentloaded') }
182
- load_task = Async { page.wait_for_navigation(wait_until: 'load') }
183
- ```
184
- - Race condition: `wait_for_navigation` might miss events if called after navigation completes
185
- - Async task scheduling order is not guaranteed
186
-
187
- ### 2. Request Promise Resolution
188
-
189
- **Issue**: `server.wait_for_request()` timeout errors are logged as warnings:
190
-
191
- ```
192
- Async::TimeoutError: execution expired
193
- ```
194
-
195
- This is expected behavior when the request arrives immediately (before `wait_for_request` is called), but the warning is noisy.
196
-
197
- **Current Workaround:**
198
-
199
- ```ruby
200
- begin
201
- server.wait_for_request('/one-style.css').wait
202
- rescue Async::TimeoutError
203
- # Request might have already arrived - ignore
204
- end
205
- ```
206
-
207
- ### 3. Response Writer Semantics
208
-
209
- **Limitation**: The custom `ResponseWriter` currently buffers the entire body before sending it back through `Protocol::HTTP::Response`. True streaming responses are not yet implemented, so large payloads are held in memory until `finish` is called. Handlers should keep payloads small, or extend the writer to stream chunks if needed in future work.
210
-
211
- ## Future Improvements
212
-
213
- ### 1. Navigation API Enhancement
214
-
215
- Consider adding a `Page.navigate` method that exposes the `wait` parameter:
216
-
217
- ```ruby
218
- # High-level API with wait control
219
- page.navigate(url, wait: 'none') # Start navigation without waiting
220
- page.navigate(url, wait: 'interactive') # Wait for domcontentloaded
221
- page.navigate(url, wait: 'complete') # Wait for load (default)
222
- ```
223
-
224
- ### 2. Event Listener Preregistration
225
-
226
- Add API to register navigation listeners before starting navigation:
227
-
228
- ```ruby
229
- page.with_navigation_listeners do |listeners|
230
- listeners.on_dom_content_loaded { puts "DOM ready" }
231
- listeners.on_load { puts "Page loaded" }
232
-
233
- page.goto(url) # Listeners already registered
234
- end
235
- ```
236
-
237
- ### 3. Test Server Request Queue
238
-
239
- Store all incoming requests in a queue for post-facto checking:
240
-
241
- ```ruby
242
- # Record all requests
243
- server.enable_request_recording
244
-
245
- page.goto(url)
246
-
247
- # Check what was requested
248
- requests = server.recorded_requests
249
- expect(requests.map(&:path)).to include('/one-style.css')
250
- ```
251
-
252
- ## Related Files
253
-
254
- - `spec/support/test_server.rb` - Test server implementation
255
- - `spec/integration/navigation_spec.rb` - Navigation tests using dynamic routes
256
- - `spec/assets/one-style.html` - Test asset with external CSS
257
- - `spec/assets/one-style.css` - CSS file for testing resource loading
258
-
259
- ## References
260
-
261
- - [Puppeteer test server](https://github.com/puppeteer/puppeteer/blob/main/test/src/server/index.ts)
262
- - [Puppeteer navigation tests](https://github.com/puppeteer/puppeteer/blob/main/test/src/navigation.spec.ts)
263
- - [async-http server guide](https://socketry.github.io/async-http/guides/getting-started/index.html#making-a-server)
@@ -1,236 +0,0 @@
1
- # Testing Strategy and Performance Optimization
2
-
3
- This document covers integration test organization, performance optimization strategies, golden image testing, and debugging techniques.
4
-
5
-
6
- #### Integration Tests Organization
7
-
8
- ```
9
- spec/
10
- ├── unit/ # Fast unit tests (future)
11
- ├── integration/ # Browser automation tests
12
- │ ├── examples/ # Example-based tests
13
- │ │ └── screenshot_spec.rb
14
- │ └── screenshot_spec.rb # Feature test suites
15
- ├── assets/ # Test HTML/CSS/JS files
16
- │ ├── grid.html
17
- │ ├── scrollbar.html
18
- │ ├── empty.html
19
- │ └── digits/*.png
20
- ├── golden-firefox/ # Reference images
21
- │ └── screenshot-*.png
22
- └── support/ # Test utilities
23
- ├── test_server.rb
24
- └── golden_comparator.rb
25
- ```
26
-
27
- #### Implemented Screenshot Tests
28
-
29
- All 12 tests ported from [Puppeteer's screenshot.spec.ts](https://github.com/puppeteer/puppeteer/blob/main/test/src/screenshot.spec.ts):
30
-
31
- 1. **should work** - Basic screenshot functionality
32
- 2. **should clip rect** - Clipping specific region
33
- 3. **should get screenshot bigger than the viewport** - Offscreen clip with captureBeyondViewport
34
- 4. **should clip bigger than the viewport without "captureBeyondViewport"** - Viewport coordinate transformation
35
- 5. **should run in parallel** - Thread-safe parallel screenshots on single page
36
- 6. **should take fullPage screenshots** - Full page with document origin
37
- 7. **should take fullPage screenshots without captureBeyondViewport** - Full page with viewport resize
38
- 8. **should run in parallel in multiple pages** - Concurrent screenshots across multiple pages
39
- 9. **should work with odd clip size on Retina displays** - Odd pixel dimensions (11x11)
40
- 10. **should return base64** - Base64 encoding verification
41
- 11. **should take fullPage screenshots when defaultViewport is null** - No explicit viewport
42
- 12. **should restore to original viewport size** - Viewport restoration after fullPage
43
-
44
- Run tests:
45
- ```bash
46
- bundle exec rspec spec/integration/screenshot_spec.rb
47
- # Expected: 12 examples, 0 failures (completes in ~8 seconds with optimized spec_helper)
48
- ```
49
-
50
- #### Test Performance Optimization
51
-
52
- **Critical**: Integration tests are ~19x faster with browser reuse strategy.
53
-
54
- ##### Before Optimization (Per-test Browser Launch)
55
- ```ruby
56
- def with_test_state(**options)
57
- server = TestServer::Server.new
58
- server.start
59
-
60
- with_browser(**options) do |browser| # New browser per test!
61
- context = browser.default_browser_context
62
- page = browser.new_page
63
- yield(page: page, server: server, browser: browser, context: context)
64
- end
65
- ensure
66
- server.stop
67
- end
68
- ```
69
-
70
- **Performance**: ~195 seconds for 35 tests (browser launch overhead × 35)
71
-
72
- ##### After Optimization (Shared Browser)
73
- ```ruby
74
- # In spec_helper.rb
75
- config.before(:suite) do
76
- if RSpec.configuration.files_to_run.any? { |f| f.include?('spec/integration') }
77
- $shared_browser = Puppeteer::Bidi.launch_browser_instance(headless: headless_mode?)
78
- $shared_test_server = TestServer::Server.new
79
- $shared_test_server.start
80
- end
81
- end
82
-
83
- def with_test_state(**options)
84
- if $shared_browser && options.empty?
85
- # Create new page (tab) per test
86
- page = $shared_browser.new_page
87
- context = $shared_browser.default_browser_context
88
-
89
- begin
90
- yield(page: page, server: $shared_test_server, browser: $shared_browser, context: context)
91
- ensure
92
- page.close unless page.closed? # Clean up tab
93
- end
94
- else
95
- # Fall back to per-test browser for custom options
96
- end
97
- end
98
- ```
99
-
100
- **Performance**: ~10 seconds for 35 tests (1 browser launch + 35 tab creations)
101
-
102
- ##### Performance Results
103
-
104
- | Test Suite | Before | After | Improvement |
105
- |------------|--------|-------|-------------|
106
- | **evaluation_spec (23 tests)** | 127s | **7.17s** | **17.7x faster** |
107
- | **screenshot_spec (12 tests)** | 68s | **8.47s** | **8.0x faster** |
108
- | **Combined (35 tests)** | 195s | **10.33s** | **18.9x faster** 🚀 |
109
-
110
- **Key Benefits**:
111
- - Browser launch only once per suite
112
- - Each test gets fresh page (tab) for isolation
113
- - Cleanup handled automatically
114
- - Backward compatible (custom options fall back to per-test browser)
115
-
116
- #### Environment Variables
117
-
118
- ```bash
119
- HEADLESS=false # Run browser in non-headless mode for debugging
120
- ```
121
-
122
- ### Debugging Techniques
123
-
124
- #### 1. Save Screenshots for Inspection
125
-
126
- ```ruby
127
- # In golden_comparator.rb
128
- def save_screenshot(screenshot_base64, filename)
129
- output_dir = File.join(__dir__, '../output')
130
- FileUtils.mkdir_p(output_dir)
131
- File.binwrite(File.join(output_dir, filename),
132
- Base64.decode64(screenshot_base64))
133
- end
134
- ```
135
-
136
- #### 2. Compare Images Pixel-by-Pixel
137
-
138
- ```ruby
139
- cat > /tmp/compare.rb << 'EOF'
140
- require 'chunky_png'
141
-
142
- golden = ChunkyPNG::Image.from_file('spec/golden-firefox/screenshot.png')
143
- actual = ChunkyPNG::Image.from_file('spec/output/debug.png')
144
-
145
- diff_count = 0
146
- (0...golden.height).each do |y|
147
- (0...golden.width).each do |x|
148
- if golden[x, y] != actual[x, y]
149
- diff_count += 1
150
- puts "Diff at (#{x}, #{y})" if diff_count <= 10
151
- end
152
- end
153
- end
154
- puts "Total: #{diff_count} pixels differ"
155
- EOF
156
- ruby /tmp/compare.rb
157
- ```
158
-
159
- #### 3. Debug BiDi Responses
160
-
161
- ```ruby
162
- # Temporarily add debugging
163
- result = @browsing_context.default_realm.evaluate(script, true)
164
- puts "BiDi result: #{result.inspect}"
165
- deserialize_result(result)
166
- ```
167
-
168
- ### Common Pitfalls and Solutions
169
-
170
- #### 1. BiDi Protocol Differences
171
-
172
- **Problem:** BiDi `origin` parameter behavior differs from expectations
173
-
174
- **Solution:** Consult BiDi spec and test both `'document'` and `'viewport'` origins
175
-
176
- ```ruby
177
- # document: Absolute coordinates in full page
178
- # viewport: Relative to current viewport
179
- options[:origin] = capture_beyond_viewport ? 'document' : 'viewport'
180
- ```
181
-
182
- #### 2. Image Comparison Failures
183
-
184
- **Problem:** Golden images don't match exactly (1-2 pixel differences)
185
-
186
- **Solution:** Implement tolerance in comparison
187
-
188
- ```ruby
189
- # Allow small rendering differences (±1 RGB per channel)
190
- compare_with_golden(screenshot, 'golden.png', pixel_threshold: 1)
191
- ```
192
-
193
- #### 3. Viewport State Management
194
-
195
- **Problem:** Viewport not restored after fullPage screenshot
196
-
197
- **Solution:** Use `ensure` block
198
-
199
- ```ruby
200
- begin
201
- set_viewport(full_page_dimensions)
202
- screenshot = capture_screenshot(...)
203
- ensure
204
- set_viewport(original_viewport) if original_viewport
205
- end
206
- ```
207
-
208
- #### 4. Thread Safety
209
-
210
- **Problem:** Parallel screenshots cause race conditions
211
-
212
- **Solution:** BiDi protocol handles this naturally - test with threads
213
-
214
- ```ruby
215
- threads = (0...3).map do |i|
216
- Thread.new { page.screenshot(clip: {...}) }
217
- end
218
- screenshots = threads.map(&:value)
219
- ```
220
-
221
- ### Documentation References
222
-
223
- **Essential reading for implementation:**
224
-
225
- 1. **WebDriver BiDi Spec**: https://w3c.github.io/webdriver-bidi/
226
- 2. **Puppeteer Source**: https://github.com/puppeteer/puppeteer
227
- 3. **Puppeteer BiDi Tests**: https://github.com/puppeteer/puppeteer/tree/main/test/src
228
- 4. **Firefox BiDi Impl**: Check Firefox implementation notes for quirks
229
-
230
- **Reference implementation workflow:**
231
- 1. Find corresponding Puppeteer test in `test/src/`
232
- 2. Read TypeScript implementation in `packages/puppeteer-core/src/`
233
- 3. Check BiDi spec for protocol details
234
- 4. Implement Ruby version maintaining same logic
235
- 5. Download golden images and verify pixel-perfect match (with tolerance)
236
-
@@ -1,180 +0,0 @@
1
- # Two-Layer Async Architecture
2
-
3
- This codebase implements a two-layer architecture to separate async complexity from user-facing APIs.
4
-
5
- ## Architecture Overview
6
-
7
- ```
8
- ┌─────────────────────────────────────────────────────────┐
9
- │ Upper Layer (Puppeteer::Bidi) │
10
- │ - User-facing, synchronous API │
11
- │ - Calls .wait internally on Core layer methods │
12
- │ - Examples: Page, Frame, JSHandle, ElementHandle │
13
- ├─────────────────────────────────────────────────────────┤
14
- │ Core Layer (Puppeteer::Bidi::Core) │
15
- │ - Returns Async::Task for all async operations │
16
- │ - Uses async_send_command internally │
17
- │ - Examples: Session, BrowsingContext, Realm │
18
- └─────────────────────────────────────────────────────────┘
19
- ```
20
-
21
- ## Design Principles
22
-
23
- 1. **Core Layer (Puppeteer::Bidi::Core)**:
24
- - All methods that communicate with BiDi protocol return `Async::Task`
25
- - Uses `session.async_send_command` (not `send_command`)
26
- - Methods are explicitly async and composable
27
- - Examples: `BrowsingContext#navigate`, `Realm#call_function`
28
-
29
- 2. **Upper Layer (Puppeteer::Bidi)**:
30
- - All methods call `.wait` on Core layer async operations
31
- - Provides synchronous, blocking API for users
32
- - Users never see `Async::Task` directly
33
- - Examples: `Page#goto`, `Frame#evaluate`, `JSHandle#get_property`
34
-
35
- ## Implementation Patterns
36
-
37
- ### Core Layer Pattern
38
-
39
- ```ruby
40
- # lib/puppeteer/bidi/core/browsing_context.rb
41
- def navigate(url, wait: nil)
42
- Async do
43
- raise BrowsingContextClosedError, @reason if closed?
44
- params = { context: @id, url: url }
45
- params[:wait] = wait if wait
46
- result = session.async_send_command('browsingContext.navigate', params).wait
47
- result
48
- end
49
- end
50
-
51
- def perform_actions(actions)
52
- raise BrowsingContextClosedError, @reason if closed?
53
- session.async_send_command('input.performActions', {
54
- context: @id,
55
- actions: actions
56
- })
57
- end
58
- ```
59
-
60
- **Key points:**
61
- - Returns `Async::Task` (implicitly from `Async do` block or explicitly from `async_send_command`)
62
- - Users of Core layer must call `.wait` to get results
63
-
64
- ### Upper Layer Pattern
65
-
66
- ```ruby
67
- # lib/puppeteer/bidi/frame.rb
68
- def goto(url, wait_until: 'load', timeout: 30000)
69
- response = wait_for_navigation(timeout: timeout, wait_until: wait_until) do
70
- @browsing_context.navigate(url, wait: 'interactive').wait # .wait call
71
- end
72
- HTTPResponse.new(url: @browsing_context.url, status: 200)
73
- end
74
-
75
- # lib/puppeteer/bidi/keyboard.rb
76
- def perform_actions(action_list)
77
- @browsing_context.perform_actions([
78
- {
79
- type: 'key',
80
- id: 'default keyboard',
81
- actions: action_list
82
- }
83
- ]).wait # .wait call
84
- end
85
- ```
86
-
87
- **Key points:**
88
- - Always calls `.wait` on Core layer methods
89
- - Returns plain Ruby objects (String, Hash, etc.), not Async::Task
90
- - User-facing API is synchronous
91
-
92
- ## Common Mistakes and How to Fix Them
93
-
94
- ### Mistake 1: Forgetting .wait on Core Layer Methods
95
-
96
- ```ruby
97
- # WRONG: Missing .wait
98
- def query_selector(selector)
99
- result = @realm.call_function(...)
100
- if result['type'] == 'exception' # Error: undefined method '[]' for Async::Task
101
- # ...
102
- end
103
- end
104
-
105
- # CORRECT: Add .wait
106
- def query_selector(selector)
107
- result = @realm.call_function(...).wait # Add .wait here
108
- if result['type'] == 'exception'
109
- # ...
110
- end
111
- end
112
- ```
113
-
114
- ### Mistake 2: Using send_command Instead of async_send_command in Core Layer
115
-
116
- ```ruby
117
- # WRONG: Using send_command (doesn't exist)
118
- def perform_actions(actions)
119
- session.send_command('input.performActions', {...}) # Error: undefined method
120
- end
121
-
122
- # CORRECT: Use async_send_command
123
- def perform_actions(actions)
124
- session.async_send_command('input.performActions', {...})
125
- end
126
- ```
127
-
128
- ### Mistake 3: Not Calling .wait on All Core Methods
129
-
130
- ```ruby
131
- # WRONG: Multiple Core calls, only one .wait
132
- def get_properties
133
- result = @realm.call_function(...).wait # OK
134
- props_result = @realm.call_function(...) # Missing .wait!
135
- if props_result['type'] == 'exception' # Error
136
- # ...
137
- end
138
- end
139
-
140
- # CORRECT: Add .wait to all Core calls
141
- def get_properties
142
- result = @realm.call_function(...).wait
143
- props_result = @realm.call_function(...).wait # Add .wait
144
- if props_result['type'] == 'exception'
145
- # ...
146
- end
147
- end
148
- ```
149
-
150
- ## Checklist for Adding New Methods
151
-
152
- When adding new methods to the Upper Layer:
153
-
154
- 1. Identify all calls to Core layer methods
155
- 2. Add `.wait` to each Core layer method call
156
- 3. Verify the method returns plain Ruby objects, not Async::Task
157
- 4. Test with integration specs
158
-
159
- When adding new methods to the Core Layer:
160
-
161
- 1. Use `session.async_send_command` (not `send_command`)
162
- 2. Wrap in `Async do ... end` if needed
163
- 3. Return `Async::Task` (don't call .wait)
164
- 4. Document that callers must call .wait
165
-
166
- ## Files Modified for Async Architecture
167
-
168
- **Core Layer:**
169
- - `lib/puppeteer/bidi/core/session.rb` - Changed `send_command` → `async_send_command`
170
- - `lib/puppeteer/bidi/core/browsing_context.rb` - All methods use `async_send_command`
171
- - `lib/puppeteer/bidi/core/realm.rb` - Methods return `Async::Task`
172
-
173
- **Upper Layer:**
174
- - `lib/puppeteer/bidi/realm.rb` - Added `.wait` to `execute_with_core`, `call_function`
175
- - `lib/puppeteer/bidi/frame.rb` - Added `.wait` to `goto`
176
- - `lib/puppeteer/bidi/js_handle.rb` - Added `.wait` to `dispose`, `get_property`, `get_properties`, `as_element`
177
- - `lib/puppeteer/bidi/element_handle.rb` - Added `.wait` to `query_selector_all`, `eval_on_selector_all`
178
- - `lib/puppeteer/bidi/keyboard.rb` - Added `.wait` to `perform_actions`
179
- - `lib/puppeteer/bidi/mouse.rb` - Added `.wait` to `perform_actions`
180
- - `lib/puppeteer/bidi/page.rb` - Added `.wait` to `capture_screenshot`, `close`, `set_viewport`, `set_javascript_enabled`