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 @playwright/experimental-ct-react / -vue.

Install
npx skills add 'https://github.com/microsoft/playwright/tree/main/packages/playwright-core/src/tools/skills/playwright-component-testing'
Download bundle ↓
main · 500c9c8Scanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md

React setup

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.

Files

  • 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).
  • Stories: src/**/*.story.tsx (the glob also picks up .story.jsx); example in templates/react/Button.story.tsx.

StrictMode

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.

Global providers

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:

// src/stories/decorators.tsx
import { ThemeProvider } from '../theme';
import { MemoryRouter } from 'react-router-dom';

export function AppScaffold({ children, route = '/' }: { children: React.ReactNode, route?: string }) {
  return (
    <ThemeProvider theme="light">
      <MemoryRouter initialEntries={[route]}>{children}</MemoryRouter>
    </ThemeProvider>
  );
}
// src/components/ProfilePage.story.tsx
export const LoggedIn = () => (
  <AppScaffold route="/profile/42">
    <ProfilePage user={{ id: 42, name: 'Test User' }} />
  </AppScaffold>
);

Do not build the decorator into the gallery — keeping it in story files makes the wrapping visible and lets stories opt out.

Typed props

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.

// src/components/Button.story.tsx
export const WithTitle = ({ title = 'Default' }: { title?: string }) =>
  <Button title={title} />;
// src/components/button.spec.ts
import type { WithTitle } from './Button.story';

const component = await mount<typeof WithTitle>('components/Button/WithTitle', { title: 'Hello' });

Alternatively, generate gallery types so the id itself is typed and no type import is needed: mount('acme-ui/components/Button/WithTitle', { title: 'Hello' }) — see references/typing.md.

CSS

  • 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.
  • Tailwind: if content scanning is path-based, make sure *.story.tsx files are covered.

Data fetching

For libraries with client objects (React Query, Apollo), create the client inside the story or decorator so each navigation starts fresh.

Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 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.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`:
SKILL.mdView in source ↗
Source excerpt starting at line 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`.
SKILL.mdView in source ↗
Source excerpt starting at line 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`).- **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`.## References
SKILL.mdView in source ↗
Source excerpt starting at line 133.
- `references/typing.md` — optional typing for `mount`: explicit story types vs generated gallery types, with the Vite plugin.- `references/react.md` — React walkthrough: providers, StrictMode, CSS.- `references/vue.md` — Vue walkthrough: `.story.ts` and `.story.vue` stories, plugins.