1 | <?php |
||
2 | declare(strict_types=1); |
||
3 | |||
4 | /* |
||
5 | * This file is part of the Behat\Mink. |
||
6 | * (c) Robert Freigang <[email protected]> |
||
7 | * |
||
8 | * For the full copyright and license information, please view the LICENSE |
||
9 | * file that was distributed with this source code. |
||
10 | */ |
||
11 | |||
12 | namespace Behat\Mink\Driver; |
||
13 | |||
14 | use Behat\Mink\Exception\DriverException; |
||
15 | use Behat\Mink\Exception\UnsupportedDriverActionException; |
||
16 | use Facebook\WebDriver\Exception\UnsupportedOperationException; |
||
17 | use Facebook\WebDriver\Interactions\Internal\WebDriverCoordinates; |
||
18 | use Facebook\WebDriver\Interactions\WebDriverActions; |
||
19 | use Facebook\WebDriver\Internal\WebDriverLocatable; |
||
20 | use Facebook\WebDriver\JavaScriptExecutor; |
||
21 | use Facebook\WebDriver\Remote\RemoteWebDriver; |
||
22 | use Facebook\WebDriver\Remote\RemoteWebElement; |
||
23 | use Facebook\WebDriver\WebDriverDimension; |
||
24 | use Facebook\WebDriver\WebDriverElement; |
||
25 | use Facebook\WebDriver\WebDriverHasInputDevices; |
||
26 | use Facebook\WebDriver\WebDriverKeys; |
||
27 | use Symfony\Component\BrowserKit\Cookie; |
||
28 | use Symfony\Component\BrowserKit\Response; |
||
29 | use Symfony\Component\DomCrawler\Field\FormField; |
||
30 | use Symfony\Component\Panther\Client; |
||
31 | use Symfony\Component\Panther\DomCrawler\Crawler; |
||
32 | use Symfony\Component\Panther\DomCrawler\Field\ChoiceFormField; |
||
33 | use Symfony\Component\Panther\DomCrawler\Field\FileFormField; |
||
34 | use Symfony\Component\Panther\DomCrawler\Field\InputFormField; |
||
35 | use Symfony\Component\Panther\DomCrawler\Field\TextareaFormField; |
||
36 | use Symfony\Component\Panther\PantherTestCaseTrait; |
||
37 | |||
38 | /** |
||
39 | * Symfony2 Panther driver. |
||
40 | * |
||
41 | * @author Robert Freigang <[email protected]> |
||
42 | */ |
||
43 | class PantherDriver extends CoreDriver |
||
44 | { |
||
45 | use PantherTestCaseTrait; |
||
46 | |||
47 | // PantherTestCaseTrait needs this constants; provided via "\Symfony\Component\Panther\PantherTestCase" |
||
48 | public const CHROME = 'chrome'; |
||
49 | public const FIREFOX = 'firefox'; |
||
50 | |||
51 | /** @var Client|null */ |
||
52 | private $client; |
||
53 | private $started = false; |
||
54 | private $removeScriptFromUrl = false; |
||
55 | private $removeHostFromUrl = false; |
||
56 | 10 | /** @var array */ |
|
57 | private $options; |
||
58 | /** @var array */ |
||
59 | 10 | private $kernelOptions; |
|
60 | 10 | /** @var array */ |
|
61 | private $managerOptions; |
||
62 | |||
63 | public function __construct( |
||
64 | array $options = [], |
||
65 | array $kernelOptions = [], |
||
66 | array $managerOptions = [] |
||
67 | 152 | ) { |
|
68 | $this->options = $options; |
||
69 | 152 | $this->kernelOptions = $kernelOptions; |
|
70 | 1 | $this->managerOptions = $managerOptions; |
|
71 | } |
||
72 | |||
73 | 152 | /** |
|
74 | * Returns BrowserKit HTTP client instance. |
||
75 | * |
||
76 | * @return Client |
||
77 | */ |
||
78 | public function getClient() |
||
79 | { |
||
80 | if (!$this->isStarted()) { |
||
81 | throw new DriverException('Client is not (yet) started.'); |
||
82 | } |
||
83 | |||
84 | return $this->client; |
||
85 | } |
||
86 | |||
87 | /** |
||
88 | * Tells driver to remove hostname from URL. |
||
89 | * |
||
90 | * @param Boolean $remove |
||
91 | * |
||
92 | * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead. |
||
93 | */ |
||
94 | public function setRemoveHostFromUrl($remove = true) |
||
95 | { |
||
96 | @trigger_error( |
||
97 | 'setRemoveHostFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.', |
||
98 | E_USER_DEPRECATED |
||
99 | ); |
||
100 | $this->removeHostFromUrl = (bool) $remove; |
||
101 | } |
||
102 | |||
103 | /** |
||
104 | * Tells driver to remove script name from URL. |
||
105 | * |
||
106 | * @param Boolean $remove |
||
107 | * |
||
108 | * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead. |
||
109 | */ |
||
110 | public function setRemoveScriptFromUrl($remove = true) |
||
111 | 3 | { |
|
112 | @trigger_error( |
||
113 | 3 | 'setRemoveScriptFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.', |
|
114 | 3 | E_USER_DEPRECATED |
|
115 | ); |
||
116 | 3 | $this->removeScriptFromUrl = (bool) $remove; |
|
117 | 3 | } |
|
118 | |||
119 | /** |
||
120 | * {@inheritdoc} |
||
121 | */ |
||
122 | 152 | public function start() |
|
123 | { |
||
124 | 152 | $this->client = self::createPantherClient($this->options, $this->kernelOptions, $this->managerOptions); |
|
125 | $this->client->start(); |
||
126 | |||
127 | $this->started = true; |
||
128 | } |
||
129 | |||
130 | 2 | /** |
|
131 | * {@inheritdoc} |
||
132 | 2 | */ |
|
133 | 2 | public function isStarted() |
|
134 | 2 | { |
|
135 | 2 | return $this->started; |
|
136 | } |
||
137 | |||
138 | /** |
||
139 | * {@inheritdoc} |
||
140 | 151 | */ |
|
141 | public function stop() |
||
142 | { |
||
143 | $this->getClient()->quit(); |
||
144 | 151 | self::stopWebServer(); |
|
145 | 151 | $this->started = false; |
|
146 | 151 | } |
|
147 | |||
148 | 151 | /** |
|
149 | * {@inheritdoc} |
||
150 | */ |
||
151 | public function reset() |
||
152 | 151 | { |
|
153 | // experimental |
||
154 | // $useSpeedUp = false; |
||
155 | $useSpeedUp = true; |
||
156 | 151 | if ($useSpeedUp) { |
|
0 ignored issues
–
show
introduced
by
![]() |
|||
157 | 151 | $this->getClient()->getWebDriver()->manage()->deleteAllCookies(); |
|
158 | try { |
||
159 | 151 | $history = $this->getClient()->getHistory(); |
|
160 | if ($history) { |
||
161 | $history->clear(); |
||
162 | } |
||
163 | } catch (\LogicException $e) { |
||
164 | // History is not available when using e.g. WebDriver. |
||
165 | } |
||
166 | if ( |
||
167 | $this->getClient()->getWebDriver() instanceof JavaScriptExecutor |
||
168 | && !in_array($this->getClient()->getCurrentURL(), ['', 'about:blank', 'data:,'], true) |
||
169 | ) { |
||
170 | $this->executeScript('localStorage.clear();'); |
||
171 | } |
||
172 | // not sure if we should also close all windows |
||
173 | // $lastWindowHandle = \end($this->getClient()->getWindowHandles()); |
||
174 | // if ($lastWindowHandle) { |
||
175 | // $this->getClient()->switchTo()->window($lastWindowHandle); |
||
176 | 151 | // } |
|
177 | // $this->getClient()->getWebDriver()->navigate()->refresh(); |
||
178 | // $this->getClient()->refreshCrawler(); |
||
179 | // if (\count($this->getClient()->getWindowHandles()) > 1) { |
||
180 | // $this->getClient()->getWebDriver()->close(); |
||
181 | 141 | // } |
|
182 | } else { |
||
183 | 141 | // Restarting the client resets the cookies and the history |
|
184 | 141 | $this->getClient()->restart(); |
|
185 | } |
||
186 | |||
187 | } |
||
188 | |||
189 | 7 | /** |
|
190 | * {@inheritdoc} |
||
191 | 7 | */ |
|
192 | public function visit($url) |
||
193 | { |
||
194 | $this->getClient()->get($this->prepareUrl($url)); |
||
195 | } |
||
196 | |||
197 | 3 | /** |
|
198 | * {@inheritdoc} |
||
199 | 3 | */ |
|
200 | 3 | public function getCurrentUrl() |
|
201 | { |
||
202 | return $this->getClient()->getCurrentURL(); |
||
203 | } |
||
204 | |||
205 | 1 | /** |
|
206 | * {@inheritdoc} |
||
207 | 1 | */ |
|
208 | 1 | public function reload() |
|
209 | { |
||
210 | $this->getClient()->reload(); |
||
211 | } |
||
212 | |||
213 | 1 | /** |
|
214 | * {@inheritdoc} |
||
215 | 1 | */ |
|
216 | 1 | public function forward() |
|
217 | { |
||
218 | $this->getClient()->forward(); |
||
219 | } |
||
220 | |||
221 | 1 | /** |
|
222 | * {@inheritdoc} |
||
223 | 1 | */ |
|
224 | 1 | public function back() |
|
225 | 1 | { |
|
226 | $this->getClient()->back(); |
||
227 | } |
||
228 | |||
229 | /** |
||
230 | 1 | * {@inheritdoc} |
|
231 | */ |
||
232 | 1 | public function switchToWindow($name = null) |
|
233 | 1 | { |
|
234 | 1 | $this->getClient()->switchTo()->window($name); |
|
235 | 1 | } |
|
236 | 1 | ||
237 | /** |
||
238 | * {@inheritdoc} |
||
239 | */ |
||
240 | 1 | public function switchToIFrame($name = null) |
|
241 | 1 | { |
|
242 | if (null === $name) { |
||
243 | $this->getClient()->switchTo()->defaultContent(); |
||
244 | } elseif ($name) { |
||
245 | $iFrameElement = $this->getCrawlerElement($this->getFilteredCrawler(\sprintf("//iframe[@name='%s']", $name))); |
||
246 | 4 | $this->getClient()->switchTo()->frame($iFrameElement); |
|
247 | } else { |
||
248 | 4 | $this->getClient()->switchTo()->frame(null); |
|
249 | 2 | } |
|
250 | $this->getClient()->refreshCrawler(); |
||
251 | 2 | } |
|
252 | |||
253 | /** |
||
254 | 3 | * {@inheritdoc} |
|
255 | */ |
||
256 | 3 | public function setCookie($name, $value = null) |
|
257 | 3 | { |
|
258 | if (null === $value) { |
||
259 | $this->deleteCookie($name); |
||
260 | |||
261 | return; |
||
262 | 3 | } |
|
263 | |||
264 | 3 | $jar = $this->getClient()->getCookieJar(); |
|
265 | // @see: https://github.com/w3c/webdriver/issues/1238 |
||
266 | 3 | $jar->set(new Cookie($name, \rawurlencode((string) $value))); |
|
267 | 3 | } |
|
268 | 3 | ||
269 | /** |
||
270 | * {@inheritdoc} |
||
271 | */ |
||
272 | 1 | public function getCookie($name) |
|
273 | { |
||
274 | $cookies = $this->getClient()->getCookieJar()->all(); |
||
275 | |||
276 | foreach ($cookies as $cookie) { |
||
277 | if ($cookie->getName() === $name) { |
||
278 | 14 | return \urldecode($cookie->getValue()); |
|
279 | } |
||
280 | 14 | } |
|
281 | |||
282 | return null; |
||
283 | } |
||
284 | |||
285 | /** |
||
286 | 1 | * {@inheritdoc} |
|
287 | */ |
||
288 | 1 | public function getContent() |
|
289 | { |
||
290 | return $this->getClient()->getWebDriver()->getPageSource(); |
||
291 | } |
||
292 | |||
293 | /** |
||
294 | 1 | * {@inheritdoc} |
|
295 | */ |
||
296 | 1 | public function getScreenshot($saveAs = null): string |
|
297 | { |
||
298 | return $this->getClient()->takeScreenshot($saveAs); |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | 1 | * {@inheritdoc} |
|
303 | */ |
||
304 | 1 | public function getWindowNames() |
|
305 | { |
||
306 | return $this->getClient()->getWindowHandles(); |
||
307 | } |
||
308 | |||
309 | /** |
||
310 | 2 | * {@inheritdoc} |
|
311 | */ |
||
312 | 2 | public function getWindowName() |
|
313 | { |
||
314 | return $this->getClient()->getWindowHandle(); |
||
315 | } |
||
316 | |||
317 | /** |
||
318 | 6 | * {@inheritdoc} |
|
319 | */ |
||
320 | 6 | public function isVisible($xpath) |
|
321 | 5 | { |
|
322 | return $this->getCrawlerElement($this->getFilteredCrawler($xpath))->isDisplayed(); |
||
323 | } |
||
324 | |||
325 | /** |
||
326 | 2 | * {@inheritdoc}. |
|
327 | */ |
||
328 | 2 | public function mouseOver($xpath) |
|
329 | 2 | { |
|
330 | 2 | $this->getClient()->getMouse()->mouseMove($this->toCoordinates($xpath)); |
|
331 | 1 | } |
|
332 | |||
333 | /** |
||
334 | * {@inheritdoc} |
||
335 | */ |
||
336 | 2 | public function focus($xpath) |
|
337 | { |
||
338 | 2 | $jsNode = $this->getJsNode($xpath); |
|
339 | 2 | $script = \sprintf('%s.focus()', $jsNode); |
|
340 | $this->executeScript($script); |
||
341 | 2 | } |
|
342 | 2 | ||
343 | /** |
||
344 | 2 | * {@inheritdoc} |
|
345 | 1 | */ |
|
346 | public function blur($xpath) |
||
347 | { |
||
348 | $jsNode = $this->getJsNode($xpath); |
||
349 | $script = \sprintf('%s.blur();', $jsNode); |
||
350 | 6 | // ensure element had active state; just for passing EventsTest::testBlur |
|
351 | if ($this->evaluateScript(\sprintf('document.activeElement !== %s', $jsNode))) { |
||
352 | 6 | $script = \sprintf('%s.focus();%s', $jsNode, $script); |
|
353 | 6 | } |
|
354 | 5 | $this->executeScript($script); |
|
355 | } |
||
356 | 5 | ||
357 | /** |
||
358 | 5 | * {@inheritdoc} |
|
359 | 4 | */ |
|
360 | 4 | public function keyPress($xpath, $char, $modifier = null) |
|
361 | 4 | { |
|
362 | $webDriverActions = $this->getWebDriverActions(); |
||
363 | 1 | $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath)); |
|
364 | $key = $this->geWebDriverKeyValue($char); |
||
365 | 5 | ||
366 | $modifier = $this->getWebdriverModifierKeyValue($modifier); |
||
367 | |||
368 | if ($modifier) { |
||
369 | $webDriverActions->keyDown($element, $modifier)->perform(); |
||
370 | 6 | $webDriverActions->sendKeys($element, $key)->perform(); |
|
371 | $webDriverActions->keyUp($element, $modifier)->perform(); |
||
372 | 6 | } else { |
|
373 | 6 | $webDriverActions->sendKeys($element, $key)->perform(); |
|
374 | 5 | } |
|
375 | } |
||
376 | 5 | ||
377 | 5 | /** |
|
378 | 5 | * {@inheritdoc} |
|
379 | 4 | */ |
|
380 | public function keyDown($xpath, $char, $modifier = null) |
||
381 | 5 | { |
|
382 | $webDriverActions = $this->getWebDriverActions(); |
||
383 | $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath)); |
||
384 | $key = $this->geWebDriverKeyValue($char); |
||
385 | |||
386 | 6 | $modifier = $this->getWebdriverModifierKeyValue($modifier); |
|
387 | if ($modifier) { |
||
388 | 6 | $webDriverActions->keyDown($element, $modifier)->perform(); |
|
389 | 6 | } |
|
390 | 5 | $webDriverActions->sendKeys($element, $key)->perform(); |
|
391 | if ($modifier) { |
||
392 | 5 | $webDriverActions->keyUp($element, $modifier)->perform(); |
|
393 | 5 | } |
|
394 | 4 | } |
|
395 | |||
396 | /** |
||
397 | 5 | * {@inheritdoc} |
|
398 | 5 | */ |
|
399 | 5 | public function keyUp($xpath, $char, $modifier = null) |
|
400 | 4 | { |
|
401 | $webDriverActions = $this->getWebDriverActions(); |
||
402 | 5 | $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath)); |
|
403 | $key = $this->geWebDriverKeyValue($char); |
||
404 | |||
405 | $modifier = $this->getWebdriverModifierKeyValue($modifier); |
||
406 | if ($modifier) { |
||
407 | 3 | $webDriverActions->keyDown($element, $modifier)->perform(); |
|
408 | } |
||
409 | 3 | $webDriverActions->sendKeys($element, $key)->perform(); |
|
410 | if ($modifier) { |
||
411 | $webDriverActions->keyUp($element, $modifier)->perform(); |
||
412 | } |
||
413 | } |
||
414 | |||
415 | 96 | /** |
|
416 | * {@inheritdoc} |
||
417 | 96 | */ |
|
418 | public function isSelected($xpath) |
||
419 | 96 | { |
|
420 | 96 | return $this->getCrawlerElement($this->getFilteredCrawler($xpath))->isSelected(); |
|
421 | 96 | } |
|
422 | |||
423 | /** |
||
424 | 96 | * {@inheritdoc} |
|
425 | */ |
||
426 | public function findElementXpaths($xpath) |
||
427 | { |
||
428 | $this->getClient()->refreshCrawler(); |
||
429 | $nodes = $this->getCrawler()->filterXPath($xpath); |
||
430 | 11 | ||
431 | $elements = []; |
||
432 | 11 | foreach ($nodes as $i => $node) { |
|
433 | $elements[] = sprintf('(%s)[%d]', $xpath, $i + 1); |
||
434 | } |
||
435 | |||
436 | return $elements; |
||
437 | } |
||
438 | 58 | ||
439 | /** |
||
440 | 58 | * {@inheritdoc} |
|
441 | 57 | */ |
|
442 | 57 | public function getTagName($xpath) |
|
443 | { |
||
444 | 57 | return $this->getCrawlerElement($this->getFilteredCrawler($xpath))->getTagName(); |
|
445 | } |
||
446 | |||
447 | /** |
||
448 | * {@inheritdoc} |
||
449 | */ |
||
450 | 5 | public function getText($xpath) |
|
451 | { |
||
452 | $this->getClient()->refreshCrawler(); |
||
453 | 5 | $text = $this->getFilteredCrawler($xpath)->text(); |
|
454 | $text = str_replace("\n", ' ', $text); |
||
455 | $text = preg_replace('/ {2,}/', ' ', $text); |
||
456 | |||
457 | return trim($text); |
||
458 | } |
||
459 | 7 | ||
460 | /** |
||
461 | 7 | * {@inheritdoc} |
|
462 | */ |
||
463 | 5 | public function getHtml($xpath) |
|
464 | { |
||
465 | // cut the tag itself (making innerHTML out of outerHTML) |
||
466 | return preg_replace('/^\<[^\>]+\>|\<[^\>]+\>$/', '', $this->getOuterHtml($xpath)); |
||
467 | } |
||
468 | |||
469 | 10 | /** |
|
470 | * {@inheritdoc} |
||
471 | 10 | */ |
|
472 | public function getOuterHtml($xpath) |
||
473 | 9 | { |
|
474 | $crawler = $this->getFilteredCrawler($xpath); |
||
475 | |||
476 | 9 | $crawlerElement = $this->getCrawlerElement($crawler); |
|
477 | 3 | if ($crawlerElement instanceof RemoteWebElement) { |
|
478 | 3 | $webDriver = $this->getClient()->getWebDriver(); |
|
479 | 3 | if ($webDriver instanceof RemoteWebDriver && $webDriver->isW3cCompliant()) { |
|
480 | 1 | try { |
|
481 | return $crawlerElement->getDomProperty('outerHTML'); |
||
482 | } catch (UnsupportedOperationException $e) { |
||
483 | throw new DriverException($e->getMessage(), $e->getCode(), $e); |
||
484 | 9 | } |
|
485 | } |
||
486 | } |
||
487 | |||
488 | return $crawler->html(); |
||
489 | } |
||
490 | 22 | ||
491 | /** |
||
492 | * {@inheritdoc} |
||
493 | 22 | */ |
|
494 | 17 | public function getAttribute($xpath, $name) |
|
495 | 17 | { |
|
496 | 17 | $crawler = $this->getFilteredCrawler($xpath); |
|
497 | |||
498 | 7 | return $this->getCrawlerElement($crawler)->getAttribute($name); |
|
499 | } |
||
500 | 7 | ||
501 | 6 | /** |
|
502 | * {@inheritdoc} |
||
503 | */ |
||
504 | 21 | public function getValue($xpath) |
|
505 | { |
||
506 | try { |
||
507 | $formField = $this->getFormField($xpath); |
||
508 | $value = $formField->getValue(); |
||
509 | if ('' === $value && $formField instanceof ChoiceFormField) { |
||
510 | 32 | $value = null; |
|
511 | } |
||
512 | 32 | } catch (DriverException $e) { |
|
513 | 31 | // e.g. element is an option |
|
514 | $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath)); |
||
515 | 31 | $value = $element->getAttribute('value'); |
|
516 | 1 | } |
|
517 | |||
518 | return $value; |
||
519 | 31 | } |
|
520 | 31 | ||
521 | /** |
||
522 | * {@inheritdoc} |
||
523 | */ |
||
524 | public function setValue($xpath, $value) |
||
525 | { |
||
526 | $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath)); |
||
527 | $jsNode = $this->getJsNode($xpath); |
||
528 | |||
529 | if ('input' === $element->getTagName() && \in_array($element->getAttribute('type'), ['date', 'time', 'color'])) { |
||
530 | $this->executeScript(\sprintf('%s.value = \'%s\'', $jsNode, $value)); |
||
531 | } else { |
||
532 | try { |
||
533 | $formField = $this->getFormField($xpath); |
||
534 | 31 | $formField->setValue($value); |
|
535 | 30 | } catch (DriverException $e) { |
|
536 | // e.g. element is on option |
||
537 | 31 | $element->sendKeys($value); |
|
538 | } |
||
539 | } |
||
540 | |||
541 | // Remove the focus from the element if the field still has focus in |
||
542 | 5 | // order to trigger the change event. By doing this instead of simply |
|
543 | // triggering the change event for the given xpath we ensure that the |
||
544 | 5 | // change event will not be triggered twice for the same element if it |
|
545 | 3 | // has lost focus in the meanwhile. If the element has lost focus |
|
546 | // already then there is nothing to do as this will already have caused |
||
547 | // the triggering of the change event for that element. |
||
548 | if ($this->evaluateScript(\sprintf('document.activeElement === %s', $jsNode))) { |
||
549 | $this->executeScript('document.activeElement.blur();'); |
||
550 | 5 | } |
|
551 | } |
||
552 | 5 | ||
553 | 3 | /** |
|
554 | * {@inheritdoc} |
||
555 | */ |
||
556 | public function check($xpath) |
||
557 | { |
||
558 | 11 | $this->getChoiceFormField($xpath)->tick(); |
|
559 | } |
||
560 | 11 | ||
561 | /** |
||
562 | 10 | * {@inheritdoc} |
|
563 | 1 | */ |
|
564 | 1 | public function uncheck($xpath) |
|
565 | 1 | { |
|
566 | 1 | $this->getChoiceFormField($xpath)->untick(); |
|
567 | } |
||
568 | |||
569 | /** |
||
570 | * {@inheritdoc} |
||
571 | 9 | */ |
|
572 | 9 | public function selectOption($xpath, $value, $multiple = false) |
|
573 | { |
||
574 | $field = $this->getFormField($xpath); |
||
575 | |||
576 | if (!$field instanceof ChoiceFormField) { |
||
577 | 33 | throw new DriverException( |
|
578 | sprintf( |
||
579 | 33 | 'Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', |
|
580 | 32 | $xpath |
|
581 | 32 | ) |
|
582 | ); |
||
583 | } |
||
584 | |||
585 | $field->select($value); |
||
586 | 3 | } |
|
587 | |||
588 | 3 | /** |
|
589 | 2 | * {@inheritdoc} |
|
590 | */ |
||
591 | public function click($xpath) |
||
592 | { |
||
593 | $this->getClient()->getMouse()->click($this->toCoordinates($xpath)); |
||
594 | 3 | } |
|
595 | |||
596 | 3 | /** |
|
597 | 2 | * {@inheritdoc} |
|
598 | */ |
||
599 | public function doubleClick($xpath) |
||
600 | { |
||
601 | $this->getClient()->getMouse()->doubleClick($this->toCoordinates($xpath)); |
||
602 | 4 | } |
|
603 | |||
604 | 4 | /** |
|
605 | * {@inheritdoc} |
||
606 | */ |
||
607 | public function rightClick($xpath) |
||
608 | { |
||
609 | $this->getClient()->getMouse()->contextClick($this->toCoordinates($xpath)); |
||
610 | 3 | } |
|
611 | |||
612 | 3 | /** |
|
613 | * {@inheritdoc} |
||
614 | 2 | */ |
|
615 | 1 | public function isChecked($xpath) |
|
616 | 1 | { |
|
617 | return $this->getCrawlerElement($this->getFilteredCrawler($xpath))->isSelected(); |
||
618 | } |
||
619 | |||
620 | 1 | /** |
|
621 | 1 | * {@inheritdoc} |
|
622 | */ |
||
623 | public function attachFile($xpath, $path) |
||
624 | { |
||
625 | $field = $this->getFormField($xpath); |
||
626 | 1 | ||
627 | if (!$field instanceof FileFormField) { |
||
628 | 1 | throw new DriverException( |
|
629 | 1 | sprintf('Impossible to attach a file on the element with XPath "%s" as it is not a file input', $xpath) |
|
630 | 1 | ); |
|
631 | 1 | } |
|
632 | 1 | ||
633 | $field->upload($path); |
||
634 | } |
||
635 | |||
636 | /** |
||
637 | 151 | * {@inheritdoc} |
|
638 | */ |
||
639 | 151 | public function dragTo($sourceXpath, $destinationXpath) |
|
640 | 2 | { |
|
641 | 2 | $webDriverActions = $this->getWebDriverActions(); |
|
642 | $source = $this->getCrawlerElement($this->getFilteredCrawler($sourceXpath)); |
||
643 | $target = $this->getCrawlerElement($this->getFilteredCrawler($destinationXpath)); |
||
644 | 151 | $webDriverActions->dragAndDrop($source, $target)->perform(); |
|
645 | } |
||
646 | |||
647 | /** |
||
648 | * {@inheritdoc} |
||
649 | */ |
||
650 | 53 | public function executeScript($script) |
|
651 | { |
||
652 | 53 | if (\preg_match('/^function[\s\(]/', $script)) { |
|
653 | 39 | $script = \preg_replace('/;$/', '', $script); |
|
654 | $script = '('.$script.')'; |
||
655 | } |
||
656 | 53 | ||
657 | return $this->getClient()->executeScript($script); |
||
658 | } |
||
659 | |||
660 | /** |
||
661 | * {@inheritdoc} |
||
662 | 22 | */ |
|
663 | public function evaluateScript($script) |
||
664 | 22 | { |
|
665 | 22 | if (0 !== \strpos(\trim($script), 'return ')) { |
|
666 | 22 | $script = 'return '.$script; |
|
667 | } |
||
668 | |||
669 | 22 | return $this->getClient()->executeScript($script); |
|
670 | 22 | } |
|
671 | 22 | ||
672 | /** |
||
673 | 22 | * {@inheritdoc} |
|
674 | */ |
||
675 | public function wait($timeout, $condition) |
||
676 | { |
||
677 | $script = "return $condition;"; |
||
678 | $start = microtime(true); |
||
679 | 2 | $end = $start + $timeout / 1000.0; |
|
680 | |||
681 | 2 | do { |
|
682 | 2 | $result = $this->evaluateScript($script); |
|
683 | 2 | \usleep(100000); |
|
684 | } while (\microtime(true) < $end && !$result); |
||
685 | |||
686 | return (bool) $result; |
||
687 | } |
||
688 | 1 | ||
689 | /** |
||
690 | 1 | * {@inheritdoc} |
|
691 | 1 | */ |
|
692 | 1 | public function resizeWindow($width, $height, $name = null) |
|
693 | 1 | { |
|
694 | $size = new WebDriverDimension($width, $height); |
||
695 | $this->getClient()->getWebDriver()->manage()->window()->setSize($size); |
||
696 | } |
||
697 | |||
698 | 4 | /** |
|
699 | * {@inheritdoc} |
||
700 | 4 | */ |
|
701 | public function maximizeWindow($name = null) |
||
702 | 3 | { |
|
703 | 2 | $width = $this->evaluateScript('screen.width'); |
|
704 | 2 | $height = $this->evaluateScript('screen.height'); |
|
705 | $this->resizeWindow($width, $height, $name); |
||
706 | } |
||
707 | |||
708 | /** |
||
709 | * {@inheritdoc} |
||
710 | */ |
||
711 | public function submitForm($xpath) |
||
712 | { |
||
713 | $crawler = $this->getFilteredCrawler($xpath); |
||
714 | |||
715 | $this->getClient()->submit($crawler->form()); |
||
716 | } |
||
717 | |||
718 | /** |
||
719 | * @return Response |
||
720 | * |
||
721 | * @throws DriverException If there is not response yet |
||
722 | */ |
||
723 | protected function getResponse() |
||
724 | { |
||
725 | $response = $this->getClient()->getInternalResponse(); |
||
726 | |||
727 | if (null === $response) { |
||
728 | throw new DriverException('Unable to access the response before visiting a page'); |
||
729 | } |
||
730 | 141 | ||
731 | return $response; |
||
732 | 141 | } |
|
733 | |||
734 | 141 | /** |
|
735 | * Prepares URL for visiting. |
||
736 | * Removes "*.php/" from urls and then passes it to BrowserKitDriver::visit(). |
||
737 | * |
||
738 | * @param string $url |
||
739 | * |
||
740 | * @return string |
||
741 | */ |
||
742 | 2 | protected function prepareUrl($url) |
|
743 | { |
||
744 | 2 | $replacement = ($this->removeHostFromUrl ? '' : '$1').($this->removeScriptFromUrl ? '' : '$2'); |
|
745 | 2 | ||
746 | return preg_replace('#(https?\://[^/]+)(/[^/\.]+\.php)?#', $replacement, $url); |
||
747 | } |
||
748 | 2 | ||
749 | 2 | /** |
|
750 | * Deletes a cookie by name. |
||
751 | * |
||
752 | 2 | * @param string $name Cookie name. |
|
753 | 2 | */ |
|
754 | 2 | private function deleteCookie($name) |
|
755 | { |
||
756 | $path = $this->getCookiePath(); |
||
757 | $jar = $this->getClient()->getCookieJar(); |
||
758 | |||
759 | do { |
||
760 | if (null !== $jar->get($name, $path)) { |
||
761 | 2 | $jar->expire($name, $path); |
|
762 | } |
||
763 | 2 | ||
764 | $path = preg_replace('/.$/', '', $path); |
||
765 | 2 | } while ($path); |
|
766 | } |
||
767 | |||
768 | /** |
||
769 | 2 | * Returns current cookie path. |
|
770 | * |
||
771 | * @return string |
||
772 | */ |
||
773 | private function getCookiePath() |
||
774 | { |
||
775 | $path = dirname(parse_url($this->getCurrentUrl(), PHP_URL_PATH)); |
||
776 | |||
777 | if ('\\' === DIRECTORY_SEPARATOR) { |
||
778 | $path = str_replace('\\', '/', $path); |
||
779 | } |
||
780 | if (0 !== \substr_compare($path, '/', -1)) { |
||
781 | 50 | $path .= '/'; |
|
782 | } |
||
783 | |||
784 | 50 | return $path; |
|
785 | 38 | } |
|
786 | 38 | ||
787 | /** |
||
788 | 50 | * Returns form field from XPath query. |
|
789 | * |
||
790 | 38 | * @param string $xpath |
|
791 | 11 | * |
|
792 | 11 | * @return FormField |
|
793 | * |
||
794 | * @throws DriverException |
||
795 | 50 | */ |
|
796 | private function getFormField($xpath) |
||
797 | 11 | { |
|
798 | 10 | try { |
|
799 | 10 | $formField = $this->getChoiceFormField($xpath); |
|
800 | } catch (DriverException $e) { |
||
801 | $formField = null; |
||
802 | 50 | } |
|
803 | 10 | if (!$formField) { |
|
804 | try { |
||
805 | $formField = $this->getInputFormField($xpath); |
||
806 | 47 | } catch (DriverException $e) { |
|
807 | $formField = null; |
||
808 | } |
||
809 | } |
||
810 | if (!$formField) { |
||
811 | try { |
||
812 | $formField = $this->getFileFormField($xpath); |
||
813 | } catch (DriverException $e) { |
||
814 | $formField = null; |
||
815 | } |
||
816 | } |
||
817 | if (!$formField) { |
||
818 | 57 | $formField = $this->getTextareaFormField($xpath); |
|
819 | } |
||
820 | 57 | ||
821 | return $formField; |
||
822 | 51 | } |
|
823 | 37 | ||
824 | 37 | /** |
|
825 | 37 | * Returns the checkbox field from xpath query, ensuring it is valid. |
|
826 | 37 | * |
|
827 | 37 | * @param string $xpath |
|
828 | 37 | * |
|
829 | * @return ChoiceFormField |
||
830 | * |
||
831 | * @throws DriverException when the field is not a checkbox |
||
832 | */ |
||
833 | 20 | private function getChoiceFormField($xpath) |
|
834 | { |
||
835 | $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath)); |
||
836 | try { |
||
837 | $choiceFormField = new ChoiceFormField($element); |
||
838 | } catch (\LogicException $e) { |
||
839 | throw new DriverException( |
||
840 | sprintf( |
||
841 | 'Impossible to get the element with XPath "%s" as it is not a choice form field. %s', |
||
842 | $xpath, |
||
843 | $e->getMessage() |
||
844 | ) |
||
845 | 38 | ); |
|
846 | } |
||
847 | 38 | ||
848 | return $choiceFormField; |
||
849 | 35 | } |
|
850 | 8 | ||
851 | 8 | /** |
|
852 | 8 | * Returns the input field from xpath query, ensuring it is valid. |
|
853 | * |
||
854 | * @param string $xpath |
||
855 | * |
||
856 | 28 | * @return InputFormField |
|
857 | * |
||
858 | * @throws DriverException when the field is not a checkbox |
||
859 | */ |
||
860 | private function getInputFormField($xpath) |
||
861 | { |
||
862 | $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath)); |
||
863 | try { |
||
864 | $inputFormField = new InputFormField($element); |
||
865 | } catch (\LogicException $e) { |
||
866 | throw new DriverException( |
||
867 | sprintf('Impossible to check the element with XPath "%s" as it is not an input form field.', $xpath) |
||
868 | 11 | ); |
|
869 | } |
||
870 | 11 | ||
871 | return $inputFormField; |
||
872 | 8 | } |
|
873 | 7 | ||
874 | 7 | /** |
|
875 | 7 | * Returns the input field from xpath query, ensuring it is valid. |
|
876 | * |
||
877 | * @param string $xpath |
||
878 | * |
||
879 | 2 | * @return FileFormField |
|
880 | * |
||
881 | * @throws DriverException when the field is not a checkbox |
||
882 | */ |
||
883 | private function getFileFormField($xpath) |
||
884 | { |
||
885 | $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath)); |
||
886 | try { |
||
887 | $fileFormField = new FileFormField($element); |
||
888 | } catch (\LogicException $e) { |
||
889 | throw new DriverException( |
||
890 | sprintf('Impossible to check the element with XPath "%s" as it is not a file form field.', $xpath) |
||
891 | 10 | ); |
|
892 | } |
||
893 | 10 | ||
894 | return $fileFormField; |
||
895 | 7 | } |
|
896 | 6 | ||
897 | 6 | /** |
|
898 | 6 | * Returns the textarea field from xpath query, ensuring it is valid. |
|
899 | * |
||
900 | * @param string $xpath |
||
901 | * |
||
902 | 2 | * @return TextareaFormField |
|
903 | * |
||
904 | * @throws DriverException when the field is not a checkbox |
||
905 | */ |
||
906 | private function getTextareaFormField($xpath) |
||
907 | { |
||
908 | $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath)); |
||
909 | try { |
||
910 | $textareaFormField = new TextareaFormField($element); |
||
911 | } catch (\LogicException $e) { |
||
912 | throw new DriverException( |
||
913 | sprintf('Impossible to check the element with XPath "%s" as it is not a textarea.', $xpath) |
||
914 | 83 | ); |
|
915 | } |
||
916 | 83 | ||
917 | return $textareaFormField; |
||
918 | 83 | } |
|
919 | 83 | ||
920 | /** |
||
921 | * Returns WebDriverElement from crawler instance. |
||
922 | * |
||
923 | * @param Crawler $crawler |
||
924 | * |
||
925 | * @return WebDriverElement |
||
926 | * |
||
927 | * @throws DriverException when the node does not exist |
||
928 | */ |
||
929 | private function getCrawlerElement(Crawler $crawler): WebDriverElement |
||
930 | { |
||
931 | $node = $crawler->getElement(0); |
||
932 | |||
933 | if (null !== $node) { |
||
934 | 121 | return $node; |
|
935 | } |
||
936 | 121 | ||
937 | 22 | throw new DriverException('The element does not exist'); |
|
938 | } |
||
939 | |||
940 | 99 | /** |
|
941 | * Returns a crawler filtered for the given XPath, requiring at least 1 result. |
||
942 | * |
||
943 | * @param string $xpath |
||
944 | * |
||
945 | * @return Crawler |
||
946 | * |
||
947 | * @throws DriverException when no matching elements are found |
||
948 | */ |
||
949 | private function getFilteredCrawler($xpath): Crawler |
||
950 | 123 | { |
|
951 | if (!count($crawler = $this->getCrawler()->filterXPath($xpath))) { |
||
952 | 123 | throw new DriverException(sprintf('There is no element matching XPath "%s"', $xpath)); |
|
953 | } |
||
954 | 123 | ||
955 | return $crawler; |
||
956 | } |
||
957 | |||
958 | 123 | /** |
|
959 | * Returns crawler instance (got from client). |
||
960 | * |
||
961 | 35 | * @return Crawler |
|
962 | * |
||
963 | 35 | * @throws DriverException |
|
964 | 35 | */ |
|
965 | 35 | private function getCrawler(): Crawler |
|
966 | { |
||
967 | $crawler = $this->getClient()->getCrawler(); |
||
968 | |||
969 | 42 | if (null === $crawler) { |
|
970 | throw new DriverException('Unable to access the response content before visiting a page'); |
||
971 | 42 | } |
|
972 | |||
973 | 38 | return $crawler; |
|
974 | } |
||
975 | |||
976 | private function getJsNode(string $xpath): string |
||
977 | { |
||
978 | return sprintf( |
||
979 | 38 | 'document.evaluate(`%s`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue', |
|
980 | $xpath |
||
981 | ); |
||
982 | 9 | } |
|
983 | |||
984 | 9 | private function toCoordinates(string $xpath): WebDriverCoordinates |
|
985 | 9 | { |
|
986 | $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath)); |
||
987 | |||
988 | 9 | if (!$element instanceof WebDriverLocatable) { |
|
989 | throw new \RuntimeException( |
||
990 | 9 | sprintf('The element of "%s" xpath selector does not implement "%s".', $xpath, WebDriverLocatable::class) |
|
991 | ); |
||
992 | } |
||
993 | 5 | ||
994 | return $element->getCoordinates(); |
||
995 | 5 | } |
|
996 | 5 | ||
997 | private function getWebDriverActions(): WebDriverActions |
||
998 | { |
||
999 | 5 | $webDriver = $this->getClient()->getWebDriver(); |
|
1000 | if (!$webDriver instanceof WebDriverHasInputDevices) { |
||
1001 | throw new UnsupportedDriverActionException('Mouse manipulations are not supported by %s', $this); |
||
1002 | 5 | } |
|
1003 | $webDriverActions = new WebDriverActions($webDriver); |
||
1004 | |||
1005 | 5 | return $webDriverActions; |
|
1006 | 1 | } |
|
1007 | 1 | ||
1008 | 4 | private function geWebDriverKeyValue($char) |
|
1009 | 1 | { |
|
1010 | 1 | if (\is_int($char)) { |
|
1011 | 3 | $char = \strtolower(\chr($char)); |
|
1012 | 1 | } |
|
1013 | 1 | ||
1014 | 2 | return $char; |
|
1015 | 1 | } |
|
1016 | 1 | ||
1017 | 1 | private function getWebdriverModifierKeyValue(string $modifier = null): ?string |
|
1018 | 1 | { |
|
1019 | switch ($modifier) { |
||
1020 | case 'alt': |
||
1021 | $modifier = WebDriverKeys::ALT; |
||
1022 | break; |
||
1023 | 5 | case 'ctrl': |
|
1024 | $modifier = WebDriverKeys::CONTROL; |
||
1025 | break; |
||
1026 | case 'shift': |
||
1027 | $modifier = WebDriverKeys::SHIFT; |
||
1028 | break; |
||
1029 | case 'meta': |
||
1030 | $modifier = WebDriverKeys::META; |
||
1031 | break; |
||
1032 | case null: |
||
0 ignored issues
–
show
|
|||
1033 | break; |
||
1034 | default: |
||
1035 | throw new DriverException(\sprintf('Unsupported modifier "%s" given.', $modifier)); |
||
1036 | } |
||
1037 | |||
1038 | return $modifier; |
||
1039 | } |
||
1040 | } |
||
1041 |