venetian 0.2.0-aarch64-linux → 0.2.3-aarch64-linux
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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/exe/aarch64-linux/LICENSE +204 -204
- data/exe/aarch64-linux/node +2 -2
- data/exe/aarch64-linux/package/browsers.json +10 -39
- data/exe/aarch64-linux/package/lib/bootstrap.js +1 -1
- data/exe/aarch64-linux/package/lib/coreBundle.js +14708 -12281
- data/exe/aarch64-linux/package/lib/server/electron/loader.js +5 -4
- data/exe/aarch64-linux/package/lib/serverRegistry.js +1674 -7078
- data/exe/aarch64-linux/package/lib/serverRegistry.js.LICENSE +8 -297
- data/exe/aarch64-linux/package/lib/tools/cli-client/help.json +39 -11
- data/exe/aarch64-linux/package/lib/tools/cli-client/minimist.js +1 -1
- data/exe/aarch64-linux/package/lib/tools/cli-client/output.js +9 -1
- data/exe/aarch64-linux/package/lib/tools/cli-client/program.js +18 -4
- data/exe/aarch64-linux/package/lib/tools/cli-client/registry.js +9 -5
- data/exe/aarch64-linux/package/lib/tools/cli-client/session.js +6 -1
- data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/SKILL.md +26 -5
- data/exe/aarch64-linux/package/lib/tools/{cli-client/skill/references/spec-driven-testing.md → skills/playwright-cli/references/test-generation.md} +135 -7
- data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/references/tracing.md +1 -1
- data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/SKILL.md +136 -0
- data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
- data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/references/migration.md +89 -0
- data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/references/react.md +69 -0
- data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/references/typing.md +173 -0
- data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/references/vue.md +77 -0
- data/exe/aarch64-linux/package/lib/tools/{trace → skills/playwright-trace}/SKILL.md +6 -3
- data/exe/aarch64-linux/package/lib/tools/utils/extension.js +44 -10
- data/exe/aarch64-linux/package/lib/utilsBundle.js +48939 -50318
- data/exe/aarch64-linux/package/lib/utilsBundle.js.LICENSE +359 -525
- data/exe/aarch64-linux/package/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css +1 -0
- data/exe/aarch64-linux/package/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js +32 -0
- data/exe/aarch64-linux/package/lib/vite/dashboard/assets/index-AwgwcYie.css +1 -0
- data/exe/aarch64-linux/package/lib/vite/dashboard/assets/index-hRvO4yU2.js +12 -0
- data/exe/aarch64-linux/package/lib/vite/dashboard/index.html +2 -2
- data/exe/aarch64-linux/package/lib/vite/htmlReport/report.css +2 -1
- data/exe/aarch64-linux/package/lib/vite/htmlReport/report.js +15 -55
- data/exe/aarch64-linux/package/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
- data/exe/aarch64-linux/package/lib/vite/recorder/assets/codeMirrorModule-CVQWJZAA.js +32 -0
- data/exe/aarch64-linux/package/lib/vite/recorder/assets/index-BZpYJZ-P.css +1 -0
- data/exe/aarch64-linux/package/lib/vite/recorder/assets/index-hrlLqLtq.js +129 -0
- data/exe/aarch64-linux/package/lib/vite/recorder/index.html +2 -2
- data/exe/aarch64-linux/package/lib/vite/traceViewer/assets/codeMirrorModule-BbkfBe3n.js +32 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/assets/defaultSettingsView-Ds6CBOo0.js +182 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/assets/xtermModule-DywYcAf8.js +7 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/defaultSettingsView.Bqk9acqE.css +1 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/index.B4cLoZK3.js +1 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/index.html +5 -5
- data/exe/aarch64-linux/package/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/snapshot.html +2 -2
- data/exe/aarch64-linux/package/lib/vite/traceViewer/sw.bundle.js +3 -4
- data/exe/aarch64-linux/package/lib/vite/traceViewer/uiMode.CU5KtEkS.js +5 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/uiMode.CyMfwkXJ.css +1 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/uiMode.html +5 -5
- data/exe/aarch64-linux/package/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
- data/exe/aarch64-linux/package/lib/webp_codec.LICENSE +173 -0
- data/exe/aarch64-linux/package/lib/webp_codec.wasm +0 -0
- data/exe/aarch64-linux/package/lib/xdg-open +338 -137
- data/exe/aarch64-linux/package/package.json +2 -2
- data/exe/aarch64-linux/package/types/protocol.d.ts +446 -430
- data/exe/aarch64-linux/package/types/structs.d.ts +8 -1
- data/exe/aarch64-linux/package/types/types.d.ts +2627 -440
- data/lib/venetian/executable.rb +6 -2
- data/lib/venetian/upstream.rb +35 -18
- data/lib/venetian/version.rb +2 -2
- metadata +45 -39
- data/exe/aarch64-linux/package/api.json +0 -1
- data/exe/aarch64-linux/package/lib/server/deviceDescriptorsSource.json +0 -2739
- data/exe/aarch64-linux/package/lib/tools/cli-client/skill/references/test-generation.md +0 -134
- data/exe/aarch64-linux/package/lib/vite/dashboard/assets/index-BY2S1tHT.css +0 -1
- data/exe/aarch64-linux/package/lib/vite/dashboard/assets/index-C_5TMfeg.js +0 -52
- data/exe/aarch64-linux/package/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css +0 -1
- data/exe/aarch64-linux/package/lib/vite/recorder/assets/codeMirrorModule-DeBYQozu.js +0 -32
- data/exe/aarch64-linux/package/lib/vite/recorder/assets/index-4ZiSSCmn.css +0 -1
- data/exe/aarch64-linux/package/lib/vite/recorder/assets/index-Bq-mQf8S.js +0 -193
- data/exe/aarch64-linux/package/lib/vite/traceViewer/assets/codeMirrorModule-LEHpjmcn.js +0 -32
- data/exe/aarch64-linux/package/lib/vite/traceViewer/assets/defaultSettingsView-BNmKHKpQ.js +0 -264
- data/exe/aarch64-linux/package/lib/vite/traceViewer/assets/urlMatch-BYQrIQwR.js +0 -1
- data/exe/aarch64-linux/package/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js +0 -9
- data/exe/aarch64-linux/package/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css +0 -1
- data/exe/aarch64-linux/package/lib/vite/traceViewer/defaultSettingsView.CjdS-WJx.css +0 -1
- data/exe/aarch64-linux/package/lib/vite/traceViewer/index.CzXZzn5A.css +0 -1
- data/exe/aarch64-linux/package/lib/vite/traceViewer/index.DMMX1gXU.js +0 -2
- data/exe/aarch64-linux/package/lib/vite/traceViewer/snapshot.v8KI4P3m.js +0 -2
- data/exe/aarch64-linux/package/lib/vite/traceViewer/uiMode.BZQ54Kgt.css +0 -1
- data/exe/aarch64-linux/package/lib/vite/traceViewer/uiMode.Ut8wwJNp.js +0 -6
- data/exe/aarch64-linux/package/lib/vite/traceViewer/xtermModule.DYP7pi_n.css +0 -32
- data/exe/aarch64-linux/package/protocol.yml +0 -4991
- /data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/references/element-attributes.md +0 -0
- /data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/references/playwright-tests.md +0 -0
- /data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/references/request-mocking.md +0 -0
- /data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/references/running-code.md +0 -0
- /data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/references/session-management.md +0 -0
- /data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/references/storage-state.md +0 -0
- /data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/references/video-recording.md +0 -0
|
@@ -130,6 +130,10 @@ to start the browser session.`);
|
|
|
130
130
|
];
|
|
131
131
|
if (cliArgs.headed)
|
|
132
132
|
args.push("--headed");
|
|
133
|
+
if (cliArgs.mobile)
|
|
134
|
+
args.push("--mobile");
|
|
135
|
+
if (cliArgs.device)
|
|
136
|
+
args.push(`--device=${cliArgs.device}`);
|
|
133
137
|
if (cliArgs.browser)
|
|
134
138
|
args.push(`--browser=${cliArgs.browser}`);
|
|
135
139
|
if (cliArgs.persistent)
|
|
@@ -147,8 +151,9 @@ to start the browser session.`);
|
|
|
147
151
|
const child = (0, import_child_process.spawn)(process.execPath, args, {
|
|
148
152
|
detached: true,
|
|
149
153
|
stdio: ["ignore", "pipe", err],
|
|
150
|
-
cwd: process.cwd()
|
|
154
|
+
cwd: process.cwd(),
|
|
151
155
|
// Will be used as root.
|
|
156
|
+
windowsHide: true
|
|
152
157
|
});
|
|
153
158
|
let signalled = false;
|
|
154
159
|
const sigintHandler = () => {
|
data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/SKILL.md
RENAMED
|
@@ -47,6 +47,11 @@ playwright-cli upload ./document.pdf
|
|
|
47
47
|
playwright-cli check e12
|
|
48
48
|
playwright-cli uncheck e12
|
|
49
49
|
playwright-cli snapshot
|
|
50
|
+
# search the snapshot for text or a regexp, returns matching nodes with surrounding context
|
|
51
|
+
playwright-cli find "Sign in"
|
|
52
|
+
playwright-cli find --regex "Sign (in|up)"
|
|
53
|
+
# wrap the regexp in slashes to add flags, e.g. /i for case-insensitive
|
|
54
|
+
playwright-cli find --regex "/sign (in|up)/i"
|
|
50
55
|
playwright-cli eval "document.title"
|
|
51
56
|
playwright-cli eval "el => el.textContent" e5
|
|
52
57
|
# get element id, class, or any attribute not visible in the snapshot
|
|
@@ -93,6 +98,7 @@ playwright-cli mousewheel 0 100
|
|
|
93
98
|
playwright-cli screenshot
|
|
94
99
|
playwright-cli screenshot e5
|
|
95
100
|
playwright-cli screenshot --filename=page.png
|
|
101
|
+
playwright-cli screenshot --hires
|
|
96
102
|
playwright-cli pdf --filename=page.pdf
|
|
97
103
|
```
|
|
98
104
|
|
|
@@ -159,6 +165,11 @@ playwright-cli run-code "async page => await page.context().grantPermissions(['g
|
|
|
159
165
|
playwright-cli run-code --filename=script.js
|
|
160
166
|
playwright-cli tracing-start
|
|
161
167
|
playwright-cli tracing-stop
|
|
168
|
+
|
|
169
|
+
# record user actions in the browser, print them as Playwright code on stop
|
|
170
|
+
playwright-cli recording-start
|
|
171
|
+
playwright-cli recording-stop
|
|
172
|
+
|
|
162
173
|
playwright-cli video-start video.webm
|
|
163
174
|
playwright-cli video-chapter "Chapter Title" --description="Details" --duration=2000
|
|
164
175
|
playwright-cli video-stop
|
|
@@ -209,6 +220,12 @@ playwright-cli open --browser=firefox
|
|
|
209
220
|
playwright-cli open --browser=webkit
|
|
210
221
|
playwright-cli open --browser=msedge
|
|
211
222
|
|
|
223
|
+
# Emulate a generic mobile device (Pixel 10 for Chromium, iPhone 17 for WebKit).
|
|
224
|
+
# Prefer this when a mobile layout is acceptable: mobile pages are usually
|
|
225
|
+
# lighter, so snapshots are smaller and cheaper.
|
|
226
|
+
playwright-cli open --mobile
|
|
227
|
+
playwright-cli open --device="iPhone 15"
|
|
228
|
+
|
|
212
229
|
# Use persistent profile (by default profile is in-memory)
|
|
213
230
|
playwright-cli open --persistent
|
|
214
231
|
# Use persistent profile with custom directory
|
|
@@ -278,6 +295,11 @@ playwright-cli snapshot e34
|
|
|
278
295
|
|
|
279
296
|
# include each element's bounding box as [box=x,y,width,height]
|
|
280
297
|
playwright-cli snapshot --boxes
|
|
298
|
+
|
|
299
|
+
# search a large snapshot instead of capturing it all — returns matching nodes
|
|
300
|
+
# with 3 lines of context around each match (like grep -C)
|
|
301
|
+
playwright-cli find "Add to cart"
|
|
302
|
+
playwright-cli find --regex "\\$[0-9]+\\.[0-9]{2}"
|
|
281
303
|
```
|
|
282
304
|
|
|
283
305
|
## Targeting elements
|
|
@@ -325,13 +347,13 @@ playwright-cli kill-all
|
|
|
325
347
|
|
|
326
348
|
## Installation
|
|
327
349
|
|
|
328
|
-
If global `playwright-cli` command is not available, try a local version via `npx playwright
|
|
350
|
+
If global `playwright-cli` command is not available, try a local version via `npx playwright cli`:
|
|
329
351
|
|
|
330
352
|
```bash
|
|
331
|
-
npx --no-install playwright
|
|
353
|
+
npx --no-install playwright --version
|
|
332
354
|
```
|
|
333
355
|
|
|
334
|
-
When local version is available, use `npx playwright
|
|
356
|
+
When local version is available, use `npx playwright cli` in all commands. Otherwise, install `playwright-cli` as a global command:
|
|
335
357
|
|
|
336
358
|
```bash
|
|
337
359
|
npm install -g @playwright/cli@latest
|
|
@@ -396,9 +418,8 @@ playwright-cli show --annotate
|
|
|
396
418
|
* **Request mocking** [references/request-mocking.md](references/request-mocking.md)
|
|
397
419
|
* **Running Playwright code** [references/running-code.md](references/running-code.md)
|
|
398
420
|
* **Browser session management** [references/session-management.md](references/session-management.md)
|
|
399
|
-
* **Spec-driven testing (plan / generate / heal)** [references/spec-driven-testing.md](references/spec-driven-testing.md)
|
|
400
421
|
* **Storage state (cookies, localStorage)** [references/storage-state.md](references/storage-state.md)
|
|
401
|
-
* **Test generation** [references/test-generation.md](references/test-generation.md)
|
|
422
|
+
* **Test generation (plan / generate / heal)** [references/test-generation.md](references/test-generation.md)
|
|
402
423
|
* **Tracing** [references/tracing.md](references/tracing.md)
|
|
403
424
|
* **Video recording** [references/video-recording.md](references/video-recording.md)
|
|
404
425
|
* **Inspecting element attributes** [references/element-attributes.md](references/element-attributes.md)
|
|
@@ -1,12 +1,141 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Test generation (plan → generate → heal)
|
|
2
2
|
|
|
3
|
-
End-to-end workflow for authoring and maintaining Playwright tests
|
|
3
|
+
End-to-end workflow for authoring and maintaining Playwright tests with `playwright-cli`. Every `playwright-cli` action emits the equivalent Playwright TypeScript, and that generated code is the raw material for every test. The sections below can be used independently:
|
|
4
4
|
|
|
5
|
-
- **
|
|
5
|
+
- **How generation works** — the core mechanic everything else relies on: actions become TypeScript, plus how to add assertions.
|
|
6
|
+
- **Plan** — explore the app, produce a spec file describing what to test.
|
|
6
7
|
- **Generate** — turn a spec into Playwright test files. Update the spec if it's vague or stale.
|
|
7
8
|
- **Heal** — diagnose failing tests, fix the code, reconcile the spec with reality.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Plan / generate / heal lean on the same mechanic: run `npx playwright test --debug=cli` in the background, then `playwright-cli attach tw-XXXX` to drive the paused page interactively. See [playwright-tests.md](playwright-tests.md) for the debug/attach mechanics.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 0. How generation works
|
|
15
|
+
|
|
16
|
+
Every action you perform with `playwright-cli` generates corresponding Playwright TypeScript code. This code appears in the output and can be copied directly into your test files.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Start a session
|
|
20
|
+
playwright-cli open https://example.com/login
|
|
21
|
+
|
|
22
|
+
# Take a snapshot to see elements
|
|
23
|
+
playwright-cli snapshot
|
|
24
|
+
# Output shows: e1 [textbox "Email"], e2 [textbox "Password"], e3 [button "Sign In"]
|
|
25
|
+
|
|
26
|
+
# Fill form fields - generates code automatically
|
|
27
|
+
playwright-cli fill e1 "user@example.com"
|
|
28
|
+
# Ran Playwright code:
|
|
29
|
+
# await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
|
|
30
|
+
|
|
31
|
+
playwright-cli fill e2 "password123"
|
|
32
|
+
# Ran Playwright code:
|
|
33
|
+
# await page.getByRole('textbox', { name: 'Password' }).fill('password123');
|
|
34
|
+
|
|
35
|
+
playwright-cli click e3
|
|
36
|
+
# Ran Playwright code:
|
|
37
|
+
# await page.getByRole('button', { name: 'Sign In' }).click();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Building a test file
|
|
41
|
+
|
|
42
|
+
Collect the generated code into a Playwright test:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { test, expect } from '@playwright/test';
|
|
46
|
+
|
|
47
|
+
test('login flow', async ({ page }) => {
|
|
48
|
+
// Generated code from playwright-cli session:
|
|
49
|
+
await page.goto('https://example.com/login');
|
|
50
|
+
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
|
|
51
|
+
await page.getByRole('textbox', { name: 'Password' }).fill('password123');
|
|
52
|
+
await page.getByRole('button', { name: 'Sign In' }).click();
|
|
53
|
+
|
|
54
|
+
// Add assertions
|
|
55
|
+
await expect(page).toHaveURL(/.*dashboard/);
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Use semantic locators
|
|
60
|
+
|
|
61
|
+
The generated code uses role-based locators when possible, which are more resilient:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Generated (good - semantic)
|
|
65
|
+
await page.getByRole('button', { name: 'Submit' }).click();
|
|
66
|
+
|
|
67
|
+
// Avoid (fragile - CSS selectors)
|
|
68
|
+
await page.locator('#submit-btn').click();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Explore before recording
|
|
72
|
+
|
|
73
|
+
Take snapshots to understand the page structure before recording actions:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
playwright-cli open https://example.com
|
|
77
|
+
playwright-cli snapshot
|
|
78
|
+
# Review the element structure
|
|
79
|
+
playwright-cli click e5
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Add assertions manually
|
|
83
|
+
|
|
84
|
+
Generated code captures actions but not assertions. Add expectations in your test using one of the recommended matchers:
|
|
85
|
+
|
|
86
|
+
- `toBeVisible()` — element is rendered and visible
|
|
87
|
+
- `toHaveText(text)` — element text content matches
|
|
88
|
+
- `toHaveValue(value) / toBeEmpty()` — input/select value matches
|
|
89
|
+
- `toBeChecked() / toBeUnchecked()` — checkbox state matches
|
|
90
|
+
- `toMatchAriaSnapshot(snapshot)` — page (or locator) matches a partial accessibility snapshot
|
|
91
|
+
|
|
92
|
+
Use `playwright-cli generate-locator <target>` to produce the locator expression for the assertion, and the snapshot/eval commands to capture the expected value.
|
|
93
|
+
|
|
94
|
+
When asserting text content, make sure that generated locator does not contain text from the element itself. `getByTestId()` or `getByLabel()` usually work well with asserting text. When locator is text-based, prefer `toBeVisible()` instead.
|
|
95
|
+
|
|
96
|
+
Snapshot to be matched does not have to contain all the information - only capture what's necessary for the assertion. You can use regular expressions for unstable values.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Get a stable locator for an element ref to use in the assertion
|
|
100
|
+
playwright-cli --raw generate-locator e5
|
|
101
|
+
# getByRole('button', { name: 'Submit' })
|
|
102
|
+
|
|
103
|
+
# Capture expected text content for toHaveText
|
|
104
|
+
playwright-cli --raw eval "el => el.textContent" e5
|
|
105
|
+
|
|
106
|
+
# Capture expected input value for toHaveValue/toBeEmpty
|
|
107
|
+
playwright-cli --raw eval "el => el.value" e5
|
|
108
|
+
|
|
109
|
+
# Capture expected aria snapshot for toMatchAriaSnapshot/toBeChecked
|
|
110
|
+
# (whole page, or use a ref to scope to a region)
|
|
111
|
+
playwright-cli --raw snapshot
|
|
112
|
+
playwright-cli --raw snapshot e5
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Generated action
|
|
117
|
+
await page.getByRole('button', { name: 'Submit' }).click();
|
|
118
|
+
|
|
119
|
+
// Manual assertions using the outputs above:
|
|
120
|
+
await expect(page.getByRole('alert', { name: 'Success' })).toBeVisible();
|
|
121
|
+
await expect(page.getByTestId('main-header')).toHaveText('Welcome, user');
|
|
122
|
+
await expect(page.getByRole('textbox', { name: 'Email' })).toHaveValue('user@example.com');
|
|
123
|
+
await expect(page.getByRole('checkbox', { name: 'Enable notifications' })).toBeChecked();
|
|
124
|
+
|
|
125
|
+
// toMatchAriaSnapshot on the whole page, finds a matching region
|
|
126
|
+
await expect(page).toMatchAriaSnapshot(`
|
|
127
|
+
- heading "Welcome, user"
|
|
128
|
+
- link /\\d+ new messages?/
|
|
129
|
+
- button "Sign out"
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
// toMatchAriaSnapshot scoped to a region
|
|
133
|
+
await expect(page.getByRole('navigation')).toMatchAriaSnapshot(`
|
|
134
|
+
- link "Home"
|
|
135
|
+
- link /\\d+ new messages?/
|
|
136
|
+
- link "Profile"
|
|
137
|
+
`);
|
|
138
|
+
```
|
|
10
139
|
|
|
11
140
|
---
|
|
12
141
|
|
|
@@ -173,7 +302,7 @@ playwright-cli attach tw-XXXX
|
|
|
173
302
|
|
|
174
303
|
Walk the scenario's `Steps:` one by one with `playwright-cli`, treating the spec as the plan and the live app as the source of truth. If a step is vague ("click the button" — which button?), references an element that no longer exists, or contradicts the app's actual behaviour, use your judgement: update the spec to match what the app really does, then keep going. Editing the spec mid-generation is expected.
|
|
175
304
|
|
|
176
|
-
Every action prints the equivalent Playwright TypeScript (see [
|
|
305
|
+
Every action prints the equivalent Playwright TypeScript (see [How generation works](#0-how-generation-works)):
|
|
177
306
|
|
|
178
307
|
```bash
|
|
179
308
|
playwright-cli snapshot # find refs
|
|
@@ -182,7 +311,7 @@ playwright-cli press Enter
|
|
|
182
311
|
playwright-cli click e7
|
|
183
312
|
```
|
|
184
313
|
|
|
185
|
-
For each `- expect:` bullet, add an explicit assertion. See [
|
|
314
|
+
For each `- expect:` bullet, add an explicit assertion. See [How generation works](#0-how-generation-works) for details.
|
|
186
315
|
|
|
187
316
|
Collect the generated code and write the test file at the path given in the spec:
|
|
188
317
|
|
|
@@ -300,6 +429,5 @@ Only after the user answers, either update the spec (intentional change) or file
|
|
|
300
429
|
| For... | See |
|
|
301
430
|
|---|---|
|
|
302
431
|
| `--debug=cli` / attach mechanics | [playwright-tests.md](playwright-tests.md) |
|
|
303
|
-
| How `playwright-cli` actions become TS | [test-generation.md](test-generation.md) |
|
|
304
432
|
| Mocking requests during exploration/generation | [request-mocking.md](request-mocking.md) |
|
|
305
433
|
| Managing the CLI browser session | [session-management.md](session-management.md) |
|
|
@@ -19,7 +19,7 @@ playwright-cli tracing-stop
|
|
|
19
19
|
|
|
20
20
|
## Trace Output Files
|
|
21
21
|
|
|
22
|
-
When you start tracing, Playwright creates a
|
|
22
|
+
When you start tracing, Playwright creates a `.playwright-cli/traces/` directory with several files:
|
|
23
23
|
|
|
24
24
|
### `trace-{timestamp}.trace`
|
|
25
25
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: playwright-component-testing
|
|
3
|
+
description: Set up component testing with Playwright using a story gallery — scaffold stories and a gallery dev page driven by the built-in mount fixture, no dedicated component-testing runtime. Use when asked to test React or Vue components in isolation with Playwright, or to migrate off @playwright/experimental-ct-react / -vue.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Component Testing with Playwright
|
|
7
|
+
|
|
8
|
+
Test components with regular Playwright e2e tests against a small **story gallery** page hosted by the app's own dev server. No extra test runner, bundler integration or npm packages are required.
|
|
9
|
+
|
|
10
|
+
## Concept
|
|
11
|
+
|
|
12
|
+
- A **story** is a tiny wrapper component that embeds the component under test in one specific scenario: hard-coded props, mock data, providers, recorded callbacks. Stories live next to the component in `*.story.tsx` (or `.ts`/`.jsx`/`.js`/`.vue`) files; each named export is one story.
|
|
13
|
+
- The **gallery** is a single page you implement to `references/gallery-spec.md`: it exposes `window.mount(params)` / `window.unmount()` that render a story — resolved from your story files (e.g. with `import.meta.glob`) — into `#root`. It is framework-specific and yours to own — there is no template to copy for it.
|
|
14
|
+
- Tests are plain Playwright tests. The built-in **`mount(storyId, props?)` fixture** (from `@playwright/test`) drives the gallery's `window.mount` and returns a `Locator` for the gallery root (`#root`). Scope the queries from there — `component.getByRole('button').click()`, not `component.click()`. Nothing to scaffold for it.
|
|
15
|
+
|
|
16
|
+
Everything the component needs must be set up *inside the story* (it runs in the browser); everything the test asserts must be observable *through the page* (DOM, URL, network). Where the component takes callbacks, the story creates the state, provides the callbacks and records the state into a hidden form for the test to assert on. `mount(id, props)` passes plain serializable `props` to the story.
|
|
17
|
+
|
|
18
|
+
## Setup workflow
|
|
19
|
+
|
|
20
|
+
1. **Detect the framework and bundler.** React vs Vue decides the framework notes and example story to follow. Then:
|
|
21
|
+
- **App runs on Vite** (has `vite.config.*`): the gallery is served by the existing dev server at `/playwright/gallery/index.html` — Vite serves any `.html` file under the project root, the app's plugins/aliases/CSS apply automatically, and `vite build` ignores it. No extra server needed.
|
|
22
|
+
- **Anything else** (Next.js, webpack, no dev server): run a small standalone dev server (e.g. Vite) that serves the gallery page, and point `baseURL` at it. Requires `vite` and the framework plugin as devDependencies.
|
|
23
|
+
2. **Implement the gallery** to `references/gallery-spec.md`: a page at `<project>/playwright/gallery/` that renders the requested story into `#root`. Start from the worked example in the spec and the framework notes in `references/react.md` / `references/vue.md`. Keep story discovery (`import.meta.glob`) and the framework mount here — this is the only framework-specific glue, so keep it small. Import the app's global CSS the same way the app's own entry does.
|
|
24
|
+
3. **Configure Playwright** — add to `playwright.config.ts`:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
projects: [
|
|
28
|
+
{
|
|
29
|
+
name: 'components',
|
|
30
|
+
testDir: './tests/components',
|
|
31
|
+
use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5173/playwright/gallery/index.html', serviceWorkers: 'block', reuseContext: true },
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
webServer: {
|
|
35
|
+
command: 'npm run dev', // or: npx vite --config playwright/vite.config.ts
|
|
36
|
+
url: 'http://localhost:5173/playwright/gallery/index.html', // standalone server: http://localhost:3100/playwright/gallery/index.html
|
|
37
|
+
reuseExistingServer: !process.env.CI,
|
|
38
|
+
},
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Match the port to the dev server. `mount` navigates to `baseURL`, so set `baseURL` to the gallery's URL. `serviceWorkers: 'block'` keeps the app's own service worker from serving cached responses that would shadow your `page.route()` mocks. `reuseContext: true` reuses the browser context across tests in a worker (as the old component-testing runtime did) — a large speedup for component suites. If the config already has projects/webServer, merge instead of replacing.
|
|
42
|
+
4. **Write a first story** next to an existing component, modeled on `templates/<react|vue>/Button.story.*`.
|
|
43
|
+
5. **Write a first spec**, modeled on `templates/react/button.spec.ts`, importing `test`/`expect` from `@playwright/test`.
|
|
44
|
+
6. **Run**: `npx playwright test --project=components`. Open `http://localhost:5173/playwright/gallery/index.html` in a browser to eyeball all stories.
|
|
45
|
+
|
|
46
|
+
## Conventions
|
|
47
|
+
|
|
48
|
+
- Story id: path under `src/` without the `.story.*` extension, plus the export name — `src/components/Button.story.tsx` export `Primary` → `components/Button/Primary`. Any unique suffix works too: `mount('Button/Primary')`. A `.story.vue` single-file component is one story, addressable by its path alone (its `default` export). With gallery types (`references/typing.md`) ids are prefixed with the package name: `acme-ui/components/Button/Primary`.
|
|
49
|
+
- One export per scenario. Prefer a new story export over parameterizing an existing one — stories are greppable, reviewable documentation of component states.
|
|
50
|
+
|
|
51
|
+
## Testing patterns
|
|
52
|
+
|
|
53
|
+
Examples are React; the Vue equivalents differ only in story syntax.
|
|
54
|
+
|
|
55
|
+
### Callbacks and events
|
|
56
|
+
|
|
57
|
+
**The story owns the state and provides the callbacks.** Where the component takes callbacks, create the state inside the story, wire the callbacks to it, and record the state into a hidden form next to the component. Tests perform operations and assert on the recorded values:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
export const Stateful = () => {
|
|
61
|
+
const [expanded, setExpanded] = useState(false);
|
|
62
|
+
return <>
|
|
63
|
+
<Expandable expanded={expanded} setExpanded={setExpanded} title="Title">Details</Expandable>
|
|
64
|
+
<form hidden><input data-testid="expanded" readOnly value={String(expanded)} /></form>
|
|
65
|
+
</>;
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
test('click should expand', async ({ mount }) => {
|
|
71
|
+
const component = await mount('components/Expandable/Stateful');
|
|
72
|
+
await component.locator('.codicon-chevron-right').click();
|
|
73
|
+
await expect(component.getByTestId('expanded')).toHaveValue('true');
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This keeps the whole scenario in the browser: no callback marshalling, the story doubles as documentation, and the recorded state is visible when eyeballing the gallery. Record each observed value in its own `data-testid` input (`String(...)` or `JSON.stringify(...)` for payloads) and assert with `toHaveValue()` — a web-first assertion that retries until the state lands. The negative direction works the same way: perform the operation, then assert the value did **not** change.
|
|
78
|
+
|
|
79
|
+
### Per-test props
|
|
80
|
+
|
|
81
|
+
When a scenario is genuinely parametric (e.g. a boundary-value sweep), pass props as the second argument to `mount`; the gallery hands them to the story as its props. Keep props to plain serializable data — callbacks belong inside the story.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
export const WithTitle = ({ title = 'Default' }: { title?: string }) =>
|
|
85
|
+
<Button title={title} />;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const component = await mount('components/Button/WithTitle', { title: 'Hello' });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Props are type-checked in two optional ways, see `references/typing.md`: pass the story type as a template argument (`mount<typeof WithTitle>('components/Button/WithTitle', { title: 'Hello' })`, no setup), or generate gallery types with a small Vite plugin so the id itself is typed (`mount('acme-ui/components/Button/WithTitle', { title: 'Hello' })`, with autocomplete and rename safety). Vue stories must additionally declare the props at runtime — see the `Typed props` sections in `references/react.md` / `references/vue.md`.
|
|
93
|
+
|
|
94
|
+
### Prop transitions with `update()`
|
|
95
|
+
|
|
96
|
+
To test how a component reacts to a prop change **without remounting** (state preserved), call `component.update(newProps)` — it re-renders the same story with new props on the existing root:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const component = await mount('components/Counter/Default', { value: 1 });
|
|
100
|
+
await expect(component.getByTestId('value')).toHaveText('1');
|
|
101
|
+
await component.update({ value: 2 });
|
|
102
|
+
await expect(component.getByTestId('value')).toHaveText('2');
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This requires the gallery to reuse its root/instance (`references/gallery-spec.md`); state survives as long as the story stays the same.
|
|
106
|
+
|
|
107
|
+
### Multiple states in one test
|
|
108
|
+
|
|
109
|
+
Each `mount()` navigates fresh, so tests are fully isolated and mounting several stories in one test is cheap:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
await expect(await mount('Button/Primary')).toHaveScreenshot('primary.png');
|
|
113
|
+
await expect(await mount('Button/Disabled')).toHaveScreenshot('disabled.png');
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
For visual comparison, screenshot the returned root locator (as above), not the page, to avoid asserting on browser chrome.
|
|
117
|
+
|
|
118
|
+
### Network mocking
|
|
119
|
+
|
|
120
|
+
Use `page.route()` as usual — register routes before `mount()`, since mounting navigates. `serviceWorkers: 'block'` (set in the config above) keeps the app's own service worker from serving cached responses that shadow the routes. Teams with MSW handler libraries can start the worker inside a story or decorator instead.
|
|
121
|
+
|
|
122
|
+
### Debugging stories
|
|
123
|
+
|
|
124
|
+
Open your gallery URL (`baseURL`) in a browser and call `await window.mount({ story: 'components/Button/Primary' })` from the devtools console — that is exactly what the `mount` fixture does. An unknown story rejects `window.mount`, which surfaces as the test's `mount()` throwing with a real stack. To browse without the console, give your gallery an optional index page.
|
|
125
|
+
|
|
126
|
+
## Decision points
|
|
127
|
+
|
|
128
|
+
- **Monorepos / non-`src` layouts**: change the glob and the id derivation in your gallery (`references/gallery-spec.md`) to match, and prefix ids with the package name (`references/typing.md`).
|
|
129
|
+
- **Global providers** (theme, i18n, store, router): create a shared `decorator` helper next to the gallery and wrap components in stories; see `references/react.md` / `references/vue.md`.
|
|
130
|
+
## References
|
|
131
|
+
|
|
132
|
+
- `references/gallery-spec.md` — the gallery endpoint contract to implement (**start here**).
|
|
133
|
+
- `references/typing.md` — optional typing for `mount`: explicit story types vs generated gallery types, with the Vite plugin.
|
|
134
|
+
- `references/react.md` — React walkthrough: providers, StrictMode, CSS.
|
|
135
|
+
- `references/vue.md` — Vue walkthrough: `.story.ts` and `.story.vue` stories, plugins.
|
|
136
|
+
- `references/migration.md` — migrating off `@playwright/experimental-ct-react` / `-vue`.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Gallery contract
|
|
2
|
+
|
|
3
|
+
The **gallery** is a single page, served by your dev server at the URL you set as `baseURL` in your
|
|
4
|
+
Playwright config, that exposes two methods on `window` for Playwright to drive:
|
|
5
|
+
|
|
6
|
+
- `window.mount(params)` — render a story.
|
|
7
|
+
- `window.unmount()` — unmount the current story.
|
|
8
|
+
|
|
9
|
+
The built-in `mount` fixture navigates to the gallery, then calls `window.mount` via
|
|
10
|
+
`page.evaluate()`. Keep props to plain serializable data — where the component takes callbacks,
|
|
11
|
+
the story creates the state, provides the callbacks and records the state into a hidden form for
|
|
12
|
+
the test to assert on.
|
|
13
|
+
|
|
14
|
+
## `window.mount(params)`
|
|
15
|
+
|
|
16
|
+
`params` is `{ story, props }`, straight from the test's `mount(story, props)` call:
|
|
17
|
+
|
|
18
|
+
- `story` — the story id (string). Resolve it (see id grammar) to a component.
|
|
19
|
+
- `props` — the plain serializable props object passed to the component.
|
|
20
|
+
|
|
21
|
+
Render the resolved component with `props` into `#root`. Return a `Promise` that resolves once the
|
|
22
|
+
component is mounted and **rejects on failure** (unknown story, render throw). The rejection
|
|
23
|
+
surfaces as the test's `await mount(...)` throwing, with a real stack — there is no HTTP-status or
|
|
24
|
+
DOM-attribute signalling.
|
|
25
|
+
|
|
26
|
+
**Reuse the root across calls.** The test's `component.update(props)` calls `window.mount` again
|
|
27
|
+
with the same story and new props, **without navigating**. If you render into the same root /
|
|
28
|
+
instance rather than recreating it, the framework reconciles and component-internal state is
|
|
29
|
+
preserved — that is CT's `update()`. Recreating the root each call (or navigating) resets state, so
|
|
30
|
+
reuse it: create it on first mount and render into it on every call. The framework reconciles,
|
|
31
|
+
remounting on its own only when the story (component type) changes.
|
|
32
|
+
|
|
33
|
+
**`window.mount` is your setup/teardown hook.** It is the browser-side equivalent of CT's
|
|
34
|
+
`beforeMount` / `afterMount`: install providers or plugins, seed a store, start an in-browser mock
|
|
35
|
+
server *before* you render, and run post-render work *after* — all inside this one function,
|
|
36
|
+
branched on the `story` / `props` the test passed. There is no separate hook registry; the function
|
|
37
|
+
you own is the hook.
|
|
38
|
+
|
|
39
|
+
## `window.unmount()`
|
|
40
|
+
|
|
41
|
+
Unmount the current story from `#root` and return a `Promise`. The test calls it via
|
|
42
|
+
`component.unmount()`. Needed only to assert teardown/cleanup effects — each `mount` navigates
|
|
43
|
+
fresh, so tests are already isolated.
|
|
44
|
+
|
|
45
|
+
## `#root`
|
|
46
|
+
|
|
47
|
+
Render the component into an element with `id="root"`. `mount` returns a `Locator` for `#root`
|
|
48
|
+
itself, so tests scope their queries from there — `component.getByRole('button').click()`, not
|
|
49
|
+
`component.click()`. Stories are free to render fragments, e.g. the component plus a hidden form
|
|
50
|
+
recording its state.
|
|
51
|
+
|
|
52
|
+
## Story id grammar (recommended)
|
|
53
|
+
|
|
54
|
+
The gallery owns resolution; `mount` passes the id through untouched. Recommended scheme:
|
|
55
|
+
|
|
56
|
+
- `<path under src, without the .story.* extension>/<ExportName>` — e.g.
|
|
57
|
+
`src/components/Button.story.tsx` export `Primary` → `components/Button/Primary`.
|
|
58
|
+
- Any unique trailing suffix resolves too: `Button/Primary`.
|
|
59
|
+
- A single-file-component story (`Button.story.vue`) is one story, addressed by its path alone
|
|
60
|
+
(its default export): `components/Button`.
|
|
61
|
+
|
|
62
|
+
## Worked example (React + Vite SPA)
|
|
63
|
+
|
|
64
|
+
An illustration of the contract, **not** a file to copy — implement the equivalent for your stack.
|
|
65
|
+
`import.meta.glob` stays inline here: Vite analyzes it statically, relative to this file, so it
|
|
66
|
+
cannot be moved into shared/shipped code. That is exactly why the gallery is yours to own.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
// playwright/gallery/main.tsx
|
|
70
|
+
import { flushSync } from 'react-dom';
|
|
71
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
72
|
+
|
|
73
|
+
const stories = import.meta.glob('../../src/**/*.story.{tsx,jsx}');
|
|
74
|
+
const id = (f: string) => f.replace(/^(\.\.\/)+src\//, '').replace(/\.story\.\w+$/, '');
|
|
75
|
+
|
|
76
|
+
async function resolve(storyId: string) {
|
|
77
|
+
const sep = storyId.lastIndexOf('/');
|
|
78
|
+
const [path, name] = [storyId.slice(0, sep), storyId.slice(sep + 1)];
|
|
79
|
+
const file = Object.keys(stories).find(f => id(f) === path || id(f).endsWith('/' + path));
|
|
80
|
+
const mod = (file && await stories[file]()) as Record<string, any> | undefined;
|
|
81
|
+
return mod?.[name] ?? mod?.default;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const rootEl = document.getElementById('root')!;
|
|
85
|
+
let root: Root | undefined;
|
|
86
|
+
|
|
87
|
+
(window as any).mount = async ({ story, props }: { story: string, props?: Record<string, any> }) => {
|
|
88
|
+
const Story = await resolve(story);
|
|
89
|
+
if (!Story)
|
|
90
|
+
throw new Error(`Unknown story: ${story}`);
|
|
91
|
+
root ??= createRoot(rootEl); // reuse the root so update() reconciles and preserves state
|
|
92
|
+
// flushSync so a render error rejects the promise instead of being swallowed.
|
|
93
|
+
flushSync(() => root!.render(<Story {...props} />));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
(window as any).unmount = async () => {
|
|
97
|
+
root?.unmount();
|
|
98
|
+
root = undefined;
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<!-- playwright/gallery/index.html -->
|
|
104
|
+
<!DOCTYPE html>
|
|
105
|
+
<div id="root"></div>
|
|
106
|
+
<script type="module" src="./main.tsx"></script>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Vue variant (state-preserving)
|
|
110
|
+
|
|
111
|
+
Vue's `createApp(...).mount()` builds a fresh instance each call, so mount a small **reactive host**
|
|
112
|
+
once and update its refs — updating them re-renders in place, which is what preserves state across
|
|
113
|
+
`update()`:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// playwright/gallery/main.ts
|
|
117
|
+
import { createApp, h, shallowRef, type App, type Component } from 'vue';
|
|
118
|
+
|
|
119
|
+
// resolve() and the import.meta.glob are the same as the React example.
|
|
120
|
+
const story = shallowRef<Component | null>(null);
|
|
121
|
+
const props = shallowRef<Record<string, any>>({});
|
|
122
|
+
const host = { render: () => (story.value ? h(story.value, props.value) : null) };
|
|
123
|
+
let app: App | undefined;
|
|
124
|
+
|
|
125
|
+
(window as any).mount = async ({ story: id, props: next }: { story: string, props?: Record<string, any> }) => {
|
|
126
|
+
const resolved = await resolve(id);
|
|
127
|
+
if (!resolved)
|
|
128
|
+
throw new Error(`Unknown story: ${id}`);
|
|
129
|
+
story.value = resolved;
|
|
130
|
+
props.value = next ?? {};
|
|
131
|
+
if (!app) { // mount once; the ref updates above re-render in place
|
|
132
|
+
app = createApp(host);
|
|
133
|
+
app.mount('#root');
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
(window as any).unmount = async () => {
|
|
138
|
+
app?.unmount();
|
|
139
|
+
app = undefined;
|
|
140
|
+
};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Keep the story-resolution glob and the framework mount in this file; everything
|
|
144
|
+
else lives in your stories and tests.
|
data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/references/migration.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Migrating from @playwright/experimental-ct-react / -vue
|
|
2
|
+
|
|
3
|
+
The CT packages compiled JSX in the test file and marshalled it into the browser. The gallery
|
|
4
|
+
pattern moves the scenario into a story export that runs natively in the browser: structure (which
|
|
5
|
+
component, its children, providers) plus behavior (state and callbacks, recorded into a hidden
|
|
6
|
+
form for the test to assert on). Plain data props travel through `mount(storyId, props)`;
|
|
7
|
+
`update()` and `unmount()` work as before.
|
|
8
|
+
|
|
9
|
+
## Concept mapping
|
|
10
|
+
|
|
11
|
+
| `@playwright/experimental-ct-*` | Gallery pattern |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `mount(<Button title="…" onClick={spy} />)` | Stateful story: the story provides `onClick`, records the effect into a hidden form input; the test asserts `toHaveValue()` |
|
|
14
|
+
| Plain data props from the test | Unchanged in spirit: `mount(id, props)` |
|
|
15
|
+
| JSX children / slots from the test | Cannot cross — bake each composition into its own story export (Vue: `.story.vue` for slot-heavy scenarios) |
|
|
16
|
+
| `component.update(<Button count={2} />)` | `component.update({ count: 2 })` — state-preserving, needs the gallery to reuse its root (`gallery-spec.md`) |
|
|
17
|
+
| `component.unmount()` | `component.unmount()` — backed by the gallery's `window.unmount()` |
|
|
18
|
+
| `beforeMount`/`afterMount` in `playwright/index.ts` | The body of the gallery's `window.mount` (global), or story decorators (per-story) |
|
|
19
|
+
| `hooksConfig` per-test variation | Props: `mount('App/Routing', { route: '/dashboard' })` — the story/decorator interprets them |
|
|
20
|
+
| `router` fixture / MSW handlers in Node | `page.route()` in the test, or MSW `setupWorker` inside a story/decorator |
|
|
21
|
+
| `playwright/index.html` (styles, fonts, theme) | The gallery's `index.html` / entry module imports |
|
|
22
|
+
| `ctViteConfig`, `ctPort`, `ctTemplateDir`, `ctCacheDir` | Gone — the gallery runs through the app's own dev server; port lives in `webServer` + `baseURL`; location is `playwright/gallery/` |
|
|
23
|
+
| `defineConfig` from `@playwright/experimental-ct-react` | Plain `defineConfig` from `@playwright/test`, with `baseURL` = gallery URL, `serviceWorkers: 'block'`, `reuseContext: true` (see `SKILL.md`) |
|
|
24
|
+
|
|
25
|
+
The CT packages were removed in Playwright 1.63 and are no longer published, so migrate while
|
|
26
|
+
pinned to Playwright 1.62 and upgrade once the last spec is ported.
|
|
27
|
+
|
|
28
|
+
## Steps
|
|
29
|
+
|
|
30
|
+
1. Set up the gallery and config per `SKILL.md`. Keep the old CT project running until the last
|
|
31
|
+
spec is migrated.
|
|
32
|
+
2. For each CT spec, split every `mount(<…/>)` call: JSX structure becomes a story export next to
|
|
33
|
+
the component; plain data props stay in the test as `mount`'s second argument. Callback spies
|
|
34
|
+
become story state recorded into a hidden form. A call site that only varies data props usually
|
|
35
|
+
needs just one generic story that spreads them:
|
|
36
|
+
`export const Default = (props: ButtonProps) => <Button title="Submit" {...props} />`.
|
|
37
|
+
3. Rewrite the spec: import `test`/`expect` from `@playwright/test`; `mount(<X a={1}/>)`
|
|
38
|
+
→ `mount('X/Default', { a: 1 })`; `update(<X a={2}/>)` → `update({ a: 2 })`;
|
|
39
|
+
`unmount()` unchanged. `mount` returns a locator for the gallery root — scope the queries:
|
|
40
|
+
`component.getByRole('button').click()`. Spy assertions become `toHaveValue()` on the story's
|
|
41
|
+
recorded state.
|
|
42
|
+
4. Port `beforeMount` hooks: app-wide setup into the gallery's `window.mount`; per-test
|
|
43
|
+
`hooksConfig` branches into props interpreted by a story or decorator.
|
|
44
|
+
5. When all specs are green, delete the CT project from the config, drop the
|
|
45
|
+
`@playwright/experimental-ct-*` dependency, remove `playwright/index.html`,
|
|
46
|
+
`playwright/index.ts*` and `playwright/.cache`, and lift the Playwright version pin.
|
|
47
|
+
|
|
48
|
+
## Gotchas
|
|
49
|
+
|
|
50
|
+
- **Story ids are strings.** Renaming or moving a story breaks specs at runtime, not compile time
|
|
51
|
+
— and the suffix-matching resolution can silently match a different story after a rename.
|
|
52
|
+
Gallery types (`typing.md`) turn registered ids into a compile-time check.
|
|
53
|
+
- **Per-test JSX is gone.** Any test that built a different JSX tree per test (children matrices,
|
|
54
|
+
inline wrappers) becomes one story export per composition.
|
|
55
|
+
|
|
56
|
+
## Before / after
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// Before (CT)
|
|
60
|
+
import { test, expect } from '@playwright/experimental-ct-react';
|
|
61
|
+
import Button from '../src/components/Button';
|
|
62
|
+
|
|
63
|
+
test('click', async ({ mount }) => {
|
|
64
|
+
const messages: string[] = [];
|
|
65
|
+
const component = await mount(<Button title="Submit" onClick={data => messages.push(data)} />);
|
|
66
|
+
await component.click();
|
|
67
|
+
expect(messages).toEqual(['hello']);
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// After: src/components/Button.story.tsx
|
|
73
|
+
import Button from './Button';
|
|
74
|
+
|
|
75
|
+
export const Default = (props: { onClick?: (data: string) => void }) =>
|
|
76
|
+
<Button title="Submit" {...props} />;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// After: src/components/Button.spec.ts
|
|
81
|
+
import { test, expect } from '@playwright/test';
|
|
82
|
+
|
|
83
|
+
test('click', async ({ mount }) => {
|
|
84
|
+
const messages: string[] = [];
|
|
85
|
+
const component = await mount('components/Button/Default', { onClick: (data: string) => messages.push(data) });
|
|
86
|
+
await component.click();
|
|
87
|
+
expect(messages).toEqual(['hello']);
|
|
88
|
+
});
|
|
89
|
+
```
|