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

Browser   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 456
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 109
dl 0
loc 456
rs 7.92
c 1
b 0
f 0
wmc 51

41 Methods

Rating   Name   Duplication   Size   Complexity  
A fillField() 0 5 1
A saveSource() 0 9 2
A dump() 0 5 1
A follow() 0 5 1
A dd() 0 4 1
A uncheckField() 0 5 1
A checkField() 0 13 3
A selectField() 0 11 3
A selectFieldOption() 0 5 1
A selectFieldOptions() 0 7 2
A dumpCurrentState() 0 3 1
A response() 0 3 1
A savedArtifacts() 0 3 1
A minkSession() 0 3 1
A assertSee() 0 5 1
A assertNotSeeElement() 0 5 1
A assertNotSelected() 0 5 1
A assertFieldEquals() 0 5 1
A assertElementAttributeNotContains() 0 5 1
A use() 0 14 1
A assertNotSeeIn() 0 5 1
A assertNotContains() 0 7 1
A assertNotSee() 0 5 1
A assertNotOn() 0 5 1
A documentElement() 0 3 1
A assertNotChecked() 0 5 1
A visit() 0 5 1
A __construct() 0 3 1
A assertElementAttributeContains() 0 5 1
A assertOn() 0 5 1
A assertSeeElement() 0 5 1
A setSourceDir() 0 5 1
A die() 0 3 1
A assertElementCount() 0 5 1
A assertChecked() 0 5 1
A click() 0 14 3
A assertContains() 0 5 1
A attachFile() 0 11 3
A assertSelected() 0 5 1
A assertFieldNotEquals() 0 5 1
A assertSeeIn() 0 5 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Zenstruck;
4
5
use Behat\Mink\Driver\DriverInterface;
6
use Behat\Mink\Element\DocumentElement;
7
use Behat\Mink\Exception\ElementNotFoundException;
8
use Behat\Mink\Mink;
9
use Behat\Mink\Session;
10
use Symfony\Component\DomCrawler\Crawler;
11
use Symfony\Component\Filesystem\Filesystem;
12
use Zenstruck\Browser\Assertion\SameUrlAssertion;
13
use Zenstruck\Browser\Component;
14
use Zenstruck\Browser\Dom;
15
use Zenstruck\Browser\Response;
16
use Zenstruck\Callback\Parameter;
17
18
/**
19
 * @author Kevin Bond <[email protected]>
20
 */
21
class Browser
22
{
23
    private const SESSION = 'app';
24
25
    private Mink $mink;
26
    private ?string $sourceDir = null;
27
    private array $savedSources = [];
28
29
    /**
30
     * @internal
31
     */
32
    public function __construct(DriverInterface $driver)
33
    {
34
        $this->mink = new Mink([self::SESSION => new Session($driver)]);
35
    }
36
37
    /**
38
     * @return static
39
     */
40
    final public function setSourceDir(string $dir): self
41
    {
42
        $this->sourceDir = $dir;
43
44
        return $this;
45
    }
46
47
    /**
48
     * @return static
49
     */
50
    final public function visit(string $uri): self
51
    {
52
        $this->minkSession()->visit($uri);
53
54
        return $this;
55
    }
56
57
    /**
58
     * @param array $parts The url parts to check {@see parse_url} (use empty array for "all")
59
     *
60
     * @return static
61
     */
62
    final public function assertOn(string $expected, array $parts = ['path', 'query', 'fragment']): self
63
    {
64
        Assert::run(new SameUrlAssertion($this->minkSession()->getCurrentUrl(), $expected, $parts));
65
66
        return $this;
67
    }
68
69
    /**
70
     * @param array $parts The url parts to check (@see parse_url)
71
     *
72
     * @return static
73
     */
74
    final public function assertNotOn(string $expected, array $parts = ['path', 'query', 'fragment']): self
75
    {
76
        Assert::not(new SameUrlAssertion($this->minkSession()->getCurrentUrl(), $expected, $parts));
77
78
        return $this;
79
    }
80
81
    /**
82
     * @return static
83
     */
84
    final public function assertContains(string $expected): self
85
    {
86
        Assert::that($this->response()->body())->contains($expected, 'Expected response body to contain "{needle}".');
87
88
        return $this;
89
    }
90
91
    /**
92
     * @return static
93
     */
94
    final public function assertNotContains(string $expected): self
95
    {
96
        Assert::that($this->response()->body())
97
            ->doesNotContain($expected, 'Expected response body to not contain "{needle}".')
98
        ;
99
100
        return $this;
101
    }
102
103
    /**
104
     * @return static
105
     */
106
    final public function use(callable $callback): self
107
    {
108
        Callback::createFor($callback)->invokeAll(
109
            Parameter::union(
110
                Parameter::untyped($this),
111
                Parameter::typed(self::class, $this),
112
                Parameter::typed(Component::class, Parameter::factory(fn(string $class) => new $class($this))),
113
                Parameter::typed(Response::class, Parameter::factory(fn() => $this->response())),
114
                Parameter::typed(Crawler::class, Parameter::factory(fn() => $this->response()->ensureDom()->crawler())),
115
                Parameter::typed(Dom::class, Parameter::factory(fn() => $this->response()->ensureDom()->dom()))
116
            )
117
        );
118
119
        return $this;
120
    }
121
122
    /**
123
     * @return static
124
     */
125
    final public function saveSource(string $filename): self
126
    {
127
        if ($this->sourceDir) {
128
            $filename = \sprintf('%s/%s', \rtrim($this->sourceDir, '/'), \ltrim($filename, '/'));
129
        }
130
131
        (new Filesystem())->dumpFile($this->savedSources[] = $filename, $this->response()->raw());
132
133
        return $this;
134
    }
135
136
    /**
137
     * @return static
138
     */
139
    final public function dump(?string $selector = null): self
140
    {
141
        $this->response()->dump($selector);
142
143
        return $this;
144
    }
145
146
    final public function dd(?string $selector = null): void
147
    {
148
        $this->dump($selector);
149
        $this->die();
150
    }
151
152
    /**
153
     * @return static
154
     */
155
    public function follow(string $link): self
156
    {
157
        $this->documentElement()->clickLink($link);
158
159
        return $this;
160
    }
161
162
    /**
163
     * @return static
164
     */
165
    final public function fillField(string $selector, string $value): self
166
    {
167
        $this->documentElement()->fillField($selector, $value);
168
169
        return $this;
170
    }
171
172
    /**
173
     * @return static
174
     */
175
    final public function checkField(string $selector): self
176
    {
177
        $field = $this->documentElement()->findField($selector);
178
179
        if ($field && 'radio' === \mb_strtolower($field->getAttribute('type'))) {
0 ignored issues
show
Bug introduced by
It seems like $field->getAttribute('type') can also be of type null; however, parameter $string of mb_strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

179
        if ($field && 'radio' === \mb_strtolower(/** @scrutinizer ignore-type */ $field->getAttribute('type'))) {
Loading history...
180
            $this->documentElement()->selectFieldOption($selector, $field->getAttribute('value'));
181
182
            return $this;
183
        }
184
185
        $this->documentElement()->checkField($selector);
186
187
        return $this;
188
    }
189
190
    /**
191
     * @return static
192
     */
193
    final public function uncheckField(string $selector): self
194
    {
195
        $this->documentElement()->uncheckField($selector);
196
197
        return $this;
198
    }
199
200
    /**
201
     * Select Radio, check checkbox, select single/multiple values.
202
     *
203
     * @param string|array|null $value null: check radio/checkbox
204
     *                                 string: single value
205
     *                                 array: multiple values
206
     *
207
     * @return static
208
     */
209
    final public function selectField(string $selector, $value = null): self
210
    {
211
        if (\is_array($value)) {
212
            return $this->selectFieldOptions($selector, $value);
213
        }
214
215
        if (\is_string($value)) {
216
            return $this->selectFieldOption($selector, $value);
217
        }
218
219
        return $this->checkField($selector);
220
    }
221
222
    /**
223
     * @return static
224
     */
225
    final public function selectFieldOption(string $selector, string $value): self
226
    {
227
        $this->documentElement()->selectFieldOption($selector, $value);
228
229
        return $this;
230
    }
231
232
    /**
233
     * @return static
234
     */
235
    final public function selectFieldOptions(string $selector, array $values): self
236
    {
237
        foreach ($values as $value) {
238
            $this->documentElement()->selectFieldOption($selector, $value, true);
239
        }
240
241
        return $this;
242
    }
243
244
    /**
245
     * @param string[]|string $filename string: single file
246
     *                                  array: multiple files
247
     *
248
     * @return static
249
     */
250
    final public function attachFile(string $selector, $filename): self
251
    {
252
        foreach ((array) $filename as $file) {
253
            if (!\file_exists($file)) {
254
                throw new \InvalidArgumentException(\sprintf('File "%s" does not exist.', $file));
255
            }
256
        }
257
258
        $this->documentElement()->attachFileToField($selector, $filename);
259
260
        return $this;
261
    }
262
263
    /**
264
     * Click on a button, link or any DOM element.
265
     *
266
     * @return static
267
     */
268
    final public function click(string $selector): self
269
    {
270
        try {
271
            $this->documentElement()->pressButton($selector);
272
        } catch (ElementNotFoundException $e) {
273
            // try link
274
            try {
275
                $this->documentElement()->clickLink($selector);
276
            } catch (ElementNotFoundException $e) {
277
                $this->documentElement()->find('css', $selector)->click();
278
            }
279
        }
280
281
        return $this;
282
    }
283
284
    /**
285
     * @return static
286
     */
287
    final public function assertSee(string $expected): self
288
    {
289
        $this->response()->ensureDom()->dom()->assertSee($expected);
290
291
        return $this;
292
    }
293
294
    /**
295
     * @return static
296
     */
297
    final public function assertNotSee(string $expected): self
298
    {
299
        $this->response()->ensureDom()->dom()->assertNotSee($expected);
300
301
        return $this;
302
    }
303
304
    /**
305
     * @return static
306
     */
307
    final public function assertSeeIn(string $selector, string $expected): self
308
    {
309
        $this->response()->ensureDom()->dom()->assertSeeIn($selector, $expected);
310
311
        return $this;
312
    }
313
314
    /**
315
     * @return static
316
     */
317
    final public function assertNotSeeIn(string $selector, string $expected): self
318
    {
319
        $this->response()->ensureDom()->dom()->assertNotSeeIn($selector, $expected);
320
321
        return $this;
322
    }
323
324
    /**
325
     * @return static
326
     */
327
    final public function assertSeeElement(string $selector): self
328
    {
329
        $this->response()->ensureDom()->dom()->assertSeeElement($selector);
330
331
        return $this;
332
    }
333
334
    /**
335
     * @return static
336
     */
337
    final public function assertNotSeeElement(string $selector): self
338
    {
339
        $this->response()->ensureDom()->dom()->assertNotSeeElement($selector);
340
341
        return $this;
342
    }
343
344
    /**
345
     * @return static
346
     */
347
    final public function assertElementCount(string $selector, int $count): self
348
    {
349
        $this->response()->ensureDom()->dom()->assertElementCount($selector, $count);
350
351
        return $this;
352
    }
353
354
    /**
355
     * @return static
356
     */
357
    final public function assertFieldEquals(string $selector, string $expected): self
358
    {
359
        $this->response()->ensureDom()->dom()->assertFieldEquals($selector, $expected);
360
361
        return $this;
362
    }
363
364
    /**
365
     * @return static
366
     */
367
    final public function assertFieldNotEquals(string $selector, string $expected): self
368
    {
369
        $this->response()->ensureDom()->dom()->assertFieldNotEquals($selector, $expected);
370
371
        return $this;
372
    }
373
374
    /**
375
     * @return static
376
     */
377
    final public function assertSelected(string $selector, string $expected): self
378
    {
379
        $this->response()->ensureDom()->dom()->assertSelected($selector, $expected);
380
381
        return $this;
382
    }
383
384
    /**
385
     * @return static
386
     */
387
    final public function assertNotSelected(string $selector, string $expected): self
388
    {
389
        $this->response()->ensureDom()->dom()->assertNotSelected($selector, $expected);
390
391
        return $this;
392
    }
393
394
    /**
395
     * @return static
396
     */
397
    final public function assertChecked(string $selector): self
398
    {
399
        $this->response()->ensureDom()->dom()->assertChecked($selector);
400
401
        return $this;
402
    }
403
404
    /**
405
     * @return static
406
     */
407
    final public function assertNotChecked(string $selector): self
408
    {
409
        $this->response()->ensureDom()->dom()->assertNotChecked($selector);
410
411
        return $this;
412
    }
413
414
    /**
415
     * @return static
416
     */
417
    final public function assertElementAttributeContains(string $selector, string $attribute, string $expected): self
418
    {
419
        $this->response()->ensureDom()->dom()->assertElementAttributeContains($selector, $attribute, $expected);
420
421
        return $this;
422
    }
423
424
    /**
425
     * @return static
426
     */
427
    final public function assertElementAttributeNotContains(string $selector, string $attribute, string $expected): self
428
    {
429
        $this->response()->ensureDom()->dom()->assertElementAttributeNotContains($selector, $attribute, $expected);
430
431
        return $this;
432
    }
433
434
    /**
435
     * @internal
436
     */
437
    public function dumpCurrentState(string $filename): void
438
    {
439
        $this->saveSource("{$filename}.txt");
440
    }
441
442
    /**
443
     * @internal
444
     */
445
    public function savedArtifacts(): array
446
    {
447
        return ['Saved Source Files' => $this->savedSources];
448
    }
449
450
    public function response(): Response
451
    {
452
        return Response::createFor($this->minkSession());
453
    }
454
455
    /**
456
     * @internal
457
     */
458
    final protected function minkSession(): Session
459
    {
460
        return $this->mink->getSession(self::SESSION);
461
    }
462
463
    /**
464
     * @internal
465
     */
466
    final protected function documentElement(): DocumentElement
467
    {
468
        return $this->minkSession()->getPage();
469
    }
470
471
    /**
472
     * @internal
473
     */
474
    protected function die(): void
475
    {
476
        exit(1);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
477
    }
478
}
479