Completed
Pull Request — master (#13)
by Dalibor
02:15
created

PantherDriver::switchToIFrame()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

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