Browser   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 493
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 120
dl 0
loc 493
rs 5.5199
c 1
b 0
f 0
wmc 56

43 Methods

Rating   Name   Duplication   Size   Complexity  
A saveSource() 0 9 2
A dd() 0 4 1
A assertNotContains() 0 4 1
A assertContains() 0 4 1
A attachFile() 0 11 3
A uncheckField() 0 5 1
A selectField() 0 11 3
A checkField() 0 13 3
A use() 0 13 1
A assertNotOn() 0 5 1
A visit() 0 5 1
A fillField() 0 5 1
A dump() 0 5 1
A assertOn() 0 5 1
A selectFieldOption() 0 5 1
A follow() 0 5 1
A setSourceDir() 0 5 1
A selectFieldOptions() 0 7 2
A assertSee() 0 4 1
A dumpCurrentState() 0 3 1
A assertNotSeeElement() 0 4 1
A assertNotSelected() 0 11 2
A assertFieldEquals() 0 4 1
A assertElementAttributeNotContains() 0 4 1
A assertNotSeeIn() 0 4 1
A assertNotSee() 0 4 1
A documentElement() 0 3 1
A assertNotChecked() 0 4 1
A response() 0 3 1
A wrapMinkExpectation() 0 5 1
A __construct() 0 3 1
A assertElementAttributeContains() 0 4 1
A savedArtifacts() 0 3 1
A assertSeeElement() 0 4 1
A webAssert() 0 3 1
A die() 0 3 1
A minkSession() 0 3 1
A assertElementCount() 0 4 1
A assertChecked() 0 4 1
A click() 0 18 4
A assertSelected() 0 11 2
A assertFieldNotEquals() 0 4 1
A assertSeeIn() 0 4 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\Exception\ExpectationException;
9
use Behat\Mink\Mink;
10
use Behat\Mink\Session;
11
use Behat\Mink\WebAssert;
12
use Symfony\Component\DomCrawler\Crawler;
13
use Symfony\Component\Filesystem\Filesystem;
14
use Zenstruck\Browser\Assertion\MinkAssertion;
15
use Zenstruck\Browser\Assertion\SameUrlAssertion;
16
use Zenstruck\Browser\Component;
17
use Zenstruck\Browser\Response;
18
use Zenstruck\Callback\Parameter;
19
20
/**
21
 * @author Kevin Bond <[email protected]>
22
 */
23
class Browser
24
{
25
    private const SESSION = 'app';
26
27
    private Mink $mink;
28
    private ?string $sourceDir = null;
29
30
    /** @var string[] */
31
    private array $savedSources = [];
32
33
    /**
34
     * @internal
35
     */
36
    public function __construct(DriverInterface $driver)
37
    {
38
        $this->mink = new Mink([self::SESSION => new Session($driver)]);
39
    }
40
41
    /**
42
     * @return static
43
     */
44
    final public function setSourceDir(string $dir): self
45
    {
46
        $this->sourceDir = $dir;
47
48
        return $this;
49
    }
50
51
    /**
52
     * @return static
53
     */
54
    final public function visit(string $uri): self
55
    {
56
        $this->minkSession()->visit($uri);
57
58
        return $this;
59
    }
60
61
    /**
62
     * @param array $parts The url parts to check {@see parse_url} (use empty array for "all")
63
     *
64
     * @return static
65
     */
66
    final public function assertOn(string $expected, array $parts = ['path', 'query', 'fragment']): self
67
    {
68
        Assert::run(new SameUrlAssertion($this->minkSession()->getCurrentUrl(), $expected, $parts));
69
70
        return $this;
71
    }
72
73
    /**
74
     * @param array $parts The url parts to check (@see parse_url)
75
     *
76
     * @return static
77
     */
78
    final public function assertNotOn(string $expected, array $parts = ['path', 'query', 'fragment']): self
79
    {
80
        Assert::not(new SameUrlAssertion($this->minkSession()->getCurrentUrl(), $expected, $parts));
81
82
        return $this;
83
    }
84
85
    /**
86
     * @return static
87
     */
88
    final public function assertContains(string $expected): self
89
    {
90
        return $this->wrapMinkExpectation(
91
            fn() => $this->webAssert()->responseContains($expected)
92
        );
93
    }
94
95
    /**
96
     * @return static
97
     */
98
    final public function assertNotContains(string $expected): self
99
    {
100
        return $this->wrapMinkExpectation(
101
            fn() => $this->webAssert()->responseNotContains($expected)
102
        );
103
    }
104
105
    /**
106
     * @return static
107
     */
108
    final public function use(callable $callback): self
109
    {
110
        Callback::createFor($callback)->invokeAll(
111
            Parameter::union(
112
                Parameter::untyped($this),
113
                Parameter::typed(self::class, $this),
114
                Parameter::typed(Component::class, Parameter::factory(fn(string $class) => new $class($this))),
115
                Parameter::typed(Response::class, Parameter::factory(fn() => $this->response())),
116
                Parameter::typed(Crawler::class, Parameter::factory(fn() => $this->response()->assertDom()->crawler()))
117
            )
118
        );
119
120
        return $this;
121
    }
122
123
    /**
124
     * @return static
125
     */
126
    final public function saveSource(string $filename): self
127
    {
128
        if ($this->sourceDir) {
129
            $filename = \sprintf('%s/%s', \rtrim($this->sourceDir, '/'), \ltrim($filename, '/'));
130
        }
131
132
        (new Filesystem())->dumpFile($this->savedSources[] = $filename, $this->response()->raw());
133
134
        return $this;
135
    }
136
137
    /**
138
     * @return static
139
     */
140
    final public function dump(?string $selector = null): self
141
    {
142
        $this->response()->dump($selector);
143
144
        return $this;
145
    }
146
147
    final public function dd(?string $selector = null): void
148
    {
149
        $this->dump($selector);
150
        $this->die();
151
    }
152
153
    /**
154
     * @return static
155
     */
156
    public function follow(string $link): self
157
    {
158
        $this->documentElement()->clickLink($link);
159
160
        return $this;
161
    }
162
163
    /**
164
     * @return static
165
     */
166
    final public function fillField(string $selector, string $value): self
167
    {
168
        $this->documentElement()->fillField($selector, $value);
169
170
        return $this;
171
    }
172
173
    /**
174
     * @return static
175
     */
176
    final public function checkField(string $selector): self
177
    {
178
        $field = $this->documentElement()->findField($selector);
179
180
        if ($field && 'radio' === \mb_strtolower((string) $field->getAttribute('type'))) {
181
            $this->documentElement()->selectFieldOption($selector, (string) $field->getAttribute('value'));
182
183
            return $this;
184
        }
185
186
        $this->documentElement()->checkField($selector);
187
188
        return $this;
189
    }
190
191
    /**
192
     * @return static
193
     */
194
    final public function uncheckField(string $selector): self
195
    {
196
        $this->documentElement()->uncheckField($selector);
197
198
        return $this;
199
    }
200
201
    /**
202
     * Select Radio, check checkbox, select single/multiple values.
203
     *
204
     * @param string|array|null $value null: check radio/checkbox
205
     *                                 string: single value
206
     *                                 array: multiple values
207
     *
208
     * @return static
209
     */
210
    final public function selectField(string $selector, $value = null): self
211
    {
212
        if (\is_array($value)) {
213
            return $this->selectFieldOptions($selector, $value);
214
        }
215
216
        if (\is_string($value)) {
217
            return $this->selectFieldOption($selector, $value);
218
        }
219
220
        return $this->checkField($selector);
221
    }
222
223
    /**
224
     * @return static
225
     */
226
    final public function selectFieldOption(string $selector, string $value): self
227
    {
228
        $this->documentElement()->selectFieldOption($selector, $value);
229
230
        return $this;
231
    }
232
233
    /**
234
     * @return static
235
     */
236
    final public function selectFieldOptions(string $selector, array $values): self
237
    {
238
        foreach ($values as $value) {
239
            $this->documentElement()->selectFieldOption($selector, $value, true);
240
        }
241
242
        return $this;
243
    }
244
245
    /**
246
     * @param string[]|string $filename string: single file
247
     *                                  array: multiple files
248
     *
249
     * @return static
250
     */
251
    final public function attachFile(string $selector, $filename): self
252
    {
253
        foreach ((array) $filename as $file) {
254
            if (!\file_exists($file)) {
255
                throw new \InvalidArgumentException(\sprintf('File "%s" does not exist.', $file));
256
            }
257
        }
258
259
        $this->documentElement()->attachFileToField($selector, $filename);
260
261
        return $this;
262
    }
263
264
    /**
265
     * Click on a button, link or any DOM element.
266
     *
267
     * @return static
268
     */
269
    final public function click(string $selector): self
270
    {
271
        try {
272
            $this->documentElement()->pressButton($selector);
273
        } catch (ElementNotFoundException $e) {
274
            // try link
275
            try {
276
                $this->documentElement()->clickLink($selector);
277
            } catch (ElementNotFoundException $e) {
278
                if (!$element = $this->documentElement()->find('css', $selector)) {
279
                    throw $e;
280
                }
281
282
                $element->click();
283
            }
284
        }
285
286
        return $this;
287
    }
288
289
    /**
290
     * @return static
291
     */
292
    final public function assertSee(string $expected): self
293
    {
294
        return $this->wrapMinkExpectation(
295
            fn() => $this->webAssert()->pageTextContains($expected)
296
        );
297
    }
298
299
    /**
300
     * @return static
301
     */
302
    final public function assertNotSee(string $expected): self
303
    {
304
        return $this->wrapMinkExpectation(
305
            fn() => $this->webAssert()->pageTextNotContains($expected)
306
        );
307
    }
308
309
    /**
310
     * @return static
311
     */
312
    final public function assertSeeIn(string $selector, string $expected): self
313
    {
314
        return $this->wrapMinkExpectation(
315
            fn() => $this->webAssert()->elementTextContains('css', $selector, $expected)
316
        );
317
    }
318
319
    /**
320
     * @return static
321
     */
322
    final public function assertNotSeeIn(string $selector, string $expected): self
323
    {
324
        return $this->wrapMinkExpectation(
325
            fn() => $this->webAssert()->elementTextNotContains('css', $selector, $expected)
326
        );
327
    }
328
329
    /**
330
     * @return static
331
     */
332
    final public function assertSeeElement(string $selector): self
333
    {
334
        return $this->wrapMinkExpectation(
335
            fn() => $this->webAssert()->elementExists('css', $selector)
336
        );
337
    }
338
339
    /**
340
     * @return static
341
     */
342
    final public function assertNotSeeElement(string $selector): self
343
    {
344
        return $this->wrapMinkExpectation(
345
            fn() => $this->webAssert()->elementNotExists('css', $selector)
346
        );
347
    }
348
349
    /**
350
     * @return static
351
     */
352
    final public function assertElementCount(string $selector, int $count): self
353
    {
354
        return $this->wrapMinkExpectation(
355
            fn() => $this->webAssert()->elementsCount('css', $selector, $count)
356
        );
357
    }
358
359
    /**
360
     * @return static
361
     */
362
    final public function assertFieldEquals(string $selector, string $expected): self
363
    {
364
        return $this->wrapMinkExpectation(
365
            fn() => $this->webAssert()->fieldValueEquals($selector, $expected)
366
        );
367
    }
368
369
    /**
370
     * @return static
371
     */
372
    final public function assertFieldNotEquals(string $selector, string $expected): self
373
    {
374
        return $this->wrapMinkExpectation(
375
            fn() => $this->webAssert()->fieldValueNotEquals($selector, $expected)
376
        );
377
    }
378
379
    /**
380
     * @return static
381
     */
382
    final public function assertSelected(string $selector, string $expected): self
383
    {
384
        try {
385
            $field = $this->webAssert()->fieldExists($selector);
386
        } catch (ExpectationException $e) {
387
            Assert::fail($e->getMessage());
388
        }
389
390
        Assert::that((array) $field->getValue())->contains($expected);
391
392
        return $this;
393
    }
394
395
    /**
396
     * @return static
397
     */
398
    final public function assertNotSelected(string $selector, string $expected): self
399
    {
400
        try {
401
            $field = $this->webAssert()->fieldExists($selector);
402
        } catch (ExpectationException $e) {
403
            Assert::fail($e->getMessage());
404
        }
405
406
        Assert::that((array) $field->getValue())->doesNotContain($expected);
407
408
        return $this;
409
    }
410
411
    /**
412
     * @return static
413
     */
414
    final public function assertChecked(string $selector): self
415
    {
416
        return $this->wrapMinkExpectation(
417
            fn() => $this->webAssert()->checkboxChecked($selector)
418
        );
419
    }
420
421
    /**
422
     * @return static
423
     */
424
    final public function assertNotChecked(string $selector): self
425
    {
426
        return $this->wrapMinkExpectation(
427
            fn() => $this->webAssert()->checkboxNotChecked($selector)
428
        );
429
    }
430
431
    /**
432
     * @return static
433
     */
434
    final public function assertElementAttributeContains(string $selector, string $attribute, string $expected): self
435
    {
436
        return $this->wrapMinkExpectation(
437
            fn() => $this->webAssert()->elementAttributeContains('css', $selector, $attribute, $expected)
438
        );
439
    }
440
441
    /**
442
     * @return static
443
     */
444
    final public function assertElementAttributeNotContains(string $selector, string $attribute, string $expected): self
445
    {
446
        return $this->wrapMinkExpectation(
447
            fn() => $this->webAssert()->elementAttributeNotContains('css', $selector, $attribute, $expected)
448
        );
449
    }
450
451
    /**
452
     * @internal
453
     */
454
    public function dumpCurrentState(string $filename): void
455
    {
456
        $this->saveSource("{$filename}.txt");
457
    }
458
459
    /**
460
     * @internal
461
     *
462
     * @return array<string,string[]>
463
     */
464
    public function savedArtifacts(): array
465
    {
466
        return ['Saved Source Files' => $this->savedSources];
467
    }
468
469
    public function response(): Response
470
    {
471
        return Response::createFor($this->minkSession());
472
    }
473
474
    /**
475
     * @internal
476
     */
477
    final protected function minkSession(): Session
478
    {
479
        return $this->mink->getSession(self::SESSION);
480
    }
481
482
    /**
483
     * @internal
484
     */
485
    final protected function webAssert(): WebAssert
486
    {
487
        return $this->mink->assertSession(self::SESSION);
488
    }
489
490
    /**
491
     * @internal
492
     */
493
    final protected function documentElement(): DocumentElement
494
    {
495
        return $this->minkSession()->getPage();
496
    }
497
498
    /**
499
     * @internal
500
     *
501
     * @return static
502
     */
503
    final protected function wrapMinkExpectation(callable $callback): self
504
    {
505
        Assert::run(new MinkAssertion($callback));
506
507
        return $this;
508
    }
509
510
    /**
511
     * @internal
512
     */
513
    protected function die(): void
514
    {
515
        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...
516
    }
517
}
518