WebDriver2

WebDriver level 2 bindings

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:

  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

  • 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

WebDriver2 v0.1.0

WebDriver level 2 bindings

Authors

  • zoss

License

Artistic-2.0

Dependencies

JSON::TinyURI::EncodeHTTP::StatusFile::TempDateTime::ParseEncodeMIME::Base64URIIO::Socket::SSL

Test Dependencies

Provides

  • WebDriver2
  • WebDriver2::Command
  • WebDriver2::Command::Accept-Alert
  • WebDriver2::Command::Active
  • WebDriver2::Command::Alert-Text
  • WebDriver2::Command::Attribute
  • WebDriver2::Command::CSS-Value
  • WebDriver2::Command::Clear
  • WebDriver2::Command::Click
  • WebDriver2::Command::Close-Window
  • WebDriver2::Command::Delete-Session
  • WebDriver2::Command::Dismiss-Alert
  • WebDriver2::Command::Displayed
  • WebDriver2::Command::Element
  • WebDriver2::Command::Element-Rect
  • WebDriver2::Command::Element-Screenshot
  • WebDriver2::Command::Element::Locator
  • WebDriver2::Command::Element::Locator::CSS
  • WebDriver2::Command::Element::Locator::ID
  • WebDriver2::Command::Element::Locator::Link-Text
  • WebDriver2::Command::Element::Locator::Tag-Name
  • WebDriver2::Command::Element::Locator::Xpath
  • WebDriver2::Command::Elements
  • WebDriver2::Command::Enabled
  • WebDriver2::Command::Execute-Script
  • WebDriver2::Command::Execution-Status
  • WebDriver2::Command::ID
  • WebDriver2::Command::Keys
  • WebDriver2::Command::Maximize-Window
  • WebDriver2::Command::Navigate
  • WebDriver2::Command::New-Window
  • WebDriver2::Command::Param
  • WebDriver2::Command::Param::Factory
  • WebDriver2::Command::Param::Factory::Chrome
  • WebDriver2::Command::Param::Factory::Edge
  • WebDriver2::Command::Param::Factory::Firefox
  • WebDriver2::Command::Param::Factory::Safari
  • WebDriver2::Command::PreResult
  • WebDriver2::Command::Property
  • WebDriver2::Command::Refresh
  • WebDriver2::Command::Result
  • WebDriver2::Command::Result::Accept-Alert
  • WebDriver2::Command::Result::Active
  • WebDriver2::Command::Result::Alert-Text
  • WebDriver2::Command::Result::Attribute
  • WebDriver2::Command::Result::Bool-Value
  • WebDriver2::Command::Result::CSS-Value
  • WebDriver2::Command::Result::Clear
  • WebDriver2::Command::Result::Click
  • WebDriver2::Command::Result::Close-Window
  • WebDriver2::Command::Result::Delete-Session
  • WebDriver2::Command::Result::Dismiss-Alert
  • WebDriver2::Command::Result::Element
  • WebDriver2::Command::Result::Element-Rect
  • WebDriver2::Command::Result::Element-Screenshot
  • WebDriver2::Command::Result::Elements
  • WebDriver2::Command::Result::Enabled
  • WebDriver2::Command::Result::Execute-Script
  • WebDriver2::Command::Result::Factory
  • WebDriver2::Command::Result::Factory::Chrome
  • WebDriver2::Command::Result::Factory::Edge
  • WebDriver2::Command::Result::Factory::Firefox
  • WebDriver2::Command::Result::Factory::Safari
  • WebDriver2::Command::Result::List-Value
  • WebDriver2::Command::Result::Maximize-Window
  • WebDriver2::Command::Result::Navigate
  • WebDriver2::Command::Result::New-Window
  • WebDriver2::Command::Result::Property
  • WebDriver2::Command::Result::Refresh
  • WebDriver2::Command::Result::Screenshot
  • WebDriver2::Command::Result::Selected
  • WebDriver2::Command::Result::Send-Alert-Text
  • WebDriver2::Command::Result::Send-Keys
  • WebDriver2::Command::Result::Session
  • WebDriver2::Command::Result::Set-Window-Rect
  • WebDriver2::Command::Result::Single-Value
  • WebDriver2::Command::Result::Status
  • WebDriver2::Command::Result::Str-Hash-Value
  • WebDriver2::Command::Result::SubElement
  • WebDriver2::Command::Result::SubElements
  • WebDriver2::Command::Result::Switch-To
  • WebDriver2::Command::Result::Switch-To-Parent
  • WebDriver2::Command::Result::Switch-To-Window
  • WebDriver2::Command::Result::Tag-Name
  • WebDriver2::Command::Result::Text
  • WebDriver2::Command::Result::Timeouts
  • WebDriver2::Command::Result::Title
  • WebDriver2::Command::Result::URL
  • WebDriver2::Command::Result::Window-Handle
  • WebDriver2::Command::Result::Window-Handles
  • WebDriver2::Command::Result::X
  • WebDriver2::Command::Screenshot
  • WebDriver2::Command::Selected
  • WebDriver2::Command::Send-Alert-Text
  • WebDriver2::Command::Send-Keys
  • WebDriver2::Command::Session
  • WebDriver2::Command::Set-Window-Rect
  • WebDriver2::Command::Status
  • WebDriver2::Command::SubElement
  • WebDriver2::Command::SubElements
  • WebDriver2::Command::Switch-To
  • WebDriver2::Command::Switch-To-Parent
  • WebDriver2::Command::Switch-to-Window
  • WebDriver2::Command::Tag-Name
  • WebDriver2::Command::Text
  • WebDriver2::Command::Timeouts
  • WebDriver2::Command::Title
  • WebDriver2::Command::URL
  • WebDriver2::Command::Window-Handle
  • WebDriver2::Command::Window-Handles
  • WebDriver2::Constants
  • WebDriver2::Driver
  • WebDriver2::Driver-Actions
  • WebDriver2::Driver::Chrome
  • WebDriver2::Driver::Edge
  • WebDriver2::Driver::Firefox
  • WebDriver2::Driver::Provider
  • WebDriver2::Driver::Safari
  • WebDriver2::Driver::Server
  • WebDriver2::Element-Actions
  • WebDriver2::HTTP::Cookie
  • WebDriver2::HTTP::Cookies
  • WebDriver2::HTTP::Header
  • WebDriver2::HTTP::Header::Field
  • WebDriver2::HTTP::MediaType
  • WebDriver2::HTTP::Message
  • WebDriver2::HTTP::Request
  • WebDriver2::HTTP::Request::Common
  • WebDriver2::HTTP::Response
  • WebDriver2::HTTP::UserAgent
  • WebDriver2::HTTP::UserAgent::Common
  • WebDriver2::HTTP::UserAgent::Exception
  • WebDriver2::Model::Context
  • WebDriver2::Model::Element
  • WebDriver2::Model::Frame
  • WebDriver2::SUT::Build
  • WebDriver2::SUT::Build::Page
  • WebDriver2::SUT::Navigator
  • WebDriver2::SUT::Navigator::Path
  • WebDriver2::SUT::Provider
  • WebDriver2::SUT::ProviderR
  • WebDriver2::SUT::Service
  • WebDriver2::SUT::Service::Loader
  • WebDriver2::SUT::Tree
  • WebDriver2::SUT::Tree::AFrame
  • WebDriver2::SUT::Tree::ANode
  • WebDriver2::SUT::Tree::APage
  • WebDriver2::SUT::Tree::Element
  • WebDriver2::SUT::Tree::Fragile
  • WebDriver2::SUT::Tree::Fragile-Visitor
  • WebDriver2::SUT::Tree::Frame
  • WebDriver2::SUT::Tree::Frame-Visitor
  • WebDriver2::SUT::Tree::List-Item
  • WebDriver2::SUT::Tree::Locatable
  • WebDriver2::SUT::Tree::Named
  • WebDriver2::SUT::Tree::Observer
  • WebDriver2::SUT::Tree::Page
  • WebDriver2::SUT::Tree::Resolvable
  • WebDriver2::SUT::Tree::Resolve-Allable
  • WebDriver2::SUT::Tree::SUT
  • WebDriver2::SUT::Tree::Select
  • WebDriver2::SUT::Tree::Subject
  • WebDriver2::SUT::Tree::URL
  • WebDriver2::SUT::Tree::Visitable
  • WebDriver2::SUT::Tree::Visitor
  • WebDriver2::Test
  • WebDriver2::Test::Config-From-File
  • WebDriver2::Test::Locating-Test
  • WebDriver2::Test::PO-Test
  • WebDriver2::Test::PO-TestR
  • WebDriver2::Test::Service-Test
  • WebDriver2::Test::Service-TestR
  • WebDriver2::Test::Template
  • WebDriver2::Test::TestR
  • WebDriver2::Until
  • WebDriver2::Until::Command
  • WebDriver2::Until::Command::Displayed
  • WebDriver2::Until::Command::Frame
  • WebDriver2::Until::Command::Not-Displayed
  • WebDriver2::Until::Command::Stale
  • WebDriver2::Until::Command::Text-To-Be
  • WebDriver2::Until::Command::Throwable
  • WebDriver2::Until::Command::Value-Not-Empty
  • WebDriver2::Until::Command::Value-To-Be
  • WebDriver2::Until::No-Throw
  • WebDriver2::Until::SUT::Present
  • WebDriver2::Until::Throwable
  • WebDriver2::Until::Throws
  • WebDriver2::Until::Timeout::X
  • WebDriver2::Until::Title-Is

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.