Passed
Push — master ( 71e607...cca9cf )
by Robert
02:42
created

PantherDriver::getJsNode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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