@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.
Files changed (45) hide show
  1. package/README.md +144 -50
  2. package/dist/build.d.ts +8 -0
  3. package/dist/build.d.ts.map +1 -0
  4. package/dist/build.js +73 -0
  5. package/dist/build.js.map +1 -0
  6. package/dist/checks/screenshot.d.ts +7 -0
  7. package/dist/checks/screenshot.d.ts.map +1 -1
  8. package/dist/checks/screenshot.js +113 -8
  9. package/dist/checks/screenshot.js.map +1 -1
  10. package/dist/clean/cli.d.ts +3 -0
  11. package/dist/clean/cli.d.ts.map +1 -0
  12. package/dist/clean/cli.js +32 -0
  13. package/dist/clean/cli.js.map +1 -0
  14. package/dist/config.d.ts +6 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/doctor/cli.d.ts +3 -0
  18. package/dist/doctor/cli.d.ts.map +1 -0
  19. package/dist/doctor/cli.js +41 -0
  20. package/dist/doctor/cli.js.map +1 -0
  21. package/dist/doctor/reporter.d.ts +22 -0
  22. package/dist/doctor/reporter.d.ts.map +1 -0
  23. package/dist/doctor/reporter.js +223 -0
  24. package/dist/doctor/reporter.js.map +1 -0
  25. package/dist/global-setup.d.ts +18 -0
  26. package/dist/global-setup.d.ts.map +1 -1
  27. package/dist/global-setup.js +9 -3
  28. package/dist/global-setup.js.map +1 -1
  29. package/dist/http/api-secret.js +1 -1
  30. package/dist/http/record-edit.d.ts.map +1 -1
  31. package/dist/http/record-edit.js +7 -6
  32. package/dist/http/record-edit.js.map +1 -1
  33. package/dist/inspect/cli.js +2 -2
  34. package/dist/inspect/links.d.ts +2 -0
  35. package/dist/inspect/links.d.ts.map +1 -1
  36. package/dist/inspect/links.js +8 -0
  37. package/dist/inspect/links.js.map +1 -1
  38. package/dist/playwright/base-config.d.ts.map +1 -1
  39. package/dist/playwright/base-config.js +3 -0
  40. package/dist/playwright/base-config.js.map +1 -1
  41. package/dist/state/run-namespace.d.ts +2 -0
  42. package/dist/state/run-namespace.d.ts.map +1 -1
  43. package/dist/state/run-namespace.js +1 -1
  44. package/dist/state/run-namespace.js.map +1 -1
  45. 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
- [![npm](https://img.shields.io/npm/v/@plan2net/typo3-playwright-toolkit)](https://www.npmjs.com/package/@plan2net/typo3-playwright-toolkit)
9
- [![Node](https://img.shields.io/badge/Node-22.12%2B-5fa04e)](https://nodejs.org)
10
- [![Playwright](https://img.shields.io/badge/Playwright-1.44%2B-2ead33)](https://playwright.dev)
11
- [![licence](https://img.shields.io/badge/licence-GPL--2.0--or--later-blue)](LICENSE)
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 [Composer extension](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/playwright-toolkit), which creates the test
18
- databases, and the [DDEV add-on](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/ddev-typo3-playwright-toolkit), which provides
19
- the database service and the commands.
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 actually sits the example assumes
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: the run works and writes its state somewhere you did not
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 a translation and a name already in use are two cases, and an extension
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 actually wrote, and warns when that is more
153
- than you asked for:
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 the save succeeded. It is where a slow setup spends its time, and
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 now prints what TYPO3 wrote to its log while that test ran, under the
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
- Before, the save looked fine and the test failed later, somewhere unrelated.
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`, which is the
215
- database name without its `db` prefix and what the backend shows in brackets behind
216
- the site name:
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()` `batch` saves them. They land on the page in
338
- the order you list them, you get a uid for each, and their files and child records come
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 one
342
- another and that means nothing across pages. They also cannot point at each other: a
343
- relation needs a uid, and only a save hands one back. Create what is pointed at first,
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 for
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 the one `defineScenario` hands your setup, and the `request`
364
- fixture in your tests carries the test ID for `testingURL`, so it reads and
365
- writes the same throwaway database the browser does. Requests to any other host
366
- get neither toolkit header.
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
- routes are in place:
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 an accessibility scan after opening an accordion, say — wait
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()` the form posts one field per value, not one for the
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-inspect` wraps it. The links
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 it mints a new
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-replay` sets it, rebuilds that database from the
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 it under the
626
- fixture root, slugs keep no test-ID suffix, setups run once with no retry, the tests
627
- themselves are skipped, and teardown drops nothing.
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 — the
643
- second has to stay comfortably above the first, since it covers every attempt plus
644
- the time spent waiting for the lock.
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) Composer extension
659
- - [DDEV add-on](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/ddev-typo3-playwright-toolkit) database service and commands
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
@@ -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;AA6DD,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,CAuBf;AAED,wBAAgB,iBAAiB,CAC7B,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,2BAA2B,GACrC,2BAA2B,CAO7B"}
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
- async function waitForImagesDecoded(page, timeout = 15000) {
46
- await page.evaluate(async (timeout) => {
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', () => resolve(), { once: true });
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 waitForImagesDecoded(page);
117
- // Playwright creates a missing reference itself and has --update-snapshots
118
- // for the rest; hand-building the snapshot path got the platform suffix
119
- // wrong off Linux.
120
- await expect(shot).toHaveScreenshot(`${name}.png`, comparisonOptions(wholePage, screenshotOptions));
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 {