playwright-component-testing
Skill5 starsChanged 15 days agoMITView on GitHub
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.
- Reads credentials
3 other projects use a version of this, 1 of them edited; 1 new project in the last 4 weeks, against 0 the 4 before.
Adopted by
Surging: 1 new project took it in the last 4 weeks, none the 4 weeks before.thin data
Most recently taken up by
- PracticalSwan/agent-skillsedited, first seen Oct 19, 2026
- microsoft/playwrightword for word, first seen Oct 13, 2026
- FutureAtoms/claude-skills-backuptheir own version, first seen Mar 11, 2026
Other versions3 projects use a version of this.
Word for word · 1
- microsoft/playwright★ 100k100% same
Edited · 1
- PracticalSwan/agent-skills★ 2275% same
Wrote their own version · 1
- FutureAtoms/claude-skills-backup★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository10 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .github/copilot-instructions.mdCopilot instructions
- .cursor/rules/playwright-e2e.mdcCursor rule
- playwright-cliSkill
- playwright-e2eSkill
- playwright-traceSkill
- playwright-cliSkill
- playwright-component-testingSkill
- playwright-e2eSkill
- playwright-traceSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 8
Test components with regular Playwright e2e tests against a small story gallery page hosted by the app's own dev server.
- Very specific: line 81
When a scenario is genuinely parametric (e.g.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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 exposeswindow.mount(params)/window.unmount()that render a story — resolved from your story files (e.g. withimport.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'swindow.mountand returns aLocatorfor the gallery root (#root). Scope the queries from there —component.getByRole('button').click(), notcomponent.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
- 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.htmlfile under the project root, the app's plugins/aliases/CSS apply automatically, andvite buildignores 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
baseURLat it. Requiresviteand the framework plugin as devDependencies.
- App runs on Vite (has
- 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 inreferences/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. 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.
mountnavigates tobaseURL, so setbaseURLto the gallery's URL.serviceWorkers: 'block'keeps the app's own service worker from serving cached responses that would shadow yourpage.route()mocks.reuseContext: truereuses 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.- Write a first story next to an existing component, modeled on
templates/<react|vue>/Button.story.*. - Write a first spec, modeled on
templates/react/button.spec.ts, importingtest/expectfrom@playwright/test. - Run:
npx playwright test --project=components. Openhttp://localhost:5173/playwright/gallery/index.htmlin a browser to eyeball all stories.
Conventions
- Story id: path under
src/without the.story.*extension, plus the export name —src/components/Button.story.tsxexportPrimary→components/Button/Primary. Any unique suffix works too:mount('Button/Primary'). A.story.vuesingle-file component is one story, addressable by its path alone (itsdefaultexport). With gallery types (references/typing.md) ids are prefixed with the package name:acme-ui/components/Button/Primary. - 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 expect(component.getByTestId('expanded')).toHaveValue('true');
});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.
Per-test props
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.
export const WithTitle = ({ title = 'Default' }: { title?: string }) =>
<Button title={title} />;const component = await mount('components/Button/WithTitle', { title: 'Hello' });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.
Prop transitions with update()
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:
const component = await mount('components/Counter/Default', { value: 1 });
await expect(component.getByTestId('value')).toHaveText('1');
await component.update({ value: 2 });
await expect(component.getByTestId('value')).toHaveText('2');This requires the gallery to reuse its root/instance (references/gallery-spec.md); state survives as long as the story stays the same.
Multiple states in one test
Each mount() navigates fresh, so tests are fully isolated and mounting several stories in one test is cheap:
await expect(await mount('Button/Primary')).toHaveScreenshot('primary.png');
await expect(await mount('Button/Disabled')).toHaveScreenshot('disabled.png');For visual comparison, screenshot the returned root locator (as above), not the page, to avoid asserting on browser chrome.
Network mocking
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.
Debugging stories
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.
Decision points
- Monorepos / non-
srclayouts: 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
decoratorhelper next to the gallery and wrap components in stories; seereferences/react.md/references/vue.md.
References
references/gallery-spec.md— the gallery endpoint contract to implement (start here).references/typing.md— optional typing formount: 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.tsand.story.vuestories, plugins.references/migration.md— migrating off@playwright/experimental-ct-react/-vue.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

