puppeteer-ruby 0.50.0.alpha5 → 0.50.0.alpha6

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.
@@ -0,0 +1,733 @@
1
+ # rbs_inline: enabled
2
+
3
+ module Puppeteer
4
+ module LocatorEvent
5
+ Action = 'action'
6
+ end
7
+ end
8
+
9
+ class Puppeteer::Locator
10
+ include Puppeteer::EventCallbackable
11
+
12
+ RETRY_DELAY_SECONDS = 0.1
13
+
14
+ class TimeoutController
15
+ def initialize(timeout)
16
+ @timeout = timeout
17
+ @deadline = nil
18
+
19
+ return if @timeout.nil? || @timeout == 0
20
+
21
+ @deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (@timeout / 1000.0)
22
+ end
23
+
24
+ attr_reader :timeout
25
+
26
+ def remaining_timeout
27
+ return 0 if @timeout == 0
28
+ return nil unless @deadline
29
+
30
+ remaining = (@deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)) * 1000.0
31
+ remaining.negative? ? 0 : remaining
32
+ end
33
+
34
+ def exceeded?
35
+ return false unless @deadline
36
+
37
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @deadline
38
+ end
39
+
40
+ def check!(cause = nil)
41
+ return unless exceeded?
42
+
43
+ error = Puppeteer::TimeoutError.new("Timed out after waiting #{@timeout}ms")
44
+ error.cause = cause if cause
45
+ raise error
46
+ end
47
+ end
48
+
49
+ def initialize
50
+ @visibility = nil
51
+ @timeout = 30_000
52
+ @ensure_element_is_in_viewport = true
53
+ @wait_for_enabled = true
54
+ @wait_for_stable_bounding_box = true
55
+ end
56
+
57
+ # @rbs locators: Array[Puppeteer::Locator] -- Locator candidates
58
+ # @rbs return: Puppeteer::Locator -- Locator that races candidates
59
+ def self.race(locators)
60
+ proxy = locators.find { |locator| locator.is_a?(Puppeteer::ReactorRunner::Proxy) }
61
+ return Puppeteer::RaceLocator.create(locators) unless proxy
62
+
63
+ runner = proxy.instance_variable_get(:@runner)
64
+ locators.each do |locator|
65
+ next unless locator.is_a?(Puppeteer::ReactorRunner::Proxy)
66
+
67
+ locator_runner = locator.instance_variable_get(:@runner)
68
+ unless locator_runner == runner
69
+ raise ArgumentError.new('Locators for race must belong to the same runner')
70
+ end
71
+ end
72
+
73
+ runner.sync do
74
+ unwrapped = locators.map { |locator| runner.send(:unwrap, locator) }
75
+ runner.wrap(Puppeteer::RaceLocator.create(unwrapped))
76
+ end
77
+ end
78
+
79
+ # @rbs input: String -- Input string to check
80
+ # @rbs return: bool -- Whether string looks like a JS function
81
+ def self.function_string?(input)
82
+ return false unless input.is_a?(String)
83
+
84
+ stripped = input.lstrip
85
+ return true if input.include?('=>')
86
+ return true if stripped.start_with?('async function')
87
+
88
+ stripped.start_with?('function')
89
+ end
90
+
91
+ # @rbs return: Numeric -- Timeout in milliseconds
92
+ def timeout
93
+ @timeout
94
+ end
95
+
96
+ # @rbs timeout: Numeric -- Timeout in milliseconds
97
+ # @rbs return: Puppeteer::Locator -- Updated locator
98
+ def set_timeout(timeout)
99
+ locator = _clone
100
+ locator.instance_variable_set(:@timeout, timeout)
101
+ locator
102
+ end
103
+
104
+ # @rbs visibility: String? -- 'visible', 'hidden', or nil
105
+ # @rbs return: Puppeteer::Locator -- Updated locator
106
+ def set_visibility(visibility)
107
+ locator = _clone
108
+ locator.instance_variable_set(:@visibility, visibility&.to_s)
109
+ locator
110
+ end
111
+
112
+ # @rbs value: bool -- Whether to wait for enabled state
113
+ # @rbs return: Puppeteer::Locator -- Updated locator
114
+ def set_wait_for_enabled(value)
115
+ locator = _clone
116
+ locator.instance_variable_set(:@wait_for_enabled, value)
117
+ locator
118
+ end
119
+
120
+ # @rbs value: bool -- Whether to ensure element is in viewport
121
+ # @rbs return: Puppeteer::Locator -- Updated locator
122
+ def set_ensure_element_is_in_the_viewport(value)
123
+ locator = _clone
124
+ locator.instance_variable_set(:@ensure_element_is_in_viewport, value)
125
+ locator
126
+ end
127
+
128
+ # @rbs value: bool -- Whether to wait for stable bounding box
129
+ # @rbs return: Puppeteer::Locator -- Updated locator
130
+ def set_wait_for_stable_bounding_box(value)
131
+ locator = _clone
132
+ locator.instance_variable_set(:@wait_for_stable_bounding_box, value)
133
+ locator
134
+ end
135
+
136
+ # @rbs locator: Puppeteer::Locator -- Locator to copy options from
137
+ # @rbs return: self -- Locator with copied options
138
+ def copy_options(locator)
139
+ @timeout = locator.timeout
140
+ @visibility = locator.instance_variable_get(:@visibility)
141
+ @wait_for_enabled = locator.instance_variable_get(:@wait_for_enabled)
142
+ @ensure_element_is_in_viewport = locator.instance_variable_get(:@ensure_element_is_in_viewport)
143
+ @wait_for_stable_bounding_box = locator.instance_variable_get(:@wait_for_stable_bounding_box)
144
+ self
145
+ end
146
+
147
+ # @rbs return: Puppeteer::Locator -- Cloned locator
148
+ def clone(freeze: nil)
149
+ _clone
150
+ end
151
+
152
+ # @rbs return: Puppeteer::JSHandle -- Handle for located value
153
+ def wait_handle
154
+ with_retry('Locator.waitHandle') do |options|
155
+ _wait(options)
156
+ end
157
+ end
158
+
159
+ # @rbs return: untyped -- JSON-serializable value
160
+ def wait
161
+ handle = wait_handle
162
+ begin
163
+ handle.json_value
164
+ ensure
165
+ handle.dispose
166
+ end
167
+ end
168
+
169
+ # @rbs mapper: String -- JS mapper function
170
+ # @rbs return: Puppeteer::Locator -- Mapped locator
171
+ def map(mapper)
172
+ Puppeteer::MappedLocator.new(_clone, lambda { |handle, _options|
173
+ handle.evaluate_handle(mapper)
174
+ })
175
+ end
176
+
177
+ # @rbs predicate: String -- JS predicate function
178
+ # @rbs return: Puppeteer::Locator -- Filtered locator
179
+ def filter(predicate)
180
+ Puppeteer::FilteredLocator.new(_clone, lambda { |handle, options|
181
+ handle.frame.wait_for_function(predicate, args: [handle], timeout: @timeout)
182
+ true
183
+ })
184
+ end
185
+
186
+ # @rbs predicate: Proc -- Handle predicate
187
+ # @rbs return: Puppeteer::Locator -- Filtered locator
188
+ def filter_handle(predicate)
189
+ Puppeteer::FilteredLocator.new(_clone, predicate)
190
+ end
191
+
192
+ # @rbs mapper: Proc -- Handle mapper
193
+ # @rbs return: Puppeteer::Locator -- Mapped locator
194
+ def map_handle(mapper)
195
+ Puppeteer::MappedLocator.new(_clone, mapper)
196
+ end
197
+
198
+ # @rbs delay: Numeric? -- Delay between down and up (ms)
199
+ # @rbs button: String? -- Mouse button
200
+ # @rbs click_count: Integer? -- Deprecated click count
201
+ # @rbs count: Integer? -- Number of clicks
202
+ # @rbs offset: Hash[Symbol, Numeric]? -- Click offset
203
+ # @rbs return: void -- No return value
204
+ def click(delay: nil, button: nil, click_count: nil, count: nil, offset: nil)
205
+ perform_action('Locator.click',
206
+ conditions: [
207
+ method(:ensure_element_is_in_viewport_if_needed),
208
+ method(:wait_for_stable_bounding_box_if_needed),
209
+ method(:wait_for_enabled_if_needed),
210
+ ]) do |handle, _options|
211
+ handle.click(delay: delay, button: button, click_count: click_count, count: count, offset: offset)
212
+ end
213
+ end
214
+
215
+ # @rbs value: String -- Value to fill
216
+ # @rbs return: void -- No return value
217
+ def fill(value)
218
+ perform_action('Locator.fill',
219
+ conditions: [
220
+ method(:ensure_element_is_in_viewport_if_needed),
221
+ method(:wait_for_stable_bounding_box_if_needed),
222
+ method(:wait_for_enabled_if_needed),
223
+ ]) do |handle, _options|
224
+ fill_element(handle, value)
225
+ end
226
+ end
227
+
228
+ # @rbs return: void -- No return value
229
+ def hover
230
+ perform_action('Locator.hover',
231
+ conditions: [
232
+ method(:ensure_element_is_in_viewport_if_needed),
233
+ method(:wait_for_stable_bounding_box_if_needed),
234
+ ]) do |handle, _options|
235
+ handle.hover
236
+ end
237
+ end
238
+
239
+ # @rbs scroll_top: Numeric? -- Scroll top position
240
+ # @rbs scroll_left: Numeric? -- Scroll left position
241
+ # @rbs return: void -- No return value
242
+ def scroll(scroll_top: nil, scroll_left: nil)
243
+ perform_action('Locator.scroll',
244
+ conditions: [
245
+ method(:ensure_element_is_in_viewport_if_needed),
246
+ method(:wait_for_stable_bounding_box_if_needed),
247
+ ]) do |handle, _options|
248
+ js = <<~JAVASCRIPT
249
+ (el, scrollTop, scrollLeft) => {
250
+ if (scrollTop !== undefined && scrollTop !== null) {
251
+ el.scrollTop = scrollTop;
252
+ }
253
+ if (scrollLeft !== undefined && scrollLeft !== null) {
254
+ el.scrollLeft = scrollLeft;
255
+ }
256
+ }
257
+ JAVASCRIPT
258
+ handle.evaluate(js, scroll_top, scroll_left)
259
+ end
260
+ end
261
+
262
+ # @rbs event_name: String -- Event name
263
+ # @rbs block: Proc -- Event handler
264
+ # @rbs return: Puppeteer::Locator -- Locator for chaining
265
+ def on(event_name, &block)
266
+ add_event_listener(event_name, &block)
267
+ self
268
+ end
269
+
270
+ # @rbs event_name: String -- Event name
271
+ # @rbs block: Proc -- Event handler
272
+ # @rbs return: Puppeteer::Locator -- Locator for chaining
273
+ def once(event_name, &block)
274
+ observe_first(event_name, &block)
275
+ self
276
+ end
277
+
278
+ protected def _clone
279
+ raise NotImplementedError
280
+ end
281
+
282
+ protected def _wait(_options)
283
+ raise NotImplementedError
284
+ end
285
+
286
+ private def perform_action(name, conditions:, &block)
287
+ with_retry(name) do |options|
288
+ handle = _wait(options)
289
+ begin
290
+ run_conditions(handle, options, conditions)
291
+ emit_event(Puppeteer::LocatorEvent::Action)
292
+ block.call(handle, options)
293
+ nil
294
+ rescue => err
295
+ begin
296
+ handle.dispose
297
+ rescue StandardError
298
+ # Ignore disposal errors after a failed action.
299
+ end
300
+ raise err
301
+ end
302
+ end
303
+ end
304
+
305
+ private def with_retry(_name, &block)
306
+ timeout_controller = TimeoutController.new(@timeout)
307
+ last_error = nil
308
+
309
+ loop do
310
+ timeout_controller.check!(last_error)
311
+
312
+ options = build_action_options(timeout_controller)
313
+ begin
314
+ remaining = options[:timeout]
315
+ if remaining && remaining > 0
316
+ return Puppeteer::AsyncUtils.async_timeout(remaining, -> { block.call(options) }).wait
317
+ end
318
+ return block.call(options)
319
+ rescue Async::TimeoutError
320
+ timeout_controller.check!(last_error)
321
+ rescue => err
322
+ last_error = err
323
+ timeout_controller.check!(last_error)
324
+
325
+ Puppeteer::AsyncUtils.sleep_seconds(RETRY_DELAY_SECONDS)
326
+ end
327
+ end
328
+ end
329
+
330
+ private def build_action_options(timeout_controller)
331
+ {
332
+ timeout: timeout_controller.remaining_timeout,
333
+ timeout_controller: timeout_controller,
334
+ }
335
+ end
336
+
337
+ private def run_conditions(handle, options, conditions)
338
+ return if conditions.empty?
339
+
340
+ tasks = conditions.map do |condition|
341
+ proc { condition.call(handle, options) }
342
+ end
343
+ Puppeteer::AsyncUtils.await_promise_all(*tasks)
344
+ end
345
+
346
+ private def wait_for_enabled_if_needed(handle, options)
347
+ return unless @wait_for_enabled
348
+
349
+ js = <<~JAVASCRIPT
350
+ element => {
351
+ if (!(element instanceof HTMLElement)) {
352
+ return true;
353
+ }
354
+ const isNativeFormControl = [
355
+ 'BUTTON',
356
+ 'INPUT',
357
+ 'SELECT',
358
+ 'TEXTAREA',
359
+ 'OPTION',
360
+ 'OPTGROUP',
361
+ ].includes(element.nodeName);
362
+ return !isNativeFormControl || !element.hasAttribute('disabled');
363
+ }
364
+ JAVASCRIPT
365
+
366
+ result = handle.frame.wait_for_function(js, args: [handle], timeout: options[:timeout])
367
+ result.dispose
368
+ end
369
+
370
+ private def wait_for_stable_bounding_box_if_needed(handle, options)
371
+ return unless @wait_for_stable_bounding_box
372
+
373
+ js = <<~JAVASCRIPT
374
+ element => {
375
+ return new Promise(resolve => {
376
+ window.requestAnimationFrame(() => {
377
+ const rect1 = element.getBoundingClientRect();
378
+ window.requestAnimationFrame(() => {
379
+ const rect2 = element.getBoundingClientRect();
380
+ resolve([
381
+ { x: rect1.x, y: rect1.y, width: rect1.width, height: rect1.height },
382
+ { x: rect2.x, y: rect2.y, width: rect2.width, height: rect2.height },
383
+ ]);
384
+ });
385
+ });
386
+ });
387
+ }
388
+ JAVASCRIPT
389
+
390
+ loop do
391
+ rects = handle.evaluate(js)
392
+ rect1 = rects[0]
393
+ rect2 = rects[1]
394
+ if rect1 && rect2 &&
395
+ rect1['x'] == rect2['x'] &&
396
+ rect1['y'] == rect2['y'] &&
397
+ rect1['width'] == rect2['width'] &&
398
+ rect1['height'] == rect2['height']
399
+
400
+ return
401
+ end
402
+
403
+ options[:timeout_controller].check!
404
+ Puppeteer::AsyncUtils.sleep_seconds(RETRY_DELAY_SECONDS)
405
+ end
406
+ end
407
+
408
+ private def ensure_element_is_in_viewport_if_needed(handle, options)
409
+ return unless @ensure_element_is_in_viewport
410
+
411
+ loop do
412
+ intersects = handle.intersecting_viewport?(threshold: 0)
413
+ return if intersects
414
+
415
+ handle.scroll_into_view_if_needed
416
+
417
+ intersects = handle.intersecting_viewport?(threshold: 0)
418
+ return if intersects
419
+
420
+ options[:timeout_controller].check!
421
+ Puppeteer::AsyncUtils.sleep_seconds(RETRY_DELAY_SECONDS)
422
+ end
423
+ end
424
+
425
+ private def fill_element(handle, value)
426
+ input_type = handle.evaluate(<<~JAVASCRIPT)
427
+ el => {
428
+ if (el instanceof HTMLSelectElement) {
429
+ return 'select';
430
+ }
431
+ if (el instanceof HTMLTextAreaElement) {
432
+ return 'typeable-input';
433
+ }
434
+ if (el instanceof HTMLInputElement) {
435
+ if (
436
+ new Set([
437
+ 'textarea',
438
+ 'text',
439
+ 'url',
440
+ 'tel',
441
+ 'search',
442
+ 'password',
443
+ 'number',
444
+ 'email',
445
+ ]).has(el.type)
446
+ ) {
447
+ return 'typeable-input';
448
+ }
449
+ return 'other-input';
450
+ }
451
+
452
+ if (el.isContentEditable) {
453
+ return 'contenteditable';
454
+ }
455
+
456
+ return 'unknown';
457
+ }
458
+ JAVASCRIPT
459
+
460
+ case input_type
461
+ when 'select'
462
+ handle.select(value)
463
+ when 'contenteditable', 'typeable-input'
464
+ text_to_type = handle.evaluate(<<~JAVASCRIPT, value)
465
+ (input, newValue) => {
466
+ const currentValue = input.isContentEditable
467
+ ? input.innerText
468
+ : input.value;
469
+
470
+ if (
471
+ newValue.length <= currentValue.length ||
472
+ !newValue.startsWith(input.value)
473
+ ) {
474
+ if (input.isContentEditable) {
475
+ input.innerText = '';
476
+ } else {
477
+ input.value = '';
478
+ }
479
+ return newValue;
480
+ }
481
+ const originalValue = input.isContentEditable
482
+ ? input.innerText
483
+ : input.value;
484
+
485
+ if (input.isContentEditable) {
486
+ input.innerText = '';
487
+ input.innerText = originalValue;
488
+ } else {
489
+ input.value = '';
490
+ input.value = originalValue;
491
+ }
492
+ return newValue.substring(originalValue.length);
493
+ }
494
+ JAVASCRIPT
495
+ text_to_type = text_to_type.to_s
496
+ handle.type_text(text_to_type)
497
+ when 'other-input'
498
+ handle.focus
499
+ handle.evaluate(<<~JAVASCRIPT, value)
500
+ (input, newValue) => {
501
+ input.value = newValue;
502
+ input.dispatchEvent(new Event('input', { bubbles: true }));
503
+ input.dispatchEvent(new Event('change', { bubbles: true }));
504
+ }
505
+ JAVASCRIPT
506
+ else
507
+ raise Puppeteer::Error.new('Element cannot be filled out.')
508
+ end
509
+ end
510
+ end
511
+
512
+ class Puppeteer::FunctionLocator < Puppeteer::Locator
513
+ # @rbs page_or_frame: Puppeteer::Page | Puppeteer::Frame -- Page or frame
514
+ # @rbs func: String -- JS function to evaluate
515
+ # @rbs return: Puppeteer::Locator -- Function locator
516
+ def self.create(page_or_frame, func)
517
+ new(page_or_frame, func).set_timeout(default_timeout_for(page_or_frame))
518
+ end
519
+
520
+ def initialize(page_or_frame, func)
521
+ super()
522
+ @page_or_frame = page_or_frame
523
+ @func = func
524
+ end
525
+
526
+ protected def _clone
527
+ self.class.new(@page_or_frame, @func).copy_options(self)
528
+ end
529
+
530
+ protected def _wait(options)
531
+ @page_or_frame.wait_for_function(@func, timeout: options[:timeout])
532
+ end
533
+
534
+ def self.default_timeout_for(page_or_frame)
535
+ if page_or_frame.respond_to?(:default_timeout)
536
+ page_or_frame.default_timeout
537
+ else
538
+ page_or_frame.page.default_timeout
539
+ end
540
+ end
541
+
542
+ private_class_method :default_timeout_for
543
+ end
544
+
545
+ class Puppeteer::DelegatedLocator < Puppeteer::Locator
546
+ def initialize(delegate)
547
+ super()
548
+ @delegate = delegate
549
+ copy_options(@delegate)
550
+ end
551
+
552
+ protected def delegate
553
+ @delegate
554
+ end
555
+
556
+ def set_timeout(timeout)
557
+ locator = super
558
+ locator.instance_variable_set(:@delegate, @delegate.set_timeout(timeout))
559
+ locator
560
+ end
561
+
562
+ def set_visibility(visibility)
563
+ locator = super
564
+ locator.instance_variable_set(:@delegate, @delegate.set_visibility(visibility))
565
+ locator
566
+ end
567
+
568
+ def set_wait_for_enabled(value)
569
+ locator = super
570
+ locator.instance_variable_set(:@delegate, @delegate.set_wait_for_enabled(value))
571
+ locator
572
+ end
573
+
574
+ def set_ensure_element_is_in_the_viewport(value)
575
+ locator = super
576
+ locator.instance_variable_set(:@delegate, @delegate.set_ensure_element_is_in_the_viewport(value))
577
+ locator
578
+ end
579
+
580
+ def set_wait_for_stable_bounding_box(value)
581
+ locator = super
582
+ locator.instance_variable_set(:@delegate, @delegate.set_wait_for_stable_bounding_box(value))
583
+ locator
584
+ end
585
+
586
+ protected def _clone
587
+ raise NotImplementedError
588
+ end
589
+
590
+ protected def _wait(_options)
591
+ raise NotImplementedError
592
+ end
593
+ end
594
+
595
+ class Puppeteer::FilteredLocator < Puppeteer::DelegatedLocator
596
+ def initialize(base, predicate)
597
+ super(base)
598
+ @predicate = predicate
599
+ end
600
+
601
+ protected def _clone
602
+ self.class.new(delegate.clone, @predicate).copy_options(self)
603
+ end
604
+
605
+ protected def _wait(options)
606
+ handle = delegate.send(:_wait, options)
607
+ result = @predicate.call(handle, options)
608
+ return handle if result
609
+
610
+ raise Puppeteer::Error.new('Locator predicate did not match')
611
+ end
612
+ end
613
+
614
+ class Puppeteer::MappedLocator < Puppeteer::DelegatedLocator
615
+ def initialize(base, mapper)
616
+ super(base)
617
+ @mapper = mapper
618
+ end
619
+
620
+ protected def _clone
621
+ self.class.new(delegate.clone, @mapper).copy_options(self)
622
+ end
623
+
624
+ protected def _wait(options)
625
+ handle = delegate.send(:_wait, options)
626
+ @mapper.call(handle, options)
627
+ end
628
+ end
629
+
630
+ class Puppeteer::NodeLocator < Puppeteer::Locator
631
+ # @rbs page_or_frame: Puppeteer::Page | Puppeteer::Frame -- Page or frame
632
+ # @rbs selector: String -- Selector
633
+ # @rbs return: Puppeteer::Locator -- Node locator
634
+ def self.create(page_or_frame, selector)
635
+ new(page_or_frame, selector).set_timeout(default_timeout_for(page_or_frame))
636
+ end
637
+
638
+ # @rbs page_or_frame: Puppeteer::Page | Puppeteer::Frame -- Page or frame
639
+ # @rbs handle: Puppeteer::ElementHandle -- Element handle
640
+ # @rbs return: Puppeteer::Locator -- Node locator
641
+ def self.create_from_handle(page_or_frame, handle)
642
+ new(page_or_frame, handle).set_timeout(default_timeout_for(page_or_frame))
643
+ end
644
+
645
+ def initialize(page_or_frame, selector_or_handle)
646
+ super()
647
+ @page_or_frame = page_or_frame
648
+ @selector_or_handle = selector_or_handle
649
+ end
650
+
651
+ protected def _clone
652
+ self.class.new(@page_or_frame, @selector_or_handle).copy_options(self)
653
+ end
654
+
655
+ protected def _wait(options)
656
+ handle = if @selector_or_handle.is_a?(String)
657
+ selector = @selector_or_handle
658
+ @page_or_frame.wait_for_selector(selector, visible: false, timeout: options[:timeout])
659
+ else
660
+ @selector_or_handle
661
+ end
662
+
663
+ raise Puppeteer::Error.new('No element found for selector') unless handle
664
+
665
+ wait_for_visibility_if_needed(handle, options)
666
+ handle
667
+ end
668
+
669
+ private def wait_for_visibility_if_needed(handle, options)
670
+ return unless @visibility
671
+
672
+ loop do
673
+ case @visibility
674
+ when 'visible'
675
+ return if handle.visible?
676
+ when 'hidden'
677
+ return if handle.hidden?
678
+ end
679
+
680
+ options[:timeout_controller].check!
681
+ Puppeteer::AsyncUtils.sleep_seconds(RETRY_DELAY_SECONDS)
682
+ end
683
+ end
684
+
685
+ def self.default_timeout_for(page_or_frame)
686
+ if page_or_frame.respond_to?(:default_timeout)
687
+ page_or_frame.default_timeout
688
+ else
689
+ page_or_frame.page.default_timeout
690
+ end
691
+ end
692
+
693
+ private_class_method :default_timeout_for
694
+ end
695
+
696
+ class Puppeteer::RaceLocator < Puppeteer::Locator
697
+ def self.create(locators)
698
+ array = check_locator_array(locators)
699
+ new(array)
700
+ end
701
+
702
+ def initialize(locators)
703
+ super()
704
+ @locators = locators
705
+ end
706
+
707
+ protected def _clone
708
+ self.class.new(@locators.map(&:clone)).copy_options(self)
709
+ end
710
+
711
+ protected def _wait(options)
712
+ tasks = @locators.map do |locator|
713
+ proc { locator.send(:_wait, options) }
714
+ end
715
+ Puppeteer::AsyncUtils.await_promise_race(*tasks)
716
+ end
717
+
718
+ def self.check_locator_array(locators)
719
+ unless locators.is_a?(Array)
720
+ raise ArgumentError.new('Unknown locator for race candidate')
721
+ end
722
+
723
+ locators.each do |locator|
724
+ unless locator.is_a?(Puppeteer::Locator)
725
+ raise ArgumentError.new('Unknown locator for race candidate')
726
+ end
727
+ end
728
+
729
+ locators
730
+ end
731
+
732
+ private_class_method :check_locator_array
733
+ end