Rand Stats

WebDriver2

zef:zoss

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:

  1. fix content length (geckodriver does not gracefully handle incorrect content lengths)
  2. increase amount of info logged (originally capped at 300 characters per entry)

TODO

Feedback

Suggestions, design recommendations, and feature requests welcome.

Implementation Status

 WindowsMacOS 
endpointchromeedgefirefoxsafarimethod
new sessionXXXX$driver.session
delete sessionXXXX$driver.delete-session
statusXXXX$driver.status
get timeouts    
set timeoutsXXXX$driver.timeouts ( Int $script, Int $page-load, Int $implicit )
navigate toXXXX$driver.navigate ( Str $url )
get current urlXXXX$driver.url
backXXXX$driver.back
forwardXXXX$driver.forward
refreshXXXX$driver.refresh
get titleXXXX$driver.title
get window handleXXXX$driver.window-handle
close windowXXXX$driver.close-window
switch to windowXXXX$driver.switch-to-window ( $handle )
get window handlesXXXX$driver.window-handles
new window    
switch to frameXXXX$driver.switch-to ( Int $frame-id ) $frame-element.switch-to
switch to parent frameXXXX$driver.switch-to-parent $element.switch-to-parent
get window rect    
set window rectXXXX$driver.set-window-rect ( Int $width, Int $height, Int $x, Int $y )
maximize window    $driver.maximize-window
minimize window    
fullscreen window    
get active elementXXXX$driver.active
get element shadow root    
find elementXXXX$driver.element ( Locator $loc )
find elementsXXXX$driver.elements ( Locator $loc )
find element from elementXXXX$element.element ( Locator $loc )
find elements from elementXXXX$element.elements ( Locator $loc )
find element from shadow root    
find elements from shadow root    
is element selectedXXXX$element.selected
get element attributeXXXX$element.attribute
get element propertyXXXX$element.property
get element css valueXXXX$element.css-value ( Str $css-prop )
get element textXXXX$element.text
get element tag nameXXXX$element.tag-name
get element rect    
is element enabledXXXX$element.enabled
get computed role    
get computed label    
element clickXXXX$element.click
element clearXXXX$element.clear
element send keys////$element.send-keys ( $text )
get page source    
execute scriptXXXX$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 alertXXXX$driver.dismiss-alert
accept alertXXXX$driver.accept-alert
get alert textXXXX$driver.alert-text
send alert textXXXX$driver.send-alert-text ( Str $text )
take screenshotXXXX$driver.screenshot
take element screenshotXXXX$element.screenshot
print page    
displayed ( optional endpoint )XXX! apple does not implement$element.displayed