Skip to content

zjmarlow/WebDriver2

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

  • 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  
endpoint chrome edge firefox safari method
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

About

WebDriver level 2 bindings

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Raku 96.1%
  • HTML 3.9%