Class: Puppeteer::ElementHandle

Inherits:
JSHandle
  • Object
show all
Includes:
DebugPrint, IfPresent
Defined in:
lib/puppeteer/element_handle.rb,
lib/puppeteer/element_handle/point.rb,
lib/puppeteer/element_handle/offset.rb,
lib/puppeteer/element_handle/box_model.rb,
lib/puppeteer/element_handle/bounding_box.rb

Defined Under Namespace

Classes: BoundingBox, BoxModel, DragInterceptionNotEnabledError, ElementNotFoundError, ElementNotVisibleError, Offset, Point, ScrollIntoViewError

Instance Attribute Summary

Attributes inherited from JSHandle

#context, #remote_object

Instance Method Summary collapse

Methods included from IfPresent

#if_present

Methods included from DebugPrint

#debug_print, #debug_puts

Methods inherited from JSHandle

#[], create, #dispose, #disposed?, #evaluate, #evaluate_handle, #execution_context, #json_value, #properties, #property, #to_s

Constructor Details

#initialize(context:, client:, remote_object:, page:, frame_manager:) ⇒ ElementHandle

Returns a new instance of ElementHandle.

Parameters:



16
17
18
19
20
21
# File 'lib/puppeteer/element_handle.rb', line 16

def initialize(context:, client:, remote_object:, page:, frame_manager:)
  super(context: context, client: client, remote_object: remote_object)
  @page = page
  @frame_manager = frame_manager
  @disposed = false
end

Instance Method Details

#as_elementObject



23
24
25
# File 'lib/puppeteer/element_handle.rb', line 23

def as_element
  self
end

#bounding_boxBoundingBox|nil

Returns:



304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/puppeteer/element_handle.rb', line 304

def bounding_box
  if_present(box_model) do |result_model|
    quads = result_model.border

    x = quads.map(&:x).min
    y = quads.map(&:y).min
    BoundingBox.new(
      x: x,
      y: y,
      width: quads.map(&:x).max - x,
      height: quads.map(&:y).max - y,
    )
  end
end

#box_modelBoxModel|nil

Returns:



320
321
322
323
324
# File 'lib/puppeteer/element_handle.rb', line 320

def box_model
  if_present(@remote_object.box_model(@client)) do |result|
    BoxModel.new(result['model'])
  end
end

#click(delay: nil, button: nil, click_count: nil, offset: nil) ⇒ Object

Parameters:

  • delay (Number) (defaults to: nil)
  • button (String) (defaults to: nil)

    “left”|“right”|“middle”

  • click_count (Number) (defaults to: nil)
  • offset (Hash) (defaults to: nil)


159
160
161
162
163
# File 'lib/puppeteer/element_handle.rb', line 159

def click(delay: nil, button: nil, click_count: nil, offset: nil)
  scroll_into_view_if_needed
  point = clickable_point(offset)
  @page.mouse.click(point.x, point.y, delay: delay, button: button, click_count: click_count)
end

#clickable_point(offset = nil) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/puppeteer/element_handle.rb', line 83

def clickable_point(offset = nil)
  offset_param = Offset.from(offset)

  result =
    begin
      @remote_object.content_quads(@client)
    rescue => err
      debug_puts(err)
      nil
    end

  if !result || result["quads"].empty?
    raise ElementNotVisibleError.new
  end

  # Filter out quads that have too small area to click into.
  layout_metrics = @client.send_message('Page.getLayoutMetrics')
  client_width = layout_metrics["layoutViewport"]["clientWidth"]
  client_height = layout_metrics["layoutViewport"]["clientHeight"]

  quads = result["quads"].
            map { |quad| from_protocol_quad(quad) }.
            map { |quad| intersect_quad_with_viewport(quad, client_width, client_height) }.
            select { |quad| compute_quad_area(quad) > 1 }
  if quads.empty?
    raise ElementNotVisibleError.new
  end

  if offset_param
    # Return the point of the first quad identified by offset.
    quad = quads.first
    min_x = quad.map(&:x).min
    min_y = quad.map(&:y).min
    if min_x && min_y
      return Point.new(
        x: min_x + offset_param.x,
        y: min_y + offset_param.y,
      )
    end
  end

  # Return the middle point of the first quad.
  quads.first.reduce(:+) / 4
end

#content_frameObject



27
28
29
30
31
32
33
34
35
# File 'lib/puppeteer/element_handle.rb', line 27

def content_frame
  node_info = @remote_object.node_info(@client)
  frame_id = node_info['node']['frameId']
  if frame_id.is_a?(String)
    @frame_manager.frame(frame_id)
  else
    nil
  end
end

#drag(x:, y:) ⇒ Object



173
174
175
176
177
178
179
180
# File 'lib/puppeteer/element_handle.rb', line 173

def drag(x:, y:)
  unless @page.drag_interception_enabled?
    raise DragInterceptionNotEnabledError.new
  end
  scroll_into_view_if_needed
  start = clickable_point
  @page.mouse.drag(start, Point.new(x: x, y: y))
end

#drag_and_drop(target, delay: nil) ⇒ Object

Parameters:



201
202
203
204
205
206
# File 'lib/puppeteer/element_handle.rb', line 201

def drag_and_drop(target, delay: nil)
  scroll_into_view_if_needed
  start_point = clickable_point
  target_point = target.clickable_point
  @page.mouse.drag_and_drop(start_point, target_point, delay: delay)
end

#drag_enter(data) ⇒ Object



182
183
184
185
186
# File 'lib/puppeteer/element_handle.rb', line 182

def drag_enter(data)
  scroll_into_view_if_needed
  target = clickable_point
  @page.mouse.drag_enter(target, data)
end

#drag_over(data) ⇒ Object



188
189
190
191
192
# File 'lib/puppeteer/element_handle.rb', line 188

def drag_over(data)
  scroll_into_view_if_needed
  target = clickable_point
  @page.mouse.drag_over(target, data)
end

#drop(data) ⇒ Object



194
195
196
197
198
# File 'lib/puppeteer/element_handle.rb', line 194

def drop(data)
  scroll_into_view_if_needed
  target = clickable_point
  @page.mouse.drop(target, data)
end

#eval_on_selector(selector, page_function, *args) ⇒ Object Also known as: Seval

‘$eval()` in JavaScript.

Parameters:

  • selector (String)
  • page_function (String)

Returns:

  • (Object)


403
404
405
406
407
408
409
410
411
412
# File 'lib/puppeteer/element_handle.rb', line 403

def eval_on_selector(selector, page_function, *args)
  element_handle = query_selector(selector)
  unless element_handle
    raise ElementNotFoundError.new(selector)
  end
  result = element_handle.evaluate(page_function, *args)
  element_handle.dispose

  result
end

#eval_on_selector_all(selector, page_function, *args) ⇒ Object Also known as: SSeval

‘$$eval()` in JavaScript.

Parameters:

  • selector (String)
  • page_function (String)

Returns:

  • (Object)


421
422
423
424
425
426
427
# File 'lib/puppeteer/element_handle.rb', line 421

def eval_on_selector_all(selector, page_function, *args)
  handles = query_handler_manager.detect_query_handler(selector).query_all_array(self)
  result = handles.evaluate(page_function, *args)
  handles.dispose

  result
end

#focusObject



278
279
280
# File 'lib/puppeteer/element_handle.rb', line 278

def focus
  evaluate('element => element.focus()')
end

#hoverObject



149
150
151
152
153
# File 'lib/puppeteer/element_handle.rb', line 149

def hover
  scroll_into_view_if_needed
  point = clickable_point
  @page.mouse.move(point.x, point.y)
end

#intersecting_viewport?(threshold: nil) ⇒ Boolean

in JS, #isIntersectingViewport.

Parameters:

  • threshold (Float|nil) (defaults to: nil)

Returns:

  • (Boolean)


458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/puppeteer/element_handle.rb', line 458

def intersecting_viewport?(threshold: nil)
  option_threshold = threshold || 0
  js = "  async (element, threshold) => {\n    const visibleRatio = await new Promise(resolve => {\n      const observer = new IntersectionObserver(entries => {\n        resolve(entries[0].intersectionRatio);\n        observer.disconnect();\n      });\n      observer.observe(element);\n    });\n    if (threshold === 1) return visibleRatio === 1;\n    else return visibleRatio > threshold;\n  }\n  JAVASCRIPT\n\n  evaluate(js, option_threshold)\nend\n"

#press(key, delay: nil, text: nil) ⇒ Object

Parameters:

  • key (String)
  • text (String) (defaults to: nil)
  • delay (number|nil) (defaults to: nil)


296
297
298
299
# File 'lib/puppeteer/element_handle.rb', line 296

def press(key, delay: nil, text: nil)
  focus
  @page.keyboard.press(key, delay: delay, text: text)
end

#query_ax_tree(accessible_name: nil, role: nil) ⇒ Object

used in AriaQueryHandler



485
486
487
488
# File 'lib/puppeteer/element_handle.rb', line 485

def query_ax_tree(accessible_name: nil, role: nil)
  @remote_object.query_ax_tree(@client,
  accessible_name: accessible_name, role: role)
end

#query_selector(selector) ⇒ Object Also known as: S

‘$()` in JavaScript.

Parameters:

  • selector (String)


381
382
383
# File 'lib/puppeteer/element_handle.rb', line 381

def query_selector(selector)
  query_handler_manager.detect_query_handler(selector).query_one(self)
end

#query_selector_all(selector) ⇒ Object Also known as: SS

‘$$()` in JavaScript.

Parameters:

  • selector (String)


388
389
390
# File 'lib/puppeteer/element_handle.rb', line 388

def query_selector_all(selector)
  query_handler_manager.detect_query_handler(selector).query_all(self)
end

#screenshot(options = {}) ⇒ Object



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/puppeteer/element_handle.rb', line 326

def screenshot(options = {})
  needs_viewport_reset = false

  box = bounding_box
  unless box
    raise ElementNotVisibleError.new
  end

  viewport = @page.viewport
  if viewport && (box.width > viewport.width || box.height > viewport.height)
    new_viewport = viewport.merge(
      width: [viewport.width, box.width.to_i].min,
      height: [viewport.height, box.height.to_i].min,
    )
    @page.viewport = new_viewport

    needs_viewport_reset = true
  end
  scroll_into_view_if_needed

  box = bounding_box
  unless box
    raise ElementNotVisibleError.new
  end
  if box.width == 0
    raise 'Node has 0 width.'
  end
  if box.height == 0
    raise 'Node has 0 height.'
  end

  layout_metrics = @client.send_message('Page.getLayoutMetrics')
  page_x = layout_metrics["layoutViewport"]["pageX"]
  page_y = layout_metrics["layoutViewport"]["pageY"]

  clip = {
    x: page_x + box.x,
    y: page_y + box.y,
    width: box.width,
    height: box.height,
  }

  @page.screenshot({ clip: clip }.merge(options))
ensure
  if needs_viewport_reset
    @page.viewport = viewport
  end
end

#scroll_into_view_if_neededObject



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/puppeteer/element_handle.rb', line 39

def scroll_into_view_if_needed
  js = "    async(element, pageJavascriptEnabled) => {\n      if (!element.isConnected)\n        return 'Node is detached from document';\n      if (element.nodeType !== Node.ELEMENT_NODE)\n        return 'Node is not of type HTMLElement';\n\n      if (element.scrollIntoViewIfNeeded) {\n        element.scrollIntoViewIfNeeded({block: 'center', inline: 'center', behavior: 'instant'});\n      } else {\n        // force-scroll if page's javascript is disabled.\n        if (!pageJavascriptEnabled) {\n          element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});\n          return false;\n        }\n        const visibleRatio = await new Promise(resolve => {\n          const observer = new IntersectionObserver(entries => {\n            resolve(entries[0].intersectionRatio);\n            observer.disconnect();\n          });\n          observer.observe(element);\n        });\n        if (visibleRatio !== 1.0)\n          element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});\n      }\n      return false;\n    }\n  JAVASCRIPT\n  error = evaluate(js, @page.javascript_enabled) # returns String or false\n  if error\n    raise ScrollIntoViewError.new(error)\n  end\n  # clickpoint is often calculated before scrolling is completed.\n  # So, just sleep about 10 frames\n  sleep 0.16\nend\n"

#select(*values) ⇒ Array<String>

Returns:

  • (Array<String>)


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
# File 'lib/puppeteer/element_handle.rb', line 209

def select(*values)
  if nonstring = values.find { |value| !value.is_a?(String) }
    raise ArgumentError.new("Values must be strings. Found value \"#{nonstring}\" of type \"#{nonstring.class}\"")
  end

  fn = "  (element, values) => {\n    if (element.nodeName.toLowerCase() !== 'select') {\n      throw new Error('Element is not a <select> element.');\n    }\n\n    const options = Array.from(element.options);\n    element.value = undefined;\n    for (const option of options) {\n      option.selected = values.includes(option.value);\n      if (option.selected && !element.multiple) {\n        break;\n      }\n    }\n    element.dispatchEvent(new Event('input', { bubbles: true }));\n    element.dispatchEvent(new Event('change', { bubbles: true }));\n    return options.filter(option => option.selected).map(option => option.value);\n  }\n  JAVASCRIPT\n  evaluate(fn, values)\nend\n"

#Sx(expression) ⇒ Array<ElementHandle>

‘$x()` in JavaScript. $ is not allowed to use as a method name in Ruby.

Parameters:

  • expression (String)

Returns:



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/puppeteer/element_handle.rb', line 435

def Sx(expression)
  fn = "  (element, expression) => {\n    const document = element.ownerDocument || element;\n    const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);\n    const array = [];\n    let item;\n    while ((item = iterator.iterateNext()))\n      array.push(item);\n    return array;\n  }\n  JAVASCRIPT\n  handles = evaluate_handle(fn, expression)\n  properties = handles.properties\n  handles.dispose\n  properties.values.map(&:as_element).compact\nend\n"

#tap(&block) ⇒ Object



268
269
270
271
272
273
274
# File 'lib/puppeteer/element_handle.rb', line 268

def tap(&block)
  return super(&block) if block

  scroll_into_view_if_needed
  point = clickable_point
  @page.touchscreen.tap(point.x, point.y)
end

#type_text(text, delay: nil) ⇒ Object

Parameters:

  • text (String)
  • delay (number|nil) (defaults to: nil)


286
287
288
289
# File 'lib/puppeteer/element_handle.rb', line 286

def type_text(text, delay: nil)
  focus
  @page.keyboard.type_text(text, delay: delay)
end

#upload_file(*file_paths) ⇒ Object

Parameters:

  • file_paths (Array<String>)


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
# File 'lib/puppeteer/element_handle.rb', line 237

def upload_file(*file_paths)
  is_multiple = evaluate("el => el.multiple")
  if !is_multiple && file_paths.length >= 2
    raise ArgumentError.new('Multiple file uploads only work with <input type=file multiple>')
  end

  if error_path = file_paths.find { |file_path| !File.exist?(file_path) }
    raise ArgumentError.new("#{error_path} does not exist or is not readable")
  end

  backend_node_id = @remote_object.node_info(@client)["node"]["backendNodeId"]

  # The zero-length array is a special case, it seems that DOM.setFileInputFiles does
  # not actually update the files in that case, so the solution is to eval the element
  # value to a new FileList directly.
  if file_paths.empty?
    fn = "    (element) => {\n      element.files = new DataTransfer().files;\n\n      // Dispatch events for this case because it should behave akin to a user action.\n      element.dispatchEvent(new Event('input', { bubbles: true }));\n      element.dispatchEvent(new Event('change', { bubbles: true }));\n    }\n    JAVASCRIPT\n    await evaluate(fn)\n  else\n    @remote_object.set_file_input_files(@client, file_paths.map { |path| File.expand_path(path) }, backend_node_id)\n  end\nend\n"