venetian 0.2.0-aarch64-linux → 0.2.2-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 +202 -202
- data/exe/aarch64-linux/node +0 -0
- data/exe/aarch64-linux/package/browsers.json +12 -18
- data/exe/aarch64-linux/package/lib/bootstrap.js +1 -1
- data/exe/aarch64-linux/package/lib/coreBundle.js +11344 -9900
- data/exe/aarch64-linux/package/lib/server/electron/loader.js +5 -2
- data/exe/aarch64-linux/package/lib/serverRegistry.js +5 -1
- data/exe/aarch64-linux/package/lib/tools/cli-client/help.json +21 -6
- data/exe/aarch64-linux/package/lib/tools/cli-client/program.js +1 -1
- data/exe/aarch64-linux/package/lib/tools/cli-client/session.js +4 -0
- data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/SKILL.md +21 -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/skills/playwright-component-testing/SKILL.md +143 -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 +85 -0
- data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
- data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
- data/exe/aarch64-linux/package/lib/tools/utils/extension.js +31 -8
- data/exe/aarch64-linux/package/lib/utilsBundle.js +37698 -33670
- data/exe/aarch64-linux/package/lib/utilsBundle.js.LICENSE +391 -268
- data/exe/aarch64-linux/package/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
- data/exe/aarch64-linux/package/lib/vite/dashboard/assets/index-DhC616m4.css +1 -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-CwYYHbcZ.js +32 -0
- data/exe/aarch64-linux/package/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
- data/exe/aarch64-linux/package/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
- data/exe/aarch64-linux/package/lib/vite/recorder/index.html +2 -2
- data/exe/aarch64-linux/package/lib/vite/traceViewer/assets/codeMirrorModule-rXmQmLUY.js +32 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/assets/defaultSettingsView-B-dXF5JN.js +181 -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-BuZfJS5v.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.BLFoOugd.css +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.KZ4wOW1K.js +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.C7UW1sC9.css +1 -0
- data/exe/aarch64-linux/package/lib/vite/traceViewer/uiMode.Dzuouizj.js +5 -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 +249 -36
- data/exe/aarch64-linux/package/types/structs.d.ts +8 -1
- data/exe/aarch64-linux/package/types/types.d.ts +2009 -266
- data/lib/venetian/executable.rb +6 -2
- data/lib/venetian/upstream.rb +35 -18
- data/lib/venetian/version.rb +2 -2
- metadata +41 -38
- 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/tracing.md +0 -0
- /data/exe/aarch64-linux/package/lib/tools/{cli-client/skill → skills/playwright-cli}/references/video-recording.md +0 -0
- /data/exe/aarch64-linux/package/lib/tools/{trace → skills/playwright-trace}/SKILL.md +0 -0
|
@@ -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,85 @@
|
|
|
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
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. Set up the gallery and config per `SKILL.md`. Keep the old CT project running until the last
|
|
28
|
+
spec is migrated.
|
|
29
|
+
2. For each CT spec, split every `mount(<…/>)` call: JSX structure becomes a story export next to
|
|
30
|
+
the component; plain data props stay in the test as `mount`'s second argument. Callback spies
|
|
31
|
+
become story state recorded into a hidden form. A call site that only varies data props usually
|
|
32
|
+
needs just one generic story that spreads them:
|
|
33
|
+
`export const Default = (props: ButtonProps) => <Button title="Submit" {...props} />`.
|
|
34
|
+
3. Rewrite the spec: import `test`/`expect` from `@playwright/test`; `mount(<X a={1}/>)`
|
|
35
|
+
→ `mount('X/Default', { a: 1 })`; `update(<X a={2}/>)` → `update({ a: 2 })`;
|
|
36
|
+
`unmount()` unchanged. `mount` returns a locator for the gallery root — scope the queries:
|
|
37
|
+
`component.getByRole('button').click()`. Spy assertions become `toHaveValue()` on the story's
|
|
38
|
+
recorded state.
|
|
39
|
+
4. Port `beforeMount` hooks: app-wide setup into the gallery's `window.mount`; per-test
|
|
40
|
+
`hooksConfig` branches into props interpreted by a story or decorator.
|
|
41
|
+
5. When all specs are green, delete the CT project from the config, drop the
|
|
42
|
+
`@playwright/experimental-ct-*` dependency, and remove `playwright/index.html`,
|
|
43
|
+
`playwright/index.ts*` and `playwright/.cache`.
|
|
44
|
+
|
|
45
|
+
## Gotchas
|
|
46
|
+
|
|
47
|
+
- **Story ids are strings.** Renaming or moving a story breaks specs at runtime, not compile time
|
|
48
|
+
— and the suffix-matching resolution can silently match a different story after a rename.
|
|
49
|
+
- **Per-test JSX is gone.** Any test that built a different JSX tree per test (children matrices,
|
|
50
|
+
inline wrappers) becomes one story export per composition.
|
|
51
|
+
|
|
52
|
+
## Before / after
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// Before (CT)
|
|
56
|
+
import { test, expect } from '@playwright/experimental-ct-react';
|
|
57
|
+
import Button from '../src/components/Button';
|
|
58
|
+
|
|
59
|
+
test('click', async ({ mount }) => {
|
|
60
|
+
const messages: string[] = [];
|
|
61
|
+
const component = await mount(<Button title="Submit" onClick={data => messages.push(data)} />);
|
|
62
|
+
await component.click();
|
|
63
|
+
expect(messages).toEqual(['hello']);
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
// After: src/components/Button.story.tsx
|
|
69
|
+
import Button from './Button';
|
|
70
|
+
|
|
71
|
+
export const Default = (props: { onClick?: (data: string) => void }) =>
|
|
72
|
+
<Button title="Submit" {...props} />;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// After: src/components/Button.spec.ts
|
|
77
|
+
import { test, expect } from '@playwright/test';
|
|
78
|
+
|
|
79
|
+
test('click', async ({ mount }) => {
|
|
80
|
+
const messages: string[] = [];
|
|
81
|
+
const component = await mount('components/Button/Default', { onClick: (data: string) => messages.push(data) });
|
|
82
|
+
await component.click();
|
|
83
|
+
expect(messages).toEqual(['hello']);
|
|
84
|
+
});
|
|
85
|
+
```
|
data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/references/react.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# React setup
|
|
2
|
+
|
|
3
|
+
Follow the setup workflow in `SKILL.md`. Implement the gallery per `references/gallery-spec.md` (which has a React worked example); this page covers React-specific details.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `playwright/gallery/` — the gallery you implement to `references/gallery-spec.md` (an `index.html` + a `main.tsx` module). Requires `react` and `react-dom` 18+ (`createRoot`).
|
|
8
|
+
- Stories: `src/**/*.story.tsx` (the glob also picks up `.story.jsx`); example in `templates/react/Button.story.tsx`.
|
|
9
|
+
|
|
10
|
+
## StrictMode
|
|
11
|
+
|
|
12
|
+
Wrap the rendered story in `<React.StrictMode>` in your gallery to match how most apps render. In development builds StrictMode intentionally double-invokes render functions and effects. This matters for stories that record events with counters set in effects — recording via state updates from event handlers (like the `CountsClicks` example story) is unaffected. If a story misbehaves under StrictMode, that is usually a real finding about the component; drop the wrapper only if the app itself does not use StrictMode.
|
|
13
|
+
|
|
14
|
+
## Global providers
|
|
15
|
+
|
|
16
|
+
If components require context (theme, store, i18n, router), create one shared decorator and use it in stories, so each story states its scenario and nothing more:
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// src/stories/decorators.tsx
|
|
20
|
+
import { ThemeProvider } from '../theme';
|
|
21
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
22
|
+
|
|
23
|
+
export function AppScaffold({ children, route = '/' }: { children: React.ReactNode, route?: string }) {
|
|
24
|
+
return (
|
|
25
|
+
<ThemeProvider theme="light">
|
|
26
|
+
<MemoryRouter initialEntries={[route]}>{children}</MemoryRouter>
|
|
27
|
+
</ThemeProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// src/components/ProfilePage.story.tsx
|
|
34
|
+
export const LoggedIn = () => (
|
|
35
|
+
<AppScaffold route="/profile/42">
|
|
36
|
+
<ProfilePage user={{ id: 42, name: 'Test User' }} />
|
|
37
|
+
</AppScaffold>
|
|
38
|
+
);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Do not build the decorator into the gallery — keeping it in story files makes the wrapping visible and lets stories opt out.
|
|
42
|
+
|
|
43
|
+
## Typed props
|
|
44
|
+
|
|
45
|
+
`mount` is generic over the story: pass the story type as a template argument to type-check per-test props (and `update()`). Props are inferred from the component signature; function and class components both work.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// src/components/Button.story.tsx
|
|
49
|
+
export const WithTitle = ({ title = 'Default' }: { title?: string }) =>
|
|
50
|
+
<Button title={title} />;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// src/components/button.spec.ts
|
|
55
|
+
import type { WithTitle } from './Button.story';
|
|
56
|
+
|
|
57
|
+
const component = await mount<typeof WithTitle>('components/Button/WithTitle', { title: 'Hello' });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## CSS
|
|
61
|
+
|
|
62
|
+
- Global stylesheets: import them in your gallery entry (`playwright/gallery/main.tsx`, e.g. `import '../../src/index.css'`), mirroring the app's own entry point.
|
|
63
|
+
- Tailwind: if content scanning is path-based, make sure `*.story.tsx` files are covered.
|
|
64
|
+
|
|
65
|
+
## Data fetching
|
|
66
|
+
|
|
67
|
+
For libraries with client objects (React Query, Apollo), create the client inside the story or decorator so each navigation starts fresh.
|
data/exe/aarch64-linux/package/lib/tools/skills/playwright-component-testing/references/vue.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Vue setup
|
|
2
|
+
|
|
3
|
+
Follow the setup workflow in `SKILL.md`. Implement the gallery per `references/gallery-spec.md`, exposing `window.mount`/`window.unmount` — in Vue, mount with `app = createApp(h(story, props)); app.mount('#root')` and unmount with `app.unmount()`; this page covers Vue-specific details.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `playwright/gallery/` — the gallery you implement to `references/gallery-spec.md` (an `index.html` + a `main.ts` module). Requires Vue 3.
|
|
8
|
+
- Stories: `src/**/*.story.{ts,js,vue}`; example in `templates/vue/Button.story.ts`.
|
|
9
|
+
|
|
10
|
+
## Two ways to write stories
|
|
11
|
+
|
|
12
|
+
**Render-function stories** (`Button.story.ts`) — several scenarios per file, one named export each; see `templates/vue/Button.story.ts`. Uses `defineComponent` + `h()`, no SFC compilation involved.
|
|
13
|
+
|
|
14
|
+
**Single-file-component stories** (`Button.primary.story.vue`) — one story per file, full template syntax:
|
|
15
|
+
|
|
16
|
+
```vue
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { ref } from 'vue';
|
|
19
|
+
import Button from './Button.vue';
|
|
20
|
+
const clicks = ref(0);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<Button title="Submit" @click="clicks++" />
|
|
25
|
+
<form hidden><input data-testid="click-count" readonly :value="String(clicks)" /></form>
|
|
26
|
+
</template>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
An SFC story is addressed by its path without the extension: `mount('components/Button.primary')`. Prefer SFC stories when the scenario needs slots or non-trivial templates.
|
|
30
|
+
|
|
31
|
+
## Global plugins
|
|
32
|
+
|
|
33
|
+
Apps that rely on plugins (Pinia, vue-router, i18n) should wrap components with a decorator story helper that creates a fresh instance per story:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// src/stories/decorators.ts
|
|
37
|
+
import { defineComponent, h, type Component } from 'vue';
|
|
38
|
+
import { createPinia } from 'pinia';
|
|
39
|
+
|
|
40
|
+
export function withStore(story: Component) {
|
|
41
|
+
return defineComponent(() => {
|
|
42
|
+
const pinia = createPinia();
|
|
43
|
+
return () => h(story, { pinia });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For plugins that must be installed on the app instance (`app.use(...)`), add them in your gallery right after `createApp(...)` — that is the equivalent of the app's own bootstrap.
|
|
49
|
+
|
|
50
|
+
## Typed props
|
|
51
|
+
|
|
52
|
+
A story that takes per-test props declares them twice: in the setup signature (for the type) and in the `props` option (so Vue delivers them as props rather than `attrs`):
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// src/components/Button.story.ts
|
|
56
|
+
export const WithTitle = defineComponent(
|
|
57
|
+
(props: { title?: string }) => () => h(Button, { title: props.title ?? 'Default' }),
|
|
58
|
+
{ props: ['title'] },
|
|
59
|
+
);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`mount` is generic over the story: pass the story type as a template argument to type-check the props (and `update()`):
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// src/components/button.spec.ts
|
|
66
|
+
import type { WithTitle } from './Button.story';
|
|
67
|
+
|
|
68
|
+
const component = await mount<typeof WithTitle>('components/Button/WithTitle', { title: 'Hello' });
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Options-API stories (`defineComponent({ props: { ... } })`) infer props the same way. For `.story.vue` SFC stories, prop types are only inferable when the setup generates SFC types (Volar/vue-tsc); otherwise pass the props type directly: `mount<{ title?: string }>('components/Button.primary', { title: 'Hello' })`.
|
|
72
|
+
|
|
73
|
+
## CSS
|
|
74
|
+
|
|
75
|
+
Import global stylesheets in your gallery entry (`playwright/gallery/main.ts`, e.g. `import '../../src/assets/main.css'`), mirroring the app's entry point.
|
|
@@ -28,6 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var extension_exports = {};
|
|
30
30
|
__export(extension_exports, {
|
|
31
|
+
findPlaywrightExtensionProfile: () => findPlaywrightExtensionProfile,
|
|
31
32
|
isPlaywrightExtensionInstalled: () => isPlaywrightExtensionInstalled,
|
|
32
33
|
playwrightExtensionId: () => playwrightExtensionId,
|
|
33
34
|
playwrightExtensionInstallUrl: () => playwrightExtensionInstallUrl
|
|
@@ -37,20 +38,41 @@ var import_fs = __toESM(require("fs"));
|
|
|
37
38
|
var import_path = __toESM(require("path"));
|
|
38
39
|
const playwrightExtensionId = "mmlmfjhmonkocbjadbfplnigmagldckm";
|
|
39
40
|
const playwrightExtensionInstallUrl = `https://chromewebstore.google.com/detail/playwright-extension/${playwrightExtensionId}`;
|
|
40
|
-
async function
|
|
41
|
+
async function findPlaywrightExtensionProfile(userDataDir) {
|
|
42
|
+
const profiles = await listProfileDirectories(userDataDir);
|
|
43
|
+
const lastUsed = await readLastUsedProfile(userDataDir);
|
|
44
|
+
const ordered = lastUsed && profiles.includes(lastUsed) ? [lastUsed, ...profiles.filter((profile) => profile !== lastUsed)] : profiles;
|
|
45
|
+
for (const profile of ordered) {
|
|
46
|
+
if (await isExtensionInstalledInProfile(import_path.default.join(userDataDir, profile)))
|
|
47
|
+
return profile;
|
|
48
|
+
}
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
async function listProfileDirectories(userDataDir) {
|
|
41
52
|
let entries;
|
|
42
53
|
try {
|
|
43
54
|
entries = await import_fs.default.promises.readdir(userDataDir);
|
|
44
55
|
} catch {
|
|
45
|
-
return
|
|
56
|
+
return [];
|
|
46
57
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
const profiles = entries.filter((entry) => entry === "Default" || /^Profile \d+$/.test(entry));
|
|
59
|
+
profiles.sort((a, b) => profileRank(a) - profileRank(b));
|
|
60
|
+
return profiles;
|
|
61
|
+
}
|
|
62
|
+
function profileRank(profile) {
|
|
63
|
+
return profile === "Default" ? -1 : parseInt(profile.slice("Profile ".length), 10);
|
|
64
|
+
}
|
|
65
|
+
async function readLastUsedProfile(userDataDir) {
|
|
66
|
+
try {
|
|
67
|
+
const localState = JSON.parse(await import_fs.default.promises.readFile(import_path.default.join(userDataDir, "Local State"), "utf-8"));
|
|
68
|
+
const lastUsed = localState?.profile?.last_used;
|
|
69
|
+
return typeof lastUsed === "string" ? lastUsed : void 0;
|
|
70
|
+
} catch {
|
|
71
|
+
return void 0;
|
|
52
72
|
}
|
|
53
|
-
|
|
73
|
+
}
|
|
74
|
+
async function isPlaywrightExtensionInstalled(userDataDir) {
|
|
75
|
+
return await findPlaywrightExtensionProfile(userDataDir) !== void 0;
|
|
54
76
|
}
|
|
55
77
|
async function isExtensionInstalledInProfile(profileDir) {
|
|
56
78
|
if (await pathExists(import_path.default.join(profileDir, "Extensions", playwrightExtensionId)))
|
|
@@ -72,6 +94,7 @@ async function pathExists(p) {
|
|
|
72
94
|
}
|
|
73
95
|
// Annotate the CommonJS export names for ESM import in node:
|
|
74
96
|
0 && (module.exports = {
|
|
97
|
+
findPlaywrightExtensionProfile,
|
|
75
98
|
isPlaywrightExtensionInstalled,
|
|
76
99
|
playwrightExtensionId,
|
|
77
100
|
playwrightExtensionInstallUrl
|