@plan2net/typo3-playwright-toolkit 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -50
- package/dist/build.d.ts +8 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +73 -0
- package/dist/build.js.map +1 -0
- package/dist/checks/screenshot.d.ts +7 -0
- package/dist/checks/screenshot.d.ts.map +1 -1
- package/dist/checks/screenshot.js +113 -8
- package/dist/checks/screenshot.js.map +1 -1
- package/dist/clean/cli.d.ts +3 -0
- package/dist/clean/cli.d.ts.map +1 -0
- package/dist/clean/cli.js +32 -0
- package/dist/clean/cli.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/doctor/cli.d.ts +3 -0
- package/dist/doctor/cli.d.ts.map +1 -0
- package/dist/doctor/cli.js +41 -0
- package/dist/doctor/cli.js.map +1 -0
- package/dist/doctor/reporter.d.ts +22 -0
- package/dist/doctor/reporter.d.ts.map +1 -0
- package/dist/doctor/reporter.js +223 -0
- package/dist/doctor/reporter.js.map +1 -0
- package/dist/global-setup.d.ts +18 -0
- package/dist/global-setup.d.ts.map +1 -1
- package/dist/global-setup.js +9 -3
- package/dist/global-setup.js.map +1 -1
- package/dist/http/api-secret.js +1 -1
- package/dist/http/record-edit.d.ts.map +1 -1
- package/dist/http/record-edit.js +7 -6
- package/dist/http/record-edit.js.map +1 -1
- package/dist/inspect/cli.js +2 -2
- package/dist/inspect/links.d.ts +2 -0
- package/dist/inspect/links.d.ts.map +1 -1
- package/dist/inspect/links.js +8 -0
- package/dist/inspect/links.js.map +1 -1
- package/dist/playwright/base-config.d.ts.map +1 -1
- package/dist/playwright/base-config.js +3 -0
- package/dist/playwright/base-config.js.map +1 -1
- package/dist/state/run-namespace.d.ts +2 -0
- package/dist/state/run-namespace.d.ts.map +1 -1
- package/dist/state/run-namespace.js +1 -1
- package/dist/state/run-namespace.js.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -5,18 +5,22 @@
|
|
|
5
5
|
<p align="center"><em>Playwright fixtures and content builders for TYPO3, one test database per test file.</em></p>
|
|
6
6
|
<br>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://www.npmjs.com/package/@plan2net/typo3-playwright-toolkit"><img src="https://img.shields.io/npm/v/@plan2net/typo3-playwright-toolkit?style=for-the-badge&logo=npm&logoColor=white&labelColor=24273a&color=fff3b0" alt="npm version"></a>
|
|
10
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-22.12%2B-a0c4ff?style=for-the-badge&logo=nodedotjs&logoColor=white&labelColor=24273a" alt="Node 22.12 or newer"></a>
|
|
11
|
+
<a href="https://playwright.dev"><img src="https://img.shields.io/badge/Playwright-1.44%2B-b5ead7?style=for-the-badge&labelColor=24273a&logo=data:image/svg%2Bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI%2BPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcgNHYxNmwxMy04eiIvPjwvc3ZnPg%3D%3D" alt="Playwright 1.44 or newer"></a>
|
|
12
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/licence-GPL--2.0--or--later-ffc6d9?style=for-the-badge&logo=gnu&logoColor=white&labelColor=24273a" alt="GPL-2.0-or-later licence"></a>
|
|
13
|
+
</p>
|
|
12
14
|
|
|
13
15
|
An npm package. It provides the [Playwright](https://playwright.dev) fixtures, the
|
|
14
16
|
content builders that create [TYPO3](https://typo3.org) records, and the
|
|
15
17
|
accessibility ([axe](https://github.com/dequelabs/axe-core)) and CSP checks.
|
|
16
18
|
|
|
17
|
-
It needs the
|
|
18
|
-
|
|
19
|
-
the
|
|
19
|
+
It needs the
|
|
20
|
+
[Composer extension](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/playwright-toolkit),
|
|
21
|
+
which creates the test databases, and the
|
|
22
|
+
[DDEV add-on](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/ddev-typo3-playwright-toolkit),
|
|
23
|
+
which provides the database service and the commands.
|
|
20
24
|
|
|
21
25
|
> [!IMPORTANT]
|
|
22
26
|
> Setting this up for the first time? Follow
|
|
@@ -38,7 +42,7 @@ the database service and the commands.
|
|
|
38
42
|
You do not need a database client. This package never talks to the database; it asks
|
|
39
43
|
the extension to create and delete them. That is why PHP and Node may run in
|
|
40
44
|
different containers.
|
|
41
|
-
[Where things run](https://github.com/plan2net/typo3-playwright-toolkit#where-things-run)
|
|
45
|
+
[Where things run](https://github.com/plan2net/typo3-playwright-toolkit/blob/main/SETUP.md#where-things-run)
|
|
42
46
|
shows how.
|
|
43
47
|
|
|
44
48
|
## Install
|
|
@@ -86,9 +90,9 @@ this package uses, because the test database exists nowhere else.
|
|
|
86
90
|
|
|
87
91
|
`consumerRoot` is the root of your TYPO3 project. The package derives the state and
|
|
88
92
|
session folders from it, so it works from `node_modules` without knowing its own
|
|
89
|
-
location. Count the `..` from where your config
|
|
93
|
+
location. Count the `..` from where your config sits: the example assumes
|
|
90
94
|
`<project>/tests/playwright/`, so it climbs two levels. Pointing it at the wrong
|
|
91
|
-
directory is not an error
|
|
95
|
+
directory is not an error; the run works and writes its state somewhere you did not
|
|
92
96
|
expect. Use `fileURLToPath` rather than `new URL(...).pathname`, which
|
|
93
97
|
percent-encodes a project path containing spaces or non-ASCII characters.
|
|
94
98
|
|
|
@@ -146,11 +150,11 @@ setup, and the other tests wait. If the setup fails, the tests are skipped with
|
|
|
146
150
|
reason instead of failing because content is missing.
|
|
147
151
|
|
|
148
152
|
The `slug` you get back is the one the site stored, which is not always the one you
|
|
149
|
-
asked for
|
|
153
|
+
asked for. A translation and a name already in use are two cases, and an extension
|
|
150
154
|
can add more. Navigate with the returned value, never with the string you passed in.
|
|
151
155
|
|
|
152
|
-
A save also reports how many records TYPO3
|
|
153
|
-
|
|
156
|
+
A save also reports how many records TYPO3 wrote, and warns when that is more than
|
|
157
|
+
you asked for:
|
|
154
158
|
|
|
155
159
|
```
|
|
156
160
|
[typo3-playwright-toolkit] Saving pages wrote 27 records for 1 requested.
|
|
@@ -158,7 +162,7 @@ than you asked for:
|
|
|
158
162
|
A slug change cascades to every descendant page and writes a redirect for each.
|
|
159
163
|
```
|
|
160
164
|
|
|
161
|
-
Nothing is wrong
|
|
165
|
+
Nothing is wrong; the save succeeded. It is where a slow setup spends its time, and
|
|
162
166
|
changing the slug of a page with descendants is the usual cause. The counts are on the
|
|
163
167
|
result as `written`. It never fails a test.
|
|
164
168
|
|
|
@@ -178,7 +182,7 @@ attempts by default. Lower them to hear about a broken setup sooner.
|
|
|
178
182
|
|
|
179
183
|
### When something fails, TYPO3 says why
|
|
180
184
|
|
|
181
|
-
A failing test
|
|
185
|
+
A failing test prints what TYPO3 wrote to its log while that test ran, under the
|
|
182
186
|
failure itself:
|
|
183
187
|
|
|
184
188
|
```
|
|
@@ -199,8 +203,10 @@ The same list is attached to the test as `typo3-errors.json`. A message that rep
|
|
|
199
203
|
is counted, not printed again. A failing setup shows the same list next to its own
|
|
200
204
|
error.
|
|
201
205
|
|
|
202
|
-
If TYPO3 refuses a record while a builder saves it, the builder stops at that line
|
|
203
|
-
|
|
206
|
+
If TYPO3 refuses a record while a builder saves it, the builder stops at that line,
|
|
207
|
+
so the failure points at the save and not at a later assertion. A `withField()` column
|
|
208
|
+
that TCA does not have fails there too, naming the closest column that exists: TYPO3
|
|
209
|
+
would drop such a field without saying so, leaving the test without its content.
|
|
204
210
|
|
|
205
211
|
<picture>
|
|
206
212
|
<source media="(max-width: 700px)" srcset="https://raw.githubusercontent.com/plan2net/typo3-playwright-toolkit/main/diagrams/scenario-fan-out-narrow.svg">
|
|
@@ -211,9 +217,9 @@ Before, the save looked fine and the test failed later, somewhere unrelated.
|
|
|
211
217
|
Besides `builders`, the setup receives `testId` (the database this attempt runs
|
|
212
218
|
against), `attempt` (`1` on the first try), `signal` (aborted when the attempt times
|
|
213
219
|
out, so pass it to your own long requests), and a `page` and `request` that already
|
|
214
|
-
carry the toolkit headers. Your tests get `testId` beside `state
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
carry the toolkit headers. Your tests get `testId` beside `state`. It is the database
|
|
221
|
+
name without its `db` prefix, and what the backend shows in brackets behind the site
|
|
222
|
+
name:
|
|
217
223
|
|
|
218
224
|
```ts
|
|
219
225
|
test('reports which database it used', async ({ page, state, testId }) => {
|
|
@@ -334,14 +340,14 @@ const [hero, intro, gallery] = await builders.batch(
|
|
|
334
340
|
)
|
|
335
341
|
```
|
|
336
342
|
|
|
337
|
-
The queued builders get no `.create()
|
|
338
|
-
the order you list them, you get a uid for each, and their files and child records
|
|
339
|
-
along in the same request.
|
|
343
|
+
The queued builders get no `.create()`; `batch` saves them. They land on the page in
|
|
344
|
+
the order you list them, you get a uid for each, and their files and child records
|
|
345
|
+
come along in the same request.
|
|
340
346
|
|
|
341
|
-
All the elements have to belong to one page, because a request positions them after
|
|
342
|
-
another and that means nothing across pages. They also cannot point at each
|
|
343
|
-
relation needs a uid, and only a save hands one back. Create what is pointed
|
|
344
|
-
then batch the rest:
|
|
347
|
+
All the elements have to belong to one page, because a request positions them after
|
|
348
|
+
one another and that means nothing across pages. They also cannot point at each
|
|
349
|
+
other: a relation needs a uid, and only a save hands one back. Create what is pointed
|
|
350
|
+
at first, then batch the rest:
|
|
345
351
|
|
|
346
352
|
```ts
|
|
347
353
|
const container = await builders.content().onPage(page.id).ofType('my_container').create()
|
|
@@ -355,21 +361,21 @@ await builders.batch(
|
|
|
355
361
|
```
|
|
356
362
|
|
|
357
363
|
A content type whose children hang off a column of its own needs none of this:
|
|
358
|
-
`withChildren` already writes them in the same request as their parent. Batching is
|
|
359
|
-
elements that sit side by side on a page.
|
|
364
|
+
`withChildren` already writes them in the same request as their parent. Batching is
|
|
365
|
+
for elements that sit side by side on a page.
|
|
360
366
|
|
|
361
367
|
### Calling the site directly
|
|
362
368
|
|
|
363
|
-
The `request` client
|
|
364
|
-
fixture in your tests
|
|
365
|
-
|
|
366
|
-
|
|
369
|
+
The `request` client, the one `defineScenario` hands your setup and the `request`
|
|
370
|
+
fixture in your tests, carries the test ID for `testingURL`, so it reads and writes
|
|
371
|
+
the same throwaway database the browser does. Requests to any other host get neither
|
|
372
|
+
toolkit header.
|
|
367
373
|
|
|
368
374
|
### Stubbing a third-party script
|
|
369
375
|
|
|
370
376
|
A cookie banner or a tracking script can swallow the clicks your test makes.
|
|
371
|
-
`prepareContext` runs on every context a test uses, after the toolkit's own
|
|
372
|
-
|
|
377
|
+
`prepareContext` runs on every context a test uses, after the toolkit's own routes
|
|
378
|
+
are in place:
|
|
373
379
|
|
|
374
380
|
```ts
|
|
375
381
|
// playwright.config.ts
|
|
@@ -378,10 +384,48 @@ prepareContext: async (context) => {
|
|
|
378
384
|
},
|
|
379
385
|
```
|
|
380
386
|
|
|
387
|
+
### Building your assets
|
|
388
|
+
|
|
389
|
+
Every run builds your frontend assets before the first test. Tests against a stale
|
|
390
|
+
bundle still pass, and the screenshots they write become the baseline for the next
|
|
391
|
+
run, so no later run reports the problem either.
|
|
392
|
+
|
|
393
|
+
If you set no `build`, the package runs the `build` script from your project's
|
|
394
|
+
`package.json`, through the package manager your lockfile names: pnpm for
|
|
395
|
+
`pnpm-lock.yaml`, yarn for `yarn.lock`, bun for a bun lockfile, npm otherwise. A
|
|
396
|
+
project without that script builds nothing and starts right away, so a site with
|
|
397
|
+
committed assets needs no setting at all.
|
|
398
|
+
|
|
399
|
+
Name your own command when the default is wrong:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
// playwright.config.ts
|
|
403
|
+
build: 'pnpm build:test', // one command
|
|
404
|
+
build: 'npm run css && npm run js', // several, because it runs through a shell
|
|
405
|
+
build: false, // this project never builds
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
The command runs in `consumerRoot` and writes its output to your terminal. A failing
|
|
409
|
+
build stops the run before the first test, so it costs you no per-test database and
|
|
410
|
+
no state to clean up. Under DDEV the template database is rebuilt earlier, by the
|
|
411
|
+
`ddev playwright` command itself, so that step runs either way.
|
|
412
|
+
|
|
413
|
+
To skip the build once, set `PW_SKIP_BUILD=1`, which is what
|
|
414
|
+
`ddev playwright test --skip-build` does.
|
|
415
|
+
|
|
381
416
|
### Screenshots
|
|
382
417
|
|
|
383
418
|
`expectScreenshot` waits for fonts, images and animations, hides the selectors from
|
|
384
|
-
`hideBeforeScreenshot`, and then compares against the stored image.
|
|
419
|
+
`hideBeforeScreenshot`, and then compares against the stored image. An image that has
|
|
420
|
+
still not decoded when that wait runs out is named in a warning, because it would
|
|
421
|
+
otherwise be blank in the picture and, on a first run, blank in the new baseline.
|
|
422
|
+
|
|
423
|
+
For the duration of the shot each image is pinned to the file it resolved to, and released
|
|
424
|
+
again afterwards. Chromium collapses the viewport to 1×1 while it captures anything taller
|
|
425
|
+
than the viewport, and a `<source media="(max-width: …)">` flips while it is collapsed, which
|
|
426
|
+
drops the decoded image and leaves a blank. Pinning it is invisible to a test, including one
|
|
427
|
+
that narrows the viewport between two shots. A `background-image` behind a width query is
|
|
428
|
+
not covered.
|
|
385
429
|
|
|
386
430
|
```ts
|
|
387
431
|
import { expectScreenshot } from '@plan2net/typo3-playwright-toolkit'
|
|
@@ -410,7 +454,7 @@ Every other option is passed on to `toHaveScreenshot`, and the tolerances come f
|
|
|
410
454
|
> ```
|
|
411
455
|
|
|
412
456
|
`expectScreenshot` waits for animations itself. When you interact and then assert
|
|
413
|
-
without a screenshot
|
|
457
|
+
without a screenshot, say an accessibility scan after opening an accordion, wait
|
|
414
458
|
first:
|
|
415
459
|
|
|
416
460
|
```ts
|
|
@@ -499,6 +543,7 @@ Everything else:
|
|
|
499
543
|
| `accessibility.disabledRules` | `[]` | axe rules turned off for the whole project |
|
|
500
544
|
| `accessibility.projects` | all projects | Projects that run axe checks |
|
|
501
545
|
| `accessibility.tags` | `DEFAULT_SCAN_TAGS` | Which axe rule sets to run |
|
|
546
|
+
| `build` | your `build` script | Command that builds your assets before the run, or `false` for none |
|
|
502
547
|
| `cleanup.failOnLeak` | true in CI | Fail the run if a test database could not be deleted |
|
|
503
548
|
| `cleanup.orphanAgeMs` | 24 hours | How old a leftover database must be before cleanup deletes it |
|
|
504
549
|
| `cleanup.preserveOnFailure` | `failed` | Which test databases to keep after a failure: `failed`, `all` or `none` |
|
|
@@ -534,7 +579,7 @@ Every CType of a normal TYPO3 installation has a builder, and you register nothi
|
|
|
534
579
|
|
|
535
580
|
All builders share `withHeader`, `withSubheader`, `withHeaderLayout`,
|
|
536
581
|
`withHeaderLink`, `withColPos`, `setHidden`, and `withField(column, value)` for any
|
|
537
|
-
other TCA column. They also share the four relation setters above
|
|
582
|
+
other TCA column. They also share the four relation setters above:
|
|
538
583
|
`withFileReference`, `withFileReferences`, `withChild` and `withChildren`. Types with
|
|
539
584
|
images add `withFile` and `withFiles` for their own media column, plus `withColumns`,
|
|
540
585
|
`withOrientation` and `withImageSize`.
|
|
@@ -588,13 +633,39 @@ export class EventListContent extends CoreContent {
|
|
|
588
633
|
```
|
|
589
634
|
|
|
590
635
|
For a structure with named sheets, or a key outside `settings.`, write the column
|
|
591
|
-
yourself with `flexForm()
|
|
636
|
+
yourself with `flexForm()`. The form posts one field per value, not one for the
|
|
592
637
|
column:
|
|
593
638
|
|
|
594
639
|
```ts
|
|
595
640
|
element.withField('pi_flexform', flexForm({ sDEF: {…}, sFilter: {…} }))
|
|
596
641
|
```
|
|
597
642
|
|
|
643
|
+
### The doctor command
|
|
644
|
+
|
|
645
|
+
The package installs `typo3-playwright-doctor`. Run it from the directory holding
|
|
646
|
+
your Playwright config to see whether the project can run tests: browser launch or
|
|
647
|
+
remote connection, access to `testingURL`, API credentials and version
|
|
648
|
+
compatibility, then a throwaway database and its backend session:
|
|
649
|
+
|
|
650
|
+
```bash
|
|
651
|
+
npx typo3-playwright-doctor
|
|
652
|
+
npx typo3-playwright-doctor --project chromium
|
|
653
|
+
npx typo3-playwright-doctor --config playwright.local.config.ts
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Every check prints success, failure or the reason it was skipped. Exit code `0` means
|
|
657
|
+
ready, `1` means a check failed. `--project` accepts wildcards and can be repeated;
|
|
658
|
+
omit it to check every project.
|
|
659
|
+
|
|
660
|
+
It runs no tests, setup hooks, builds or repairs, and touches neither your existing
|
|
661
|
+
results nor a kept test database. The one database it creates is its own, and it
|
|
662
|
+
drops that again even after a failed check. On DDEV, `ddev playwright doctor` wraps
|
|
663
|
+
it.
|
|
664
|
+
|
|
665
|
+
It also checks nothing about your installation. The TYPO3 extension has a wizard for
|
|
666
|
+
that, `playwright:setup`, which reads your files, versions and settings and writes
|
|
667
|
+
what is missing.
|
|
668
|
+
|
|
598
669
|
### The inspect command
|
|
599
670
|
|
|
600
671
|
The package installs `typo3-playwright-inspect`. Run it from your project root
|
|
@@ -607,24 +678,43 @@ npx typo3-playwright-inspect accordion # only matching test files
|
|
|
607
678
|
```
|
|
608
679
|
|
|
609
680
|
It reads the API secret from `var/playwright/api-secret` or from
|
|
610
|
-
`PLAYWRIGHT_TOOLKIT_SECRET`. On DDEV, `ddev playwright
|
|
681
|
+
`PLAYWRIGHT_TOOLKIT_SECRET`. On DDEV, `ddev playwright inspect` wraps it. The links
|
|
611
682
|
log in as the pre-seeded backend user and live 15 minutes.
|
|
612
683
|
|
|
613
684
|
`--replay` prints a link into the database a replay run built, instead of the kept
|
|
614
|
-
test databases. Use it when the link that run printed has expired
|
|
685
|
+
test databases. Use it when the link that run printed has expired; it mints a new
|
|
615
686
|
one rather than rebuilding anything.
|
|
616
687
|
|
|
688
|
+
### The clean command
|
|
689
|
+
|
|
690
|
+
The package installs `typo3-playwright-clean`. Run it from your project root after a
|
|
691
|
+
run you stopped, to drop the test databases and run state it left behind:
|
|
692
|
+
|
|
693
|
+
```bash
|
|
694
|
+
npx typo3-playwright-clean
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Teardown reclaims those by itself, but only once they are `cleanup.orphanAgeMs` old,
|
|
698
|
+
a day by default. This does it now. A run whose state was touched in the last 30
|
|
699
|
+
seconds counts as still going: its databases go into the sweep's keep list and its
|
|
700
|
+
run directory stays, so a suite running in another terminal is safe. The template and
|
|
701
|
+
the replay database are never dropped. On DDEV, `ddev playwright clean` wraps it.
|
|
702
|
+
|
|
703
|
+
Databases kept for `typo3-playwright-inspect` are dropped too, since nothing marks
|
|
704
|
+
them as worth keeping. Read what you need from them first.
|
|
705
|
+
|
|
617
706
|
### Replay mode
|
|
618
707
|
|
|
619
708
|
`PW_REPLAY=1` runs every scenario's setup into one shared database rather than a
|
|
620
709
|
per-test one, so all the content the suite builds ends up in a single place you can
|
|
621
|
-
browse and export. `ddev playwright
|
|
710
|
+
browse and export. `ddev playwright replay` sets it, rebuilds that database from the
|
|
622
711
|
template first, and prints a backend link when the run ends.
|
|
623
712
|
|
|
624
713
|
What changes while it is set: every scenario uses the one fixed test ID
|
|
625
|
-
`REPLAY0000000000`, its content goes into a sysfolder named after
|
|
626
|
-
fixture root, slugs keep no test-ID suffix, setups run once with no
|
|
627
|
-
|
|
714
|
+
`REPLAY0000000000`, its content, images included, goes into a sysfolder named after
|
|
715
|
+
it under the fixture root, slugs keep no test-ID suffix, setups run once with no
|
|
716
|
+
retry, and teardown drops nothing. The tests themselves are skipped, because their
|
|
717
|
+
assertions and screenshot baselines belong to a per-test database.
|
|
628
718
|
|
|
629
719
|
## Troubleshooting
|
|
630
720
|
|
|
@@ -637,11 +727,15 @@ first thing in `playwright.config.ts`.
|
|
|
637
727
|
**Cleanup refuses a path.** `stateDir` and `sessionDir` must be absolute and inside
|
|
638
728
|
`consumerRoot`, because cleanup deletes files in both.
|
|
639
729
|
|
|
730
|
+
**The run builds nothing, or builds the wrong thing.** The default reads
|
|
731
|
+
`consumerRoot/package.json`, not the `package.json` next to your Playwright config.
|
|
732
|
+
Point `build` at your real command if the two differ.
|
|
733
|
+
|
|
640
734
|
**"Timed out after 300000ms waiting for the setup".** A setup that builds a lot of
|
|
641
735
|
content can outgrow the defaults on a slow machine. Raise `setup.attemptTimeoutMs`
|
|
642
|
-
for the setup itself and `setup.waitTimeoutMs` for the tests waiting on it
|
|
643
|
-
second has to stay
|
|
644
|
-
|
|
736
|
+
for the setup itself and `setup.waitTimeoutMs` for the tests waiting on it. The
|
|
737
|
+
second has to stay well above the first, since it covers every attempt plus the time
|
|
738
|
+
spent waiting for the lock.
|
|
645
739
|
|
|
646
740
|
**Test databases stay after a failed run.** That is intended: the databases of the
|
|
647
741
|
failed test files are kept for debugging, and the run prints a link for each one
|
|
@@ -655,5 +749,5 @@ minutes.
|
|
|
655
749
|
|
|
656
750
|
## Related packages
|
|
657
751
|
|
|
658
|
-
- [`plan2net/playwright-toolkit`](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/playwright-toolkit)
|
|
659
|
-
- [DDEV add-on](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/ddev-typo3-playwright-toolkit)
|
|
752
|
+
- [`plan2net/playwright-toolkit`](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/playwright-toolkit), the Composer extension
|
|
753
|
+
- [DDEV add-on](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/ddev-typo3-playwright-toolkit), the database service and commands
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolkitConfig } from './config.js';
|
|
2
|
+
export declare function resolveBuildCommand(config: ToolkitConfig): string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* `shell: true` is safe here: the command comes from the consumer's own config, never
|
|
5
|
+
* from a request, and `npm run css && npm run js` has to work.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runBuild(config: ToolkitConfig): void;
|
|
8
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAUhD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAqC7E;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAgCpD"}
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
/** First match wins, so npm is the fallback rather than an entry. */
|
|
5
|
+
const LOCKFILES = [
|
|
6
|
+
{ lockfile: 'pnpm-lock.yaml', packageManager: 'pnpm' },
|
|
7
|
+
{ lockfile: 'yarn.lock', packageManager: 'yarn' },
|
|
8
|
+
{ lockfile: 'bun.lockb', packageManager: 'bun' },
|
|
9
|
+
{ lockfile: 'bun.lock', packageManager: 'bun' },
|
|
10
|
+
];
|
|
11
|
+
export function resolveBuildCommand(config) {
|
|
12
|
+
if (false === config.build) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (config.build) {
|
|
16
|
+
return config.build;
|
|
17
|
+
}
|
|
18
|
+
const root = config.paths.consumerRoot;
|
|
19
|
+
const manifestPath = path.join(root, 'package.json');
|
|
20
|
+
if (!fs.existsSync(manifestPath)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
let manifest;
|
|
24
|
+
try {
|
|
25
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error([
|
|
29
|
+
`[typo3-playwright-toolkit] Could not read ${manifestPath}, so there is no way`,
|
|
30
|
+
`to tell whether this project builds: ${error instanceof Error ? error.message : String(error)}`,
|
|
31
|
+
'',
|
|
32
|
+
'Fix the file, or say what to do in defineToolkitConfig: build: <your command>,',
|
|
33
|
+
'or build: false for a project that never builds.',
|
|
34
|
+
].join('\n'));
|
|
35
|
+
}
|
|
36
|
+
if (!manifest?.scripts?.build) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const found = LOCKFILES.find((entry) => fs.existsSync(path.join(root, entry.lockfile)));
|
|
40
|
+
return `${found?.packageManager ?? 'npm'} run build`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `shell: true` is safe here: the command comes from the consumer's own config, never
|
|
44
|
+
* from a request, and `npm run css && npm run js` has to work.
|
|
45
|
+
*/
|
|
46
|
+
export function runBuild(config) {
|
|
47
|
+
if ('1' === process.env.PW_SKIP_BUILD) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const command = resolveBuildCommand(config);
|
|
51
|
+
if (!command) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
process.stdout.write(`[toolkit] Building assets: ${command}\n`);
|
|
55
|
+
const { status, error } = spawnSync(command, {
|
|
56
|
+
cwd: config.paths.consumerRoot,
|
|
57
|
+
shell: true,
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
});
|
|
60
|
+
if (error) {
|
|
61
|
+
throw new Error(`[typo3-playwright-toolkit] The asset build could not start: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
if (0 !== status) {
|
|
64
|
+
throw new Error([
|
|
65
|
+
`[typo3-playwright-toolkit] The asset build failed (exit ${String(status)}): ${command}`,
|
|
66
|
+
'',
|
|
67
|
+
'Fix the build, or run without it: PW_SKIP_BUILD=1, which is what',
|
|
68
|
+
'`ddev playwright test --skip-build` sets. A project that should never',
|
|
69
|
+
'build wants build: false in defineToolkitConfig instead.',
|
|
70
|
+
].join('\n'));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
|
|
@@ -28,6 +28,13 @@ export declare function buildHideStyles(selectors: string[]): string;
|
|
|
28
28
|
*/
|
|
29
29
|
export declare function waitForAnimations(page: Page, selector?: string, timeout?: number): Promise<void>;
|
|
30
30
|
export declare function loadLazyElements(page: Page): Promise<void>;
|
|
31
|
+
export declare function freezeResponsiveImages(page: Page): Promise<void>;
|
|
32
|
+
export declare function prepareImagesForCapture(page: Page): Promise<string[]>;
|
|
33
|
+
export declare function restoreResponsiveImages(page: Page): Promise<void>;
|
|
34
|
+
/** @returns the sources of the images that never decoded, empty when all did */
|
|
35
|
+
export declare function waitForImagesDecoded(page: Page, timeout?: number): Promise<string[]>;
|
|
36
|
+
/** An undecoded image is blank, and a first run writes that blank as the baseline. */
|
|
37
|
+
export declare function warnAboutUndecodedImages(stalled: string[], timeout: number): void;
|
|
31
38
|
export declare function resolveScreenshotTarget(target: Page | Locator, include?: string): {
|
|
32
39
|
shot: Page | Locator;
|
|
33
40
|
wholePage: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/checks/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAGxD,+CAA+C;AAC/C,MAAM,WAAW,2BAA2B;IACxC,UAAU,CAAC,EAAE,UAAU,GAAG,OAAO,CAAA;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,IAAI,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,KAAK,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAA;IACxB,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAUD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAM3D;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBpG;AAID,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhE;
|
|
1
|
+
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/checks/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAGxD,+CAA+C;AAC/C,MAAM,WAAW,2BAA2B;IACxC,UAAU,CAAC,EAAE,UAAU,GAAG,OAAO,CAAA;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,IAAI,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,KAAK,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAA;IACxB,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAUD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAM3D;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBpG;AAID,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhE;AAID,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCtE;AAID,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAM3E;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBvE;AAID,gFAAgF;AAChF,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,SAAiB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAalG;AA0CD,sFAAsF;AACtF,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAMjF;AAuCD,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,IAAI,GAAG,OAAO,EACtB,OAAO,CAAC,EAAE,MAAM,GACjB;IAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAM9C;AAED,MAAM,WAAW,iBAAkB,SAAQ,2BAA2B;IAClE,+BAA+B;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAEzG;AAED,wBAAsB,gBAAgB,CAClC,MAAM,EAAE,IAAI,GAAG,OAAO,EACtB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,iBAAsB,GAChC,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED,wBAAgB,iBAAiB,CAC7B,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,2BAA2B,GACrC,2BAA2B,CAO7B"}
|
|
@@ -42,12 +42,93 @@ export async function loadLazyElements(page) {
|
|
|
42
42
|
.querySelectorAll('[loading="lazy"]')
|
|
43
43
|
.forEach((element) => (element.loading = 'eager')));
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// A capture reaching past the viewport collapses it to 1x1 first, and a width-based
|
|
46
|
+
// `media` on a `<source>` flips while it is collapsed, dropping the decoded image.
|
|
47
|
+
export async function freezeResponsiveImages(page) {
|
|
48
|
+
await page.evaluate(() => {
|
|
49
|
+
document.querySelectorAll('img').forEach((image) => {
|
|
50
|
+
const resolved = image.currentSrc;
|
|
51
|
+
if (!resolved) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
image.dataset.toolkitFrozen = JSON.stringify({
|
|
55
|
+
src: image.getAttribute('src'),
|
|
56
|
+
srcset: image.getAttribute('srcset'),
|
|
57
|
+
sizes: image.getAttribute('sizes'),
|
|
58
|
+
width: image.getAttribute('width'),
|
|
59
|
+
height: image.getAttribute('height'),
|
|
60
|
+
});
|
|
61
|
+
const sources = Array.from(image.closest('picture')?.querySelectorAll('source') ?? []);
|
|
62
|
+
// width and height on the chosen source lay the img out, and disabling it
|
|
63
|
+
// would drop them.
|
|
64
|
+
const chosen = sources.find((source) => !source.media || matchMedia(source.media).matches);
|
|
65
|
+
for (const dimension of ['width', 'height']) {
|
|
66
|
+
const value = chosen?.getAttribute(dimension);
|
|
67
|
+
if (null != value) {
|
|
68
|
+
image.setAttribute(dimension, value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
sources.forEach((source) => {
|
|
72
|
+
source.dataset.toolkitFrozen = JSON.stringify({ media: source.getAttribute('media') });
|
|
73
|
+
// Never matches, so selection falls through to the img below.
|
|
74
|
+
source.media = 'not all';
|
|
75
|
+
});
|
|
76
|
+
image.removeAttribute('srcset');
|
|
77
|
+
image.removeAttribute('sizes');
|
|
78
|
+
if (image.src !== resolved) {
|
|
79
|
+
image.src = resolved;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Settle first, or a loading image has no currentSrc to pin it to; settle again,
|
|
85
|
+
// because pinning a <picture> starts a load of its own.
|
|
86
|
+
export async function prepareImagesForCapture(page) {
|
|
87
|
+
const stalled = await waitForImagesDecoded(page);
|
|
88
|
+
await freezeResponsiveImages(page);
|
|
89
|
+
await waitForImagesDecoded(page);
|
|
90
|
+
return stalled;
|
|
91
|
+
}
|
|
92
|
+
export async function restoreResponsiveImages(page) {
|
|
93
|
+
await page.evaluate(() => {
|
|
94
|
+
const put = (element, name, value) => null === value ? element.removeAttribute(name) : element.setAttribute(name, value);
|
|
95
|
+
document.querySelectorAll('img[data-toolkit-frozen]').forEach((image) => {
|
|
96
|
+
image
|
|
97
|
+
.closest('picture')
|
|
98
|
+
?.querySelectorAll('source[data-toolkit-frozen]')
|
|
99
|
+
.forEach((source) => {
|
|
100
|
+
const { media } = JSON.parse(source.dataset.toolkitFrozen ?? '{}');
|
|
101
|
+
put(source, 'media', media);
|
|
102
|
+
delete source.dataset.toolkitFrozen;
|
|
103
|
+
});
|
|
104
|
+
const frozen = JSON.parse(image.dataset.toolkitFrozen ?? '{}');
|
|
105
|
+
Object.entries(frozen).forEach(([name, value]) => put(image, name, value));
|
|
106
|
+
delete image.dataset.toolkitFrozen;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const DECODE_TIMEOUT = 15000;
|
|
111
|
+
/** @returns the sources of the images that never decoded, empty when all did */
|
|
112
|
+
export async function waitForImagesDecoded(page, timeout = DECODE_TIMEOUT) {
|
|
113
|
+
const deadline = Date.now() + timeout;
|
|
114
|
+
const stalled = await settleImages(page, timeout);
|
|
115
|
+
const left = deadline - Date.now();
|
|
116
|
+
// Out of budget, so a second round would settle nothing and report every image.
|
|
117
|
+
if (left <= 0) {
|
|
118
|
+
return stalled;
|
|
119
|
+
}
|
|
120
|
+
// A load that starts during a round is not waited for by it, which is how
|
|
121
|
+
// applyDeferredStylesheets re-selects a source after the image read as complete.
|
|
122
|
+
return settleImages(page, left);
|
|
123
|
+
}
|
|
124
|
+
function settleImages(page, timeout) {
|
|
125
|
+
return page.evaluate(async (timeout) => {
|
|
47
126
|
const images = Array.from(document.querySelectorAll('img'));
|
|
127
|
+
const stalled = new Set(images);
|
|
48
128
|
const settle = (image) => new Promise((resolve) => {
|
|
49
129
|
const finish = async () => {
|
|
50
130
|
await image.decode().catch(() => { });
|
|
131
|
+
stalled.delete(image);
|
|
51
132
|
resolve();
|
|
52
133
|
};
|
|
53
134
|
if (image.complete) {
|
|
@@ -55,13 +136,29 @@ async function waitForImagesDecoded(page, timeout = 15000) {
|
|
|
55
136
|
return;
|
|
56
137
|
}
|
|
57
138
|
image.addEventListener('load', () => void finish(), { once: true });
|
|
58
|
-
image.addEventListener('error', () =>
|
|
139
|
+
image.addEventListener('error', () => {
|
|
140
|
+
stalled.delete(image);
|
|
141
|
+
resolve();
|
|
142
|
+
}, { once: true });
|
|
59
143
|
});
|
|
60
144
|
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, timeout));
|
|
61
145
|
await Promise.race([Promise.all(images.map(settle)), timeoutPromise]);
|
|
62
146
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
147
|
+
// The snapshot above predates anything that started loading since.
|
|
148
|
+
document.querySelectorAll('img').forEach((image) => {
|
|
149
|
+
if (!image.complete) {
|
|
150
|
+
stalled.add(image);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return Array.from(stalled, (image) => image.currentSrc || image.src);
|
|
63
154
|
}, timeout);
|
|
64
155
|
}
|
|
156
|
+
/** An undecoded image is blank, and a first run writes that blank as the baseline. */
|
|
157
|
+
export function warnAboutUndecodedImages(stalled, timeout) {
|
|
158
|
+
console.warn(`[typo3-playwright-toolkit] ${stalled.length} ${1 === stalled.length ? 'image' : 'images'} ` +
|
|
159
|
+
`did not decode within ${timeout}ms and will be blank in the screenshot:\n ` +
|
|
160
|
+
stalled.join('\n '));
|
|
161
|
+
}
|
|
65
162
|
/**
|
|
66
163
|
* Finishes the `media="print"` async-CSS trick by hand. A stylesheet loaded as
|
|
67
164
|
* print and swapped to `all` on load is not applied while it is still print, so
|
|
@@ -113,11 +210,19 @@ export async function expectScreenshot(target, name, options = {}) {
|
|
|
113
210
|
await page.evaluate(() => document.fonts.ready);
|
|
114
211
|
await waitForAnimations(page, undefined, 3000);
|
|
115
212
|
await loadLazyElements(page);
|
|
116
|
-
await
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
213
|
+
const stalled = await prepareImagesForCapture(page);
|
|
214
|
+
if (stalled.length > 0) {
|
|
215
|
+
warnAboutUndecodedImages(stalled, DECODE_TIMEOUT);
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
// Playwright creates a missing reference itself and has --update-snapshots
|
|
219
|
+
// for the rest; hand-building the snapshot path got the platform suffix
|
|
220
|
+
// wrong off Linux.
|
|
221
|
+
await expect(shot).toHaveScreenshot(`${name}.png`, comparisonOptions(wholePage, screenshotOptions));
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
await restoreResponsiveImages(page);
|
|
225
|
+
}
|
|
121
226
|
}
|
|
122
227
|
export function comparisonOptions(wholePage, perCall) {
|
|
123
228
|
return {
|