puppeteer-bidi 0.0.3.beta1 → 0.0.3.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/puppeteer/bidi/browser.rb +15 -0
- data/lib/puppeteer/bidi/version.rb +1 -1
- data/sig/puppeteer/bidi/browser.rbs +3 -0
- metadata +1 -24
- data/CLAUDE/README.md +0 -158
- data/CLAUDE/async_programming.md +0 -158
- data/CLAUDE/click_implementation.md +0 -340
- data/CLAUDE/core_layer_gotchas.md +0 -136
- data/CLAUDE/error_handling.md +0 -232
- data/CLAUDE/expose_function_implementation.md +0 -271
- data/CLAUDE/file_chooser.md +0 -95
- data/CLAUDE/frame_architecture.md +0 -346
- data/CLAUDE/javascript_evaluation.md +0 -341
- data/CLAUDE/jshandle_implementation.md +0 -505
- data/CLAUDE/keyboard_implementation.md +0 -250
- data/CLAUDE/mouse_implementation.md +0 -140
- data/CLAUDE/navigation_waiting.md +0 -234
- data/CLAUDE/porting_puppeteer.md +0 -234
- data/CLAUDE/query_handler.md +0 -194
- data/CLAUDE/reactor_runner.md +0 -111
- data/CLAUDE/rspec_pending_vs_skip.md +0 -262
- data/CLAUDE/selector_evaluation.md +0 -198
- data/CLAUDE/test_server_routes.md +0 -263
- data/CLAUDE/testing_strategy.md +0 -236
- data/CLAUDE/two_layer_architecture.md +0 -180
- data/CLAUDE/wrapped_element_click.md +0 -247
- data/CLAUDE.md +0 -238
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
# Click Implementation and Mouse Input
|
|
2
|
-
|
|
3
|
-
This document provides comprehensive coverage of the click functionality implementation, including architecture, bug fixes, event handling, and BiDi protocol format requirements.
|
|
4
|
-
|
|
5
|
-
### Overview
|
|
6
|
-
|
|
7
|
-
Implemented full click functionality following Puppeteer's architecture, including mouse input actions, element visibility detection, and automatic scrolling.
|
|
8
|
-
|
|
9
|
-
### Architecture
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
Page#click
|
|
13
|
-
↓ delegates to
|
|
14
|
-
Frame#click
|
|
15
|
-
↓ delegates to
|
|
16
|
-
ElementHandle#click
|
|
17
|
-
↓ implementation
|
|
18
|
-
1. scroll_into_view_if_needed
|
|
19
|
-
2. clickable_point calculation
|
|
20
|
-
3. Mouse#click (BiDi input.performActions)
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### Key Components
|
|
24
|
-
|
|
25
|
-
#### Mouse Class (`lib/puppeteer/bidi/mouse.rb`)
|
|
26
|
-
|
|
27
|
-
Implements mouse input actions via BiDi `input.performActions`:
|
|
28
|
-
|
|
29
|
-
```ruby
|
|
30
|
-
def click(x, y, button: LEFT, count: 1, delay: nil)
|
|
31
|
-
actions = []
|
|
32
|
-
if @x != x || @y != y
|
|
33
|
-
actions << {
|
|
34
|
-
type: 'pointerMove',
|
|
35
|
-
x: x.to_i,
|
|
36
|
-
y: y.to_i,
|
|
37
|
-
origin: 'viewport' # BiDi expects string, not hash!
|
|
38
|
-
}
|
|
39
|
-
end
|
|
40
|
-
@x = x
|
|
41
|
-
@y = y
|
|
42
|
-
bidi_button = button_to_bidi(button)
|
|
43
|
-
count.times do
|
|
44
|
-
actions << { type: 'pointerDown', button: bidi_button }
|
|
45
|
-
actions << { type: 'pause', duration: delay.to_i } if delay
|
|
46
|
-
actions << { type: 'pointerUp', button: bidi_button }
|
|
47
|
-
end
|
|
48
|
-
perform_actions(actions)
|
|
49
|
-
end
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
**Critical BiDi Protocol Detail**: The `origin` parameter must be the string `'viewport'`, NOT a hash like `{type: 'viewport'}`. This caused a protocol error during initial implementation.
|
|
53
|
-
|
|
54
|
-
#### ElementHandle Click Methods
|
|
55
|
-
|
|
56
|
-
##### scroll_into_view_if_needed
|
|
57
|
-
|
|
58
|
-
Uses IntersectionObserver API to detect viewport visibility:
|
|
59
|
-
|
|
60
|
-
```ruby
|
|
61
|
-
def scroll_into_view_if_needed
|
|
62
|
-
return if intersecting_viewport?
|
|
63
|
-
|
|
64
|
-
scroll_info = evaluate(<<~JS)
|
|
65
|
-
element => {
|
|
66
|
-
if (!element.isConnected) return 'Node is detached from document';
|
|
67
|
-
if (element.nodeType !== Node.ELEMENT_NODE) return 'Node is not of type HTMLElement';
|
|
68
|
-
|
|
69
|
-
element.scrollIntoView({
|
|
70
|
-
block: 'center',
|
|
71
|
-
inline: 'center',
|
|
72
|
-
behavior: 'instant'
|
|
73
|
-
});
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
JS
|
|
77
|
-
|
|
78
|
-
raise scroll_info if scroll_info
|
|
79
|
-
end
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
##### intersecting_viewport?
|
|
83
|
-
|
|
84
|
-
Uses browser's IntersectionObserver for accurate visibility detection:
|
|
85
|
-
|
|
86
|
-
```ruby
|
|
87
|
-
def intersecting_viewport?(threshold: 0)
|
|
88
|
-
evaluate(<<~JS, threshold)
|
|
89
|
-
(element, threshold) => {
|
|
90
|
-
return new Promise(resolve => {
|
|
91
|
-
const observer = new IntersectionObserver(entries => {
|
|
92
|
-
resolve(entries[0].intersectionRatio > threshold);
|
|
93
|
-
observer.disconnect();
|
|
94
|
-
});
|
|
95
|
-
observer.observe(element);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
JS
|
|
99
|
-
end
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
##### clickable_point
|
|
103
|
-
|
|
104
|
-
Calculates click coordinates with optional offset:
|
|
105
|
-
|
|
106
|
-
```ruby
|
|
107
|
-
def clickable_point(offset: nil)
|
|
108
|
-
box = clickable_box
|
|
109
|
-
if offset
|
|
110
|
-
{ x: box[:x] + offset[:x], y: box[:y] + offset[:y] }
|
|
111
|
-
else
|
|
112
|
-
{ x: box[:x] + box[:width] / 2, y: box[:y] + box[:height] / 2 }
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Critical Bug Fixes
|
|
118
|
-
|
|
119
|
-
#### 1. Missing session.subscribe Call
|
|
120
|
-
|
|
121
|
-
**Problem**: Navigation events (browsingContext.load, etc.) were not firing, causing tests to timeout.
|
|
122
|
-
|
|
123
|
-
**Root Cause**: Missing subscription to BiDi modules. Puppeteer subscribes to these modules on session creation:
|
|
124
|
-
- browsingContext
|
|
125
|
-
- network
|
|
126
|
-
- log
|
|
127
|
-
- script
|
|
128
|
-
- input
|
|
129
|
-
|
|
130
|
-
**Fix**: Added subscription in two places:
|
|
131
|
-
|
|
132
|
-
```ruby
|
|
133
|
-
# lib/puppeteer/bidi/browser.rb
|
|
134
|
-
subscribe_modules = %w[
|
|
135
|
-
browsingContext
|
|
136
|
-
network
|
|
137
|
-
log
|
|
138
|
-
script
|
|
139
|
-
input
|
|
140
|
-
]
|
|
141
|
-
@session.subscribe(subscribe_modules)
|
|
142
|
-
|
|
143
|
-
# lib/puppeteer/bidi/core/session.rb
|
|
144
|
-
def initialize_session
|
|
145
|
-
subscribe_modules = %w[
|
|
146
|
-
browsingContext
|
|
147
|
-
network
|
|
148
|
-
log
|
|
149
|
-
script
|
|
150
|
-
input
|
|
151
|
-
]
|
|
152
|
-
subscribe(subscribe_modules)
|
|
153
|
-
end
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
**Impact**: This fix enabled all navigation-related functionality, including the "click links which cause navigation" test.
|
|
157
|
-
|
|
158
|
-
#### 2. Event-Based URL Updates
|
|
159
|
-
|
|
160
|
-
**Problem**: Initial implementation updated `@url` directly in `navigate()` method, which is not how Puppeteer works.
|
|
161
|
-
|
|
162
|
-
**Puppeteer's Approach**: URL updates happen via BiDi events:
|
|
163
|
-
- `browsingContext.historyUpdated`
|
|
164
|
-
- `browsingContext.domContentLoaded`
|
|
165
|
-
- `browsingContext.load`
|
|
166
|
-
|
|
167
|
-
**Fix**: Removed direct URL assignment from navigate():
|
|
168
|
-
|
|
169
|
-
```ruby
|
|
170
|
-
# lib/puppeteer/bidi/core/browsing_context.rb
|
|
171
|
-
def navigate(url, wait: nil)
|
|
172
|
-
raise BrowsingContextClosedError, @reason if closed?
|
|
173
|
-
params = { context: @id, url: url }
|
|
174
|
-
params[:wait] = wait if wait
|
|
175
|
-
result = session.send_command('browsingContext.navigate', params)
|
|
176
|
-
# URL will be updated via browsingContext.load event
|
|
177
|
-
result
|
|
178
|
-
end
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Event handlers (already implemented) update URL automatically:
|
|
182
|
-
|
|
183
|
-
```ruby
|
|
184
|
-
# History updated
|
|
185
|
-
session.on('browsingContext.historyUpdated') do |info|
|
|
186
|
-
next unless info['context'] == @id
|
|
187
|
-
@url = info['url']
|
|
188
|
-
emit(:history_updated, nil)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# DOM content loaded
|
|
192
|
-
session.on('browsingContext.domContentLoaded') do |info|
|
|
193
|
-
next unless info['context'] == @id
|
|
194
|
-
@url = info['url']
|
|
195
|
-
emit(:dom_content_loaded, nil)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Page loaded
|
|
199
|
-
session.on('browsingContext.load') do |info|
|
|
200
|
-
next unless info['context'] == @id
|
|
201
|
-
@url = info['url']
|
|
202
|
-
emit(:load, nil)
|
|
203
|
-
end
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
**Why this matters**: Event-based updates ensure URL synchronization even when navigation is triggered by user actions (like clicking links) rather than explicit `navigate()` calls.
|
|
207
|
-
|
|
208
|
-
### Test Coverage
|
|
209
|
-
|
|
210
|
-
#### Click Tests (20 tests in spec/integration/click_spec.rb)
|
|
211
|
-
|
|
212
|
-
Ported from [Puppeteer's click.spec.ts](https://github.com/puppeteer/puppeteer/blob/main/test/src/click.spec.ts):
|
|
213
|
-
|
|
214
|
-
1. **Basic clicking**: button, svg, wrapped links
|
|
215
|
-
2. **Edge cases**: window.Node removed, span with inline elements
|
|
216
|
-
3. **Navigation**: click after navigation, click links causing navigation
|
|
217
|
-
4. **Scrolling**: offscreen buttons, scrollable content
|
|
218
|
-
5. **Multi-click**: double click, triple click (text selection)
|
|
219
|
-
6. **Different buttons**: left, right (contextmenu), middle (auxclick)
|
|
220
|
-
7. **Visibility**: partially obscured button, rotated button
|
|
221
|
-
8. **Form elements**: checkbox toggle (input and label)
|
|
222
|
-
9. **Error handling**: missing selector
|
|
223
|
-
10. **Special cases**: disabled JavaScript, iframes (pending)
|
|
224
|
-
|
|
225
|
-
#### Page Tests (3 tests in spec/integration/page_spec.rb)
|
|
226
|
-
|
|
227
|
-
1. **Page.url**: Verify URL updates after navigation
|
|
228
|
-
2. **Page.setJavaScriptEnabled**: Control JavaScript execution (pending - Firefox limitation)
|
|
229
|
-
|
|
230
|
-
**All 108 integration tests pass** (4 pending due to Firefox BiDi limitations).
|
|
231
|
-
|
|
232
|
-
### Firefox BiDi Limitations
|
|
233
|
-
|
|
234
|
-
- `emulation.setScriptingEnabled`: Part of WebDriver BiDi spec but not yet implemented in Firefox
|
|
235
|
-
- Tests gracefully skip with clear messages using RSpec's `skip` feature
|
|
236
|
-
|
|
237
|
-
### Implementation Best Practices Learned
|
|
238
|
-
|
|
239
|
-
#### 1. Always Consult Puppeteer's Implementation First
|
|
240
|
-
|
|
241
|
-
**Workflow**:
|
|
242
|
-
1. Read Puppeteer's TypeScript implementation
|
|
243
|
-
2. Understand BiDi protocol calls being made
|
|
244
|
-
3. Implement Ruby equivalent with same logic flow
|
|
245
|
-
4. Port corresponding test cases
|
|
246
|
-
|
|
247
|
-
**Example**: The click implementation journey revealed that Puppeteer's architecture (Page → Frame → ElementHandle delegation) is critical for proper functionality.
|
|
248
|
-
|
|
249
|
-
#### 2. Stay Faithful to Puppeteer's Test Structure
|
|
250
|
-
|
|
251
|
-
**Initial mistake**: Created complex polling logic for navigation test
|
|
252
|
-
**Correction**: Simplified to match Puppeteer's simple approach:
|
|
253
|
-
|
|
254
|
-
```ruby
|
|
255
|
-
# Simple and correct (matches Puppeteer)
|
|
256
|
-
page.set_content("<a href=\"#{server.empty_page}\">empty.html</a>")
|
|
257
|
-
page.click('a') # Should not hang
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
#### 3. Event Subscription is Critical
|
|
261
|
-
|
|
262
|
-
**Key lesson**: BiDi requires explicit subscription to event modules. Without it:
|
|
263
|
-
- Navigation events don't fire
|
|
264
|
-
- URL updates don't work
|
|
265
|
-
- Tests timeout mysteriously
|
|
266
|
-
|
|
267
|
-
**Solution**: Subscribe early in browser/session initialization.
|
|
268
|
-
|
|
269
|
-
#### 4. Use RSpec `it` Syntax
|
|
270
|
-
|
|
271
|
-
Per Ruby/RSpec conventions, use `it` instead of `example`:
|
|
272
|
-
|
|
273
|
-
```ruby
|
|
274
|
-
# Correct
|
|
275
|
-
it 'should click the button' do
|
|
276
|
-
# ...
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
# Incorrect
|
|
280
|
-
example 'should click the button' do
|
|
281
|
-
# ...
|
|
282
|
-
end
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### BiDi Protocol Format Requirements
|
|
286
|
-
|
|
287
|
-
#### Origin Parameter Format
|
|
288
|
-
|
|
289
|
-
**Critical**: BiDi `input.performActions` expects `origin` as a string, not a hash:
|
|
290
|
-
|
|
291
|
-
```ruby
|
|
292
|
-
# CORRECT
|
|
293
|
-
origin: 'viewport'
|
|
294
|
-
|
|
295
|
-
# WRONG - causes protocol error
|
|
296
|
-
origin: { type: 'viewport' }
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
**Error message if wrong**:
|
|
300
|
-
```
|
|
301
|
-
Expected "origin" to be undefined, "viewport", "pointer", or an element,
|
|
302
|
-
got: [object Object] {"type":"viewport"}
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### Performance and Reliability
|
|
306
|
-
|
|
307
|
-
- **IntersectionObserver**: Fast and accurate visibility detection
|
|
308
|
-
- **Auto-scrolling**: Ensures elements are clickable before interaction
|
|
309
|
-
- **Event-driven**: URL updates via events enable proper async handling
|
|
310
|
-
- **Thread-safe**: BiDi protocol handles concurrent operations naturally
|
|
311
|
-
|
|
312
|
-
### Future Enhancements
|
|
313
|
-
|
|
314
|
-
Potential improvements for click/mouse functionality:
|
|
315
|
-
|
|
316
|
-
1. **Drag and drop**: Implement drag operations
|
|
317
|
-
2. **Hover**: Mouse move without click
|
|
318
|
-
3. **Wheel**: Mouse wheel scrolling
|
|
319
|
-
4. **Touch**: Touch events for mobile emulation
|
|
320
|
-
5. **Keyboard modifiers**: Click with Ctrl/Shift/Alt
|
|
321
|
-
6. **Frame support**: Click inside iframes (currently pending)
|
|
322
|
-
|
|
323
|
-
### Reference Implementation
|
|
324
|
-
|
|
325
|
-
Based on Puppeteer's implementation:
|
|
326
|
-
- [Page.click](https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/api/Page.ts)
|
|
327
|
-
- [Frame.click](https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/api/Frame.ts)
|
|
328
|
-
- [ElementHandle.click](https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/api/ElementHandle.ts)
|
|
329
|
-
- [Mouse input](https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/bidi/Input.ts)
|
|
330
|
-
- [Test specs](https://github.com/puppeteer/puppeteer/blob/main/test/src/click.spec.ts)
|
|
331
|
-
|
|
332
|
-
### Key Takeaways
|
|
333
|
-
|
|
334
|
-
1. **session.subscribe is mandatory** for BiDi event handling - don't forget it!
|
|
335
|
-
2. **Event-based state management** (URL updates via events, not direct assignment)
|
|
336
|
-
3. **BiDi protocol details matter** (string vs hash for origin parameter)
|
|
337
|
-
4. **Follow Puppeteer's architecture** (delegation patterns, event handling)
|
|
338
|
-
5. **Test simplicity** - stay faithful to Puppeteer's test structure
|
|
339
|
-
6. **Browser limitations** - gracefully handle unimplemented features (setScriptingEnabled)
|
|
340
|
-
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# Core Layer Gotchas
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This document covers non-obvious issues and pitfalls in the Core layer implementation.
|
|
6
|
-
|
|
7
|
-
## BrowsingContext: disposed? vs closed? Conflict
|
|
8
|
-
|
|
9
|
-
### The Problem
|
|
10
|
-
|
|
11
|
-
`BrowsingContext` uses both `Disposable::DisposableMixin` and defines `alias disposed? closed?`:
|
|
12
|
-
|
|
13
|
-
```ruby
|
|
14
|
-
class BrowsingContext < EventEmitter
|
|
15
|
-
include Disposable::DisposableMixin
|
|
16
|
-
|
|
17
|
-
def closed?
|
|
18
|
-
!@reason.nil?
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
alias disposed? closed?
|
|
22
|
-
end
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
This creates a conflict:
|
|
26
|
-
- `DisposableMixin#dispose` checks `disposed?` before proceeding
|
|
27
|
-
- `disposed?` is aliased to `closed?`
|
|
28
|
-
- `closed?` returns `true` when `@reason` is set
|
|
29
|
-
|
|
30
|
-
### The Bug
|
|
31
|
-
|
|
32
|
-
Original `dispose_context` implementation:
|
|
33
|
-
|
|
34
|
-
```ruby
|
|
35
|
-
def dispose_context(reason)
|
|
36
|
-
@reason = reason # Sets @reason, making closed? return true
|
|
37
|
-
dispose # dispose checks disposed?/closed?, sees true, returns early!
|
|
38
|
-
end
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
**Result**: `:closed` event was never emitted because `dispose` returned early.
|
|
42
|
-
|
|
43
|
-
### The Fix
|
|
44
|
-
|
|
45
|
-
Set `@reason` AFTER calling `dispose`:
|
|
46
|
-
|
|
47
|
-
```ruby
|
|
48
|
-
def dispose_context(reason)
|
|
49
|
-
# IMPORTANT: Call dispose BEFORE setting @reason
|
|
50
|
-
# Otherwise disposed?/closed? returns true and dispose returns early
|
|
51
|
-
dispose
|
|
52
|
-
@reason = reason
|
|
53
|
-
end
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Additional Fix: Emit :closed Before @disposed = true
|
|
57
|
-
|
|
58
|
-
`EventEmitter#emit` returns early if `@disposed` is true. But `DisposableMixin#dispose` sets `@disposed = true` before calling `perform_dispose`. This means any events emitted in `perform_dispose` would be ignored.
|
|
59
|
-
|
|
60
|
-
Solution: Override `dispose` to emit `:closed` before calling `super`:
|
|
61
|
-
|
|
62
|
-
```ruby
|
|
63
|
-
def dispose
|
|
64
|
-
return if disposed?
|
|
65
|
-
|
|
66
|
-
@reason ||= 'Browsing context closed'
|
|
67
|
-
emit(:closed, { reason: @reason }) # Emit BEFORE @disposed = true
|
|
68
|
-
|
|
69
|
-
super # This sets @disposed = true and calls perform_dispose
|
|
70
|
-
end
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## EventEmitter and DisposableMixin @disposed Interaction
|
|
74
|
-
|
|
75
|
-
Both `EventEmitter` and `DisposableMixin` use `@disposed` instance variable:
|
|
76
|
-
|
|
77
|
-
```ruby
|
|
78
|
-
# EventEmitter
|
|
79
|
-
def emit(event, data = nil)
|
|
80
|
-
return if @disposed # Early return if disposed
|
|
81
|
-
# ...
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def dispose
|
|
85
|
-
@disposed = true
|
|
86
|
-
@listeners.clear
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# DisposableMixin
|
|
90
|
-
def dispose
|
|
91
|
-
return if @disposed
|
|
92
|
-
@disposed = true
|
|
93
|
-
perform_dispose
|
|
94
|
-
end
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
When a class includes both (like `BrowsingContext`), they share the same `@disposed` variable. This is usually fine, but be aware:
|
|
98
|
-
|
|
99
|
-
1. **Order matters**: If you need to emit events during disposal, do it BEFORE setting `@disposed = true`
|
|
100
|
-
2. **Check disposal state carefully**: Use `disposed?` method, not `@disposed` directly
|
|
101
|
-
3. **Override dispose if needed**: To emit events or do cleanup that requires the emitter to still be active
|
|
102
|
-
|
|
103
|
-
## Debugging Tips
|
|
104
|
-
|
|
105
|
-
### Enable BiDi Debug Logging
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
DEBUG_BIDI_COMMAND=1 bundle exec rspec spec/integration/frame_spec.rb
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Track Disposal State
|
|
112
|
-
|
|
113
|
-
Add temporary debug logs:
|
|
114
|
-
|
|
115
|
-
```ruby
|
|
116
|
-
def dispose
|
|
117
|
-
puts "[DEBUG] dispose called for #{@id}, disposed?=#{disposed?}"
|
|
118
|
-
# ...
|
|
119
|
-
end
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Check Event Listener Registration
|
|
123
|
-
|
|
124
|
-
Verify listeners are registered on the correct instance:
|
|
125
|
-
|
|
126
|
-
```ruby
|
|
127
|
-
browsing_context.once(:closed) do
|
|
128
|
-
puts "[DEBUG] :closed received for #{browsing_context.id}"
|
|
129
|
-
end
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Related Files
|
|
133
|
-
|
|
134
|
-
- `lib/puppeteer/bidi/core/browsing_context.rb` - BrowsingContext implementation
|
|
135
|
-
- `lib/puppeteer/bidi/core/event_emitter.rb` - EventEmitter base class
|
|
136
|
-
- `lib/puppeteer/bidi/core/disposable.rb` - DisposableMixin module
|
data/CLAUDE/error_handling.md
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
# Error Handling and Custom Exceptions
|
|
2
|
-
|
|
3
|
-
This document covers the custom exception hierarchy, implementation patterns, and benefits of type-safe error handling in puppeteer-bidi.
|
|
4
|
-
|
|
5
|
-
### Philosophy
|
|
6
|
-
|
|
7
|
-
Use custom exception classes instead of inline string raises for:
|
|
8
|
-
- **Type safety**: Enable `rescue` by specific exception type
|
|
9
|
-
- **DRY principle**: Centralize error messages
|
|
10
|
-
- **Debugging**: Attach contextual data to exception objects
|
|
11
|
-
- **Consistency**: Uniform error handling across codebase
|
|
12
|
-
|
|
13
|
-
### Custom Exception Hierarchy
|
|
14
|
-
|
|
15
|
-
```ruby
|
|
16
|
-
StandardError
|
|
17
|
-
└── Puppeteer::Bidi::Error
|
|
18
|
-
├── JSHandleDisposedError
|
|
19
|
-
├── PageClosedError
|
|
20
|
-
├── FrameDetachedError
|
|
21
|
-
└── SelectorNotFoundError
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
All custom exceptions inherit from `Puppeteer::Bidi::Error` for consistent rescue patterns.
|
|
25
|
-
|
|
26
|
-
### Exception Classes
|
|
27
|
-
|
|
28
|
-
#### JSHandleDisposedError
|
|
29
|
-
|
|
30
|
-
**When raised**: Attempting to use a disposed JSHandle or ElementHandle
|
|
31
|
-
|
|
32
|
-
**Location**: `lib/puppeteer/bidi/errors.rb`
|
|
33
|
-
|
|
34
|
-
```ruby
|
|
35
|
-
class JSHandleDisposedError < Error
|
|
36
|
-
def initialize
|
|
37
|
-
super('JSHandle is disposed')
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**Usage**:
|
|
43
|
-
```ruby
|
|
44
|
-
# JSHandle and ElementHandle
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
def assert_not_disposed
|
|
48
|
-
raise JSHandleDisposedError if @disposed
|
|
49
|
-
end
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
**Affected methods**:
|
|
53
|
-
- `JSHandle#evaluate`, `#evaluate_handle`, `#get_property`, `#get_properties`, `#json_value`
|
|
54
|
-
- `ElementHandle#query_selector`, `#query_selector_all`, `#eval_on_selector`, `#eval_on_selector_all`
|
|
55
|
-
|
|
56
|
-
#### PageClosedError
|
|
57
|
-
|
|
58
|
-
**When raised**: Attempting to use a closed Page
|
|
59
|
-
|
|
60
|
-
**Location**: `lib/puppeteer/bidi/errors.rb`
|
|
61
|
-
|
|
62
|
-
```ruby
|
|
63
|
-
class PageClosedError < Error
|
|
64
|
-
def initialize
|
|
65
|
-
super('Page is closed')
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
**Usage**:
|
|
71
|
-
```ruby
|
|
72
|
-
# Page
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
def assert_not_closed
|
|
76
|
-
raise PageClosedError if closed?
|
|
77
|
-
end
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
**Affected methods**:
|
|
81
|
-
- `Page#goto`, `#set_content`, `#screenshot`
|
|
82
|
-
|
|
83
|
-
#### FrameDetachedError
|
|
84
|
-
|
|
85
|
-
**When raised**: Attempting to use a detached Frame
|
|
86
|
-
|
|
87
|
-
**Location**: `lib/puppeteer/bidi/errors.rb`
|
|
88
|
-
|
|
89
|
-
```ruby
|
|
90
|
-
class FrameDetachedError < Error
|
|
91
|
-
def initialize
|
|
92
|
-
super('Frame is detached')
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
**Usage**:
|
|
98
|
-
```ruby
|
|
99
|
-
# Frame
|
|
100
|
-
private
|
|
101
|
-
|
|
102
|
-
def assert_not_detached
|
|
103
|
-
raise FrameDetachedError if @browsing_context.closed?
|
|
104
|
-
end
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
**Affected methods**:
|
|
108
|
-
- `Frame#evaluate`, `#evaluate_handle`, `#document`
|
|
109
|
-
|
|
110
|
-
#### SelectorNotFoundError
|
|
111
|
-
|
|
112
|
-
**When raised**: CSS selector doesn't match any elements in `eval_on_selector`
|
|
113
|
-
|
|
114
|
-
**Location**: `lib/puppeteer/bidi/errors.rb`
|
|
115
|
-
|
|
116
|
-
```ruby
|
|
117
|
-
class SelectorNotFoundError < Error
|
|
118
|
-
attr_reader :selector
|
|
119
|
-
|
|
120
|
-
def initialize(selector)
|
|
121
|
-
@selector = selector
|
|
122
|
-
super("Error: failed to find element matching selector \"#{selector}\"")
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**Usage**:
|
|
128
|
-
```ruby
|
|
129
|
-
# ElementHandle#eval_on_selector
|
|
130
|
-
element_handle = query_selector(selector)
|
|
131
|
-
raise SelectorNotFoundError, selector unless element_handle
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
**Contextual data**: The `selector` value is accessible via the exception object for debugging.
|
|
135
|
-
|
|
136
|
-
### Implementation Pattern
|
|
137
|
-
|
|
138
|
-
#### 1. Define Custom Exception
|
|
139
|
-
|
|
140
|
-
```ruby
|
|
141
|
-
# lib/puppeteer/bidi/errors.rb
|
|
142
|
-
class MyCustomError < Error
|
|
143
|
-
def initialize(context = nil)
|
|
144
|
-
@context = context
|
|
145
|
-
super("Error message with #{context}")
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
#### 2. Add Private Assertion Method
|
|
151
|
-
|
|
152
|
-
```ruby
|
|
153
|
-
class MyClass
|
|
154
|
-
private
|
|
155
|
-
|
|
156
|
-
def assert_valid_state
|
|
157
|
-
raise MyCustomError, @context if invalid?
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
#### 3. Replace Inline Raises
|
|
163
|
-
|
|
164
|
-
```ruby
|
|
165
|
-
# Before
|
|
166
|
-
def my_method
|
|
167
|
-
raise 'Invalid state' if invalid?
|
|
168
|
-
# ...
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# After
|
|
172
|
-
def my_method
|
|
173
|
-
assert_valid_state
|
|
174
|
-
# ...
|
|
175
|
-
end
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Benefits
|
|
179
|
-
|
|
180
|
-
**Type-safe error handling**:
|
|
181
|
-
```ruby
|
|
182
|
-
begin
|
|
183
|
-
page.eval_on_selector('.missing', 'e => e.id')
|
|
184
|
-
rescue SelectorNotFoundError => e
|
|
185
|
-
puts "Selector '#{e.selector}' not found"
|
|
186
|
-
rescue JSHandleDisposedError
|
|
187
|
-
puts "Handle was disposed"
|
|
188
|
-
end
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**Consistent error messages**: Single source of truth for error text
|
|
192
|
-
|
|
193
|
-
**Reduced duplication**: 16 inline raises eliminated across codebase
|
|
194
|
-
|
|
195
|
-
**Better debugging**: Exception objects carry contextual information
|
|
196
|
-
|
|
197
|
-
### Testing Custom Exceptions
|
|
198
|
-
|
|
199
|
-
Tests use regex matching for backward compatibility:
|
|
200
|
-
|
|
201
|
-
```ruby
|
|
202
|
-
# Test remains compatible with custom exception
|
|
203
|
-
expect {
|
|
204
|
-
page.eval_on_selector('non-existing', 'e => e.id')
|
|
205
|
-
}.to raise_error(/failed to find element matching selector/)
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
This allows tests to pass with both string raises and custom exceptions.
|
|
209
|
-
|
|
210
|
-
### Refactoring Statistics
|
|
211
|
-
|
|
212
|
-
| Class | Inline Raises Replaced | Private Assert Method |
|
|
213
|
-
|-------|------------------------|----------------------|
|
|
214
|
-
| JSHandle | 5 | `assert_not_disposed` |
|
|
215
|
-
| ElementHandle | 4 + 1 (selector) | (inherited) |
|
|
216
|
-
| Page | 3 | `assert_not_closed` |
|
|
217
|
-
| Frame | 3 | `assert_not_detached` |
|
|
218
|
-
| **Total** | **16** | **3 methods** |
|
|
219
|
-
|
|
220
|
-
### Future Considerations
|
|
221
|
-
|
|
222
|
-
When adding new error conditions:
|
|
223
|
-
|
|
224
|
-
1. **Create custom exception** in `lib/puppeteer/bidi/errors.rb`
|
|
225
|
-
2. **Add to exception hierarchy** by inheriting from `Error`
|
|
226
|
-
3. **Include contextual data** as `attr_reader` if needed
|
|
227
|
-
4. **Create private assert method** in the relevant class
|
|
228
|
-
5. **Replace inline raises** with assert method calls
|
|
229
|
-
6. **Update tests** to use regex matching for flexibility
|
|
230
|
-
|
|
231
|
-
This pattern ensures consistency and maintainability across the entire codebase.
|
|
232
|
-
|