Completed
Pull Request — master (#14)
by 12345
04:19
created

PantherDriver   F

Complexity

Total Complexity 118

Size/Duplication

Total Lines 918
Duplicated Lines 0 %

Test Coverage

Coverage 84.5%

Importance

Changes 15
Bugs 0 Features 3
Metric Value
wmc 118
eloc 267
c 15
b 0
f 3
dl 0
loc 918
ccs 289
cts 342
cp 0.845
rs 2

68 Methods

Rating   Name   Duplication   Size   Complexity  
A switchToWindow() 0 4 1
A getHtml() 0 4 1
A check() 0 3 1
A getCrawlerElement() 0 9 2
A getJsNode() 0 3 1
A keyUp() 0 6 1
A setCookie() 0 11 2
A keyDown() 0 6 1
A getOuterHtml() 0 5 1
A click() 0 4 1
A isSelected() 0 3 1
A resizeWindow() 0 4 1
A getInputFormField() 0 10 2
A isChecked() 0 3 1
A prepareUrl() 0 5 3
A getResponse() 0 9 2
A stop() 0 5 1
A findElementXpaths() 0 10 2
A getTextareaFormField() 0 10 2
A getFileFormField() 0 10 2
A getWindowName() 0 3 1
A isStarted() 0 3 1
A selectOption() 0 11 2
A getCurrentUrl() 0 3 1
A visit() 0 3 1
A mouseOver() 0 3 1
A reload() 0 3 1
A toCoordinates() 0 11 2
A dragTo() 0 6 1
A deleteCookie() 0 12 3
A maximizeWindow() 0 5 1
A getValue() 0 15 4
A getAttribute() 0 16 3
A getCrawler() 0 9 2
A focus() 0 5 1
A doubleClick() 0 3 1
B geWebDriverKeyValue() 0 18 7
A getCookiePath() 0 9 2
A getWindowNames() 0 3 1
A getText() 0 7 1
A getWebDriverActions() 0 9 2
B getFormField() 0 26 7
A getScreenshot() 0 3 1
A submitForm() 0 6 1
A attachFile() 0 11 2
A reset() 0 24 3
A executeScript() 0 8 2
A rightClick() 0 3 1
A start() 0 6 1
A switchToIFrame() 0 8 2
A uncheck() 0 3 1
A back() 0 3 1
A getContent() 0 3 1
A getChoiceFormField() 0 16 2
A isVisible() 0 3 1
A evaluateScript() 0 7 2
A getClient() 0 3 1
A keyPress() 0 6 1
A wait() 0 12 3
A getCookie() 0 11 3
A setRemoveHostFromUrl() 0 7 1
A setValue() 0 26 5
A blur() 0 9 2
A setRemoveScriptFromUrl() 0 7 1
A getTagName() 0 3 1
A __construct() 0 4 1
A getFilteredCrawler() 0 7 2
A forward() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like PantherDriver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PantherDriver, and based on these observations, apply Extract Interface, too.

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