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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8a2905e58b0b31a75a0b286fa14986def369fe62d122dd62dc6432a763070ea9
4
+ data.tar.gz: b6cb21ba581490c0cbe28ae4515498660091ec9653df2c6d8c1e4b9d5f34197e
5
+ SHA512:
6
+ metadata.gz: 68c9e714dd633886188df24ccedc71139ac4ed8f5525e470c99b0c802dcdc5ce261279fea56b2c60dd6456a245b056acf987d9cd43cd6283c33f3bca2f568794
7
+ data.tar.gz: beb1f011607f387f10329db6078a91d41eb5582631256f742655af39d7ea15638aa3d29c3e7c44a1fce4072585c141898147c783be65b1cf20deda1a73e25d4f
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,13 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 120
data/CLAUDE/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # Detailed Implementation Documentation
2
+
3
+ This directory contains detailed documentation for specific implementation topics in puppeteer-bidi.
4
+
5
+ ## Quick Reference
6
+
7
+ | Document | Topic | Key Takeaway |
8
+ |----------|-------|--------------|
9
+ | [async_programming.md](async_programming.md) | Fiber-based async | Use Async, NOT concurrent-ruby |
10
+ | [two_layer_architecture.md](two_layer_architecture.md) | Core vs Upper layer | Always call `.wait` on Core methods |
11
+ | [porting_puppeteer.md](porting_puppeteer.md) | Implementing features | Study TypeScript first, port tests |
12
+ | [query_handler.md](query_handler.md) | Selector handling | Override script methods, use PuppeteerUtil |
13
+ | [javascript_evaluation.md](javascript_evaluation.md) | JS evaluation | IIFE detection is critical |
14
+ | [jshandle_implementation.md](jshandle_implementation.md) | Handle management | resultOwnership must be 'root' |
15
+ | [selector_evaluation.md](selector_evaluation.md) | Selector methods | Use `eval_on_selector` not `$eval` |
16
+ | [error_handling.md](error_handling.md) | Custom exceptions | Type-safe error handling |
17
+ | [click_implementation.md](click_implementation.md) | Click & mouse | session.subscribe is mandatory |
18
+ | [wrapped_element_click.md](wrapped_element_click.md) | Wrapped elements | Use getClientRects() |
19
+ | [navigation_waiting.md](navigation_waiting.md) | waitForNavigation | Event-driven with Async::Promise |
20
+ | [testing_strategy.md](testing_strategy.md) | Test optimization | Browser reuse = 19x faster |
21
+ | [frame_architecture.md](frame_architecture.md) | Frame hierarchy | `(parent, browsing_context)` |
22
+ | [rspec_pending_vs_skip.md](rspec_pending_vs_skip.md) | Test documentation | Use `pending` for Firefox |
23
+ | [test_server_routes.md](test_server_routes.md) | Dynamic routes | `server.set_route` for tests |
24
+
25
+ ## Architecture & Patterns
26
+
27
+ ### [Async Programming](async_programming.md)
28
+
29
+ Guide to Fiber-based async programming with socketry/async.
30
+
31
+ **Key concepts:**
32
+ - Use Async (Fiber-based), NOT concurrent-ruby (Thread-based)
33
+ - No Mutex needed - cooperative multitasking
34
+ - WebSocket messages must use `Async do` for non-blocking processing
35
+
36
+ ### [Two-Layer Architecture](two_layer_architecture.md)
37
+
38
+ Core vs Upper layer separation for async complexity management.
39
+
40
+ **Key concepts:**
41
+ - Core layer returns `Async::Task`
42
+ - Upper layer calls `.wait` on all Core methods
43
+ - User-facing API is synchronous
44
+
45
+ ### [Porting Puppeteer](porting_puppeteer.md)
46
+
47
+ Best practices for implementing Puppeteer features in Ruby.
48
+
49
+ **Key concepts:**
50
+ - Study TypeScript implementation first
51
+ - Port corresponding test cases
52
+ - Use official test assets without modification
53
+
54
+ ## Implementation Details
55
+
56
+ ### [QueryHandler](query_handler.md)
57
+
58
+ Extensible selector handling for CSS, XPath, text selectors.
59
+
60
+ **Key concepts:**
61
+ - Override `query_one_script` and `query_all_script`
62
+ - TextQueryHandler uses PuppeteerUtil directly (closure dependency)
63
+ - Handle adoption pattern for navigation
64
+
65
+ ### [JavaScript Evaluation](javascript_evaluation.md)
66
+
67
+ Implementation of `evaluate()` and `evaluate_handle()`.
68
+
69
+ **Key concepts:**
70
+ - IIFE must be detected and treated as expressions
71
+ - Always deserialize BiDi results before returning
72
+
73
+ ### [JSHandle and ElementHandle](jshandle_implementation.md)
74
+
75
+ Handle management and BiDi protocol parameters.
76
+
77
+ **Key concepts:**
78
+ - Set `resultOwnership: 'root'` to get handles
79
+ - Handle parameters must be arrays
80
+
81
+ ### [Selector Evaluation](selector_evaluation.md)
82
+
83
+ Implementation of `eval_on_selector` methods.
84
+
85
+ **Key concepts:**
86
+ - Delegation pattern: Page → Frame → ElementHandle
87
+ - Always dispose handles in ensure blocks
88
+
89
+ ### [Click Implementation](click_implementation.md)
90
+
91
+ Mouse input and click functionality.
92
+
93
+ **Key concepts:**
94
+ - Must call `session.subscribe` for events
95
+ - URL updates happen via events
96
+
97
+ ### [Wrapped Element Click](wrapped_element_click.md)
98
+
99
+ Clicking wrapped/multi-line text elements.
100
+
101
+ **Key concepts:**
102
+ - Use getClientRects() for wrapped elements
103
+ - Viewport clipping ensures clicks stay visible
104
+
105
+ ### [Navigation Waiting](navigation_waiting.md)
106
+
107
+ waitForNavigation patterns.
108
+
109
+ **Key concepts:**
110
+ - Event-driven with Async::Promise
111
+ - Attach to existing navigations before block execution
112
+
113
+ ### [Frame Architecture](frame_architecture.md)
114
+
115
+ Parent-based frame hierarchy.
116
+
117
+ **Key concepts:**
118
+ - First parameter is `parent` (Page or Frame)
119
+ - Enables future iframe support
120
+
121
+ ### [Error Handling](error_handling.md)
122
+
123
+ Custom exception types.
124
+
125
+ **Key concepts:**
126
+ - Use custom exceptions for type-safe rescue
127
+ - Include contextual data in exceptions
128
+
129
+ ## Testing
130
+
131
+ ### [Testing Strategy](testing_strategy.md)
132
+
133
+ Test organization and optimization.
134
+
135
+ **Key concepts:**
136
+ - Reuse browser across tests for 19x speedup
137
+ - Use official Puppeteer test assets
138
+
139
+ ### [RSpec: pending vs skip](rspec_pending_vs_skip.md)
140
+
141
+ Documenting browser limitations.
142
+
143
+ **Key concepts:**
144
+ - Use `pending` for Firefox BiDi limitations
145
+ - Use `skip` for unimplemented features
146
+
147
+ ### [Test Server Routes](test_server_routes.md)
148
+
149
+ Dynamic route handling for tests.
150
+
151
+ **Key concepts:**
152
+ - `server.set_route(path)` for intercepting requests
153
+ - `server.wait_for_request(path)` for synchronization
154
+
155
+ ## Related Documentation
156
+
157
+ - **Main guide**: [CLAUDE.md](../CLAUDE.md) - High-level development guide
158
+ - **Core layer**: [lib/puppeteer/bidi/core/README.md](../lib/puppeteer/bidi/core/README.md) - Core BiDi abstraction layer
@@ -0,0 +1,158 @@
1
+ # Async Programming with socketry/async
2
+
3
+ This project uses the [socketry/async](https://github.com/socketry/async) library for asynchronous operations.
4
+
5
+ ## Why Async Instead of concurrent-ruby?
6
+
7
+ **IMPORTANT**: This project uses `Async` (Fiber-based), **NOT** `concurrent-ruby` (Thread-based).
8
+
9
+ | Feature | Async (Fiber-based) | concurrent-ruby (Thread-based) |
10
+ | --------------------- | ------------------------------------------------------ | -------------------------------------- |
11
+ | **Concurrency Model** | Cooperative multitasking (like JavaScript async/await) | Preemptive multitasking |
12
+ | **Race Conditions** | Not possible within a Fiber | Requires Mutex, locks, etc. |
13
+ | **Synchronization** | Not needed (cooperative) | Required (Mutex, Semaphore) |
14
+ | **Mental Model** | Similar to JavaScript async/await | Traditional thread programming |
15
+ | **Bug Risk** | Lower (no race conditions) | Higher (race conditions, deadlocks) |
16
+
17
+ **Key advantages:**
18
+
19
+ - **No race conditions**: Fibers yield control cooperatively, so no concurrent access to shared state
20
+ - **No Mutex needed**: Since there are no race conditions, no synchronization primitives required
21
+ - **Similar to JavaScript**: If you understand `async/await` in JavaScript, you understand Async in Ruby
22
+ - **Easier to reason about**: Code executes sequentially within a Fiber until it explicitly yields
23
+
24
+ **Example:**
25
+
26
+ ```ruby
27
+ # DON'T: Use concurrent-ruby (Thread-based, requires Mutex)
28
+ require 'concurrent'
29
+ @pending = Concurrent::Map.new # Thread-safe map
30
+ promise = Concurrent::Promises.resolvable_future
31
+ promise.fulfill(value)
32
+
33
+ # DO: Use Async (Fiber-based, no synchronization needed)
34
+ require 'async/promise'
35
+ @pending = {} # Plain Hash is safe with Fibers
36
+ promise = Async::Promise.new
37
+ promise.resolve(value)
38
+ ```
39
+
40
+ ## Best Practices
41
+
42
+ 1. **Use `Sync` at top level**: When running async code at the top level of a thread or application, use `Sync { }` instead of `Async { }`
43
+
44
+ ```ruby
45
+ Thread.new do
46
+ Sync do
47
+ # async operations here
48
+ end
49
+ end
50
+ ```
51
+
52
+ 2. **Reactor lifecycle**: The reactor is automatically managed by `Sync { }`. No need to create explicit `Async::Reactor` instances in application code.
53
+
54
+ 3. **Background operations**: For long-running background tasks (like WebSocket connections), wrap `Sync { }` in a Thread:
55
+
56
+ ```ruby
57
+ connection_task = Thread.new do
58
+ Sync do
59
+ transport.connect # Async operation that blocks until connection closes
60
+ end
61
+ end
62
+ ```
63
+
64
+ 4. **Promise usage**: Use `Async::Promise` for async coordination:
65
+
66
+ ```ruby
67
+ promise = Async::Promise.new
68
+
69
+ # Resolve the promise
70
+ promise.resolve(value)
71
+
72
+ # Wait for the promise with timeout
73
+ Async do |task|
74
+ task.with_timeout(5) do
75
+ result = promise.wait
76
+ end
77
+ end.wait
78
+ ```
79
+
80
+ 5. **No Mutex needed**: Since Async is Fiber-based, you don't need Mutex for shared state within the same event loop
81
+
82
+ ## AsyncUtils: Promise.all and Promise.race
83
+
84
+ The `lib/puppeteer/bidi/async_utils.rb` module provides JavaScript-like Promise utilities:
85
+
86
+ ```ruby
87
+ # Promise.all - Wait for all tasks to complete
88
+ results = AsyncUtils.promise_all(
89
+ -> { sleep 0.1; 'first' },
90
+ -> { sleep 0.2; 'second' },
91
+ -> { sleep 0.05; 'third' }
92
+ )
93
+ # => ['first', 'second', 'third'] (in order, runs in parallel)
94
+
95
+ # Promise.race - Return the first to complete
96
+ result = AsyncUtils.promise_race(
97
+ -> { sleep 0.3; 'slow' },
98
+ -> { sleep 0.1; 'fast' },
99
+ -> { sleep 0.2; 'medium' }
100
+ )
101
+ # => 'fast' (cancels remaining tasks)
102
+ ```
103
+
104
+ **When to use AsyncUtils:**
105
+
106
+ - **Parallel task execution**: Running multiple independent async operations
107
+ - **Racing timeouts**: First of multiple operations to complete
108
+ - **NOT for event-driven waiting**: Use `Async::Promise` directly for event listeners
109
+
110
+ ## WebSocket Message Handling Pattern
111
+
112
+ **CRITICAL**: BiDi message handling must use `Async do` to process messages asynchronously:
113
+
114
+ ```ruby
115
+ # lib/puppeteer/bidi/transport.rb
116
+ while (message = connection.read)
117
+ next if message.nil?
118
+
119
+ # DO: Use Async do for non-blocking message processing
120
+ Async do
121
+ data = JSON.parse(message)
122
+ debug_print_receive(data)
123
+ @on_message&.call(data)
124
+ rescue StandardError => e
125
+ # Handle errors
126
+ end
127
+ end
128
+ ```
129
+
130
+ **Why this matters:**
131
+
132
+ - **Without `Async do`**: Message processing blocks the message loop, preventing other messages from being read
133
+ - **With `Async do`**: Each message is processed in a separate fiber, allowing concurrent message handling
134
+ - **Prevents deadlocks**: When multiple operations are waiting for responses, they can all be processed concurrently
135
+
136
+ **Example of the problem this solves:**
137
+
138
+ ```ruby
139
+ # Without Async do:
140
+ # 1. Message A arrives and starts processing
141
+ # 2. Processing A calls wait_for_navigation which waits for Message B
142
+ # 3. Message B arrives but can't be read because Message A is still being processed
143
+ # 4. DEADLOCK
144
+
145
+ # With Async do:
146
+ # 1. Message A arrives and starts processing in Fiber 1
147
+ # 2. Fiber 1 yields when calling wait (cooperative multitasking)
148
+ # 3. Message B can now be read and processed in Fiber 2
149
+ # 4. Both messages complete successfully
150
+ ```
151
+
152
+ This pattern is essential for the BiDi protocol's bidirectional communication model.
153
+
154
+ ## References
155
+
156
+ - [Async Best Practices](https://socketry.github.io/async/guides/best-practices/)
157
+ - [Async Documentation](https://socketry.github.io/async/)
158
+ - [Async::Barrier Guide](https://socketry.github.io/async/guides/tasks/index.html)
@@ -0,0 +1,340 @@
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
+