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,136 @@
|
|
|
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
|
|
@@ -0,0 +1,232 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# FileChooser Implementation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
FileChooser provides file upload functionality through the WebDriver BiDi `input.setFiles` command and `input.fileDialogOpened` event.
|
|
6
|
+
|
|
7
|
+
## Firefox Nightly Requirement
|
|
8
|
+
|
|
9
|
+
**Important**: The `input.fileDialogOpened` event is only supported in Firefox Nightly. Stable Firefox does not fire this event.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Run tests with Firefox Nightly
|
|
13
|
+
FIREFOX_PATH="/Applications/Firefox Nightly.app/Contents/MacOS/firefox" bundle exec rspec spec/integration/input_spec.rb
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The browser launcher prioritizes Firefox Nightly in its search order.
|
|
17
|
+
|
|
18
|
+
## API Design
|
|
19
|
+
|
|
20
|
+
### Block-based Interface
|
|
21
|
+
|
|
22
|
+
Ruby uses a block-based interface instead of JavaScript's Promise.all pattern:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# Ruby (block-based)
|
|
26
|
+
chooser = page.wait_for_file_chooser do
|
|
27
|
+
page.click('input[type=file]')
|
|
28
|
+
end
|
|
29
|
+
chooser.accept(['/path/to/file.txt'])
|
|
30
|
+
|
|
31
|
+
# JavaScript equivalent
|
|
32
|
+
const [chooser] = await Promise.all([
|
|
33
|
+
page.waitForFileChooser(),
|
|
34
|
+
page.click('input[type=file]'),
|
|
35
|
+
]);
|
|
36
|
+
await chooser.accept(['/path/to/file.txt']);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Direct Upload
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
input = page.query_selector('input[type=file]')
|
|
43
|
+
input.upload_file('/path/to/file.txt')
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Implementation Flow
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
ElementHandle#upload_file(files)
|
|
50
|
+
└── Frame#set_files(element, files)
|
|
51
|
+
└── BrowsingContext#set_files(shared_reference, files)
|
|
52
|
+
└── BiDi command: input.setFiles
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This follows Puppeteer's pattern where ElementHandle delegates to Frame, which then calls the BrowsingContext.
|
|
56
|
+
|
|
57
|
+
## Firefox BiDi Limitations
|
|
58
|
+
|
|
59
|
+
### 1. Detached Elements Not Supported
|
|
60
|
+
|
|
61
|
+
Firefox does not fire `input.fileDialogOpened` for file inputs that are not attached to the DOM:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# This will timeout - detached element
|
|
65
|
+
page.wait_for_file_chooser do
|
|
66
|
+
page.evaluate(<<~JS)
|
|
67
|
+
() => {
|
|
68
|
+
const el = document.createElement('input');
|
|
69
|
+
el.type = 'file';
|
|
70
|
+
el.click(); // No event fired
|
|
71
|
+
}
|
|
72
|
+
JS
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Non-existent Files Rejected
|
|
77
|
+
|
|
78
|
+
Firefox BiDi rejects files that don't exist with `NS_ERROR_FILE_NOT_FOUND`. Chrome allows setting non-existent files.
|
|
79
|
+
|
|
80
|
+
### 3. Event Subscription
|
|
81
|
+
|
|
82
|
+
The `input` module must be subscribed at session level. This is handled automatically in `Session#initialize_session`:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
subscribe_modules = %w[browsingContext network log script input]
|
|
86
|
+
subscribe(subscribe_modules)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Key Classes
|
|
90
|
+
|
|
91
|
+
- `FileChooser` - Wraps the element with `accept()`, `cancel()`, `multiple?()` methods
|
|
92
|
+
- `Page#wait_for_file_chooser` - Listens for `filedialogopened` event with timeout
|
|
93
|
+
- `ElementHandle#upload_file` - Resolves paths and delegates to frame
|
|
94
|
+
- `Frame#set_files` - Calls BrowsingContext with shared reference
|
|
95
|
+
- `BrowsingContext#set_files` - Sends BiDi `input.setFiles` command
|