@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.
- package/LICENSE +338 -0
- package/README.md +404 -0
- package/dist/builders/content-builder.d.ts +30 -0
- package/dist/builders/content-builder.d.ts.map +1 -0
- package/dist/builders/content-builder.js +67 -0
- package/dist/builders/content-builder.js.map +1 -0
- package/dist/builders/content-factory.d.ts +10 -0
- package/dist/builders/content-factory.d.ts.map +1 -0
- package/dist/builders/content-factory.js +23 -0
- package/dist/builders/content-factory.js.map +1 -0
- package/dist/builders/core-content.d.ts +127 -0
- package/dist/builders/core-content.d.ts.map +1 -0
- package/dist/builders/core-content.js +223 -0
- package/dist/builders/core-content.js.map +1 -0
- package/dist/builders/fields.d.ts +7 -0
- package/dist/builders/fields.d.ts.map +1 -0
- package/dist/builders/fields.js +20 -0
- package/dist/builders/fields.js.map +1 -0
- package/dist/builders/identifier.d.ts +2 -0
- package/dist/builders/identifier.d.ts.map +1 -0
- package/dist/builders/identifier.js +11 -0
- package/dist/builders/identifier.js.map +1 -0
- package/dist/builders/page-builder.d.ts +40 -0
- package/dist/builders/page-builder.d.ts.map +1 -0
- package/dist/builders/page-builder.js +119 -0
- package/dist/builders/page-builder.js.map +1 -0
- package/dist/builders/request-context.d.ts +23 -0
- package/dist/builders/request-context.d.ts.map +1 -0
- package/dist/builders/request-context.js +32 -0
- package/dist/builders/request-context.js.map +1 -0
- package/dist/checks/accessibility.d.ts +34 -0
- package/dist/checks/accessibility.d.ts.map +1 -0
- package/dist/checks/accessibility.js +90 -0
- package/dist/checks/accessibility.js.map +1 -0
- package/dist/checks/csp.d.ts +39 -0
- package/dist/checks/csp.d.ts.map +1 -0
- package/dist/checks/csp.js +179 -0
- package/dist/checks/csp.js.map +1 -0
- package/dist/checks/screenshot.d.ts +18 -0
- package/dist/checks/screenshot.d.ts.map +1 -0
- package/dist/checks/screenshot.js +125 -0
- package/dist/checks/screenshot.js.map +1 -0
- package/dist/config.d.ts +100 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +68 -0
- package/dist/config.js.map +1 -0
- package/dist/contract.d.ts +9 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +25 -0
- package/dist/contract.js.map +1 -0
- package/dist/global-setup.d.ts +20 -0
- package/dist/global-setup.d.ts.map +1 -0
- package/dist/global-setup.js +103 -0
- package/dist/global-setup.js.map +1 -0
- package/dist/global-teardown.d.ts +34 -0
- package/dist/global-teardown.d.ts.map +1 -0
- package/dist/global-teardown.js +269 -0
- package/dist/global-teardown.js.map +1 -0
- package/dist/http/api-secret.d.ts +11 -0
- package/dist/http/api-secret.d.ts.map +1 -0
- package/dist/http/api-secret.js +27 -0
- package/dist/http/api-secret.js.map +1 -0
- package/dist/http/cleanup-client.d.ts +26 -0
- package/dist/http/cleanup-client.d.ts.map +1 -0
- package/dist/http/cleanup-client.js +91 -0
- package/dist/http/cleanup-client.js.map +1 -0
- package/dist/http/off-site-headers.d.ts +13 -0
- package/dist/http/off-site-headers.d.ts.map +1 -0
- package/dist/http/off-site-headers.js +27 -0
- package/dist/http/off-site-headers.js.map +1 -0
- package/dist/http/prepare-context.d.ts +4 -0
- package/dist/http/prepare-context.d.ts.map +1 -0
- package/dist/http/prepare-context.js +6 -0
- package/dist/http/prepare-context.js.map +1 -0
- package/dist/http/record-edit.d.ts +37 -0
- package/dist/http/record-edit.d.ts.map +1 -0
- package/dist/http/record-edit.js +86 -0
- package/dist/http/record-edit.js.map +1 -0
- package/dist/http/toolkit-request.d.ts +8 -0
- package/dist/http/toolkit-request.d.ts.map +1 -0
- package/dist/http/toolkit-request.js +45 -0
- package/dist/http/toolkit-request.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect/cli.d.ts +3 -0
- package/dist/inspect/cli.d.ts.map +1 -0
- package/dist/inspect/cli.js +36 -0
- package/dist/inspect/cli.js.map +1 -0
- package/dist/inspect/links.d.ts +10 -0
- package/dist/inspect/links.d.ts.map +1 -0
- package/dist/inspect/links.js +51 -0
- package/dist/inspect/links.js.map +1 -0
- package/dist/inspect/token.d.ts +5 -0
- package/dist/inspect/token.d.ts.map +1 -0
- package/dist/inspect/token.js +15 -0
- package/dist/inspect/token.js.map +1 -0
- package/dist/playwright/base-config.d.ts +4 -0
- package/dist/playwright/base-config.d.ts.map +1 -0
- package/dist/playwright/base-config.js +34 -0
- package/dist/playwright/base-config.js.map +1 -0
- package/dist/playwright/index.d.ts +3 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +3 -0
- package/dist/playwright/index.js.map +1 -0
- package/dist/scenario.d.ts +46 -0
- package/dist/scenario.d.ts.map +1 -0
- package/dist/scenario.js +139 -0
- package/dist/scenario.js.map +1 -0
- package/dist/state/attempt-claim.d.ts +8 -0
- package/dist/state/attempt-claim.d.ts.map +1 -0
- package/dist/state/attempt-claim.js +47 -0
- package/dist/state/attempt-claim.js.map +1 -0
- package/dist/state/attempt-registry.d.ts +36 -0
- package/dist/state/attempt-registry.d.ts.map +1 -0
- package/dist/state/attempt-registry.js +68 -0
- package/dist/state/attempt-registry.js.map +1 -0
- package/dist/state/ensure-state.d.ts +42 -0
- package/dist/state/ensure-state.d.ts.map +1 -0
- package/dist/state/ensure-state.js +169 -0
- package/dist/state/ensure-state.js.map +1 -0
- package/dist/state/json-safe.d.ts +9 -0
- package/dist/state/json-safe.d.ts.map +1 -0
- package/dist/state/json-safe.js +18 -0
- package/dist/state/json-safe.js.map +1 -0
- package/dist/state/run-id.d.ts +6 -0
- package/dist/state/run-id.d.ts.map +1 -0
- package/dist/state/run-id.js +28 -0
- package/dist/state/run-id.js.map +1 -0
- package/dist/state/run-namespace.d.ts +33 -0
- package/dist/state/run-namespace.d.ts.map +1 -0
- package/dist/state/run-namespace.js +136 -0
- package/dist/state/run-namespace.js.map +1 -0
- package/dist/state/safe-paths.d.ts +10 -0
- package/dist/state/safe-paths.d.ts.map +1 -0
- package/dist/state/safe-paths.js +56 -0
- package/dist/state/safe-paths.js.map +1 -0
- package/dist/state/scenario-outcome.d.ts +9 -0
- package/dist/state/scenario-outcome.d.ts.map +1 -0
- package/dist/state/scenario-outcome.js +23 -0
- package/dist/state/scenario-outcome.js.map +1 -0
- package/dist/state/scenario-state.d.ts +41 -0
- package/dist/state/scenario-state.d.ts.map +1 -0
- package/dist/state/scenario-state.js +50 -0
- package/dist/state/scenario-state.js.map +1 -0
- package/dist/state/setup-lock.d.ts +22 -0
- package/dist/state/setup-lock.d.ts.map +1 -0
- package/dist/state/setup-lock.js +90 -0
- package/dist/state/setup-lock.js.map +1 -0
- package/dist/types/common.d.ts +17 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +2 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/content-builder.d.ts +16 -0
- package/dist/types/content-builder.d.ts.map +1 -0
- package/dist/types/content-builder.js +2 -0
- package/dist/types/content-builder.js.map +1 -0
- package/dist/types/playwright-extensions.d.ts +8 -0
- package/dist/types/playwright-extensions.d.ts.map +1 -0
- package/dist/types/playwright-extensions.js +2 -0
- package/dist/types/playwright-extensions.js.map +1 -0
- package/package.json +88 -0
- 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
|
+
[](https://www.npmjs.com/package/@plan2net/typo3-playwright-toolkit)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](https://playwright.dev)
|
|
11
|
+
[](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"}
|