Test Failed
Push — master ( 485f5c...a89989 )
by Robert
03:10
created

src/PantherDriver.php (1 issue)

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