Completed
Push — 1.x ( 34b55f...3a787a )
by Kevin
16s queued 11s
created

BrowserKitDriver::addButtons()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 4
nop 2
dl 0
loc 18
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Behat\Mink.
5
 * (c) Konstantin Kudryashov <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Zenstruck\Browser\Mink;
12
13
use Behat\Mink\Driver\CoreDriver;
14
use Behat\Mink\Exception\DriverException;
15
use Behat\Mink\Exception\UnsupportedDriverActionException;
16
use Symfony\Component\BrowserKit\AbstractBrowser;
17
use Symfony\Component\BrowserKit\Cookie;
18
use Symfony\Component\BrowserKit\Exception\BadMethodCallException;
19
use Symfony\Component\BrowserKit\Response;
20
use Symfony\Component\DomCrawler\Crawler;
21
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
22
use Symfony\Component\DomCrawler\Field\FileFormField;
23
use Symfony\Component\DomCrawler\Field\FormField;
24
use Symfony\Component\DomCrawler\Field\InputFormField;
25
use Symfony\Component\DomCrawler\Field\TextareaFormField;
26
use Symfony\Component\DomCrawler\Form;
27
use Symfony\Component\HttpKernel\HttpKernelBrowser;
28
29
/**
30
 * Copied from https://github.com/minkphp/MinkBrowserKitDriver for use
31
 * until it supports Symfony 5/PHP 8.
32
 *
33
 * @ref https://github.com/minkphp/MinkBrowserKitDriver
34
 * @ref https://github.com/minkphp/MinkBrowserKitDriver/pull/151
35
 *
36
 * @author Konstantin Kudryashov <[email protected]>
37
 *
38
 * @internal
39
 */
40
final class BrowserKitDriver extends CoreDriver
41
{
42
    private $client;
43
44
    /**
45
     * @var Form[]
46
     */
47
    private $forms = [];
48
    private $serverParameters = [];
49
    private $started = false;
50
    private $removeScriptFromUrl = false;
51
    private $removeHostFromUrl = false;
52
53
    /**
54
     * Initializes BrowserKit driver.
55
     *
56
     * @param AbstractBrowser $client  BrowserKit client instance
57
     * @param string|null     $baseUrl Base URL for HttpKernel clients
58
     */
59
    public function __construct(AbstractBrowser $client, $baseUrl = null)
60
    {
61
        $this->client = $client;
62
        $this->client->followRedirects(true);
63
64
        if (null !== $baseUrl && $client instanceof HttpKernelBrowser) {
65
            $client->setServerParameter('SCRIPT_FILENAME', \parse_url($baseUrl, \PHP_URL_PATH));
66
        }
67
    }
68
69
    /**
70
     * Returns BrowserKit HTTP client instance.
71
     *
72
     * @return AbstractBrowser
73
     */
74
    public function getClient()
75
    {
76
        return $this->client;
77
    }
78
79
    /**
80
     * Tells driver to remove hostname from URL.
81
     *
82
     * @param bool $remove
83
     *
84
     * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead.
85
     */
86
    public function setRemoveHostFromUrl($remove = true)
87
    {
88
        @\trigger_error(
89
            'setRemoveHostFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.',
90
            \E_USER_DEPRECATED
91
        );
92
        $this->removeHostFromUrl = (bool) $remove;
93
    }
94
95
    /**
96
     * Tells driver to remove script name from URL.
97
     *
98
     * @param bool $remove
99
     *
100
     * @deprecated Deprecated as of 1.2, to be removed in 2.0. Pass the base url in the constructor instead.
101
     */
102
    public function setRemoveScriptFromUrl($remove = true)
103
    {
104
        @\trigger_error(
105
            'setRemoveScriptFromUrl() is deprecated as of 1.2 and will be removed in 2.0. Pass the base url in the constructor instead.',
106
            \E_USER_DEPRECATED
107
        );
108
        $this->removeScriptFromUrl = (bool) $remove;
109
    }
110
111
    public function start()
112
    {
113
        $this->started = true;
114
    }
115
116
    public function isStarted()
117
    {
118
        return $this->started;
119
    }
120
121
    public function stop()
122
    {
123
        $this->reset();
124
        $this->started = false;
125
    }
126
127
    public function reset()
128
    {
129
        // Restarting the client resets the cookies and the history
130
        $this->client->restart();
131
        $this->forms = [];
132
        $this->serverParameters = [];
133
    }
134
135
    public function visit($url)
136
    {
137
        $this->client->request('GET', $this->prepareUrl($url), [], [], $this->serverParameters);
138
        $this->forms = [];
139
    }
140
141
    public function getCurrentUrl()
142
    {
143
        // This should be encapsulated in `getRequest` method if any other method needs the request
144
        try {
145
            $request = $this->client->getInternalRequest();
146
        } catch (BadMethodCallException $e) {
147
            // Handling Symfony 5+ behaviour
148
            $request = null;
149
        }
150
151
        if (null === $request) {
152
            throw new DriverException('Unable to access the request before visiting a page');
153
        }
154
155
        return $request->getUri();
156
    }
157
158
    public function reload()
159
    {
160
        $this->client->reload();
161
        $this->forms = [];
162
    }
163
164
    public function forward()
165
    {
166
        $this->client->forward();
167
        $this->forms = [];
168
    }
169
170
    public function back()
171
    {
172
        $this->client->back();
173
        $this->forms = [];
174
    }
175
176
    public function setBasicAuth($user, $password)
177
    {
178
        if (false === $user) {
179
            unset($this->serverParameters['PHP_AUTH_USER'], $this->serverParameters['PHP_AUTH_PW']);
180
181
            return;
182
        }
183
184
        $this->serverParameters['PHP_AUTH_USER'] = $user;
185
        $this->serverParameters['PHP_AUTH_PW'] = $password;
186
    }
187
188
    public function setRequestHeader($name, $value)
189
    {
190
        $contentHeaders = ['CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true];
191
        $name = \str_replace('-', '_', \mb_strtoupper($name));
192
193
        // CONTENT_* are not prefixed with HTTP_ in PHP when building $_SERVER
194
        if (!isset($contentHeaders[$name])) {
195
            $name = 'HTTP_'.$name;
196
        }
197
198
        $this->serverParameters[$name] = $value;
199
    }
200
201
    public function getResponseHeaders()
202
    {
203
        return $this->getResponse()->getHeaders();
204
    }
205
206
    public function setCookie($name, $value = null)
207
    {
208
        if (null === $value) {
209
            $this->deleteCookie($name);
210
211
            return;
212
        }
213
214
        $jar = $this->client->getCookieJar();
215
        $jar->set(new Cookie($name, $value));
216
    }
217
218
    public function getCookie($name)
219
    {
220
        // Note that the following doesn't work well because
221
        // Symfony\Component\BrowserKit\CookieJar stores cookies by name,
222
        // path, AND domain and if you don't fill them all in correctly then
223
        // you won't get the value that you're expecting.
224
        //
225
        // $jar = $this->client->getCookieJar();
226
        //
227
        // if (null !== $cookie = $jar->get($name)) {
228
        //     return $cookie->getValue();
229
        // }
230
231
        $allValues = $this->client->getCookieJar()->allValues($this->getCurrentUrl());
232
233
        if (isset($allValues[$name])) {
234
            return $allValues[$name];
235
        }
236
237
        return null;
238
    }
239
240
    public function getStatusCode()
241
    {
242
        $response = $this->getResponse();
243
244
        // BC layer for Symfony < 4.3
245
        if (!\method_exists($response, 'getStatusCode')) {
246
            return $response->getStatus();
0 ignored issues
show
Bug introduced by
The method getStatus() does not exist on Symfony\Component\BrowserKit\Response. Did you maybe mean getStatusCode()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

246
            return $response->/** @scrutinizer ignore-call */ getStatus();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
247
        }
248
249
        return $response->getStatusCode();
250
    }
251
252
    public function getContent()
253
    {
254
        return $this->getResponse()->getContent();
255
    }
256
257
    public function findElementXpaths($xpath)
258
    {
259
        $nodes = $this->getCrawler()->filterXPath($xpath);
260
261
        $elements = [];
262
        foreach ($nodes as $i => $node) {
263
            $elements[] = \sprintf('(%s)[%d]', $xpath, $i + 1);
264
        }
265
266
        return $elements;
267
    }
268
269
    public function getTagName($xpath)
270
    {
271
        return $this->getCrawlerNode($this->getFilteredCrawler($xpath))->nodeName;
272
    }
273
274
    public function getText($xpath)
275
    {
276
        $text = $this->getFilteredCrawler($xpath)->text(null, true);
277
        // TODO drop our own normalization once supporting only dom-crawler 4.4+ as it already does it.
278
        $text = \str_replace("\n", ' ', $text);
279
        $text = \preg_replace('/ {2,}/', ' ', $text);
280
281
        return \trim($text);
282
    }
283
284
    public function getHtml($xpath)
285
    {
286
        return $this->getFilteredCrawler($xpath)->html();
287
    }
288
289
    public function getOuterHtml($xpath)
290
    {
291
        $crawler = $this->getFilteredCrawler($xpath);
292
293
        if (\method_exists($crawler, 'outerHtml')) {
294
            return $crawler->outerHtml();
295
        }
296
297
        $node = $this->getCrawlerNode($crawler);
298
299
        return $node->ownerDocument->saveHTML($node);
0 ignored issues
show
Bug introduced by
The method saveHTML() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

299
        return $node->ownerDocument->/** @scrutinizer ignore-call */ saveHTML($node);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
300
    }
301
302
    public function getAttribute($xpath, $name)
303
    {
304
        $node = $this->getFilteredCrawler($xpath);
305
306
        if ($this->getCrawlerNode($node)->hasAttribute($name)) {
307
            return $node->attr($name);
308
        }
309
310
        return null;
311
    }
312
313
    public function getValue($xpath)
314
    {
315
        if (\in_array($this->getAttribute($xpath, 'type'), ['submit', 'image', 'button'], true)) {
316
            return $this->getAttribute($xpath, 'value');
317
        }
318
319
        $node = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
320
321
        if ('option' === $node->tagName) {
322
            return $this->getOptionValue($node);
323
        }
324
325
        try {
326
            $field = $this->getFormField($xpath);
327
        } catch (\InvalidArgumentException $e) {
328
            return $this->getAttribute($xpath, 'value');
329
        }
330
331
        $value = $field->getValue();
332
333
        if ('select' === $node->tagName && null === $value) {
0 ignored issues
show
introduced by
The condition null === $value is always false.
Loading history...
334
            // symfony/dom-crawler returns null as value for a non-multiple select without
335
            // options but we want an empty string to match browsers.
336
            $value = '';
337
        }
338
339
        return $value;
340
    }
341
342
    public function setValue($xpath, $value)
343
    {
344
        $this->getFormField($xpath)->setValue($value);
345
    }
346
347
    public function check($xpath)
348
    {
349
        $this->getCheckboxField($xpath)->tick();
350
    }
351
352
    public function uncheck($xpath)
353
    {
354
        $this->getCheckboxField($xpath)->untick();
355
    }
356
357
    public function selectOption($xpath, $value, $multiple = false)
358
    {
359
        $field = $this->getFormField($xpath);
360
361
        if (!$field instanceof ChoiceFormField) {
362
            throw new DriverException(\sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath));
363
        }
364
365
        if ($multiple) {
366
            $oldValue = (array) $field->getValue();
367
            $oldValue[] = $value;
368
            $value = $oldValue;
369
        }
370
371
        $field->select($value);
372
    }
373
374
    public function isSelected($xpath)
375
    {
376
        $optionValue = $this->getOptionValue($this->getCrawlerNode($this->getFilteredCrawler($xpath)));
377
        $selectField = $this->getFormField('('.$xpath.')/ancestor-or-self::*[local-name()="select"]');
378
        $selectValue = $selectField->getValue();
379
380
        return \is_array($selectValue) ? \in_array($optionValue, $selectValue, true) : $optionValue === $selectValue;
0 ignored issues
show
introduced by
The condition is_array($selectValue) is always false.
Loading history...
381
    }
382
383
    public function click($xpath)
384
    {
385
        $crawler = $this->getFilteredCrawler($xpath);
386
        $node = $this->getCrawlerNode($crawler);
387
        $tagName = $node->nodeName;
388
389
        if ('a' === $tagName) {
390
            $this->client->click($crawler->link());
391
            $this->forms = [];
392
        } elseif ($this->canSubmitForm($node)) {
393
            $this->submit($crawler->form());
394
        } elseif ($this->canResetForm($node)) {
395
            $this->resetForm($node);
396
        } else {
397
            $message = \sprintf('%%s supports clicking on links and submit or reset buttons only. But "%s" provided', $tagName);
398
399
            throw new UnsupportedDriverActionException($message, $this);
400
        }
401
    }
402
403
    public function isChecked($xpath)
404
    {
405
        $field = $this->getFormField($xpath);
406
407
        if (!$field instanceof ChoiceFormField || 'select' === $field->getType()) {
408
            throw new DriverException(\sprintf('Impossible to get the checked state of the element with XPath "%s" as it is not a checkbox or radio input', $xpath));
409
        }
410
411
        if ('checkbox' === $field->getType()) {
412
            return $field->hasValue();
413
        }
414
415
        $radio = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
416
417
        return $radio->getAttribute('value') === $field->getValue();
418
    }
419
420
    public function attachFile($xpath, $path)
421
    {
422
        $files = (array) $path;
423
        $field = $this->getFormField($xpath);
424
425
        if (!$field instanceof FileFormField) {
426
            throw new DriverException(\sprintf('Impossible to attach a file on the element with XPath "%s" as it is not a file input', $xpath));
427
        }
428
429
        $field->upload(\array_shift($files));
430
431
        if (empty($files)) {
432
            // not multiple files
433
            return;
434
        }
435
436
        $node = $this->getFilteredCrawler($xpath);
437
438
        if (null === $node->attr('multiple')) {
439
            throw new \InvalidArgumentException('Cannot attach multiple files to a non-multiple file field.');
440
        }
441
442
        $fieldNode = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
443
        $form = $this->getFormForFieldNode($fieldNode);
444
445
        foreach ($files as $file) {
446
            $field = new FileFormField($fieldNode);
447
            $field->upload($file);
448
449
            $form->set($field);
450
        }
451
    }
452
453
    public function submitForm($xpath)
454
    {
455
        $crawler = $this->getFilteredCrawler($xpath);
456
457
        $this->submit($crawler->form());
458
    }
459
460
    /**
461
     * @return Response
462
     *
463
     * @throws DriverException If there is not response yet
464
     */
465
    protected function getResponse()
466
    {
467
        try {
468
            $response = $this->client->getInternalResponse();
469
        } catch (BadMethodCallException $e) {
470
            // Handling Symfony 5+ behaviour
471
            $response = null;
472
        }
473
474
        if (null === $response) {
475
            throw new DriverException('Unable to access the response before visiting a page');
476
        }
477
478
        return $response;
479
    }
480
481
    /**
482
     * Prepares URL for visiting.
483
     * Removes "*.php/" from urls and then passes it to BrowserKitDriver::visit().
484
     *
485
     * @param string $url
486
     *
487
     * @return string
488
     */
489
    protected function prepareUrl($url)
490
    {
491
        if (!$this->removeHostFromUrl && !$this->removeScriptFromUrl) {
492
            return $url;
493
        }
494
495
        $replacement = ($this->removeHostFromUrl ? '' : '$1').($this->removeScriptFromUrl ? '' : '$2');
496
497
        return \preg_replace('#(https?\://[^/]+)(/[^/\.]+\.php)?#', $replacement, $url);
498
    }
499
500
    /**
501
     * Returns form field from XPath query.
502
     *
503
     * @param string $xpath
504
     *
505
     * @return FormField
506
     *
507
     * @throws DriverException
508
     */
509
    protected function getFormField($xpath)
510
    {
511
        $fieldNode = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
512
        $fieldName = \str_replace('[]', '', $fieldNode->getAttribute('name'));
513
514
        $form = $this->getFormForFieldNode($fieldNode);
515
516
        if (\is_array($form[$fieldName])) {
517
            return $form[$fieldName][$this->getFieldPosition($fieldNode)];
518
        }
519
520
        return $form[$fieldName];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $form[$fieldName] also could return the type Symfony\Component\DomCra...wler\Field\FormField[]> which is incompatible with the documented return type Symfony\Component\DomCrawler\Field\FormField.
Loading history...
521
    }
522
523
    private function getFormForFieldNode(\DOMElement $fieldNode): Form
524
    {
525
        $formNode = $this->getFormNode($fieldNode);
526
        $formId = $this->getFormNodeId($formNode);
527
528
        if (!isset($this->forms[$formId])) {
529
            $this->forms[$formId] = new Form($formNode, $this->getCurrentUrl());
530
        }
531
532
        return $this->forms[$formId];
533
    }
534
535
    /**
536
     * Deletes a cookie by name.
537
     *
538
     * @param string $name cookie name
539
     */
540
    private function deleteCookie($name)
541
    {
542
        $path = $this->getCookiePath();
543
        $jar = $this->client->getCookieJar();
544
545
        do {
546
            if (null !== $jar->get($name, $path)) {
547
                $jar->expire($name, $path);
548
            }
549
550
            $path = \preg_replace('/.$/', '', $path);
551
        } while ($path);
552
    }
553
554
    /**
555
     * Returns current cookie path.
556
     *
557
     * @return string
558
     */
559
    private function getCookiePath()
560
    {
561
        $path = \dirname(\parse_url($this->getCurrentUrl(), \PHP_URL_PATH));
562
563
        if ('\\' === \DIRECTORY_SEPARATOR) {
564
            $path = \str_replace('\\', '/', $path);
565
        }
566
567
        return $path;
568
    }
569
570
    /**
571
     * Returns the checkbox field from xpath query, ensuring it is valid.
572
     *
573
     * @param string $xpath
574
     *
575
     * @return ChoiceFormField
576
     *
577
     * @throws DriverException when the field is not a checkbox
578
     */
579
    private function getCheckboxField($xpath)
580
    {
581
        $field = $this->getFormField($xpath);
582
583
        if (!$field instanceof ChoiceFormField) {
584
            throw new DriverException(\sprintf('Impossible to check the element with XPath "%s" as it is not a checkbox', $xpath));
585
        }
586
587
        return $field;
588
    }
589
590
    /**
591
     * @return \DOMElement
592
     *
593
     * @throws DriverException if the form node cannot be found
594
     */
595
    private function getFormNode(\DOMElement $element)
596
    {
597
        if ($element->hasAttribute('form')) {
598
            $formId = $element->getAttribute('form');
599
            $formNode = $element->ownerDocument->getElementById($formId);
0 ignored issues
show
Bug introduced by
The method getElementById() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

599
            /** @scrutinizer ignore-call */ 
600
            $formNode = $element->ownerDocument->getElementById($formId);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
600
601
            if (null === $formNode || 'form' !== $formNode->nodeName) {
602
                throw new DriverException(\sprintf('The selected node has an invalid form attribute (%s).', $formId));
603
            }
604
605
            return $formNode;
606
        }
607
608
        $formNode = $element;
609
610
        do {
611
            // use the ancestor form element
612
            if (null === $formNode = $formNode->parentNode) {
613
                throw new DriverException('The selected node does not have a form ancestor.');
614
            }
615
        } while ('form' !== $formNode->nodeName);
616
617
        return $formNode;
618
    }
619
620
    /**
621
     * Gets the position of the field node among elements with the same name.
622
     *
623
     * BrowserKit uses the field name as index to find the field in its Form object.
624
     * When multiple fields have the same name (checkboxes for instance), it will return
625
     * an array of elements in the order they appear in the DOM.
626
     *
627
     * @return int
628
     */
629
    private function getFieldPosition(\DOMElement $fieldNode)
630
    {
631
        $elements = $this->getCrawler()->filterXPath('//*[@name=\''.$fieldNode->getAttribute('name').'\']');
632
633
        if (\count($elements) > 1) {
634
            // more than one element contains this name !
635
            // so we need to find the position of $fieldNode
636
            foreach ($elements as $key => $element) {
637
                /** @var \DOMElement $element */
638
                if ($element->getNodePath() === $fieldNode->getNodePath()) {
639
                    return $key;
640
                }
641
            }
642
        }
643
644
        return 0;
645
    }
646
647
    private function submit(Form $form)
648
    {
649
        $formId = $this->getFormNodeId($form->getFormNode());
650
651
        if (isset($this->forms[$formId])) {
652
            $form = $this->addButtons($form, $this->forms[$formId]);
653
        }
654
655
        // remove empty file fields from request
656
        foreach ($form->getFiles() as $name => $field) {
657
            if (empty($field['name']) && empty($field['tmp_name'])) {
658
                $form->remove($name);
659
            }
660
        }
661
662
        foreach ($form->all() as $field) {
663
            // Add a fix for https://github.com/symfony/symfony/pull/10733 to support Symfony versions which are not fixed
664
            if ($field instanceof TextareaFormField && null === $field->getValue()) {
665
                $field->setValue('');
666
            }
667
        }
668
669
        $this->client->submit($form, [], $this->serverParameters);
670
671
        $this->forms = [];
672
    }
673
674
    private function resetForm(\DOMElement $fieldNode)
675
    {
676
        $formNode = $this->getFormNode($fieldNode);
677
        $formId = $this->getFormNodeId($formNode);
678
        unset($this->forms[$formId]);
679
    }
680
681
    /**
682
     * Determines if a node can submit a form.
683
     *
684
     * @param \DOMElement $node node
685
     *
686
     * @return bool
687
     */
688
    private function canSubmitForm(\DOMElement $node)
689
    {
690
        $type = $node->hasAttribute('type') ? $node->getAttribute('type') : null;
691
692
        if ('input' === $node->nodeName && \in_array($type, ['submit', 'image'], true)) {
693
            return true;
694
        }
695
696
        return 'button' === $node->nodeName && (null === $type || 'submit' === $type);
697
    }
698
699
    /**
700
     * Determines if a node can reset a form.
701
     *
702
     * @param \DOMElement $node node
703
     *
704
     * @return bool
705
     */
706
    private function canResetForm(\DOMElement $node)
707
    {
708
        $type = $node->hasAttribute('type') ? $node->getAttribute('type') : null;
709
710
        return \in_array($node->nodeName, ['input', 'button'], true) && 'reset' === $type;
711
    }
712
713
    /**
714
     * Returns form node unique identifier.
715
     *
716
     * @return string
717
     */
718
    private function getFormNodeId(\DOMElement $form)
719
    {
720
        return \md5($form->getLineNo().$form->getNodePath().$form->nodeValue);
721
    }
722
723
    /**
724
     * Gets the value of an option element.
725
     *
726
     * @return string
727
     *
728
     * @see \Symfony\Component\DomCrawler\Field\ChoiceFormField::buildOptionValue
729
     */
730
    private function getOptionValue(\DOMElement $option)
731
    {
732
        if ($option->hasAttribute('value')) {
733
            return $option->getAttribute('value');
734
        }
735
736
        if (!empty($option->nodeValue)) {
737
            return $option->nodeValue;
738
        }
739
740
        return '1'; // DomCrawler uses 1 by default if there is no text in the option
741
    }
742
743
    /**
744
     * Returns DOMElement from crawler instance.
745
     *
746
     * @return \DOMElement
747
     *
748
     * @throws DriverException when the node does not exist
749
     */
750
    private function getCrawlerNode(Crawler $crawler)
751
    {
752
        $node = null;
753
754
        if ($crawler instanceof \Iterator) {
755
            // for symfony 2.3 compatibility as getNode is not public before symfony 2.4
756
            $crawler->rewind();
757
            $node = $crawler->current();
758
        } else {
759
            $node = $crawler->getNode(0);
760
        }
761
762
        if (null !== $node) {
763
            return $node;
764
        }
765
766
        throw new DriverException('The element does not exist');
767
    }
768
769
    /**
770
     * Returns a crawler filtered for the given XPath, requiring at least 1 result.
771
     *
772
     * @param string $xpath
773
     *
774
     * @return Crawler
775
     *
776
     * @throws DriverException when no matching elements are found
777
     */
778
    private function getFilteredCrawler($xpath)
779
    {
780
        if (!\count($crawler = $this->getCrawler()->filterXPath($xpath))) {
781
            throw new DriverException(\sprintf('There is no element matching XPath "%s"', $xpath));
782
        }
783
784
        return $crawler;
785
    }
786
787
    /**
788
     * Returns crawler instance (got from client).
789
     *
790
     * @return Crawler
791
     *
792
     * @throws DriverException
793
     */
794
    private function getCrawler()
795
    {
796
        $crawler = $this->client->getCrawler();
797
798
        if (null === $crawler) {
799
            throw new DriverException('Unable to access the response content before visiting a page');
800
        }
801
802
        return $crawler;
803
    }
804
805
    /**
806
     * Adds button fields from submitted form to cached version.
807
     */
808
    private function addButtons(Form $submitted, Form $cached): Form
809
    {
810
        foreach ($submitted->all() as $field) {
811
            if (!$field instanceof InputFormField) {
812
                continue;
813
            }
814
815
            $nodeReflection = (new \ReflectionObject($field))->getProperty('node');
816
            $nodeReflection->setAccessible(true);
817
818
            $node = $nodeReflection->getValue($field);
819
820
            if ('button' === $node->nodeName || \in_array($node->getAttribute('type'), ['submit', 'button', 'image'])) {
821
                $cached->set($field);
822
            }
823
        }
824
825
        return $cached;
826
    }
827
}
828