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,597 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Puppeteer
5
+ module Bidi
6
+ # Frame represents a frame (main frame or iframe) in the page
7
+ # This is a high-level wrapper around Core::BrowsingContext
8
+ # Following Puppeteer's BidiFrame implementation
9
+ class Frame
10
+ attr_reader :browsing_context #: Core::BrowsingContext
11
+
12
+ # Factory method following Puppeteer's BidiFrame.from pattern
13
+ # @rbs parent: Page | Frame
14
+ # @rbs browsing_context: Core::BrowsingContext
15
+ # @rbs return: Frame
16
+ def self.from(parent, browsing_context)
17
+ frame = new(parent, browsing_context)
18
+ frame.send(:initialize_frame)
19
+ frame
20
+ end
21
+
22
+ # @rbs parent: Page | Frame
23
+ # @rbs browsing_context: Core::BrowsingContext
24
+ # @rbs return: void
25
+ def initialize(parent, browsing_context)
26
+ @parent = parent
27
+ @browsing_context = browsing_context
28
+ @frames = {} # Map of browsing context id to Frame (like WeakMap in JS)
29
+
30
+ default_core_realm = @browsing_context.default_realm
31
+ internal_core_realm = @browsing_context.create_window_realm("__puppeteer_internal_#{rand(1..10_000)}")
32
+
33
+ @main_realm = FrameRealm.new(self, default_core_realm)
34
+ @isolated_realm = FrameRealm.new(self, internal_core_realm)
35
+ end
36
+
37
+ # @rbs return: FrameRealm
38
+ def main_realm
39
+ @main_realm
40
+ end
41
+
42
+ # @rbs return: FrameRealm
43
+ def isolated_realm
44
+ @isolated_realm
45
+ end
46
+
47
+ # Backwards compatibility for call sites that previously accessed Frame#realm.
48
+ # @rbs return: FrameRealm
49
+ def realm
50
+ main_realm
51
+ end
52
+
53
+ # Get the page that owns this frame
54
+ # Traverses up the parent chain until reaching a Page
55
+ # @rbs return: Page
56
+ def page
57
+ @parent.is_a?(Page) ? @parent : @parent.page
58
+ end
59
+
60
+ # Get the parent frame
61
+ # @rbs return: Frame?
62
+ def parent_frame
63
+ @parent.is_a?(Frame) ? @parent : nil
64
+ end
65
+
66
+ # Evaluate JavaScript in the frame context
67
+ # @rbs script: String
68
+ # @rbs *args: untyped
69
+ # @rbs return: untyped
70
+ def evaluate(script, *args)
71
+ assert_not_detached
72
+ main_realm.evaluate(script, *args)
73
+ end
74
+
75
+ # Evaluate JavaScript and return a handle to the result
76
+ # @rbs script: String
77
+ # @rbs *args: untyped
78
+ # @rbs return: JSHandle
79
+ def evaluate_handle(script, *args)
80
+ assert_not_detached
81
+ main_realm.evaluate_handle(script, *args)
82
+ end
83
+
84
+ # Get the document element handle
85
+ # @rbs return: ElementHandle
86
+ def document
87
+ assert_not_detached
88
+ handle = main_realm.evaluate_handle('document')
89
+ unless handle.is_a?(ElementHandle)
90
+ handle.dispose
91
+ raise 'Failed to get document'
92
+ end
93
+ handle
94
+ end
95
+
96
+ # Query for an element matching the selector
97
+ # @rbs selector: String
98
+ # @rbs return: ElementHandle?
99
+ def query_selector(selector)
100
+ doc = document
101
+ begin
102
+ doc.query_selector(selector)
103
+ ensure
104
+ doc.dispose
105
+ end
106
+ end
107
+
108
+ # Query for all elements matching the selector
109
+ # @rbs selector: String
110
+ # @rbs return: Array[ElementHandle]
111
+ def query_selector_all(selector)
112
+ doc = document
113
+ begin
114
+ doc.query_selector_all(selector)
115
+ ensure
116
+ doc.dispose
117
+ end
118
+ end
119
+
120
+ # Evaluate a function on the first element matching the selector
121
+ # @rbs selector: String
122
+ # @rbs page_function: String
123
+ # @rbs *args: untyped
124
+ # @rbs return: untyped
125
+ def eval_on_selector(selector, page_function, *args)
126
+ doc = document
127
+ begin
128
+ doc.eval_on_selector(selector, page_function, *args)
129
+ ensure
130
+ doc.dispose
131
+ end
132
+ end
133
+
134
+ # Evaluate a function on all elements matching the selector
135
+ # @rbs selector: String
136
+ # @rbs page_function: String
137
+ # @rbs *args: untyped
138
+ # @rbs return: untyped
139
+ def eval_on_selector_all(selector, page_function, *args)
140
+ doc = document
141
+ begin
142
+ doc.eval_on_selector_all(selector, page_function, *args)
143
+ ensure
144
+ doc.dispose
145
+ end
146
+ end
147
+
148
+ # Click an element matching the selector
149
+ # @rbs selector: String
150
+ # @rbs button: String
151
+ # @rbs count: Integer
152
+ # @rbs delay: Numeric?
153
+ # @rbs offset: Hash[Symbol, Numeric]?
154
+ # @rbs return: void
155
+ def click(selector, button: 'left', count: 1, delay: nil, offset: nil)
156
+ assert_not_detached
157
+
158
+ handle = query_selector(selector)
159
+ raise SelectorNotFoundError, selector unless handle
160
+
161
+ begin
162
+ handle.click(button: button, count: count, delay: delay, offset: offset)
163
+ ensure
164
+ handle.dispose
165
+ end
166
+ end
167
+
168
+ # Type text into an element matching the selector
169
+ # @rbs selector: String
170
+ # @rbs text: String
171
+ # @rbs delay: Numeric
172
+ # @rbs return: void
173
+ def type(selector, text, delay: 0)
174
+ assert_not_detached
175
+
176
+ handle = query_selector(selector)
177
+ raise SelectorNotFoundError, selector unless handle
178
+
179
+ begin
180
+ handle.type(text, delay: delay)
181
+ ensure
182
+ handle.dispose
183
+ end
184
+ end
185
+
186
+ # Hover over an element matching the selector
187
+ # @rbs selector: String
188
+ # @rbs return: void
189
+ def hover(selector)
190
+ assert_not_detached
191
+
192
+ handle = query_selector(selector)
193
+ raise SelectorNotFoundError, selector unless handle
194
+
195
+ begin
196
+ handle.hover
197
+ ensure
198
+ handle.dispose
199
+ end
200
+ end
201
+
202
+ # Get the frame URL
203
+ # @rbs return: String
204
+ def url
205
+ @browsing_context.url
206
+ end
207
+
208
+ # Navigate to a URL
209
+ # @rbs url: String
210
+ # @rbs wait_until: String
211
+ # @rbs timeout: Numeric
212
+ # @rbs return: HTTPResponse?
213
+ def goto(url, wait_until: 'load', timeout: 30000)
214
+ response = wait_for_navigation(timeout: timeout, wait_until: wait_until) do
215
+ @browsing_context.navigate(url, wait: 'interactive').wait
216
+ end
217
+ # Return HTTPResponse with the final URL
218
+ # Note: Currently we don't track HTTP status codes from BiDi protocol
219
+ # Assuming successful navigation (200 OK)
220
+ HTTPResponse.new(url: @browsing_context.url, status: 200)
221
+ end
222
+
223
+ # Set frame content
224
+ # @rbs html: String
225
+ # @rbs wait_until: String
226
+ # @rbs return: void
227
+ def set_content(html, wait_until: 'load')
228
+ assert_not_detached
229
+
230
+ # Puppeteer BiDi implementation:
231
+ # await Promise.all([
232
+ # this.setFrameContent(html),
233
+ # firstValueFrom(combineLatest([this.#waitForLoad$(options), this.#waitForNetworkIdle$(options)]))
234
+ # ]);
235
+
236
+ # IMPORTANT: Register listener BEFORE document.write to avoid race condition
237
+ load_event = case wait_until
238
+ when 'load'
239
+ :load
240
+ when 'domcontentloaded'
241
+ :dom_content_loaded
242
+ else
243
+ raise ArgumentError, "Unknown wait_until value: #{wait_until}"
244
+ end
245
+
246
+ promise = Async::Promise.new
247
+ listener = proc { promise.resolve(nil) }
248
+ @browsing_context.once(load_event, &listener)
249
+
250
+ # Execute both operations: document.write AND wait for load
251
+ # Use promise_all to wait for both to complete (like Puppeteer's Promise.all)
252
+ AsyncUtils.await_promise_all(
253
+ -> { set_frame_content(html) },
254
+ promise
255
+ )
256
+
257
+ nil
258
+ end
259
+
260
+ # Set frame content using document.open/write/close
261
+ # This is a low-level method that doesn't wait for load events
262
+ # @rbs content: String
263
+ # @rbs return: void
264
+ def set_frame_content(content)
265
+ assert_not_detached
266
+
267
+ evaluate(<<~JS, content)
268
+ html => {
269
+ document.open();
270
+ document.write(html);
271
+ document.close();
272
+ }
273
+ JS
274
+ end
275
+
276
+ # Get the frame name
277
+ # @rbs return: String
278
+ def name
279
+ @_name || ''
280
+ end
281
+
282
+ # Check if frame is detached
283
+ # @rbs return: bool
284
+ def detached?
285
+ @browsing_context.closed?
286
+ end
287
+
288
+ # Get child frames
289
+ # Returns cached frame instances following Puppeteer's pattern
290
+ # @rbs return: Array[Frame]
291
+ def child_frames
292
+ @browsing_context.children.map do |child_context|
293
+ @frames[child_context.id]
294
+ end.compact
295
+ end
296
+
297
+ # Get the frame element (iframe/frame DOM element) for this frame
298
+ # Returns nil for the main frame
299
+ # Following Puppeteer's Frame.frameElement() implementation exactly
300
+ # @rbs return: ElementHandle?
301
+ def frame_element
302
+ assert_not_detached
303
+
304
+ parent = parent_frame
305
+ return nil unless parent
306
+
307
+ # Query all iframe and frame elements in the parent frame
308
+ list = parent.isolated_realm.evaluate_handle('() => document.querySelectorAll("iframe,frame")')
309
+
310
+ begin
311
+ # Get the array of elements
312
+ length = list.evaluate('list => list.length')
313
+
314
+ length.times do |i|
315
+ iframe = list.evaluate_handle("(list, i) => list[i]", i)
316
+ begin
317
+ # Check if this iframe's content frame matches our frame
318
+ content_frame = iframe.as_element&.content_frame
319
+ if content_frame&.browsing_context&.id == @browsing_context.id
320
+ # Transfer the handle to the main realm (adopt handle)
321
+ # This ensures the returned handle is in the correct execution context
322
+ return parent.main_realm.transfer_handle(iframe.as_element)
323
+ end
324
+ ensure
325
+ iframe.dispose unless iframe.disposed?
326
+ end
327
+ end
328
+
329
+ nil
330
+ ensure
331
+ list.dispose unless list.disposed?
332
+ end
333
+ end
334
+
335
+ # Wait for navigation to complete
336
+ # @rbs timeout: Numeric
337
+ # @rbs wait_until: String | Array[String]
338
+ # @rbs &block: (-> void)?
339
+ # @rbs return: HTTPResponse?
340
+ def wait_for_navigation(timeout: 30000, wait_until: 'load', &block)
341
+ assert_not_detached
342
+
343
+ # Normalize wait_until to array
344
+ wait_until_array = wait_until.is_a?(Array) ? wait_until : [wait_until]
345
+
346
+ # Separate lifecycle events from network idle events
347
+ lifecycle_events = wait_until_array.select { |e| ['load', 'domcontentloaded'].include?(e) }
348
+ network_idle_events = wait_until_array.select { |e| ['networkidle0', 'networkidle2'].include?(e) }
349
+
350
+ # Default to 'load' if no lifecycle event specified
351
+ lifecycle_events = ['load'] if lifecycle_events.empty? && network_idle_events.any?
352
+
353
+ # Determine which load event to wait for (use the first one)
354
+ load_event = case lifecycle_events.first
355
+ when 'load'
356
+ :load
357
+ when 'domcontentloaded'
358
+ :dom_content_loaded
359
+ else
360
+ :load # Default
361
+ end
362
+
363
+ # Use Async::Promise for signaling (Fiber-based, not Thread-based)
364
+ # This avoids race conditions and follows Puppeteer's Promise-based pattern
365
+ promise = Async::Promise.new
366
+
367
+ # Track navigation type for response creation
368
+ navigation_type = nil # :full_page, :fragment, or :history
369
+ navigation_obj = nil # The navigation object we're waiting for
370
+
371
+ # Helper to set up navigation listeners
372
+ setup_navigation_listeners = proc do |navigation|
373
+ navigation_obj = navigation
374
+ navigation_type = :full_page
375
+
376
+ # Set up listeners for navigation completion
377
+ # Listen for fragment, failed, aborted events
378
+ navigation.once(:fragment) do
379
+ promise.resolve(nil) unless promise.resolved?
380
+ end
381
+
382
+ navigation.once(:failed) do
383
+ promise.resolve(nil) unless promise.resolved?
384
+ end
385
+
386
+ navigation.once(:aborted) do
387
+ next if detached?
388
+ promise.resolve(nil) unless promise.resolved?
389
+ end
390
+
391
+ # Also listen for load/domcontentloaded events to complete navigation
392
+ @browsing_context.once(load_event) do
393
+ promise.resolve(:full_page) unless promise.resolved?
394
+ end
395
+ end
396
+
397
+ # Listen for navigation events from BrowsingContext
398
+ # This follows Puppeteer's pattern: race between 'navigation', 'historyUpdated', and 'fragmentNavigated'
399
+ navigation_listener = proc do |data|
400
+ # Only handle if we haven't already attached to a navigation
401
+ next if navigation_obj
402
+
403
+ navigation = data[:navigation]
404
+ setup_navigation_listeners.call(navigation)
405
+ end
406
+
407
+ history_listener = proc do
408
+ # History API navigations (without Navigation object)
409
+ # Only resolve if we haven't attached to a navigation
410
+ promise.resolve(nil) unless navigation_obj || promise.resolved?
411
+ end
412
+
413
+ fragment_listener = proc do
414
+ # Fragment navigations (anchor links, hash changes)
415
+ # Only resolve if we haven't attached to a navigation
416
+ promise.resolve(nil) unless navigation_obj || promise.resolved?
417
+ end
418
+
419
+ closed_listener = proc do
420
+ # Handle frame detachment by rejecting the promise
421
+ promise.reject(FrameDetachedError.new('Navigating frame was detached')) unless promise.resolved?
422
+ end
423
+
424
+ @browsing_context.on(:navigation, &navigation_listener)
425
+ @browsing_context.on(:history_updated, &history_listener)
426
+ @browsing_context.on(:fragment_navigated, &fragment_listener)
427
+ @browsing_context.once(:closed, &closed_listener)
428
+
429
+ begin
430
+ # CRITICAL: Check for existing navigation BEFORE executing block
431
+ # This follows Puppeteer's pattern where waitForNavigation can attach to
432
+ # an already-started navigation (e.g., when called after goto)
433
+ existing_nav = @browsing_context.navigation
434
+ if existing_nav && !existing_nav.disposed?
435
+ # Attach to the existing navigation
436
+ setup_navigation_listeners.call(existing_nav)
437
+ end
438
+
439
+ # Execute the block if provided (this may trigger navigation)
440
+ # Block executes in the same Fiber context for cooperative multitasking
441
+ Async(&block).wait if block
442
+
443
+ # Wait for navigation with timeout using Async (Fiber-based)
444
+ if network_idle_events.any?
445
+ # Puppeteer's pattern: wait for both navigation completion AND network idle
446
+ # Determine concurrency based on network idle event
447
+ concurrency = network_idle_events.include?('networkidle0') ? 0 : 2
448
+
449
+ # Wait for both navigation and network idle in parallel using promise_all
450
+ navigation_result, _ = AsyncUtils.async_timeout(timeout, -> do
451
+ AsyncUtils.await_promise_all(
452
+ promise,
453
+ -> { page.wait_for_network_idle(idle_time: 500, timeout: timeout, concurrency: concurrency) }
454
+ )
455
+ end).wait
456
+
457
+ result = navigation_result
458
+ else
459
+ # Only wait for navigation
460
+ result = AsyncUtils.async_timeout(timeout, promise).wait
461
+ end
462
+
463
+ # Return HTTPResponse for full page navigation, nil for fragment/history
464
+ if result == :full_page
465
+ HTTPResponse.new(url: @browsing_context.url, status: 200)
466
+ else
467
+ nil
468
+ end
469
+ rescue Async::TimeoutError
470
+ raise Puppeteer::Bidi::TimeoutError, "Navigation timeout of #{timeout}ms exceeded"
471
+ ensure
472
+ # Clean up listeners
473
+ @browsing_context.off(:navigation, &navigation_listener)
474
+ @browsing_context.off(:history_updated, &history_listener)
475
+ @browsing_context.off(:fragment_navigated, &fragment_listener)
476
+ @browsing_context.off(:closed, &closed_listener)
477
+ end
478
+ end
479
+
480
+ # Wait for a function to return a truthy value
481
+ # @rbs page_function: String
482
+ # @rbs options: Hash[Symbol, untyped]
483
+ # @rbs *args: untyped
484
+ # @rbs &block: ((JSHandle) -> void)?
485
+ # @rbs return: JSHandle
486
+ def wait_for_function(page_function, options = {}, *args, &block)
487
+ main_realm.wait_for_function(page_function, options, *args, &block)
488
+ end
489
+
490
+ # Wait for an element matching the selector to appear in the frame
491
+ # @rbs selector: String
492
+ # @rbs visible: bool?
493
+ # @rbs hidden: bool?
494
+ # @rbs timeout: Numeric?
495
+ # @rbs &block: ((ElementHandle?) -> void)?
496
+ # @rbs return: ElementHandle?
497
+ def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil, &block)
498
+ result = QueryHandler.instance.get_query_handler_and_selector(selector)
499
+ result.query_handler.new.wait_for(self, result.updated_selector, visible: visible, hidden: hidden, polling: result.polling, timeout: timeout, &block)
500
+ end
501
+
502
+ # Set files on an input element
503
+ # @rbs element: ElementHandle
504
+ # @rbs files: Array[String]
505
+ # @rbs return: void
506
+ def set_files(element, files)
507
+ assert_not_detached
508
+
509
+ @browsing_context.set_files(
510
+ element.remote_value_as_shared_reference,
511
+ files
512
+ ).wait
513
+ end
514
+
515
+ # Get the frame ID (browsing context ID)
516
+ # Following Puppeteer's _id pattern
517
+ # @rbs return: String
518
+ def _id
519
+ @browsing_context.id
520
+ end
521
+
522
+ private
523
+
524
+ # Initialize the frame by setting up child frame tracking
525
+ # Following Puppeteer's BidiFrame.#initialize pattern exactly
526
+ # @rbs return: void
527
+ def initialize_frame
528
+ # Create Frame objects for existing child contexts
529
+ @browsing_context.children.each do |child_context|
530
+ create_frame_target(child_context)
531
+ end
532
+
533
+ # Listen for new child frames
534
+ @browsing_context.on(:browsingcontext) do |data|
535
+ create_frame_target(data[:browsing_context])
536
+ end
537
+
538
+ # Emit framedetached when THIS frame's browsing context is closed
539
+ # Following Puppeteer's pattern: this.browsingContext.on('closed', () => {
540
+ # this.page().trustedEmitter.emit(PageEvent.FrameDetached, this);
541
+ # });
542
+ @browsing_context.on(:closed) do
543
+ @frames.clear
544
+ page.emit(:framedetached, self)
545
+ end
546
+
547
+ # Listen for navigation events and emit framenavigated
548
+ # Following Puppeteer's pattern: emit framenavigated on DOMContentLoaded
549
+ @browsing_context.on(:dom_content_loaded) do
550
+ page.emit(:framenavigated, self)
551
+ end
552
+
553
+ # Also emit framenavigated on fragment navigation (anchor links, hash changes)
554
+ # Note: Puppeteer uses navigation.once('fragment'), but we listen to
555
+ # browsingContext's fragment_navigated which is equivalent
556
+ @browsing_context.on(:fragment_navigated) do
557
+ page.emit(:framenavigated, self)
558
+ end
559
+ end
560
+
561
+ # Create a Frame for a child browsing context
562
+ # Following Puppeteer's BidiFrame.#createFrameTarget pattern exactly:
563
+ # const frame = BidiFrame.from(this, browsingContext);
564
+ # this.#frames.set(browsingContext, frame);
565
+ # this.page().trustedEmitter.emit(PageEvent.FrameAttached, frame);
566
+ # browsingContext.on('closed', () => {
567
+ # this.#frames.delete(browsingContext);
568
+ # });
569
+ # Note: FrameDetached is NOT emitted here - it's emitted in #initialize
570
+ # when the frame's own browsing context closes
571
+ # @rbs browsing_context: Core::BrowsingContext
572
+ # @rbs return: Frame
573
+ def create_frame_target(browsing_context)
574
+ frame = Frame.from(self, browsing_context)
575
+ @frames[browsing_context.id] = frame
576
+
577
+ # Emit frameattached event
578
+ page.emit(:frameattached, frame)
579
+
580
+ # Remove frame from parent's frames map when its context is closed
581
+ # Note: FrameDetached is emitted by the frame itself in its initialize_frame
582
+ browsing_context.once(:closed) do
583
+ @frames.delete(browsing_context.id)
584
+ end
585
+
586
+ frame
587
+ end
588
+
589
+ # Check if this frame is detached and raise error if so
590
+ # @rbs return: void
591
+ def assert_not_detached
592
+ raise FrameDetachedError, "Attempted to use detached Frame '#{_id}'." if @browsing_context.closed?
593
+ end
594
+
595
+ end
596
+ end
597
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puppeteer
4
+ module Bidi
5
+ # HTTPResponse represents a response to an HTTP request
6
+ class HTTPResponse
7
+ attr_reader :url
8
+
9
+ # @param url [String] Response URL
10
+ # @param status [Integer] HTTP status code
11
+ def initialize(url:, status: 200)
12
+ @url = url
13
+ @status = status
14
+ end
15
+
16
+ # Check if the response was successful (status code 200-299)
17
+ # @return [Boolean] True if status code is in 2xx range
18
+ def ok?
19
+ @status >= 200 && @status < 300
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1 @@
1
+ "use strict";var g=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var Y=Object.prototype.hasOwnProperty;var l=(t,e)=>{for(var r in e)g(t,r,{get:e[r],enumerable:!0})},G=(t,e,r,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of B(e))!Y.call(t,s)&&s!==r&&g(t,s,{get:()=>e[s],enumerable:!(o=X(e,s))||o.enumerable});return t};var J=t=>G(g({},"__esModule",{value:!0}),t);var pe={};l(pe,{default:()=>he});module.exports=J(pe);var N=class extends Error{constructor(e,r){super(e,r),this.name=this.constructor.name}get[Symbol.toStringTag](){return this.constructor.name}},p=class extends N{};var c=class t{static create(e){return new t(e)}static async race(e){let r=new Set;try{let o=e.map(s=>s instanceof t?(s.#s&&r.add(s),s.valueOrThrow()):s);return await Promise.race(o)}finally{for(let o of r)o.reject(new Error("Timeout cleared"))}}#e=!1;#r=!1;#o;#t;#a=new Promise(e=>{this.#t=e});#s;#i;constructor(e){e&&e.timeout>0&&(this.#i=new p(e.message),this.#s=setTimeout(()=>{this.reject(this.#i)},e.timeout))}#l(e){clearTimeout(this.#s),this.#o=e,this.#t()}resolve(e){this.#r||this.#e||(this.#e=!0,this.#l(e))}reject(e){this.#r||this.#e||(this.#r=!0,this.#l(e))}resolved(){return this.#e}finished(){return this.#e||this.#r}value(){return this.#o}#n;valueOrThrow(){return this.#n||(this.#n=(async()=>{if(await this.#a,this.#r)throw this.#o;return this.#o})()),this.#n}};var L=new Map,W=t=>{let e=L.get(t);return e||(e=new Function(`return ${t}`)(),L.set(t,e),e)};var b={};l(b,{ariaQuerySelector:()=>z,ariaQuerySelectorAll:()=>x});var z=(t,e)=>globalThis.__ariaQuerySelector(t,e),x=async function*(t,e){yield*await globalThis.__ariaQuerySelectorAll(t,e)};var E={};l(E,{cssQuerySelector:()=>K,cssQuerySelectorAll:()=>Z});var K=(t,e)=>t.querySelector(e),Z=function(t,e){return t.querySelectorAll(e)};var A={};l(A,{customQuerySelectors:()=>P});var v=class{#e=new Map;register(e,r){if(!r.queryOne&&r.queryAll){let o=r.queryAll;r.queryOne=(s,i)=>{for(let n of o(s,i))return n;return null}}else if(r.queryOne&&!r.queryAll){let o=r.queryOne;r.queryAll=(s,i)=>{let n=o(s,i);return n?[n]:[]}}else if(!r.queryOne||!r.queryAll)throw new Error("At least one query method must be defined.");this.#e.set(e,{querySelector:r.queryOne,querySelectorAll:r.queryAll})}unregister(e){this.#e.delete(e)}get(e){return this.#e.get(e)}clear(){this.#e.clear()}},P=new v;var R={};l(R,{pierceQuerySelector:()=>ee,pierceQuerySelectorAll:()=>te});var ee=(t,e)=>{let r=null,o=s=>{let i=document.createTreeWalker(s,NodeFilter.SHOW_ELEMENT);do{let n=i.currentNode;n.shadowRoot&&o(n.shadowRoot),!(n instanceof ShadowRoot)&&n!==s&&!r&&n.matches(e)&&(r=n)}while(!r&&i.nextNode())};return t instanceof Document&&(t=t.documentElement),o(t),r},te=(t,e)=>{let r=[],o=s=>{let i=document.createTreeWalker(s,NodeFilter.SHOW_ELEMENT);do{let n=i.currentNode;n.shadowRoot&&o(n.shadowRoot),!(n instanceof ShadowRoot)&&n!==s&&n.matches(e)&&r.push(n)}while(i.nextNode())};return t instanceof Document&&(t=t.documentElement),o(t),r};var u=(t,e)=>{if(!t)throw new Error(e)};var y=class{#e;#r;#o;#t;constructor(e,r){this.#e=e,this.#r=r}async start(){let e=this.#t=c.create(),r=await this.#e();if(r){e.resolve(r);return}this.#o=new MutationObserver(async()=>{let o=await this.#e();o&&(e.resolve(o),await this.stop())}),this.#o.observe(this.#r,{childList:!0,subtree:!0,attributes:!0})}async stop(){u(this.#t,"Polling never started."),this.#t.finished()||this.#t.reject(new Error("Polling stopped")),this.#o&&(this.#o.disconnect(),this.#o=void 0)}result(){return u(this.#t,"Polling never started."),this.#t.valueOrThrow()}},w=class{#e;#r;constructor(e){this.#e=e}async start(){let e=this.#r=c.create(),r=await this.#e();if(r){e.resolve(r);return}let o=async()=>{if(e.finished())return;let s=await this.#e();if(!s){window.requestAnimationFrame(o);return}e.resolve(s),await this.stop()};window.requestAnimationFrame(o)}async stop(){u(this.#r,"Polling never started."),this.#r.finished()||this.#r.reject(new Error("Polling stopped"))}result(){return u(this.#r,"Polling never started."),this.#r.valueOrThrow()}},T=class{#e;#r;#o;#t;constructor(e,r){this.#e=e,this.#r=r}async start(){let e=this.#t=c.create(),r=await this.#e();if(r){e.resolve(r);return}this.#o=setInterval(async()=>{let o=await this.#e();o&&(e.resolve(o),await this.stop())},this.#r)}async stop(){u(this.#t,"Polling never started."),this.#t.finished()||this.#t.reject(new Error("Polling stopped")),this.#o&&(clearInterval(this.#o),this.#o=void 0)}result(){return u(this.#t,"Polling never started."),this.#t.valueOrThrow()}};var _={};l(_,{PCombinator:()=>H,pQuerySelector:()=>fe,pQuerySelectorAll:()=>$});var a=class{static async*map(e,r){for await(let o of e)yield await r(o)}static async*flatMap(e,r){for await(let o of e)yield*r(o)}static async collect(e){let r=[];for await(let o of e)r.push(o);return r}static async first(e){for await(let r of e)return r}};var C={};l(C,{textQuerySelectorAll:()=>m});var re=new Set(["checkbox","image","radio"]),oe=t=>t instanceof HTMLSelectElement||t instanceof HTMLTextAreaElement||t instanceof HTMLInputElement&&!re.has(t.type),se=new Set(["SCRIPT","STYLE"]),f=t=>!se.has(t.nodeName)&&!document.head?.contains(t),I=new WeakMap,F=t=>{for(;t;)I.delete(t),t instanceof ShadowRoot?t=t.host:t=t.parentNode},j=new WeakSet,ne=new MutationObserver(t=>{for(let e of t)F(e.target)}),d=t=>{let e=I.get(t);if(e||(e={full:"",immediate:[]},!f(t)))return e;let r="";if(oe(t))e.full=t.value,e.immediate.push(t.value),t.addEventListener("input",o=>{F(o.target)},{once:!0,capture:!0});else{for(let o=t.firstChild;o;o=o.nextSibling){if(o.nodeType===Node.TEXT_NODE){e.full+=o.nodeValue??"",r+=o.nodeValue??"";continue}r&&e.immediate.push(r),r="",o.nodeType===Node.ELEMENT_NODE&&(e.full+=d(o).full)}r&&e.immediate.push(r),t instanceof Element&&t.shadowRoot&&(e.full+=d(t.shadowRoot).full),j.has(t)||(ne.observe(t,{childList:!0,characterData:!0,subtree:!0}),j.add(t))}return I.set(t,e),e};var m=function*(t,e){let r=!1;for(let o of t.childNodes)if(o instanceof Element&&f(o)){let s;o.shadowRoot?s=m(o.shadowRoot,e):s=m(o,e);for(let i of s)yield i,r=!0}r||t instanceof Element&&f(t)&&d(t).full.includes(e)&&(yield t)};var k={};l(k,{checkVisibility:()=>le,pierce:()=>S,pierceAll:()=>O});var ie=["hidden","collapse"],le=(t,e)=>{if(!t)return e===!1;if(e===void 0)return t;let r=t.nodeType===Node.TEXT_NODE?t.parentElement:t,o=window.getComputedStyle(r),s=o&&!ie.includes(o.visibility)&&!ae(r);return e===s?t:!1};function ae(t){let e=t.getBoundingClientRect();return e.width===0||e.height===0}var ce=t=>"shadowRoot"in t&&t.shadowRoot instanceof ShadowRoot;function*S(t){ce(t)?yield t.shadowRoot:yield t}function*O(t){t=S(t).next().value,yield t;let e=[document.createTreeWalker(t,NodeFilter.SHOW_ELEMENT)];for(let r of e){let o;for(;o=r.nextNode();)o.shadowRoot&&(yield o.shadowRoot,e.push(document.createTreeWalker(o.shadowRoot,NodeFilter.SHOW_ELEMENT)))}}var D={};l(D,{xpathQuerySelectorAll:()=>q});var q=function*(t,e,r=-1){let s=(t.ownerDocument||document).evaluate(e,t,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE),i=[],n;for(;(n=s.iterateNext())&&(i.push(n),!(r&&i.length===r)););for(let h=0;h<i.length;h++)n=i[h],yield n,delete i[h]};var ue=/[-\w\P{ASCII}*]/u,H=(r=>(r.Descendent=">>>",r.Child=">>>>",r))(H||{}),V=t=>"querySelectorAll"in t,Q=class{#e;#r=[];#o=void 0;elements;constructor(e,r){this.elements=[e],this.#e=r,this.#t()}async run(){if(typeof this.#o=="string")switch(this.#o.trimStart()){case":scope":this.#t();break}for(;this.#o!==void 0;this.#t()){let e=this.#o;typeof e=="string"?e[0]&&ue.test(e[0])?this.elements=a.flatMap(this.elements,async function*(r){V(r)&&(yield*r.querySelectorAll(e))}):this.elements=a.flatMap(this.elements,async function*(r){if(!r.parentElement){if(!V(r))return;yield*r.querySelectorAll(e);return}let o=0;for(let s of r.parentElement.children)if(++o,s===r)break;yield*r.parentElement.querySelectorAll(`:scope>:nth-child(${o})${e}`)}):this.elements=a.flatMap(this.elements,async function*(r){switch(e.name){case"text":yield*m(r,e.value);break;case"xpath":yield*q(r,e.value);break;case"aria":yield*x(r,e.value);break;default:let o=P.get(e.name);if(!o)throw new Error(`Unknown selector type: ${e.name}`);yield*o.querySelectorAll(r,e.value)}})}}#t(){if(this.#r.length!==0){this.#o=this.#r.shift();return}if(this.#e.length===0){this.#o=void 0;return}let e=this.#e.shift();switch(e){case">>>>":{this.elements=a.flatMap(this.elements,S),this.#t();break}case">>>":{this.elements=a.flatMap(this.elements,O),this.#t();break}default:this.#r=e,this.#t();break}}},M=class{#e=new WeakMap;calculate(e,r=[]){if(e===null)return r;e instanceof ShadowRoot&&(e=e.host);let o=this.#e.get(e);if(o)return[...o,...r];let s=0;for(let n=e.previousSibling;n;n=n.previousSibling)++s;let i=this.calculate(e.parentNode,[s]);return this.#e.set(e,i),[...i,...r]}},U=(t,e)=>{if(t.length+e.length===0)return 0;let[r=-1,...o]=t,[s=-1,...i]=e;return r===s?U(o,i):r<s?-1:1},de=async function*(t){let e=new Set;for await(let o of t)e.add(o);let r=new M;yield*[...e.values()].map(o=>[o,r.calculate(o)]).sort(([,o],[,s])=>U(o,s)).map(([o])=>o)},$=function(t,e){let r=JSON.parse(e);if(r.some(o=>{let s=0;return o.some(i=>(typeof i=="string"?++s:s=0,s>1))}))throw new Error("Multiple deep combinators found in sequence.");return de(a.flatMap(r,o=>{let s=new Q(t,o);return s.run(),s.elements}))},fe=async function(t,e){for await(let r of $(t,e))return r;return null};var me=Object.freeze({...b,...A,...R,..._,...C,...k,...D,...E,Deferred:c,createFunction:W,createTextContent:d,IntervalPoller:T,isSuitableNodeForTextMatching:f,MutationPoller:y,RAFPoller:w}),he=me;
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puppeteer
4
+ module Bidi
5
+ # Puppeteer's injected utilities (Poller classes, Deferred, etc.)
6
+ # Source: https://unpkg.com/puppeteer-core@24.31.0/lib/esm/puppeteer/generated/injected.js
7
+ # Version: puppeteer-core@24.31.0
8
+ #
9
+ # To update this file, run:
10
+ # bundle exec ruby scripts/update_injected_source.rb
11
+ #
12
+ # This script provides:
13
+ # - RAFPoller: requestAnimationFrame-based polling
14
+ # - MutationPoller: MutationObserver-based polling
15
+ # - IntervalPoller: setInterval-based polling
16
+ # - Deferred: Promise wrapper
17
+ # - createFunction: Creates function from string
18
+ # - Various query selector utilities
19
+ PUPPETEER_INJECTED_SOURCE = File.read(File.join(__dir__, 'injected.js')).freeze
20
+ end
21
+ end