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