Class: Unmagic::Browser::Element

Inherits:
Object
  • Object
show all
Defined in:
lib/unmagic/browser/element.rb

Overview

One element on the page, found by a Locator.

Everything a Session verb does to a single element goes through here, so session.click_button "Go" and session.find("#go").click take exactly the same path. Like a session, an element serialises its operations against the session it came from.

Examples:

session.all(".product").map { |element| element.text }

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(session, handle) ⇒ Element

Returns a new instance of Element.

Parameters:

  • session (Session)

    the session this element was found in

  • handle (Puppeteer::ElementHandle)

    the live handle in the page



17
18
19
20
# File 'lib/unmagic/browser/element.rb', line 17

def initialize(session, handle)
  @session = session
  @handle = handle
end

Instance Attribute Details

#handlePuppeteer::ElementHandle (readonly) Also known as: driver

The live Puppeteer handle, for work this API doesn't wrap.

Returns:

  • (Puppeteer::ElementHandle)


25
26
27
# File 'lib/unmagic/browser/element.rb', line 25

def handle
  @handle
end

Instance Method Details

#all(query = nil, **options) ⇒ Array<Element>

Find every element inside this one. Same locator rules as Session#all.

Parameters:

Returns:



214
215
216
# File 'lib/unmagic/browser/element.rb', line 214

def all(query = nil, **options)
  @session.all(query, scope: self, **options)
end

#attach(*paths) ⇒ self

Attach files to a file input.

Parameters:

  • paths (Array<String>)

    paths to the files

Returns:

  • (self)


179
180
181
# File 'lib/unmagic/browser/element.rb', line 179

def attach(*paths)
  act { @handle.upload_file(*paths.flatten.map(&:to_s)) }
end

#attribute(name) ⇒ String? Also known as: []

Returns the attribute's value, or nil when it isn't set.

Parameters:

  • name (String, Symbol)

    the attribute name

Returns:

  • (String, nil)

    the attribute's value, or nil when it isn't set



59
60
61
# File 'lib/unmagic/browser/element.rb', line 59

def attribute(name)
  evaluate("(element, name) => element.getAttribute(name)", name.to_s)
end

#checkself

Tick a checkbox, or leave it ticked if it already is.

Returns:

  • (self)


164
165
166
# File 'lib/unmagic/browser/element.rb', line 164

def check
  set_checked(true)
end

#checked?Boolean

Returns whether a checkbox or radio is ticked.

Returns:

  • (Boolean)

    whether a checkbox or radio is ticked



77
78
79
# File 'lib/unmagic/browser/element.rb', line 77

def checked?
  !!evaluate("element => !!element.checked")
end

#clickself

Click the element, scrolling it into view first.

Returns:

  • (self)


89
90
91
92
93
94
# File 'lib/unmagic/browser/element.rb', line 89

def click
  act do
    @handle.scroll_into_view_if_needed
    @handle.click
  end
end

#disabled?Boolean

Returns whether the element is disabled.

Returns:

  • (Boolean)

    whether the element is disabled



82
83
84
# File 'lib/unmagic/browser/element.rb', line 82

def disabled?
  !!evaluate("element => !!element.disabled")
end

#disposevoid

This method returns an undefined value.

Release the handle. Elements found by a session are disposed with the page, so this is only worth calling in a long loop.



231
232
233
# File 'lib/unmagic/browser/element.rb', line 231

def dispose
  @session.synchronize { @handle.dispose }
end

#evaluate(script, *args) ⇒ Object

Run JavaScript with this element as the first argument.

Parameters:

  • script (String)

    a JavaScript function body, e.g. "el => el.dataset.id"

  • args (Array)

    extra arguments passed after the element

Returns:

  • (Object)

    whatever the script returns, as a Ruby value



223
224
225
# File 'lib/unmagic/browser/element.rb', line 223

def evaluate(script, *args)
  @session.synchronize { @handle.evaluate(script, *args) }
end

#fill_in(value) ⇒ self

Replace whatever's in the field with value.

The existing content is selected and deleted rather than assigned over, so frameworks that watch for real key events (React and friends) see the change.

Parameters:

  • value (String)

    the text to type

Returns:

  • (self)


114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/unmagic/browser/element.rb', line 114

def fill_in(value)
  act do
    @handle.scroll_into_view_if_needed
    @handle.focus
    @handle.evaluate(<<~JAVASCRIPT)
      (element) => {
        if (element.select) { element.select(); return; }
        const range = document.createRange();
        range.selectNodeContents(element);
        const selection = getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
      }
    JAVASCRIPT
    @handle.press("Backspace")
    @handle.type_text(value.to_s) unless value.to_s.empty?
  end
end

#find(query = nil, **options) ⇒ Element

Find one element inside this one. Same locator rules as Session#find.

Parameters:

Returns:

Raises:



205
206
207
# File 'lib/unmagic/browser/element.rb', line 205

def find(query = nil, **options)
  @session.find(query, scope: self, **options)
end

#hoverself

Move the pointer over the element.

Returns:

  • (self)


99
100
101
102
103
104
# File 'lib/unmagic/browser/element.rb', line 99

def hover
  act do
    @handle.scroll_into_view_if_needed
    @handle.hover
  end
end

#htmlString

Returns the element's own HTML, tag included.

Returns:

  • (String)

    the element's own HTML, tag included



35
36
37
# File 'lib/unmagic/browser/element.rb', line 35

def html
  evaluate("element => element.outerHTML")
end

#inner_htmlString

Returns the element's inner HTML.

Returns:

  • (String)

    the element's inner HTML



40
41
42
# File 'lib/unmagic/browser/element.rb', line 40

def inner_html
  evaluate("element => element.innerHTML")
end

#inspectString

Returns the element and the handle behind it.

Returns:

  • (String)

    the element and the handle behind it



236
237
238
# File 'lib/unmagic/browser/element.rb', line 236

def inspect
  "#<#{self.class.name} #{@handle.inspect}>"
end

#press(key) ⇒ self

Press a key with this element focused.

Parameters:

  • key (String, Symbol)

    a key name, e.g. :enter or "ArrowDown"

Returns:

  • (self)


187
188
189
# File 'lib/unmagic/browser/element.rb', line 187

def press(key)
  act { @handle.press(Session.key_name(key)) }
end

#screenshotString

Returns PNG bytes of just this element.

Returns:

  • (String)

    PNG bytes of just this element



192
193
194
195
196
197
# File 'lib/unmagic/browser/element.rb', line 192

def screenshot
  @session.synchronize do
    @handle.scroll_into_view_if_needed
    @handle.screenshot(type: "png")
  end
end

#select_option(option) ⇒ self

Pick an option in a <select> by its visible label, or failing that its value — a caller reading the page sees labels, not values.

Parameters:

  • option (String)

    the option's label or value

Returns:

  • (self)

Raises:



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/unmagic/browser/element.rb', line 139

def select_option(option)
  act do
    found = @handle.evaluate(<<~JAVASCRIPT, option.to_s)
      (element, wanted) => {
        if (element.tagName.toLowerCase() !== "select") throw new Error("not a <select>");
        const norm = (value) => (value || "").replace(/\\s+/g, " ").trim().toLowerCase();
        const needle = norm(wanted);
        const options = Array.from(element.options);
        const match = options.find((option) => norm(option.label || option.textContent) === needle) ||
                      options.find((option) => norm(option.value) === needle) ||
                      options.find((option) => norm(option.label || option.textContent).indexOf(needle) !== -1);
        if (!match) return false;
        if (element.multiple) { match.selected = true; } else { element.value = match.value; }
        element.dispatchEvent(new Event("input", { bubbles: true }));
        element.dispatchEvent(new Event("change", { bubbles: true }));
        return true;
      }
    JAVASCRIPT
    raise Error::NotFound, "No option matching #{option.inspect} in this select." unless found
  end
end

#tag_nameString

Returns the lowercased tag name, e.g. "input".

Returns:

  • (String)

    the lowercased tag name, e.g. "input"



45
46
47
# File 'lib/unmagic/browser/element.rb', line 45

def tag_name
  evaluate("element => element.tagName.toLowerCase()")
end

#textString

Returns the element's visible text, whitespace collapsed.

Returns:

  • (String)

    the element's visible text, whitespace collapsed



29
30
31
32
# File 'lib/unmagic/browser/element.rb', line 29

def text
  evaluate("element => (element.innerText === undefined ? element.textContent : element.innerText)")
    .to_s.gsub(/\s+/, " ").strip
end

#uncheckself

Untick a checkbox, or leave it unticked if it already is.

Returns:

  • (self)


171
172
173
# File 'lib/unmagic/browser/element.rb', line 171

def uncheck
  set_checked(false)
end

#valueString?

The element's current value — what's typed into a field, what's selected in a <select>, or nil for anything without one.

Returns:

  • (String, nil)


53
54
55
# File 'lib/unmagic/browser/element.rb', line 53

def value
  evaluate("element => ('value' in element ? element.value : null)")
end

#visible?Boolean

Returns whether the element is rendered and not hidden.

Returns:

  • (Boolean)

    whether the element is rendered and not hidden



65
66
67
68
69
70
71
72
73
74
# File 'lib/unmagic/browser/element.rb', line 65

def visible?
  evaluate(<<~JAVASCRIPT)
    (element) => {
      if (element.hidden) return false;
      const style = getComputedStyle(element);
      if (style.visibility === "hidden" || style.display === "none") return false;
      return element.getClientRects().length > 0;
    }
  JAVASCRIPT
end