Passed
Pull Request — 1.x (#60)
by Tobias
02:03
created

Browser::clickOnElement()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
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
    private array $savedSources = [];
30
31
    /**
32
     * @internal
33
     */
34
    public function __construct(DriverInterface $driver)
35
    {
36
        $this->mink = new Mink([self::SESSION => new Session($driver)]);
37
    }
38
39
    /**
40
     * @return static
41
     */
42
    final public function setSourceDir(string $dir): self
43
    {
44
        $this->sourceDir = $dir;
45
46
        return $this;
47
    }
48
49
    /**
50
     * @return static
51
     */
52
    final public function visit(string $uri): self
53
    {
54
        $this->minkSession()->visit($uri);
55
56
        return $this;
57
    }
58
59
    /**
60
     * @param array $parts The url parts to check {@see parse_url} (use empty array for "all")
61
     *
62
     * @return static
63
     */
64
    final public function assertOn(string $expected, array $parts = ['path', 'query', 'fragment']): self
65
    {
66
        Assert::run(new SameUrlAssertion($this->minkSession()->getCurrentUrl(), $expected, $parts));
67
68
        return $this;
69
    }
70
71
    /**
72
     * @param array $parts The url parts to check (@see parse_url)
73
     *
74
     * @return static
75
     */
76
    final public function assertNotOn(string $expected, array $parts = ['path', 'query', 'fragment']): self
77
    {
78
        Assert::not(new SameUrlAssertion($this->minkSession()->getCurrentUrl(), $expected, $parts));
79
80
        return $this;
81
    }
82
83
    /**
84
     * @return static
85
     */
86
    final public function assertContains(string $expected): self
87
    {
88
        return $this->wrapMinkExpectation(
89
            fn() => $this->webAssert()->responseContains($expected)
90
        );
91
    }
92
93
    /**
94
     * @return static
95
     */
96
    final public function assertNotContains(string $expected): self
97
    {
98
        return $this->wrapMinkExpectation(
99
            fn() => $this->webAssert()->responseNotContains($expected)
100
        );
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()->assertDom()->crawler()))
115
            )
116
        );
117
118
        return $this;
119
    }
120
121
    /**
122
     * @return static
123
     */
124
    final public function saveSource(string $filename): self
125
    {
126
        if ($this->sourceDir) {
127
            $filename = \sprintf('%s/%s', \rtrim($this->sourceDir, '/'), \ltrim($filename, '/'));
128
        }
129
130
        (new Filesystem())->dumpFile($this->savedSources[] = $filename, $this->response()->raw());
131
132
        return $this;
133
    }
134
135
    /**
136
     * @return static
137
     */
138
    final public function dump(?string $selector = null): self
139
    {
140
        $this->response()->dump($selector);
141
142
        return $this;
143
    }
144
145
    final public function dd(?string $selector = null): void
146
    {
147
        $this->dump($selector);
148
        $this->die();
149
    }
150
151
    /**
152
     * @return static
153
     */
154
    public function follow(string $link): self
155
    {
156
        $this->documentElement()->clickLink($link);
157
158
        return $this;
159
    }
160
161
    /**
162
     * @return static
163
     */
164
    final public function fillField(string $selector, string $value): self
165
    {
166
        $this->documentElement()->fillField($selector, $value);
167
168
        return $this;
169
    }
170
171
    /**
172
     * @return static
173
     */
174
    final public function checkField(string $selector): self
175
    {
176
        $field = $this->documentElement()->findField($selector);
177
178
        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

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