puppeteer-bidi 0.0.1.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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +13 -0
  4. data/CLAUDE/README.md +158 -0
  5. data/CLAUDE/async_programming.md +158 -0
  6. data/CLAUDE/click_implementation.md +340 -0
  7. data/CLAUDE/core_layer_gotchas.md +136 -0
  8. data/CLAUDE/error_handling.md +232 -0
  9. data/CLAUDE/file_chooser.md +95 -0
  10. data/CLAUDE/frame_architecture.md +346 -0
  11. data/CLAUDE/javascript_evaluation.md +341 -0
  12. data/CLAUDE/jshandle_implementation.md +505 -0
  13. data/CLAUDE/keyboard_implementation.md +250 -0
  14. data/CLAUDE/mouse_implementation.md +140 -0
  15. data/CLAUDE/navigation_waiting.md +234 -0
  16. data/CLAUDE/porting_puppeteer.md +214 -0
  17. data/CLAUDE/query_handler.md +194 -0
  18. data/CLAUDE/rspec_pending_vs_skip.md +262 -0
  19. data/CLAUDE/selector_evaluation.md +198 -0
  20. data/CLAUDE/test_server_routes.md +263 -0
  21. data/CLAUDE/testing_strategy.md +236 -0
  22. data/CLAUDE/two_layer_architecture.md +180 -0
  23. data/CLAUDE/wrapped_element_click.md +247 -0
  24. data/CLAUDE.md +185 -0
  25. data/LICENSE.txt +21 -0
  26. data/README.md +488 -0
  27. data/Rakefile +21 -0
  28. data/lib/puppeteer/bidi/async_utils.rb +151 -0
  29. data/lib/puppeteer/bidi/browser.rb +285 -0
  30. data/lib/puppeteer/bidi/browser_context.rb +53 -0
  31. data/lib/puppeteer/bidi/browser_launcher.rb +240 -0
  32. data/lib/puppeteer/bidi/connection.rb +182 -0
  33. data/lib/puppeteer/bidi/core/README.md +169 -0
  34. data/lib/puppeteer/bidi/core/browser.rb +230 -0
  35. data/lib/puppeteer/bidi/core/browsing_context.rb +601 -0
  36. data/lib/puppeteer/bidi/core/disposable.rb +69 -0
  37. data/lib/puppeteer/bidi/core/errors.rb +64 -0
  38. data/lib/puppeteer/bidi/core/event_emitter.rb +83 -0
  39. data/lib/puppeteer/bidi/core/navigation.rb +128 -0
  40. data/lib/puppeteer/bidi/core/realm.rb +315 -0
  41. data/lib/puppeteer/bidi/core/request.rb +300 -0
  42. data/lib/puppeteer/bidi/core/session.rb +153 -0
  43. data/lib/puppeteer/bidi/core/user_context.rb +208 -0
  44. data/lib/puppeteer/bidi/core/user_prompt.rb +102 -0
  45. data/lib/puppeteer/bidi/core.rb +45 -0
  46. data/lib/puppeteer/bidi/deserializer.rb +132 -0
  47. data/lib/puppeteer/bidi/element_handle.rb +602 -0
  48. data/lib/puppeteer/bidi/errors.rb +42 -0
  49. data/lib/puppeteer/bidi/file_chooser.rb +52 -0
  50. data/lib/puppeteer/bidi/frame.rb +597 -0
  51. data/lib/puppeteer/bidi/http_response.rb +23 -0
  52. data/lib/puppeteer/bidi/injected.js +1 -0
  53. data/lib/puppeteer/bidi/injected_source.rb +21 -0
  54. data/lib/puppeteer/bidi/js_handle.rb +302 -0
  55. data/lib/puppeteer/bidi/keyboard.rb +265 -0
  56. data/lib/puppeteer/bidi/lazy_arg.rb +23 -0
  57. data/lib/puppeteer/bidi/mouse.rb +170 -0
  58. data/lib/puppeteer/bidi/page.rb +613 -0
  59. data/lib/puppeteer/bidi/query_handler.rb +397 -0
  60. data/lib/puppeteer/bidi/realm.rb +242 -0
  61. data/lib/puppeteer/bidi/serializer.rb +139 -0
  62. data/lib/puppeteer/bidi/target.rb +81 -0
  63. data/lib/puppeteer/bidi/task_manager.rb +44 -0
  64. data/lib/puppeteer/bidi/timeout_settings.rb +20 -0
  65. data/lib/puppeteer/bidi/transport.rb +129 -0
  66. data/lib/puppeteer/bidi/version.rb +7 -0
  67. data/lib/puppeteer/bidi/wait_task.rb +322 -0
  68. data/lib/puppeteer/bidi.rb +49 -0
  69. data/scripts/update_injected_source.rb +57 -0
  70. data/sig/puppeteer/bidi/browser.rbs +80 -0
  71. data/sig/puppeteer/bidi/element_handle.rbs +238 -0
  72. data/sig/puppeteer/bidi/frame.rbs +205 -0
  73. data/sig/puppeteer/bidi/js_handle.rbs +90 -0
  74. data/sig/puppeteer/bidi/page.rbs +247 -0
  75. data/sig/puppeteer/bidi.rbs +15 -0
  76. metadata +176 -0
@@ -0,0 +1,198 @@
1
+ # Selector Evaluation Methods Implementation
2
+
3
+ This document explains the implementation of `eval_on_selector` and `eval_on_selector_all` methods, including delegation patterns, handle lifecycle, and performance considerations.
4
+
5
+ ### Overview
6
+
7
+ The `eval_on_selector` and `eval_on_selector_all` methods provide convenient shortcuts for querying elements and evaluating JavaScript functions on them, equivalent to Puppeteer's `$eval` and `$$eval`.
8
+
9
+ ### API Design
10
+
11
+ #### Method Naming Convention
12
+
13
+ Ruby cannot use `$` in method names, so we use descriptive alternatives:
14
+
15
+ | Puppeteer | Ruby | Description |
16
+ |-----------|------|-------------|
17
+ | `$eval` | `eval_on_selector` | Evaluate on first matching element |
18
+ | `$$eval` | `eval_on_selector_all` | Evaluate on all matching elements |
19
+
20
+ #### Implementation Hierarchy
21
+
22
+ Following Puppeteer's delegation pattern:
23
+
24
+ ```
25
+ Page#eval_on_selector(_all)
26
+ ↓ delegates to
27
+ Frame#eval_on_selector(_all)
28
+ ↓ delegates to
29
+ ElementHandle#eval_on_selector(_all) (on document)
30
+ ↓ implementation
31
+ 1. query_selector(_all) - Find element(s)
32
+ 2. Validate results
33
+ 3. evaluate() - Execute function
34
+ 4. dispose - Clean up handles
35
+ ```
36
+
37
+ ### Implementation Details
38
+
39
+ #### Page and Frame Methods
40
+
41
+ ```ruby
42
+ # lib/puppeteer/bidi/page.rb
43
+ def eval_on_selector(selector, page_function, *args)
44
+ main_frame.eval_on_selector(selector, page_function, *args)
45
+ end
46
+
47
+ # lib/puppeteer/bidi/frame.rb
48
+ def eval_on_selector(selector, page_function, *args)
49
+ document.eval_on_selector(selector, page_function, *args)
50
+ end
51
+ ```
52
+
53
+ **Design rationale**: Page and Frame act as thin wrappers, delegating to the document element handle.
54
+
55
+ #### ElementHandle#eval_on_selector
56
+
57
+ ```ruby
58
+ def eval_on_selector(selector, page_function, *args)
59
+ assert_not_disposed
60
+
61
+ element_handle = query_selector(selector)
62
+ raise SelectorNotFoundError, selector unless element_handle
63
+
64
+ begin
65
+ element_handle.evaluate(page_function, *args)
66
+ ensure
67
+ element_handle.dispose
68
+ end
69
+ end
70
+ ```
71
+
72
+ **Key points**:
73
+ - Throws `SelectorNotFoundError` if no element found (matches Puppeteer behavior)
74
+ - Uses `begin/ensure` to guarantee handle disposal
75
+ - Searches within element's subtree (not page-wide)
76
+
77
+ #### ElementHandle#eval_on_selector_all
78
+
79
+ ```ruby
80
+ def eval_on_selector_all(selector, page_function, *args)
81
+ assert_not_disposed
82
+
83
+ element_handles = query_selector_all(selector)
84
+
85
+ begin
86
+ # Create array handle in browser context
87
+ array_handle = @realm.call_function(
88
+ '(...elements) => elements',
89
+ false,
90
+ arguments: element_handles.map(&:remote_value)
91
+ )
92
+
93
+ array_js_handle = JSHandle.from(array_handle['result'], @realm)
94
+
95
+ begin
96
+ array_js_handle.evaluate(page_function, *args)
97
+ ensure
98
+ array_js_handle.dispose
99
+ end
100
+ ensure
101
+ element_handles.each(&:dispose)
102
+ end
103
+ end
104
+ ```
105
+
106
+ **Key points**:
107
+ - Returns result for empty array without error (differs from `eval_on_selector`)
108
+ - Creates array handle using spread operator trick: `(...elements) => elements`
109
+ - Nested `ensure` blocks for proper resource cleanup
110
+ - Disposes both individual element handles and array handle
111
+
112
+ ### Error Handling Differences
113
+
114
+ | Method | Behavior when no elements found |
115
+ |--------|--------------------------------|
116
+ | `eval_on_selector` | Throws `SelectorNotFoundError` |
117
+ | `eval_on_selector_all` | Returns evaluation result (e.g., `0` for `divs => divs.length`) |
118
+
119
+ This matches Puppeteer's behavior:
120
+ - `$eval`: Must find exactly one element
121
+ - `$$eval`: Works with zero or more elements
122
+
123
+ ### Usage Examples
124
+
125
+ ```ruby
126
+ # Basic usage
127
+ page.set_content('<section id="test">Hello</section>')
128
+ id = page.eval_on_selector('section', 'e => e.id')
129
+ # => "test"
130
+
131
+ # With arguments
132
+ text = page.eval_on_selector('section', '(e, suffix) => e.textContent + suffix', '!')
133
+ # => "Hello!"
134
+
135
+ # ElementHandle arguments
136
+ div = page.query_selector('div')
137
+ result = page.eval_on_selector('section', '(e, div) => e.textContent + div.textContent', div)
138
+
139
+ # eval_on_selector_all with multiple elements
140
+ page.set_content('<div>A</div><div>B</div><div>C</div>')
141
+ count = page.eval_on_selector_all('div', 'divs => divs.length')
142
+ # => 3
143
+
144
+ # Subtree search with ElementHandle
145
+ tweet = page.query_selector('.tweet')
146
+ likes = tweet.eval_on_selector('.like', 'node => node.innerText')
147
+ # Only searches within .tweet element
148
+ ```
149
+
150
+ ### Test Coverage
151
+
152
+ **Total**: 13 integration tests
153
+
154
+ **Page.eval_on_selector** (4 tests):
155
+ - Basic functionality (property access)
156
+ - Argument passing
157
+ - ElementHandle arguments
158
+ - Error on missing selector
159
+
160
+ **ElementHandle.eval_on_selector** (3 tests):
161
+ - Basic functionality
162
+ - Subtree isolation
163
+ - Error on missing selector
164
+
165
+ **Page.eval_on_selector_all** (4 tests):
166
+ - Basic functionality (array length)
167
+ - Extra arguments
168
+ - ElementHandle arguments
169
+ - Large element count (1001 elements)
170
+
171
+ **ElementHandle.eval_on_selector_all** (2 tests):
172
+ - Subtree retrieval
173
+ - Empty result handling
174
+
175
+ ### Performance Considerations
176
+
177
+ #### Handle Lifecycle
178
+
179
+ - **eval_on_selector**: Creates 1 temporary handle per call
180
+ - **eval_on_selector_all**: Creates N+1 handles (N elements + 1 array)
181
+ - All handles automatically disposed after evaluation
182
+
183
+ #### Large Element Sets
184
+
185
+ Tested with 1001 elements without issues. The implementation efficiently:
186
+ 1. Queries all elements at once
187
+ 2. Creates single array handle
188
+ 3. Evaluates function in single round-trip
189
+ 4. Disposes all handles in parallel
190
+
191
+ ### Reference Implementation
192
+
193
+ Based on Puppeteer's implementation:
194
+ - [Page.$eval](https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/api/Page.ts)
195
+ - [Frame.$eval](https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/api/Frame.ts)
196
+ - [ElementHandle.$eval](https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/api/ElementHandle.ts)
197
+ - [Test specs](https://github.com/puppeteer/puppeteer/blob/main/test/src/queryselector.spec.ts)
198
+
@@ -0,0 +1,263 @@
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)
@@ -0,0 +1,236 @@
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(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
+