puppeteer-ruby 0.45.6 → 0.50.0.alpha6

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/AGENTS.md +170 -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 +605 -0
  9. data/CLAUDE/rbs_type_checking.md +101 -0
  10. data/CLAUDE/spec_migration_plans.md +1039 -0
  11. data/CLAUDE/testing.md +278 -0
  12. data/CLAUDE.md +242 -0
  13. data/README.md +9 -0
  14. data/Rakefile +7 -0
  15. data/Steepfile +28 -0
  16. data/docs/api_coverage.md +106 -57
  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 +236 -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 +177 -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 +77 -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/locators.rb +733 -0
  54. data/lib/puppeteer/mouse.rb +273 -64
  55. data/lib/puppeteer/network_event_manager.rb +7 -0
  56. data/lib/puppeteer/network_manager.rb +393 -112
  57. data/lib/puppeteer/p_query_handler.rb +367 -0
  58. data/lib/puppeteer/p_selector_parser.rb +241 -0
  59. data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
  60. data/lib/puppeteer/page.rb +583 -226
  61. data/lib/puppeteer/puppeteer.rb +171 -64
  62. data/lib/puppeteer/query_handler_manager.rb +66 -16
  63. data/lib/puppeteer/reactor_runner.rb +247 -0
  64. data/lib/puppeteer/remote_object.rb +127 -47
  65. data/lib/puppeteer/target.rb +74 -27
  66. data/lib/puppeteer/task_manager.rb +3 -1
  67. data/lib/puppeteer/timeout_helper.rb +6 -10
  68. data/lib/puppeteer/touch_handle.rb +39 -0
  69. data/lib/puppeteer/touch_screen.rb +72 -22
  70. data/lib/puppeteer/tracing.rb +3 -3
  71. data/lib/puppeteer/version.rb +1 -1
  72. data/lib/puppeteer/wait_task.rb +264 -101
  73. data/lib/puppeteer/web_socket.rb +2 -2
  74. data/lib/puppeteer/web_socket_transport.rb +91 -27
  75. data/lib/puppeteer/web_worker.rb +175 -0
  76. data/lib/puppeteer.rb +23 -4
  77. data/puppeteer-ruby.gemspec +15 -11
  78. data/sig/_external.rbs +8 -0
  79. data/sig/_supplementary.rbs +314 -0
  80. data/sig/puppeteer/browser.rbs +166 -0
  81. data/sig/puppeteer/cdp_session.rbs +64 -0
  82. data/sig/puppeteer/dialog.rbs +41 -0
  83. data/sig/puppeteer/element_handle.rbs +308 -0
  84. data/sig/puppeteer/execution_context.rbs +87 -0
  85. data/sig/puppeteer/frame.rbs +233 -0
  86. data/sig/puppeteer/http_request.rbs +214 -0
  87. data/sig/puppeteer/http_response.rbs +89 -0
  88. data/sig/puppeteer/js_handle.rbs +64 -0
  89. data/sig/puppeteer/keyboard.rbs +40 -0
  90. data/sig/puppeteer/locators.rbs +222 -0
  91. data/sig/puppeteer/mouse.rbs +113 -0
  92. data/sig/puppeteer/p_query_handler.rbs +73 -0
  93. data/sig/puppeteer/p_selector_parser.rbs +31 -0
  94. data/sig/puppeteer/page.rbs +522 -0
  95. data/sig/puppeteer/puppeteer.rbs +98 -0
  96. data/sig/puppeteer/remote_object.rbs +78 -0
  97. data/sig/puppeteer/touch_handle.rbs +21 -0
  98. data/sig/puppeteer/touch_screen.rbs +35 -0
  99. data/sig/puppeteer/web_worker.rbs +83 -0
  100. metadata +122 -45
  101. data/CHANGELOG.md +0 -397
  102. data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
  103. data/lib/puppeteer/firefox_target_manager.rb +0 -157
  104. data/lib/puppeteer/launcher/firefox.rb +0 -453
@@ -0,0 +1,605 @@
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
+ ## Fidelity Notes
201
+
202
+ - `JSHandle#json_value`: Node.js normalizes CDP errors like "Object reference chain is too long" and
203
+ "Object couldn't be returned by value" via `ExecutionContext#rewriteError` to `undefined`. The
204
+ Ruby port keeps the same behavior by rescuing in `JSHandle#json_value` and returning `nil`.
205
+ - PSelectors/PQueryHandler: The Ruby PSelector path relies on `PQueryHandler` with the same
206
+ `IDENT_TOKEN_START` regex behavior as upstream. Keep the CSS query selector JS in a single-quoted
207
+ heredoc so the regex survives Ruby parsing. `wait_for` should use `Frame#default_timeout` to match
208
+ Node's timeout settings.
209
+
210
+ ### Ruby-Specific Tests (`*_ext_spec.rb`)
211
+
212
+ When porting tests, separate Ruby-only features into dedicated extension spec files:
213
+
214
+ ```
215
+ spec/integration/
216
+ ├── keyboard_spec.rb # Upstream port (faithful to test/src/keyboard.spec.ts)
217
+ └── keyboard_ext_spec.rb # Ruby-specific extensions
218
+ ```
219
+
220
+ **Ruby-specific features to separate:**
221
+ - Block DSL syntax: `page.keyboard { type_text('hello'); press('Enter') }`
222
+ - Nested block syntax: `press('Shift') { press('Comma') }`
223
+ - Other Ruby idioms not present in upstream
224
+
225
+ **Example `*_ext_spec.rb` structure:**
226
+
227
+ ```ruby
228
+ RSpec.describe 'Keyboard (white-box / Ruby-specific)' do
229
+ def with_textarea(&block)
230
+ with_test_state do |page:, **|
231
+ page.evaluate(<<~JAVASCRIPT)
232
+ () => {
233
+ const textarea = document.createElement('textarea');
234
+ document.body.appendChild(textarea);
235
+ textarea.focus();
236
+ }
237
+ JAVASCRIPT
238
+ block.call(page: page)
239
+ end
240
+ end
241
+
242
+ it 'should input < by pressing Shift + , using press with block' do
243
+ with_textarea do |page:|
244
+ page.keyboard do
245
+ press('Shift') { press('Comma') }
246
+ end
247
+ expect(page.evaluate("() => document.querySelector('textarea').value")).to eq('<')
248
+ end
249
+ end
250
+ end
251
+ ```
252
+
253
+ ### TypeScript to Ruby Translation
254
+
255
+ #### Test State Setup
256
+
257
+ Use `with_test_state` block to access test helpers explicitly:
258
+
259
+ ```ruby
260
+ RSpec.describe Puppeteer::Page do
261
+ it 'should click button' do
262
+ with_test_state do |page:, server:, **|
263
+ page.goto("#{server.prefix}/input/button.html")
264
+ page.click('button')
265
+ expect(page.evaluate('() => globalThis.result')).to eq('Clicked')
266
+ end
267
+ end
268
+ end
269
+ ```
270
+
271
+ Available block arguments:
272
+ - `page:` - Current `Puppeteer::Page` instance
273
+ - `server:` - Test server (use `server.prefix` for URL base)
274
+ - `https_server:` - HTTPS test server
275
+ - `browser:` - Browser instance
276
+ - `browser_context:` - BrowserContext instance
277
+
278
+ **Do NOT use** `include_context 'with test state'` - prefer explicit `with_test_state` blocks.
279
+
280
+ #### Basic Test Structure
281
+
282
+ ```typescript
283
+ // TypeScript
284
+ describe('Keyboard', function () {
285
+ it('should type into a textarea', async () => {
286
+ await page.evaluate(() => {
287
+ const textarea = document.createElement('textarea');
288
+ document.body.appendChild(textarea);
289
+ textarea.focus();
290
+ });
291
+ const text = 'Hello world. I am the text that was typed!';
292
+ await page.keyboard.type(text);
293
+ expect(
294
+ await page.evaluate(() => document.querySelector('textarea').value)
295
+ ).toBe(text);
296
+ });
297
+ });
298
+ ```
299
+
300
+ ```ruby
301
+ # Ruby
302
+ RSpec.describe Puppeteer::Keyboard do
303
+ it 'should type into a textarea' do
304
+ with_test_state do |page:, **|
305
+ page.evaluate(<<~JAVASCRIPT)
306
+ () => {
307
+ const textarea = document.createElement('textarea');
308
+ document.body.appendChild(textarea);
309
+ textarea.focus();
310
+ }
311
+ JAVASCRIPT
312
+ text = 'Hello world. I am the text that was typed!'
313
+ page.keyboard.type_text(text)
314
+ expect(page.evaluate("() => document.querySelector('textarea').value")).to eq(text)
315
+ end
316
+ end
317
+ end
318
+ ```
319
+
320
+ #### Method Name Mappings
321
+
322
+ | TypeScript | Ruby |
323
+ |------------|------|
324
+ | `page.keyboard.type(text)` | `page.keyboard.type_text(text)` |
325
+ | `page.$(selector)` | `page.query_selector(selector)` |
326
+ | `page.$$(selector)` | `page.query_selector_all(selector)` |
327
+ | `page.$eval(sel, fn)` | `page.eval_on_selector(sel, fn)` |
328
+ | `page.$$eval(sel, fn)` | `page.eval_on_selector_all(sel, fn)` |
329
+ | `element.press(key, {text: ...})` | `element.press(key)` (text option ignored) |
330
+
331
+ #### Assertion Mappings
332
+
333
+ | TypeScript (Jest) | Ruby (RSpec) |
334
+ |-------------------|--------------|
335
+ | `expect(x).toBe(y)` | `expect(x).to eq(y)` |
336
+ | `expect(x).toEqual(y)` | `expect(x).to eq(y)` |
337
+ | `expect(fn).toThrow()` | `expect { fn }.to raise_error` |
338
+ | `expect(fn).toThrow('msg')` | `expect { fn }.to raise_error(/msg/)` |
339
+
340
+ #### Platform-Specific Tests
341
+
342
+ ```typescript
343
+ // TypeScript
344
+ it('should press the meta key', async () => {
345
+ if (os.platform() !== 'darwin') {
346
+ return;
347
+ }
348
+ // test body
349
+ });
350
+ ```
351
+
352
+ ```ruby
353
+ # Ruby
354
+ it 'should press the meta key' do
355
+ skip('This test only runs on macOS.') unless Puppeteer.env.darwin?
356
+ # test body
357
+ end
358
+ ```
359
+
360
+ #### JavaScript Object Comparison
361
+
362
+ ```typescript
363
+ // TypeScript
364
+ expect(
365
+ await page.$eval('textarea', (textarea) => ({
366
+ value: textarea.value,
367
+ inputs: globalThis.inputCount,
368
+ }))
369
+ ).toEqual({ value: '嗨', inputs: 1 });
370
+ ```
371
+
372
+ ```ruby
373
+ # Ruby - JS object keys become string keys
374
+ result = page.eval_on_selector('textarea', <<~JAVASCRIPT)
375
+ (textarea) => ({
376
+ value: textarea.value,
377
+ inputs: globalThis.inputCount,
378
+ })
379
+ JAVASCRIPT
380
+ expect(result).to eq({ 'value' => '嗨', 'inputs' => 1 })
381
+ ```
382
+
383
+ #### Nested iframes with srcdoc
384
+
385
+ ```typescript
386
+ // TypeScript
387
+ await page.setContent(`
388
+ <iframe srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"></iframe>
389
+ `);
390
+ const frame = await page.waitForFrame((frame) => frame.name() === 'test');
391
+ ```
392
+
393
+ ```ruby
394
+ # Ruby
395
+ page.set_content(<<~HTML)
396
+ <iframe
397
+ srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"
398
+ ></iframe>
399
+ HTML
400
+ frame = page.wait_for_frame(predicate: ->(frame) { frame.name == 'test' })
401
+ ```
402
+
403
+ ### Test Asset Policy
404
+
405
+ Assets in `spec/assets/` must be **identical** to upstream `test/assets/`:
406
+
407
+ ```bash
408
+ # Fetch asset from upstream
409
+ wget -O spec/assets/input/keyboard.html \
410
+ https://raw.githubusercontent.com/puppeteer/puppeteer/main/test/assets/input/keyboard.html
411
+
412
+ # Verify content matches
413
+ diff spec/assets/input/keyboard.html <(curl -s https://raw.githubusercontent.com/puppeteer/puppeteer/main/test/assets/input/keyboard.html)
414
+ ```
415
+
416
+ **Never hand-edit asset files.** If a test needs different HTML:
417
+ 1. Check if upstream has the asset you need
418
+ 2. If not, create a new file with a different name
419
+ 3. If upstream changes, re-fetch the asset
420
+
421
+ ### Common Gotchas
422
+
423
+ #### 0. AbortSignal Not Supported
424
+
425
+ **Do NOT port `signal` parameters from upstream Puppeteer.**
426
+
427
+ ```typescript
428
+ // TypeScript - has signal parameter
429
+ async click(options?: {signal?: AbortSignal}): Promise<void> {
430
+ // ...
431
+ }
432
+ ```
433
+
434
+ ```ruby
435
+ # Ruby - do NOT include signal parameter
436
+ def click(delay: nil, button: nil)
437
+ # ...
438
+ end
439
+ ```
440
+
441
+ Ruby's concurrency model doesn't align with JavaScript's AbortController/AbortSignal pattern. Use timeout parameters instead for cancellation.
442
+
443
+ #### 1. Event Type Differences
444
+
445
+ Upstream keyboard tests use `input` events, not `keypress`:
446
+
447
+ ```javascript
448
+ // Correct (upstream uses this)
449
+ textarea.addEventListener('input', event => {
450
+ log('input:', event.data, event.inputType, event.isComposing);
451
+ });
452
+
453
+ // Incorrect (older puppeteer-ruby had this)
454
+ textarea.addEventListener('keypress', event => {
455
+ log('Keypress:', event.key, event.code, event.which, event.charCode);
456
+ });
457
+ ```
458
+
459
+ #### 2. Modifier Key Mapping
460
+
461
+ ```ruby
462
+ # Correct: Meta on macOS, Control elsewhere
463
+ cmd_key = Puppeteer.env.darwin? ? 'Meta' : 'Control'
464
+
465
+ # Wrong: reversed mapping
466
+ cmd_key = Puppeteer.env.darwin? ? 'Control' : 'Meta'
467
+ ```
468
+
469
+ #### 3. Loop Iteration
470
+
471
+ ```typescript
472
+ // TypeScript
473
+ for (const char of 'World!') {
474
+ await page.keyboard.press('ArrowLeft');
475
+ }
476
+ ```
477
+
478
+ ```ruby
479
+ # Ruby
480
+ 'World!'.each_char { page.keyboard.press('ArrowLeft') }
481
+ ```
482
+
483
+ ## Step 5: Update API Coverage
484
+
485
+ **Important:** `docs/api_coverage.md` is auto-generated. Do not edit it manually.
486
+
487
+ To update the API coverage documentation:
488
+
489
+ ```bash
490
+ bundle exec ruby development/generate_api_coverage.rb
491
+ ```
492
+
493
+ This script reads `development/puppeteer.api.json` and compares it with the Ruby implementation to generate the coverage report.
494
+
495
+ ### How the Coverage Report Works
496
+
497
+ The script marks methods as:
498
+ - `~~methodName~~` (strikethrough) = Not implemented in Ruby
499
+ - `methodName` = Implemented with same name
500
+ - `methodName => \`#ruby_method\`` = Implemented with different name
501
+
502
+ The CI workflow "Check / documents updated" verifies this file is up-to-date. If it fails, run the command above and commit the changes.
503
+
504
+ ## Code Style Guidelines
505
+
506
+ ### Keyword Arguments
507
+
508
+ Use explicit keyword arguments in public APIs:
509
+
510
+ ```ruby
511
+ # Good
512
+ def goto(url, referer: nil, timeout: nil, wait_until: nil)
513
+ end
514
+
515
+ # Avoid
516
+ def goto(url, options = {})
517
+ end
518
+ ```
519
+
520
+ ### Error Handling
521
+
522
+ ```ruby
523
+ # Raise specific errors
524
+ raise Puppeteer::TimeoutError, "Waiting for selector timed out: #{selector}"
525
+
526
+ # Use begin/ensure for cleanup
527
+ def screenshot(path: nil)
528
+ data = capture_screenshot
529
+ File.write(path, data) if path
530
+ data
531
+ ensure
532
+ restore_viewport
533
+ end
534
+ ```
535
+
536
+ ### Nil Handling
537
+
538
+ ```ruby
539
+ # Use safe navigation
540
+ element&.click
541
+
542
+ # Explicit nil returns
543
+ def query_selector(selector)
544
+ result = @client.send_message('DOM.querySelector', selector: selector)
545
+ return nil if result['nodeId'].zero?
546
+ create_handle(result)
547
+ end
548
+ ```
549
+
550
+ ## Common Gotchas
551
+
552
+ ### 1. JavaScript vs Ruby Truthiness
553
+
554
+ ```typescript
555
+ // JavaScript: 0, '', null, undefined are falsy
556
+ if (result) { ... }
557
+
558
+ // Ruby: only nil and false are falsy
559
+ if result && !result.zero? && !result.empty?
560
+ ```
561
+
562
+ ### 2. Parameter Ordering
563
+
564
+ Puppeteer often uses options objects; Ruby prefers keyword args:
565
+
566
+ ```typescript
567
+ // TypeScript
568
+ page.screenshot({ path: 'screenshot.png', fullPage: true });
569
+
570
+ // Ruby
571
+ page.screenshot(path: 'screenshot.png', full_page: true)
572
+ ```
573
+
574
+ ### 3. Async Patterns
575
+
576
+ ```typescript
577
+ // TypeScript - concurrent
578
+ await Promise.all([
579
+ page.waitForNavigation(),
580
+ page.click('a'),
581
+ ]);
582
+
583
+ // Ruby (current) - use block pattern
584
+ page.wait_for_navigation do
585
+ page.click('a')
586
+ end
587
+ ```
588
+
589
+ ### 4. Base64 Data
590
+
591
+ ```typescript
592
+ // TypeScript
593
+ const data = await page.screenshot({ encoding: 'base64' });
594
+
595
+ // Ruby
596
+ data = page.screenshot(encoding: 'base64')
597
+ # Returns Base64 string, not binary
598
+ ```
599
+
600
+ ## Reference Resources
601
+
602
+ - [Puppeteer TypeScript source](https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core/src)
603
+ - [Puppeteer API docs](https://pptr.dev/api)
604
+ - [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
605
+ - [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