WebDriver2
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 implements WebDriver2::Test
. The test class
will need to specify the browser upon instantiation:
use Test;
use WebDriver2::Test;
my IO::Path $html-file =
.add: 'test.html' with $*PROGRAM.parent.parent.add: 'content';
class Local does WebDriver2::Test {
has Bool $!screenshot;
method new ( Str $browser? is copy, Int:D :$debug = 0 ) {
self.set-from-file: $browser;
my Local:D $self =
self.bless:
:$browser,
:$debug,
plan => 39,
name => 'local',
description => 'local test';
$self.init;
$self;
}
method test {
$.driver.navigate: 'file://' ~ $html-file.absolute;
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 ...
}
}
WebDriver2::Test
(indirectly) does
WebDriver2::Test::Template
, which will call
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 extending the
provided WebDriver2::Test
.
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 =
.add: 'doc-login.html'
with $*PROGRAM.parent.parent.add: 'content';
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