Test Failed
Push — master ( fae140...0d9b71 )
by Robert
19:06
created

PantherDriver::getWindowName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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