NAME
EV::WebKit::Element - a handle to a DOM element found by EV::WebKit
SYNOPSIS
$b->find('#login', sub {
my ($el, $err) = @_;
# A die inside an EV callback only reaches $EV::DIED, which warns and
# keeps going -- break instead.
if ($err) { warn "find failed: $err\n"; return EV::break }
if (!$el) { warn "no #login on this page\n"; return EV::break }
$el->type('alice', sub {
my (undef, $err) = @_;
if ($err) { warn "type failed: $err\n"; return EV::break }
$el->submit(sub {
my (undef, $err) = @_;
warn "submit failed: $err\n" if $err;
EV::break;
});
});
});
DESCRIPTION
An EV::WebKit::Element is a handle to a single DOM node discovered via EV::WebKit's find, find_all, find_js, find_all_js or wait_for, or (scoped to that element's descendants) its own find/ find_all below. Internally it is just a small page-side registry id, the per-document epoch stamp of the registry it was created from, and a back-reference to the owning EV::WebKit browser -- not a live DOM reference held on the Perl side. Every method runs JavaScript against that node asynchronously, on the same EV loop as the browser, and follows the browser's own callback convention: a trailing sub { my ($result, $err) = @_; ... }, $err undef on success. See "CALLBACK CONVENTION" in EV::WebKit.
Instances are only ever returned by EV::WebKit methods; there is no public constructor.
If the underlying DOM node has since been removed from the document, any method call on that handle fails with a script error whose message mentions "stale element". The same happens if navigation has replaced the page entirely, even though the new page's own registry happens to reuse the same numeric id the old handle had: each navigation re-injects a fresh registry with a new epoch stamp, every handle carries the epoch it was created with, and a mismatch is treated exactly like a removed node. id and epoch are reserved argument names, always set by this class internally, for every JavaScript snippet run through these methods.
A handle found through the frame => { url => ... } form remembers which frame it came from, and every call on it runs there -- see "Addressing an iframe" in EV::WebKit. A handle found through the selector form (frame => '#f') does not: main-frame script walked contentDocument to reach it, and every call is made the same way.
Neither outlives its frame, but they say so differently: once the frame is removed or the page navigates away, the first kind fails with frame is gone and the second with the ordinary stale element.
METHODS
All methods below take a trailing sub { my ($result, $err) = @_; ... } callback.
text
$el->text($cb);
$result is the node's textContent.
html
$el->html($cb);
$result is the node's innerHTML.
value
$el->value($cb);
$result is the form control's current value.
tag
$el->tag($cb);
$result is the node's lower-cased tag name, e.g. "div".
attr
$el->attr($name, $cb);
$result is the HTML attribute $name (via getAttribute), or undef if the attribute is not present.
prop
$el->prop($name, $cb);
$result is the live JavaScript/DOM property $name (e.g. prop('checked')), as opposed to attr's raw HTML attribute -- useful when the two differ (checkbox checked state, current vs default value, and so on).
is_visible
$el->is_visible($cb);
$result is true if the element's computed display is not none, its computed visibility is not hidden, and it has at least one client rect (roughly: it takes up visible space in the rendered page).
find
$el->find($selector, $cb);
Scoped querySelector under this element. $result is an EV::WebKit::Element on a match, or undef if nothing matched -- not-found is not an error.
find_all
$el->find_all($selector, $cb);
Scoped querySelectorAll under this element. $result is a (possibly empty) arrayref of EV::WebKit::Element.
click
$el->click($cb);
Presses the element the way a pointer does: pointerdown, mousedown, pointerup, mouseup, then the node's own click(). The press matters -- plenty of controls (custom dropdowns, menus, sliders) act on mousedown and never see a bare click() at all. A disabled control gets the click() only, with no synthetic press, because no engine dispatches mouse events to one and firing them here would drive something a person could not.
Visibility is not checked: an off-screen or display:none element is clicked as asked, and reports success. Ask "is_visible" first if that matters, and "scroll_into_view" if the page reacts to scroll position.
type
$el->type($text, $cb);
On an <input> or <textarea>, appends $text to the element's current value and dispatches input and change events. On a contenteditable element (isContentEditable), appends $text to its textContent instead (there is no native value to set) and dispatches an input event. Either way this sets the content directly and fires the event(s) once -- it does not simulate individual keydown/keyup events per character. On anything else (not a form control, not contenteditable), $err is set to an error mentioning "not editable" rather than silently doing nothing.
send_keys
An alias for type above (identical behavior, including the "not really per-key" caveat and the contenteditable/not-editable handling).
clear
$el->clear($cb);
On an <input> or <textarea>, empties value and dispatches an input event. On a contenteditable element, empties textContent instead and dispatches an input event. On anything else, $err is set to an error mentioning "not editable".
focus
$el->focus($cb);
Calls the node's focus().
submit
$el->submit($cb);
Calls the native submit() of the element's owning <form>, or of the element itself if it has no form (e.g. calling submit directly on a <form> element). This bypasses any onsubmit handler and may navigate the page. Resolves successfully even if there was nothing to submit (a silent no-op).
select_option
$el->select_option($value, $cb); # $cb->($selected_value, $err)
Selects an <option> of a <select> by its value, falling back to matching the visible label when no value matches -- so an option whose value differs from what the page displays, such as <option value="b">Beta</option>, is still reachable by the text the user sees. (An option with no value attribute at all takes its value from its own text, so it already matches on the first pass.) Fires input and change so page code bound to those actually runs; assigning el.value alone updates the DOM and leaves application state stale. Errors if the element is not a <select>, or if no option matches.
check / uncheck
$el->check($cb); # ensure checked
$el->check(0, $cb); # ensure UNchecked
$el->uncheck($cb); # the same thing, spelled out
Sets a checkbox or radio to a definite state and resolves with that state. Deliberately not click: clicking toggles, so calling it twice "to be sure" silently undoes itself. These are idempotent -- already in the requested state means no change and, like a real browser, no change event. Errors if the element is not a checkbox or radio.
The single-argument form is read as a callback if it is a code reference and as the state otherwise -- evaluated for truth, so a decoded-JSON boolean works as well as a plain 1 or 0. uncheck has no state argument, so a non-coderef there is a mistyped callback and croaks.
hover
$el->hover($cb);
Dispatches the pointer/mouse sequence an entering cursor produces: pointerover/mouseover (which bubble), then pointerenter/mouseenter (which do not), then mousemove, positioned at the element's centre. Menus and tooltips listen for various of these, so dispatching only mouseover leaves a menu that opens on mouseenter shut. These are synthetic events: isTrusted is false, so a page that checks it is not fooled.
box
$el->box($cb); # $cb->($box, $err)
The element's geometry as a hashref: x, y, width, height, top, left, bottom, right -- viewport-relative, exactly as getBoundingClientRect gives them, which is what you want when comparing against a screenshot -- plus page_x and page_y, the same origin with the scroll offset added, for absolute document coordinates.
$box is undef for an element that is not rendered (display:none, or detached from the document). Such an element has an all-zero rect in every browser, and returning that as a box would be a plausible-looking lie a caller could divide by; ask "is_visible" if that is the question.
For an element inside a frame the rect is relative to that frame's viewport, not the top-level one, since that is the only coordinate space the element's own document has. Offsetting it into page coordinates would need the frame's position, which is the parent document's to know.
frame_id
my $id = $el->frame_id;
The id of the frame this handle is bound to, or undef if it is not bound to one. Synchronous; the id itself is opaque and lives only as long as the frame does.
Note what undef does not mean. Only the frame => { url => ... } form binds a handle to a frame; a handle reached through the selector form (frame => '#f') is inside an iframe but reports undef, exactly as a main-frame handle does. So this tells you how the handle will be operated on, not where the element lives -- it is not a test for "is this inside an iframe". See "Addressing an iframe" in EV::WebKit.
scroll_into_view
$el->scroll_into_view($cb);
Scrolls the element to the centre of the viewport. Worth doing before reading around it: click works on an off-screen element, but a page that reacts to scroll position (lazy images, infinite scroll, sticky headers) will not have done that work yet.
SEE ALSO
AUTHOR
vividsnow
LICENSE
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.