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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +13 -0
  4. data/CLAUDE/README.md +158 -0
  5. data/CLAUDE/async_programming.md +158 -0
  6. data/CLAUDE/click_implementation.md +340 -0
  7. data/CLAUDE/core_layer_gotchas.md +136 -0
  8. data/CLAUDE/error_handling.md +232 -0
  9. data/CLAUDE/file_chooser.md +95 -0
  10. data/CLAUDE/frame_architecture.md +346 -0
  11. data/CLAUDE/javascript_evaluation.md +341 -0
  12. data/CLAUDE/jshandle_implementation.md +505 -0
  13. data/CLAUDE/keyboard_implementation.md +250 -0
  14. data/CLAUDE/mouse_implementation.md +140 -0
  15. data/CLAUDE/navigation_waiting.md +234 -0
  16. data/CLAUDE/porting_puppeteer.md +214 -0
  17. data/CLAUDE/query_handler.md +194 -0
  18. data/CLAUDE/rspec_pending_vs_skip.md +262 -0
  19. data/CLAUDE/selector_evaluation.md +198 -0
  20. data/CLAUDE/test_server_routes.md +263 -0
  21. data/CLAUDE/testing_strategy.md +236 -0
  22. data/CLAUDE/two_layer_architecture.md +180 -0
  23. data/CLAUDE/wrapped_element_click.md +247 -0
  24. data/CLAUDE.md +185 -0
  25. data/LICENSE.txt +21 -0
  26. data/README.md +488 -0
  27. data/Rakefile +21 -0
  28. data/lib/puppeteer/bidi/async_utils.rb +151 -0
  29. data/lib/puppeteer/bidi/browser.rb +285 -0
  30. data/lib/puppeteer/bidi/browser_context.rb +53 -0
  31. data/lib/puppeteer/bidi/browser_launcher.rb +240 -0
  32. data/lib/puppeteer/bidi/connection.rb +182 -0
  33. data/lib/puppeteer/bidi/core/README.md +169 -0
  34. data/lib/puppeteer/bidi/core/browser.rb +230 -0
  35. data/lib/puppeteer/bidi/core/browsing_context.rb +601 -0
  36. data/lib/puppeteer/bidi/core/disposable.rb +69 -0
  37. data/lib/puppeteer/bidi/core/errors.rb +64 -0
  38. data/lib/puppeteer/bidi/core/event_emitter.rb +83 -0
  39. data/lib/puppeteer/bidi/core/navigation.rb +128 -0
  40. data/lib/puppeteer/bidi/core/realm.rb +315 -0
  41. data/lib/puppeteer/bidi/core/request.rb +300 -0
  42. data/lib/puppeteer/bidi/core/session.rb +153 -0
  43. data/lib/puppeteer/bidi/core/user_context.rb +208 -0
  44. data/lib/puppeteer/bidi/core/user_prompt.rb +102 -0
  45. data/lib/puppeteer/bidi/core.rb +45 -0
  46. data/lib/puppeteer/bidi/deserializer.rb +132 -0
  47. data/lib/puppeteer/bidi/element_handle.rb +602 -0
  48. data/lib/puppeteer/bidi/errors.rb +42 -0
  49. data/lib/puppeteer/bidi/file_chooser.rb +52 -0
  50. data/lib/puppeteer/bidi/frame.rb +597 -0
  51. data/lib/puppeteer/bidi/http_response.rb +23 -0
  52. data/lib/puppeteer/bidi/injected.js +1 -0
  53. data/lib/puppeteer/bidi/injected_source.rb +21 -0
  54. data/lib/puppeteer/bidi/js_handle.rb +302 -0
  55. data/lib/puppeteer/bidi/keyboard.rb +265 -0
  56. data/lib/puppeteer/bidi/lazy_arg.rb +23 -0
  57. data/lib/puppeteer/bidi/mouse.rb +170 -0
  58. data/lib/puppeteer/bidi/page.rb +613 -0
  59. data/lib/puppeteer/bidi/query_handler.rb +397 -0
  60. data/lib/puppeteer/bidi/realm.rb +242 -0
  61. data/lib/puppeteer/bidi/serializer.rb +139 -0
  62. data/lib/puppeteer/bidi/target.rb +81 -0
  63. data/lib/puppeteer/bidi/task_manager.rb +44 -0
  64. data/lib/puppeteer/bidi/timeout_settings.rb +20 -0
  65. data/lib/puppeteer/bidi/transport.rb +129 -0
  66. data/lib/puppeteer/bidi/version.rb +7 -0
  67. data/lib/puppeteer/bidi/wait_task.rb +322 -0
  68. data/lib/puppeteer/bidi.rb +49 -0
  69. data/scripts/update_injected_source.rb +57 -0
  70. data/sig/puppeteer/bidi/browser.rbs +80 -0
  71. data/sig/puppeteer/bidi/element_handle.rbs +238 -0
  72. data/sig/puppeteer/bidi/frame.rbs +205 -0
  73. data/sig/puppeteer/bidi/js_handle.rbs +90 -0
  74. data/sig/puppeteer/bidi/page.rbs +247 -0
  75. data/sig/puppeteer/bidi.rbs +15 -0
  76. 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"