Passed
Push — 1.x ( 473914...34b55f )
by Kevin
02:24
created

BrowserKitDriver   F

Complexity

Total Complexity 124

Size/Duplication

Total Lines 763
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 250
c 1
b 0
f 0
dl 0
loc 763
rs 2
wmc 124

54 Methods

Rating   Name   Duplication   Size   Complexity  
A reload() 0 4 1
A setBasicAuth() 0 10 2
A isStarted() 0 3 1
A selectOption() 0 15 3
A visit() 0 4 1
A getCookie() 0 20 2
A findElementXpaths() 0 10 2
A getClient() 0 3 1
A isSelected() 0 7 2
A click() 0 17 4
A getStatusCode() 0 10 2
A getOuterHtml() 0 11 2
A start() 0 3 1
A __construct() 0 7 3
A isChecked() 0 15 4
A getCurrentUrl() 0 15 3
A check() 0 3 1
A getHtml() 0 3 1
A back() 0 4 1
A setRequestHeader() 0 11 2
A setRemoveScriptFromUrl() 0 7 1
A getTagName() 0 3 1
A setRemoveHostFromUrl() 0 7 1
A getContent() 0 3 1
A uncheck() 0 3 1
A getAttribute() 0 9 2
A getResponseHeaders() 0 3 1
A forward() 0 4 1
A getText() 0 8 1
A setCookie() 0 10 2
A reset() 0 6 1
A stop() 0 4 1
A getValue() 0 27 6
A setValue() 0 3 1
A getFormForFieldNode() 0 10 2
A getCrawler() 0 9 2
A submitForm() 0 5 1
A attachFile() 0 30 5
A getCrawlerNode() 0 17 3
A resetForm() 0 5 1
A getFieldPosition() 0 16 4
A getFormField() 0 12 2
B submit() 0 25 8
A deleteCookie() 0 12 3
A canSubmitForm() 0 9 6
A getFormNode() 0 23 6
A getCheckboxField() 0 9 2
A getCookiePath() 0 9 2
A canResetForm() 0 5 3
A prepareUrl() 0 9 5
A getOptionValue() 0 11 3
A getFormNodeId() 0 3 1
A getResponse() 0 14 3
A getFilteredCrawler() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like BrowserKitDriver 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 BrowserKitDriver, and based on these observations, apply Extract Interface, too.

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
        $files = (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(\array_shift($files));
429
430
        if (empty($files)) {
431
            // not multiple files
432
            return;
433
        }
434
435
        $node = $this->getFilteredCrawler($xpath);
436
437
        if (null === $node->attr('multiple')) {
438
            throw new \InvalidArgumentException('Cannot attach multiple files to a non-multiple file field.');
439
        }
440
441
        $fieldNode = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
442
        $form = $this->getFormForFieldNode($fieldNode);
443
444
        foreach ($files as $file) {
445
            $field = new FileFormField($fieldNode);
446
            $field->upload($file);
447
448
            $form->set($field);
449
        }
450
    }
451
452
    public function submitForm($xpath)
453
    {
454
        $crawler = $this->getFilteredCrawler($xpath);
455
456
        $this->submit($crawler->form());
457
    }
458
459
    /**
460
     * @return Response
461
     *
462
     * @throws DriverException If there is not response yet
463
     */
464
    protected function getResponse()
465
    {
466
        try {
467
            $response = $this->client->getInternalResponse();
468
        } catch (BadMethodCallException $e) {
469
            // Handling Symfony 5+ behaviour
470
            $response = null;
471
        }
472
473
        if (null === $response) {
474
            throw new DriverException('Unable to access the response before visiting a page');
475
        }
476
477
        return $response;
478
    }
479
480
    /**
481
     * Prepares URL for visiting.
482
     * Removes "*.php/" from urls and then passes it to BrowserKitDriver::visit().
483
     *
484
     * @param string $url
485
     *
486
     * @return string
487
     */
488
    protected function prepareUrl($url)
489
    {
490
        if (!$this->removeHostFromUrl && !$this->removeScriptFromUrl) {
491
            return $url;
492
        }
493
494
        $replacement = ($this->removeHostFromUrl ? '' : '$1').($this->removeScriptFromUrl ? '' : '$2');
495
496
        return \preg_replace('#(https?\://[^/]+)(/[^/\.]+\.php)?#', $replacement, $url);
497
    }
498
499
    /**
500
     * Returns form field from XPath query.
501
     *
502
     * @param string $xpath
503
     *
504
     * @return FormField
505
     *
506
     * @throws DriverException
507
     */
508
    protected function getFormField($xpath)
509
    {
510
        $fieldNode = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
511
        $fieldName = \str_replace('[]', '', $fieldNode->getAttribute('name'));
512
513
        $form = $this->getFormForFieldNode($fieldNode);
514
515
        if (\is_array($form[$fieldName])) {
516
            return $form[$fieldName][$this->getFieldPosition($fieldNode)];
517
        }
518
519
        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...
520
    }
521
522
    private function getFormForFieldNode(\DOMElement $fieldNode): Form
523
    {
524
        $formNode = $this->getFormNode($fieldNode);
525
        $formId = $this->getFormNodeId($formNode);
526
527
        if (!isset($this->forms[$formId])) {
528
            $this->forms[$formId] = new Form($formNode, $this->getCurrentUrl());
529
        }
530
531
        return $this->forms[$formId];
532
    }
533
534
    /**
535
     * Deletes a cookie by name.
536
     *
537
     * @param string $name cookie name
538
     */
539
    private function deleteCookie($name)
540
    {
541
        $path = $this->getCookiePath();
542
        $jar = $this->client->getCookieJar();
543
544
        do {
545
            if (null !== $jar->get($name, $path)) {
546
                $jar->expire($name, $path);
547
            }
548
549
            $path = \preg_replace('/.$/', '', $path);
550
        } while ($path);
551
    }
552
553
    /**
554
     * Returns current cookie path.
555
     *
556
     * @return string
557
     */
558
    private function getCookiePath()
559
    {
560
        $path = \dirname(\parse_url($this->getCurrentUrl(), \PHP_URL_PATH));
561
562
        if ('\\' === \DIRECTORY_SEPARATOR) {
563
            $path = \str_replace('\\', '/', $path);
564
        }
565
566
        return $path;
567
    }
568
569
    /**
570
     * Returns the checkbox field from xpath query, ensuring it is valid.
571
     *
572
     * @param string $xpath
573
     *
574
     * @return ChoiceFormField
575
     *
576
     * @throws DriverException when the field is not a checkbox
577
     */
578
    private function getCheckboxField($xpath)
579
    {
580
        $field = $this->getFormField($xpath);
581
582
        if (!$field instanceof ChoiceFormField) {
583
            throw new DriverException(\sprintf('Impossible to check the element with XPath "%s" as it is not a checkbox', $xpath));
584
        }
585
586
        return $field;
587
    }
588
589
    /**
590
     * @return \DOMElement
591
     *
592
     * @throws DriverException if the form node cannot be found
593
     */
594
    private function getFormNode(\DOMElement $element)
595
    {
596
        if ($element->hasAttribute('form')) {
597
            $formId = $element->getAttribute('form');
598
            $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

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