comparison vendor/behat/mink-selenium2-driver/src/Selenium2Driver.php @ 14:1fec387a4317

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