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
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
data/.rubocop.yml
ADDED
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
|
+
|