/playwright-component-testing
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
$ npx -y skills add microsoft/playwright --skill playwright-component-testing --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/playwright-component-testing
Context preview
The summary Claude sees to decide when to auto-load this skill.
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
SKILL.md
playwright-component-testing.SKILL.mdname: playwright-component-testing
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.
Component Testing with Playwright
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.
Concept
- 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.
- 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.
- 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.
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.
Setup workflow
1. **Detect the framework and bundler.** React vs Vue decides the framework notes and example story to follow. Then:
- **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.
- **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.
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. 3. **Configure Playwright** — add to `playwright.config.ts`:
projects: [
{
name: 'components',
testDir: './tests/components',
use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5173/playwright/gallery/index.html', serviceWorkers: 'block', reuseContext: true },
},
],
webServer: {
command: 'npm run dev', // or: npx vite --config playwright/vite.config.ts
url: 'http://localhost:5173/playwright/gallery/index.html', // standalone server: http://localhost:3100/playwright/gallery/index.html
reuseExistingServer: !process.env.CI,
},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. 4. **Write a first story** next to an existing component, modeled on `templates/<react|vue>/Button.story.*`. 5. **Write a first spec**, modeled on `templates/react/button.spec.ts`, importing `test`/`expect` from `@playwright/test`. 6. **Run**: `npx playwright test --project=components`. Open `http://localhost:5173/playwright/gallery/index.html` in a browser to eyeball all stories.
Conventions
- 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).
- One export per scenario. Prefer a new story export over parameterizing an existing one — stories are greppable, reviewable documentation of component states.
Testing patterns
Examples are React; the Vue equivalents differ only in story syntax.
Callbacks and events
**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:
export const Stateful = () => {
const [expanded, setExpanded] = useState(false);
return <>
<Expandable expanded={expanded} setExpanded={setExpanded} title="Title">Details</Expandable>
<form hidden><input data-testid="expanded" readOnly value={String(expanded)} /></form>
</>;
};test('click should expand', async ({ mount }) => {
const component = await mount('components/Expandable/Stateful');
await component.locator('.codicon-chevron-right').click();
await expRead more
name: playwright-component-testing 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.
Component Testing with Playwright
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.
Concept
- 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.
- 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.
- 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.
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.
Setup workflow
1. **Detect the framework and bundler.** React vs Vue decides the framework notes and example story to follow. Then:
- **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.
- **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.
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. 3. **Configure Playwright** — add to `playwright.config.ts`:
projects: [
{
name: 'components',
testDir: './tests/components',
use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5173/playwright/gallery/index.html', serviceWorkers: 'block', reuseContext: true },
},
],
webServer: {
command: 'npm run dev', // or: npx vite --config playwright/vite.config.ts
url: 'http://localhost:5173/playwright/gallery/index.html', // standalone server: http://localhost:3100/playwright/gallery/index.html
reuseExistingServer: !process.env.CI,
},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. 4. **Write a first story** next to an existing component, modeled on `templates/<react|vue>/Button.story.*`. 5. **Write a first spec**, modeled on `templates/react/button.spec.ts`, importing `test`/`expect` from `@playwright/test`. 6. **Run**: `npx playwright test --project=components`. Open `http://localhost:5173/playwright/gallery/index.html` in a browser to eyeball all stories.
Conventions
- 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).
- One export per scenario. Prefer a new story export over parameterizing an existing one — stories are greppable, reviewable documentation of component states.
Testing patterns
Examples are React; the Vue equivalents differ only in story syntax.
Callbacks and events
**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:
export const Stateful = () => {
const [expanded, setExpanded] = useState(false);
return <>
<Expandable expanded={expanded} setExpanded={setExpanded} title="Title">Details</Expandable>
<form hidden><input data-testid="expanded" readOnly value={String(expanded)} /></form>
</>;
};test('click should expand', async ({ mount }) => {
const component = await mount('components/Expandable/Stateful');
await component.locator('.codicon-chevron-right').click();
await expPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
Repo: microsoft/playwright
Other skills on playwright.
- /playwright-dev
Explains how to develop Playwright - add APIs, MCP tools, CLI commands, and vendor dependencies.
Open skill - /playwright-devops
DevOps workflows for Playwright - CI failure analysis, workflow debugging, and release operations.
Open skill - /playwright-test-results
Query Playwright CI test results from the aggregated DuckDB database. Answers questions about flaky tests, failure rates, slow tests, and per-run/SHA/PR results without hunting through GitHub artifacts.
Open skill - /playwright-triage
Triage a Playwright bug report by reproducing it from the information in the issue. Use when asked to triage, reproduce, or verify a GitHub issue (a new bug report, or an existing report with a new comment).
Open skill - /playwright-cli
Automate browser interactions, test web pages and work with Playwright tests.
Open skill - /playwright-trace
Inspect Playwright trace files from the command line — list actions, view requests, console, errors, snapshots and screenshots.
Open skill

