Class: Puppeteer::Bidi::Locator

Inherits:
Object
  • Object
show all
Defined in:
lib/puppeteer/bidi/locator.rb,
sig/puppeteer/bidi/locator.rbs

Overview

Locators describe a strategy of locating objects and performing an action on them. Actions are retried when the element is not ready.

Constant Summary collapse

RETRY_DELAY =

Returns:

  • (::Float)
0.1

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeLocator

Returns a new instance of Locator.



26
27
28
29
30
31
32
33
# File 'lib/puppeteer/bidi/locator.rb', line 26

def initialize
  @visibility = nil
  @timeout = 30_000
  @ensure_element_is_in_viewport = true
  @wait_for_enabled = true
  @wait_for_stable_bounding_box = true
  @emitter = Core::EventEmitter.new
end

Instance Attribute Details

#ensure_element_is_in_viewport=(value) ⇒ Object (writeonly)

Sets the attribute ensure_element_is_in_viewport

Parameters:

  • value (Object)

    the value to set the attribute ensure_element_is_in_viewport to.



366
367
368
# File 'lib/puppeteer/bidi/locator.rb', line 366

def ensure_element_is_in_viewport=(value)
  @ensure_element_is_in_viewport = value
end

#timeoutNumeric

: Numeric

Returns:

  • (Numeric)


24
25
26
# File 'lib/puppeteer/bidi/locator.rb', line 24

def timeout
  @timeout
end

#visibilityObject

Returns:

  • (Object)


336
337
338
# File 'lib/puppeteer/bidi/locator.rb', line 336

def visibility
  @visibility
end

#wait_for_enabled=(value) ⇒ Object (writeonly)

Sets the attribute wait_for_enabled

Parameters:

  • value (Object)

    the value to set the attribute wait_for_enabled to.



366
367
368
# File 'lib/puppeteer/bidi/locator.rb', line 366

def wait_for_enabled=(value)
  @wait_for_enabled = value
end

#wait_for_stable_bounding_box=(value) ⇒ Object (writeonly)

Sets the attribute wait_for_stable_bounding_box

Parameters:

  • value (Object)

    the value to set the attribute wait_for_stable_bounding_box to.



366
367
368
# File 'lib/puppeteer/bidi/locator.rb', line 366

def wait_for_stable_bounding_box=(value)
  @wait_for_stable_bounding_box = value
end

Class Method Details

.race(*locators) ⇒ Locator

Create a race between multiple locators.

Parameters:

Returns:



38
39
40
41
42
43
44
# File 'lib/puppeteer/bidi/locator.rb', line 38

def self.race(*locators)
  locators = locators.first if locators.length == 1 && locators.first.is_a?(Array)
  locators.each do |locator|
    raise Error, "Unknown locator for race candidate" unless locator.is_a?(Locator)
  end
  RaceLocator.new(locators)
end

Instance Method Details

#_cloneObject

Returns:

  • (Object)

Raises:

  • (NotImplementedError)


356
357
358
# File 'lib/puppeteer/bidi/locator.rb', line 356

def _clone
  raise NotImplementedError, "#{self.class}#_clone must be implemented"
end

#_wait(timeout_ms:, deadline:) ⇒ Object

Parameters:

  • timeout_ms: (Object)
  • deadline: (Object)

Returns:

  • (Object)

Raises:

  • (NotImplementedError)


360
361
362
# File 'lib/puppeteer/bidi/locator.rb', line 360

def _wait(timeout_ms:, deadline:)
  raise NotImplementedError, "#{self.class}#_wait must be implemented"
end

#build_deadlineObject

Returns:

  • (Object)


433
434
435
436
437
# File 'lib/puppeteer/bidi/locator.rb', line 433

def build_deadline
  return nil if @timeout.nil? || @timeout == 0

  Process.clock_gettime(Process::CLOCK_MONOTONIC) + (@timeout / 1000.0)
end

#click(button: "left", count: 1, delay: nil, offset: nil) ⇒ void

This method returns an undefined value.

Click the located element.

Parameters:

  • button: (String) (defaults to: "left")
  • count: (Integer) (defaults to: 1)
  • delay: (Numeric, nil) (defaults to: nil)
  • offset: (Hash[Symbol, Numeric], nil) (defaults to: nil)


172
173
174
175
176
177
# File 'lib/puppeteer/bidi/locator.rb', line 172

def click(button: "left", count: 1, delay: nil, offset: nil)
  perform_action("Locator.click", wait_for_enabled: true) do |handle|
    handle.click(button: button, count: count, delay: delay, offset: offset)
    nil
  end
end

#cloneLocator

Clone the locator.

Returns:



75
76
77
# File 'lib/puppeteer/bidi/locator.rb', line 75

def clone
  _clone
end

#copy_options(locator) ⇒ Object

Parameters:

  • locator (Object)

Returns:

  • (Object)


327
328
329
330
331
332
333
334
# File 'lib/puppeteer/bidi/locator.rb', line 327

def copy_options(locator)
  @timeout = locator.timeout
  @visibility = locator.visibility
  @wait_for_enabled = locator.wait_for_enabled?
  @ensure_element_is_in_viewport = locator.ensure_element_is_in_viewport?
  @wait_for_stable_bounding_box = locator.wait_for_stable_bounding_box?
  self
end

#ensure_element_is_in_viewport?Boolean

Returns:

  • (Boolean)


344
345
346
# File 'lib/puppeteer/bidi/locator.rb', line 344

def ensure_element_is_in_viewport?
  @ensure_element_is_in_viewport
end

#ensure_element_is_in_viewport_if_needed(handle, deadline) ⇒ Object

Parameters:

  • handle (Object)
  • deadline (Object)

Returns:

  • (Object)


458
459
460
461
462
463
464
465
466
467
# File 'lib/puppeteer/bidi/locator.rb', line 458

def ensure_element_is_in_viewport_if_needed(handle, deadline)
  return unless @ensure_element_is_in_viewport

  wait_until(deadline) do
    next true if handle.intersecting_viewport?(threshold: 0)

    handle.scroll_into_view
    false
  end
end

#fill(value, typing_threshold: 100) ⇒ void

This method returns an undefined value.

Fill the located element with the provided value.

Parameters:

  • value (String, bool)
  • typing_threshold: (Integer) (defaults to: 100)


183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/puppeteer/bidi/locator.rb', line 183

def fill(value, typing_threshold: 100)
  typing_threshold ||= 100

  perform_action("Locator.fill", wait_for_enabled: true) do |handle|
    input_type = handle.evaluate(<<~JS)
      (el) => {
        if (el instanceof HTMLSelectElement) {
          return "select";
        }
        if (el instanceof HTMLTextAreaElement) {
          return "typeable-input";
        }
        if (el instanceof HTMLInputElement) {
          switch (el.type) {
            case "checkbox":
            case "radio":
              return "checkable-input";
            case "text":
            case "url":
            case "tel":
            case "search":
            case "password":
            case "number":
            case "email":
              return "typeable-input";
            default:
              return "other-input";
          }
        }

        switch (el.getAttribute("role")) {
          case "checkbox":
          case "radio":
          case "switch":
            return "checkable-input";
        }

        if (el.isContentEditable) {
          return "contenteditable";
        }

        return "unknown";
      }
    JS

    case input_type
    when "checkable-input"
      current_state = handle.evaluate(<<~JS)
        (toggleEl) => {
          if (
            toggleEl.indeterminate ||
            toggleEl.getAttribute("aria-checked") === "mixed"
          ) {
            return "mixed";
          }
          return (
            toggleEl.checked ||
            toggleEl.getAttribute("aria-checked") === "true"
          );
        }
      JS
      desired_state = value == true || (value.is_a?(String) && !value.empty?)
      handle.click if current_state == "mixed" || current_state != desired_state
    when "select"
      handle.select(value)
    when "contenteditable", "typeable-input"
      if value.is_a?(String) && value.length < typing_threshold
        text_to_type = handle.evaluate(<<~JS, value)
          (input, newValue) => {
            const valString = String(newValue);
            const currentValue = input.isContentEditable
              ? input.innerText
              : input.value;

            if (currentValue === valString) {
              return "";
            }

            if (
              !valString.startsWith(currentValue) ||
              !currentValue
            ) {
              if (input.isContentEditable) {
                input.innerText = "";
              } else {
                input.value = "";
              }
              return valString;
            }

            if (input.isContentEditable) {
              input.innerText = "";
              input.innerText = currentValue;
            } else {
              input.value = "";
              input.value = currentValue;
            }
            return valString.substring(currentValue.length);
          }
        JS
        handle.type(text_to_type) unless text_to_type.to_s.empty?
      else
        fill_directly(handle, value)
      end
    when "other-input"
      fill_directly(handle, value)
    else
      raise StandardError, "Element cannot be filled out."
    end
    nil
  end
end

#fill_directly(handle, value) ⇒ Object

Parameters:

  • handle (Object)
  • value (Object)

Returns:

  • (Object)


372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/puppeteer/bidi/locator.rb', line 372

def fill_directly(handle, value)
  handle.focus
  handle.evaluate(<<~JS, value)
    (input, newValue) => {
      const valString = String(newValue);
      const currentValue = input.isContentEditable
        ? input.innerText
        : input.value;
      if (currentValue === valString) {
        return;
      }
      if (input.isContentEditable) {
        input.innerText = valString;
      } else {
        input.value = valString;
      }
      input.dispatchEvent(new Event("input", {bubbles: true}));
      input.dispatchEvent(new Event("change", {bubbles: true}));
    }
  JS
end

#filter(predicate = nil) { ... } ⇒ Locator

Filter the locator using a JavaScript predicate.

Parameters:

  • predicate (String) (defaults to: nil)

Yields:

Yield Returns:

  • (String)

Returns:



159
160
161
162
163
164
# File 'lib/puppeteer/bidi/locator.rb', line 159

def filter(predicate = nil, &block)
  predicate = predicate || block&.call
  raise ArgumentError, "predicate is required" unless predicate

  FilteredLocator.new(_clone, predicate)
end

#hovervoid

This method returns an undefined value.

Hover over the located element.



298
299
300
301
302
303
# File 'lib/puppeteer/bidi/locator.rb', line 298

def hover
  perform_action("Locator.hover") do |handle|
    handle.hover
    nil
  end
end

#map(mapper = nil) { ... } ⇒ Locator

Map the locator using a JavaScript mapper.

Parameters:

  • mapper (String) (defaults to: nil)

Yields:

Yield Returns:

  • (String)

Returns:



148
149
150
151
152
153
# File 'lib/puppeteer/bidi/locator.rb', line 148

def map(mapper = nil, &block)
  mapper = mapper || block&.call
  raise ArgumentError, "mapper is required" unless mapper

  map_handle(mapper)
end

#map_handle(mapper) ⇒ Object

Parameters:

  • mapper (Object)

Returns:

  • (Object)


352
353
354
# File 'lib/puppeteer/bidi/locator.rb', line 352

def map_handle(mapper)
  MappedLocator.new(_clone, mapper)
end

#off(event, &block) ⇒ void

This method returns an undefined value.

Remove an event listener.

Parameters:

  • event (Symbol, String)


68
69
70
71
# File 'lib/puppeteer/bidi/locator.rb', line 68

def off(event, &block)
  @emitter.off(event, &block)
  self
end

#on(event) {|arg0| ... } ⇒ Locator

Register an event listener.

Parameters:

  • event (Symbol, String)

Yields:

Yield Parameters:

  • arg0 (Object)

Yield Returns:

  • (void)

Returns:



50
51
52
53
# File 'lib/puppeteer/bidi/locator.rb', line 50

def on(event, &block)
  @emitter.on(event, &block)
  self
end

#once(event) {|arg0| ... } ⇒ Locator

Register a one-time event listener.

Parameters:

  • event (Symbol, String)

Yields:

Yield Parameters:

  • arg0 (Object)

Yield Returns:

  • (void)

Returns:



59
60
61
62
# File 'lib/puppeteer/bidi/locator.rb', line 59

def once(event, &block)
  @emitter.once(event, &block)
  self
end

#perform_action(cause, wait_for_enabled: false) {|arg0| ... } ⇒ void

This method returns an undefined value.

Parameters:

  • cause (String)
  • wait_for_enabled: (Boolean) (defaults to: false)

Yields:

Yield Parameters:

Yield Returns:

  • (void)


398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/puppeteer/bidi/locator.rb', line 398

def perform_action(cause, wait_for_enabled: false, &block)
  with_retry(cause) do |deadline, remaining_ms|
    handle = _wait(timeout_ms: remaining_ms, deadline: deadline)
    begin
      ensure_element_is_in_viewport_if_needed(handle, deadline)
      wait_for_stable_bounding_box_if_needed(handle, deadline)
      wait_for_enabled_if_needed(handle, deadline) if wait_for_enabled
      @emitter.emit(LocatorEvent::ACTION, nil)
      block.call(handle)
    rescue StandardError => error
      handle.dispose if handle.respond_to?(:dispose)
      raise error
    end
  end
end

#raise_timeoutObject

Returns:

  • (Object)

Raises:



443
444
445
# File 'lib/puppeteer/bidi/locator.rb', line 443

def raise_timeout
  raise TimeoutError, "Timed out after waiting #{@timeout}ms"
end

#remaining_time_ms(deadline) ⇒ Object

Parameters:

  • deadline (Object)

Returns:

  • (Object)


439
440
441
# File 'lib/puppeteer/bidi/locator.rb', line 439

def remaining_time_ms(deadline)
  ((deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)) * 1000.0).ceil
end

#scroll(scroll_top: nil, scroll_left: nil) ⇒ void

This method returns an undefined value.

Scroll the located element.

Parameters:

  • scroll_top: (Numeric, nil) (defaults to: nil)
  • scroll_left: (Numeric, nil) (defaults to: nil)


309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/puppeteer/bidi/locator.rb', line 309

def scroll(scroll_top: nil, scroll_left: nil)
  perform_action("Locator.scroll") do |handle|
    handle.evaluate(<<~JS, scroll_top, scroll_left)
      (el, scrollTop, scrollLeft) => {
        if (scrollTop !== undefined && scrollTop !== null) {
          el.scrollTop = scrollTop;
        }
        if (scrollLeft !== undefined && scrollLeft !== null) {
          el.scrollLeft = scrollLeft;
        }
      }
    JS
    nil
  end
end

#set_ensure_element_is_in_the_viewport(value) ⇒ Locator

Set whether to ensure elements are in the viewport.

Parameters:

  • value (Boolean)

Returns:



110
111
112
113
114
# File 'lib/puppeteer/bidi/locator.rb', line 110

def set_ensure_element_is_in_the_viewport(value)
  locator = _clone
  locator.send(:ensure_element_is_in_viewport=, value)
  locator
end

#set_timeout(timeout) ⇒ Locator

Set the total timeout for locator actions. Pass 0 to disable timeout.

Parameters:

  • timeout (Numeric)

Returns:



83
84
85
86
87
# File 'lib/puppeteer/bidi/locator.rb', line 83

def set_timeout(timeout)
  locator = _clone
  locator.send(:timeout=, timeout)
  locator
end

#set_visibility(visibility) ⇒ Locator

Set visibility checks for the locator.

Parameters:

  • visibility (String, nil)

Returns:



92
93
94
95
96
# File 'lib/puppeteer/bidi/locator.rb', line 92

def set_visibility(visibility)
  locator = _clone
  locator.send(:visibility=, visibility&.to_s)
  locator
end

#set_wait_for_enabled(value) ⇒ Locator

Set whether to wait for elements to become enabled.

Parameters:

  • value (Boolean)

Returns:



101
102
103
104
105
# File 'lib/puppeteer/bidi/locator.rb', line 101

def set_wait_for_enabled(value)
  locator = _clone
  locator.send(:wait_for_enabled=, value)
  locator
end

#set_wait_for_stable_bounding_box(value) ⇒ Locator

Set whether to wait for a stable bounding box.

Parameters:

  • value (Boolean)

Returns:



119
120
121
122
123
# File 'lib/puppeteer/bidi/locator.rb', line 119

def set_wait_for_stable_bounding_box(value)
  locator = _clone
  locator.send(:wait_for_stable_bounding_box=, value)
  locator
end

#waitObject

Wait for the locator to produce a JSON-serializable value.

Returns:

  • (Object)


135
136
137
138
139
140
141
142
# File 'lib/puppeteer/bidi/locator.rb', line 135

def wait
  handle = wait_handle
  begin
    handle.json_value
  ensure
    handle.dispose if handle.respond_to?(:dispose)
  end
end

#wait_for_enabled?Boolean

Returns:

  • (Boolean)


340
341
342
# File 'lib/puppeteer/bidi/locator.rb', line 340

def wait_for_enabled?
  @wait_for_enabled
end

#wait_for_enabled_if_needed(handle, deadline) ⇒ Object

Parameters:

  • handle (Object)
  • deadline (Object)

Returns:

  • (Object)


509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/puppeteer/bidi/locator.rb', line 509

def wait_for_enabled_if_needed(handle, deadline)
  return unless @wait_for_enabled

  wait_until(deadline) do
    handle.evaluate(<<~JS)
      (element) => {
        if (!(element instanceof HTMLElement)) {
          return true;
        }
        const isNativeFormControl = [
          "BUTTON",
          "INPUT",
          "SELECT",
          "TEXTAREA",
          "OPTION",
          "OPTGROUP",
        ].includes(element.nodeName);
        return !isNativeFormControl || !element.hasAttribute("disabled");
      }
    JS
  end
end

#wait_for_stable_bounding_box?Boolean

Returns:

  • (Boolean)


348
349
350
# File 'lib/puppeteer/bidi/locator.rb', line 348

def wait_for_stable_bounding_box?
  @wait_for_stable_bounding_box
end

#wait_for_stable_bounding_box_if_needed(handle, deadline) ⇒ Object

Parameters:

  • handle (Object)
  • deadline (Object)

Returns:

  • (Object)


469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'lib/puppeteer/bidi/locator.rb', line 469

def wait_for_stable_bounding_box_if_needed(handle, deadline)
  return unless @wait_for_stable_bounding_box

  wait_until(deadline) do
    rects = handle.evaluate(<<~JS)
      (element) => {
        return new Promise(resolve => {
          window.requestAnimationFrame(() => {
            const rect1 = element.getBoundingClientRect();
            window.requestAnimationFrame(() => {
              const rect2 = element.getBoundingClientRect();
              resolve([
                {
                  x: rect1.x,
                  y: rect1.y,
                  width: rect1.width,
                  height: rect1.height,
                },
                {
                  x: rect2.x,
                  y: rect2.y,
                  width: rect2.width,
                  height: rect2.height,
                },
              ]);
            });
          });
        });
      }
    JS

    rect1 = rects[0]
    rect2 = rects[1]
    rect1["x"] == rect2["x"] &&
      rect1["y"] == rect2["y"] &&
      rect1["width"] == rect2["width"] &&
      rect1["height"] == rect2["height"]
  end
end

#wait_handleJSHandle

Wait for the locator to produce a handle.

Returns:



127
128
129
130
131
# File 'lib/puppeteer/bidi/locator.rb', line 127

def wait_handle
  with_retry("Locator.wait_handle") do |deadline, remaining_ms|
    _wait(timeout_ms: remaining_ms, deadline: deadline)
  end
end

#wait_until(deadline) { ... } ⇒ void

This method returns an undefined value.

Parameters:

  • deadline (Numeric, nil)

Yields:

Yield Returns:

  • (boolish)


450
451
452
453
454
455
456
# File 'lib/puppeteer/bidi/locator.rb', line 450

def wait_until(deadline, &block)
  loop do
    return if block.call
    raise_timeout if deadline && remaining_time_ms(deadline) <= 0
    sleep RETRY_DELAY
  end
end

#with_retry(cause) {|arg0, arg1| ... } ⇒ Object

Parameters:

  • cause (String)

Yields:

Yield Parameters:

  • arg0 (Numeric, nil)
  • arg1 (Numeric, nil)

Yield Returns:

  • (Object)

Returns:

  • (Object)


417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/puppeteer/bidi/locator.rb', line 417

def with_retry(cause, &block)
  deadline = build_deadline
  loop do
    remaining_ms = deadline ? remaining_time_ms(deadline) : nil
    raise_timeout if deadline && remaining_ms <= 0
    begin
      return block.call(deadline, remaining_ms)
    rescue TimeoutError
      raise
    rescue StandardError
      raise_timeout if deadline && remaining_time_ms(deadline) <= 0
      sleep RETRY_DELAY
    end
  end
end