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
data/README.md ADDED
@@ -0,0 +1,488 @@
1
+ # Puppeteer::BiDi
2
+
3
+ A Ruby port of [Puppeteer](https://pptr.dev/) using the WebDriver BiDi protocol for Firefox automation.
4
+
5
+ ## Overview
6
+
7
+ `puppeteer-bidi` is a Ruby implementation of Puppeteer that leverages the [WebDriver BiDi protocol](https://w3c.github.io/webdriver-bidi/) to automate Firefox browsers. Unlike the existing [puppeteer-ruby](https://github.com/YusukeIwaki/puppeteer-ruby) gem which uses the Chrome DevTools Protocol (CDP), this gem focuses specifically on BiDi protocol support for cross-browser automation.
8
+
9
+ ### Why BiDi?
10
+
11
+ - **Cross-browser compatibility**: BiDi is a W3C standard protocol designed to work across different browsers
12
+ - **Firefox-first**: While CDP is Chrome-centric, BiDi provides better support for Firefox automation
13
+ - **Future-proof**: BiDi represents the future direction of browser automation standards
14
+
15
+ ### Relationship with puppeteer-ruby
16
+
17
+ This gem complements the existing `puppeteer-ruby` ecosystem:
18
+
19
+ - **puppeteer-ruby**: Uses CDP (Chrome DevTools Protocol) → Best for Chrome/Chromium automation
20
+ - **puppeteer-bidi** (this gem): Uses BiDi protocol → Best for Firefox automation
21
+
22
+ This gem ports only the BiDi-related portions of Puppeteer to Ruby, intentionally excluding CDP implementations.
23
+
24
+ ## Installation
25
+
26
+ Install the gem and add to the application's Gemfile by executing:
27
+
28
+ ```bash
29
+ bundle add puppeteer-bidi
30
+ ```
31
+
32
+ If bundler is not being used to manage dependencies, install the gem by executing:
33
+
34
+ ```bash
35
+ gem install puppeteer-bidi
36
+ ```
37
+
38
+ Or add this line to your application's Gemfile:
39
+
40
+ ```ruby
41
+ gem 'puppeteer-bidi'
42
+ ```
43
+
44
+ ## Prerequisites
45
+
46
+ - Ruby 3.2 or higher (required by socketry/async dependency)
47
+ - Firefox browser with BiDi support
48
+
49
+ ## Usage
50
+
51
+ ### High-Level Page API (Recommended)
52
+
53
+ ```ruby
54
+ require 'puppeteer/bidi'
55
+
56
+ # Launch Firefox with BiDi protocol
57
+ browser = Puppeteer::Bidi.launch(headless: false)
58
+
59
+ # Create a new page
60
+ page = browser.new_page
61
+
62
+ # Set viewport size
63
+ page.set_viewport(width: 1280, height: 720)
64
+
65
+ # Navigate to a URL
66
+ page.goto('https://example.com')
67
+
68
+ # Take a screenshot
69
+ page.screenshot(path: 'screenshot.png')
70
+
71
+ # Take a full page screenshot
72
+ page.screenshot(path: 'fullpage.png', full_page: true)
73
+
74
+ # Screenshot with clipping
75
+ page.screenshot(
76
+ path: 'clip.png',
77
+ clip: { x: 0, y: 0, width: 100, height: 100 }
78
+ )
79
+
80
+ # Evaluate JavaScript expressions
81
+ title = page.evaluate('document.title')
82
+ puts "Page title: #{title}"
83
+
84
+ # Evaluate JavaScript functions with arguments
85
+ sum = page.evaluate('(a, b) => a + b', 3, 4)
86
+ puts "Sum: #{sum}" # => 7
87
+
88
+ # Access frame and evaluate
89
+ frame = page.main_frame
90
+ result = frame.evaluate('() => window.innerWidth')
91
+
92
+ # Query selectors
93
+ section = page.query_selector('section')
94
+ divs = page.query_selector_all('div')
95
+
96
+ # Evaluate on selectors (convenience methods)
97
+ # Equivalent to Puppeteer's $eval and $$eval
98
+ id = page.eval_on_selector('section', 'e => e.id')
99
+ count = page.eval_on_selector_all('div', 'divs => divs.length')
100
+
101
+ # Set page content
102
+ page.set_content('<h1>Hello, World!</h1>')
103
+
104
+ # Wait for navigation (Async/Fiber-based, no race conditions)
105
+ # Block pattern - executes code and waits for resulting navigation
106
+ response = page.wait_for_navigation do
107
+ page.click('a#navigation-link')
108
+ end
109
+
110
+ # Wait for fragment navigation (#hash changes)
111
+ page.wait_for_navigation do
112
+ page.click('a[href="#section"]')
113
+ end # => nil (fragment navigation returns nil)
114
+
115
+ # Wait for History API navigation
116
+ page.wait_for_navigation do
117
+ page.evaluate('history.pushState({}, "", "/new-url")')
118
+ end # => nil (History API returns nil)
119
+
120
+ # Wait with different conditions
121
+ page.wait_for_navigation(wait_until: 'domcontentloaded') do
122
+ page.click('a')
123
+ end
124
+
125
+ # User input simulation
126
+ page.click('button#submit')
127
+ page.type('input[name="email"]', 'user@example.com', delay: 100)
128
+ page.focus('textarea')
129
+
130
+ # Close the page
131
+ page.close
132
+
133
+ # Close the browser
134
+ browser.close
135
+ ```
136
+
137
+ ### Low-Level BiDi API
138
+
139
+ ```ruby
140
+ require 'puppeteer/bidi'
141
+
142
+ # Launch Firefox with BiDi protocol
143
+ browser = Puppeteer::Bidi.launch(headless: false)
144
+
145
+ # Create a new browsing context (tab)
146
+ result = browser.new_context(type: 'tab')
147
+ context_id = result['context']
148
+
149
+ # Navigate to a URL
150
+ browser.navigate(
151
+ context: context_id,
152
+ url: 'https://example.com',
153
+ wait: 'complete'
154
+ )
155
+
156
+ # Close the browsing context
157
+ browser.close_context(context_id)
158
+
159
+ # Close the browser
160
+ browser.close
161
+ ```
162
+
163
+ ### Launch Options
164
+
165
+ ```ruby
166
+ browser = Puppeteer::Bidi.launch(
167
+ headless: true, # Run in headless mode (default: true)
168
+ executable_path: '/path/to/firefox', # Path to Firefox executable (optional)
169
+ user_data_dir: '/path/to/profile', # User data directory (optional)
170
+ args: ['--width=1280', '--height=720'] # Additional Firefox arguments
171
+ )
172
+ ```
173
+
174
+ ### Event Handling
175
+
176
+ ```ruby
177
+ require 'puppeteer/bidi'
178
+
179
+ browser = Puppeteer::Bidi.launch(headless: false)
180
+
181
+ # Subscribe to BiDi events
182
+ browser.subscribe([
183
+ 'browsingContext.navigationStarted',
184
+ 'browsingContext.navigationComplete'
185
+ ])
186
+
187
+ # Register event handlers
188
+ browser.on('browsingContext.navigationStarted') do |params|
189
+ puts "Navigation started: #{params['url']}"
190
+ end
191
+
192
+ browser.on('browsingContext.navigationComplete') do |params|
193
+ puts "Navigation completed: #{params['url']}"
194
+ end
195
+
196
+ # Create context and navigate
197
+ result = browser.new_context(type: 'tab')
198
+ browser.navigate(context: result['context'], url: 'https://example.com')
199
+ ```
200
+
201
+ ### Connecting to Existing Browser
202
+
203
+ ```ruby
204
+ # Connect to an already running Firefox instance with BiDi
205
+ browser = Puppeteer::Bidi.connect('ws://localhost:9222/session')
206
+
207
+ # Use the browser
208
+ status = browser.status
209
+ puts "Connected to browser: #{status.inspect}"
210
+
211
+ browser.close
212
+ ```
213
+
214
+ ### Using Core Layer (Advanced)
215
+
216
+ The Core layer provides a structured API over BiDi protocol:
217
+
218
+ ```ruby
219
+ require 'puppeteer/bidi'
220
+
221
+ # Launch browser and access connection
222
+ browser = Puppeteer::Bidi.launch(headless: false)
223
+
224
+ # Create Core layer objects
225
+ session_info = { 'sessionId' => 'default-session', 'capabilities' => {} }
226
+ session = Puppeteer::Bidi::Core::Session.new(browser.connection, session_info)
227
+ core_browser = Puppeteer::Bidi::Core::Browser.from(session)
228
+ session.browser = core_browser
229
+
230
+ # Get default user context
231
+ context = core_browser.default_user_context
232
+
233
+ # Create browsing context with Core API
234
+ browsing_context = context.create_browsing_context('tab')
235
+
236
+ # Subscribe to events
237
+ browsing_context.on(:load) do
238
+ puts "Page loaded!"
239
+ end
240
+
241
+ browsing_context.subscribe(['browsingContext.load'])
242
+
243
+ # Navigate
244
+ browsing_context.navigate('https://example.com', wait: 'complete')
245
+
246
+ # Evaluate JavaScript
247
+ result = browsing_context.default_realm.evaluate('document.title', true)
248
+ puts "Title: #{result['value']}"
249
+
250
+ # Take screenshot
251
+ image_data = browsing_context.capture_screenshot(format: 'png')
252
+
253
+ # Error handling with custom exceptions
254
+ begin
255
+ browsing_context.navigate('https://example.com')
256
+ rescue Puppeteer::Bidi::Core::BrowsingContextClosedError => e
257
+ puts "Context was closed: #{e.reason}"
258
+ end
259
+
260
+ # Clean up
261
+ browsing_context.close
262
+ core_browser.close
263
+ ```
264
+
265
+ For more details on the Core layer, see `lib/puppeteer/bidi/core/README.md`.
266
+
267
+ For more examples, see the [examples](examples/) directory and integration tests in [spec/integration/](spec/integration/).
268
+
269
+ ## Testing
270
+
271
+ ### Running Tests
272
+
273
+ ```bash
274
+ # Run all tests
275
+ bundle exec rspec
276
+
277
+ # Run integration tests (launches actual Firefox browser)
278
+ bundle exec rspec spec/integration/
279
+
280
+ # Run evaluation tests (23 examples, ~7 seconds)
281
+ bundle exec rspec spec/integration/evaluation_spec.rb
282
+
283
+ # Run screenshot tests (12 examples, ~8 seconds)
284
+ bundle exec rspec spec/integration/screenshot_spec.rb
285
+
286
+ # Run all integration tests (35 examples, ~10 seconds)
287
+ bundle exec rspec spec/integration/evaluation_spec.rb spec/integration/screenshot_spec.rb
288
+
289
+ # Run in non-headless mode for debugging
290
+ HEADLESS=false bundle exec rspec spec/integration/
291
+ ```
292
+
293
+ ### Integration Tests
294
+
295
+ Integration tests in `spec/integration/` demonstrate real-world usage by launching Firefox and performing browser automation tasks. These tests are useful for:
296
+
297
+ - Verifying end-to-end functionality
298
+ - Learning by example
299
+ - Ensuring browser compatibility
300
+
301
+ **Performance Note**: Integration tests are optimized to reuse a single browser instance across all tests (~19x faster than launching per test). Each test gets a fresh page (tab) for proper isolation.
302
+
303
+ #### Test Coverage
304
+
305
+ **Integration Tests**: 136 examples covering end-to-end functionality
306
+
307
+ - **Evaluation Tests** (`evaluation_spec.rb`): 23 tests ported from Puppeteer
308
+ - JavaScript expression and function evaluation
309
+ - Argument serialization (numbers, arrays, objects, special values)
310
+ - Result deserialization (NaN, Infinity, -0, Maps)
311
+ - Exception handling and thrown values
312
+ - IIFE (Immediately Invoked Function Expression) support
313
+ - Frame.evaluate functionality
314
+
315
+ - **Screenshot Tests** (`screenshot_spec.rb`): 12 tests ported from Puppeteer
316
+ - Basic screenshots and clipping regions
317
+ - Full page screenshots with viewport management
318
+ - Parallel execution across single/multiple pages
319
+ - Retina display compatibility
320
+ - Viewport restoration
321
+ - All tests use golden image comparison with tolerance for cross-platform compatibility
322
+
323
+ - **Navigation Tests** (`navigation_spec.rb`): 8 tests ported from Puppeteer
324
+ - Full page navigation with HTTPResponse
325
+ - Fragment navigation (#hash changes)
326
+ - History API navigation (pushState, replaceState, back, forward)
327
+ - Multiple wait conditions (load, domcontentloaded)
328
+ - Async/Fiber-based concurrent navigation waiting
329
+
330
+ - **Click Tests** (`click_spec.rb`): 20 tests ported from Puppeteer
331
+ - Element clicking (buttons, links, SVG, checkboxes)
332
+ - Scrolling and viewport handling
333
+ - Wrapped element clicks (multi-line text)
334
+ - Obscured element detection
335
+ - Rotated element clicks
336
+ - Mouse button variations (left, right, middle)
337
+ - Click counts (single, double, triple)
338
+
339
+ - **Keyboard Tests** (`keyboard_spec.rb`): Tests for keyboard input simulation
340
+ - Text typing with customizable delay
341
+ - Special key presses
342
+ - Key combinations
343
+
344
+ ## Project Status
345
+
346
+ This project is in early development. The API may change as the implementation progresses.
347
+
348
+ ### Implemented Features
349
+
350
+ #### High-Level Page API (`lib/puppeteer/bidi/`)
351
+ Puppeteer-compatible API for browser automation:
352
+
353
+ - ✅ **Browser**: Browser instance management
354
+ - ✅ **BrowserContext**: Isolated browsing sessions
355
+ - ✅ **Page**: High-level page automation
356
+ - ✅ Navigation (`goto`, `set_content`, `wait_for_navigation`)
357
+ - ✅ Wait for full page navigation, fragment navigation (#hash), and History API (pushState/replaceState)
358
+ - ✅ Async/Fiber-based concurrency (no race conditions, Thread-safe)
359
+ - ✅ Multiple wait conditions (`load`, `domcontentloaded`)
360
+ - ✅ JavaScript evaluation (`evaluate`, `evaluate_handle`) with functions, arguments, and IIFE support
361
+ - ✅ Element querying (`query_selector`, `query_selector_all`)
362
+ - ✅ Selector evaluation (`eval_on_selector`, `eval_on_selector_all`) - Ruby equivalents of Puppeteer's `$eval` and `$$eval`
363
+ - ✅ User input (`click`, `type`, `focus`)
364
+ - ✅ Mouse operations (click with offset, double-click, context menu, middle-click)
365
+ - ✅ Keyboard operations (type with delay, press, key combinations)
366
+ - ✅ Screenshots (basic, clipping, full page, parallel)
367
+ - ✅ Viewport management with automatic restoration
368
+ - ✅ Page state queries (`title`, `url`, `viewport`)
369
+ - ✅ Frame access (`main_frame`, `focused_frame`)
370
+ - ✅ **Frame**: Frame-level operations
371
+ - ✅ JavaScript evaluation with full feature parity to Page
372
+ - ✅ Element querying and selector evaluation
373
+ - ✅ Navigation waiting (`wait_for_navigation`)
374
+ - ✅ User input (`click`, `type`, `focus`)
375
+ - ✅ **JSHandle & ElementHandle**: JavaScript object references
376
+ - ✅ Handle creation, disposal, and property access
377
+ - ✅ Element operations (click, bounding box, scroll into view)
378
+ - ✅ Type-safe custom exceptions for error handling
379
+ - ✅ **Mouse & Keyboard**: User input simulation
380
+ - ✅ Mouse clicks (single, double, triple) with customizable delay
381
+ - ✅ Mouse movements and button states
382
+ - ✅ Keyboard typing with per-character delay
383
+ - ✅ Special key support
384
+
385
+ #### Screenshot Features
386
+ Comprehensive screenshot functionality with 12 passing tests:
387
+
388
+ - ✅ Basic screenshots
389
+ - ✅ Clipping regions (document/viewport coordinates)
390
+ - ✅ Full page screenshots (with/without viewport expansion)
391
+ - ✅ Thread-safe parallel execution (single/multiple pages)
392
+ - ✅ Retina display compatibility (odd-sized clips)
393
+ - ✅ Automatic viewport restoration
394
+ - ✅ Base64 encoding
395
+
396
+ #### Foundation Layer
397
+ - ✅ Browser launching with Firefox
398
+ - ✅ BiDi protocol connection (WebSocket-based)
399
+ - ✅ WebSocket transport with async/await support
400
+ - ✅ Command execution with timeout
401
+ - ✅ Event subscription and handling
402
+
403
+ #### Core Layer (`lib/puppeteer/bidi/core/`)
404
+ A low-level object-oriented abstraction over the WebDriver BiDi protocol:
405
+
406
+ - ✅ **Infrastructure**: EventEmitter, Disposable, Custom Exceptions
407
+ - ✅ **Session Management**: BiDi session lifecycle
408
+ - ✅ **Browser & Contexts**: Browser, UserContext, BrowsingContext
409
+ - ✅ **Navigation**: Navigation lifecycle tracking
410
+ - ✅ **JavaScript Execution**: Realm classes (Window, Worker)
411
+ - ✅ **Network**: Request/Response management with interception
412
+ - ✅ **User Interaction**: UserPrompt handling (alert/confirm/prompt)
413
+
414
+ #### BiDi Operations
415
+ - ✅ Browsing context management (create/close tabs/windows)
416
+ - ✅ Page navigation with wait conditions
417
+ - ✅ JavaScript evaluation (`script.evaluate`, `script.callFunction`)
418
+ - ✅ Expression evaluation
419
+ - ✅ Function calls with argument serialization
420
+ - ✅ Result deserialization (numbers, strings, arrays, objects, Maps, special values)
421
+ - ✅ Exception handling and propagation
422
+ - ✅ IIFE support
423
+ - ✅ Screenshot capture
424
+ - ✅ PDF generation
425
+ - ✅ Cookie management (get/set/delete)
426
+ - ✅ Network request interception
427
+ - ✅ Geolocation and timezone emulation
428
+
429
+ ### Custom Exception Handling
430
+
431
+ The gem provides type-safe custom exceptions for better error handling:
432
+
433
+ ```ruby
434
+ begin
435
+ page.eval_on_selector('.missing', 'e => e.id')
436
+ rescue Puppeteer::Bidi::SelectorNotFoundError => e
437
+ puts "Selector '#{e.selector}' not found"
438
+ rescue Puppeteer::Bidi::JSHandleDisposedError
439
+ puts "Handle was disposed"
440
+ rescue Puppeteer::Bidi::PageClosedError
441
+ puts "Page is closed"
442
+ rescue Puppeteer::Bidi::FrameDetachedError
443
+ puts "Frame was detached"
444
+ end
445
+ ```
446
+
447
+ Available custom exceptions:
448
+ - `JSHandleDisposedError` - JSHandle or ElementHandle is disposed
449
+ - `PageClosedError` - Page is closed
450
+ - `FrameDetachedError` - Frame is detached
451
+ - `SelectorNotFoundError` - Selector doesn't match any elements (includes `selector` attribute)
452
+
453
+ ### Planned Features
454
+
455
+ - File upload handling
456
+ - Enhanced network monitoring (NetworkManager)
457
+ - Frame management (FrameManager with iframe support)
458
+ - Service Worker support
459
+ - Dialog handling (alert, confirm, prompt)
460
+ - Advanced navigation options (referrer, AbortSignal support)
461
+
462
+ ## Comparison with Puppeteer (Node.js)
463
+
464
+ This gem aims to provide a Ruby-friendly API that closely mirrors the original Puppeteer API while following Ruby conventions and idioms.
465
+
466
+ ## Development
467
+
468
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
469
+
470
+ To install this gem onto your local machine, run `bundle exec rake install`.
471
+
472
+ ### Type Annotations
473
+
474
+ This gem includes RBS type definitions generated by [rbs-inline](https://github.com/soutaro/rbs-inline).
475
+
476
+ ## Contributing
477
+
478
+ Bug reports and pull requests are welcome on GitHub at https://github.com/YusukeIwaki/puppeteer-bidi.
479
+
480
+ 1. Fork the repository
481
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
482
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
483
+ 4. Push to the branch (`git push origin my-new-feature`)
484
+ 5. Create a new Pull Request
485
+
486
+ ## License
487
+
488
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
13
+
14
+ # Generate RBS files from rbs-inline annotations
15
+ desc "Generate RBS files with rbs-inline"
16
+ task :rbs do
17
+ sh "bundle", "exec", "rbs-inline", "--output=sig", "lib"
18
+ end
19
+
20
+ # Run rbs-inline before building the gem
21
+ Rake::Task[:build].enhance([:rbs])
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+ require 'async/promise'
5
+ require 'async/barrier'
6
+
7
+ module Puppeteer
8
+ module Bidi
9
+ # Utility methods for working with Async tasks
10
+ # Provides Promise.all and Promise.race equivalents using Async::Barrier
11
+ module AsyncUtils
12
+ extend self
13
+
14
+ def await(task)
15
+ if task.is_a?(Proc)
16
+ task.call
17
+ elsif task.respond_to?(:wait)
18
+ task.wait
19
+ else
20
+ task
21
+ end
22
+ end
23
+
24
+ # Execute a task with a timeout using Async::Task#with_timeout
25
+ # @param timeout_ms [Numeric] Timeout duration in milliseconds
26
+ # @param task [Proc, Async::Promise, nil] Task to execute; falls back to block
27
+ # @yield [async_task] Execute a task within the timeout, optionally receiving Async::Task
28
+ # @return [Async::Task] Async task that resolves/rejects once the operation completes
29
+ def async_timeout(timeout_ms, task = nil, &block)
30
+ timeout_seconds = timeout_ms / 1000.0
31
+
32
+ if task
33
+ Async do |async_task|
34
+ async_task.with_timeout(timeout_seconds) do
35
+ if task.is_a?(Proc)
36
+ args = task.arity.positive? ? [async_task] : []
37
+ task.call(*args)
38
+ else
39
+ await(task)
40
+ end
41
+ end
42
+ end
43
+ elsif block
44
+ Async do |async_task|
45
+ async_task.with_timeout(timeout_seconds) do
46
+ args = block.arity.positive? ? [async_task] : []
47
+ await(block.call(*args))
48
+ end
49
+ end
50
+ else
51
+ raise ArgumentError, 'AsyncUtils.async_timeout requires a task or block'
52
+ end
53
+ end
54
+
55
+ def promise_all(*tasks)
56
+ Async { zip(*tasks) }
57
+ end
58
+
59
+ # Wait for all async tasks to complete and return results
60
+ # Similar to Promise.all in JavaScript
61
+ # @param tasks [Array<Proc, Async::Promise>] Array of procs or promises
62
+ # @return [Array] Array of results in the same order as the input tasks
63
+ # @raise If any task raises an exception, it will be propagated
64
+ # @example With procs
65
+ # results = AsyncUtils.await_promise_all(
66
+ # -> { sleep 0.1; "first" },
67
+ # -> { sleep 0.2; "second" },
68
+ # -> { sleep 0.05; "third" }
69
+ # )
70
+ # # => ["first", "second", "third"]
71
+ # @example With promises
72
+ # promise1 = Async::Promise.new
73
+ # promise2 = Async::Promise.new
74
+ # Thread.new { sleep 0.1; promise1.resolve("first") }
75
+ # Thread.new { sleep 0.2; promise2.resolve("second") }
76
+ # results = AsyncUtils.await_promise_all(promise1, promise2)
77
+ # # => ["first", "second"]
78
+ def await_promise_all(*tasks)
79
+ Sync { zip(*tasks) }
80
+ end
81
+
82
+
83
+ def promise_race(*tasks)
84
+ Async { first(*tasks) }
85
+ end
86
+
87
+ # Race multiple async tasks and return the result of the first one to complete
88
+ # Similar to Promise.race in JavaScript
89
+ # @param tasks [Array<Proc, Async::Promise>] Array of procs or promises
90
+ # @return The result of the first task to complete
91
+ # @example With procs
92
+ # result = AsyncUtils.await_promise_race(
93
+ # -> { sleep 1; "slow" },
94
+ # -> { sleep 0.1; "fast" }
95
+ # )
96
+ # # => "fast"
97
+ # @example With promises
98
+ # promise1 = Async::Promise.new
99
+ # promise2 = Async::Promise.new
100
+ # Thread.new { sleep 0.3; promise1.resolve("slow") }
101
+ # Thread.new { sleep 0.1; promise2.resolve("fast") }
102
+ # result = AsyncUtils.await_promise_race(promise1, promise2)
103
+ # # => "fast"
104
+ def await_promise_race(*tasks)
105
+ Sync { first(*tasks) }
106
+ end
107
+
108
+ private
109
+
110
+ def zip(*tasks)
111
+ barrier = Async::Barrier.new
112
+ results = Array.new(tasks.size)
113
+
114
+ tasks.each_with_index do |task, index|
115
+ barrier.async do
116
+ results[index] = await(task)
117
+ end
118
+ end
119
+
120
+ # Wait for all tasks to complete
121
+ barrier.wait
122
+
123
+ results
124
+ end
125
+
126
+ def first(*tasks)
127
+ barrier = Async::Barrier.new
128
+ result = nil
129
+
130
+ begin
131
+ tasks.each do |task|
132
+ barrier.async do
133
+ await(task)
134
+ end
135
+ end
136
+
137
+ # Wait for the first task to complete
138
+ barrier.wait do |completed_task|
139
+ result = completed_task.wait
140
+ break # Stop waiting after the first task completes
141
+ end
142
+
143
+ result
144
+ ensure
145
+ # Cancel all remaining tasks
146
+ barrier.stop
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end