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.
- checksums.yaml +4 -4
- data/lib/puppeteer/bidi/browser.rb +15 -0
- data/lib/puppeteer/bidi/version.rb +1 -1
- data/sig/puppeteer/bidi/browser.rbs +3 -0
- metadata +1 -24
- data/CLAUDE/README.md +0 -158
- data/CLAUDE/async_programming.md +0 -158
- data/CLAUDE/click_implementation.md +0 -340
- data/CLAUDE/core_layer_gotchas.md +0 -136
- data/CLAUDE/error_handling.md +0 -232
- data/CLAUDE/expose_function_implementation.md +0 -271
- data/CLAUDE/file_chooser.md +0 -95
- data/CLAUDE/frame_architecture.md +0 -346
- data/CLAUDE/javascript_evaluation.md +0 -341
- data/CLAUDE/jshandle_implementation.md +0 -505
- data/CLAUDE/keyboard_implementation.md +0 -250
- data/CLAUDE/mouse_implementation.md +0 -140
- data/CLAUDE/navigation_waiting.md +0 -234
- data/CLAUDE/porting_puppeteer.md +0 -234
- data/CLAUDE/query_handler.md +0 -194
- data/CLAUDE/reactor_runner.md +0 -111
- data/CLAUDE/rspec_pending_vs_skip.md +0 -262
- data/CLAUDE/selector_evaluation.md +0 -198
- data/CLAUDE/test_server_routes.md +0 -263
- data/CLAUDE/testing_strategy.md +0 -236
- data/CLAUDE/two_layer_architecture.md +0 -180
- data/CLAUDE/wrapped_element_click.md +0 -247
- data/CLAUDE.md +0 -238
|
@@ -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)
|
data/CLAUDE/testing_strategy.md
DELETED
|
@@ -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`
|