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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -3
- data/AGENTS.md +169 -0
- data/CLAUDE/README.md +41 -0
- data/CLAUDE/architecture.md +253 -0
- data/CLAUDE/cdp_protocol.md +230 -0
- data/CLAUDE/concurrency.md +216 -0
- data/CLAUDE/porting_puppeteer.md +575 -0
- data/CLAUDE/rbs_type_checking.md +101 -0
- data/CLAUDE/spec_migration_plans.md +1041 -0
- data/CLAUDE/testing.md +278 -0
- data/CLAUDE.md +242 -0
- data/README.md +8 -0
- data/Rakefile +7 -0
- data/Steepfile +28 -0
- data/docs/api_coverage.md +105 -56
- data/lib/puppeteer/aria_query_handler.rb +3 -2
- data/lib/puppeteer/async_utils.rb +214 -0
- data/lib/puppeteer/browser.rb +98 -56
- data/lib/puppeteer/browser_connector.rb +18 -3
- data/lib/puppeteer/browser_context.rb +196 -3
- data/lib/puppeteer/browser_runner.rb +18 -10
- data/lib/puppeteer/cdp_session.rb +67 -23
- data/lib/puppeteer/chrome_target_manager.rb +65 -40
- data/lib/puppeteer/connection.rb +55 -36
- data/lib/puppeteer/console_message.rb +9 -1
- data/lib/puppeteer/console_patch.rb +47 -0
- data/lib/puppeteer/css_coverage.rb +5 -3
- data/lib/puppeteer/custom_query_handler.rb +80 -33
- data/lib/puppeteer/define_async_method.rb +31 -37
- data/lib/puppeteer/dialog.rb +47 -14
- data/lib/puppeteer/element_handle.rb +231 -62
- data/lib/puppeteer/emulation_manager.rb +1 -1
- data/lib/puppeteer/env.rb +1 -1
- data/lib/puppeteer/errors.rb +25 -2
- data/lib/puppeteer/event_callbackable.rb +15 -0
- data/lib/puppeteer/events.rb +4 -0
- data/lib/puppeteer/execution_context.rb +148 -3
- data/lib/puppeteer/file_chooser.rb +6 -0
- data/lib/puppeteer/frame.rb +162 -91
- data/lib/puppeteer/frame_manager.rb +69 -48
- data/lib/puppeteer/http_request.rb +114 -38
- data/lib/puppeteer/http_response.rb +24 -7
- data/lib/puppeteer/isolated_world.rb +64 -41
- data/lib/puppeteer/js_coverage.rb +5 -3
- data/lib/puppeteer/js_handle.rb +58 -16
- data/lib/puppeteer/keyboard.rb +30 -17
- data/lib/puppeteer/launcher/browser_options.rb +3 -1
- data/lib/puppeteer/launcher/chrome.rb +8 -5
- data/lib/puppeteer/launcher/launch_options.rb +7 -2
- data/lib/puppeteer/launcher.rb +4 -8
- data/lib/puppeteer/lifecycle_watcher.rb +38 -22
- data/lib/puppeteer/mouse.rb +273 -64
- data/lib/puppeteer/network_event_manager.rb +7 -0
- data/lib/puppeteer/network_manager.rb +393 -112
- data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
- data/lib/puppeteer/page.rb +568 -226
- data/lib/puppeteer/puppeteer.rb +171 -64
- data/lib/puppeteer/query_handler_manager.rb +112 -16
- data/lib/puppeteer/reactor_runner.rb +247 -0
- data/lib/puppeteer/remote_object.rb +127 -47
- data/lib/puppeteer/target.rb +74 -27
- data/lib/puppeteer/task_manager.rb +3 -1
- data/lib/puppeteer/timeout_helper.rb +6 -10
- data/lib/puppeteer/touch_handle.rb +39 -0
- data/lib/puppeteer/touch_screen.rb +72 -22
- data/lib/puppeteer/tracing.rb +3 -3
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer/wait_task.rb +264 -101
- data/lib/puppeteer/web_socket.rb +2 -2
- data/lib/puppeteer/web_socket_transport.rb +91 -27
- data/lib/puppeteer/web_worker.rb +175 -0
- data/lib/puppeteer.rb +20 -4
- data/puppeteer-ruby.gemspec +15 -11
- data/sig/_external.rbs +8 -0
- data/sig/_supplementary.rbs +314 -0
- data/sig/puppeteer/browser.rbs +166 -0
- data/sig/puppeteer/cdp_session.rbs +64 -0
- data/sig/puppeteer/dialog.rbs +41 -0
- data/sig/puppeteer/element_handle.rbs +305 -0
- data/sig/puppeteer/execution_context.rbs +87 -0
- data/sig/puppeteer/frame.rbs +226 -0
- data/sig/puppeteer/http_request.rbs +214 -0
- data/sig/puppeteer/http_response.rbs +89 -0
- data/sig/puppeteer/js_handle.rbs +64 -0
- data/sig/puppeteer/keyboard.rbs +40 -0
- data/sig/puppeteer/mouse.rbs +113 -0
- data/sig/puppeteer/page.rbs +515 -0
- data/sig/puppeteer/puppeteer.rbs +98 -0
- data/sig/puppeteer/remote_object.rbs +78 -0
- data/sig/puppeteer/touch_handle.rbs +21 -0
- data/sig/puppeteer/touch_screen.rbs +35 -0
- data/sig/puppeteer/web_worker.rbs +83 -0
- metadata +116 -45
- data/CHANGELOG.md +0 -397
- data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
- data/lib/puppeteer/firefox_target_manager.rb +0 -157
- 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
|