puppeteer-bidi 0.0.1.beta10 → 0.0.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +44 -0
  3. data/API_COVERAGE.md +345 -0
  4. data/CLAUDE/porting_puppeteer.md +20 -0
  5. data/CLAUDE.md +2 -1
  6. data/DEVELOPMENT.md +14 -0
  7. data/README.md +47 -415
  8. data/development/generate_api_coverage.rb +411 -0
  9. data/development/puppeteer_revision.txt +1 -0
  10. data/lib/puppeteer/bidi/browser.rb +118 -22
  11. data/lib/puppeteer/bidi/browser_context.rb +185 -2
  12. data/lib/puppeteer/bidi/connection.rb +16 -5
  13. data/lib/puppeteer/bidi/cookie_utils.rb +192 -0
  14. data/lib/puppeteer/bidi/core/browsing_context.rb +83 -40
  15. data/lib/puppeteer/bidi/core/realm.rb +6 -0
  16. data/lib/puppeteer/bidi/core/request.rb +79 -35
  17. data/lib/puppeteer/bidi/core/user_context.rb +5 -3
  18. data/lib/puppeteer/bidi/element_handle.rb +200 -8
  19. data/lib/puppeteer/bidi/errors.rb +4 -0
  20. data/lib/puppeteer/bidi/frame.rb +115 -11
  21. data/lib/puppeteer/bidi/http_request.rb +577 -0
  22. data/lib/puppeteer/bidi/http_response.rb +161 -10
  23. data/lib/puppeteer/bidi/locator.rb +792 -0
  24. data/lib/puppeteer/bidi/page.rb +859 -7
  25. data/lib/puppeteer/bidi/query_handler.rb +1 -1
  26. data/lib/puppeteer/bidi/version.rb +1 -1
  27. data/lib/puppeteer/bidi.rb +39 -6
  28. data/sig/puppeteer/bidi/browser.rbs +53 -6
  29. data/sig/puppeteer/bidi/browser_context.rbs +36 -0
  30. data/sig/puppeteer/bidi/cookie_utils.rbs +64 -0
  31. data/sig/puppeteer/bidi/core/browsing_context.rbs +16 -6
  32. data/sig/puppeteer/bidi/core/request.rbs +14 -11
  33. data/sig/puppeteer/bidi/core/user_context.rbs +2 -2
  34. data/sig/puppeteer/bidi/element_handle.rbs +28 -0
  35. data/sig/puppeteer/bidi/errors.rbs +4 -0
  36. data/sig/puppeteer/bidi/frame.rbs +17 -0
  37. data/sig/puppeteer/bidi/http_request.rbs +162 -0
  38. data/sig/puppeteer/bidi/http_response.rbs +67 -8
  39. data/sig/puppeteer/bidi/locator.rbs +267 -0
  40. data/sig/puppeteer/bidi/page.rbs +170 -0
  41. data/sig/puppeteer/bidi.rbs +15 -3
  42. metadata +12 -1
@@ -0,0 +1,792 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Puppeteer
5
+ module Bidi
6
+ # Visibility options for locators.
7
+ module VisibilityOption
8
+ HIDDEN = "hidden"
9
+ VISIBLE = "visible"
10
+ end
11
+
12
+ # Events emitted by locators.
13
+ module LocatorEvent
14
+ ACTION = :action
15
+ end
16
+
17
+ class RetryableError < StandardError; end
18
+
19
+ # Locators describe a strategy of locating objects and performing an action on them.
20
+ # Actions are retried when the element is not ready.
21
+ class Locator
22
+ RETRY_DELAY = 0.1
23
+
24
+ attr_reader :timeout #: Numeric
25
+
26
+ def initialize
27
+ @visibility = nil
28
+ @timeout = 30_000
29
+ @ensure_element_is_in_viewport = true
30
+ @wait_for_enabled = true
31
+ @wait_for_stable_bounding_box = true
32
+ @emitter = Core::EventEmitter.new
33
+ end
34
+
35
+ # Create a race between multiple locators.
36
+ # @rbs *locators: Array[Locator] -- Locators to race
37
+ # @rbs return: Locator -- Locator that resolves to the first match
38
+ def self.race(*locators)
39
+ locators = locators.first if locators.length == 1 && locators.first.is_a?(Array)
40
+ locators.each do |locator|
41
+ raise Error, "Unknown locator for race candidate" unless locator.is_a?(Locator)
42
+ end
43
+ RaceLocator.new(locators)
44
+ end
45
+
46
+ # Register an event listener.
47
+ # @rbs event: Symbol | String -- Event name
48
+ # @rbs &block: (untyped) -> void -- Event handler
49
+ # @rbs return: Locator -- This locator
50
+ def on(event, &block)
51
+ @emitter.on(event, &block)
52
+ self
53
+ end
54
+
55
+ # Register a one-time event listener.
56
+ # @rbs event: Symbol | String -- Event name
57
+ # @rbs &block: (untyped) -> void -- Event handler
58
+ # @rbs return: Locator -- This locator
59
+ def once(event, &block)
60
+ @emitter.once(event, &block)
61
+ self
62
+ end
63
+
64
+ # Remove an event listener.
65
+ # @rbs event: Symbol | String -- Event name
66
+ # @rbs &block: ((untyped) -> void)? -- Handler to remove
67
+ # @rbs return: Locator -- This locator
68
+ def off(event, &block)
69
+ @emitter.off(event, &block)
70
+ self
71
+ end
72
+
73
+ # Clone the locator.
74
+ # @rbs return: Locator -- Cloned locator
75
+ def clone
76
+ _clone
77
+ end
78
+
79
+ # Set the total timeout for locator actions.
80
+ # Pass 0 to disable timeout.
81
+ # @rbs timeout: Numeric -- Timeout in ms
82
+ # @rbs return: Locator -- Cloned locator with timeout
83
+ def set_timeout(timeout)
84
+ locator = _clone
85
+ locator.send(:timeout=, timeout)
86
+ locator
87
+ end
88
+
89
+ # Set visibility checks for the locator.
90
+ # @rbs visibility: String? -- Visibility option ("hidden", "visible", or nil)
91
+ # @rbs return: Locator -- Cloned locator with visibility option
92
+ def set_visibility(visibility)
93
+ locator = _clone
94
+ locator.send(:visibility=, visibility&.to_s)
95
+ locator
96
+ end
97
+
98
+ # Set whether to wait for elements to become enabled.
99
+ # @rbs value: bool -- Whether to wait for enabled state
100
+ # @rbs return: Locator -- Cloned locator with enabled check
101
+ def set_wait_for_enabled(value)
102
+ locator = _clone
103
+ locator.send(:wait_for_enabled=, value)
104
+ locator
105
+ end
106
+
107
+ # Set whether to ensure elements are in the viewport.
108
+ # @rbs value: bool -- Whether to ensure viewport visibility
109
+ # @rbs return: Locator -- Cloned locator with viewport check
110
+ def set_ensure_element_is_in_the_viewport(value)
111
+ locator = _clone
112
+ locator.send(:ensure_element_is_in_viewport=, value)
113
+ locator
114
+ end
115
+
116
+ # Set whether to wait for a stable bounding box.
117
+ # @rbs value: bool -- Whether to wait for stable bounding box
118
+ # @rbs return: Locator -- Cloned locator with stable bounding box check
119
+ def set_wait_for_stable_bounding_box(value)
120
+ locator = _clone
121
+ locator.send(:wait_for_stable_bounding_box=, value)
122
+ locator
123
+ end
124
+
125
+ # Wait for the locator to produce a handle.
126
+ # @rbs return: JSHandle -- Handle to located value
127
+ def wait_handle
128
+ with_retry("Locator.wait_handle") do |deadline, remaining_ms|
129
+ _wait(timeout_ms: remaining_ms, deadline: deadline)
130
+ end
131
+ end
132
+
133
+ # Wait for the locator to produce a JSON-serializable value.
134
+ # @rbs return: untyped -- JSON-serializable value
135
+ def wait
136
+ handle = wait_handle
137
+ begin
138
+ handle.json_value
139
+ ensure
140
+ handle.dispose if handle.respond_to?(:dispose)
141
+ end
142
+ end
143
+
144
+ # Map the locator using a JavaScript mapper.
145
+ # @rbs mapper: String -- JavaScript mapper function
146
+ # @rbs &block: () -> String -- Optional block returning mapper string
147
+ # @rbs return: Locator -- Mapped locator
148
+ def map(mapper = nil, &block)
149
+ mapper = mapper || block&.call
150
+ raise ArgumentError, "mapper is required" unless mapper
151
+
152
+ map_handle(mapper)
153
+ end
154
+
155
+ # Filter the locator using a JavaScript predicate.
156
+ # @rbs predicate: String -- JavaScript predicate function
157
+ # @rbs &block: () -> String -- Optional block returning predicate string
158
+ # @rbs return: Locator -- Filtered locator
159
+ def filter(predicate = nil, &block)
160
+ predicate = predicate || block&.call
161
+ raise ArgumentError, "predicate is required" unless predicate
162
+
163
+ FilteredLocator.new(_clone, predicate)
164
+ end
165
+
166
+ # Click the located element.
167
+ # @rbs button: String -- Mouse button ('left', 'right', 'middle')
168
+ # @rbs count: Integer -- Number of clicks
169
+ # @rbs delay: Numeric? -- Delay between clicks in ms
170
+ # @rbs offset: Hash[Symbol, Numeric]? -- Click offset from element center
171
+ # @rbs return: void
172
+ def click(button: "left", count: 1, delay: nil, offset: nil)
173
+ perform_action("Locator.click", wait_for_enabled: true) do |handle|
174
+ handle.click(button: button, count: count, delay: delay, offset: offset)
175
+ nil
176
+ end
177
+ end
178
+
179
+ # Fill the located element with the provided value.
180
+ # @rbs value: String -- Value to fill
181
+ # @rbs return: void
182
+ def fill(value)
183
+ perform_action("Locator.fill", wait_for_enabled: true) do |handle|
184
+ input_type = handle.evaluate(<<~JS)
185
+ (el) => {
186
+ if (el instanceof HTMLSelectElement) {
187
+ return "select";
188
+ }
189
+ if (el instanceof HTMLTextAreaElement) {
190
+ return "typeable-input";
191
+ }
192
+ if (el instanceof HTMLInputElement) {
193
+ if (
194
+ new Set([
195
+ "textarea",
196
+ "text",
197
+ "url",
198
+ "tel",
199
+ "search",
200
+ "password",
201
+ "number",
202
+ "email",
203
+ ]).has(el.type)
204
+ ) {
205
+ return "typeable-input";
206
+ }
207
+ return "other-input";
208
+ }
209
+
210
+ if (el.isContentEditable) {
211
+ return "contenteditable";
212
+ }
213
+
214
+ return "unknown";
215
+ }
216
+ JS
217
+
218
+ case input_type
219
+ when "select"
220
+ handle.select(value)
221
+ when "contenteditable", "typeable-input"
222
+ text_to_type = handle.evaluate(<<~JS, value)
223
+ (input, newValue) => {
224
+ const currentValue = input.isContentEditable
225
+ ? input.innerText
226
+ : input.value;
227
+
228
+ if (
229
+ newValue.length <= currentValue.length ||
230
+ !newValue.startsWith(input.value)
231
+ ) {
232
+ if (input.isContentEditable) {
233
+ input.innerText = "";
234
+ } else {
235
+ input.value = "";
236
+ }
237
+ return newValue;
238
+ }
239
+ const originalValue = input.isContentEditable
240
+ ? input.innerText
241
+ : input.value;
242
+
243
+ if (input.isContentEditable) {
244
+ input.innerText = "";
245
+ input.innerText = originalValue;
246
+ } else {
247
+ input.value = "";
248
+ input.value = originalValue;
249
+ }
250
+ return newValue.substring(originalValue.length);
251
+ }
252
+ JS
253
+ handle.type(text_to_type)
254
+ when "other-input"
255
+ handle.focus
256
+ handle.evaluate(<<~JS, value)
257
+ (input, newValue) => {
258
+ input.value = newValue;
259
+ input.dispatchEvent(new Event("input", {bubbles: true}));
260
+ input.dispatchEvent(new Event("change", {bubbles: true}));
261
+ }
262
+ JS
263
+ else
264
+ raise StandardError, "Element cannot be filled out."
265
+ end
266
+ nil
267
+ end
268
+ end
269
+
270
+ # Hover over the located element.
271
+ # @rbs return: void
272
+ def hover
273
+ perform_action("Locator.hover") do |handle|
274
+ handle.hover
275
+ nil
276
+ end
277
+ end
278
+
279
+ # Scroll the located element.
280
+ # @rbs scroll_top: Numeric? -- Scroll top offset
281
+ # @rbs scroll_left: Numeric? -- Scroll left offset
282
+ # @rbs return: void
283
+ def scroll(scroll_top: nil, scroll_left: nil)
284
+ perform_action("Locator.scroll") do |handle|
285
+ handle.evaluate(<<~JS, scroll_top, scroll_left)
286
+ (el, scrollTop, scrollLeft) => {
287
+ if (scrollTop !== undefined && scrollTop !== null) {
288
+ el.scrollTop = scrollTop;
289
+ }
290
+ if (scrollLeft !== undefined && scrollLeft !== null) {
291
+ el.scrollLeft = scrollLeft;
292
+ }
293
+ }
294
+ JS
295
+ nil
296
+ end
297
+ end
298
+
299
+ protected
300
+
301
+ def copy_options(locator)
302
+ @timeout = locator.timeout
303
+ @visibility = locator.visibility
304
+ @wait_for_enabled = locator.wait_for_enabled?
305
+ @ensure_element_is_in_viewport = locator.ensure_element_is_in_viewport?
306
+ @wait_for_stable_bounding_box = locator.wait_for_stable_bounding_box?
307
+ self
308
+ end
309
+
310
+ def visibility
311
+ @visibility
312
+ end
313
+
314
+ def wait_for_enabled?
315
+ @wait_for_enabled
316
+ end
317
+
318
+ def ensure_element_is_in_viewport?
319
+ @ensure_element_is_in_viewport
320
+ end
321
+
322
+ def wait_for_stable_bounding_box?
323
+ @wait_for_stable_bounding_box
324
+ end
325
+
326
+ def map_handle(mapper)
327
+ MappedLocator.new(_clone, mapper)
328
+ end
329
+
330
+ def _clone
331
+ raise NotImplementedError, "#{self.class}#_clone must be implemented"
332
+ end
333
+
334
+ def _wait(timeout_ms:, deadline:)
335
+ raise NotImplementedError, "#{self.class}#_wait must be implemented"
336
+ end
337
+
338
+ private
339
+
340
+ attr_writer :timeout,
341
+ :visibility,
342
+ :wait_for_enabled,
343
+ :ensure_element_is_in_viewport,
344
+ :wait_for_stable_bounding_box
345
+
346
+ # @rbs cause: String -- Action name
347
+ # @rbs wait_for_enabled: bool -- Whether to wait for enabled
348
+ # @rbs &block: (ElementHandle) -> void -- Action to perform
349
+ # @rbs return: void
350
+ def perform_action(cause, wait_for_enabled: false, &block)
351
+ with_retry(cause) do |deadline, remaining_ms|
352
+ handle = _wait(timeout_ms: remaining_ms, deadline: deadline)
353
+ begin
354
+ ensure_element_is_in_viewport_if_needed(handle, deadline)
355
+ wait_for_stable_bounding_box_if_needed(handle, deadline)
356
+ wait_for_enabled_if_needed(handle, deadline) if wait_for_enabled
357
+ @emitter.emit(LocatorEvent::ACTION, nil)
358
+ block.call(handle)
359
+ rescue StandardError => error
360
+ handle.dispose if handle.respond_to?(:dispose)
361
+ raise error
362
+ end
363
+ end
364
+ end
365
+
366
+ # @rbs cause: String -- Action name
367
+ # @rbs &block: (Numeric?, Numeric?) -> untyped -- Retry block
368
+ # @rbs return: untyped
369
+ def with_retry(cause, &block)
370
+ deadline = build_deadline
371
+ loop do
372
+ remaining_ms = deadline ? remaining_time_ms(deadline) : nil
373
+ raise_timeout if deadline && remaining_ms <= 0
374
+ begin
375
+ return block.call(deadline, remaining_ms)
376
+ rescue TimeoutError
377
+ raise
378
+ rescue StandardError
379
+ raise_timeout if deadline && remaining_time_ms(deadline) <= 0
380
+ sleep RETRY_DELAY
381
+ end
382
+ end
383
+ end
384
+
385
+ def build_deadline
386
+ return nil if @timeout.nil? || @timeout == 0
387
+
388
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) + (@timeout / 1000.0)
389
+ end
390
+
391
+ def remaining_time_ms(deadline)
392
+ ((deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)) * 1000.0).ceil
393
+ end
394
+
395
+ def raise_timeout
396
+ raise TimeoutError, "Timed out after waiting #{@timeout}ms"
397
+ end
398
+
399
+ # @rbs deadline: Numeric? -- Deadline timestamp
400
+ # @rbs &block: () -> boolish -- Condition block
401
+ # @rbs return: void
402
+ def wait_until(deadline, &block)
403
+ loop do
404
+ return if block.call
405
+ raise_timeout if deadline && remaining_time_ms(deadline) <= 0
406
+ sleep RETRY_DELAY
407
+ end
408
+ end
409
+
410
+ def ensure_element_is_in_viewport_if_needed(handle, deadline)
411
+ return unless @ensure_element_is_in_viewport
412
+
413
+ wait_until(deadline) do
414
+ next true if handle.intersecting_viewport?(threshold: 0)
415
+
416
+ handle.scroll_into_view
417
+ false
418
+ end
419
+ end
420
+
421
+ def wait_for_stable_bounding_box_if_needed(handle, deadline)
422
+ return unless @wait_for_stable_bounding_box
423
+
424
+ wait_until(deadline) do
425
+ rects = handle.evaluate(<<~JS)
426
+ (element) => {
427
+ return new Promise(resolve => {
428
+ window.requestAnimationFrame(() => {
429
+ const rect1 = element.getBoundingClientRect();
430
+ window.requestAnimationFrame(() => {
431
+ const rect2 = element.getBoundingClientRect();
432
+ resolve([
433
+ {
434
+ x: rect1.x,
435
+ y: rect1.y,
436
+ width: rect1.width,
437
+ height: rect1.height,
438
+ },
439
+ {
440
+ x: rect2.x,
441
+ y: rect2.y,
442
+ width: rect2.width,
443
+ height: rect2.height,
444
+ },
445
+ ]);
446
+ });
447
+ });
448
+ });
449
+ }
450
+ JS
451
+
452
+ rect1 = rects[0]
453
+ rect2 = rects[1]
454
+ rect1["x"] == rect2["x"] &&
455
+ rect1["y"] == rect2["y"] &&
456
+ rect1["width"] == rect2["width"] &&
457
+ rect1["height"] == rect2["height"]
458
+ end
459
+ end
460
+
461
+ def wait_for_enabled_if_needed(handle, deadline)
462
+ return unless @wait_for_enabled
463
+
464
+ wait_until(deadline) do
465
+ handle.evaluate(<<~JS)
466
+ (element) => {
467
+ if (!(element instanceof HTMLElement)) {
468
+ return true;
469
+ }
470
+ const isNativeFormControl = [
471
+ "BUTTON",
472
+ "INPUT",
473
+ "SELECT",
474
+ "TEXTAREA",
475
+ "OPTION",
476
+ "OPTGROUP",
477
+ ].includes(element.nodeName);
478
+ return !isNativeFormControl || !element.hasAttribute("disabled");
479
+ }
480
+ JS
481
+ end
482
+ end
483
+ end
484
+
485
+ # Locator implementation based on JavaScript functions.
486
+ class FunctionLocator < Locator
487
+ # @rbs page_or_frame: Page | Frame -- Page or frame to evaluate in
488
+ # @rbs function: String -- JavaScript function to evaluate
489
+ # @rbs return: Locator -- Function locator
490
+ def self.create(page_or_frame, function)
491
+ timeout = if page_or_frame.respond_to?(:default_timeout)
492
+ page_or_frame.default_timeout
493
+ else
494
+ page_or_frame.page.default_timeout
495
+ end
496
+ new(page_or_frame, function).set_timeout(timeout)
497
+ end
498
+
499
+ def initialize(page_or_frame, function)
500
+ super()
501
+ @page_or_frame = page_or_frame
502
+ @function = function
503
+ end
504
+
505
+ protected
506
+
507
+ def _clone
508
+ FunctionLocator.new(@page_or_frame, @function).copy_options(self)
509
+ end
510
+
511
+ def _wait(timeout_ms:, deadline:)
512
+ handle = @page_or_frame.evaluate_handle(@function)
513
+ begin
514
+ truthy = handle.evaluate("(value) => Boolean(value)")
515
+ raise RetryableError unless truthy
516
+ handle
517
+ rescue StandardError
518
+ handle.dispose
519
+ raise
520
+ end
521
+ end
522
+ end
523
+
524
+ # Abstract locator that delegates to another locator.
525
+ class DelegatedLocator < Locator
526
+ def initialize(delegate)
527
+ super()
528
+ @delegate = delegate
529
+ copy_options(@delegate)
530
+ end
531
+
532
+ def set_timeout(timeout)
533
+ locator = super
534
+ locator.delegate = @delegate.set_timeout(timeout)
535
+ locator
536
+ end
537
+
538
+ def set_visibility(visibility)
539
+ locator = super
540
+ locator.delegate = @delegate.set_visibility(visibility)
541
+ locator
542
+ end
543
+
544
+ def set_wait_for_enabled(value)
545
+ locator = super
546
+ locator.delegate = @delegate.set_wait_for_enabled(value)
547
+ locator
548
+ end
549
+
550
+ def set_ensure_element_is_in_the_viewport(value)
551
+ locator = super
552
+ locator.delegate = @delegate.set_ensure_element_is_in_the_viewport(value)
553
+ locator
554
+ end
555
+
556
+ def set_wait_for_stable_bounding_box(value)
557
+ locator = super
558
+ locator.delegate = @delegate.set_wait_for_stable_bounding_box(value)
559
+ locator
560
+ end
561
+
562
+ protected
563
+
564
+ attr_accessor :delegate
565
+ end
566
+
567
+ # Locator that filters results using a predicate.
568
+ class FilteredLocator < DelegatedLocator
569
+ def initialize(delegate, predicate)
570
+ super(delegate)
571
+ @predicate = predicate
572
+ end
573
+
574
+ protected
575
+
576
+ def _clone
577
+ FilteredLocator.new(delegate.clone, @predicate).copy_options(self)
578
+ end
579
+
580
+ def _wait(timeout_ms:, deadline:)
581
+ handle = delegate.__send__(:_wait, timeout_ms: timeout_ms, deadline: deadline)
582
+ matched = handle.evaluate(@predicate)
583
+ raise RetryableError unless matched
584
+
585
+ handle
586
+ end
587
+ end
588
+
589
+ # Locator that maps results using a mapper.
590
+ class MappedLocator < DelegatedLocator
591
+ def initialize(delegate, mapper)
592
+ super(delegate)
593
+ @mapper = mapper
594
+ end
595
+
596
+ protected
597
+
598
+ def _clone
599
+ MappedLocator.new(delegate.clone, @mapper).copy_options(self)
600
+ end
601
+
602
+ def _wait(timeout_ms:, deadline:)
603
+ handle = delegate.__send__(:_wait, timeout_ms: timeout_ms, deadline: deadline)
604
+ handle.evaluate_handle(@mapper)
605
+ end
606
+ end
607
+
608
+ # Locator that queries nodes by selector or handle.
609
+ class NodeLocator < Locator
610
+ # @rbs page_or_frame: Page | Frame -- Page or frame to query
611
+ # @rbs selector: String -- Selector to query
612
+ # @rbs return: Locator -- Node locator
613
+ def self.create(page_or_frame, selector)
614
+ timeout = if page_or_frame.respond_to?(:default_timeout)
615
+ page_or_frame.default_timeout
616
+ else
617
+ page_or_frame.page.default_timeout
618
+ end
619
+ new(page_or_frame, selector).set_timeout(timeout)
620
+ end
621
+
622
+ # @rbs page_or_frame: Page | Frame -- Page or frame to query
623
+ # @rbs handle: ElementHandle -- Element handle to wrap
624
+ # @rbs return: Locator -- Node locator
625
+ def self.create_from_handle(page_or_frame, handle)
626
+ timeout = if page_or_frame.respond_to?(:default_timeout)
627
+ page_or_frame.default_timeout
628
+ else
629
+ page_or_frame.page.default_timeout
630
+ end
631
+ new(page_or_frame, handle).set_timeout(timeout)
632
+ end
633
+
634
+ def initialize(page_or_frame, selector_or_handle)
635
+ super()
636
+ @page_or_frame = page_or_frame
637
+ @selector_or_handle = selector_or_handle
638
+ end
639
+
640
+ protected
641
+
642
+ def _clone
643
+ NodeLocator.new(@page_or_frame, @selector_or_handle).copy_options(self)
644
+ end
645
+
646
+ def _wait(timeout_ms:, deadline:)
647
+ handle = if @selector_or_handle.is_a?(String)
648
+ query_selector_with_pseudo_selectors(@selector_or_handle)
649
+ else
650
+ @selector_or_handle
651
+ end
652
+
653
+ raise RetryableError unless handle
654
+ raise RetryableError if handle.respond_to?(:disposed?) && handle.disposed?
655
+
656
+ if visibility
657
+ matches_visibility = case visibility
658
+ when VisibilityOption::VISIBLE
659
+ handle.visible?
660
+ when VisibilityOption::HIDDEN
661
+ handle.hidden?
662
+ else
663
+ true
664
+ end
665
+ unless matches_visibility
666
+ handle.dispose if @selector_or_handle.is_a?(String) && handle.respond_to?(:dispose)
667
+ raise RetryableError
668
+ end
669
+ end
670
+
671
+ handle
672
+ end
673
+
674
+ def query_selector_with_pseudo_selectors(selector)
675
+ candidates = p_selector_candidates(selector)
676
+ candidates.each do |candidate|
677
+ handle = @page_or_frame.query_selector(candidate)
678
+ return handle if handle
679
+ end
680
+ nil
681
+ end
682
+
683
+ def p_selector_candidates(selector)
684
+ return [selector] unless selector.include?("::-p-")
685
+
686
+ parts = split_selector_list(selector)
687
+ candidates = parts.filter_map do |part|
688
+ part = part.strip
689
+ match = part.match(/\A::\-p\-(text|xpath)\((.*)\)\z/)
690
+ next unless match
691
+
692
+ name = match[1]
693
+ value = unquote_selector_value(match[2])
694
+ prefix = name == "text" ? "text/" : "xpath/"
695
+ "#{prefix}#{value}"
696
+ end
697
+
698
+ candidates.empty? ? [selector] : candidates
699
+ end
700
+
701
+ def split_selector_list(selector)
702
+ parts = []
703
+ current = +""
704
+ depth = 0
705
+ in_string = nil
706
+ escape = false
707
+
708
+ selector.each_char do |char|
709
+ if escape
710
+ current << char
711
+ escape = false
712
+ next
713
+ end
714
+
715
+ if in_string
716
+ if char == "\\"
717
+ escape = true
718
+ elsif char == in_string
719
+ in_string = nil
720
+ end
721
+ current << char
722
+ next
723
+ end
724
+
725
+ case char
726
+ when "'", '"'
727
+ in_string = char
728
+ when '('
729
+ depth += 1
730
+ when ')'
731
+ depth -= 1 if depth.positive?
732
+ when ','
733
+ if depth.zero?
734
+ parts << current
735
+ current = +""
736
+ next
737
+ end
738
+ end
739
+
740
+ current << char
741
+ end
742
+
743
+ parts << current unless current.empty?
744
+ parts
745
+ end
746
+
747
+ def unquote_selector_value(value)
748
+ stripped = value.strip
749
+ return stripped if stripped.length < 2
750
+
751
+ quote = stripped[0]
752
+ return stripped unless quote == "'" || quote == '"'
753
+ return stripped unless stripped.end_with?(quote)
754
+
755
+ stripped[1..-2].gsub(/\\([\s\S])/, '\1')
756
+ end
757
+ end
758
+
759
+ # Locator that races multiple locators.
760
+ class RaceLocator < Locator
761
+ def initialize(locators)
762
+ super()
763
+ @locators = locators
764
+ end
765
+
766
+ protected
767
+
768
+ def _clone
769
+ RaceLocator.new(@locators.map(&:clone)).copy_options(self)
770
+ end
771
+
772
+ def _wait(timeout_ms:, deadline:)
773
+ found = nil
774
+
775
+ wait_until(deadline) do
776
+ @locators.each do |locator|
777
+ begin
778
+ found = locator.__send__(:_wait, timeout_ms: timeout_ms, deadline: deadline)
779
+ break
780
+ rescue RetryableError
781
+ next
782
+ end
783
+ end
784
+
785
+ !found.nil?
786
+ end
787
+
788
+ found
789
+ end
790
+ end
791
+ end
792
+ end