Completed
Pull Request — master (#13)
by Dalibor
03:14
created

PantherDriver::getOuterHtml()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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