Passed
Push — master ( bbb0b7...2843e3 )
by Robert
03:41
created

PantherDriver::executeScript()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
ccs 3
cts 5
cp 0.6
crap 2.2559
rs 10
c 0
b 0
f 0
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\WebDriverDimension;
20
use Facebook\WebDriver\WebDriverElement;
21
use Facebook\WebDriver\WebDriverHasInputDevices;
22
use Facebook\WebDriver\WebDriverKeys;
23
use Symfony\Component\BrowserKit\Cookie;
24
use Symfony\Component\BrowserKit\Response;
25
use Symfony\Component\DomCrawler\Field\FormField;
26
use Symfony\Component\Panther\Client;
27
use Symfony\Component\Panther\DomCrawler\Crawler;
28
use Symfony\Component\Panther\DomCrawler\Field\ChoiceFormField;
29
use Symfony\Component\Panther\DomCrawler\Field\FileFormField;
30
use Symfony\Component\Panther\DomCrawler\Field\InputFormField;
31
use Symfony\Component\Panther\DomCrawler\Field\TextareaFormField;
32
use Symfony\Component\Panther\PantherTestCaseTrait;
33
34
/**
35
 * Symfony2 Panther driver.
36
 *
37
 * @author Robert Freigang <[email protected]>
38
 */
39
class PantherDriver extends CoreDriver
40
{
41
    use PantherTestCaseTrait;
42
43
    /** @var Client */
44
    private $client;
45
    private $started = false;
46
    private $removeScriptFromUrl = false;
47
    private $removeHostFromUrl = false;
48
    /** @var string */
49
    private $clientType;
50
    /** @var array */
51
    private $clientOptions;
52
    /** @var array */
53
    private $clientKernelOptions;
54
55
    /**
56
     * Initializes Panther driver.
57
     * external_base_uri
58
     * webServerDir PANTHER_WEB_SERVER_DIR
59
     * port PANTHER_WEB_SERVER_PORT
60
     * router PANTHER_WEB_SERVER_ROUTER
61
     *    protected static $defaultOptions = [
62
     *        'webServerDir' => __DIR__.'/../../../../public', // the Flex directory structure
63
     *        'hostname' => '127.0.0.1',
64
     *        'port' => 9080,
65
     *        'router' => '',
66
     *        'external_base_uri' => null,
67
     *    ];
68
     *
69
     * @param string   $clientType BrowserKit client instance
70
     * @param array    $options
71
     * @param array    $kernelOptions
72
     */
73 10
    public function __construct(
74
        string $clientType = 'panther',
75
        array $options = [],
76
        array $kernelOptions = []
77
    ) {
78 10
        $this->clientType = $clientType;
79 10
        $this->clientOptions = $options;
80 10
        $this->clientKernelOptions = $kernelOptions;
81 10
    }
82
83
    /**
84
     * Returns BrowserKit HTTP client instance.
85
     *
86
     * @return Client
87
     */
88
    public function getClient()
89
    {
90
        return $this->client;
91
    }
92
93
    /**
94
     * Tells driver to remove hostname from URL.
95
     *
96
     * @param Boolean $remove
97
     *
98
     * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead.
99
     */
100
    public function setRemoveHostFromUrl($remove = true)
101
    {
102
        @trigger_error(
103
            'setRemoveHostFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.',
104
            E_USER_DEPRECATED
105
        );
106
        $this->removeHostFromUrl = (bool)$remove;
107
    }
108
109
    /**
110
     * Tells driver to remove script name from URL.
111
     *
112
     * @param Boolean $remove
113
     *
114
     * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead.
115
     */
116
    public function setRemoveScriptFromUrl($remove = true)
117
    {
118
        @trigger_error(
119
            'setRemoveScriptFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.',
120
            E_USER_DEPRECATED
121
        );
122
        $this->removeScriptFromUrl = (bool)$remove;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 2
    public function start()
129
    {
130 2
        if ($this->clientType === 'panther') {
131 2
            $this->client = self::createPantherClient($this->clientOptions, $this->clientKernelOptions);
132
        } elseif ($this->clientType === 'goutte') {
133
            $this->client = self::createGoutteClient($this->clientOptions, $this->clientKernelOptions);
134
        } else {
135
            throw new \InvalidArgumentException('$clientType has to be "panther" or "goutte".');
136
        }
137
138 2
        $this->started = true;
139 2
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144 117
    public function isStarted()
145
    {
146 117
        return $this->started;
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152 1
    public function stop()
153
    {
154 1
        $this->client->quit();
155 1
        self::stopWebServer();
156 1
        $this->started = false;
157 1
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 117
    public function reset()
163
    {
164
        // experimental
165
        // $useSpeedUp = false;
166 117
        $useSpeedUp = true;
167 117
        if ($useSpeedUp) {
0 ignored issues
show
introduced by
The condition $useSpeedUp is always true.
Loading history...
168 117
            $this->client->getWebDriver()->manage()->deleteAllCookies();
169 117
            $history = $this->client->getHistory();
170 117
            if ($history) {
0 ignored issues
show
introduced by
$history is of type Symfony\Component\BrowserKit\History, thus it always evaluated to true.
Loading history...
171 117
                $history->clear();
172
            }
173
            // not sure if we should also close all windows
174
            // $lastWindowHandle = \end($this->client->getWindowHandles());
175
            // if ($lastWindowHandle) {
176
            //     $this->client->switchTo()->window($lastWindowHandle);
177
            // }
178
            // $this->client->getWebDriver()->navigate()->refresh();
179
            // $this->client->refreshCrawler();
180
            // if (\count($this->client->getWindowHandles()) > 1) {
181
            //     $this->client->getWebDriver()->close();
182
            // }
183
        } else {
184
            // Restarting the client resets the cookies and the history
185
            $this->client->restart();
186
        }
187
188 117
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193 107
    public function visit($url)
194
    {
195 107
        $this->client->get($this->prepareUrl($url));
196 107
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 4
    public function getCurrentUrl()
202
    {
203 4
        return $this->client->getCurrentURL();
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209 2
    public function reload()
210
    {
211 2
        $this->client->reload();
212 2
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217
    public function forward()
218
    {
219
        $this->client->forward();
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225
    public function back()
226
    {
227
        $this->client->back();
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233
    public function switchToWindow($name = null)
234
    {
235
        $this->client->switchTo()->window($name);
236
        $this->client->refreshCrawler();
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242 1
    public function switchToIFrame($name = null)
243
    {
244 1
        if (null === $name) {
245 1
            $this->client->switchTo()->defaultContent();
246
        } else {
247 1
            $this->client->switchTo()->frame($name);
248
        }
249 1
        $this->client->refreshCrawler();
250 1
    }
251
252
    /**
253
     * {@inheritdoc}
254
     */
255 4
    public function setCookie($name, $value = null)
256
    {
257 4
        if (null === $value) {
258 2
            $this->deleteCookie($name);
259
260 2
            return;
261
        }
262
263 3
        $jar = $this->client->getCookieJar();
264
        // @see: https://github.com/w3c/webdriver/issues/1238
265 3
        $jar->set(new Cookie($name, \rawurlencode((string)$value)));
266 3
    }
267
268
    /**
269
     * Deletes a cookie by name.
270
     *
271
     * @param string $name Cookie name.
272
     */
273 2
    private function deleteCookie($name)
274
    {
275 2
        $path = $this->getCookiePath();
276 2
        $jar = $this->client->getCookieJar();
277
278
        do {
279 2
            if (null !== $jar->get($name, $path)) {
280 2
                $jar->expire($name, $path);
281
            }
282
283 2
            $path = preg_replace('/.$/', '', $path);
284 2
        } while ($path);
285 2
    }
286
287
    /**
288
     * Returns current cookie path.
289
     *
290
     * @return string
291
     */
292 2
    private function getCookiePath()
293
    {
294 2
        $path = dirname(parse_url($this->getCurrentUrl(), PHP_URL_PATH));
295
296 2
        if ('\\' === DIRECTORY_SEPARATOR) {
297
            $path = str_replace('\\', '/', $path);
298
        }
299
300 2
        return $path;
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306 3
    public function getCookie($name)
307
    {
308 3
        $cookies = $this->client->getCookieJar()->all();
309
310 3
        foreach ($cookies as $cookie) {
311 3
            if ($cookie->getName() === $name) {
312 3
                return \urldecode($cookie->getValue());
313
            }
314
        }
315
316 1
        return null;
317
    }
318
319
    /**
320
     * {@inheritdoc}
321
     */
322 13
    public function getContent()
323
    {
324 13
        return $this->client->getWebDriver()->getPageSource();
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330 1
    public function getScreenshot($saveAs = null): string
331
    {
332 1
        return $this->client->takeScreenshot($saveAs);
333
    }
334
335
    /**
336
     * {@inheritdoc}
337
     */
338
    public function getWindowNames()
339
    {
340
        return $this->client->getWindowHandles();
341
    }
342
343
    /**
344
     * {@inheritdoc}
345
     */
346 1
    public function getWindowName()
347
    {
348 1
        return $this->client->getWindowHandle();
349
    }
350
351
    /**
352
     * {@inheritdoc}
353
     */
354 1
    public function isVisible($xpath)
355
    {
356 1
        return $this->getCrawlerElement($this->getFilteredCrawler($xpath))->isDisplayed();
357
    }
358
359
    /**
360
     * {@inheritdoc}.
361
     */
362 6
    public function mouseOver($xpath)
363
    {
364 6
        $this->client->getMouse()->mouseMove($this->toCoordinates($xpath));
365 5
    }
366
367
    /**
368
     * {@inheritdoc}
369
     */
370 2
    public function focus($xpath)
371
    {
372 2
        $jsNode = $this->getJsNode($xpath);
373 2
        $script = \sprintf('%s.focus()', $jsNode);
374 2
        $this->executeScript($script);
375 1
    }
376
377
    /**
378
     * {@inheritdoc}
379
     */
380 2
    public function blur($xpath)
381
    {
382 2
        $jsNode = $this->getJsNode($xpath);
383 2
        $script = \sprintf('%s.blur();', $jsNode);
384
        // ensure element had active state; just for passing EventsTest::testBlur
385 2
        if ($this->evaluateScript(\sprintf('document.activeElement !== %s', $jsNode))) {
386 2
            $script = \sprintf('%s.focus();%s', $jsNode, $script);
387
        }
388 2
        $this->executeScript($script);
389 1
    }
390
391
    /**
392
     * {@inheritdoc}
393
     */
394 3
    public function keyPress($xpath, $char, $modifier = null)
395
    {
396 3
        $webDriverActions = $this->getWebDriverActions();
397 3
        $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath));
398 2
        $key = $this->geWebDriverKeyValue($char, $modifier);
399 2
        $webDriverActions->sendKeys($element, $key.WebDriverKeys::NULL)->perform();
400 2
    }
401
402
    /**
403
     * {@inheritdoc}
404
     */
405 3
    public function keyDown($xpath, $char, $modifier = null)
406
    {
407 3
        $webDriverActions = $this->getWebDriverActions();
408 3
        $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath));
409 2
        $key = $this->geWebDriverKeyValue($char, $modifier);
410 2
        $webDriverActions->keyDown($element, $key.WebDriverKeys::NULL)->perform();
411 2
    }
412
413
    /**
414
     * {@inheritdoc}
415
     */
416 3
    public function keyUp($xpath, $char, $modifier = null)
417
    {
418 3
        $webDriverActions = $this->getWebDriverActions();
419 3
        $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath));
420 2
        $key = $this->geWebDriverKeyValue($char, $modifier);
421 2
        $webDriverActions->keyUp($element, $key)->perform();
422 2
    }
423
424
    /**
425
     * {@inheritdoc}
426
     */
427 3
    public function isSelected($xpath)
428
    {
429 3
        return $this->getCrawlerElement($this->getFilteredCrawler($xpath))->isSelected();
430
    }
431
432
    /**
433
     * {@inheritdoc}
434
     */
435 64
    public function findElementXpaths($xpath)
436
    {
437 64
        $nodes = $this->getCrawler()->filterXPath($xpath);
438
439 64
        $elements = array();
440 64
        foreach ($nodes as $i => $node) {
441 57
            $elements[] = sprintf('(%s)[%d]', $xpath, $i + 1);
442
        }
443
444 64
        return $elements;
445
    }
446
447
    /**
448
     * {@inheritdoc}
449
     */
450 7
    public function getTagName($xpath)
451
    {
452 7
        return $this->getCrawlerElement($this->getFilteredCrawler($xpath))->getTagName();
453
    }
454
455
    /**
456
     * {@inheritdoc}
457
     */
458 27
    public function getText($xpath)
459
    {
460 27
        $text = $this->getFilteredCrawler($xpath)->text();
461 26
        $text = str_replace("\n", ' ', $text);
462 26
        $text = preg_replace('/ {2,}/', ' ', $text);
463
464 26
        return trim($text);
465
    }
466
467
    /**
468
     * {@inheritdoc}
469
     */
470 3
    public function getHtml($xpath)
471
    {
472
        // cut the tag itself (making innerHTML out of outerHTML)
473 3
        return preg_replace('/^\<[^\>]+\>|\<[^\>]+\>$/', '', $this->getOuterHtml($xpath));
474
    }
475
476
    /**
477
     * {@inheritdoc}
478
     */
479 5
    public function getOuterHtml($xpath)
480
    {
481 5
        $crawler = $this->getFilteredCrawler($xpath);
482
483 3
        return $crawler->html();
484
    }
485
486
    /**
487
     * {@inheritdoc}
488
     */
489 6
    public function getAttribute($xpath, $name)
490
    {
491 6
        $crawler = $this->getFilteredCrawler($xpath);
492
493 5
        $attribute = $this->getCrawlerElement($crawler)->getAttribute($name);
494
495
        // let's get hacky
496 5
        if ('' === $attribute) {
497 2
            $html = \strtolower($crawler->html());
498 2
            $name = \strtolower($name).'=';
499 2
            if (0 === \substr_count($html, $name)) {
500
                $attribute = null;
501
            }
502
        }
503
504 5
        return $attribute;
505
    }
506
507
    /**
508
     * {@inheritdoc}
509
     */
510 19
    public function getValue($xpath)
511
    {
512
        try {
513 19
            $formField = $this->getFormField($xpath);
514 16
            $value = $formField->getValue();
515 16
            if ('' === $value && $formField instanceof ChoiceFormField) {
516 16
                $value = null;
517
            }
518 5
        } catch (DriverException $e) {
519
            // e.g. element is an option
520 5
            $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath));
521 4
            $value = $element->getAttribute('value');
522
        }
523
524 18
         return $value;
525
    }
526
527
    /**
528
     * {@inheritdoc}
529
     */
530 22
    public function setValue($xpath, $value)
531
    {
532 22
        $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath));
533 21
        $jsNode = $this->getJsNode($xpath);
534
535
        // add workaround for now in case value is not a canonical path
536
        // also see: https://github.com/minkphp/driver-testsuite/pull/32
537 21
        if ('input' === $element->getTagName() && 'file' === $element->getAttribute('type')) {
538
            $realpathValue = \realpath($value);
539
            $value = \is_string($realpathValue) ? $realpathValue : $value;
0 ignored issues
show
introduced by
The condition is_string($realpathValue) is always true.
Loading history...
540
        }
541
542 21
        if ('input' === $element->getTagName() && \in_array($element->getAttribute('type'), ['date', 'color'])) {
543 1
            $this->executeScript(\sprintf('%s.value = \'%s\'', $jsNode, $value));
544
        } else {
545
            try {
546 21
                $formField = $this->getFormField($xpath);
547 21
                $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 21
        if ($this->evaluateScript(\sprintf('document.activeElement === %s', $jsNode))) {
562 21
            $this->executeScript('document.activeElement.blur();');
563
        }
564 21
    }
565
566
    /**
567
     * {@inheritdoc}
568
     */
569 4
    public function check($xpath)
570
    {
571 4
        $this->getChoiceFormField($xpath)->tick();
572 2
    }
573
574
    /**
575
     * {@inheritdoc}
576
     */
577 4
    public function uncheck($xpath)
578
    {
579 4
        $this->getChoiceFormField($xpath)->untick();
580 2
    }
581
582
    /**
583
     * {@inheritdoc}
584
     */
585 8
    public function selectOption($xpath, $value, $multiple = false)
586
    {
587 8
        $field = $this->getFormField($xpath);
588
589 7
        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 6
        $field->select($value);
596 6
    }
597
598
    /**
599
     * {@inheritdoc}
600
     */
601 21
    public function click($xpath)
602
    {
603 21
        $this->client->getMouse()->click($this->toCoordinates($xpath));
604 20
        $this->client->refreshCrawler();
605 20
    }
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
    public function dragTo($sourceXpath, $destinationXpath)
651
    {
652
        $webDriverActions = $this->getWebDriverActions();
653
        $source = $this->getCrawlerElement($this->getFilteredCrawler($sourceXpath));
654
        $target = $this->getCrawlerElement($this->getFilteredCrawler($destinationXpath));
655
        $webDriverActions->dragAndDrop($source, $target)->perform();
656
    }
657
658
    /**
659
     * {@inheritdoc}
660
     */
661 25
    public function executeScript($script)
662
    {
663 25
        if (\preg_match('/^function[\s\(]/', $script)) {
664
            $script = \preg_replace('/;$/', '', $script);
665
            $script = '(' . $script . ')';
666
        }
667
668 25
        return $this->client->executeScript($script);
669
    }
670
671
    /**
672
     * {@inheritdoc}
673
     */
674 37
    public function evaluateScript($script)
675
    {
676 37
        if (0 !== \strpos(\trim($script), 'return ')) {
677 29
            $script = 'return ' . $script;
678
        }
679
680 37
        return $this->client->executeScript($script);
681
    }
682
683
    /**
684
     * {@inheritdoc}
685
     */
686 16
    public function wait($timeout, $condition)
687
    {
688 16
        $script = "return $condition;";
689 16
        $start = microtime(true);
690 16
        $end = $start + $timeout / 1000.0;
691
692
        do {
693 16
            $result = $this->evaluateScript($script);
694 16
            \usleep(100000);
695 16
        } while (\microtime(true) < $end && !$result);
696
697 16
        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 107
    protected function prepareUrl($url)
755
    {
756 107
        $replacement = ($this->removeHostFromUrl ? '' : '$1').($this->removeScriptFromUrl ? '' : '$2');
757
758 107
        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 36
    private function getFormField($xpath)
771
    {
772
        try {
773 36
            $formField = $this->getChoiceFormField($xpath);
774 30
        } catch (DriverException $e) {
775 30
            $formField = null;
776
        }
777 36
        if (!$formField) {
778
            try {
779 30
                $formField = $this->getInputFormField($xpath);
780 7
            } catch (DriverException $e) {
781 7
                $formField = null;
782
            }
783
        }
784 36
        if (!$formField) {
785
            try {
786 7
                $formField = $this->getFileFormField($xpath);
787 7
            } catch (DriverException $e) {
788 7
                $formField = null;
789
            }
790
        }
791 36
        if (!$formField) {
792 7
            $formField = $this->getTextareaFormField($xpath);
793
        }
794
795 33
        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 41
    private function getChoiceFormField($xpath)
808
    {
809 41
        $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath));
810
        try {
811 35
            $choiceFormField = new ChoiceFormField($element);
812 29
        } catch (\LogicException $e) {
813 29
            throw new DriverException(
814 29
                sprintf(
815 29
                    'Impossible to get the element with XPath "%s" as it is not a choice form field. %s',
816
                    $xpath,
817 29
                    $e->getMessage()
818
                )
819
            );
820
        }
821
822 10
        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 30
    private function getInputFormField($xpath)
835
    {
836 30
        $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath));
837
        try {
838 27
            $inputFormField = new InputFormField($element);
839 4
        } catch (\LogicException $e) {
840 4
            throw new DriverException(sprintf('Impossible to check the element with XPath "%s" as it is not an input form field.', $xpath));
841
        }
842
843 24
        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 7
    private function getFileFormField($xpath)
856
    {
857 7
        $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath));
858
        try {
859 4
            $fileFormField = new FileFormField($element);
860 4
        } catch (\LogicException $e) {
861 4
            throw new DriverException(sprintf('Impossible to check the element with XPath "%s" as it is not a file form field.', $xpath));
862
        }
863
864 1
        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 7
    private function getTextareaFormField($xpath)
877
    {
878 7
        $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath));
879
        try {
880 4
            $textareaFormField = new TextareaFormField($element);
881 4
        } catch (\LogicException $e) {
882 4
            throw new DriverException(sprintf('Impossible to check the element with XPath "%s" as it is not a textarea.', $xpath));
883
        }
884
885 1
        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 51
    private function getCrawlerElement(Crawler $crawler): WebDriverElement
898
    {
899 51
        $node = $crawler->getElement(0);
900
901 51
        if (null !== $node) {
902 51
            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 84
    private function getFilteredCrawler($xpath): Crawler
918
    {
919 84
        if (!count($crawler = $this->getCrawler()->filterXPath($xpath))) {
920 22
            throw new DriverException(sprintf('There is no element matching XPath "%s"', $xpath));
921
        }
922
923 62
        return $crawler;
924
    }
925
926
    /**
927
     * Returns crawler instance (got from client).
928
     *
929
     * @return Crawler
930
     *
931
     * @throws DriverException
932
     */
933 91
    private function getCrawler(): Crawler
934
    {
935 91
        $crawler = $this->client->getCrawler();
936
937 91
        if (null === $crawler) {
938
            throw new DriverException('Unable to access the response content before visiting a page');
939
        }
940
941 91
        return $crawler;
942
    }
943
944 25
    private function getJsNode(string $xpath): string
945
    {
946 25
        return sprintf('document.evaluate(`%s`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue', $xpath);
947
    }
948
949 30
    private function toCoordinates(string $xpath): WebDriverCoordinates
950
    {
951 30
        $element = $this->getCrawlerElement($this->getFilteredCrawler($xpath));
952
953 26
        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 26
        return $element->getCoordinates();
960
    }
961
962 5
    private function getWebDriverActions(): WebDriverActions
963
    {
964 5
        $webDriver = $this->client->getWebDriver();
965 5
        if (!$webDriver instanceof WebDriverHasInputDevices) {
966
            throw new UnsupportedDriverActionException('Mouse manipulations are not supported by %s', $this);
967
        }
968 5
        $webDriverActions = new WebDriverActions($webDriver);
969
970 5
        return $webDriverActions;
971
    }
972
973 2
    private function geWebDriverKeyValue($char, $modifier = null)
974
    {
975 2
        if (\is_int($char)) {
976 2
            $char = \strtolower(\chr($char));
977
        }
978
979 1
        switch ($modifier) {
980 2
            case 'alt':
981
                return WebDriverKeys::ALT.$char;
982 2
            case 'ctrl':
983 1
                return WebDriverKeys::CONTROL.$char;
984 1
            case 'shift':
985
                return WebDriverKeys::SHIFT.$char;
986 1
            case 'meta':
987
                return WebDriverKeys::META.$char;
988
            case null:
989
            default:
990 1
                return $char;
991
        }
992
    }
993
}
994