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,341 +0,0 @@
1
- # JavaScript Evaluation Implementation
2
-
3
- This document details the implementation of JavaScript evaluation in Page and Frame classes, including script detection logic, argument serialization, and result deserialization.
4
-
5
- ### Page.evaluate and Frame.evaluate
6
-
7
- The `evaluate` method supports both JavaScript expressions and functions with proper argument serialization.
8
-
9
- #### Detection Logic
10
-
11
- The implementation distinguishes between three types of JavaScript code:
12
-
13
- 1. **Expressions**: Simple JavaScript code
14
- 2. **Functions**: Arrow functions or function declarations (use `script.callFunction`)
15
- 3. **IIFE**: Immediately Invoked Function Expressions (use `script.evaluate`)
16
-
17
- ```ruby
18
- # Expression - uses script.evaluate
19
- page.evaluate('7 * 3') # => 21
20
-
21
- # Function - uses script.callFunction
22
- page.evaluate('(a, b) => a + b', 3, 4) # => 7
23
-
24
- # IIFE - uses script.evaluate (not script.callFunction)
25
- page.evaluate('(() => document.title)()') # => "Page Title"
26
- ```
27
-
28
- #### IIFE Detection Pattern
29
-
30
- **Critical**: IIFE must be detected and treated as expressions:
31
-
32
- ```ruby
33
- # Check if it's an IIFE - ends with () after the function body
34
- is_iife = script_trimmed.match?(/\)\s*\(\s*\)\s*\z/)
35
-
36
- # Only treat as function if not IIFE
37
- is_function = !is_iife && (
38
- script_trimmed.match?(/\A\s*(?:async\s+)?(?:\(.*?\)|[a-zA-Z_$][\w$]*)\s*=>/) ||
39
- script_trimmed.match?(/\A\s*(?:async\s+)?function\s*\w*\s*\(/)
40
- )
41
- ```
42
-
43
- **Why this matters**: IIFE like `(() => {...})()` looks like a function but must be evaluated as an expression. Using `script.callFunction` on IIFE causes syntax errors.
44
-
45
- #### Argument Serialization
46
-
47
- Arguments are serialized to BiDi `LocalValue` format:
48
-
49
- ```ruby
50
- # Special numbers
51
- { type: 'number', value: 'NaN' }
52
- { type: 'number', value: 'Infinity' }
53
- { type: 'number', value: '-0' }
54
-
55
- # Collections
56
- { type: 'array', value: [...] }
57
- { type: 'object', value: [[key, value], ...] }
58
- { type: 'map', value: [[key, value], ...] }
59
- ```
60
-
61
- #### Result Deserialization
62
-
63
- BiDi returns results in special format that must be deserialized:
64
-
65
- ```ruby
66
- # BiDi response format
67
- {
68
- "type" => "success",
69
- "realm" => "...",
70
- "result" => {
71
- "type" => "number",
72
- "value" => 42
73
- }
74
- }
75
-
76
- # Extract and deserialize
77
- actual_result = result['result'] || result
78
- deserialize_result(actual_result) # => 42
79
- ```
80
-
81
- #### Exception Handling
82
-
83
- Exceptions from JavaScript are returned in the result, not thrown by BiDi:
84
-
85
- ```ruby
86
- if result['type'] == 'exception'
87
- exception_details = result['exceptionDetails']
88
- text = exception_details['text'] # "ReferenceError: notExistingObject is not defined"
89
- raise text
90
- end
91
- ```
92
-
93
- ### Core::Realm Return Values
94
-
95
- **Important**: Core::Realm methods return the **complete BiDi result**, not just the value:
96
-
97
- ```ruby
98
- # Core::Realm.call_function returns:
99
- {
100
- "type" => "success" | "exception",
101
- "realm" => "...",
102
- "result" => {...} | nil,
103
- "exceptionDetails" => {...} | nil
104
- }
105
-
106
- # NOT result['result'] (this was a bug that was fixed)
107
- ```
108
-
109
- ### Testing Strategy
110
-
111
- #### Integration Tests Organization
112
-
113
- ```
114
- spec/
115
- ├── unit/ # Fast unit tests (future)
116
- ├── integration/ # Browser automation tests
117
- │ ├── examples/ # Example-based tests
118
- │ │ └── screenshot_spec.rb
119
- │ └── screenshot_spec.rb # Feature test suites
120
- ├── assets/ # Test HTML/CSS/JS files
121
- │ ├── grid.html
122
- │ ├── scrollbar.html
123
- │ ├── empty.html
124
- │ └── digits/*.png
125
- ├── golden-firefox/ # Reference images
126
- │ └── screenshot-*.png
127
- └── support/ # Test utilities
128
- ├── test_server.rb
129
- └── golden_comparator.rb
130
- ```
131
-
132
- #### Implemented Screenshot Tests
133
-
134
- All 12 tests ported from [Puppeteer's screenshot.spec.ts](https://github.com/puppeteer/puppeteer/blob/main/test/src/screenshot.spec.ts):
135
-
136
- 1. **should work** - Basic screenshot functionality
137
- 2. **should clip rect** - Clipping specific region
138
- 3. **should get screenshot bigger than the viewport** - Offscreen clip with captureBeyondViewport
139
- 4. **should clip bigger than the viewport without "captureBeyondViewport"** - Viewport coordinate transformation
140
- 5. **should run in parallel** - Thread-safe parallel screenshots on single page
141
- 6. **should take fullPage screenshots** - Full page with document origin
142
- 7. **should take fullPage screenshots without captureBeyondViewport** - Full page with viewport resize
143
- 8. **should run in parallel in multiple pages** - Concurrent screenshots across multiple pages
144
- 9. **should work with odd clip size on Retina displays** - Odd pixel dimensions (11x11)
145
- 10. **should return base64** - Base64 encoding verification
146
- 11. **should take fullPage screenshots when defaultViewport is null** - No explicit viewport
147
- 12. **should restore to original viewport size** - Viewport restoration after fullPage
148
-
149
- Run tests:
150
- ```bash
151
- bundle exec rspec spec/integration/screenshot_spec.rb
152
- # Expected: 12 examples, 0 failures (completes in ~8 seconds with optimized spec_helper)
153
- ```
154
-
155
- #### Test Performance Optimization
156
-
157
- **Critical**: Integration tests are ~19x faster with browser reuse strategy.
158
-
159
- ##### Before Optimization (Per-test Browser Launch)
160
- ```ruby
161
- def with_test_state(**options)
162
- server = TestServer::Server.new
163
- server.start
164
-
165
- with_browser(**options) do |browser| # New browser per test!
166
- context = browser.default_browser_context
167
- page = browser.new_page
168
- yield(page: page, server: server, browser: browser, context: context)
169
- end
170
- ensure
171
- server.stop
172
- end
173
- ```
174
-
175
- **Performance**: ~195 seconds for 35 tests (browser launch overhead × 35)
176
-
177
- ##### After Optimization (Shared Browser)
178
- ```ruby
179
- # In spec_helper.rb
180
- config.before(:suite) do
181
- if RSpec.configuration.files_to_run.any? { |f| f.include?('spec/integration') }
182
- $shared_browser = Puppeteer::Bidi.launch_browser_instance(headless: headless_mode?)
183
- $shared_test_server = TestServer::Server.new
184
- $shared_test_server.start
185
- end
186
- end
187
-
188
- def with_test_state(**options)
189
- if $shared_browser && options.empty?
190
- # Create new page (tab) per test
191
- page = $shared_browser.new_page
192
- context = $shared_browser.default_browser_context
193
-
194
- begin
195
- yield(page: page, server: $shared_test_server, browser: $shared_browser, context: context)
196
- ensure
197
- page.close unless page.closed? # Clean up tab
198
- end
199
- else
200
- # Fall back to per-test browser for custom options
201
- end
202
- end
203
- ```
204
-
205
- **Performance**: ~10 seconds for 35 tests (1 browser launch + 35 tab creations)
206
-
207
- ##### Performance Results
208
-
209
- | Test Suite | Before | After | Improvement |
210
- |------------|--------|-------|-------------|
211
- | **evaluation_spec (23 tests)** | 127s | **7.17s** | **17.7x faster** |
212
- | **screenshot_spec (12 tests)** | 68s | **8.47s** | **8.0x faster** |
213
- | **Combined (35 tests)** | 195s | **10.33s** | **18.9x faster** 🚀 |
214
-
215
- **Key Benefits**:
216
- - Browser launch only once per suite
217
- - Each test gets fresh page (tab) for isolation
218
- - Cleanup handled automatically
219
- - Backward compatible (custom options fall back to per-test browser)
220
-
221
- #### Environment Variables
222
-
223
- ```bash
224
- HEADLESS=false # Run browser in non-headless mode for debugging
225
- ```
226
-
227
- ### Debugging Techniques
228
-
229
- #### 1. Save Screenshots for Inspection
230
-
231
- ```ruby
232
- # In golden_comparator.rb
233
- def save_screenshot(screenshot_base64, filename)
234
- output_dir = File.join(__dir__, '../output')
235
- FileUtils.mkdir_p(output_dir)
236
- File.binwrite(File.join(output_dir, filename),
237
- Base64.decode64(screenshot_base64))
238
- end
239
- ```
240
-
241
- #### 2. Compare Images Pixel-by-Pixel
242
-
243
- ```ruby
244
- cat > /tmp/compare.rb << 'EOF'
245
- require 'chunky_png'
246
-
247
- golden = ChunkyPNG::Image.from_file('spec/golden-firefox/screenshot.png')
248
- actual = ChunkyPNG::Image.from_file('spec/output/debug.png')
249
-
250
- diff_count = 0
251
- (0...golden.height).each do |y|
252
- (0...golden.width).each do |x|
253
- if golden[x, y] != actual[x, y]
254
- diff_count += 1
255
- puts "Diff at (#{x}, #{y})" if diff_count <= 10
256
- end
257
- end
258
- end
259
- puts "Total: #{diff_count} pixels differ"
260
- EOF
261
- ruby /tmp/compare.rb
262
- ```
263
-
264
- #### 3. Debug BiDi Responses
265
-
266
- ```ruby
267
- # Temporarily add debugging
268
- result = @browsing_context.default_realm.evaluate(script, true)
269
- puts "BiDi result: #{result.inspect}"
270
- deserialize_result(result)
271
- ```
272
-
273
- ### Common Pitfalls and Solutions
274
-
275
- #### 1. BiDi Protocol Differences
276
-
277
- **Problem:** BiDi `origin` parameter behavior differs from expectations
278
-
279
- **Solution:** Consult BiDi spec and test both `'document'` and `'viewport'` origins
280
-
281
- ```ruby
282
- # document: Absolute coordinates in full page
283
- # viewport: Relative to current viewport
284
- options[:origin] = capture_beyond_viewport ? 'document' : 'viewport'
285
- ```
286
-
287
- #### 2. Image Comparison Failures
288
-
289
- **Problem:** Golden images don't match exactly (1-2 pixel differences)
290
-
291
- **Solution:** Implement tolerance in comparison
292
-
293
- ```ruby
294
- # Allow small rendering differences (±1 RGB per channel)
295
- compare_with_golden(screenshot, 'golden.png', pixel_threshold: 1)
296
- ```
297
-
298
- #### 3. Viewport State Management
299
-
300
- **Problem:** Viewport not restored after fullPage screenshot
301
-
302
- **Solution:** Use `ensure` block
303
-
304
- ```ruby
305
- begin
306
- set_viewport(full_page_dimensions)
307
- screenshot = capture_screenshot(...)
308
- ensure
309
- set_viewport(original_viewport) if original_viewport
310
- end
311
- ```
312
-
313
- #### 4. Thread Safety
314
-
315
- **Problem:** Parallel screenshots cause race conditions
316
-
317
- **Solution:** BiDi protocol handles this naturally - test with threads
318
-
319
- ```ruby
320
- threads = (0...3).map do |i|
321
- Thread.new { page.screenshot(clip: {...}) }
322
- end
323
- screenshots = threads.map(&:value)
324
- ```
325
-
326
- ### Documentation References
327
-
328
- **Essential reading for implementation:**
329
-
330
- 1. **WebDriver BiDi Spec**: https://w3c.github.io/webdriver-bidi/
331
- 2. **Puppeteer Source**: https://github.com/puppeteer/puppeteer
332
- 3. **Puppeteer BiDi Tests**: https://github.com/puppeteer/puppeteer/tree/main/test/src
333
- 4. **Firefox BiDi Impl**: Check Firefox implementation notes for quirks
334
-
335
- **Reference implementation workflow:**
336
- 1. Find corresponding Puppeteer test in `test/src/`
337
- 2. Read TypeScript implementation in `packages/puppeteer-core/src/`
338
- 3. Check BiDi spec for protocol details
339
- 4. Implement Ruby version maintaining same logic
340
- 5. Download golden images and verify pixel-perfect match (with tolerance)
341
-