Passed
Pull Request — 1.x (#62)
by Kevin
02:06
created

Dom::selectField()   B

Complexity

Conditions 7
Paths 24

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 14
nc 24
nop 1
dl 0
loc 31
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
namespace Zenstruck\Browser;
4
5
use Symfony\Component\DomCrawler\Crawler;
6
use Symfony\Component\DomCrawler\Form;
7
use Zenstruck\Assert;
8
use Zenstruck\Browser\Dom\Assertion\FieldCheckedAssertion;
9
use Zenstruck\Browser\Dom\Assertion\FieldSelectedAssertion;
10
use Zenstruck\Browser\Dom\Form\Field;
11
12
/**
13
 * @mixin Crawler
14
 *
15
 * @author Kevin Bond <[email protected]>
16
 */
17
final class Dom implements \IteratorAggregate, \Countable
18
{
19
    private Crawler $crawler;
20
21
    private function __construct(Crawler $crawler)
22
    {
23
        $this->crawler = $crawler;
24
    }
25
26
    public function __call(string $name, array $arguments)
27
    {
28
        if (!\method_exists($this->crawler, $name)) {
29
            throw new \BadMethodCallException(\sprintf('Method "%s" does not exist on "%s".', $name, \get_class($this->crawler)));
30
        }
31
32
        $ret = $this->crawler->{$name}(...$arguments);
33
34
        return $ret instanceof Crawler ? new self($ret) : $ret;
35
    }
36
37
    /**
38
     * @param Crawler|self $crawler
39
     */
40
    public static function wrap($crawler): self
41
    {
42
        return $crawler instanceof self ? $crawler : new self($crawler);
43
    }
44
45
    public static function fromString(string $value): self
46
    {
47
        return self::wrap(new Crawler($value));
48
    }
49
50
    public function crawler(): Crawler
51
    {
52
        return $this->crawler;
53
    }
54
55
    /**
56
     * Override to normalize/remove deprecation in 4.4.
57
     */
58
    public function text(?string $default = null, bool $normalizeWhitespace = true): string
59
    {
60
        return $this->crawler->text($default, $normalizeWhitespace);
61
    }
62
63
    /**
64
     * Similar to {@see Crawler::getIterator()} but iterate over the nodes
65
     * as {@see self} instead of {@see \DOMNode}.
66
     *
67
     * @return \Traversable|self[]
68
     */
69
    public function getIterator(): \Traversable
70
    {
71
        return new \ArrayIterator($this->each(fn(Crawler $c) => new self($c)));
0 ignored issues
show
Bug introduced by
The method each() does not exist on Zenstruck\Browser\Dom. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

71
        return new \ArrayIterator($this->/** @scrutinizer ignore-call */ each(fn(Crawler $c) => new self($c)));
Loading history...
72
    }
73
74
    public function count(): int
75
    {
76
        return $this->crawler->count();
77
    }
78
79
    /**
80
     * @param string $selector XPath or CSS expression
81
     */
82
    public function find(string $selector): self
83
    {
84
        try {
85
            return $this->filter($selector);
0 ignored issues
show
Bug introduced by
The method filter() does not exist on Zenstruck\Browser\Dom. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

85
            return $this->/** @scrutinizer ignore-call */ filter($selector);
Loading history...
86
        } catch (\Exception $e) {
87
            // could not covert selector to xpath, try as xpath directly
88
            try {
89
                \set_error_handler(static function($errno, $errstr, $errfile, $errline) {
90
                    throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
91
                });
92
93
                return $this->filterXPath($selector);
0 ignored issues
show
Bug introduced by
The method filterXPath() does not exist on Zenstruck\Browser\Dom. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

93
                return $this->/** @scrutinizer ignore-call */ filterXPath($selector);
Loading history...
94
            } finally {
95
                \restore_error_handler();
96
            }
97
        }
98
    }
99
100
    /**
101
     * Similar to {@see BaseCrawler::form()} but with a few differences:
102
     * 1. If not currently on a form node, attempt to find the closest.
103
     * 2. todo Cache the form object so it can be manipulated by the form manipulation methods.
104
     */
105
    public function form(?array $values = null, ?string $method = null): Form
106
    {
107
        if (!$element = $this->closest('form')) {
0 ignored issues
show
Bug introduced by
The method closest() does not exist on Zenstruck\Browser\Dom. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

107
        if (!$element = $this->/** @scrutinizer ignore-call */ closest('form')) {
Loading history...
108
            throw new \InvalidArgumentException('Unable to find form in DOM tree.');
109
        }
110
111
        return $element->crawler->form($values, $method);
112
    }
113
114
    public function field(): Field
115
    {
116
        return Field::create($this);
117
    }
118
119
    /**
120
     * @param string $selector css, xpath, name, id, or label text
121
     */
122
    public function findFormField(string $selector): ?Field
123
    {
124
        try {
125
            return $this->selectField($selector)->field();
126
        } catch (\InvalidArgumentException $e) {
127
            return null;
128
        }
129
    }
130
131
    /**
132
     * @see findFormField()
133
     *
134
     * @throws \InvalidArgumentException If not found
135
     */
136
    public function getFormField(string $selector): Field
137
    {
138
        if (!$field = $this->findFormField($selector)) {
139
            throw new \InvalidArgumentException("Form field matching \"{$selector}\" not found.");
140
        }
141
142
        return $field;
143
    }
144
145
    /**
146
     * @param string $selector css, xpath, name, id, or label text
147
     */
148
    public function selectField(string $selector): self
149
    {
150
        // try css/xpath
151
        try {
152
            $element = $this->find($selector);
153
        } catch (\Exception $e) {
154
            $element = [];
155
        }
156
157
        if (!\count($element)) {
158
            // try by name
159
            $element = $this->filter("form [name=\"{$selector}\"]");
160
        }
161
162
        if (!\count($element)) {
163
            // try by id
164
            $element = $this->filterXPath(".//*[@id=\"{$selector}\"]");
165
        }
166
167
        if (!\count($element) && \count($label = $this->filterXPath(".//label[contains(normalize-space(),\"{$selector}\")]"))) {
168
            // try by label text
169
            if ($attr = $label->attr('for')) {
0 ignored issues
show
Bug introduced by
The method attr() does not exist on Zenstruck\Browser\Dom. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

169
            if ($attr = $label->/** @scrutinizer ignore-call */ attr('for')) {
Loading history...
170
                // <label for="input-id">
171
                $element = $this->filter("#{$attr}");
172
            } else {
173
                // <label><input/></label>
174
                $element = $label->filter('[name]');
175
            }
176
        }
177
178
        return $element;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $element could return the type array which is incompatible with the type-hinted return Zenstruck\Browser\Dom. Consider adding an additional type-check to rule them out.
Loading history...
179
    }
180
181
    public function assertSee(string $expected): self
182
    {
183
        Assert::that($this->text())->contains($expected, 'Expected to see text "{needle}".');
184
185
        return $this;
186
    }
187
188
    public function assertNotSee(string $expected): self
189
    {
190
        Assert::that($this->text())->doesNotContain($expected, 'Expected to not see text "{needle}".');
191
192
        return $this;
193
    }
194
195
    public function assertSeeElement(string $selector): self
196
    {
197
        Assert::that($this->find($selector))
198
            ->isNotEmpty('Expected to find element matching "{selector}".', [
199
                'selector' => $selector,
200
                'actual' => $this->html(),
0 ignored issues
show
Bug introduced by
The method html() does not exist on Zenstruck\Browser\Dom. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

200
                'actual' => $this->/** @scrutinizer ignore-call */ html(),
Loading history...
201
            ])
202
        ;
203
204
        return $this;
205
    }
206
207
    public function assertNotSeeElement(string $selector): self
208
    {
209
        Assert::that($this->find($selector))
210
            ->isEmpty(
211
                'Expected to not find element matching "{selector}" but found {count}.',
212
                ['selector' => $selector, 'actual' => $this->html()]
213
            )
214
        ;
215
216
        return $this;
217
    }
218
219
    public function assertSeeIn(string $selector, string $expected): self
220
    {
221
        $this->assertSeeElement($selector);
222
223
        Assert::that($this->find($selector)->text())->contains(
224
            $expected,
225
            'Expected to see text "{needle}" in element matching "{selector}".',
226
            ['selector' => $selector]
227
        );
228
229
        return $this;
230
    }
231
232
    public function assertNotSeeIn(string $selector, string $expected): self
233
    {
234
        $this->assertSeeElement($selector);
235
236
        Assert::that($this->find($selector)->text())->doesNotContain(
237
            $expected,
238
            'Expected to not see text "{needle}" in element matching "{selector}".',
239
            ['selector' => $selector, 'haystack' => $this->html()]
240
        );
241
242
        return $this;
243
    }
244
245
    public function assertElementCount(string $selector, int $expected): self
246
    {
247
        Assert::that($this->find($selector))
248
            ->hasCount($expected, 'Expected to find {expected} element(s) matching "{selector}" but found {actual}.',
249
                ['selector' => $selector, 'haystack' => $this->html()]
250
            )
251
        ;
252
253
        return $this;
254
    }
255
256
    public function assertElementAttributeContains(string $selector, string $attribute, string $expected): self
257
    {
258
        $this->assertElementHasAttribute($selector, $attribute);
259
260
        Assert::that($this->find($selector)->attr($attribute))->contains(
261
            $expected,
262
            'Expected "{attribute}" attribute for element matching "{selector}" to contain "{needle}".',
263
            ['attribute' => $attribute, 'selector' => $selector]
264
        );
265
266
        return $this;
267
    }
268
269
    public function assertElementAttributeNotContains(string $selector, string $attribute, string $expected): self
270
    {
271
        $this->assertElementHasAttribute($selector, $attribute);
272
273
        Assert::that($this->find($selector)->attr($attribute))->doesNotContain(
274
            $expected,
275
            'Expected "{attribute}" attribute for element matching "{selector}" to not contain "{needle}".',
276
            ['attribute' => $attribute, 'selector' => $selector]
277
        );
278
279
        return $this;
280
    }
281
282
    public function assertElementHasAttribute(string $selector, string $attribute): self
283
    {
284
        $this->assertSeeElement($selector);
285
286
        Assert::that($this->find($selector)->attr($attribute))->isNotEmpty(
287
            'Expected element matching "{selector}" to have "{attribute}" attribute.',
288
            ['selector' => $selector, 'attribute' => $attribute]
289
        );
290
291
        return $this;
292
    }
293
294
    public function assertFieldEquals(string $selector, string $expected): self
295
    {
296
        Assert::that(Assert::try(fn() => $this->getFormField($selector))->getValue())
297
            ->equals(
298
                $expected,
299
                'Expected form field matching "{selector}" to equal "{expected}".',
300
                ['selector' => $selector]
301
            )
302
        ;
303
304
        return $this;
305
    }
306
307
    public function assertFieldNotEquals(string $selector, string $expected): self
308
    {
309
        Assert::that(Assert::try(fn() => $this->getFormField($selector))->getValue())
310
            ->isNotEqualTo(
311
                $expected,
312
                'Expected form field matching "{selector}" to not equal "{expected}".',
313
                ['selector' => $selector]
314
            )
315
        ;
316
317
        return $this;
318
    }
319
320
    public function assertFieldContains(string $selector, string $expected): self
321
    {
322
        Assert::that(Assert::try(fn() => $this->getFormField($selector))->getValue())
323
            ->contains(
324
                $expected,
325
                'Expected form field matching "{selector}" to contain "{needle}".',
326
                ['selector' => $selector]
327
            )
328
        ;
329
330
        return $this;
331
    }
332
333
    public function assertFieldNotContains(string $selector, string $expected): self
334
    {
335
        Assert::that(Assert::try(fn() => $this->getFormField($selector))->getValue())
336
            ->doesNotContain(
337
                $expected,
338
                'Expected form field matching "{selector}" to not contain "{needle}".',
339
                ['selector' => $selector]
340
            )
341
        ;
342
343
        return $this;
344
    }
345
346
    public function assertFieldEmpty(string $selector): self
347
    {
348
        Assert::that(Assert::try(fn() => $this->getFormField($selector))->getValue())
349
            ->isEmpty(
350
                'Expected form field matching "{selector}" to be empty.',
351
                ['selector' => $selector]
352
            )
353
        ;
354
355
        return $this;
356
    }
357
358
    public function assertFieldNotEmpty(string $selector): self
359
    {
360
        Assert::that(Assert::try(fn() => $this->getFormField($selector))->getValue())
361
            ->isNotEmpty(
362
                'Expected form field matching "{selector}" to not be empty.',
363
                ['selector' => $selector]
364
            )
365
        ;
366
367
        return $this;
368
    }
369
370
    public function assertChecked(string $selector): self
371
    {
372
        Assert::run(new FieldCheckedAssertion($this, $selector));
373
374
        return $this;
375
    }
376
377
    public function assertNotChecked(string $selector): self
378
    {
379
        Assert::not(new FieldCheckedAssertion($this, $selector));
380
381
        return $this;
382
    }
383
384
    public function assertSelected(string $selector, ?string $expected = null): self
385
    {
386
        if (null === $expected) {
387
            return $this->assertChecked($selector);
388
        }
389
390
        Assert::run(new FieldSelectedAssertion($this, $selector, $expected));
391
392
        return $this;
393
    }
394
395
    public function assertNotSelected(string $selector, ?string $expected = null): self
396
    {
397
        if (null === $expected) {
398
            return $this->assertNotChecked($selector);
399
        }
400
401
        Assert::not(new FieldSelectedAssertion($this, $selector, $expected));
402
403
        return $this;
404
    }
405
}
406