txt: ## dist_zef-zoss-WebDriver2.md
  # WebDriver2
  
  WebDriver level 2 bindings implementing
  [W3C's specification](https://www.w3.org/TR/webdriver/).
  Current implementation status is
  [documented below](#implementation-status).
  
  ## Usage
  
  ### Using a driver directly
  
  To use a driver directly for all [endpoint commands](#implementation-status), 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](lib/WebDriver2/SUT/Build/Page.rakumod).
  
  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` |
  