WebDriver2
WebDriver level 2 bindings implementing
W3C's specification.
Current implementation status is
documented below.
Usage
Using a driver directly
To use a driver directly for all endpoint commands, create a
test class that does WebDriver2::Test::Template
. The test class
will need to specify the browser upon instantiation:
use Test;
use WebDriver2::Test::Template;
use WebDriver2::SUT::Tree;
# can be file path or web address
my WebDriver2::SUT::Tree::URL:D $page =
WebDriver2::SUT::Tree::URL.new: 'file://xt/content/test.html';
class Local does WebDriver2::Test::Template {
has Bool $!screenshot;
has Int:D $.plan = 38;
has Str:D $.name = 'local';
has Str:D $.description = 'local test';
# WebDriver2::Test::Template provides method new, which
# sets the browser / loads from file if not passed
# and instantiates the corresponding driver
method test {
$.driver.navigate: $page.Str;
is $.driver.title, 'test', 'page title';
ok
self.element-by-id( 'outer' ) ~~ self.element-by-tag( 'ul' ),
'same element found different ways';
my WebDriver2::Command::Element::Locator $by-tag-ul =
WebDriver2::Command::Element::Locator::Tag-Name.new: 'ul';
my WebDriver2::Model::Element $el = $.driver.element: $by-tag-ul;
nok $el ~~ $el.element( $by-tag-ul ), 'different elements';
my WebDriver2::Command::Element::Locator $locator =
WebDriver2::Command::Element::Locator::Tag-Name.new: 'li';
$el = $.driver.element: $locator;
my Str $outer-li = $el.text;
my Str $inner-li =
self.element-by-id( 'inner' ).element( $locator ).text;
isnt $inner-li, $outer-li, 'inner li != outer li';
# test continues ...
}
}
sub MAIN (
Str $browser?,
Int:D :$debug = 0
) {
.execute with Local.new: $browser, :$debug;
}
WebDriver2::Test::Template
calls
method init { ... }
method pre-test { ... }
method test { ... }
method post-test { ... }
method close { ... }
method done-testing { done-testing }
method cleanup { ... }
when its execute
method is called.
Before starting into the test code, $.driver.session
needs
to be called, along with $.driver.delete-session
after
test code has completed. These two calls are made
automatically during init
and close
when doing the
provided role WebDriver2::Test::Template
.
Defining a site's pages and the services they provide
A simple page description language is defined in the
page grammar file.
For a multi-page site, e.g., with a login page and a
main page with an iframe, in addition to the html
files, a "system under test" definition,
which could optionally be split into multiple .page
files, and .service
definitions are needed.
For example, for
doc-site.sut
#include 'doc-login.page'
#include 'doc-main.page'
doc-login.html
<html>
<head><title>start page</title></head>
<body>
<form action="doc-main.html">
<input type="text" id="user" name="user"/>
<input type="text" id="pass" name="pass"/>
<button name="k" value="v">log in</button>
</form>
</body>
</html>
doc-login.page
page doc-login 'file://relative/path/to/doc-login.html' {
elemt username id 'user';
elemt password id 'pass';
elemt login-button tag-name 'button';
}
doc-login.service
#page: doc-login
username: /username
password: /password
login-button: /login-button
doc-main.html
<html>
<head><title>simple example</title></head>
<body>
<h1>simple example</h1>
<p id="before">text</p>
<form><input type="text" value="main-1"/></form>
<iframe src="doc-frame.html"></iframe>
<form><input type="text" value="main-2"/></form>
<p>other content</p>
<form><input type="text" value="main-3"/></form>
<form><input type="text" value="main-4"/></form>
<p id="after">more text</p>
</body>
</html>
doc-main.page
- with only content we're interested in outlined
page doc-main 'file://relative/path/to/doc-main.html' {
elemt heading tag-name 'h1';
elemt first-para id 'before';
#include 'doc-frame.page'
list of
#include 'doc-form.page'
elemt last-para id 'after';
}
doc-main.service
#page: doc-main
heading: /heading
pf: /first-para
iframe: /iframe
form: /form
pl: /last-para
doc-frame.html
<html>
<head><title>iframe</title></head>
<body>
<form><input type="text" value="head"/></form>
<ul>
<li>
<ol>
<li>Mirzakhani</li>
<li>Noether</li>
<li>Oh</li>
</ol>
</li>
<li>
<ol>
<li>Delta</li>
<li>Echo</li>
<li>Foxtrot</li>
</ol>
</li>
<li>
<ol>
<li>apple</li>
<li>banana</li>
<li>cantaloupe</li>
</ol>
</li>
</ul>
<div><form><input type="text" value="foot"/></form></div>
</body>
</html>
doc-frame.page
- again, only content we're interested in is outlined
frame iframe tag-name 'iframe' {
#include 'doc-form.page'
list of elgrp outer xpath '*/ul/li' {
list of elemt inner xpath 'ol/li';
}
elgrp div tag-name 'div' {
#include 'doc-form.page'
}
}
doc-frame.service
#page: doc-main
iframe: /iframe
outer: /iframe/outer
inner: /iframe/outer/inner
if identical content exists in multiple parts of the SUT ( e.g.,
calendar widgets ), it can be defined once and included in those parts by
specifying a prefix
doc-form.page
elgrp form xpath 'form' {
elemt input tag-name 'input';
}
doc-form.service
#page: doc-main
form: /form
input: /form/input
script with supporting code:
use Test;
use lib <lib t/lib>;
use WebDriver2::Test::Template;
use WebDriver2::Test::Service-Test;
use WebDriver2::SUT::Service::Loader;
use WebDriver2::SUT::Service;
use WebDriver2::SUT::Tree;
class Login-Service does WebDriver2::SUT::Service {
has Str:D $.name = 'doc-login';
my IO::Path $html-file = $*CWD.add: <xt content doc-login.html>;
my WebDriver2::SUT::Tree::URL $url =
WebDriver2::SUT::Tree::URL.new: 'file://' ~ $html-file.Str;
submethod BUILD ( WebDriver2::Driver:D :$!driver ) { }
method log-in ( Str:D $username, Str:D $password ) {
$!driver.navigate: $url.Str;
.resolve.send-keys: $username with self.get: 'username';
.resolve.send-keys: $password with self.get: 'password';
.resolve.click with self.get: 'login-button';
}
}
class Main-Service does WebDriver2::SUT::Service {
has Str:D $.name = 'doc-main';
submethod BUILD ( WebDriver2::Driver:D :$!driver ) { }
method question ( --> Str:D ) {
.resolve.text with self.get: 'question';
}
method interesting-text ( --> Str:D ) {
my Str @text;
@text.push: .resolve.text with self.get: 'heading';
@text.push: .resolve.text with self.get: 'pf';
@text.push: .resolve.text with self.get: 'pl';
@text.join: "\n";
}
}
class Form-Service does WebDriver2::SUT::Service {
has Str:D $.name = 'doc-form';
submethod BUILD ( WebDriver2::Driver:D :$!driver, Str:D :$!prefix = '' ) { }
method value ( --> Str:D ) {
.resolve.value with self.get: 'input';
}
method first ( &cb ) {
for self.get( 'form' ).iterator {
return self if &cb( self );
}
return Form-Service;
}
method each ( &action ) {
for self.get( 'form' ).iterator {
&action( self );
}
}
}
class Frame-Service does WebDriver2::SUT::Service {
has Str:D $.name = 'doc-frame';
submethod BUILD ( WebDriver2::Driver:D :$!driver ) { }
method each-outer ( &cb ) {
for self.get( 'outer' ).iterator {
&cb( self );
}
}
method each-inner ( &cb ) {
for self.get( 'inner' ).iterator {
&cb( self );
}
}
method item-text ( --> Str:D ) {
.resolve.text with self.get: 'inner';
}
}
class Readme-Test
does WebDriver2::Test::Service-Test
does WebDriver2::Test::Template
{
has Login-Service $!ls;
has Main-Service $!ms;
has Form-Service $!fs-main;
has Form-Service $!fs-div;
has Form-Service $!fs-frame;
has Frame-Service $!frs;
submethod BUILD (
Str :$!browser,
Str:D :$!name,
Str:D :$!description,
Str:D :$!sut-name,
Int :$!plan,
Int :$!debug = 0
) { }
submethod TWEAK (
Str:D :$sut-name,
Int :$debug
) {
$!sut = WebDriver2::SUT::Build.page: { self.driver.top }, $!sut-name, debug => self.debug;
$!loader =
WebDriver2::SUT::Service::Loader.new:
driver => self.driver,
:$!browser,
:$sut-name,
:$debug;
}
method new ( Str $browser is rw, Int $debug = 0 ) {
my $self = self.bless:
:$browser,
:$debug,
sut-name => 'doc-site',
name => 'readme example',
description => 'service / page object example',
plan => 26;
$self.init;
$self.services;
$self;
}
method services {
$!loader.load-elements: $!ls = Login-Service.new: :$.driver;
$!loader.load-elements: $!ms = Main-Service.new: :$.driver;
$!loader.load-elements: $!fs-main = Form-Service.new: :$.driver, prefix => '/';
$!loader.load-elements: $!fs-frame = Form-Service.new: :$.driver, prefix => '/iframe';
$!loader.load-elements: $!fs-div = Form-Service.new: :$.driver, prefix => '/iframe/div';
$!loader.load-elements: $!frs = Frame-Service.new: :$.driver;
}
method test {
$!ls.log-in: 'user', 'pass';
self.is: 'sub xpath', 'subelement test', .resolve.text with $!ms.get: 'subelement';
self.is:
'interesting text',
q:to /END/.trim,
simple example
text
more text
END
$!ms.interesting-text;
my Str:D @results =
'Mirzakhani',
'Noether',
'Oh',
'Delta',
'Echo',
'Foxtrot',
'apple',
'banana',
'cantaloupe',
;
my Int $els = 9;
my Bool:D $list-seen = False;
$!frs.each-outer: {
$list-seen = True;
self.is: "correct number of elements left", $els, @results.elems;
$!frs.each-inner: {
self.is: "correct inner element : @results[0]", @results.shift,
.item-text;
}
$els -= 3;
}
self.ok: 'outer', $list-seen;
self.is: '$els decremented', 0, $els;
self.is: '@results empty', 0, @results.elems;
@results = 'main-1', 'main-2', 'main-3', 'main-4';
$!fs-main.each: { self.is: 'correct form element', @results.shift, .value };
self.is: '@results empty', 0, @results.elems;
self.is: 'first frame form is head', 'head', $!fs-frame.value;
self.is: 'main page form', 'main-1', $!fs-main.first( { True; } ).value;
self.is: 'final frame form is foot', 'foot', $!fs-div.value;
}
}
sub MAIN(
Str:D $browser is copy = 'chrome',
Int :$debug = 0
) {
.execute with Readme-Test.new: $browser, $debug;
}
Extended examples can be seen in the xt/02-driver
(direct driver use)
and the xt/03-service
(page definition and service use) subdirectories, which
use resources from xt/content
and xt/def
.
HTTP::UserAgent
A minor fork of HTTP::UserAgent is provided under the
WebDriver2 directory. Please see its license:
LICENSE-HTTP-UserAgent.
The changes are:
- fix content length (geckodriver does not gracefully handle
incorrect content lengths)
- increase amount of info logged (originally capped at 300
characters per entry)
TODO
- cover all implemented endpoints with unit tests
- add POD
- implement the rest of the endpoints
- page and service object features
Feedback
Suggestions, design recommendations, and feature requests
welcome.
Implementation Status
| Windows | MacOS | |
---|
new session | X | X | X | X | $driver.session |
delete session | X | X | X | X | $driver.delete-session |
status | X | X | X | X | $driver.status |
get timeouts | | | | |
|
set timeouts | X | X | X | X | $driver.timeouts ( Int $script, Int $page-load, Int $implicit ) |
navigate to | X | X | X | X | $driver.navigate ( Str $url ) |
get current url | X | X | X | X | $driver.url |
back | X | X | X | X | $driver.back |
forward | X | X | X | X | $driver.forward |
refresh | X | X | X | X | $driver.refresh |
get title | X | X | X | X | $driver.title |
get window handle | X | X | X | X | $driver.window-handle |
close window | X | X | X | X | $driver.close-window |
switch to window | X | X | X | X | $driver.switch-to-window ( $handle ) |
get window handles | X | X | X | X | $driver.window-handles |
new window | | | | |
|
switch to frame | X | X | X | X | $driver.switch-to ( Int $frame-id )
$frame-element.switch-to |
switch to parent frame | X | X | X | X | $driver.switch-to-parent
$element.switch-to-parent |
get window rect | | | | |
|
set window rect | X | X | X | X | $driver.set-window-rect (
Int $width, Int $height, Int $x, Int $y
) |
maximize window | | | | | $driver.maximize-window |
minimize window | | | | |
|
fullscreen window | | | | |
|
get active element | X | X | X | X | $driver.active |
get element shadow root | | | | |
|
find element | X | X | X | X | $driver.element ( Locator $loc ) |
find elements | X | X | X | X | $driver.elements ( Locator $loc ) |
find element from element | X | X | X | X | $element.element ( Locator $loc ) |
find elements from element | X | X | X | X | $element.elements ( Locator $loc ) |
find element from shadow root | | | | |
|
find elements from shadow root | | | | |
|
is element selected | X | X | X | X | $element.selected |
get element attribute | X | X | X | X | $element.attribute |
get element property | X | X | X | X | $element.property |
get element css value | X | X | X | X | $element.css-value ( Str $css-prop ) |
get element text | X | X | X | X | $element.text |
get element tag name | X | X | X | X | $element.tag-name |
get element rect | | | | |
|
is element enabled | X | X | X | X | $element.enabled |
get computed role | | | | |
|
get computed label | | | | |
|
element click | X | X | X | X | $element.click |
element clear | X | X | X | X | $element.clear |
element send keys | / | / | / | / | $element.send-keys ( $text ) |
get page source | | | | |
|
execute script | X | X | X | X | $driver.execute-script ( Str $scr, @args ) |
execute async script | | | | |
|
get all cookies | | | | |
|
get named cookie | | | | |
|
add cookie | | | | |
|
delete cookie | | | | |
|
delete all cookies | | | | |
|
perform actions | | | | |
|
release actions | | | | |
|
dismiss alert | X | X | X | X | $driver.dismiss-alert |
accept alert | X | X | X | X | $driver.accept-alert |
get alert text | X | X | X | X | $driver.alert-text |
send alert text | X | X | X | X | $driver.send-alert-text ( Str $text ) |
take screenshot | X | X | X | X | $driver.screenshot |
take element screenshot | X | X | X | X | $element.screenshot |
print page | | | | |
|
displayed ( optional endpoint ) | X | X | X | ! apple does not implement | $element.displayed |