Security update for permissions_by_term
[yaffs-website] / vendor / behat / mink-selenium2-driver / src / Selenium2Driver.php
1 <?php
2
3 /*
4  * This file is part of the Behat\Mink.
5  * (c) Konstantin Kudryashov <ever.zet@gmail.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 namespace Behat\Mink\Driver;
12
13 use Behat\Mink\Exception\DriverException;
14 use Behat\Mink\Selector\Xpath\Escaper;
15 use WebDriver\Element;
16 use WebDriver\Exception\NoSuchElement;
17 use WebDriver\Exception\UnknownError;
18 use WebDriver\Exception;
19 use WebDriver\Key;
20 use WebDriver\WebDriver;
21
22 /**
23  * Selenium2 driver.
24  *
25  * @author Pete Otaqui <pete@otaqui.com>
26  */
27 class Selenium2Driver extends CoreDriver
28 {
29     /**
30      * Whether the browser has been started
31      * @var Boolean
32      */
33     private $started = false;
34
35     /**
36      * The WebDriver instance
37      * @var WebDriver
38      */
39     private $webDriver;
40
41     /**
42      * @var string
43      */
44     private $browserName;
45
46     /**
47      * @var array
48      */
49     private $desiredCapabilities;
50
51     /**
52      * The WebDriverSession instance
53      * @var \WebDriver\Session
54      */
55     private $wdSession;
56
57     /**
58      * The timeout configuration
59      * @var array
60      */
61     private $timeouts = array();
62
63     /**
64      * @var Escaper
65      */
66     private $xpathEscaper;
67
68     /**
69      * Instantiates the driver.
70      *
71      * @param string $browserName         Browser name
72      * @param array  $desiredCapabilities The desired capabilities
73      * @param string $wdHost              The WebDriver host
74      */
75     public function __construct($browserName = 'firefox', $desiredCapabilities = null, $wdHost = 'http://localhost:4444/wd/hub')
76     {
77         $this->setBrowserName($browserName);
78         $this->setDesiredCapabilities($desiredCapabilities);
79         $this->setWebDriver(new WebDriver($wdHost));
80         $this->xpathEscaper = new Escaper();
81     }
82
83     /**
84      * Sets the browser name
85      *
86      * @param string $browserName the name of the browser to start, default is 'firefox'
87      */
88     protected function setBrowserName($browserName = 'firefox')
89     {
90         $this->browserName = $browserName;
91     }
92
93     /**
94      * Sets the desired capabilities - called on construction.  If null is provided, will set the
95      * defaults as desired.
96      *
97      * See http://code.google.com/p/selenium/wiki/DesiredCapabilities
98      *
99      * @param array $desiredCapabilities an array of capabilities to pass on to the WebDriver server
100      */
101     public function setDesiredCapabilities($desiredCapabilities = null)
102     {
103         if (null === $desiredCapabilities) {
104             $desiredCapabilities = self::getDefaultCapabilities();
105         }
106
107         if (isset($desiredCapabilities['firefox'])) {
108             foreach ($desiredCapabilities['firefox'] as $capability => $value) {
109                 switch ($capability) {
110                     case 'profile':
111                         $desiredCapabilities['firefox_'.$capability] = base64_encode(file_get_contents($value));
112                         break;
113                     default:
114                         $desiredCapabilities['firefox_'.$capability] = $value;
115                 }
116             }
117
118             unset($desiredCapabilities['firefox']);
119         }
120
121         // See https://sites.google.com/a/chromium.org/chromedriver/capabilities
122         if (isset($desiredCapabilities['chrome'])) {
123
124             $chromeOptions = array();
125
126             foreach ($desiredCapabilities['chrome'] as $capability => $value) {
127                 if ($capability == 'switches') {
128                     $chromeOptions['args'] = $value;
129                 } else {
130                     $chromeOptions[$capability] = $value;
131                 }
132                 $desiredCapabilities['chrome.'.$capability] = $value;
133             }
134
135             $desiredCapabilities['chromeOptions'] = $chromeOptions;
136
137             unset($desiredCapabilities['chrome']);
138         }
139
140         $this->desiredCapabilities = $desiredCapabilities;
141     }
142
143     /**
144      * Sets the WebDriver instance
145      *
146      * @param WebDriver $webDriver An instance of the WebDriver class
147      */
148     public function setWebDriver(WebDriver $webDriver)
149     {
150         $this->webDriver = $webDriver;
151     }
152
153     /**
154      * Gets the WebDriverSession instance
155      *
156      * @return \WebDriver\Session
157      */
158     public function getWebDriverSession()
159     {
160         return $this->wdSession;
161     }
162
163     /**
164      * Returns the default capabilities
165      *
166      * @return array
167      */
168     public static function getDefaultCapabilities()
169     {
170         return array(
171             'browserName'       => 'firefox',
172             'version'           => '9',
173             'platform'          => 'ANY',
174             'browserVersion'    => '9',
175             'browser'           => 'firefox',
176             'name'              => 'Behat Test',
177             'deviceOrientation' => 'portrait',
178             'deviceType'        => 'tablet',
179             'selenium-version'  => '2.31.0'
180         );
181     }
182
183     /**
184      * Makes sure that the Syn event library has been injected into the current page,
185      * and return $this for a fluid interface,
186      *
187      *     $this->withSyn()->executeJsOnXpath($xpath, $script);
188      *
189      * @return Selenium2Driver
190      */
191     protected function withSyn()
192     {
193         $hasSyn = $this->wdSession->execute(array(
194             'script' => 'return typeof window["Syn"]!=="undefined" && typeof window["Syn"].trigger!=="undefined"',
195             'args'   => array()
196         ));
197
198         if (!$hasSyn) {
199             $synJs = file_get_contents(__DIR__.'/Resources/syn.js');
200             $this->wdSession->execute(array(
201                 'script' => $synJs,
202                 'args'   => array()
203             ));
204         }
205
206         return $this;
207     }
208
209     /**
210      * Creates some options for key events
211      *
212      * @param string $char     the character or code
213      * @param string $modifier one of 'shift', 'alt', 'ctrl' or 'meta'
214      *
215      * @return string a json encoded options array for Syn
216      */
217     protected static function charToOptions($char, $modifier = null)
218     {
219         $ord = ord($char);
220         if (is_numeric($char)) {
221             $ord = $char;
222         }
223
224         $options = array(
225             'keyCode'  => $ord,
226             'charCode' => $ord
227         );
228
229         if ($modifier) {
230             $options[$modifier.'Key'] = 1;
231         }
232
233         return json_encode($options);
234     }
235
236     /**
237      * Executes JS on a given element - pass in a js script string and {{ELEMENT}} will
238      * be replaced with a reference to the result of the $xpath query
239      *
240      * @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length');
241      *
242      * @param string  $xpath  the xpath to search with
243      * @param string  $script the script to execute
244      * @param Boolean $sync   whether to run the script synchronously (default is TRUE)
245      *
246      * @return mixed
247      */
248     protected function executeJsOnXpath($xpath, $script, $sync = true)
249     {
250         return $this->executeJsOnElement($this->findElement($xpath), $script, $sync);
251     }
252
253     /**
254      * Executes JS on a given element - pass in a js script string and {{ELEMENT}} will
255      * be replaced with a reference to the element
256      *
257      * @example $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.childNodes.length');
258      *
259      * @param Element $element the webdriver element
260      * @param string  $script  the script to execute
261      * @param Boolean $sync    whether to run the script synchronously (default is TRUE)
262      *
263      * @return mixed
264      */
265     private function executeJsOnElement(Element $element, $script, $sync = true)
266     {
267         $script  = str_replace('{{ELEMENT}}', 'arguments[0]', $script);
268
269         $options = array(
270             'script' => $script,
271             'args'   => array(array('ELEMENT' => $element->getID())),
272         );
273
274         if ($sync) {
275             return $this->wdSession->execute($options);
276         }
277
278         return $this->wdSession->execute_async($options);
279     }
280
281     /**
282      * {@inheritdoc}
283      */
284     public function start()
285     {
286         try {
287             $this->wdSession = $this->webDriver->session($this->browserName, $this->desiredCapabilities);
288             $this->applyTimeouts();
289         } catch (\Exception $e) {
290             throw new DriverException('Could not open connection: '.$e->getMessage(), 0, $e);
291         }
292
293         if (!$this->wdSession) {
294             throw new DriverException('Could not connect to a Selenium 2 / WebDriver server');
295         }
296         $this->started = true;
297     }
298
299     /**
300      * Sets the timeouts to apply to the webdriver session
301      *
302      * @param array $timeouts The session timeout settings: Array of {script, implicit, page} => time in milliseconds
303      *
304      * @throws DriverException
305      */
306     public function setTimeouts($timeouts)
307     {
308         $this->timeouts = $timeouts;
309
310         if ($this->isStarted()) {
311             $this->applyTimeouts();
312         }
313     }
314
315     /**
316      * Applies timeouts to the current session
317      */
318     private function applyTimeouts()
319     {
320         try {
321             foreach ($this->timeouts as $type => $param) {
322                 $this->wdSession->timeouts($type, $param);
323             }
324         } catch (UnknownError $e) {
325             throw new DriverException('Error setting timeout: ' . $e->getMessage(), 0, $e);
326         }
327     }
328
329     /**
330      * {@inheritdoc}
331      */
332     public function isStarted()
333     {
334         return $this->started;
335     }
336
337     /**
338      * {@inheritdoc}
339      */
340     public function stop()
341     {
342         if (!$this->wdSession) {
343             throw new DriverException('Could not connect to a Selenium 2 / WebDriver server');
344         }
345
346         $this->started = false;
347         try {
348             $this->wdSession->close();
349         } catch (\Exception $e) {
350             throw new DriverException('Could not close connection', 0, $e);
351         }
352     }
353
354     /**
355      * {@inheritdoc}
356      */
357     public function reset()
358     {
359         $this->wdSession->deleteAllCookies();
360     }
361
362     /**
363      * {@inheritdoc}
364      */
365     public function visit($url)
366     {
367         $this->wdSession->open($url);
368     }
369
370     /**
371      * {@inheritdoc}
372      */
373     public function getCurrentUrl()
374     {
375         return $this->wdSession->url();
376     }
377
378     /**
379      * {@inheritdoc}
380      */
381     public function reload()
382     {
383         $this->wdSession->refresh();
384     }
385
386     /**
387      * {@inheritdoc}
388      */
389     public function forward()
390     {
391         $this->wdSession->forward();
392     }
393
394     /**
395      * {@inheritdoc}
396      */
397     public function back()
398     {
399         $this->wdSession->back();
400     }
401
402     /**
403      * {@inheritdoc}
404      */
405     public function switchToWindow($name = null)
406     {
407         $this->wdSession->focusWindow($name ? $name : '');
408     }
409
410     /**
411      * {@inheritdoc}
412      */
413     public function switchToIFrame($name = null)
414     {
415         $this->wdSession->frame(array('id' => $name));
416     }
417
418     /**
419      * {@inheritdoc}
420      */
421     public function setCookie($name, $value = null)
422     {
423         if (null === $value) {
424             $this->wdSession->deleteCookie($name);
425
426             return;
427         }
428
429         $cookieArray = array(
430             'name'   => $name,
431             'value'  => urlencode($value),
432             'secure' => false, // thanks, chibimagic!
433         );
434
435         $this->wdSession->setCookie($cookieArray);
436     }
437
438     /**
439      * {@inheritdoc}
440      */
441     public function getCookie($name)
442     {
443         $cookies = $this->wdSession->getAllCookies();
444         foreach ($cookies as $cookie) {
445             if ($cookie['name'] === $name) {
446                 return urldecode($cookie['value']);
447             }
448         }
449     }
450
451     /**
452      * {@inheritdoc}
453      */
454     public function getContent()
455     {
456         return $this->wdSession->source();
457     }
458
459     /**
460      * {@inheritdoc}
461      */
462     public function getScreenshot()
463     {
464         return base64_decode($this->wdSession->screenshot());
465     }
466
467     /**
468      * {@inheritdoc}
469      */
470     public function getWindowNames()
471     {
472         return $this->wdSession->window_handles();
473     }
474
475     /**
476      * {@inheritdoc}
477      */
478     public function getWindowName()
479     {
480         return $this->wdSession->window_handle();
481     }
482
483     /**
484      * {@inheritdoc}
485      */
486     public function findElementXpaths($xpath)
487     {
488         $nodes = $this->wdSession->elements('xpath', $xpath);
489
490         $elements = array();
491         foreach ($nodes as $i => $node) {
492             $elements[] = sprintf('(%s)[%d]', $xpath, $i+1);
493         }
494
495         return $elements;
496     }
497
498     /**
499      * {@inheritdoc}
500      */
501     public function getTagName($xpath)
502     {
503         return $this->findElement($xpath)->name();
504     }
505
506     /**
507      * {@inheritdoc}
508      */
509     public function getText($xpath)
510     {
511         $node = $this->findElement($xpath);
512         $text = $node->text();
513         $text = (string) str_replace(array("\r", "\r\n", "\n"), ' ', $text);
514
515         return $text;
516     }
517
518     /**
519      * {@inheritdoc}
520      */
521     public function getHtml($xpath)
522     {
523         return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.innerHTML;');
524     }
525
526     /**
527      * {@inheritdoc}
528      */
529     public function getOuterHtml($xpath)
530     {
531         return $this->executeJsOnXpath($xpath, 'return {{ELEMENT}}.outerHTML;');
532     }
533
534     /**
535      * {@inheritdoc}
536      */
537     public function getAttribute($xpath, $name)
538     {
539         $script = 'return {{ELEMENT}}.getAttribute(' . json_encode((string) $name) . ')';
540
541         return $this->executeJsOnXpath($xpath, $script);
542     }
543
544     /**
545      * {@inheritdoc}
546      */
547     public function getValue($xpath)
548     {
549         $element = $this->findElement($xpath);
550         $elementName = strtolower($element->name());
551         $elementType = strtolower($element->attribute('type'));
552
553         // Getting the value of a checkbox returns its value if selected.
554         if ('input' === $elementName && 'checkbox' === $elementType) {
555             return $element->selected() ? $element->attribute('value') : null;
556         }
557
558         if ('input' === $elementName && 'radio' === $elementType) {
559             $script = <<<JS
560 var node = {{ELEMENT}},
561     value = null;
562
563 var name = node.getAttribute('name');
564 if (name) {
565     var fields = window.document.getElementsByName(name),
566         i, l = fields.length;
567     for (i = 0; i < l; i++) {
568         var field = fields.item(i);
569         if (field.form === node.form && field.checked) {
570             value = field.value;
571             break;
572         }
573     }
574 }
575
576 return value;
577 JS;
578
579             return $this->executeJsOnElement($element, $script);
580         }
581
582         // Using $element->attribute('value') on a select only returns the first selected option
583         // even when it is a multiple select, so a custom retrieval is needed.
584         if ('select' === $elementName && $element->attribute('multiple')) {
585             $script = <<<JS
586 var node = {{ELEMENT}},
587     value = [];
588
589 for (var i = 0; i < node.options.length; i++) {
590     if (node.options[i].selected) {
591         value.push(node.options[i].value);
592     }
593 }
594
595 return value;
596 JS;
597
598             return $this->executeJsOnElement($element, $script);
599         }
600
601         return $element->attribute('value');
602     }
603
604     /**
605      * {@inheritdoc}
606      */
607     public function setValue($xpath, $value)
608     {
609         $element = $this->findElement($xpath);
610         $elementName = strtolower($element->name());
611
612         if ('select' === $elementName) {
613             if (is_array($value)) {
614                 $this->deselectAllOptions($element);
615
616                 foreach ($value as $option) {
617                     $this->selectOptionOnElement($element, $option, true);
618                 }
619
620                 return;
621             }
622
623             $this->selectOptionOnElement($element, $value);
624
625             return;
626         }
627
628         if ('input' === $elementName) {
629             $elementType = strtolower($element->attribute('type'));
630
631             if (in_array($elementType, array('submit', 'image', 'button', 'reset'))) {
632                 throw new DriverException(sprintf('Impossible to set value an element with XPath "%s" as it is not a select, textarea or textbox', $xpath));
633             }
634
635             if ('checkbox' === $elementType) {
636                 if ($element->selected() xor (bool) $value) {
637                     $this->clickOnElement($element);
638                 }
639
640                 return;
641             }
642
643             if ('radio' === $elementType) {
644                 $this->selectRadioValue($element, $value);
645
646                 return;
647             }
648
649             if ('file' === $elementType) {
650                 $element->postValue(array('value' => array(strval($value))));
651
652                 return;
653             }
654         }
655
656         $value = strval($value);
657
658         if (in_array($elementName, array('input', 'textarea'))) {
659             $existingValueLength = strlen($element->attribute('value'));
660             // Add the TAB key to ensure we unfocus the field as browsers are triggering the change event only
661             // after leaving the field.
662             $value = str_repeat(Key::BACKSPACE . Key::DELETE, $existingValueLength) . $value . Key::TAB;
663         }
664
665         $element->postValue(array('value' => array($value)));
666     }
667
668     /**
669      * {@inheritdoc}
670      */
671     public function check($xpath)
672     {
673         $element = $this->findElement($xpath);
674         $this->ensureInputType($element, $xpath, 'checkbox', 'check');
675
676         if ($element->selected()) {
677             return;
678         }
679
680         $this->clickOnElement($element);
681     }
682
683     /**
684      * {@inheritdoc}
685      */
686     public function uncheck($xpath)
687     {
688         $element = $this->findElement($xpath);
689         $this->ensureInputType($element, $xpath, 'checkbox', 'uncheck');
690
691         if (!$element->selected()) {
692             return;
693         }
694
695         $this->clickOnElement($element);
696     }
697
698     /**
699      * {@inheritdoc}
700      */
701     public function isChecked($xpath)
702     {
703         return $this->findElement($xpath)->selected();
704     }
705
706     /**
707      * {@inheritdoc}
708      */
709     public function selectOption($xpath, $value, $multiple = false)
710     {
711         $element = $this->findElement($xpath);
712         $tagName = strtolower($element->name());
713
714         if ('input' === $tagName && 'radio' === strtolower($element->attribute('type'))) {
715             $this->selectRadioValue($element, $value);
716
717             return;
718         }
719
720         if ('select' === $tagName) {
721             $this->selectOptionOnElement($element, $value, $multiple);
722
723             return;
724         }
725
726         throw new DriverException(sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath));
727     }
728
729     /**
730      * {@inheritdoc}
731      */
732     public function isSelected($xpath)
733     {
734         return $this->findElement($xpath)->selected();
735     }
736
737     /**
738      * {@inheritdoc}
739      */
740     public function click($xpath)
741     {
742         $this->clickOnElement($this->findElement($xpath));
743     }
744
745     private function clickOnElement(Element $element)
746     {
747         $this->wdSession->moveto(array('element' => $element->getID()));
748         $element->click();
749     }
750
751     /**
752      * {@inheritdoc}
753      */
754     public function doubleClick($xpath)
755     {
756         $this->mouseOver($xpath);
757         $this->wdSession->doubleclick();
758     }
759
760     /**
761      * {@inheritdoc}
762      */
763     public function rightClick($xpath)
764     {
765         $this->mouseOver($xpath);
766         $this->wdSession->click(array('button' => 2));
767     }
768
769     /**
770      * {@inheritdoc}
771      */
772     public function attachFile($xpath, $path)
773     {
774         $element = $this->findElement($xpath);
775         $this->ensureInputType($element, $xpath, 'file', 'attach a file on');
776
777         $element->postValue(array('value' => array($path)));
778     }
779
780     /**
781      * {@inheritdoc}
782      */
783     public function isVisible($xpath)
784     {
785         return $this->findElement($xpath)->displayed();
786     }
787
788     /**
789      * {@inheritdoc}
790      */
791     public function mouseOver($xpath)
792     {
793         $this->wdSession->moveto(array(
794             'element' => $this->findElement($xpath)->getID()
795         ));
796     }
797
798     /**
799      * {@inheritdoc}
800      */
801     public function focus($xpath)
802     {
803         $script = 'Syn.trigger("focus", {}, {{ELEMENT}})';
804         $this->withSyn()->executeJsOnXpath($xpath, $script);
805     }
806
807     /**
808      * {@inheritdoc}
809      */
810     public function blur($xpath)
811     {
812         $script = 'Syn.trigger("blur", {}, {{ELEMENT}})';
813         $this->withSyn()->executeJsOnXpath($xpath, $script);
814     }
815
816     /**
817      * {@inheritdoc}
818      */
819     public function keyPress($xpath, $char, $modifier = null)
820     {
821         $options = self::charToOptions($char, $modifier);
822         $script = "Syn.trigger('keypress', $options, {{ELEMENT}})";
823         $this->withSyn()->executeJsOnXpath($xpath, $script);
824     }
825
826     /**
827      * {@inheritdoc}
828      */
829     public function keyDown($xpath, $char, $modifier = null)
830     {
831         $options = self::charToOptions($char, $modifier);
832         $script = "Syn.trigger('keydown', $options, {{ELEMENT}})";
833         $this->withSyn()->executeJsOnXpath($xpath, $script);
834     }
835
836     /**
837      * {@inheritdoc}
838      */
839     public function keyUp($xpath, $char, $modifier = null)
840     {
841         $options = self::charToOptions($char, $modifier);
842         $script = "Syn.trigger('keyup', $options, {{ELEMENT}})";
843         $this->withSyn()->executeJsOnXpath($xpath, $script);
844     }
845
846     /**
847      * {@inheritdoc}
848      */
849     public function dragTo($sourceXpath, $destinationXpath)
850     {
851         $source      = $this->findElement($sourceXpath);
852         $destination = $this->findElement($destinationXpath);
853
854         $this->wdSession->moveto(array(
855             'element' => $source->getID()
856         ));
857
858         $script = <<<JS
859 (function (element) {
860     var event = document.createEvent("HTMLEvents");
861
862     event.initEvent("dragstart", true, true);
863     event.dataTransfer = {};
864
865     element.dispatchEvent(event);
866 }({{ELEMENT}}));
867 JS;
868         $this->withSyn()->executeJsOnElement($source, $script);
869
870         $this->wdSession->buttondown();
871         $this->wdSession->moveto(array(
872             'element' => $destination->getID()
873         ));
874         $this->wdSession->buttonup();
875
876         $script = <<<JS
877 (function (element) {
878     var event = document.createEvent("HTMLEvents");
879
880     event.initEvent("drop", true, true);
881     event.dataTransfer = {};
882
883     element.dispatchEvent(event);
884 }({{ELEMENT}}));
885 JS;
886         $this->withSyn()->executeJsOnElement($destination, $script);
887     }
888
889     /**
890      * {@inheritdoc}
891      */
892     public function executeScript($script)
893     {
894         if (preg_match('/^function[\s\(]/', $script)) {
895             $script = preg_replace('/;$/', '', $script);
896             $script = '(' . $script . ')';
897         }
898
899         $this->wdSession->execute(array('script' => $script, 'args' => array()));
900     }
901
902     /**
903      * {@inheritdoc}
904      */
905     public function evaluateScript($script)
906     {
907         if (0 !== strpos(trim($script), 'return ')) {
908             $script = 'return ' . $script;
909         }
910
911         return $this->wdSession->execute(array('script' => $script, 'args' => array()));
912     }
913
914     /**
915      * {@inheritdoc}
916      */
917     public function wait($timeout, $condition)
918     {
919         $script = "return $condition;";
920         $start = microtime(true);
921         $end = $start + $timeout / 1000.0;
922
923         do {
924             $result = $this->wdSession->execute(array('script' => $script, 'args' => array()));
925             usleep(100000);
926         } while (microtime(true) < $end && !$result);
927
928         return (bool) $result;
929     }
930
931     /**
932      * {@inheritdoc}
933      */
934     public function resizeWindow($width, $height, $name = null)
935     {
936         $this->wdSession->window($name ? $name : 'current')->postSize(
937             array('width' => $width, 'height' => $height)
938         );
939     }
940
941     /**
942      * {@inheritdoc}
943      */
944     public function submitForm($xpath)
945     {
946         $this->findElement($xpath)->submit();
947     }
948
949     /**
950      * {@inheritdoc}
951      */
952     public function maximizeWindow($name = null)
953     {
954         $this->wdSession->window($name ? $name : 'current')->maximize();
955     }
956
957     /**
958      * Returns Session ID of WebDriver or `null`, when session not started yet.
959      *
960      * @return string|null
961      */
962     public function getWebDriverSessionId()
963     {
964         return $this->isStarted() ? basename($this->wdSession->getUrl()) : null;
965     }
966
967     /**
968      * @param string $xpath
969      *
970      * @return Element
971      */
972     private function findElement($xpath)
973     {
974         return $this->wdSession->element('xpath', $xpath);
975     }
976
977     /**
978      * Selects a value in a radio button group
979      *
980      * @param Element $element An element referencing one of the radio buttons of the group
981      * @param string  $value   The value to select
982      *
983      * @throws DriverException when the value cannot be found
984      */
985     private function selectRadioValue(Element $element, $value)
986     {
987         // short-circuit when we already have the right button of the group to avoid XPath queries
988         if ($element->attribute('value') === $value) {
989             $element->click();
990
991             return;
992         }
993
994         $name = $element->attribute('name');
995
996         if (!$name) {
997             throw new DriverException(sprintf('The radio button does not have the value "%s"', $value));
998         }
999
1000         $formId = $element->attribute('form');
1001
1002         try {
1003             if (null !== $formId) {
1004                 $xpath = <<<'XPATH'
1005 //form[@id=%1$s]//input[@type="radio" and not(@form) and @name=%2$s and @value = %3$s]
1006 |
1007 //input[@type="radio" and @form=%1$s and @name=%2$s and @value = %3$s]
1008 XPATH;
1009
1010                 $xpath = sprintf(
1011                     $xpath,
1012                     $this->xpathEscaper->escapeLiteral($formId),
1013                     $this->xpathEscaper->escapeLiteral($name),
1014                     $this->xpathEscaper->escapeLiteral($value)
1015                 );
1016                 $input = $this->wdSession->element('xpath', $xpath);
1017             } else {
1018                 $xpath = sprintf(
1019                     './ancestor::form//input[@type="radio" and not(@form) and @name=%s and @value = %s]',
1020                     $this->xpathEscaper->escapeLiteral($name),
1021                     $this->xpathEscaper->escapeLiteral($value)
1022                 );
1023                 $input = $element->element('xpath', $xpath);
1024             }
1025         } catch (NoSuchElement $e) {
1026             $message = sprintf('The radio group "%s" does not have an option "%s"', $name, $value);
1027
1028             throw new DriverException($message, 0, $e);
1029         }
1030
1031         $input->click();
1032     }
1033
1034     /**
1035      * @param Element $element
1036      * @param string  $value
1037      * @param bool    $multiple
1038      */
1039     private function selectOptionOnElement(Element $element, $value, $multiple = false)
1040     {
1041         $escapedValue = $this->xpathEscaper->escapeLiteral($value);
1042         // The value of an option is the normalized version of its text when it has no value attribute
1043         $optionQuery = sprintf('.//option[@value = %s or (not(@value) and normalize-space(.) = %s)]', $escapedValue, $escapedValue);
1044         $option = $element->element('xpath', $optionQuery);
1045
1046         if ($multiple || !$element->attribute('multiple')) {
1047             if (!$option->selected()) {
1048                 $option->click();
1049             }
1050
1051             return;
1052         }
1053
1054         // Deselect all options before selecting the new one
1055         $this->deselectAllOptions($element);
1056         $option->click();
1057     }
1058
1059     /**
1060      * Deselects all options of a multiple select
1061      *
1062      * Note: this implementation does not trigger a change event after deselecting the elements.
1063      *
1064      * @param Element $element
1065      */
1066     private function deselectAllOptions(Element $element)
1067     {
1068         $script = <<<JS
1069 var node = {{ELEMENT}};
1070 var i, l = node.options.length;
1071 for (i = 0; i < l; i++) {
1072     node.options[i].selected = false;
1073 }
1074 JS;
1075
1076         $this->executeJsOnElement($element, $script);
1077     }
1078
1079     /**
1080      * Ensures the element is a checkbox
1081      *
1082      * @param Element $element
1083      * @param string  $xpath
1084      * @param string  $type
1085      * @param string  $action
1086      *
1087      * @throws DriverException
1088      */
1089     private function ensureInputType(Element $element, $xpath, $type, $action)
1090     {
1091         if ('input' !== strtolower($element->name()) || $type !== strtolower($element->attribute('type'))) {
1092             $message = 'Impossible to %s the element with XPath "%s" as it is not a %s input';
1093
1094             throw new DriverException(sprintf($message, $action, $xpath, $type));
1095         }
1096     }
1097 }