Passed
Push — master ( 698224...644d9a )
by Robert
03:24
created

PantherDriver::getClient()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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