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,180 @@
1
+ # Two-Layer Async Architecture
2
+
3
+ This codebase implements a two-layer architecture to separate async complexity from user-facing APIs.
4
+
5
+ ## Architecture Overview
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────┐
9
+ │ Upper Layer (Puppeteer::Bidi) │
10
+ │ - User-facing, synchronous API │
11
+ │ - Calls .wait internally on Core layer methods │
12
+ │ - Examples: Page, Frame, JSHandle, ElementHandle │
13
+ ├─────────────────────────────────────────────────────────┤
14
+ │ Core Layer (Puppeteer::Bidi::Core) │
15
+ │ - Returns Async::Task for all async operations │
16
+ │ - Uses async_send_command internally │
17
+ │ - Examples: Session, BrowsingContext, Realm │
18
+ └─────────────────────────────────────────────────────────┘
19
+ ```
20
+
21
+ ## Design Principles
22
+
23
+ 1. **Core Layer (Puppeteer::Bidi::Core)**:
24
+ - All methods that communicate with BiDi protocol return `Async::Task`
25
+ - Uses `session.async_send_command` (not `send_command`)
26
+ - Methods are explicitly async and composable
27
+ - Examples: `BrowsingContext#navigate`, `Realm#call_function`
28
+
29
+ 2. **Upper Layer (Puppeteer::Bidi)**:
30
+ - All methods call `.wait` on Core layer async operations
31
+ - Provides synchronous, blocking API for users
32
+ - Users never see `Async::Task` directly
33
+ - Examples: `Page#goto`, `Frame#evaluate`, `JSHandle#get_property`
34
+
35
+ ## Implementation Patterns
36
+
37
+ ### Core Layer Pattern
38
+
39
+ ```ruby
40
+ # lib/puppeteer/bidi/core/browsing_context.rb
41
+ def navigate(url, wait: nil)
42
+ Async do
43
+ raise BrowsingContextClosedError, @reason if closed?
44
+ params = { context: @id, url: url }
45
+ params[:wait] = wait if wait
46
+ result = session.async_send_command('browsingContext.navigate', params).wait
47
+ result
48
+ end
49
+ end
50
+
51
+ def perform_actions(actions)
52
+ raise BrowsingContextClosedError, @reason if closed?
53
+ session.async_send_command('input.performActions', {
54
+ context: @id,
55
+ actions: actions
56
+ })
57
+ end
58
+ ```
59
+
60
+ **Key points:**
61
+ - Returns `Async::Task` (implicitly from `Async do` block or explicitly from `async_send_command`)
62
+ - Users of Core layer must call `.wait` to get results
63
+
64
+ ### Upper Layer Pattern
65
+
66
+ ```ruby
67
+ # lib/puppeteer/bidi/frame.rb
68
+ def goto(url, wait_until: 'load', timeout: 30000)
69
+ response = wait_for_navigation(timeout: timeout, wait_until: wait_until) do
70
+ @browsing_context.navigate(url, wait: 'interactive').wait # .wait call
71
+ end
72
+ HTTPResponse.new(url: @browsing_context.url, status: 200)
73
+ end
74
+
75
+ # lib/puppeteer/bidi/keyboard.rb
76
+ def perform_actions(action_list)
77
+ @browsing_context.perform_actions([
78
+ {
79
+ type: 'key',
80
+ id: 'default keyboard',
81
+ actions: action_list
82
+ }
83
+ ]).wait # .wait call
84
+ end
85
+ ```
86
+
87
+ **Key points:**
88
+ - Always calls `.wait` on Core layer methods
89
+ - Returns plain Ruby objects (String, Hash, etc.), not Async::Task
90
+ - User-facing API is synchronous
91
+
92
+ ## Common Mistakes and How to Fix Them
93
+
94
+ ### Mistake 1: Forgetting .wait on Core Layer Methods
95
+
96
+ ```ruby
97
+ # WRONG: Missing .wait
98
+ def query_selector(selector)
99
+ result = @realm.call_function(...)
100
+ if result['type'] == 'exception' # Error: undefined method '[]' for Async::Task
101
+ # ...
102
+ end
103
+ end
104
+
105
+ # CORRECT: Add .wait
106
+ def query_selector(selector)
107
+ result = @realm.call_function(...).wait # Add .wait here
108
+ if result['type'] == 'exception'
109
+ # ...
110
+ end
111
+ end
112
+ ```
113
+
114
+ ### Mistake 2: Using send_command Instead of async_send_command in Core Layer
115
+
116
+ ```ruby
117
+ # WRONG: Using send_command (doesn't exist)
118
+ def perform_actions(actions)
119
+ session.send_command('input.performActions', {...}) # Error: undefined method
120
+ end
121
+
122
+ # CORRECT: Use async_send_command
123
+ def perform_actions(actions)
124
+ session.async_send_command('input.performActions', {...})
125
+ end
126
+ ```
127
+
128
+ ### Mistake 3: Not Calling .wait on All Core Methods
129
+
130
+ ```ruby
131
+ # WRONG: Multiple Core calls, only one .wait
132
+ def get_properties
133
+ result = @realm.call_function(...).wait # OK
134
+ props_result = @realm.call_function(...) # Missing .wait!
135
+ if props_result['type'] == 'exception' # Error
136
+ # ...
137
+ end
138
+ end
139
+
140
+ # CORRECT: Add .wait to all Core calls
141
+ def get_properties
142
+ result = @realm.call_function(...).wait
143
+ props_result = @realm.call_function(...).wait # Add .wait
144
+ if props_result['type'] == 'exception'
145
+ # ...
146
+ end
147
+ end
148
+ ```
149
+
150
+ ## Checklist for Adding New Methods
151
+
152
+ When adding new methods to the Upper Layer:
153
+
154
+ 1. Identify all calls to Core layer methods
155
+ 2. Add `.wait` to each Core layer method call
156
+ 3. Verify the method returns plain Ruby objects, not Async::Task
157
+ 4. Test with integration specs
158
+
159
+ When adding new methods to the Core Layer:
160
+
161
+ 1. Use `session.async_send_command` (not `send_command`)
162
+ 2. Wrap in `Async do ... end` if needed
163
+ 3. Return `Async::Task` (don't call .wait)
164
+ 4. Document that callers must call .wait
165
+
166
+ ## Files Modified for Async Architecture
167
+
168
+ **Core Layer:**
169
+ - `lib/puppeteer/bidi/core/session.rb` - Changed `send_command` → `async_send_command`
170
+ - `lib/puppeteer/bidi/core/browsing_context.rb` - All methods use `async_send_command`
171
+ - `lib/puppeteer/bidi/core/realm.rb` - Methods return `Async::Task`
172
+
173
+ **Upper Layer:**
174
+ - `lib/puppeteer/bidi/realm.rb` - Added `.wait` to `execute_with_core`, `call_function`
175
+ - `lib/puppeteer/bidi/frame.rb` - Added `.wait` to `goto`
176
+ - `lib/puppeteer/bidi/js_handle.rb` - Added `.wait` to `dispose`, `get_property`, `get_properties`, `as_element`
177
+ - `lib/puppeteer/bidi/element_handle.rb` - Added `.wait` to `query_selector_all`, `eval_on_selector_all`
178
+ - `lib/puppeteer/bidi/keyboard.rb` - Added `.wait` to `perform_actions`
179
+ - `lib/puppeteer/bidi/mouse.rb` - Added `.wait` to `perform_actions`
180
+ - `lib/puppeteer/bidi/page.rb` - Added `.wait` to `capture_screenshot`, `close`, `set_viewport`, `set_javascript_enabled`
@@ -0,0 +1,247 @@
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 ADDED
@@ -0,0 +1,185 @@
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. See [Porting Puppeteer Guide](CLAUDE/porting_puppeteer.md)
64
+
65
+ ## Coding Conventions
66
+
67
+ ### Ruby
68
+
69
+ - Use Ruby 3.0+ features
70
+ - Follow RuboCop guidelines
71
+ - Class names: `PascalCase`, Methods: `snake_case`, Constants: `SCREAMING_SNAKE_CASE`
72
+
73
+ ### Type Annotations (rbs-inline)
74
+
75
+ Use [rbs-inline](https://github.com/soutaro/rbs-inline) for type annotations in Ruby source files.
76
+
77
+ **Setup:**
78
+ - Add `# rbs_inline: enabled` magic comment at the top of the file
79
+ - Use Doc style syntax for type annotations
80
+
81
+ **Example:**
82
+ ```ruby
83
+ # frozen_string_literal: true
84
+ # rbs_inline: enabled
85
+
86
+ class Example
87
+ attr_reader :name #: String
88
+
89
+ # @rbs name: String
90
+ # @rbs return: void
91
+ def initialize(name)
92
+ @name = name
93
+ end
94
+
95
+ # @rbs selector: String
96
+ # @rbs return: ElementHandle?
97
+ def query_selector(selector)
98
+ # ...
99
+ end
100
+ end
101
+ ```
102
+
103
+ **Conventions:**
104
+ - Use `A?` for nullable types (not `A | nil`)
105
+ - Use `A | B | nil` for union types with nil
106
+ - Add space around `--` for inline comments: `# @rbs name: String -- the name`
107
+ - Public methods should have type annotations
108
+
109
+ **Generate RBS files:**
110
+ ```bash
111
+ bundle exec rake rbs # Generates sig/**/*.rbs
112
+ ```
113
+
114
+ **Note:** `sig/` directory is gitignored. RBS files are generated automatically during `rake build`.
115
+
116
+ ### Testing
117
+
118
+ - Use RSpec for unit and integration tests
119
+ - Integration tests in `spec/integration/`
120
+ - Use `with_test_state` helper for browser reuse
121
+
122
+ ### Test Assets
123
+
124
+ **CRITICAL**: Always use Puppeteer's official test assets without modification.
125
+
126
+ - Source: https://github.com/puppeteer/puppeteer/tree/main/test/assets
127
+ - Never modify files in `spec/assets/`
128
+ - Revert any experimental changes before PRs
129
+
130
+ ## Detailed Documentation
131
+
132
+ See the [CLAUDE/](CLAUDE/) directory for detailed implementation guides:
133
+
134
+ ### Architecture & Patterns
135
+
136
+ - **[Two-Layer Architecture](CLAUDE/two_layer_architecture.md)** - Core vs Upper layer, async patterns
137
+ - **[Async Programming](CLAUDE/async_programming.md)** - Fiber-based concurrency with socketry/async
138
+ - **[Porting Puppeteer](CLAUDE/porting_puppeteer.md)** - Best practices for implementing features
139
+ - **[Core Layer Gotchas](CLAUDE/core_layer_gotchas.md)** - EventEmitter/Disposable pitfalls, @disposed conflicts
140
+
141
+ ### Implementation Details
142
+
143
+ - **[QueryHandler](CLAUDE/query_handler.md)** - CSS, XPath, text selector handling
144
+ - **[JavaScript Evaluation](CLAUDE/javascript_evaluation.md)** - `evaluate()` and `evaluate_handle()`
145
+ - **[JSHandle and ElementHandle](CLAUDE/jshandle_implementation.md)** - Object handle management
146
+ - **[Selector Evaluation](CLAUDE/selector_evaluation.md)** - `eval_on_selector` methods
147
+ - **[Click Implementation](CLAUDE/click_implementation.md)** - Mouse input and clicking
148
+ - **[Mouse Implementation](CLAUDE/mouse_implementation.md)** - wheel, reset, hover, BoundingBox/Point data classes
149
+ - **[Wrapped Element Click](CLAUDE/wrapped_element_click.md)** - getClientRects() for multi-line elements
150
+ - **[Navigation Waiting](CLAUDE/navigation_waiting.md)** - waitForNavigation patterns
151
+ - **[Frame Architecture](CLAUDE/frame_architecture.md)** - Parent-based frame hierarchy
152
+ - **[FileChooser](CLAUDE/file_chooser.md)** - File upload and dialog handling (requires Firefox Nightly)
153
+ - **[Error Handling](CLAUDE/error_handling.md)** - Custom exception types
154
+
155
+ ### Testing
156
+
157
+ - **[Testing Strategy](CLAUDE/testing_strategy.md)** - Test organization and optimization
158
+ - **[RSpec pending vs skip](CLAUDE/rspec_pending_vs_skip.md)** - Documenting limitations
159
+ - **[Test Server Routes](CLAUDE/test_server_routes.md)** - Dynamic route handling
160
+
161
+ ## Releasing
162
+
163
+ To release a new version:
164
+
165
+ 1. Update the version number in `lib/puppeteer/bidi/version.rb`
166
+ 2. Commit the change and push to main
167
+ 3. Create and push a version tag:
168
+ ```bash
169
+ git tag 1.2.3
170
+ git push origin 1.2.3
171
+ ```
172
+
173
+ GitHub Actions will automatically build and publish the gem to RubyGems. Supported tag formats:
174
+ - `1.2.3` - stable release
175
+ - `1.2.3.alpha1` - alpha release
176
+ - `1.2.3.beta2` - beta release
177
+
178
+ **Note:** `RUBYGEMS_API_KEY` must be configured in repository secrets.
179
+
180
+ ## References
181
+
182
+ - [WebDriver BiDi Specification](https://w3c.github.io/webdriver-bidi/)
183
+ - [Puppeteer Documentation](https://pptr.dev/)
184
+ - [Puppeteer Source Code](https://github.com/puppeteer/puppeteer)
185
+ - [puppeteer-ruby](https://github.com/YusukeIwaki/puppeteer-ruby) - CDP implementation reference
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 YusukeIwaki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.