@plan2net/typo3-playwright-toolkit 0.1.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 (164) hide show
  1. package/LICENSE +338 -0
  2. package/README.md +404 -0
  3. package/dist/builders/content-builder.d.ts +30 -0
  4. package/dist/builders/content-builder.d.ts.map +1 -0
  5. package/dist/builders/content-builder.js +67 -0
  6. package/dist/builders/content-builder.js.map +1 -0
  7. package/dist/builders/content-factory.d.ts +10 -0
  8. package/dist/builders/content-factory.d.ts.map +1 -0
  9. package/dist/builders/content-factory.js +23 -0
  10. package/dist/builders/content-factory.js.map +1 -0
  11. package/dist/builders/core-content.d.ts +127 -0
  12. package/dist/builders/core-content.d.ts.map +1 -0
  13. package/dist/builders/core-content.js +223 -0
  14. package/dist/builders/core-content.js.map +1 -0
  15. package/dist/builders/fields.d.ts +7 -0
  16. package/dist/builders/fields.d.ts.map +1 -0
  17. package/dist/builders/fields.js +20 -0
  18. package/dist/builders/fields.js.map +1 -0
  19. package/dist/builders/identifier.d.ts +2 -0
  20. package/dist/builders/identifier.d.ts.map +1 -0
  21. package/dist/builders/identifier.js +11 -0
  22. package/dist/builders/identifier.js.map +1 -0
  23. package/dist/builders/page-builder.d.ts +40 -0
  24. package/dist/builders/page-builder.d.ts.map +1 -0
  25. package/dist/builders/page-builder.js +119 -0
  26. package/dist/builders/page-builder.js.map +1 -0
  27. package/dist/builders/request-context.d.ts +23 -0
  28. package/dist/builders/request-context.d.ts.map +1 -0
  29. package/dist/builders/request-context.js +32 -0
  30. package/dist/builders/request-context.js.map +1 -0
  31. package/dist/checks/accessibility.d.ts +34 -0
  32. package/dist/checks/accessibility.d.ts.map +1 -0
  33. package/dist/checks/accessibility.js +90 -0
  34. package/dist/checks/accessibility.js.map +1 -0
  35. package/dist/checks/csp.d.ts +39 -0
  36. package/dist/checks/csp.d.ts.map +1 -0
  37. package/dist/checks/csp.js +179 -0
  38. package/dist/checks/csp.js.map +1 -0
  39. package/dist/checks/screenshot.d.ts +18 -0
  40. package/dist/checks/screenshot.d.ts.map +1 -0
  41. package/dist/checks/screenshot.js +125 -0
  42. package/dist/checks/screenshot.js.map +1 -0
  43. package/dist/config.d.ts +100 -0
  44. package/dist/config.d.ts.map +1 -0
  45. package/dist/config.js +68 -0
  46. package/dist/config.js.map +1 -0
  47. package/dist/contract.d.ts +9 -0
  48. package/dist/contract.d.ts.map +1 -0
  49. package/dist/contract.js +25 -0
  50. package/dist/contract.js.map +1 -0
  51. package/dist/global-setup.d.ts +20 -0
  52. package/dist/global-setup.d.ts.map +1 -0
  53. package/dist/global-setup.js +103 -0
  54. package/dist/global-setup.js.map +1 -0
  55. package/dist/global-teardown.d.ts +34 -0
  56. package/dist/global-teardown.d.ts.map +1 -0
  57. package/dist/global-teardown.js +269 -0
  58. package/dist/global-teardown.js.map +1 -0
  59. package/dist/http/api-secret.d.ts +11 -0
  60. package/dist/http/api-secret.d.ts.map +1 -0
  61. package/dist/http/api-secret.js +27 -0
  62. package/dist/http/api-secret.js.map +1 -0
  63. package/dist/http/cleanup-client.d.ts +26 -0
  64. package/dist/http/cleanup-client.d.ts.map +1 -0
  65. package/dist/http/cleanup-client.js +91 -0
  66. package/dist/http/cleanup-client.js.map +1 -0
  67. package/dist/http/off-site-headers.d.ts +13 -0
  68. package/dist/http/off-site-headers.d.ts.map +1 -0
  69. package/dist/http/off-site-headers.js +27 -0
  70. package/dist/http/off-site-headers.js.map +1 -0
  71. package/dist/http/prepare-context.d.ts +4 -0
  72. package/dist/http/prepare-context.d.ts.map +1 -0
  73. package/dist/http/prepare-context.js +6 -0
  74. package/dist/http/prepare-context.js.map +1 -0
  75. package/dist/http/record-edit.d.ts +37 -0
  76. package/dist/http/record-edit.d.ts.map +1 -0
  77. package/dist/http/record-edit.js +86 -0
  78. package/dist/http/record-edit.js.map +1 -0
  79. package/dist/http/toolkit-request.d.ts +8 -0
  80. package/dist/http/toolkit-request.d.ts.map +1 -0
  81. package/dist/http/toolkit-request.js +45 -0
  82. package/dist/http/toolkit-request.js.map +1 -0
  83. package/dist/index.d.ts +18 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +14 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/inspect/cli.d.ts +3 -0
  88. package/dist/inspect/cli.d.ts.map +1 -0
  89. package/dist/inspect/cli.js +36 -0
  90. package/dist/inspect/cli.js.map +1 -0
  91. package/dist/inspect/links.d.ts +10 -0
  92. package/dist/inspect/links.d.ts.map +1 -0
  93. package/dist/inspect/links.js +51 -0
  94. package/dist/inspect/links.js.map +1 -0
  95. package/dist/inspect/token.d.ts +5 -0
  96. package/dist/inspect/token.d.ts.map +1 -0
  97. package/dist/inspect/token.js +15 -0
  98. package/dist/inspect/token.js.map +1 -0
  99. package/dist/playwright/base-config.d.ts +4 -0
  100. package/dist/playwright/base-config.d.ts.map +1 -0
  101. package/dist/playwright/base-config.js +34 -0
  102. package/dist/playwright/base-config.js.map +1 -0
  103. package/dist/playwright/index.d.ts +3 -0
  104. package/dist/playwright/index.d.ts.map +1 -0
  105. package/dist/playwright/index.js +3 -0
  106. package/dist/playwright/index.js.map +1 -0
  107. package/dist/scenario.d.ts +46 -0
  108. package/dist/scenario.d.ts.map +1 -0
  109. package/dist/scenario.js +139 -0
  110. package/dist/scenario.js.map +1 -0
  111. package/dist/state/attempt-claim.d.ts +8 -0
  112. package/dist/state/attempt-claim.d.ts.map +1 -0
  113. package/dist/state/attempt-claim.js +47 -0
  114. package/dist/state/attempt-claim.js.map +1 -0
  115. package/dist/state/attempt-registry.d.ts +36 -0
  116. package/dist/state/attempt-registry.d.ts.map +1 -0
  117. package/dist/state/attempt-registry.js +68 -0
  118. package/dist/state/attempt-registry.js.map +1 -0
  119. package/dist/state/ensure-state.d.ts +42 -0
  120. package/dist/state/ensure-state.d.ts.map +1 -0
  121. package/dist/state/ensure-state.js +169 -0
  122. package/dist/state/ensure-state.js.map +1 -0
  123. package/dist/state/json-safe.d.ts +9 -0
  124. package/dist/state/json-safe.d.ts.map +1 -0
  125. package/dist/state/json-safe.js +18 -0
  126. package/dist/state/json-safe.js.map +1 -0
  127. package/dist/state/run-id.d.ts +6 -0
  128. package/dist/state/run-id.d.ts.map +1 -0
  129. package/dist/state/run-id.js +28 -0
  130. package/dist/state/run-id.js.map +1 -0
  131. package/dist/state/run-namespace.d.ts +33 -0
  132. package/dist/state/run-namespace.d.ts.map +1 -0
  133. package/dist/state/run-namespace.js +136 -0
  134. package/dist/state/run-namespace.js.map +1 -0
  135. package/dist/state/safe-paths.d.ts +10 -0
  136. package/dist/state/safe-paths.d.ts.map +1 -0
  137. package/dist/state/safe-paths.js +56 -0
  138. package/dist/state/safe-paths.js.map +1 -0
  139. package/dist/state/scenario-outcome.d.ts +9 -0
  140. package/dist/state/scenario-outcome.d.ts.map +1 -0
  141. package/dist/state/scenario-outcome.js +23 -0
  142. package/dist/state/scenario-outcome.js.map +1 -0
  143. package/dist/state/scenario-state.d.ts +41 -0
  144. package/dist/state/scenario-state.d.ts.map +1 -0
  145. package/dist/state/scenario-state.js +50 -0
  146. package/dist/state/scenario-state.js.map +1 -0
  147. package/dist/state/setup-lock.d.ts +22 -0
  148. package/dist/state/setup-lock.d.ts.map +1 -0
  149. package/dist/state/setup-lock.js +90 -0
  150. package/dist/state/setup-lock.js.map +1 -0
  151. package/dist/types/common.d.ts +17 -0
  152. package/dist/types/common.d.ts.map +1 -0
  153. package/dist/types/common.js +2 -0
  154. package/dist/types/common.js.map +1 -0
  155. package/dist/types/content-builder.d.ts +16 -0
  156. package/dist/types/content-builder.d.ts.map +1 -0
  157. package/dist/types/content-builder.js +2 -0
  158. package/dist/types/content-builder.js.map +1 -0
  159. package/dist/types/playwright-extensions.d.ts +8 -0
  160. package/dist/types/playwright-extensions.d.ts.map +1 -0
  161. package/dist/types/playwright-extensions.js +2 -0
  162. package/dist/types/playwright-extensions.js.map +1 -0
  163. package/package.json +88 -0
  164. package/tsconfig.base.json +13 -0
package/README.md ADDED
@@ -0,0 +1,404 @@
1
+ <p align="center">
2
+ <img src="../playwright-toolkit/Resources/Public/Icons/Extension.svg" alt="" width="96" height="96">
3
+ </p>
4
+ <h1 align="center">@plan2net/typo3-playwright-toolkit</h1>
5
+ <p align="center"><em>Playwright fixtures and content builders for TYPO3, one test database per test file.</em></p>
6
+ <br>
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.56%2B-2ead33)](https://playwright.dev)
11
+ [![licence](https://img.shields.io/badge/licence-GPL--2.0--or--later-blue)](LICENSE)
12
+
13
+ An npm package. It provides the Playwright fixtures, the content builders that
14
+ create TYPO3 records, and the accessibility and CSP checks.
15
+
16
+ It needs the [Composer extension](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/playwright-toolkit), which creates the test
17
+ databases, and the [DDEV add-on](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/ddev-typo3-playwright-toolkit), which provides
18
+ the database service and the commands.
19
+
20
+ [Requirements](#requirements) · [Install](#install) · [Configure](#configure) ·
21
+ [Using the package](#using-the-package) · [Reference](#reference) ·
22
+ [Troubleshooting](#troubleshooting) · [Related packages](#related-packages)
23
+
24
+ ## Requirements
25
+
26
+ - Node 22.12 or newer
27
+ - `@playwright/test` 1.56 or newer
28
+ - The Composer extension, installed in the same project
29
+
30
+ You do not need a database client. This package never talks to the database; it asks
31
+ the extension to create and delete them. That is why PHP and Node may run in
32
+ different containers.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ npm i -D @plan2net/typo3-playwright-toolkit @playwright/test
38
+ ```
39
+
40
+ Add a `tsconfig.json` next to your tests that extends the one shipped here:
41
+
42
+ ```json
43
+ {
44
+ "extends": "@plan2net/typo3-playwright-toolkit/tsconfig.base.json",
45
+ "include": ["**/*.ts"]
46
+ }
47
+ ```
48
+
49
+ Important: this package is ESM and needs `"moduleResolution": "NodeNext"`. Without
50
+ it TypeScript cannot read the package's exports, so `page` and `state` in your tests
51
+ type as `any` and you lose every check your editor could give you.
52
+
53
+ ## Configure
54
+
55
+ Call `defineToolkitConfig(...)` at the top of your `playwright.config.ts`, before any
56
+ other file from this package is loaded. Two values are enough:
57
+
58
+ ```ts
59
+ // <project>/tests/playwright/playwright.config.ts
60
+ import { fileURLToPath } from 'url'
61
+ import { defineToolkitConfig, defineBasePlaywrightConfig } from '@plan2net/typo3-playwright-toolkit/playwright'
62
+
63
+ const toolkitConfig = defineToolkitConfig({
64
+ testingURL: 'https://example-testing.test',
65
+ paths: { consumerRoot: fileURLToPath(new URL('../..', import.meta.url)) },
66
+ })
67
+
68
+ export default defineBasePlaywrightConfig(toolkitConfig, {
69
+ testDir: './tests',
70
+ })
71
+ ```
72
+
73
+ `testingURL` is the host name that runs in the Testing context. It is the only URL
74
+ this package uses, because the test database exists nowhere else.
75
+
76
+ `consumerRoot` is the root of your TYPO3 project. The package derives the state and
77
+ session folders from it, so it works from `node_modules` without knowing its own
78
+ location. Count the `..` from where your config actually sits — the example assumes
79
+ `<project>/tests/playwright/`, so it climbs two levels. Pointing it at the wrong
80
+ directory is not an error: the run works and writes its state somewhere you did not
81
+ expect. Use `fileURLToPath` rather than `new URL(...).pathname`, which
82
+ percent-encodes a project path containing spaces or non-ASCII characters.
83
+
84
+ Important: `defineToolkitConfig` must run before the first import from this package.
85
+ Everything else reads the values it stores.
86
+
87
+ ## Using the package
88
+
89
+ ### Writing a test
90
+
91
+ One test file is one scenario: a setup function that creates content through the real
92
+ TYPO3 backend, and the tests that read it.
93
+
94
+ ```ts
95
+ // tests/my-feature.spec.ts
96
+ import { defineScenario, expect } from '@plan2net/typo3-playwright-toolkit'
97
+
98
+ const test = defineScenario(async ({ builders }) => {
99
+ const { id, slug } = await builders
100
+ .page()
101
+ .withTitle('My Page')
102
+ .withSlug('/my-page')
103
+ .create()
104
+
105
+ await builders
106
+ .content()
107
+ .onPage(id)
108
+ .ofType('textmedia')
109
+ .configure((element) => element.withHeader('Hello').withBodyText('<p>Copy.</p>'))
110
+ .create()
111
+
112
+ return { slug }
113
+ })
114
+
115
+ test('renders the page title', async ({ page, state }) => {
116
+ await page.goto(state.slug)
117
+
118
+ await expect(page.locator('h1')).toHaveText('My Page')
119
+ })
120
+ ```
121
+
122
+ `state` is whatever your setup returned. The first test that needs it runs the
123
+ setup, and the other tests wait. If the setup fails, the tests are skipped with the
124
+ reason instead of failing because content is missing.
125
+
126
+ The setup runs once per file, even when the file runs in several browser projects.
127
+ The other projects use the state and the test database that the first one created.
128
+
129
+ ```mermaid
130
+ flowchart TD
131
+ F["one test file"]
132
+ S["setup — runs once<br>builds content through the backend"]
133
+ D["state + its own test database"]
134
+ X["tests skip, with the reason"]
135
+ P1["chromium-desktop"]
136
+ P2["firefox"]
137
+ P3["webkit"]
138
+
139
+ F --> S
140
+ S -->|built| D
141
+ S -->|failed| X
142
+ D --> P1 & P2 & P3
143
+
144
+ classDef ok fill:#2ea04326,stroke:#2ea043b3
145
+ classDef bad fill:#f8514926,stroke:#f85149b3
146
+ class D ok
147
+ class X bad
148
+ ```
149
+
150
+ Besides `builders`, the setup receives `testId` (the database this attempt runs
151
+ against), `attempt` (`1` on the first try), `signal` (aborted when the attempt times
152
+ out, so pass it to your own long requests), and a `page` and `request` that already
153
+ carry the toolkit headers. Your tests get `testId` beside `state`, which is the
154
+ database name without its `db` prefix and what the backend shows in brackets behind
155
+ the site name:
156
+
157
+ ```ts
158
+ test('reports which database it used', async ({ page, state, testId }) => {
159
+ await page.goto(state.slug)
160
+
161
+ console.log(`built in db${testId}`)
162
+ })
163
+ ```
164
+
165
+ ### Your own content types
166
+
167
+ Every CType of a normal TYPO3 installation already has a builder. For your own,
168
+ extend `CoreContent` to get the shared setters, then register the class:
169
+
170
+ ```ts
171
+ import { CoreContent } from '@plan2net/typo3-playwright-toolkit'
172
+
173
+ export class ArticleContent extends CoreContent {
174
+ readonly type = 'article'
175
+
176
+ withTeaser(text: string): this {
177
+ return this.withField('tx_myext_teaser', text)
178
+ }
179
+ }
180
+ ```
181
+
182
+ ```ts
183
+ // playwright.config.ts
184
+ contentTypes: { article: ArticleContent }
185
+ ```
186
+
187
+ To get typed setters in `.configure()`, add the class to `ContentTypeMap`:
188
+
189
+ ```ts
190
+ declare module '@plan2net/typo3-playwright-toolkit' {
191
+ interface ContentTypeMap {
192
+ article: ArticleContent
193
+ }
194
+ }
195
+ ```
196
+
197
+ A key with the name of a core CType replaces the built-in builder, and the other
198
+ core types stay as they are. That works for the type too: `ContentTypeMap` starts
199
+ empty, so your `text: MyTextContent` is what `.configure()` hands you.
200
+
201
+ ### Calling the site directly
202
+
203
+ The `request` client — the one `defineScenario` hands your setup, and the `request`
204
+ fixture in your tests — carries the test ID for `testingURL`, so it reads and
205
+ writes the same throwaway database the browser does. Requests to any other host
206
+ get neither toolkit header.
207
+
208
+ ### Stubbing a third-party script
209
+
210
+ A cookie banner or a tracking script can swallow the clicks your test makes.
211
+ `prepareContext` runs on every context a test uses, after the toolkit's own
212
+ routes are in place:
213
+
214
+ ```ts
215
+ // playwright.config.ts
216
+ prepareContext: async (context) => {
217
+ await context.route('**/vendor-widget.js', (route) => route.fulfill({ body: '' }))
218
+ },
219
+ ```
220
+
221
+ ### Screenshots
222
+
223
+ `takeScreenshot` waits for fonts, images and animations, hides the selectors from
224
+ `hideBeforeScreenshot`, and then compares against the stored image.
225
+
226
+ ```ts
227
+ import { takeScreenshot } from '@plan2net/typo3-playwright-toolkit'
228
+
229
+ await takeScreenshot(page, 'my-page') // the whole page
230
+ await takeScreenshot(page, 'accordion', { include: '.accordion' }) // one element
231
+ ```
232
+
233
+ The name carries no file extension; Playwright adds `.png` and the platform suffix
234
+ itself. The first run writes the missing image and fails, as Playwright always does.
235
+ Every other option is passed on to `toHaveScreenshot`.
236
+
237
+ `takeScreenshot` waits for animations itself. When you interact and then assert
238
+ without a screenshot — an accessibility scan after opening an accordion, say — wait
239
+ first:
240
+
241
+ ```ts
242
+ import { waitForAnimations } from '@plan2net/typo3-playwright-toolkit'
243
+
244
+ await page.locator('summary').first().click()
245
+ await waitForAnimations(page, '.accordion')
246
+ await runAccessibilityScan(page, { include: '.accordion' })
247
+ ```
248
+
249
+ ### Accessibility checks
250
+
251
+ `runAccessibilityScan` checks the current page with axe and fails the test if it
252
+ finds a problem.
253
+
254
+ ```ts
255
+ import { runAccessibilityScan } from '@plan2net/typo3-playwright-toolkit'
256
+
257
+ await runAccessibilityScan(page) // the whole page
258
+ await runAccessibilityScan(page, { include: '.card' }) // one component
259
+ await runAccessibilityScan(page, { exclude: '#ads' })
260
+ await runAccessibilityScan(page, { disabledRules: ['color-contrast'] })
261
+ ```
262
+
263
+ Important: the test also fails when the check ran no rules at all. An empty area
264
+ would otherwise pass without testing anything.
265
+
266
+ A check with `include` turns off the `heading-order` rule, because axe compares the
267
+ headings of a part of the page with the structure of the whole page. Checks of a
268
+ whole page do test the heading structure.
269
+
270
+ To check every test without writing the call each time, turn it on in the config:
271
+
272
+ ```ts
273
+ accessibility: { auto: true },
274
+ ```
275
+
276
+ Every test that opened a page is then scanned after it finishes. A test that never
277
+ navigated is skipped, because there is nothing to check, and a test that already
278
+ failed is skipped too, so the first failure stays the one you read.
279
+
280
+ `scanAccessibility(page, options)` returns the raw axe result instead of failing the
281
+ test. It returns `null` when the current project does not run checks.
282
+
283
+ ### CSP violations
284
+
285
+ `CspVerifier` collects policy violations from your own pages and fails the test if
286
+ there were any.
287
+
288
+ ```ts
289
+ import { CspVerifier } from '@plan2net/typo3-playwright-toolkit'
290
+
291
+ const verifier = new CspVerifier(context)
292
+ await verifier.install() // before the first page is opened
293
+
294
+ await page.goto('/')
295
+
296
+ await verifier.assertNoViolations(testInfo) // testInfo is optional
297
+ ```
298
+
299
+ Important: `install()` must run before anything is loaded. A page that is already
300
+ open reports nothing.
301
+
302
+ The test also fails if pages were requested but none of your own pages ever arrived,
303
+ so a navigation that never loaded cannot pass. Pass `testInfo` to attach the full
304
+ report as a JSON file when the test fails.
305
+
306
+ ## Reference
307
+
308
+ ### Configuration options
309
+
310
+ Required:
311
+
312
+ | Name | Purpose |
313
+ |---|---|
314
+ | `paths.consumerRoot` | Absolute path to your TYPO3 project root |
315
+ | `testingURL` | Bare origin that runs in the Testing context |
316
+
317
+ Everything else:
318
+
319
+ | Name | Default | Purpose |
320
+ |---|---|---|
321
+ | `accessibility.auto` | `false` | Scan after every test that opened a page, instead of calling `runAccessibilityScan` yourself |
322
+ | `accessibility.disabledRules` | `[]` | axe rules turned off for the whole project |
323
+ | `accessibility.projects` | all projects | Projects that run axe checks |
324
+ | `accessibility.tags` | `DEFAULT_SCAN_TAGS` | Which axe rule sets to run |
325
+ | `cleanup.failOnLeak` | true in CI | Fail the run if a test database could not be deleted |
326
+ | `cleanup.orphanAgeMs` | 24 hours | How old a leftover database must be before cleanup deletes it |
327
+ | `cleanup.preserveOnFailure` | `failed` | Which test databases to keep after a failure: `failed`, `all` or `none` |
328
+ | `contentTypes` | `{}` | Your own content builders, one per CType |
329
+ | `csp.expectedOrigin` | origin of `testingURL` | Whose violations count |
330
+ | `csp.mode` | `any` | Which policy header a page must send: `any`, `report-only` or `enforced` |
331
+ | `hideBeforeScreenshot` | `[]` | CSS selectors hidden before every screenshot |
332
+ | `paths.sessionDir` | `<consumerRoot>/var/session` | TYPO3 session folder, cleaned after a run |
333
+ | `paths.stateDir` | `<consumerRoot>/.test-state` | Folder for setup state |
334
+ | `prepareContext` | none | Runs on every context a test uses, for your own routes and stubs |
335
+ | `screenshot.maxDiffPixelRatio` | `0.005` | How many pixels may differ, as a share of the image |
336
+ | `screenshot.threshold` | `0.2` | How different one pixel may be, from 0 to 1 |
337
+ | `setup.attemptTimeoutMs` | `90000` | How long one setup attempt may take |
338
+ | `setup.attempts` | `2` | Attempts per setup, so `2` means one retry |
339
+ | `setup.lockStaleMs` | `15000` | Silence after which another worker may take over the setup |
340
+ | `setup.pollMs` | `100` | Gap between polls while waiting for a scenario |
341
+ | `setup.waitTimeoutMs` | `300000` | How long a test waits for its scenario in total, lock included |
342
+
343
+ `defineBasePlaywrightConfig` sets Playwright's `baseURL` to `testingURL` and adds
344
+ this package's setup and cleanup functions. Values in its second argument win, and
345
+ `use` and `expect` are merged instead of replaced.
346
+
347
+ ### Content types
348
+
349
+ Every CType of a normal TYPO3 installation has a builder, and you register nothing:
350
+ `header`, `text`, `textmedia`, `textpic`, `image`, `bullets`, `table`, `uploads`,
351
+ `html`, `div`, `shortcut`, and the eleven `menu_*` types.
352
+
353
+ All builders share `withHeader`, `withSubheader`, `withHeaderLayout`,
354
+ `withHeaderLink`, `withColPos`, `setHidden`, and `withField(column, value)` for any
355
+ other TCA column. Types with images add `withFile` and `withFiles`, which write the
356
+ `sys_file_reference` rows for you, plus `withColumns`, `withOrientation` and
357
+ `withImageSize`.
358
+
359
+ ### The inspect command
360
+
361
+ The package installs `typo3-playwright-inspect`. Run it from your project root
362
+ after a failed run to print a backend link for every test database that was kept,
363
+ optionally filtered by a part of the test file name:
364
+
365
+ ```bash
366
+ npx typo3-playwright-inspect # every kept database
367
+ npx typo3-playwright-inspect accordion # only matching test files
368
+ ```
369
+
370
+ It reads the API secret from `var/playwright/api-secret` or from
371
+ `PLAYWRIGHT_TOOLKIT_SECRET`. On DDEV, `ddev playwright-inspect` wraps it. The links
372
+ log in as the pre-seeded backend user and live 15 minutes.
373
+
374
+ ## Troubleshooting
375
+
376
+ **"No test ID for this request".** The builder received a page that the toolkit
377
+ fixtures did not create. Use the `builders` argument of `defineScenario`.
378
+
379
+ **Settings seem to be ignored.** `defineToolkitConfig` ran too late. It must be the
380
+ first thing in `playwright.config.ts`.
381
+
382
+ **Cleanup refuses a path.** `stateDir` and `sessionDir` must be absolute and inside
383
+ `consumerRoot`, because cleanup deletes files in both.
384
+
385
+ **"Timed out after 300000ms waiting for the setup".** A setup that builds a lot of
386
+ content can outgrow the defaults on a slow machine. Raise `setup.attemptTimeoutMs`
387
+ for the setup itself and `setup.waitTimeoutMs` for the tests waiting on it — the
388
+ second has to stay comfortably above the first, since it covers every attempt plus
389
+ the time spent waiting for the lock.
390
+
391
+ **Test databases stay after a failed run.** That is intended: the databases of the
392
+ failed test files are kept for debugging, and the run prints a link for each one
393
+ that opens it in the TYPO3 backend. Set `cleanup.preserveOnFailure` to `none` to
394
+ turn this off.
395
+
396
+ **You want to look at what a failing test built.** Open the link the run printed
397
+ under "Kept for debugging". It logs you into that test's backend, and the frontend
398
+ is reachable from there. The link is signed with the API secret and lives 15
399
+ minutes.
400
+
401
+ ## Related packages
402
+
403
+ - [`plan2net/playwright-toolkit`](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/playwright-toolkit) — Composer extension
404
+ - [DDEV add-on](https://github.com/plan2net/typo3-playwright-toolkit/tree/main/packages/ddev-typo3-playwright-toolkit) — database service and commands
@@ -0,0 +1,30 @@
1
+ import { Page } from '@playwright/test';
2
+ import { ContentBuilderInterface } from '../types/content-builder.js';
3
+ import type { ContentTypeFor, ContentTypeKey } from './core-content.js';
4
+ import { type RequestContext } from './request-context.js';
5
+ export declare class ContentBuilder {
6
+ private page;
7
+ private pageId;
8
+ private readonly requestContext?;
9
+ constructor(page: Page, requestContext?: Partial<RequestContext>);
10
+ onPage(pageId: string): this;
11
+ /**
12
+ * A key of ContentTypeMap hands `.configure()` that builder's own type, so
13
+ * the setters autocomplete; any other string falls back to the bare interface.
14
+ */
15
+ ofType<K extends ContentTypeKey>(type: K): TypedContentBuilder<ContentTypeFor<K>>;
16
+ ofType(type: string): TypedContentBuilder<ContentBuilderInterface>;
17
+ }
18
+ declare class TypedContentBuilder<B extends ContentBuilderInterface = ContentBuilderInterface> {
19
+ private page;
20
+ private pageId;
21
+ private requestContext?;
22
+ private builder;
23
+ constructor(page: Page, pageId: string, type: string, requestContext?: Partial<RequestContext> | undefined);
24
+ configure(fn: (builder: B) => void): this;
25
+ create(): Promise<{
26
+ id: string;
27
+ }>;
28
+ }
29
+ export {};
30
+ //# sourceMappingURL=content-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-builder.d.ts","sourceRoot":"","sources":["../../src/builders/content-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AACvC,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAA;AAErE,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAGvE,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGjF,qBAAa,cAAc;IACvB,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAyB;gBAE7C,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAKhE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK5B;;;OAGG;IACH,MAAM,CAAC,CAAC,SAAS,cAAc,EAAE,IAAI,EAAE,CAAC,GAAG,mBAAmB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;IACjF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAAC,uBAAuB,CAAC;CAWrE;AAED,cAAM,mBAAmB,CAAC,CAAC,SAAS,uBAAuB,GAAG,uBAAuB;IAI7E,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,MAAM;IAEd,OAAO,CAAC,cAAc,CAAC;IAN3B,OAAO,CAAC,OAAO,CAAyB;gBAG5B,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,MAAM,EACtB,IAAI,EAAE,MAAM,EACJ,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,YAAA;IAKpD,SAAS,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,GAAG,IAAI;IAKnC,MAAM,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CAiC1C"}
@@ -0,0 +1,67 @@
1
+ import { createContent } from './content-factory.js';
2
+ import { saveRecord } from '../http/record-edit.js';
3
+ import { coerceFields } from './fields.js';
4
+ import { resolveRequestContext } from './request-context.js';
5
+ import { newRecordIdentifier } from './identifier.js';
6
+ export class ContentBuilder {
7
+ page;
8
+ pageId = '';
9
+ requestContext;
10
+ constructor(page, requestContext) {
11
+ this.page = page;
12
+ this.requestContext = requestContext;
13
+ }
14
+ onPage(pageId) {
15
+ this.pageId = pageId;
16
+ return this;
17
+ }
18
+ ofType(type) {
19
+ if (!this.pageId) {
20
+ throw new Error(`[typo3-playwright-toolkit] Call .onPage(pageId) before .ofType('${type}') — ` +
21
+ 'without a page the content element has nowhere to live.');
22
+ }
23
+ return new TypedContentBuilder(this.page, this.pageId, type, this.requestContext);
24
+ }
25
+ }
26
+ class TypedContentBuilder {
27
+ page;
28
+ pageId;
29
+ requestContext;
30
+ builder;
31
+ constructor(page, pageId, type, requestContext) {
32
+ this.page = page;
33
+ this.pageId = pageId;
34
+ this.requestContext = requestContext;
35
+ this.builder = createContent(type);
36
+ }
37
+ configure(fn) {
38
+ fn(this.builder);
39
+ return this;
40
+ }
41
+ async create() {
42
+ const context = resolveRequestContext(this.page, this.requestContext);
43
+ const identifier = newRecordIdentifier();
44
+ const fields = this.builder.getFields();
45
+ // CType and colPos are set below; an empty string means "the type did not
46
+ // set it".
47
+ const own = Object.fromEntries(Object.entries(fields).filter(([column, value]) => value !== '' && !['CType', 'colPos'].includes(column)));
48
+ const record = {
49
+ pid: this.pageId,
50
+ sys_language_uid: 0,
51
+ CType: fields.CType || this.builder.type,
52
+ colPos: fields.colPos ?? 0,
53
+ ...coerceFields(own),
54
+ };
55
+ const uid = await saveRecord(this.page.request, context, {
56
+ table: 'tt_content',
57
+ identifier: identifier,
58
+ target: Number(this.pageId),
59
+ data: {
60
+ tt_content: { [identifier]: record },
61
+ ...(this.builder.getAdditionalRecords?.(identifier, this.pageId) ?? {}),
62
+ },
63
+ });
64
+ return { id: String(uid) };
65
+ }
66
+ }
67
+ //# sourceMappingURL=content-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-builder.js","sourceRoot":"","sources":["../../src/builders/content-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { ContentBuilderInterface } from '../types/content-builder.js';
2
+ import type { ContentTypeConstructor } from '../config.js';
3
+ /**
4
+ * Core CTypes are always registered. A consumer key with the same name replaces
5
+ * the shipped builder. Each call replaces the whole set, so calling
6
+ * defineToolkitConfig twice gives the same result as calling it once.
7
+ */
8
+ export declare function registerContentTypes(types: Record<string, ContentTypeConstructor>): void;
9
+ export declare function createContent(type: string): ContentBuilderInterface;
10
+ //# sourceMappingURL=content-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-factory.d.ts","sourceRoot":"","sources":["../../src/builders/content-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAA;AAC1E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAK1D;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,GAAG,IAAI,CAKxF;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,uBAAuB,CAWnE"}
@@ -0,0 +1,23 @@
1
+ import { coreContentTypes } from './core-content.js';
2
+ const registry = new Map();
3
+ /**
4
+ * Core CTypes are always registered. A consumer key with the same name replaces
5
+ * the shipped builder. Each call replaces the whole set, so calling
6
+ * defineToolkitConfig twice gives the same result as calling it once.
7
+ */
8
+ export function registerContentTypes(types) {
9
+ registry.clear();
10
+ for (const [type, constructor] of Object.entries({ ...coreContentTypes(), ...types })) {
11
+ registry.set(type, constructor);
12
+ }
13
+ }
14
+ export function createContent(type) {
15
+ const Constructor = registry.get(type) ?? coreContentTypes()[type];
16
+ if (!Constructor) {
17
+ throw new Error(`[typo3-playwright-toolkit] Content type "${type}" is not registered. ` +
18
+ `Core CTypes ship with the toolkit; add your own to contentTypes in ` +
19
+ `defineToolkitConfig(...). Registered: [${[...registry.keys()].sort().join(', ')}].`);
20
+ }
21
+ return new Constructor();
22
+ }
23
+ //# sourceMappingURL=content-factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-factory.js","sourceRoot":"","sources":["../../src/builders/content-factory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEpD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkC,CAAA;AAE1D;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAA6C;IAC9E,QAAQ,CAAC,KAAK,EAAE,CAAA;IAChB,KAAK,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,gBAAgB,EAAE,EAAE,GAAG,KAAK,EAAE,CAAC,EAAE,CAAC;QACpF,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;IACnC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACtC,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,gBAAgB,EAAE,CAAC,IAAI,CAAC,CAAA;IAClE,IAAI,CAAC,WAAW,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACX,4CAA4C,IAAI,uBAAuB;YACnE,qEAAqE;YACrE,0CAA0C,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAC3F,CAAA;IACL,CAAC;IAED,OAAO,IAAI,WAAW,EAAE,CAAA;AAC5B,CAAC"}