Passed
Pull Request — 1.x (#74)
by Kevin
02:19
created

Browser::useParameters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 8
rs 10
c 0
b 0
f 0
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(...$this->useParameters())
112
        );
113
114
        return $this;
115
    }
116
117
    /**
118
     * @return static
119
     */
120
    final public function saveSource(string $filename): self
121
    {
122
        if ($this->sourceDir) {
123
            $filename = \sprintf('%s/%s', \rtrim($this->sourceDir, '/'), \ltrim($filename, '/'));
124
        }
125
126
        (new Filesystem())->dumpFile($this->savedSources[] = $filename, $this->response()->raw());
127
128
        return $this;
129
    }
130
131
    /**
132
     * @return static
133
     */
134
    final public function dump(?string $selector = null): self
135
    {
136
        $this->response()->dump($selector);
137
138
        return $this;
139
    }
140
141
    final public function dd(?string $selector = null): void
142
    {
143
        $this->dump($selector);
144
        $this->die();
145
    }
146
147
    /**
148
     * @return static
149
     */
150
    public function follow(string $link): self
151
    {
152
        $this->documentElement()->clickLink($link);
153
154
        return $this;
155
    }
156
157
    /**
158
     * @return static
159
     */
160
    final public function fillField(string $selector, string $value): self
161
    {
162
        $this->documentElement()->fillField($selector, $value);
163
164
        return $this;
165
    }
166
167
    /**
168
     * @return static
169
     */
170
    final public function checkField(string $selector): self
171
    {
172
        $field = $this->documentElement()->findField($selector);
173
174
        if ($field && 'radio' === \mb_strtolower((string) $field->getAttribute('type'))) {
175
            $this->documentElement()->selectFieldOption($selector, (string) $field->getAttribute('value'));
176
177
            return $this;
178
        }
179
180
        $this->documentElement()->checkField($selector);
181
182
        return $this;
183
    }
184
185
    /**
186
     * @return static
187
     */
188
    final public function uncheckField(string $selector): self
189
    {
190
        $this->documentElement()->uncheckField($selector);
191
192
        return $this;
193
    }
194
195
    /**
196
     * Select Radio, check checkbox, select single/multiple values.
197
     *
198
     * @param string|array|null $value null: check radio/checkbox
199
     *                                 string: single value
200
     *                                 array: multiple values
201
     *
202
     * @return static
203
     */
204
    final public function selectField(string $selector, $value = null): self
205
    {
206
        if (\is_array($value)) {
207
            return $this->selectFieldOptions($selector, $value);
208
        }
209
210
        if (\is_string($value)) {
211
            return $this->selectFieldOption($selector, $value);
212
        }
213
214
        return $this->checkField($selector);
215
    }
216
217
    /**
218
     * @return static
219
     */
220
    final public function selectFieldOption(string $selector, string $value): self
221
    {
222
        $this->documentElement()->selectFieldOption($selector, $value);
223
224
        return $this;
225
    }
226
227
    /**
228
     * @return static
229
     */
230
    final public function selectFieldOptions(string $selector, array $values): self
231
    {
232
        foreach ($values as $value) {
233
            $this->documentElement()->selectFieldOption($selector, $value, true);
234
        }
235
236
        return $this;
237
    }
238
239
    /**
240
     * @param string[]|string $filename string: single file
241
     *                                  array: multiple files
242
     *
243
     * @return static
244
     */
245
    final public function attachFile(string $selector, $filename): self
246
    {
247
        foreach ((array) $filename as $file) {
248
            if (!\file_exists($file)) {
249
                throw new \InvalidArgumentException(\sprintf('File "%s" does not exist.', $file));
250
            }
251
        }
252
253
        $this->documentElement()->attachFileToField($selector, $filename);
254
255
        return $this;
256
    }
257
258
    /**
259
     * Click on a button, link or any DOM element.
260
     *
261
     * @return static
262
     */
263
    final public function click(string $selector): self
264
    {
265
        try {
266
            $this->documentElement()->pressButton($selector);
267
        } catch (ElementNotFoundException $e) {
268
            // try link
269
            try {
270
                $this->documentElement()->clickLink($selector);
271
            } catch (ElementNotFoundException $e) {
272
                if (!$element = $this->documentElement()->find('css', $selector)) {
273
                    throw $e;
274
                }
275
276
                $element->click();
277
            }
278
        }
279
280
        return $this;
281
    }
282
283
    /**
284
     * @return static
285
     */
286
    final public function assertSee(string $expected): self
287
    {
288
        return $this->wrapMinkExpectation(
289
            fn() => $this->webAssert()->pageTextContains($expected)
290
        );
291
    }
292
293
    /**
294
     * @return static
295
     */
296
    final public function assertNotSee(string $expected): self
297
    {
298
        return $this->wrapMinkExpectation(
299
            fn() => $this->webAssert()->pageTextNotContains($expected)
300
        );
301
    }
302
303
    /**
304
     * @return static
305
     */
306
    final public function assertSeeIn(string $selector, string $expected): self
307
    {
308
        return $this->wrapMinkExpectation(
309
            fn() => $this->webAssert()->elementTextContains('css', $selector, $expected)
310
        );
311
    }
312
313
    /**
314
     * @return static
315
     */
316
    final public function assertNotSeeIn(string $selector, string $expected): self
317
    {
318
        return $this->wrapMinkExpectation(
319
            fn() => $this->webAssert()->elementTextNotContains('css', $selector, $expected)
320
        );
321
    }
322
323
    /**
324
     * @return static
325
     */
326
    final public function assertSeeElement(string $selector): self
327
    {
328
        return $this->wrapMinkExpectation(
329
            fn() => $this->webAssert()->elementExists('css', $selector)
330
        );
331
    }
332
333
    /**
334
     * @return static
335
     */
336
    final public function assertNotSeeElement(string $selector): self
337
    {
338
        return $this->wrapMinkExpectation(
339
            fn() => $this->webAssert()->elementNotExists('css', $selector)
340
        );
341
    }
342
343
    /**
344
     * @return static
345
     */
346
    final public function assertElementCount(string $selector, int $count): self
347
    {
348
        return $this->wrapMinkExpectation(
349
            fn() => $this->webAssert()->elementsCount('css', $selector, $count)
350
        );
351
    }
352
353
    /**
354
     * @return static
355
     */
356
    final public function assertFieldEquals(string $selector, string $expected): self
357
    {
358
        return $this->wrapMinkExpectation(
359
            fn() => $this->webAssert()->fieldValueEquals($selector, $expected)
360
        );
361
    }
362
363
    /**
364
     * @return static
365
     */
366
    final public function assertFieldNotEquals(string $selector, string $expected): self
367
    {
368
        return $this->wrapMinkExpectation(
369
            fn() => $this->webAssert()->fieldValueNotEquals($selector, $expected)
370
        );
371
    }
372
373
    /**
374
     * @return static
375
     */
376
    final public function assertSelected(string $selector, string $expected): self
377
    {
378
        try {
379
            $field = $this->webAssert()->fieldExists($selector);
380
        } catch (ExpectationException $e) {
381
            Assert::fail($e->getMessage());
382
        }
383
384
        Assert::that((array) $field->getValue())->contains($expected);
385
386
        return $this;
387
    }
388
389
    /**
390
     * @return static
391
     */
392
    final public function assertNotSelected(string $selector, string $expected): self
393
    {
394
        try {
395
            $field = $this->webAssert()->fieldExists($selector);
396
        } catch (ExpectationException $e) {
397
            Assert::fail($e->getMessage());
398
        }
399
400
        Assert::that((array) $field->getValue())->doesNotContain($expected);
401
402
        return $this;
403
    }
404
405
    /**
406
     * @return static
407
     */
408
    final public function assertChecked(string $selector): self
409
    {
410
        return $this->wrapMinkExpectation(
411
            fn() => $this->webAssert()->checkboxChecked($selector)
412
        );
413
    }
414
415
    /**
416
     * @return static
417
     */
418
    final public function assertNotChecked(string $selector): self
419
    {
420
        return $this->wrapMinkExpectation(
421
            fn() => $this->webAssert()->checkboxNotChecked($selector)
422
        );
423
    }
424
425
    /**
426
     * @return static
427
     */
428
    final public function assertElementAttributeContains(string $selector, string $attribute, string $expected): self
429
    {
430
        return $this->wrapMinkExpectation(
431
            fn() => $this->webAssert()->elementAttributeContains('css', $selector, $attribute, $expected)
432
        );
433
    }
434
435
    /**
436
     * @return static
437
     */
438
    final public function assertElementAttributeNotContains(string $selector, string $attribute, string $expected): self
439
    {
440
        return $this->wrapMinkExpectation(
441
            fn() => $this->webAssert()->elementAttributeNotContains('css', $selector, $attribute, $expected)
442
        );
443
    }
444
445
    /**
446
     * @internal
447
     */
448
    public function dumpCurrentState(string $filename): void
449
    {
450
        $this->saveSource("{$filename}.txt");
451
    }
452
453
    /**
454
     * @internal
455
     *
456
     * @return array<string,string[]>
457
     */
458
    public function savedArtifacts(): array
459
    {
460
        return ['Saved Source Files' => $this->savedSources];
461
    }
462
463
    public function response(): Response
464
    {
465
        return Response::createFor($this->minkSession());
466
    }
467
468
    /**
469
     * @internal
470
     */
471
    final protected function minkSession(): Session
472
    {
473
        return $this->mink->getSession(self::SESSION);
474
    }
475
476
    /**
477
     * @internal
478
     */
479
    final protected function webAssert(): WebAssert
480
    {
481
        return $this->mink->assertSession(self::SESSION);
482
    }
483
484
    /**
485
     * @internal
486
     */
487
    final protected function documentElement(): DocumentElement
488
    {
489
        return $this->minkSession()->getPage();
490
    }
491
492
    /**
493
     * @internal
494
     *
495
     * @return static
496
     */
497
    final protected function wrapMinkExpectation(callable $callback): self
498
    {
499
        Assert::run(new MinkAssertion($callback));
500
501
        return $this;
502
    }
503
504
    /**
505
     * @internal
506
     */
507
    protected function die(): void
508
    {
509
        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...
510
    }
511
512
    /**
513
     * @internal
514
     *
515
     * @return Parameter[]
516
     */
517
    protected function useParameters(): array
518
    {
519
        return [
520
            Parameter::untyped($this),
521
            Parameter::typed(self::class, $this),
522
            Parameter::typed(Component::class, Parameter::factory(fn(string $class) => new $class($this))),
523
            Parameter::typed(Response::class, Parameter::factory(fn() => $this->response())),
524
            Parameter::typed(Crawler::class, Parameter::factory(fn() => $this->response()->assertDom()->crawler())),
525
        ];
526
    }
527
}
528