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,214 @@
|
|
|
1
|
+
# Porting Puppeteer to Ruby
|
|
2
|
+
|
|
3
|
+
Best practices for implementing Puppeteer features in puppeteer-bidi.
|
|
4
|
+
|
|
5
|
+
## 1. Reference Implementation First
|
|
6
|
+
|
|
7
|
+
**Always consult the official Puppeteer implementation before implementing features:**
|
|
8
|
+
|
|
9
|
+
- **TypeScript source files**:
|
|
10
|
+
- `packages/puppeteer-core/src/bidi/Page.ts` - High-level Page API
|
|
11
|
+
- `packages/puppeteer-core/src/bidi/BrowsingContext.ts` - Core BiDi context
|
|
12
|
+
- `packages/puppeteer-core/src/api/Page.ts` - Common Page interface
|
|
13
|
+
|
|
14
|
+
- **Test files**:
|
|
15
|
+
- `test/src/screenshot.spec.ts` - Screenshot test suite
|
|
16
|
+
- `test/golden-firefox/` - Golden images for visual regression testing
|
|
17
|
+
|
|
18
|
+
**Example workflow:**
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# 1. Read Puppeteer's TypeScript implementation
|
|
22
|
+
# 2. Understand the BiDi protocol calls being made
|
|
23
|
+
# 3. Implement Ruby equivalent with same logic flow
|
|
24
|
+
# 4. Port corresponding test cases
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 2. Test Infrastructure Setup
|
|
28
|
+
|
|
29
|
+
**Use async-http for test servers** (lightweight + Async-friendly):
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# spec/support/test_server.rb
|
|
33
|
+
endpoint = Async::HTTP::Endpoint.parse("http://127.0.0.1:#{@port}")
|
|
34
|
+
|
|
35
|
+
server = Async::HTTP::Server.for(endpoint) do |request|
|
|
36
|
+
if handler = lookup_route(request.path)
|
|
37
|
+
notify_request(request.path)
|
|
38
|
+
respond_with_handler(handler, request)
|
|
39
|
+
else
|
|
40
|
+
serve_static_asset(request)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
server.run
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Helper pattern for integration tests:**
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# Optimized helper - reuses shared browser, creates new page per test
|
|
51
|
+
def with_test_state
|
|
52
|
+
page = $shared_browser.new_page
|
|
53
|
+
context = $shared_browser.default_browser_context
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
yield(page: page, server: $shared_test_server, browser: $shared_browser, context: context)
|
|
57
|
+
ensure
|
|
58
|
+
page.close unless page.closed?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 3. BiDi Protocol Data Deserialization
|
|
64
|
+
|
|
65
|
+
**BiDi returns values in special format - always deserialize:**
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# BiDi response format:
|
|
69
|
+
# [["width", {"type" => "number", "value" => 500}],
|
|
70
|
+
# ["height", {"type" => "number", "value" => 1000}]]
|
|
71
|
+
|
|
72
|
+
def deserialize_result(result)
|
|
73
|
+
value = result['value']
|
|
74
|
+
return value unless value.is_a?(Array)
|
|
75
|
+
|
|
76
|
+
# Convert to Ruby Hash
|
|
77
|
+
if value.all? { |item| item.is_a?(Array) && item.length == 2 }
|
|
78
|
+
value.each_with_object({}) do |(key, val), hash|
|
|
79
|
+
hash[key] = deserialize_value(val)
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
value
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def deserialize_value(val)
|
|
87
|
+
case val['type']
|
|
88
|
+
when 'number' then val['value']
|
|
89
|
+
when 'string' then val['value']
|
|
90
|
+
when 'boolean' then val['value']
|
|
91
|
+
when 'undefined', 'null' then nil
|
|
92
|
+
else val['value']
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 4. Implementing Puppeteer-Compatible APIs
|
|
98
|
+
|
|
99
|
+
**Follow Puppeteer's exact logic flow:**
|
|
100
|
+
|
|
101
|
+
Example: `fullPage` screenshot implementation
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# From Puppeteer's Page.ts:
|
|
105
|
+
# if (options.fullPage) {
|
|
106
|
+
# if (!options.captureBeyondViewport) {
|
|
107
|
+
# // Resize viewport to full page
|
|
108
|
+
# }
|
|
109
|
+
# } else {
|
|
110
|
+
# options.captureBeyondViewport = false;
|
|
111
|
+
# }
|
|
112
|
+
|
|
113
|
+
if full_page
|
|
114
|
+
unless capture_beyond_viewport
|
|
115
|
+
scroll_dimensions = evaluate(...)
|
|
116
|
+
set_viewport(scroll_dimensions)
|
|
117
|
+
begin
|
|
118
|
+
data = capture_screenshot(origin: 'viewport')
|
|
119
|
+
ensure
|
|
120
|
+
set_viewport(original_viewport) # Always restore
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
options[:origin] = 'document'
|
|
124
|
+
end
|
|
125
|
+
elsif !clip
|
|
126
|
+
capture_beyond_viewport = false # Match Puppeteer behavior
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Key principles:**
|
|
131
|
+
|
|
132
|
+
- Use `begin/ensure` blocks for cleanup (viewport restoration, etc.)
|
|
133
|
+
- Match Puppeteer's parameter defaults exactly
|
|
134
|
+
- Follow the same conditional logic order
|
|
135
|
+
|
|
136
|
+
## 5. Layer Architecture
|
|
137
|
+
|
|
138
|
+
**Maintain clear separation:**
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
High-level API (lib/puppeteer/bidi/)
|
|
142
|
+
├── Browser - User-facing browser interface
|
|
143
|
+
├── BrowserContext - Session management
|
|
144
|
+
└── Page - Page automation API
|
|
145
|
+
|
|
146
|
+
Core Layer (lib/puppeteer/bidi/core/)
|
|
147
|
+
├── Session - BiDi session management
|
|
148
|
+
├── Browser - Low-level browser operations
|
|
149
|
+
├── UserContext - BiDi user context
|
|
150
|
+
└── BrowsingContext - BiDi browsing context (tab/frame)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 6. Setting Page Content
|
|
154
|
+
|
|
155
|
+
**Use data URLs with base64 encoding:**
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
def set_content(html, wait_until: 'load')
|
|
159
|
+
# Encode HTML in base64 to avoid URL encoding issues
|
|
160
|
+
encoded = Base64.strict_encode64(html)
|
|
161
|
+
data_url = "data:text/html;base64,#{encoded}"
|
|
162
|
+
goto(data_url, wait_until: wait_until)
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Why base64:**
|
|
167
|
+
|
|
168
|
+
- Avoids URL encoding issues with special characters
|
|
169
|
+
- Handles multi-byte characters correctly
|
|
170
|
+
- Standard approach in browser automation tools
|
|
171
|
+
|
|
172
|
+
## 7. Viewport Restoration
|
|
173
|
+
|
|
174
|
+
**Always restore viewport after temporary changes:**
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# Save current viewport (may be nil)
|
|
178
|
+
original_viewport = viewport
|
|
179
|
+
|
|
180
|
+
# If no viewport set, save window size
|
|
181
|
+
unless original_viewport
|
|
182
|
+
original_size = evaluate('({ width: window.innerWidth, height: window.innerHeight })')
|
|
183
|
+
original_viewport = { width: original_size['width'].to_i, height: original_size['height'].to_i }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Change viewport temporarily
|
|
187
|
+
set_viewport(width: new_width, height: new_height)
|
|
188
|
+
|
|
189
|
+
begin
|
|
190
|
+
# Do work
|
|
191
|
+
ensure
|
|
192
|
+
# Always restore
|
|
193
|
+
set_viewport(**original_viewport) if original_viewport
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## 8. Test Assets Policy
|
|
198
|
+
|
|
199
|
+
**CRITICAL**: Always use Puppeteer's official test assets without modification.
|
|
200
|
+
|
|
201
|
+
- **Source**: https://github.com/puppeteer/puppeteer/tree/main/test/assets
|
|
202
|
+
- **Rule**: Never modify test asset files (HTML, CSS, images) in `spec/assets/`
|
|
203
|
+
- **Verification**: Before creating PR, verify all `spec/assets/` files match Puppeteer's official versions
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# During development - OK to experiment
|
|
207
|
+
vim spec/assets/test.html # Temporary modification for debugging
|
|
208
|
+
|
|
209
|
+
# Before PR - MUST revert to official
|
|
210
|
+
curl -sL https://raw.githubusercontent.com/puppeteer/puppeteer/main/test/assets/test.html \
|
|
211
|
+
-o spec/assets/test.html
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Why this matters**: Test assets are designed to test specific edge cases (rotated elements, complex layouts, etc.). Using simplified versions defeats the purpose of these tests.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# QueryHandler Implementation
|
|
2
|
+
|
|
3
|
+
The QueryHandler system provides extensible selector handling for CSS, XPath, text, and other selector types.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
QueryHandler (singleton)
|
|
9
|
+
├── get_query_handler_and_selector(selector)
|
|
10
|
+
│ └── Returns: { updated_selector, polling, query_handler }
|
|
11
|
+
|
|
12
|
+
BaseQueryHandler
|
|
13
|
+
├── run_query_one(element, selector) → ElementHandle | nil
|
|
14
|
+
├── run_query_all(element, selector) → Array<ElementHandle>
|
|
15
|
+
└── wait_for(element_or_frame, selector, options)
|
|
16
|
+
|
|
17
|
+
Implementations:
|
|
18
|
+
├── CSSQueryHandler - Default, uses cssQuerySelector/cssQuerySelectorAll
|
|
19
|
+
├── XPathQueryHandler - xpath/ prefix, uses xpathQuerySelectorAll
|
|
20
|
+
└── TextQueryHandler - text/ prefix, uses textQuerySelectorAll
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Selector Prefixes
|
|
24
|
+
|
|
25
|
+
| Prefix | Handler | Example |
|
|
26
|
+
| --------- | ------------------ | --------------------------- |
|
|
27
|
+
| (none) | CSSQueryHandler | `div.foo`, `#id` |
|
|
28
|
+
| `xpath/` | XPathQueryHandler | `xpath/html/body/div` |
|
|
29
|
+
| `text/` | TextQueryHandler | `text/Hello World` |
|
|
30
|
+
| `aria/` | ARIAQueryHandler | `aria/Submit[role="button"]`|
|
|
31
|
+
| `pierce/` | PierceQueryHandler | `pierce/.shadow-element` |
|
|
32
|
+
|
|
33
|
+
## Implementation Pattern
|
|
34
|
+
|
|
35
|
+
All query handlers follow the same pattern: override `query_one_script`, `query_all_script`, and `wait_for_selector_script` to define the JavaScript that runs in the browser.
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
class CSSQueryHandler < BaseQueryHandler
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def query_one_script
|
|
42
|
+
<<~JAVASCRIPT
|
|
43
|
+
(PuppeteerUtil, element, selector) => {
|
|
44
|
+
return PuppeteerUtil.cssQuerySelector(element, selector);
|
|
45
|
+
}
|
|
46
|
+
JAVASCRIPT
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def query_all_script
|
|
50
|
+
<<~JAVASCRIPT
|
|
51
|
+
async (PuppeteerUtil, element, selector) => {
|
|
52
|
+
return [...PuppeteerUtil.cssQuerySelectorAll(element, selector)];
|
|
53
|
+
}
|
|
54
|
+
JAVASCRIPT
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def wait_for_selector_script
|
|
58
|
+
<<~JAVASCRIPT
|
|
59
|
+
(PuppeteerUtil, selector, root, visibility) => {
|
|
60
|
+
const element = PuppeteerUtil.cssQuerySelector(root || document, selector);
|
|
61
|
+
return PuppeteerUtil.checkVisibility(element, visibility === null ? undefined : visibility);
|
|
62
|
+
}
|
|
63
|
+
JAVASCRIPT
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## TextQueryHandler - Special Case
|
|
69
|
+
|
|
70
|
+
`textQuerySelectorAll` cannot be extracted via `toString()` because it references helper functions (`f`, `m`, `d`) that are only available within the PuppeteerUtil closure. So TextQueryHandler uses a different pattern: call `textQuerySelectorAll` directly from PuppeteerUtil instead of recreating the function.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
class TextQueryHandler < BaseQueryHandler
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def query_one_script
|
|
77
|
+
<<~JAVASCRIPT
|
|
78
|
+
(PuppeteerUtil, element, selector) => {
|
|
79
|
+
for (const result of PuppeteerUtil.textQuerySelectorAll(element, selector)) {
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
JAVASCRIPT
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def query_all_script
|
|
88
|
+
<<~JAVASCRIPT
|
|
89
|
+
async (PuppeteerUtil, element, selector) => {
|
|
90
|
+
return [...PuppeteerUtil.textQuerySelectorAll(element, selector)];
|
|
91
|
+
}
|
|
92
|
+
JAVASCRIPT
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Handle Adoption Pattern
|
|
98
|
+
|
|
99
|
+
After navigation, the sandbox realm is destroyed and `puppeteer_util` handles become stale. The solution is to adopt the element into the isolated realm BEFORE calling any query methods:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
def run_query_one(element, selector)
|
|
103
|
+
realm = element.frame.isolated_realm
|
|
104
|
+
|
|
105
|
+
# Adopt the element into the isolated realm first.
|
|
106
|
+
# This ensures the realm is valid and triggers puppeteer_util reset if needed
|
|
107
|
+
# after navigation (mirrors Puppeteer's @bindIsolatedHandle decorator pattern).
|
|
108
|
+
adopted_element = realm.adopt_handle(element)
|
|
109
|
+
|
|
110
|
+
result = realm.call_function(
|
|
111
|
+
query_one_script,
|
|
112
|
+
false,
|
|
113
|
+
arguments: [
|
|
114
|
+
Serializer.serialize(realm.puppeteer_util_lazy_arg),
|
|
115
|
+
adopted_element.remote_value,
|
|
116
|
+
Serializer.serialize(selector)
|
|
117
|
+
]
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# ... handle result ...
|
|
121
|
+
ensure
|
|
122
|
+
adopted_element&.dispose
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Why this matters:**
|
|
127
|
+
|
|
128
|
+
1. After navigation, sandbox realm is destroyed
|
|
129
|
+
2. `:updated` event that resets `puppeteer_util` isn't fired until we call INTO the sandbox
|
|
130
|
+
3. Calling `adopt_handle` first ensures the realm exists and is valid
|
|
131
|
+
4. Then `puppeteer_util` will be fresh (re-evaluated if realm was recreated)
|
|
132
|
+
|
|
133
|
+
## Debugging
|
|
134
|
+
|
|
135
|
+
Use `DEBUG_BIDI_COMMAND=1` to see protocol messages:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
DEBUG_BIDI_COMMAND=1 bundle exec rspec spec/integration/queryhandler_spec.rb:8
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This shows:
|
|
142
|
+
1. PuppeteerUtil being evaluated in sandbox
|
|
143
|
+
2. The query function being called with arguments
|
|
144
|
+
3. The result being returned
|
|
145
|
+
|
|
146
|
+
## Adding New Query Handlers
|
|
147
|
+
|
|
148
|
+
1. Create a new class extending `BaseQueryHandler`
|
|
149
|
+
2. Override `query_one_script`, `query_all_script`, and `wait_for_selector_script`
|
|
150
|
+
3. Register in `BUILTIN_QUERY_HANDLERS` constant
|
|
151
|
+
4. The script receives `(PuppeteerUtil, element, selector)` as arguments
|
|
152
|
+
5. Return the element(s) found or null/empty array
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
class MyQueryHandler < BaseQueryHandler
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def query_one_script
|
|
159
|
+
<<~JAVASCRIPT
|
|
160
|
+
(PuppeteerUtil, element, selector) => {
|
|
161
|
+
// Use PuppeteerUtil.myQuerySelector if available
|
|
162
|
+
// Or implement custom logic
|
|
163
|
+
return element.querySelector(selector);
|
|
164
|
+
}
|
|
165
|
+
JAVASCRIPT
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def query_all_script
|
|
169
|
+
<<~JAVASCRIPT
|
|
170
|
+
async (PuppeteerUtil, element, selector) => {
|
|
171
|
+
return [...element.querySelectorAll(selector)];
|
|
172
|
+
}
|
|
173
|
+
JAVASCRIPT
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def wait_for_selector_script
|
|
177
|
+
<<~JAVASCRIPT
|
|
178
|
+
(PuppeteerUtil, selector, root, visibility) => {
|
|
179
|
+
const element = (root || document).querySelector(selector);
|
|
180
|
+
return PuppeteerUtil.checkVisibility(element, visibility === null ? undefined : visibility);
|
|
181
|
+
}
|
|
182
|
+
JAVASCRIPT
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Test Coverage
|
|
188
|
+
|
|
189
|
+
Tests are in `spec/integration/queryhandler_spec.rb`:
|
|
190
|
+
|
|
191
|
+
- Text selectors: 12 tests (query_selector, query_selector_all, shadow DOM piercing, etc.)
|
|
192
|
+
- XPath selectors: 6 tests (in Page and ElementHandle)
|
|
193
|
+
|
|
194
|
+
Tests ported from Puppeteer's `test/src/queryhandler.spec.ts`.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# RSpec: pending vs skip
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
RSpec provides two mechanisms for handling tests that cannot or should not run: `pending` and `skip`. Understanding when to use each is critical for documenting browser limitations and future work.
|
|
6
|
+
|
|
7
|
+
## Difference
|
|
8
|
+
|
|
9
|
+
### skip
|
|
10
|
+
|
|
11
|
+
**Completely skips the test** - does not run any code:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
it 'should work' do
|
|
15
|
+
skip 'feature not implemented'
|
|
16
|
+
|
|
17
|
+
# This code NEVER runs
|
|
18
|
+
page.do_something
|
|
19
|
+
expect(result).to be_truthy
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Output**: Test marked as skipped, no execution, no error trace.
|
|
24
|
+
|
|
25
|
+
### pending
|
|
26
|
+
|
|
27
|
+
**Runs the test** and expects it to fail:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
it 'should work' do
|
|
31
|
+
pending 'feature not implemented'
|
|
32
|
+
|
|
33
|
+
# This code RUNS and is expected to fail
|
|
34
|
+
page.do_something # Raises error
|
|
35
|
+
expect(result).to be_truthy
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Output**: Test marked as pending with full error trace showing exactly what failed.
|
|
40
|
+
|
|
41
|
+
## When to Use Each
|
|
42
|
+
|
|
43
|
+
### Use `pending` for:
|
|
44
|
+
|
|
45
|
+
1. **Browser limitations** - Features not yet supported by Firefox BiDi
|
|
46
|
+
2. **Known failures** - Code exists but fails due to external issues
|
|
47
|
+
3. **Documentation** - Want to show error trace to document what's missing
|
|
48
|
+
|
|
49
|
+
### Use `skip` for:
|
|
50
|
+
|
|
51
|
+
1. **Unimplemented features** - Code doesn't exist yet
|
|
52
|
+
2. **Environment issues** - Test requires specific setup not available
|
|
53
|
+
3. **Temporary exclusion** - Test is broken and needs fixing
|
|
54
|
+
|
|
55
|
+
## Firefox BiDi Limitations
|
|
56
|
+
|
|
57
|
+
For features that exist in BiDi spec but not yet implemented in Firefox, use `pending`:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
describe 'Page.setJavaScriptEnabled' do
|
|
61
|
+
it 'should work' do
|
|
62
|
+
# Pending: Firefox does not yet support emulation.setScriptingEnabled BiDi command
|
|
63
|
+
pending 'emulation.setScriptingEnabled not supported by Firefox yet'
|
|
64
|
+
|
|
65
|
+
with_test_state do |page:, **|
|
|
66
|
+
page.set_javascript_enabled(false)
|
|
67
|
+
expect(page.javascript_enabled?).to be false
|
|
68
|
+
|
|
69
|
+
page.goto('data:text/html, <script>var something = "forbidden"</script>')
|
|
70
|
+
|
|
71
|
+
error = nil
|
|
72
|
+
begin
|
|
73
|
+
page.evaluate('something')
|
|
74
|
+
rescue => e
|
|
75
|
+
error = e
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
expect(error).not_to be_nil
|
|
79
|
+
expect(error.message).to include('something is not defined')
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Why pending, not skip**:
|
|
86
|
+
- Code path exists (`page.set_javascript_enabled`)
|
|
87
|
+
- BiDi command exists in spec (`emulation.setScriptingEnabled`)
|
|
88
|
+
- Firefox just hasn't implemented it yet
|
|
89
|
+
- Running the test shows exactly what error Firefox returns
|
|
90
|
+
|
|
91
|
+
## Output Comparison
|
|
92
|
+
|
|
93
|
+
### With `skip`
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Page
|
|
97
|
+
Page.setJavaScriptEnabled
|
|
98
|
+
should work (SKIPPED)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
No error information, no way to know what's missing.
|
|
102
|
+
|
|
103
|
+
### With `pending`
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Page
|
|
107
|
+
Page.setJavaScriptEnabled
|
|
108
|
+
should work (PENDING: emulation.setScriptingEnabled not supported by Firefox yet)
|
|
109
|
+
|
|
110
|
+
Pending: (Failures listed here are expected and do not affect your suite's status)
|
|
111
|
+
|
|
112
|
+
1) Page Page.setJavaScriptEnabled should work
|
|
113
|
+
# emulation.setScriptingEnabled not supported by Firefox yet
|
|
114
|
+
Failure/Error: raise ProtocolError, "BiDi error (#{method}): #{result['error']['message']}"
|
|
115
|
+
|
|
116
|
+
Puppeteer::Bidi::Connection::ProtocolError:
|
|
117
|
+
BiDi error (emulation.setScriptingEnabled):
|
|
118
|
+
# ./lib/puppeteer/bidi/connection.rb:71:in 'send_command'
|
|
119
|
+
# ./lib/puppeteer/bidi/core/browsing_context.rb:331:in 'set_javascript_enabled'
|
|
120
|
+
# ./lib/puppeteer/bidi/page.rb:313:in 'set_javascript_enabled'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Full error trace shows:
|
|
124
|
+
- Which BiDi command failed
|
|
125
|
+
- Error message from Firefox
|
|
126
|
+
- Complete stack trace
|
|
127
|
+
- Where in our code it failed
|
|
128
|
+
|
|
129
|
+
## Implementation Pattern
|
|
130
|
+
|
|
131
|
+
### Before (Incorrect - Using skip in before block)
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
describe 'Page.setJavaScriptEnabled' do
|
|
135
|
+
before do
|
|
136
|
+
skip 'emulation.setScriptingEnabled not supported by Firefox yet'
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'should work' do
|
|
140
|
+
# Never runs
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'setInterval should pause' do
|
|
144
|
+
# Never runs
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Problems**:
|
|
150
|
+
- Tests don't run at all
|
|
151
|
+
- No error information
|
|
152
|
+
- Not clear which BiDi command is missing
|
|
153
|
+
|
|
154
|
+
### After (Correct - Using pending in individual tests)
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
describe 'Page.setJavaScriptEnabled' do
|
|
158
|
+
it 'should work' do
|
|
159
|
+
pending 'emulation.setScriptingEnabled not supported by Firefox yet'
|
|
160
|
+
|
|
161
|
+
with_test_state do |page:, **|
|
|
162
|
+
# Test code runs and fails with proper error trace
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'setInterval should pause' do
|
|
167
|
+
pending 'emulation.setScriptingEnabled not supported by Firefox yet'
|
|
168
|
+
|
|
169
|
+
with_test_state do |page:, **|
|
|
170
|
+
# Test code runs and fails with proper error trace
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Benefits**:
|
|
177
|
+
- Tests run and document exact failure
|
|
178
|
+
- Each test can have specific pending message
|
|
179
|
+
- Easy to identify when Firefox adds support (test will pass)
|
|
180
|
+
- Full error trace available for debugging
|
|
181
|
+
|
|
182
|
+
## Best Practices
|
|
183
|
+
|
|
184
|
+
### 1. Be Specific in Pending Messages
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
# Good
|
|
188
|
+
pending 'emulation.setScriptingEnabled not supported by Firefox yet'
|
|
189
|
+
|
|
190
|
+
# Bad
|
|
191
|
+
pending 'not supported'
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 2. Include BiDi Command Name
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
# Good
|
|
198
|
+
pending 'browsingContext.setViewport not implemented'
|
|
199
|
+
|
|
200
|
+
# Bad
|
|
201
|
+
pending 'viewport not working'
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 3. Document When to Re-check
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# Good
|
|
208
|
+
pending 'network.addIntercept requires Firefox 120+, current: 119'
|
|
209
|
+
|
|
210
|
+
# Bad
|
|
211
|
+
pending 'network interception broken'
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 4. Remove Pending When Fixed
|
|
215
|
+
|
|
216
|
+
When Firefox adds support, the test will fail with:
|
|
217
|
+
```
|
|
218
|
+
Expected example to fail but it passed
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
This is your signal to remove the `pending` line!
|
|
222
|
+
|
|
223
|
+
## Firefox BiDi Limitations (Current)
|
|
224
|
+
|
|
225
|
+
As of this implementation, the following BiDi commands are not supported by Firefox:
|
|
226
|
+
|
|
227
|
+
1. `emulation.setScriptingEnabled` - Control JavaScript execution
|
|
228
|
+
- Tests: `spec/integration/page_spec.rb` (2 tests)
|
|
229
|
+
- Tests: `spec/integration/click_spec.rb` (1 test)
|
|
230
|
+
|
|
231
|
+
## Files Changed
|
|
232
|
+
|
|
233
|
+
- `spec/integration/click_spec.rb`: Changed `skip` to `pending` (line 71)
|
|
234
|
+
- `spec/integration/page_spec.rb`: Moved `skip` from before block to individual tests as `pending` (lines 19, 48)
|
|
235
|
+
|
|
236
|
+
## Test Results
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
bundle exec rspec spec/integration/
|
|
240
|
+
# 108 examples, 0 failures, 4 pending
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
All pending tests show proper error traces documenting Firefox limitations.
|
|
244
|
+
|
|
245
|
+
## Key Takeaways
|
|
246
|
+
|
|
247
|
+
1. **Use `pending` for browser limitations** - Shows what's missing with error trace
|
|
248
|
+
2. **Use `skip` for unimplemented features** - Our code doesn't exist yet
|
|
249
|
+
3. **Be specific in messages** - Include BiDi command name and reason
|
|
250
|
+
4. **Pending in test body, not before block** - Each test should be explicit
|
|
251
|
+
5. **Pending tests run code** - They document exact failure mode
|
|
252
|
+
6. **Remove pending when fixed** - Test will fail with "expected to fail but passed"
|
|
253
|
+
|
|
254
|
+
## References
|
|
255
|
+
|
|
256
|
+
- [RSpec Documentation: Pending and Skipped Examples](https://rspec.info/features/3-12/rspec-core/pending-and-skipped-examples/)
|
|
257
|
+
- [WebDriver BiDi Spec](https://w3c.github.io/webdriver-bidi/) - Check which commands are standardized
|
|
258
|
+
- [Firefox BiDi Implementation Status](https://wiki.mozilla.org/WebDriver/RemoteProtocol/WebDriver_BiDi) - Check Firefox support
|
|
259
|
+
|
|
260
|
+
## Commit Reference
|
|
261
|
+
|
|
262
|
+
See commit: "test: Use pending instead of skip for Firefox unsupported features"
|