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

Dom   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 379
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 139
dl 0
loc 379
rs 8.5599
c 1
b 0
f 1
wmc 48

33 Methods

Rating   Name   Duplication   Size   Complexity  
B selectField() 0 31 7
A field() 0 3 1
A assertFieldContains() 0 11 1
A assertElementCount() 0 9 1
A assertSee() 0 5 1
A assertElementAttributeContains() 0 11 1
A assertNotSeeIn() 0 11 1
A __call() 0 9 3
A assertNotSeeElement() 0 10 1
A assertNotSee() 0 5 1
A wrap() 0 3 2
A assertElementAttributeNotContains() 0 11 1
A count() 0 3 1
A getIterator() 0 3 1
A assertFieldNotEmpty() 0 10 1
A crawler() 0 3 1
A form() 0 7 2
A assertElementHasAttribute() 0 10 1
A assertFieldEmpty() 0 10 1
A assertFieldNotEquals() 0 11 1
A __construct() 0 3 1
A assertSeeElement() 0 10 1
A getFormField() 0 7 2
A assertSeeIn() 0 11 1
A assertSelected() 0 9 2
A findFormField() 0 6 2
A assertNotSelected() 0 9 2
A fromString() 0 3 1
A assertNotChecked() 0 5 1
A assertFieldNotContains() 0 11 1
A find() 0 14 2
A assertChecked() 0 5 1
A assertFieldEquals() 0 11 1

How to fix   Complexity   

Complex Class

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

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
     * Similar to {@see Crawler::getIterator()} but iterate over the nodes
57
     * as {@see self} instead of {@see \DOMNode}.
58
     *
59
     * @return \Traversable|self[]
60
     */
61
    public function getIterator(): \Traversable
62
    {
63
        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

63
        return new \ArrayIterator($this->/** @scrutinizer ignore-call */ each(fn(Crawler $c) => new self($c)));
Loading history...
64
    }
65
66
    public function count(): int
67
    {
68
        return $this->crawler->count();
69
    }
70
71
    /**
72
     * @param string $selector XPath or CSS expression
73
     */
74
    public function find(string $selector): self
75
    {
76
        try {
77
            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

77
            return $this->/** @scrutinizer ignore-call */ filter($selector);
Loading history...
78
        } catch (\Exception $e) {
79
            // could not covert selector to xpath, try as xpath directly
80
            try {
81
                \set_error_handler(static function($errno, $errstr, $errfile, $errline) {
82
                    throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
83
                });
84
85
                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

85
                return $this->/** @scrutinizer ignore-call */ filterXPath($selector);
Loading history...
86
            } finally {
87
                \restore_error_handler();
88
            }
89
        }
90
    }
91
92
    /**
93
     * Similar to {@see BaseCrawler::form()} but with a few differences:
94
     * 1. If not currently on a form node, attempt to find the closest.
95
     * 2. todo Cache the form object so it can be manipulated by the form manipulation methods.
96
     */
97
    public function form(?array $values = null, ?string $method = null): Form
98
    {
99
        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

99
        if (!$element = $this->/** @scrutinizer ignore-call */ closest('form')) {
Loading history...
100
            throw new \InvalidArgumentException('Unable to find form in DOM tree.');
101
        }
102
103
        return $element->crawler->form($values, $method);
104
    }
105
106
    public function field(): Field
107
    {
108
        return Field::create($this);
109
    }
110
111
    /**
112
     * @param string $selector css, xpath, name, id, or label text
113
     */
114
    public function findFormField(string $selector): ?Field
115
    {
116
        try {
117
            return $this->selectField($selector)->field();
118
        } catch (\InvalidArgumentException $e) {
119
            return null;
120
        }
121
    }
122
123
    /**
124
     * @see findFormField()
125
     *
126
     * @throws \InvalidArgumentException If not found
127
     */
128
    public function getFormField(string $selector): Field
129
    {
130
        if (!$field = $this->findFormField($selector)) {
131
            throw new \InvalidArgumentException("Form field matching \"{$selector}\" not found.");
132
        }
133
134
        return $field;
135
    }
136
137
    /**
138
     * @param string $selector css, xpath, name, id, or label text
139
     */
140
    public function selectField(string $selector): self
141
    {
142
        // try css/xpath
143
        try {
144
            $element = $this->find($selector);
145
        } catch (\Exception $e) {
146
            $element = [];
147
        }
148
149
        if (!\count($element)) {
150
            // try by name
151
            $element = $this->filter("form [name=\"{$selector}\"]");
152
        }
153
154
        if (!\count($element)) {
155
            // try by id
156
            $element = $this->filterXPath(".//*[@id=\"{$selector}\"]");
157
        }
158
159
        if (!\count($element) && \count($label = $this->filterXPath(".//label[contains(normalize-space(),\"{$selector}\")]"))) {
160
            // try by label text
161
            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

161
            if ($attr = $label->/** @scrutinizer ignore-call */ attr('for')) {
Loading history...
162
                // <label for="input-id">
163
                $element = $this->filter("#{$attr}");
164
            } else {
165
                // <label><input/></label>
166
                $element = $label->filter('[name]');
167
            }
168
        }
169
170
        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...
171
    }
172
173
    public function assertSee(string $expected): self
174
    {
175
        Assert::that($this->text())->contains($expected, 'Expected to see text "{needle}".');
0 ignored issues
show
Bug introduced by
The method text() 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

175
        Assert::that($this->/** @scrutinizer ignore-call */ text())->contains($expected, 'Expected to see text "{needle}".');
Loading history...
176
177
        return $this;
178
    }
179
180
    public function assertNotSee(string $expected): self
181
    {
182
        Assert::that($this->text())->doesNotContain($expected, 'Expected to not see text "{needle}".');
183
184
        return $this;
185
    }
186
187
    public function assertSeeElement(string $selector): self
188
    {
189
        Assert::that($this->find($selector))
190
            ->isNotEmpty('Expected to find element matching "{selector}".', [
191
                'selector' => $selector,
192
                '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

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