puppeteer-bidi 0.0.1.beta1
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CLAUDE/README.md +158 -0
- data/CLAUDE/async_programming.md +158 -0
- data/CLAUDE/click_implementation.md +340 -0
- data/CLAUDE/core_layer_gotchas.md +136 -0
- data/CLAUDE/error_handling.md +232 -0
- data/CLAUDE/file_chooser.md +95 -0
- data/CLAUDE/frame_architecture.md +346 -0
- data/CLAUDE/javascript_evaluation.md +341 -0
- data/CLAUDE/jshandle_implementation.md +505 -0
- data/CLAUDE/keyboard_implementation.md +250 -0
- data/CLAUDE/mouse_implementation.md +140 -0
- data/CLAUDE/navigation_waiting.md +234 -0
- data/CLAUDE/porting_puppeteer.md +214 -0
- data/CLAUDE/query_handler.md +194 -0
- data/CLAUDE/rspec_pending_vs_skip.md +262 -0
- data/CLAUDE/selector_evaluation.md +198 -0
- data/CLAUDE/test_server_routes.md +263 -0
- data/CLAUDE/testing_strategy.md +236 -0
- data/CLAUDE/two_layer_architecture.md +180 -0
- data/CLAUDE/wrapped_element_click.md +247 -0
- data/CLAUDE.md +185 -0
- data/LICENSE.txt +21 -0
- data/README.md +488 -0
- data/Rakefile +21 -0
- data/lib/puppeteer/bidi/async_utils.rb +151 -0
- data/lib/puppeteer/bidi/browser.rb +285 -0
- data/lib/puppeteer/bidi/browser_context.rb +53 -0
- data/lib/puppeteer/bidi/browser_launcher.rb +240 -0
- data/lib/puppeteer/bidi/connection.rb +182 -0
- data/lib/puppeteer/bidi/core/README.md +169 -0
- data/lib/puppeteer/bidi/core/browser.rb +230 -0
- data/lib/puppeteer/bidi/core/browsing_context.rb +601 -0
- data/lib/puppeteer/bidi/core/disposable.rb +69 -0
- data/lib/puppeteer/bidi/core/errors.rb +64 -0
- data/lib/puppeteer/bidi/core/event_emitter.rb +83 -0
- data/lib/puppeteer/bidi/core/navigation.rb +128 -0
- data/lib/puppeteer/bidi/core/realm.rb +315 -0
- data/lib/puppeteer/bidi/core/request.rb +300 -0
- data/lib/puppeteer/bidi/core/session.rb +153 -0
- data/lib/puppeteer/bidi/core/user_context.rb +208 -0
- data/lib/puppeteer/bidi/core/user_prompt.rb +102 -0
- data/lib/puppeteer/bidi/core.rb +45 -0
- data/lib/puppeteer/bidi/deserializer.rb +132 -0
- data/lib/puppeteer/bidi/element_handle.rb +602 -0
- data/lib/puppeteer/bidi/errors.rb +42 -0
- data/lib/puppeteer/bidi/file_chooser.rb +52 -0
- data/lib/puppeteer/bidi/frame.rb +597 -0
- data/lib/puppeteer/bidi/http_response.rb +23 -0
- data/lib/puppeteer/bidi/injected.js +1 -0
- data/lib/puppeteer/bidi/injected_source.rb +21 -0
- data/lib/puppeteer/bidi/js_handle.rb +302 -0
- data/lib/puppeteer/bidi/keyboard.rb +265 -0
- data/lib/puppeteer/bidi/lazy_arg.rb +23 -0
- data/lib/puppeteer/bidi/mouse.rb +170 -0
- data/lib/puppeteer/bidi/page.rb +613 -0
- data/lib/puppeteer/bidi/query_handler.rb +397 -0
- data/lib/puppeteer/bidi/realm.rb +242 -0
- data/lib/puppeteer/bidi/serializer.rb +139 -0
- data/lib/puppeteer/bidi/target.rb +81 -0
- data/lib/puppeteer/bidi/task_manager.rb +44 -0
- data/lib/puppeteer/bidi/timeout_settings.rb +20 -0
- data/lib/puppeteer/bidi/transport.rb +129 -0
- data/lib/puppeteer/bidi/version.rb +7 -0
- data/lib/puppeteer/bidi/wait_task.rb +322 -0
- data/lib/puppeteer/bidi.rb +49 -0
- data/scripts/update_injected_source.rb +57 -0
- data/sig/puppeteer/bidi/browser.rbs +80 -0
- data/sig/puppeteer/bidi/element_handle.rbs +238 -0
- data/sig/puppeteer/bidi/frame.rbs +205 -0
- data/sig/puppeteer/bidi/js_handle.rbs +90 -0
- data/sig/puppeteer/bidi/page.rbs +247 -0
- data/sig/puppeteer/bidi.rbs +15 -0
- metadata +176 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
# JSHandle and ElementHandle Implementation
|
|
2
|
+
|
|
3
|
+
This document provides comprehensive details on implementing JSHandle and ElementHandle classes, including BiDi protocol parameters, debugging techniques, and common pitfalls.
|
|
4
|
+
|
|
5
|
+
### Overview
|
|
6
|
+
|
|
7
|
+
JSHandle and ElementHandle are fundamental classes for interacting with JavaScript objects in the browser. This section documents the implementation details and debugging techniques learned during development.
|
|
8
|
+
|
|
9
|
+
### Architecture
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Puppeteer::Bidi
|
|
13
|
+
├── Serializer # Ruby → BiDi LocalValue
|
|
14
|
+
├── Deserializer # BiDi RemoteValue → Ruby
|
|
15
|
+
├── JSHandle # JavaScript object reference
|
|
16
|
+
└── ElementHandle # DOM element reference (extends JSHandle)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Key Implementation Files
|
|
20
|
+
|
|
21
|
+
| File | Purpose | Lines |
|
|
22
|
+
|------|---------|-------|
|
|
23
|
+
| `lib/puppeteer/bidi/serializer.rb` | Centralized argument serialization | 136 |
|
|
24
|
+
| `lib/puppeteer/bidi/deserializer.rb` | Centralized result deserialization | 132 |
|
|
25
|
+
| `lib/puppeteer/bidi/js_handle.rb` | JavaScript object handles | 291 |
|
|
26
|
+
| `lib/puppeteer/bidi/element_handle.rb` | DOM element handles | 91 |
|
|
27
|
+
|
|
28
|
+
**Code reduction**: ~300 lines of duplicate serialization code eliminated from Frame and Page classes.
|
|
29
|
+
|
|
30
|
+
### Critical BiDi Protocol Parameters
|
|
31
|
+
|
|
32
|
+
#### 1. resultOwnership - Handle Lifecycle Management
|
|
33
|
+
|
|
34
|
+
**Problem**: BiDi returns `{"type" => "object"}` without `handle` or `sharedId`, making it impossible to reference the object later.
|
|
35
|
+
|
|
36
|
+
**Root Cause**: Missing `resultOwnership` parameter in `script.callFunction` and `script.evaluate`.
|
|
37
|
+
|
|
38
|
+
**Solution**: Always set `resultOwnership: 'root'` when you need a handle:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# lib/puppeteer/bidi/core/realm.rb
|
|
42
|
+
def call_function(function_declaration, await_promise, **options)
|
|
43
|
+
# Critical: Use 'root' ownership to keep handles alive
|
|
44
|
+
unless options.key?(:resultOwnership)
|
|
45
|
+
options[:resultOwnership] = 'root'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
session.send_command('script.callFunction', {
|
|
49
|
+
functionDeclaration: function_declaration,
|
|
50
|
+
awaitPromise: await_promise,
|
|
51
|
+
target: target,
|
|
52
|
+
**options
|
|
53
|
+
})
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**BiDi resultOwnership values**:
|
|
58
|
+
- `'root'`: Keep handle alive (garbage collection resistant)
|
|
59
|
+
- `'none'`: Don't return handle (for one-time evaluations)
|
|
60
|
+
|
|
61
|
+
**Important**: Don't confuse with `awaitPromise`:
|
|
62
|
+
- `awaitPromise`: Controls whether to wait for promises to resolve
|
|
63
|
+
- `resultOwnership`: Controls handle lifecycle (independent concern)
|
|
64
|
+
|
|
65
|
+
#### 2. serializationOptions - Control Serialization Depth
|
|
66
|
+
|
|
67
|
+
**When requesting handles**, set `maxObjectDepth: 0` to prevent deep serialization:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# When awaitPromise is false (returning handle):
|
|
71
|
+
options[:serializationOptions] = {
|
|
72
|
+
maxObjectDepth: 0, # Don't serialize, return handle
|
|
73
|
+
maxDomDepth: 0 # Don't serialize DOM children
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Without serializationOptions**: BiDi may serialize the entire object graph, losing the handle reference.
|
|
78
|
+
|
|
79
|
+
### Debugging Techniques
|
|
80
|
+
|
|
81
|
+
#### 1. Protocol Message Inspection
|
|
82
|
+
|
|
83
|
+
Use `DEBUG_BIDI_COMMAND=1` to see all BiDi protocol messages:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
DEBUG_BIDI_COMMAND=1 bundle exec rspec spec/integration/jshandle_spec.rb:24
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Output**:
|
|
90
|
+
```
|
|
91
|
+
[BiDi] Request script.callFunction: {
|
|
92
|
+
id: 1,
|
|
93
|
+
method: "script.callFunction",
|
|
94
|
+
params: {
|
|
95
|
+
functionDeclaration: "() => navigator",
|
|
96
|
+
awaitPromise: false,
|
|
97
|
+
target: {context: "..."},
|
|
98
|
+
resultOwnership: "root", # ← Check this!
|
|
99
|
+
serializationOptions: { # ← Check this!
|
|
100
|
+
maxObjectDepth: 0
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
[BiDi] Response for script.callFunction: {
|
|
106
|
+
type: "success",
|
|
107
|
+
result: {
|
|
108
|
+
type: "object",
|
|
109
|
+
handle: "6af2844f-..." # ← Should have handle!
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### 2. Comparing with Puppeteer's Protocol Messages
|
|
115
|
+
|
|
116
|
+
**Workflow**:
|
|
117
|
+
1. Clone Puppeteer repository: `git clone https://github.com/puppeteer/puppeteer`
|
|
118
|
+
2. Set up Puppeteer: `npm install && npm run build`
|
|
119
|
+
3. Enable protocol logging: `DEBUG_PROTOCOL=1 npm test -- test/src/jshandle.spec.ts`
|
|
120
|
+
4. Compare messages side-by-side with Ruby implementation
|
|
121
|
+
|
|
122
|
+
**Example comparison**:
|
|
123
|
+
```bash
|
|
124
|
+
# Puppeteer (TypeScript)
|
|
125
|
+
DEBUG_PROTOCOL=1 npm test -- test/src/jshandle.spec.ts -g "should accept object handle"
|
|
126
|
+
|
|
127
|
+
# Ruby implementation
|
|
128
|
+
DEBUG_BIDI_COMMAND=1 bundle exec rspec spec/integration/jshandle_spec.rb:24
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Look for differences in**:
|
|
132
|
+
- Parameter names (camelCase vs snake_case)
|
|
133
|
+
- Missing parameters (resultOwnership, serializationOptions)
|
|
134
|
+
- Parameter values (arrays vs strings)
|
|
135
|
+
|
|
136
|
+
#### 3. Extracting Specific Protocol Messages
|
|
137
|
+
|
|
138
|
+
Use `grep` to filter specific BiDi methods:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
DEBUG_BIDI_COMMAND=1 bundle exec rspec spec/integration/jshandle_spec.rb:227 \
|
|
142
|
+
--format documentation 2>&1 | grep -A 5 "script\.disown"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Output**:
|
|
146
|
+
```
|
|
147
|
+
[BiDi] Request script.disown: {
|
|
148
|
+
id: 6,
|
|
149
|
+
method: "script.disown",
|
|
150
|
+
params: {
|
|
151
|
+
target: {context: "..."},
|
|
152
|
+
handles: "6af2844f-..." # ← ERROR: Should be array!
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### 4. Step-by-Step Protocol Flow Analysis
|
|
158
|
+
|
|
159
|
+
For complex issues, trace the entire flow:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
# Add temporary debugging in code
|
|
163
|
+
def evaluate_handle(script, *args)
|
|
164
|
+
puts "1. Input script: #{script}"
|
|
165
|
+
puts "2. Serialized args: #{serialized_args.inspect}"
|
|
166
|
+
|
|
167
|
+
result = @realm.call_function(script, false, arguments: serialized_args)
|
|
168
|
+
puts "3. BiDi result: #{result.inspect}"
|
|
169
|
+
|
|
170
|
+
handle = JSHandle.from(result['result'], @realm)
|
|
171
|
+
puts "4. Created handle: #{handle.inspect}"
|
|
172
|
+
|
|
173
|
+
handle
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Common Pitfalls and Solutions
|
|
178
|
+
|
|
179
|
+
#### 1. Handle Parameters Must Be Arrays
|
|
180
|
+
|
|
181
|
+
**Problem**: BiDi error "Expected 'handles' to be an array, got [object String]"
|
|
182
|
+
|
|
183
|
+
**Root Cause**: `script.disown` expects `handles` parameter as array, but single string was passed:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
# WRONG
|
|
187
|
+
@realm.disown(handle_id) # → {handles: "abc-123"}
|
|
188
|
+
|
|
189
|
+
# CORRECT
|
|
190
|
+
@realm.disown([handle_id]) # → {handles: ["abc-123"]}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Location**: `lib/puppeteer/bidi/js_handle.rb:57`
|
|
194
|
+
|
|
195
|
+
**Fix**:
|
|
196
|
+
```ruby
|
|
197
|
+
def dispose
|
|
198
|
+
handle_id = id
|
|
199
|
+
@realm.disown([handle_id]) if handle_id # Wrap in array
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### 2. Handle Not Returned from evaluate_handle
|
|
204
|
+
|
|
205
|
+
**Symptoms**:
|
|
206
|
+
- `remote_value['handle']` is `nil`
|
|
207
|
+
- BiDi returns `{"type" => "object"}` without handle
|
|
208
|
+
- Error: "Expected 'serializedKeyValueList' to be an array"
|
|
209
|
+
|
|
210
|
+
**Root Cause**: Missing `resultOwnership` and `serializationOptions` parameters
|
|
211
|
+
|
|
212
|
+
**Fix**: Add to `Core::Realm#call_function` (see section 1 above)
|
|
213
|
+
|
|
214
|
+
#### 3. Confusing awaitPromise with returnByValue
|
|
215
|
+
|
|
216
|
+
**Common mistake**: Thinking `awaitPromise` controls serialization
|
|
217
|
+
|
|
218
|
+
**Reality**:
|
|
219
|
+
- `awaitPromise`: Wait for promises? (`true`/`false`)
|
|
220
|
+
- `resultOwnership`: Return handle? (`'root'`/`'none'`)
|
|
221
|
+
- These are **independent** concerns!
|
|
222
|
+
|
|
223
|
+
**Example**:
|
|
224
|
+
```ruby
|
|
225
|
+
# Want handle to a promise result? Use both!
|
|
226
|
+
call_function(script, true, resultOwnership: 'root') # await=true, handle=yes
|
|
227
|
+
|
|
228
|
+
# Want serialized promise result? Different!
|
|
229
|
+
call_function(script, true, resultOwnership: 'none') # await=true, serialize=yes
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### 4. Date Serialization in json_value
|
|
233
|
+
|
|
234
|
+
**Problem**: Dates converted to strings instead of Time objects
|
|
235
|
+
|
|
236
|
+
**Wrong approach**:
|
|
237
|
+
```ruby
|
|
238
|
+
# DON'T: Using JSON.stringify loses BiDi's native date type
|
|
239
|
+
result = evaluate('(value) => JSON.stringify(value)')
|
|
240
|
+
JSON.parse(result) # Date becomes string!
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Correct approach**:
|
|
244
|
+
```ruby
|
|
245
|
+
# DO: Use BiDi's built-in serialization
|
|
246
|
+
def json_value
|
|
247
|
+
evaluate('(value) => value') # BiDi handles dates natively
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**BiDi date format**:
|
|
252
|
+
```ruby
|
|
253
|
+
# BiDi returns: {type: 'date', value: '2020-05-27T01:31:38.506Z'}
|
|
254
|
+
# Deserializer converts to: Time.parse('2020-05-27T01:31:38.506Z')
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Testing Strategy for Handle Implementation
|
|
258
|
+
|
|
259
|
+
#### Test Organization
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
spec/integration/
|
|
263
|
+
├── jshandle_spec.rb # 21 tests - JSHandle functionality
|
|
264
|
+
├── queryselector_spec.rb # 8 tests - DOM querying
|
|
265
|
+
└── evaluation_spec.rb # Updated - ElementHandle arguments
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Test Coverage Checklist
|
|
269
|
+
|
|
270
|
+
When implementing handle-related features, ensure tests cover:
|
|
271
|
+
|
|
272
|
+
- ✅ Handle creation from primitives and objects
|
|
273
|
+
- ✅ Handle passing as function arguments
|
|
274
|
+
- ✅ Property access (single and multiple)
|
|
275
|
+
- ✅ JSON value serialization
|
|
276
|
+
- ✅ Special values (dates, circular references, undefined)
|
|
277
|
+
- ✅ Type conversion (`as_element`)
|
|
278
|
+
- ✅ String representation (`to_s`)
|
|
279
|
+
- ✅ Handle disposal and error handling
|
|
280
|
+
- ✅ DOM querying (single and multiple)
|
|
281
|
+
- ✅ Empty result handling
|
|
282
|
+
|
|
283
|
+
#### Running Handle Tests
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
# All handle-related tests
|
|
287
|
+
bundle exec rspec spec/integration/jshandle_spec.rb spec/integration/queryselector_spec.rb
|
|
288
|
+
|
|
289
|
+
# With protocol debugging
|
|
290
|
+
DEBUG_BIDI_COMMAND=1 bundle exec rspec spec/integration/jshandle_spec.rb:24
|
|
291
|
+
|
|
292
|
+
# Specific test
|
|
293
|
+
bundle exec rspec spec/integration/jshandle_spec.rb:227 --format documentation
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Code Patterns and Best Practices
|
|
297
|
+
|
|
298
|
+
#### 1. Serializer Usage
|
|
299
|
+
|
|
300
|
+
**Always use Serializer** for argument preparation:
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
# Good
|
|
304
|
+
args = [element, selector].map { |arg| Serializer.serialize(arg) }
|
|
305
|
+
call_function(script, true, arguments: args)
|
|
306
|
+
|
|
307
|
+
# Bad - manual serialization (duplicates logic)
|
|
308
|
+
args = [
|
|
309
|
+
{ type: 'object', handle: element.id },
|
|
310
|
+
{ type: 'string', value: selector }
|
|
311
|
+
]
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### 2. Deserializer Usage
|
|
315
|
+
|
|
316
|
+
**Always use Deserializer** for result processing:
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
# Good
|
|
320
|
+
result = call_function(script, true)
|
|
321
|
+
Deserializer.deserialize(result['result'])
|
|
322
|
+
|
|
323
|
+
# Bad - manual deserialization (misses edge cases)
|
|
324
|
+
result['result']['value'] # Breaks for dates, handles, etc.
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
#### 3. Factory Pattern for Handle Creation
|
|
328
|
+
|
|
329
|
+
**Use `JSHandle.from`** for polymorphic handle creation:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
# Good - automatically creates ElementHandle for nodes
|
|
333
|
+
handle = JSHandle.from(remote_value, realm)
|
|
334
|
+
|
|
335
|
+
# Bad - manual type checking
|
|
336
|
+
if remote_value['type'] == 'node'
|
|
337
|
+
ElementHandle.new(realm, remote_value)
|
|
338
|
+
else
|
|
339
|
+
JSHandle.new(realm, remote_value)
|
|
340
|
+
end
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### 4. Handle Disposal Pattern
|
|
344
|
+
|
|
345
|
+
**Always check disposal state** before operations:
|
|
346
|
+
|
|
347
|
+
```ruby
|
|
348
|
+
def get_property(name)
|
|
349
|
+
raise 'JSHandle is disposed' if @disposed
|
|
350
|
+
|
|
351
|
+
# ... implementation
|
|
352
|
+
end
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Performance Considerations
|
|
356
|
+
|
|
357
|
+
#### Handle Lifecycle
|
|
358
|
+
|
|
359
|
+
**Handles consume browser memory** - dispose when no longer needed:
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
# Manual disposal
|
|
363
|
+
handle = page.evaluate_handle('window')
|
|
364
|
+
# ... use handle
|
|
365
|
+
handle.dispose
|
|
366
|
+
|
|
367
|
+
# Automatic disposal via block (future enhancement)
|
|
368
|
+
page.evaluate_handle('window') do |handle|
|
|
369
|
+
# handle automatically disposed after block
|
|
370
|
+
end
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
#### Serialization vs Handle References
|
|
374
|
+
|
|
375
|
+
**Trade-off**:
|
|
376
|
+
- **Serialization** (`resultOwnership: 'none'`): One-time use, no memory overhead
|
|
377
|
+
- **Handle** (`resultOwnership: 'root'`): Reusable, requires disposal
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
# One-time evaluation - serialize
|
|
381
|
+
page.evaluate('document.title') # No handle created
|
|
382
|
+
|
|
383
|
+
# Reusable reference - handle
|
|
384
|
+
handle = page.evaluate_handle('document') # Keep for multiple operations
|
|
385
|
+
handle.evaluate('doc => doc.title')
|
|
386
|
+
handle.evaluate('doc => doc.body.innerHTML')
|
|
387
|
+
handle.dispose # Clean up
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### ElementHandle Frame Access Pattern
|
|
391
|
+
|
|
392
|
+
**Following Puppeteer's architecture**, ElementHandle accesses the Page through its Frame:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// Puppeteer TypeScript
|
|
396
|
+
class BidiElementHandle extends ElementHandle {
|
|
397
|
+
override get frame(): BidiFrame {
|
|
398
|
+
return this.realm.environment; // realm.environment is the Frame
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Ruby implementation**:
|
|
404
|
+
```ruby
|
|
405
|
+
# lib/puppeteer/bidi/element_handle.rb
|
|
406
|
+
class ElementHandle < JSHandle
|
|
407
|
+
def frame
|
|
408
|
+
@realm.environment # WindowRealm#environment returns Frame
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def type(text, delay: 0)
|
|
412
|
+
keyboard = Keyboard.new(frame.page, @realm.browsing_context)
|
|
413
|
+
# ...
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**Architecture**:
|
|
419
|
+
```
|
|
420
|
+
ElementHandle
|
|
421
|
+
└─ @realm (WindowRealm)
|
|
422
|
+
└─ environment (Frame)
|
|
423
|
+
└─ page() → Page
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Implementation steps**:
|
|
427
|
+
|
|
428
|
+
1. **WindowRealm** stores Frame reference:
|
|
429
|
+
```ruby
|
|
430
|
+
# lib/puppeteer/bidi/core/realm.rb
|
|
431
|
+
class WindowRealm < Realm
|
|
432
|
+
attr_accessor :environment # Holds Frame reference
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
2. **Frame** sets itself as realm's environment:
|
|
437
|
+
```ruby
|
|
438
|
+
# lib/puppeteer/bidi/frame.rb
|
|
439
|
+
def initialize(parent, browsing_context)
|
|
440
|
+
@parent = parent
|
|
441
|
+
@browsing_context = browsing_context
|
|
442
|
+
|
|
443
|
+
# Set this frame as the environment
|
|
444
|
+
realm = @browsing_context.default_realm
|
|
445
|
+
realm.environment = self if realm.respond_to?(:environment=)
|
|
446
|
+
end
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
3. **ElementHandle** accesses Page via frame:
|
|
450
|
+
```ruby
|
|
451
|
+
# lib/puppeteer/bidi/element_handle.rb
|
|
452
|
+
def frame
|
|
453
|
+
@realm.environment
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Use frame.page instead of workaround
|
|
457
|
+
keyboard = Keyboard.new(frame.page, @realm.browsing_context)
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Benefits**:
|
|
461
|
+
- Matches Puppeteer's design exactly
|
|
462
|
+
- No temporary object creation
|
|
463
|
+
- Clear ownership hierarchy
|
|
464
|
+
- Type-safe frame access
|
|
465
|
+
|
|
466
|
+
### Reference Implementation Mapping
|
|
467
|
+
|
|
468
|
+
| Puppeteer TypeScript | Ruby Implementation | Notes |
|
|
469
|
+
|---------------------|---------------------|-------|
|
|
470
|
+
| `BidiJSHandle.from()` | `JSHandle.from()` | Factory method |
|
|
471
|
+
| `BidiJSHandle#dispose()` | `JSHandle#dispose` | Handle cleanup |
|
|
472
|
+
| `BidiJSHandle#jsonValue()` | `JSHandle#json_value` | Uses evaluate trick |
|
|
473
|
+
| `BidiJSHandle#getProperty()` | `JSHandle#get_property` | Single property |
|
|
474
|
+
| `BidiJSHandle#getProperties()` | `JSHandle#get_properties` | Walks prototype chain |
|
|
475
|
+
| `BidiElementHandle#$()` | `ElementHandle#query_selector` | CSS selector |
|
|
476
|
+
| `BidiElementHandle#$$()` | `ElementHandle#query_selector_all` | Multiple elements |
|
|
477
|
+
| `BidiElementHandle#frame` | `ElementHandle#frame` | realm.environment |
|
|
478
|
+
| `BidiElementHandle#contentFrame()` | `ElementHandle#content_frame` | iframe/frame content |
|
|
479
|
+
| `BidiElementHandle#isVisible()` | `ElementHandle#visible?` | Visibility check |
|
|
480
|
+
| `BidiElementHandle#isHidden()` | `ElementHandle#hidden?` | Hidden check |
|
|
481
|
+
| `BidiElementHandle#toElement()` | `ElementHandle#to_element` | Tag name validation |
|
|
482
|
+
| `BidiElementHandle#boxModel()` | `ElementHandle#box_model` | CSS box model |
|
|
483
|
+
| `BidiSerializer.serialize()` | `Serializer.serialize()` | Centralized |
|
|
484
|
+
| `BidiDeserializer.deserialize()` | `Deserializer.deserialize()` | Centralized |
|
|
485
|
+
|
|
486
|
+
### Future Enhancements
|
|
487
|
+
|
|
488
|
+
Potential improvements for handle implementation:
|
|
489
|
+
|
|
490
|
+
1. **Automatic disposal**: Block-based API with automatic cleanup
|
|
491
|
+
2. **Handle pooling**: Reuse handle IDs to reduce memory overhead
|
|
492
|
+
3. **Lazy deserialization**: Defer conversion until value is accessed
|
|
493
|
+
4. **Type hints**: RBS type definitions for better IDE support
|
|
494
|
+
5. **Handle debugging**: Track handle creation/disposal for leak detection
|
|
495
|
+
|
|
496
|
+
### Lessons Learned
|
|
497
|
+
|
|
498
|
+
1. **Always compare protocol messages** with Puppeteer when debugging BiDi issues
|
|
499
|
+
2. **resultOwnership is critical** for handle-based APIs - always set it explicitly
|
|
500
|
+
3. **Don't confuse awaitPromise with serialization** - they control different aspects
|
|
501
|
+
4. **BiDi arrays must be arrays** - wrapping single values is often necessary
|
|
502
|
+
5. **Use Puppeteer's tricks** - like `evaluate('(value) => value')` for json_value
|
|
503
|
+
6. **Test disposal thoroughly** - handle lifecycle bugs are subtle and common
|
|
504
|
+
7. **Centralize serialization** - eliminates duplication and ensures consistency
|
|
505
|
+
|