Passed
Pull Request — 1.x (#53)
by Kevin
02:04
created

BrowserKitDriver::mergeForms()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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