puppeteer-bidi 0.0.3.beta1 → 0.0.3.beta2

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.
@@ -1,247 +0,0 @@
1
- # Wrapped Element Click Implementation
2
-
3
- ## Problem
4
-
5
- When clicking on wrapped or multi-line text elements, using `getBoundingClientRect()` returns a single large bounding box that may have empty space in the center, causing clicks to miss the actual element.
6
-
7
- ## Example: Wrapped Link
8
-
9
- ```html
10
- <div style="width: 10ch; word-wrap: break-word;">
11
- <a href='#clicked'>123321</a>
12
- </div>
13
- ```
14
-
15
- The link text wraps into two lines:
16
- ```
17
- 123
18
- 321
19
- ```
20
-
21
- ### getBoundingClientRect() Problem
22
-
23
- Returns single large box:
24
- ```ruby
25
- {x: 628.45, y: 62.47, width: 109.1, height: 49.73}
26
- ```
27
-
28
- Click point (center): `(683, 87)` → **hits empty space between lines!**
29
-
30
- ### getClientRects() Solution
31
-
32
- Returns multiple boxes for wrapped text:
33
- ```ruby
34
- [
35
- {x: 708.58, y: 62.47, width: 32.73, height: 22.67}, # "123"
36
- {x: 628.45, y: 85.15, width: 32.73, height: 22.67} # "321"
37
- ]
38
- ```
39
-
40
- Click point (first box center): `(725, 73)` → **hits actual text!**
41
-
42
- ## Implementation
43
-
44
- ### clickable_box Method
45
-
46
- ```ruby
47
- def clickable_box
48
- assert_not_disposed
49
-
50
- # Get client rects - returns multiple boxes for wrapped elements
51
- boxes = evaluate(<<~JS)
52
- element => {
53
- if (!(element instanceof Element)) {
54
- return null;
55
- }
56
- return [...element.getClientRects()].map(rect => {
57
- return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
58
- });
59
- }
60
- JS
61
-
62
- return nil unless boxes&.is_a?(Array) && !boxes.empty?
63
-
64
- # Intersect boxes with frame boundaries
65
- intersect_bounding_boxes_with_frame(boxes)
66
-
67
- # Find first box with valid dimensions
68
- box = boxes.find { |rect| rect['width'] >= 1 && rect['height'] >= 1 }
69
- return nil unless box
70
-
71
- {
72
- x: box['x'],
73
- y: box['y'],
74
- width: box['width'],
75
- height: box['height']
76
- }
77
- end
78
- ```
79
-
80
- ### Viewport Clipping: intersectBoundingBoxesWithFrame
81
-
82
- Clips element boxes to visible viewport boundaries:
83
-
84
- ```ruby
85
- def intersect_bounding_boxes_with_frame(boxes)
86
- # Get document dimensions using element's evaluate
87
- dimensions = evaluate(<<~JS)
88
- element => {
89
- return {
90
- documentWidth: element.ownerDocument.documentElement.clientWidth,
91
- documentHeight: element.ownerDocument.documentElement.clientHeight
92
- };
93
- }
94
- JS
95
-
96
- document_width = dimensions['documentWidth']
97
- document_height = dimensions['documentHeight']
98
-
99
- boxes.each do |box|
100
- intersect_bounding_box(box, document_width, document_height)
101
- end
102
- end
103
-
104
- def intersect_bounding_box(box, width, height)
105
- # Clip width
106
- box['width'] = [
107
- box['x'] >= 0 ?
108
- [width - box['x'], box['width']].min :
109
- [width, box['width'] + box['x']].min,
110
- 0
111
- ].max
112
-
113
- # Clip height
114
- box['height'] = [
115
- box['y'] >= 0 ?
116
- [height - box['y'], box['height']].min :
117
- [height, box['height'] + box['y']].min,
118
- 0
119
- ].max
120
-
121
- # Ensure non-negative coordinates
122
- box['x'] = [box['x'], 0].max
123
- box['y'] = [box['y'], 0].max
124
- end
125
- ```
126
-
127
- ## Why This Matters
128
-
129
- ### Use Cases for getClientRects()
130
-
131
- 1. **Wrapped text**: Multi-line links, buttons with text wrapping
132
- 2. **Inline elements**: `<span>` elements that span multiple lines
133
- 3. **Complex layouts**: Elements with transforms, rotations
134
-
135
- ### Algorithm Flow
136
-
137
- 1. **Get all client rects** for element (array of boxes)
138
- 2. **Clip to viewport** using intersectBoundingBox algorithm
139
- 3. **Find first valid box** (width >= 1 && height >= 1)
140
- 4. **Click center of that box**
141
-
142
- ## Puppeteer Reference
143
-
144
- Based on [ElementHandle.ts#clickableBox](https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/api/ElementHandle.ts):
145
-
146
- ```typescript
147
- async #clickableBox(): Promise<BoundingBox | null> {
148
- const boxes = await this.evaluate(element => {
149
- if (!(element instanceof Element)) {
150
- return null;
151
- }
152
- return [...element.getClientRects()].map(rect => {
153
- return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
154
- });
155
- });
156
-
157
- if (!boxes?.length) {
158
- return null;
159
- }
160
-
161
- await this.#intersectBoundingBoxesWithFrame(boxes);
162
-
163
- // ... parent frame handling ...
164
-
165
- const box = boxes.find(box => {
166
- return box.width >= 1 && box.height >= 1;
167
- });
168
-
169
- return box || null;
170
- }
171
- ```
172
-
173
- ## Testing
174
-
175
- ### Test Asset
176
-
177
- Official Puppeteer test asset: `spec/assets/wrappedlink.html`
178
-
179
- ```html
180
- <div style="width: 10ch; word-wrap: break-word; transform: rotate(33deg);">
181
- <a href='#clicked'>123321</a>
182
- </div>
183
- ```
184
-
185
- **Critical**: Always use official test assets without modification!
186
-
187
- ### Test Case
188
-
189
- ```ruby
190
- it 'should click wrapped links' do
191
- with_test_state do |page:, server:, **|
192
- page.goto("#{server.prefix}/wrappedlink.html")
193
- page.click('a')
194
- result = page.evaluate('window.__clicked')
195
- expect(result).to be true
196
- end
197
- end
198
- ```
199
-
200
- ## Debugging Protocol Messages
201
-
202
- Compare BiDi protocol messages with Puppeteer to verify coordinates:
203
-
204
- ```bash
205
- # Ruby implementation
206
- DEBUG_BIDI_COMMAND=1 bundle exec rspec spec/integration/click_spec.rb:138
207
-
208
- # Look for input.performActions with click coordinates
209
- # Verify they fall within actual element bounds
210
- ```
211
-
212
- ## Key Takeaways
213
-
214
- 1. **getClientRects() > getBoundingClientRect()** for clickable elements
215
- 2. **First valid box** is the click target (not center of bounding box)
216
- 3. **Viewport clipping** ensures clicks stay within visible area
217
- 4. **Test with official assets** - simplified versions hide edge cases
218
- 5. **Follow Puppeteer exactly** - algorithm has been battle-tested
219
-
220
- ## Performance
221
-
222
- - `getClientRects()` is fast (native browser API)
223
- - Intersection algorithm is O(n) where n = number of boxes (typically 1-3)
224
- - No additional round-trips to browser
225
-
226
- ## Future: Parent Frame Support
227
-
228
- For iframe support, add coordinate transformation:
229
-
230
- ```ruby
231
- # TODO: Handle parent frames
232
- frame = self.frame
233
- while (parent_frame = frame.parent_frame)
234
- # Adjust coordinates for parent frame offset
235
- # boxes.each { |box| box['x'] += offset_x; box['y'] += offset_y }
236
- end
237
- ```
238
-
239
- ## Files
240
-
241
- - `lib/puppeteer/bidi/element_handle.rb`: clickable_box, intersect methods
242
- - `spec/integration/click_spec.rb`: Test case for wrapped links
243
- - `spec/assets/wrappedlink.html`: Official test asset (never modify!)
244
-
245
- ## Commit Reference
246
-
247
- See commit: "Implement clickable_box with getClientRects() and viewport clipping"
data/CLAUDE.md DELETED
@@ -1,238 +0,0 @@
1
- # Puppeteer-BiDi Development Guide
2
-
3
- ## Project Overview
4
-
5
- Port the WebDriver BiDi protocol portions of Puppeteer to Ruby, providing a standards-based tool for Firefox automation.
6
-
7
- ### Development Principles
8
-
9
- - **BiDi-only**: Do not port CDP protocol-related code
10
- - **Standards compliance**: Adhere to W3C WebDriver BiDi specification
11
- - **Firefox optimization**: Maximize BiDi protocol capabilities
12
- - **Ruby conventions**: Design Ruby-idiomatic interfaces
13
-
14
- ## Quick Reference
15
-
16
- ### Running Tests
17
-
18
- ```bash
19
- # All integration tests (requires Firefox Nightly for full functionality)
20
- bundle exec rspec spec/integration/
21
-
22
- # Single test file
23
- bundle exec rspec spec/integration/click_spec.rb
24
-
25
- # Non-headless mode
26
- HEADLESS=false bundle exec rspec spec/integration/
27
-
28
- # Debug protocol messages
29
- DEBUG_BIDI_COMMAND=1 bundle exec rspec spec/integration/click_spec.rb
30
-
31
- # Use specific Firefox path (e.g., Nightly)
32
- FIREFOX_PATH="/Applications/Firefox Nightly.app/Contents/MacOS/firefox" bundle exec rspec
33
- ```
34
-
35
- **Note**: Some features (e.g., FileChooser) require Firefox Nightly. The browser launcher prioritizes Nightly automatically.
36
-
37
- ### Key Architecture
38
-
39
- ```
40
- Upper Layer (Puppeteer::Bidi) - User-facing synchronous API
41
- └── Page, Frame, ElementHandle, JSHandle
42
-
43
- Core Layer (Puppeteer::Bidi::Core) - Async operations, returns Async::Task
44
- └── Session, BrowsingContext, Realm
45
- ```
46
-
47
- **Critical**: Upper layer methods must call `.wait` on all Core layer method calls. See [Two-Layer Architecture](CLAUDE/two_layer_architecture.md).
48
-
49
- ### Async Programming
50
-
51
- This project uses **Async (Fiber-based)**, NOT concurrent-ruby (Thread-based).
52
-
53
- - No Mutex needed (cooperative multitasking)
54
- - Similar to JavaScript async/await
55
- - See [Async Programming Guide](CLAUDE/async_programming.md)
56
-
57
- ## Development Workflow
58
-
59
- 1. Study Puppeteer's TypeScript implementation first
60
- 2. Understand BiDi protocol calls
61
- 3. Implement with proper deserialization
62
- 4. Port tests from Puppeteer
63
- 5. **Update `API_COVERAGE.md`** - Mark implemented methods as ✅ and update coverage count
64
- 6. See [Porting Puppeteer Guide](CLAUDE/porting_puppeteer.md)
65
-
66
- ## Coding Conventions
67
-
68
- ### Ruby
69
-
70
- - Use Ruby 3.0+ features
71
- - Follow RuboCop guidelines
72
- - Class names: `PascalCase`, Methods: `snake_case`, Constants: `SCREAMING_SNAKE_CASE`
73
-
74
- ### Type Annotations (rbs-inline)
75
-
76
- Use [rbs-inline](https://github.com/soutaro/rbs-inline) for type annotations in Ruby source files.
77
-
78
- **Setup:**
79
- - Add `# rbs_inline: enabled` magic comment at the top of the file
80
- - Use Doc style syntax for type annotations
81
-
82
- **Example:**
83
- ```ruby
84
- # frozen_string_literal: true
85
- # rbs_inline: enabled
86
-
87
- class Example
88
- attr_reader :name #: String
89
-
90
- # @rbs name: String -- The name to set
91
- # @rbs return: void
92
- def initialize(name)
93
- @name = name
94
- end
95
-
96
- # Query for an element matching the selector
97
- # @rbs selector: String -- CSS selector to query
98
- # @rbs return: ElementHandle? -- Matching element or nil
99
- def query_selector(selector)
100
- # ...
101
- end
102
- end
103
- ```
104
-
105
- **Conventions:**
106
- - Use `A?` for nullable types (not `A | nil`)
107
- - Use `A | B | nil` for union types with nil
108
- - **Always include descriptions** with `--` separator: `# @rbs name: String -- the name`
109
- - Public methods should have type annotations
110
- - **Do NOT use `@rbs!` blocks** - RubyMine IDE doesn't recognize them
111
- - **Use direct union types** instead of type aliases: `BrowserTarget | PageTarget | FrameTarget` not `target`
112
- - **Do NOT use `**options` in public APIs** - RubyMine shows as `untyped`. Use explicit keyword arguments:
113
- - Optional params: `param: nil`
114
- - Boolean params with default: `headless: true` or `enabled: false`
115
- - Internal/Core layer methods may still use `**options` for flexibility
116
-
117
- **Generate RBS files:**
118
- ```bash
119
- bundle exec rake rbs # Generates sig/**/*.rbs
120
- ```
121
-
122
- **Note:** `sig/` directory is gitignored. RBS files are generated automatically during `rake build`.
123
-
124
- ### Type Checking with Steep
125
-
126
- Run type checking locally:
127
- ```bash
128
- bundle exec rake rbs # Generate RBS files first
129
- bundle exec steep check # Run type checker
130
- ```
131
-
132
- **Steepfile Configuration:**
133
- - Currently configured with lenient `:hint` level diagnostics for gradual typing
134
- - As type coverage improves, change to `:warning` or `:error` in `Steepfile`
135
-
136
- **Special RBS Files (manually maintained, NOT gitignored):**
137
-
138
- 1. **`sig/_external.rbs`** - External dependency stubs
139
- - Types for async gem (`Async`, `Kernel#Async`, `Kernel#Sync`, `Async::Task`, etc.)
140
- - Standard library extensions (`Dir.mktmpdir`, `Time.parse`)
141
- - Third-party types (`Singleton`, `Protocol::WebSocket`)
142
-
143
- 2. **`sig/_supplementary.rbs`** - Types rbs-inline cannot generate
144
- - `extend self` pattern: Add `extend ModuleName` declaration
145
- - Singleton pattern: Add `extend Singleton::SingletonClassMethods`
146
-
147
- **Common Issues:**
148
-
149
- 1. **Type alias must be lowercase**: In RBS, `type target = ...` not `type Target = ...`
150
-
151
- 2. **`extend self` not recognized**: Add to `sig/_supplementary.rbs`:
152
- ```rbs
153
- module Foo
154
- extend Foo # Makes instance methods callable as singleton methods
155
- end
156
- ```
157
-
158
- 3. **Singleton pattern**: Add to `sig/_supplementary.rbs`:
159
- ```rbs
160
- class Bar
161
- extend Singleton::SingletonClassMethods
162
- end
163
- ```
164
-
165
- 4. **Missing external types**: Add stubs to `sig/_external.rbs`
166
-
167
- ### Testing
168
-
169
- - Use RSpec for unit and integration tests
170
- - Integration tests in `spec/integration/`
171
- - Use `with_test_state` helper for browser reuse
172
-
173
- ### Test Assets
174
-
175
- **CRITICAL**: Always use Puppeteer's official test assets without modification.
176
-
177
- - Source: https://github.com/puppeteer/puppeteer/tree/main/test/assets
178
- - Never modify files in `spec/assets/`
179
- - Revert any experimental changes before PRs
180
-
181
- ## Detailed Documentation
182
-
183
- See the [CLAUDE/](CLAUDE/) directory for detailed implementation guides:
184
-
185
- ### Architecture & Patterns
186
-
187
- - **[Two-Layer Architecture](CLAUDE/two_layer_architecture.md)** - Core vs Upper layer, async patterns
188
- - **[Async Programming](CLAUDE/async_programming.md)** - Fiber-based concurrency with socketry/async
189
- - **[ReactorRunner](CLAUDE/reactor_runner.md)** - Using browser outside Sync blocks (at_exit, etc.)
190
- - **[Porting Puppeteer](CLAUDE/porting_puppeteer.md)** - Best practices for implementing features
191
- - **[Core Layer Gotchas](CLAUDE/core_layer_gotchas.md)** - EventEmitter/Disposable pitfalls, @disposed conflicts
192
-
193
- ### Implementation Details
194
-
195
- - **[QueryHandler](CLAUDE/query_handler.md)** - CSS, XPath, text selector handling
196
- - **[JavaScript Evaluation](CLAUDE/javascript_evaluation.md)** - `evaluate()` and `evaluate_handle()`
197
- - **[JSHandle and ElementHandle](CLAUDE/jshandle_implementation.md)** - Object handle management
198
- - **[Selector Evaluation](CLAUDE/selector_evaluation.md)** - `eval_on_selector` methods
199
- - **[Click Implementation](CLAUDE/click_implementation.md)** - Mouse input and clicking
200
- - **[Mouse Implementation](CLAUDE/mouse_implementation.md)** - wheel, reset, hover, BoundingBox/Point data classes
201
- - **[Wrapped Element Click](CLAUDE/wrapped_element_click.md)** - getClientRects() for multi-line elements
202
- - **[Navigation Waiting](CLAUDE/navigation_waiting.md)** - waitForNavigation patterns
203
- - **[Frame Architecture](CLAUDE/frame_architecture.md)** - Parent-based frame hierarchy
204
- - **[FileChooser](CLAUDE/file_chooser.md)** - File upload and dialog handling (requires Firefox Nightly)
205
- - **[ExposeFunction](CLAUDE/expose_function_implementation.md)** - `exposeFunction` and `evaluateOnNewDocument` using BiDi script.message
206
- - **[Error Handling](CLAUDE/error_handling.md)** - Custom exception types
207
-
208
- ### Testing
209
-
210
- - **[Testing Strategy](CLAUDE/testing_strategy.md)** - Test organization and optimization
211
- - **[RSpec pending vs skip](CLAUDE/rspec_pending_vs_skip.md)** - Documenting limitations
212
- - **[Test Server Routes](CLAUDE/test_server_routes.md)** - Dynamic route handling
213
-
214
- ## Releasing
215
-
216
- To release a new version:
217
-
218
- 1. Update the version number in `lib/puppeteer/bidi/version.rb`
219
- 2. Commit the change and push to main
220
- 3. Create and push a version tag:
221
- ```bash
222
- git tag 1.2.3
223
- git push origin 1.2.3
224
- ```
225
-
226
- GitHub Actions will automatically build and publish the gem to RubyGems. Supported tag formats:
227
- - `1.2.3` - stable release
228
- - `1.2.3.alpha1` - alpha release
229
- - `1.2.3.beta2` - beta release
230
-
231
- **Note:** `RUBYGEMS_API_KEY` must be configured in repository secrets.
232
-
233
- ## References
234
-
235
- - [WebDriver BiDi Specification](https://w3c.github.io/webdriver-bidi/)
236
- - [Puppeteer Documentation](https://pptr.dev/)
237
- - [Puppeteer Source Code](https://github.com/puppeteer/puppeteer)
238
- - [puppeteer-ruby](https://github.com/YusukeIwaki/puppeteer-ruby) - CDP implementation reference