Passed
Push — 1.x ( de413f...885d6a )
by Kevin
01:48
created

PantherDriver::jsNode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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