puppeteer-ruby 0.45.6 → 0.50.0.alpha5

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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/AGENTS.md +169 -0
  4. data/CLAUDE/README.md +41 -0
  5. data/CLAUDE/architecture.md +253 -0
  6. data/CLAUDE/cdp_protocol.md +230 -0
  7. data/CLAUDE/concurrency.md +216 -0
  8. data/CLAUDE/porting_puppeteer.md +575 -0
  9. data/CLAUDE/rbs_type_checking.md +101 -0
  10. data/CLAUDE/spec_migration_plans.md +1041 -0
  11. data/CLAUDE/testing.md +278 -0
  12. data/CLAUDE.md +242 -0
  13. data/README.md +8 -0
  14. data/Rakefile +7 -0
  15. data/Steepfile +28 -0
  16. data/docs/api_coverage.md +105 -56
  17. data/lib/puppeteer/aria_query_handler.rb +3 -2
  18. data/lib/puppeteer/async_utils.rb +214 -0
  19. data/lib/puppeteer/browser.rb +98 -56
  20. data/lib/puppeteer/browser_connector.rb +18 -3
  21. data/lib/puppeteer/browser_context.rb +196 -3
  22. data/lib/puppeteer/browser_runner.rb +18 -10
  23. data/lib/puppeteer/cdp_session.rb +67 -23
  24. data/lib/puppeteer/chrome_target_manager.rb +65 -40
  25. data/lib/puppeteer/connection.rb +55 -36
  26. data/lib/puppeteer/console_message.rb +9 -1
  27. data/lib/puppeteer/console_patch.rb +47 -0
  28. data/lib/puppeteer/css_coverage.rb +5 -3
  29. data/lib/puppeteer/custom_query_handler.rb +80 -33
  30. data/lib/puppeteer/define_async_method.rb +31 -37
  31. data/lib/puppeteer/dialog.rb +47 -14
  32. data/lib/puppeteer/element_handle.rb +231 -62
  33. data/lib/puppeteer/emulation_manager.rb +1 -1
  34. data/lib/puppeteer/env.rb +1 -1
  35. data/lib/puppeteer/errors.rb +25 -2
  36. data/lib/puppeteer/event_callbackable.rb +15 -0
  37. data/lib/puppeteer/events.rb +4 -0
  38. data/lib/puppeteer/execution_context.rb +148 -3
  39. data/lib/puppeteer/file_chooser.rb +6 -0
  40. data/lib/puppeteer/frame.rb +162 -91
  41. data/lib/puppeteer/frame_manager.rb +69 -48
  42. data/lib/puppeteer/http_request.rb +114 -38
  43. data/lib/puppeteer/http_response.rb +24 -7
  44. data/lib/puppeteer/isolated_world.rb +64 -41
  45. data/lib/puppeteer/js_coverage.rb +5 -3
  46. data/lib/puppeteer/js_handle.rb +58 -16
  47. data/lib/puppeteer/keyboard.rb +30 -17
  48. data/lib/puppeteer/launcher/browser_options.rb +3 -1
  49. data/lib/puppeteer/launcher/chrome.rb +8 -5
  50. data/lib/puppeteer/launcher/launch_options.rb +7 -2
  51. data/lib/puppeteer/launcher.rb +4 -8
  52. data/lib/puppeteer/lifecycle_watcher.rb +38 -22
  53. data/lib/puppeteer/mouse.rb +273 -64
  54. data/lib/puppeteer/network_event_manager.rb +7 -0
  55. data/lib/puppeteer/network_manager.rb +393 -112
  56. data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
  57. data/lib/puppeteer/page.rb +568 -226
  58. data/lib/puppeteer/puppeteer.rb +171 -64
  59. data/lib/puppeteer/query_handler_manager.rb +112 -16
  60. data/lib/puppeteer/reactor_runner.rb +247 -0
  61. data/lib/puppeteer/remote_object.rb +127 -47
  62. data/lib/puppeteer/target.rb +74 -27
  63. data/lib/puppeteer/task_manager.rb +3 -1
  64. data/lib/puppeteer/timeout_helper.rb +6 -10
  65. data/lib/puppeteer/touch_handle.rb +39 -0
  66. data/lib/puppeteer/touch_screen.rb +72 -22
  67. data/lib/puppeteer/tracing.rb +3 -3
  68. data/lib/puppeteer/version.rb +1 -1
  69. data/lib/puppeteer/wait_task.rb +264 -101
  70. data/lib/puppeteer/web_socket.rb +2 -2
  71. data/lib/puppeteer/web_socket_transport.rb +91 -27
  72. data/lib/puppeteer/web_worker.rb +175 -0
  73. data/lib/puppeteer.rb +20 -4
  74. data/puppeteer-ruby.gemspec +15 -11
  75. data/sig/_external.rbs +8 -0
  76. data/sig/_supplementary.rbs +314 -0
  77. data/sig/puppeteer/browser.rbs +166 -0
  78. data/sig/puppeteer/cdp_session.rbs +64 -0
  79. data/sig/puppeteer/dialog.rbs +41 -0
  80. data/sig/puppeteer/element_handle.rbs +305 -0
  81. data/sig/puppeteer/execution_context.rbs +87 -0
  82. data/sig/puppeteer/frame.rbs +226 -0
  83. data/sig/puppeteer/http_request.rbs +214 -0
  84. data/sig/puppeteer/http_response.rbs +89 -0
  85. data/sig/puppeteer/js_handle.rbs +64 -0
  86. data/sig/puppeteer/keyboard.rbs +40 -0
  87. data/sig/puppeteer/mouse.rbs +113 -0
  88. data/sig/puppeteer/page.rbs +515 -0
  89. data/sig/puppeteer/puppeteer.rbs +98 -0
  90. data/sig/puppeteer/remote_object.rbs +78 -0
  91. data/sig/puppeteer/touch_handle.rbs +21 -0
  92. data/sig/puppeteer/touch_screen.rbs +35 -0
  93. data/sig/puppeteer/web_worker.rbs +83 -0
  94. metadata +116 -45
  95. data/CHANGELOG.md +0 -397
  96. data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
  97. data/lib/puppeteer/firefox_target_manager.rb +0 -157
  98. data/lib/puppeteer/launcher/firefox.rb +0 -453
@@ -0,0 +1,575 @@
1
+ # Porting from TypeScript Puppeteer
2
+
3
+ This guide explains how to port features from the TypeScript Puppeteer to puppeteer-ruby.
4
+
5
+ ## Workflow Overview
6
+
7
+ 1. **Find the TypeScript source** in [puppeteer/puppeteer](https://github.com/puppeteer/puppeteer)
8
+ 2. **Understand the CDP calls** being made
9
+ 3. **Implement in Ruby** following existing patterns
10
+ 4. **Port the tests** from Puppeteer's test suite
11
+ 5. **Update API coverage** in `docs/api_coverage.md`
12
+
13
+ ## Step 1: Find the TypeScript Source
14
+
15
+ Puppeteer source is organized in:
16
+
17
+ ```
18
+ packages/puppeteer-core/src/
19
+ ├── api/ # Public API definitions
20
+ │ ├── Page.ts
21
+ │ ├── Frame.ts
22
+ │ └── ...
23
+ ├── cdp/ # CDP implementation
24
+ │ ├── Page.ts
25
+ │ ├── Frame.ts
26
+ │ └── ...
27
+ ├── bidi/ # BiDi implementation (not needed for this project)
28
+ └── common/ # Shared utilities
29
+ ```
30
+
31
+ For CDP-based puppeteer-ruby, focus on the `cdp/` directory.
32
+
33
+ ### Example: Finding waitForSelector
34
+
35
+ ```
36
+ packages/puppeteer-core/src/
37
+ ├── api/Frame.ts # waitForSelector API definition
38
+ └── cdp/Frame.ts # CDP implementation
39
+ ```
40
+
41
+ ## Step 2: Understand CDP Calls
42
+
43
+ Read the TypeScript code to understand what CDP commands are used.
44
+
45
+ ### TypeScript Example
46
+
47
+ ```typescript
48
+ // From packages/puppeteer-core/src/cdp/Frame.ts
49
+ async click(selector: string, options?: ClickOptions): Promise<void> {
50
+ const handle = await this.$(selector);
51
+ await handle?.click(options);
52
+ await handle?.dispose();
53
+ }
54
+ ```
55
+
56
+ This shows:
57
+ 1. Query selector to find element
58
+ 2. Click the element
59
+ 3. Dispose the handle
60
+
61
+ Look deeper to find CDP calls:
62
+
63
+ ```typescript
64
+ // From ElementHandle
65
+ async click(options: ClickOptions = {}): Promise<void> {
66
+ await this.scrollIntoViewIfNeeded();
67
+ const {x, y} = await this.clickablePoint(options.offset);
68
+ await this.page.mouse.click(x, y, options);
69
+ }
70
+
71
+ // From Mouse
72
+ async click(x: number, y: number, options: MouseClickOptions = {}): Promise<void> {
73
+ await this.#client.send('Input.dispatchMouseEvent', {
74
+ type: 'mousePressed',
75
+ // ...
76
+ });
77
+ }
78
+ ```
79
+
80
+ ## Step 3: Implement in Ruby
81
+
82
+ Translate the TypeScript to idiomatic Ruby:
83
+
84
+ ### Ruby Implementation
85
+
86
+ ```ruby
87
+ # lib/puppeteer/frame.rb
88
+ def click(selector, delay: nil, button: nil, click_count: nil, count: nil)
89
+ handle = query_selector(selector)
90
+ raise ArgumentError, "No element found for selector: #{selector}" unless handle
91
+
92
+ begin
93
+ handle.click(delay: delay, button: button, click_count: click_count, count: count)
94
+ ensure
95
+ handle.dispose
96
+ end
97
+ end
98
+
99
+ # lib/puppeteer/element_handle.rb
100
+ def click(delay: nil, button: nil, click_count: nil, count: nil, offset: nil)
101
+ scroll_into_view_if_needed
102
+ point = clickable_point(offset: offset)
103
+ @page.mouse.click(point.x, point.y,
104
+ delay: delay,
105
+ button: button,
106
+ click_count: click_count,
107
+ count: count
108
+ )
109
+ end
110
+
111
+ # lib/puppeteer/mouse.rb
112
+ def click(x, y, delay: nil, button: nil, click_count: nil, count: nil)
113
+ move(x, y)
114
+ down(button: button, click_count: click_count)
115
+ sleep(delay / 1000.0) if delay
116
+ up(button: button, click_count: click_count)
117
+ end
118
+ ```
119
+
120
+ Note: `click_count` is deprecated (mirrors Puppeteer's `clickCount` deprecation). Use `count` for multiple clicks and let `click_count` default to `count`.
121
+
122
+ ### Mouse Button Types
123
+
124
+ The `button` parameter accepts these values (defined in `Puppeteer::Mouse::Button`):
125
+
126
+ | Value | Button Code | Description |
127
+ |-------|-------------|-------------|
128
+ | `'left'` | 0 | Primary button (default) |
129
+ | `'right'` | 2 | Secondary button (context menu) |
130
+ | `'middle'` | 1 | Auxiliary button (wheel click) |
131
+ | `'back'` | 3 | Browser back button |
132
+ | `'forward'` | 4 | Browser forward button |
133
+
134
+ ### Key Translation Patterns
135
+
136
+ | TypeScript | Ruby |
137
+ |------------|------|
138
+ | `async/await` | Direct method calls (current), `.wait` (after migration) |
139
+ | `Promise.all([...])` | Execute in sequence or use `Concurrent::Promises.zip` |
140
+ | `options: {...}` | Keyword arguments `(key: value)` |
141
+ | `options?.key` | `options&.[](:key)` or explicit nil check |
142
+ | `throw new Error()` | `raise ErrorClass, 'message'` |
143
+ | `try/finally` | `begin/ensure` |
144
+
145
+ ## Step 4: Port Tests
146
+
147
+ Find corresponding tests in Puppeteer's test suite:
148
+
149
+ ```
150
+ test/src/ # Test specs
151
+ ├── keyboard.spec.ts
152
+ ├── click.spec.ts
153
+ ├── page.spec.ts
154
+ └── ...
155
+ test/assets/ # Test fixtures (HTML, JS, CSS)
156
+ ├── input/
157
+ │ ├── keyboard.html
158
+ │ ├── textarea.html
159
+ │ └── button.html
160
+ └── ...
161
+ ```
162
+
163
+ ### Test File Comparison Workflow
164
+
165
+ To verify test alignment between TypeScript and Ruby:
166
+
167
+ 1. **Fetch TypeScript test structure:**
168
+ ```
169
+ https://raw.githubusercontent.com/puppeteer/puppeteer/main/test/src/page.spec.ts
170
+ ```
171
+
172
+ 2. **Compare describe/it block titles** between files, checking:
173
+ - Order matches
174
+ - Names match (accounting for Ruby naming conventions)
175
+ - No missing tests
176
+
177
+ 3. **Handle differences:**
178
+ - **TypeScript-only tests**: Add to Ruby spec with `skip('Not implemented')` or implement
179
+ - **Ruby-only tests**: Move to `*_ext_spec.rb` file
180
+
181
+ ### Key Test File Pairs
182
+
183
+ | TypeScript | Ruby | Ruby Extension |
184
+ |------------|------|----------------|
185
+ | `test/src/page.spec.ts` | `spec/integration/page_spec.rb` | `page_ext_spec.rb` |
186
+ | `test/src/frame.spec.ts` | `spec/integration/frame_spec.rb` | `frame_ext_spec.rb` |
187
+ | `test/src/keyboard.spec.ts` | `spec/integration/keyboard_spec.rb` | `keyboard_ext_spec.rb` |
188
+ | `test/src/click.spec.ts` | `spec/integration/click_spec.rb` | - |
189
+
190
+ Note: Some tests (e.g., `BrowserContext#override_permissions`) may be split into separate files like `browser_context_permissions_spec.rb` if they represent distinct features.
191
+
192
+ ### Porting Principles
193
+
194
+ 1. **Preserve test order** - Keep `it` blocks in the exact same order as upstream
195
+ 2. **Preserve test names** - Use the same test descriptions
196
+ 3. **Preserve test structure** - Don't add extra `context`/`describe` wrappers
197
+ 4. **Preserve asset files** - Keep `spec/assets/` identical to upstream `test/assets/`
198
+ 5. **Separate Ruby-specific tests** - Move Ruby-only features to `*_ext_spec.rb` files
199
+
200
+ ### Ruby-Specific Tests (`*_ext_spec.rb`)
201
+
202
+ When porting tests, separate Ruby-only features into dedicated extension spec files:
203
+
204
+ ```
205
+ spec/integration/
206
+ ├── keyboard_spec.rb # Upstream port (faithful to test/src/keyboard.spec.ts)
207
+ └── keyboard_ext_spec.rb # Ruby-specific extensions
208
+ ```
209
+
210
+ **Ruby-specific features to separate:**
211
+ - Block DSL syntax: `page.keyboard { type_text('hello'); press('Enter') }`
212
+ - Nested block syntax: `press('Shift') { press('Comma') }`
213
+ - Other Ruby idioms not present in upstream
214
+
215
+ **Example `*_ext_spec.rb` structure:**
216
+
217
+ ```ruby
218
+ RSpec.describe 'Keyboard (white-box / Ruby-specific)' do
219
+ def with_textarea(&block)
220
+ with_test_state do |page:, **|
221
+ page.evaluate(<<~JAVASCRIPT)
222
+ () => {
223
+ const textarea = document.createElement('textarea');
224
+ document.body.appendChild(textarea);
225
+ textarea.focus();
226
+ }
227
+ JAVASCRIPT
228
+ block.call(page: page)
229
+ end
230
+ end
231
+
232
+ it 'should input < by pressing Shift + , using press with block' do
233
+ with_textarea do |page:|
234
+ page.keyboard do
235
+ press('Shift') { press('Comma') }
236
+ end
237
+ expect(page.evaluate("() => document.querySelector('textarea').value")).to eq('<')
238
+ end
239
+ end
240
+ end
241
+ ```
242
+
243
+ ### TypeScript to Ruby Translation
244
+
245
+ #### Test State Setup
246
+
247
+ Use `with_test_state` block to access test helpers explicitly:
248
+
249
+ ```ruby
250
+ RSpec.describe Puppeteer::Page do
251
+ it 'should click button' do
252
+ with_test_state do |page:, server:, **|
253
+ page.goto("#{server.prefix}/input/button.html")
254
+ page.click('button')
255
+ expect(page.evaluate('() => globalThis.result')).to eq('Clicked')
256
+ end
257
+ end
258
+ end
259
+ ```
260
+
261
+ Available block arguments:
262
+ - `page:` - Current `Puppeteer::Page` instance
263
+ - `server:` - Test server (use `server.prefix` for URL base)
264
+ - `https_server:` - HTTPS test server
265
+ - `browser:` - Browser instance
266
+ - `browser_context:` - BrowserContext instance
267
+
268
+ **Do NOT use** `include_context 'with test state'` - prefer explicit `with_test_state` blocks.
269
+
270
+ #### Basic Test Structure
271
+
272
+ ```typescript
273
+ // TypeScript
274
+ describe('Keyboard', function () {
275
+ it('should type into a textarea', async () => {
276
+ await page.evaluate(() => {
277
+ const textarea = document.createElement('textarea');
278
+ document.body.appendChild(textarea);
279
+ textarea.focus();
280
+ });
281
+ const text = 'Hello world. I am the text that was typed!';
282
+ await page.keyboard.type(text);
283
+ expect(
284
+ await page.evaluate(() => document.querySelector('textarea').value)
285
+ ).toBe(text);
286
+ });
287
+ });
288
+ ```
289
+
290
+ ```ruby
291
+ # Ruby
292
+ RSpec.describe Puppeteer::Keyboard do
293
+ it 'should type into a textarea' do
294
+ with_test_state do |page:, **|
295
+ page.evaluate(<<~JAVASCRIPT)
296
+ () => {
297
+ const textarea = document.createElement('textarea');
298
+ document.body.appendChild(textarea);
299
+ textarea.focus();
300
+ }
301
+ JAVASCRIPT
302
+ text = 'Hello world. I am the text that was typed!'
303
+ page.keyboard.type_text(text)
304
+ expect(page.evaluate("() => document.querySelector('textarea').value")).to eq(text)
305
+ end
306
+ end
307
+ end
308
+ ```
309
+
310
+ #### Method Name Mappings
311
+
312
+ | TypeScript | Ruby |
313
+ |------------|------|
314
+ | `page.keyboard.type(text)` | `page.keyboard.type_text(text)` |
315
+ | `page.$(selector)` | `page.query_selector(selector)` |
316
+ | `page.$$(selector)` | `page.query_selector_all(selector)` |
317
+ | `page.$eval(sel, fn)` | `page.eval_on_selector(sel, fn)` |
318
+ | `page.$$eval(sel, fn)` | `page.eval_on_selector_all(sel, fn)` |
319
+ | `element.press(key, {text: ...})` | `element.press(key)` (text option ignored) |
320
+
321
+ #### Assertion Mappings
322
+
323
+ | TypeScript (Jest) | Ruby (RSpec) |
324
+ |-------------------|--------------|
325
+ | `expect(x).toBe(y)` | `expect(x).to eq(y)` |
326
+ | `expect(x).toEqual(y)` | `expect(x).to eq(y)` |
327
+ | `expect(fn).toThrow()` | `expect { fn }.to raise_error` |
328
+ | `expect(fn).toThrow('msg')` | `expect { fn }.to raise_error(/msg/)` |
329
+
330
+ #### Platform-Specific Tests
331
+
332
+ ```typescript
333
+ // TypeScript
334
+ it('should press the meta key', async () => {
335
+ if (os.platform() !== 'darwin') {
336
+ return;
337
+ }
338
+ // test body
339
+ });
340
+ ```
341
+
342
+ ```ruby
343
+ # Ruby
344
+ it 'should press the meta key' do
345
+ skip('This test only runs on macOS.') unless Puppeteer.env.darwin?
346
+ # test body
347
+ end
348
+ ```
349
+
350
+ #### JavaScript Object Comparison
351
+
352
+ ```typescript
353
+ // TypeScript
354
+ expect(
355
+ await page.$eval('textarea', (textarea) => ({
356
+ value: textarea.value,
357
+ inputs: globalThis.inputCount,
358
+ }))
359
+ ).toEqual({ value: '嗨', inputs: 1 });
360
+ ```
361
+
362
+ ```ruby
363
+ # Ruby - JS object keys become string keys
364
+ result = page.eval_on_selector('textarea', <<~JAVASCRIPT)
365
+ (textarea) => ({
366
+ value: textarea.value,
367
+ inputs: globalThis.inputCount,
368
+ })
369
+ JAVASCRIPT
370
+ expect(result).to eq({ 'value' => '嗨', 'inputs' => 1 })
371
+ ```
372
+
373
+ #### Nested iframes with srcdoc
374
+
375
+ ```typescript
376
+ // TypeScript
377
+ await page.setContent(`
378
+ <iframe srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"></iframe>
379
+ `);
380
+ const frame = await page.waitForFrame((frame) => frame.name() === 'test');
381
+ ```
382
+
383
+ ```ruby
384
+ # Ruby
385
+ page.set_content(<<~HTML)
386
+ <iframe
387
+ srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"
388
+ ></iframe>
389
+ HTML
390
+ frame = page.wait_for_frame(predicate: ->(frame) { frame.name == 'test' })
391
+ ```
392
+
393
+ ### Test Asset Policy
394
+
395
+ Assets in `spec/assets/` must be **identical** to upstream `test/assets/`:
396
+
397
+ ```bash
398
+ # Fetch asset from upstream
399
+ wget -O spec/assets/input/keyboard.html \
400
+ https://raw.githubusercontent.com/puppeteer/puppeteer/main/test/assets/input/keyboard.html
401
+
402
+ # Verify content matches
403
+ diff spec/assets/input/keyboard.html <(curl -s https://raw.githubusercontent.com/puppeteer/puppeteer/main/test/assets/input/keyboard.html)
404
+ ```
405
+
406
+ **Never hand-edit asset files.** If a test needs different HTML:
407
+ 1. Check if upstream has the asset you need
408
+ 2. If not, create a new file with a different name
409
+ 3. If upstream changes, re-fetch the asset
410
+
411
+ ### Common Gotchas
412
+
413
+ #### 1. Event Type Differences
414
+
415
+ Upstream keyboard tests use `input` events, not `keypress`:
416
+
417
+ ```javascript
418
+ // Correct (upstream uses this)
419
+ textarea.addEventListener('input', event => {
420
+ log('input:', event.data, event.inputType, event.isComposing);
421
+ });
422
+
423
+ // Incorrect (older puppeteer-ruby had this)
424
+ textarea.addEventListener('keypress', event => {
425
+ log('Keypress:', event.key, event.code, event.which, event.charCode);
426
+ });
427
+ ```
428
+
429
+ #### 2. Modifier Key Mapping
430
+
431
+ ```ruby
432
+ # Correct: Meta on macOS, Control elsewhere
433
+ cmd_key = Puppeteer.env.darwin? ? 'Meta' : 'Control'
434
+
435
+ # Wrong: reversed mapping
436
+ cmd_key = Puppeteer.env.darwin? ? 'Control' : 'Meta'
437
+ ```
438
+
439
+ #### 3. Loop Iteration
440
+
441
+ ```typescript
442
+ // TypeScript
443
+ for (const char of 'World!') {
444
+ await page.keyboard.press('ArrowLeft');
445
+ }
446
+ ```
447
+
448
+ ```ruby
449
+ # Ruby
450
+ 'World!'.each_char { page.keyboard.press('ArrowLeft') }
451
+ ```
452
+
453
+ ## Step 5: Update API Coverage
454
+
455
+ **Important:** `docs/api_coverage.md` is auto-generated. Do not edit it manually.
456
+
457
+ To update the API coverage documentation:
458
+
459
+ ```bash
460
+ bundle exec ruby development/generate_api_coverage.rb
461
+ ```
462
+
463
+ This script reads `development/puppeteer.api.json` and compares it with the Ruby implementation to generate the coverage report.
464
+
465
+ ### How the Coverage Report Works
466
+
467
+ The script marks methods as:
468
+ - `~~methodName~~` (strikethrough) = Not implemented in Ruby
469
+ - `methodName` = Implemented with same name
470
+ - `methodName => \`#ruby_method\`` = Implemented with different name
471
+
472
+ The CI workflow "Check / documents updated" verifies this file is up-to-date. If it fails, run the command above and commit the changes.
473
+
474
+ ## Code Style Guidelines
475
+
476
+ ### Keyword Arguments
477
+
478
+ Use explicit keyword arguments in public APIs:
479
+
480
+ ```ruby
481
+ # Good
482
+ def goto(url, referer: nil, timeout: nil, wait_until: nil)
483
+ end
484
+
485
+ # Avoid
486
+ def goto(url, options = {})
487
+ end
488
+ ```
489
+
490
+ ### Error Handling
491
+
492
+ ```ruby
493
+ # Raise specific errors
494
+ raise Puppeteer::TimeoutError, "Waiting for selector timed out: #{selector}"
495
+
496
+ # Use begin/ensure for cleanup
497
+ def screenshot(path: nil)
498
+ data = capture_screenshot
499
+ File.write(path, data) if path
500
+ data
501
+ ensure
502
+ restore_viewport
503
+ end
504
+ ```
505
+
506
+ ### Nil Handling
507
+
508
+ ```ruby
509
+ # Use safe navigation
510
+ element&.click
511
+
512
+ # Explicit nil returns
513
+ def query_selector(selector)
514
+ result = @client.send_message('DOM.querySelector', selector: selector)
515
+ return nil if result['nodeId'].zero?
516
+ create_handle(result)
517
+ end
518
+ ```
519
+
520
+ ## Common Gotchas
521
+
522
+ ### 1. JavaScript vs Ruby Truthiness
523
+
524
+ ```typescript
525
+ // JavaScript: 0, '', null, undefined are falsy
526
+ if (result) { ... }
527
+
528
+ // Ruby: only nil and false are falsy
529
+ if result && !result.zero? && !result.empty?
530
+ ```
531
+
532
+ ### 2. Parameter Ordering
533
+
534
+ Puppeteer often uses options objects; Ruby prefers keyword args:
535
+
536
+ ```typescript
537
+ // TypeScript
538
+ page.screenshot({ path: 'screenshot.png', fullPage: true });
539
+
540
+ // Ruby
541
+ page.screenshot(path: 'screenshot.png', full_page: true)
542
+ ```
543
+
544
+ ### 3. Async Patterns
545
+
546
+ ```typescript
547
+ // TypeScript - concurrent
548
+ await Promise.all([
549
+ page.waitForNavigation(),
550
+ page.click('a'),
551
+ ]);
552
+
553
+ // Ruby (current) - use block pattern
554
+ page.wait_for_navigation do
555
+ page.click('a')
556
+ end
557
+ ```
558
+
559
+ ### 4. Base64 Data
560
+
561
+ ```typescript
562
+ // TypeScript
563
+ const data = await page.screenshot({ encoding: 'base64' });
564
+
565
+ // Ruby
566
+ data = page.screenshot(encoding: 'base64')
567
+ # Returns Base64 string, not binary
568
+ ```
569
+
570
+ ## Reference Resources
571
+
572
+ - [Puppeteer TypeScript source](https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core/src)
573
+ - [Puppeteer API docs](https://pptr.dev/api)
574
+ - [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
575
+ - [Puppeteer test suite](https://github.com/puppeteer/puppeteer/tree/main/test)
@@ -0,0 +1,101 @@
1
+ # RBS Type Checking
2
+
3
+ This document covers RBS type annotations and Steep type checking for puppeteer-ruby.
4
+
5
+ ## Overview
6
+
7
+ puppeteer-ruby uses [rbs-inline](https://github.com/soutaro/rbs-inline) for inline type annotations and [Steep](https://github.com/soutaro/steep) for type checking.
8
+
9
+ ### File Structure
10
+
11
+ | File/Directory | Purpose |
12
+ |----------------|---------|
13
+ | `sig/_supplementary.rbs` | Manual RBS definitions for classes not using rbs-inline |
14
+ | `sig/puppeteer/*.rbs` | Auto-generated RBS files from rbs-inline |
15
+ | `Steepfile` | Steep configuration |
16
+
17
+ ## Adding Type Annotations
18
+
19
+ ### Inline Annotations (Preferred)
20
+
21
+ Use `# @rbs` comments in Ruby source files:
22
+
23
+ ```ruby
24
+ # rbs_inline: enabled
25
+
26
+ class Foo
27
+ # @rbs name: String
28
+ # @rbs return: Integer
29
+ def bar(name)
30
+ name.length
31
+ end
32
+ end
33
+ ```
34
+
35
+ ### Manual RBS Definitions
36
+
37
+ For classes that don't use rbs-inline, add definitions to `sig/_supplementary.rbs`.
38
+
39
+ ## Common Issues
40
+
41
+ ### Duplicate Method Definitions
42
+
43
+ **Problem**: Type check fails with `DuplicatedMethodDefinition` error.
44
+
45
+ ```
46
+ sig/_supplementary.rbs:72:2: [error] Non-overloading method definition of `initialize`
47
+ in `::Puppeteer::ExecutionContext` cannot be duplicated
48
+ ```
49
+
50
+ **Cause**: The same class/method is defined in both:
51
+ - `sig/_supplementary.rbs` (manual definitions)
52
+ - `sig/puppeteer/*.rbs` (rbs-inline generated)
53
+
54
+ **Solution**: Remove the duplicate definition from `_supplementary.rbs`. When rbs-inline generates definitions for a class, remove the corresponding manual definitions from `_supplementary.rbs`.
55
+
56
+ ### String Slice Returns Nil
57
+
58
+ **Problem**: String slice operations with ranges may return `nil`:
59
+
60
+ ```ruby
61
+ # @rbs return: [String, String]
62
+ def parse_something(text)
63
+ part1 = text[1...5] # Type: String? (may be nil)
64
+ part2 = text[6..] # Type: String? (may be nil)
65
+ [part1, part2] # Error: [String?, String?] not compatible with [String, String]
66
+ end
67
+ ```
68
+
69
+ **Solution**: Add nil coalescing:
70
+
71
+ ```ruby
72
+ def parse_something(text)
73
+ part1 = text[1...5] || ''
74
+ part2 = text[6..] || ''
75
+ [part1, part2]
76
+ end
77
+ ```
78
+
79
+ ## Running Type Checks
80
+
81
+ ```bash
82
+ # Generate RBS files from inline annotations
83
+ bundle exec rake rbs
84
+
85
+ # Run Steep type check
86
+ bundle exec steep check
87
+ ```
88
+
89
+ ## CI Integration
90
+
91
+ Type checking runs automatically in the `Check` workflow on every PR. The workflow:
92
+ 1. Generates RBS files with `bundle exec rake rbs`
93
+ 2. Validates RBS syntax with `rbs validate`
94
+ 3. Runs Steep type check with `bundle exec steep check`
95
+
96
+ ## Best Practices
97
+
98
+ 1. **Prefer rbs-inline**: Use inline `# @rbs` annotations when possible instead of manual RBS files
99
+ 2. **Remove duplicates**: When adding rbs-inline to a class, remove its entry from `_supplementary.rbs`
100
+ 3. **Handle nil explicitly**: String slice and similar operations may return nil - handle this in code
101
+ 4. **Run locally first**: Always run `bundle exec steep check` locally before pushing