Mercurial > hg > isophonics-drupal-site
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 } |