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

Vue setup

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.

Files

  • playwright/gallery/ — the gallery you implement to references/gallery-spec.md (an index.html + a main.ts module). Requires Vue 3.
  • Stories: src/**/*.story.{ts,js,vue}; example in templates/vue/Button.story.ts.

Two ways to write stories

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.

Single-file-component stories (Button.primary.story.vue) — one story per file, full template syntax:

<script setup lang="ts">
import { ref } from 'vue';
import Button from './Button.vue';
const clicks = ref(0);
</script>

<template>
  <Button title="Submit" @click="clicks++" />
  <form hidden><input data-testid="click-count" readonly :value="String(clicks)" /></form>
</template>

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.

Global plugins

Apps that rely on plugins (Pinia, vue-router, i18n) should wrap components with a decorator story helper that creates a fresh instance per story:

// src/stories/decorators.ts
import { defineComponent, h, type Component } from 'vue';
import { createPinia } from 'pinia';

export function withStore(story: Component) {
  return defineComponent(() => {
    const pinia = createPinia();
    return () => h(story, { pinia });
  });
}

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.

Typed props

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):

// src/components/Button.story.ts
export const WithTitle = defineComponent(
  (props: { title?: string }) => () => h(Button, { title: props.title ?? 'Default' }),
  { props: ['title'] },
);

mount is generic over the story: pass the story type as a template argument to type-check the props (and update()):

// 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.

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' }).

CSS

Import global stylesheets in your gallery entry (playwright/gallery/main.ts, e.g. import '../../src/assets/main.css'), mirroring the app's entry point.

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 134.
- `references/react.md` — React walkthrough: providers, StrictMode, CSS.- `references/vue.md` — Vue walkthrough: `.story.ts` and `.story.vue` stories, plugins.- `references/migration.md` — migrating off `@playwright/experimental-ct-react` / `-vue`.