PantherDriver   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 326
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
eloc 117
dl 0
loc 326
rs 3.28
c 2
b 1
f 1
wmc 64

36 Methods

Rating   Name   Duplication   Size   Complexity  
A isVisible() 0 3 1
A isChecked() 0 9 2
A getHtml() 0 4 1
A evaluateScript() 0 7 2
A attachFile() 0 8 4
A click() 0 4 1
A executeScript() 0 8 2
A visit() 0 3 1
A start() 0 3 1
A crawler() 0 6 2
A getCurrentUrl() 0 3 1
A __construct() 0 3 1
A inputFormField() 0 8 2
A findElementXpaths() 0 11 2
A isStarted() 0 3 1
A toCoordinates() 0 9 2
A getTagName() 0 3 1
A selectOption() 0 12 2
A crawlerElement() 0 7 2
A choiceFormField() 0 8 2
A uncheck() 0 3 1
A setValue() 0 26 5
A getOuterHtml() 0 3 1
A filteredCrawler() 0 7 2
A getContent() 0 3 1
A textareaFormField() 0 8 2
A check() 0 3 1
A getText() 0 12 3
A getAttribute() 0 3 1
A reset() 0 3 1
A formField() 0 12 4
A stop() 0 3 1
A prepareUrl() 0 3 1
A jsNode() 0 3 1
A fileFormField() 0 8 2
A getValue() 0 16 4

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
namespace Zenstruck\Browser\Mink;
4
5
use Behat\Mink\Driver\CoreDriver;
6
use Behat\Mink\Exception\DriverException;
7
use Facebook\WebDriver\Exception\NoSuchElementException;
8
use Facebook\WebDriver\Interactions\Internal\WebDriverCoordinates;
9
use Facebook\WebDriver\Internal\WebDriverLocatable;
10
use Facebook\WebDriver\WebDriverElement;
11
use Facebook\WebDriver\WebDriverSelect;
12
use Symfony\Component\BrowserKit\Exception\BadMethodCallException;
13
use Symfony\Component\DomCrawler\Field\FormField;
14
use Symfony\Component\Panther\Client;
15
use Symfony\Component\Panther\DomCrawler\Crawler;
16
use Symfony\Component\Panther\DomCrawler\Field\ChoiceFormField;
17
use Symfony\Component\Panther\DomCrawler\Field\FileFormField;
18
use Symfony\Component\Panther\DomCrawler\Field\InputFormField;
19
use Symfony\Component\Panther\DomCrawler\Field\TextareaFormField;
20
21
/**
22
 * @ref https://github.com/robertfausk/mink-panther-driver
23
 *
24
 * @author Robert Freigang <[email protected]>
25
 * @author Kevin Bond <[email protected]>
26
 *
27
 * @internal
28
 */
29
final class PantherDriver extends CoreDriver
30
{
31
    private Client $client;
32
    private bool $started = false;
33
34
    public function __construct(Client $client)
35
    {
36
        $this->client = $client;
37
    }
38
39
    public function start(): void
40
    {
41
        $this->started = true;
42
    }
43
44
    public function stop(): void
45
    {
46
        $this->started = false;
47
    }
48
49
    public function isStarted(): bool
50
    {
51
        return $this->started;
52
    }
53
54
    public function reset(): void
55
    {
56
        $this->client->restart();
57
    }
58
59
    public function visit($url): void
60
    {
61
        $this->client->request('GET', $this->prepareUrl($url));
62
    }
63
64
    public function getCurrentUrl(): string
65
    {
66
        return $this->client->getCurrentURL();
67
    }
68
69
    public function getContent(): string
70
    {
71
        return $this->client->getWebDriver()->getPageSource();
72
    }
73
74
    public function getText($xpath): string
75
    {
76
        $crawler = $this->filteredCrawler($xpath);
77
78
        if (($element = $crawler->getElement(0)) && 'title' === $element->getTagName()) {
79
            // hack to get the text of the title html element
80
            // for this element, WebDriverElement::getText() returns an empty string
81
            // the only way to get the value is to get title from the client
82
            return $this->client->getTitle();
83
        }
84
85
        return \trim($crawler->text(null, true));
86
    }
87
88
    public function getValue($xpath)
89
    {
90
        try {
91
            $formField = $this->formField($xpath);
92
            $value = $formField->getValue();
93
94
            if ('' === $value && $formField instanceof ChoiceFormField) {
95
                $value = null;
96
            }
97
        } catch (DriverException $e) {
98
            // e.g. element is an option
99
            $element = $this->crawlerElement($this->filteredCrawler($xpath));
100
            $value = $element->getAttribute('value');
101
        }
102
103
        return $value;
104
    }
105
106
    public function setValue($xpath, $value): void
107
    {
108
        $element = $this->crawlerElement($this->filteredCrawler($xpath));
109
        $jsNode = $this->jsNode($xpath);
110
111
        if ('input' === $element->getTagName() && \in_array($element->getAttribute('type'), ['date', 'time', 'color'])) {
112
            $this->executeScript(\sprintf('%s.value = \'%s\'', $jsNode, $value));
113
        } else {
114
            try {
115
                $formField = $this->formField($xpath);
116
                $formField->setValue($value);
117
            } catch (DriverException $e) {
118
                // e.g. element is on option
119
                $element->sendKeys($value);
120
            }
121
        }
122
123
        // Remove the focus from the element if the field still has focus in
124
        // order to trigger the change event. By doing this instead of simply
125
        // triggering the change event for the given xpath we ensure that the
126
        // change event will not be triggered twice for the same element if it
127
        // has lost focus in the meanwhile. If the element has lost focus
128
        // already then there is nothing to do as this will already have caused
129
        // the triggering of the change event for that element.
130
        if ($this->evaluateScript(\sprintf('document.activeElement === %s', $jsNode))) {
131
            $this->executeScript('document.activeElement.blur();');
132
        }
133
    }
134
135
    public function getTagName($xpath): string
136
    {
137
        return $this->crawlerElement($this->filteredCrawler($xpath))->getTagName();
138
    }
139
140
    public function check($xpath): void
141
    {
142
        $this->choiceFormField($xpath)->tick();
143
    }
144
145
    public function uncheck($xpath): void
146
    {
147
        $this->choiceFormField($xpath)->untick();
148
    }
149
150
    public function selectOption($xpath, $value, $multiple = false): void
151
    {
152
        try {
153
            $this->choiceFormField($xpath)->select($value);
154
155
            return;
156
        } catch (NoSuchElementException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
157
        }
158
159
        // try selecting by visible text
160
        $select = new WebDriverSelect($this->crawlerElement($this->filteredCrawler($xpath)));
161
        $select->selectByVisibleText($value);
162
    }
163
164
    /**
165
     * @param string|string[] $path
166
     */
167
    public function attachFile($xpath, $path): void
168
    {
169
        if (\is_array($path) && empty($this->filteredCrawler($xpath)->attr('multiple'))) {
170
            throw new \InvalidArgumentException('Cannot attach multiple files to a non-multiple file field.');
171
        }
172
173
        foreach ((array) $path as $file) {
174
            $this->fileFormField($xpath)->upload($file);
175
        }
176
    }
177
178
    public function isChecked($xpath): bool
179
    {
180
        $element = $this->crawlerElement($this->filteredCrawler($xpath));
181
182
        if ('radio' === \mb_strtolower((string) $element->getAttribute('type'))) {
183
            return null !== $element->getAttribute('checked');
184
        }
185
186
        return $this->choiceFormField($xpath)->hasValue();
187
    }
188
189
    public function click($xpath): void
190
    {
191
        $this->client->getMouse()->click($this->toCoordinates($xpath));
192
        $this->client->refreshCrawler();
193
    }
194
195
    public function executeScript($script): void
196
    {
197
        if (\preg_match('/^function[\s(]/', $script)) {
198
            $script = \preg_replace('/;$/', '', $script);
199
            $script = '('.$script.')';
200
        }
201
202
        $this->client->executeScript($script);
203
    }
204
205
    public function evaluateScript($script)
206
    {
207
        if (0 !== \mb_strpos(\trim($script), 'return ')) {
208
            $script = 'return '.$script;
209
        }
210
211
        return $this->client->executeScript($script);
212
    }
213
214
    public function getHtml($xpath): string
215
    {
216
        // cut the tag itself (making innerHTML out of outerHTML)
217
        return (string) \preg_replace('/^<[^>]+>|<[^>]+>$/', '', $this->getOuterHtml($xpath));
218
    }
219
220
    public function isVisible($xpath): bool
221
    {
222
        return $this->crawlerElement($this->filteredCrawler($xpath))->isDisplayed();
223
    }
224
225
    public function getOuterHtml($xpath): string
226
    {
227
        return $this->filteredCrawler($xpath)->html();
228
    }
229
230
    public function getAttribute($xpath, $name): ?string
231
    {
232
        return $this->crawlerElement($this->filteredCrawler($xpath))->getAttribute($name);
233
    }
234
235
    protected function findElementXpaths($xpath): array
236
    {
237
        $nodes = $this->crawler()->filterXPath($xpath);
238
239
        $elements = [];
240
241
        foreach ($nodes as $i => $node) {
242
            $elements[] = \sprintf('(%s)[%d]', $xpath, $i + 1);
243
        }
244
245
        return $elements;
246
    }
247
248
    private function crawlerElement(Crawler $crawler): WebDriverElement
249
    {
250
        if (null !== $node = $crawler->getElement(0)) {
251
            return $node;
252
        }
253
254
        throw new DriverException('The element does not exist');
255
    }
256
257
    private function prepareUrl(string $url): string
258
    {
259
        return (string) \preg_replace('#(https?://[^/]+)(/[^/.]+\.php)?#', '$1$2', $url);
260
    }
261
262
    private function filteredCrawler(string $xpath): Crawler
263
    {
264
        if (!\count($crawler = $this->crawler()->filterXPath($xpath))) {
265
            throw new DriverException(\sprintf('There is no element matching XPath "%s"', $xpath));
266
        }
267
268
        return $crawler;
269
    }
270
271
    private function crawler(): Crawler
272
    {
273
        try {
274
            return $this->client->getCrawler();
275
        } catch (BadMethodCallException $e) {
276
            throw new DriverException('Unable to access the response content before visiting a page', 0, $e);
277
        }
278
    }
279
280
    private function formField(string $xpath): FormField
281
    {
282
        try {
283
            return $this->choiceFormField($xpath);
284
        } catch (DriverException $e) {
285
            try {
286
                return $this->inputFormField($xpath);
287
            } catch (DriverException $e) {
288
                try {
289
                    return $this->fileFormField($xpath);
290
                } catch (DriverException $e) {
291
                    return $this->textareaFormField($xpath);
292
                }
293
            }
294
        }
295
    }
296
297
    private function choiceFormField(string $xpath): ChoiceFormField
298
    {
299
        $element = $this->crawlerElement($this->filteredCrawler($xpath));
300
301
        try {
302
            return new ChoiceFormField($element);
303
        } catch (\LogicException $e) {
304
            throw new DriverException(\sprintf('Impossible to get the element with XPath "%s" as it is not a choice form field. %s', $xpath, $e->getMessage()));
305
        }
306
    }
307
308
    private function inputFormField(string $xpath): InputFormField
309
    {
310
        $element = $this->crawlerElement($this->filteredCrawler($xpath));
311
312
        try {
313
            return new InputFormField($element);
314
        } catch (\LogicException $e) {
315
            throw new DriverException(\sprintf('Impossible to check the element with XPath "%s" as it is not an input form field.', $xpath));
316
        }
317
    }
318
319
    private function fileFormField(string $xpath): FileFormField
320
    {
321
        $element = $this->crawlerElement($this->filteredCrawler($xpath));
322
323
        try {
324
            return new FileFormField($element);
325
        } catch (\LogicException $e) {
326
            throw new DriverException(\sprintf('Impossible to check the element with XPath "%s" as it is not a file form field.', $xpath));
327
        }
328
    }
329
330
    private function textareaFormField(string $xpath): TextareaFormField
331
    {
332
        $element = $this->crawlerElement($this->filteredCrawler($xpath));
333
334
        try {
335
            return new TextareaFormField($element);
336
        } catch (\LogicException $e) {
337
            throw new DriverException(\sprintf('Impossible to check the element with XPath "%s" as it is not a textarea.', $xpath));
338
        }
339
    }
340
341
    private function toCoordinates(string $xpath): WebDriverCoordinates
342
    {
343
        $element = $this->crawlerElement($this->filteredCrawler($xpath));
344
345
        if (!$element instanceof WebDriverLocatable) {
346
            throw new \RuntimeException(\sprintf('The element of "%s" xpath selector does not implement "%s".', $xpath, WebDriverLocatable::class));
347
        }
348
349
        return $element->getCoordinates();
350
    }
351
352
    private function jsNode(string $xpath): string
353
    {
354
        return "document.evaluate(`{$xpath}`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue";
355
    }
356
}
357