Home / Services / Playwright component testing
Playwright component testing, built into the repo you already have
You keep a gallery module, a set of stories, a components project in your Playwright config and a CI job that starts your dev server before it runs any of them.
We build a browser-rendered component suite inside your own build: the gallery module your dev server serves, stories beside the components that carry a browser test, a components project in your Playwright config, and the CI job that runs it. The work also says which of your existing tests it replaces, which is usually fewer than you expect. Engineers are billed hourly, from $50 an hour, minimum one full-time engineer for one month.
Most of the first call is about your build. Whether a gallery route can exist in your dev server, and who owns the bundler config, decide more here than the size of the component library does.
Who this is for
Everything else on this site is written for somebody who owns a test suite. This page is for
the person who owns src/components/.
- A front-end lead or design-system owner whose components are used in places nobody can enumerate, where a change to one of them is a change to twenty screens.
- The platform or developer-experience engineer who owns the dev server, the aliases and the pipeline. This engagement touches all three, so if that person is not in the room the work does not get approved.
- A team that already has component tests. Almost every team does, and this engagement does not delete them.
It is not for a QA manager with no access to the front-end repository. The gallery is a file in your application repository and your own dev server renders it, so there is no way to do this from outside the build. If there is no Playwright suite in the repository at all, the conversation starts with a suite built and handed over and the component layer follows it.
What you get
The gallery module
A file in your repository that resolves a story id to a component, renders it into the page and reuses that root across calls so state survives an update. It is application code, it runs through your bundler, and it belongs to your project rather than to Playwright.
The story set
One named export per scenario, sitting beside the component: the props, the mock data, the providers and the fixed states you want pinned down. Written for the components on the list that comes out of phase one, and the rest of the library carries on as it is.
A components project in your config
Added to playwright.config.ts beside whatever projects you run today, with
its own testDir, a baseURL at the gallery and a
webServer block that starts your dev server.
The CI job, dev server included
The components project running in your pipeline, which means your dev server coming up there. A component suite whose dev server will not start never runs at all, and that failure lands on whoever owns the pipeline, not on whoever wrote the test.
A written record of what stays where
One page in the repository: which components got a browser test, which stay in the suite you already run, and which assertions belong in the end-to-end suite instead. It stops you owning three overlapping suites in a year.
How it works
Read the build
We open the repository and start the dev server: which command, which port, which environment variables, whether a gallery route can ship in the production build or lives only in development, and where theming, providers and global CSS come from. Out of that comes the list of components worth a browser and the list that is fine where it is. You owe us a branch, the dev server command, and whoever owns the bundler config by name.
The gallery and one component, end to end
The gallery module, one story, one spec, green locally. We take the component your team argues about most, because the point of this phase is to find out what your build does when a second entry point renders it. You make that choice.
The story set
Stories and specs for the rest of the list, with the mock data and providers each one needs. You owe us review time from a person who knows what each component is supposed to do, because a story that is wrong about the scenario passes just as reliably as one that is right.
CI, then handover
The components project running in the pipeline with the dev server starting there, and the written record of what stays where. You owe us somebody who can approve a workflow change and a repository they can merge into.
The two files that land in your repository
Both samples were written for the stories-and-galleries model that Playwright 1.62 made
stable, and both ran on 1.62.1 with Chromium 151.0.7922.34, Vue 3.5.42 and Vite 8.2.2. Every
mount() call in this model takes a story id as a string.
The gallery is the framework-specific piece and the one nobody can ship for you. This is the Vue one; the React version, and what each line of the contract is for, are in the component testing guide.
import { createApp, defineComponent, h, markRaw, nextTick, ref, shallowRef } from 'vue';
import type { App, Component } from 'vue';
import '../../src/styles.css';
const loaders = import.meta.glob<Record<string, Component>>('../../src/**/*.story.ts');
const stories = new Map(
Object.entries(loaders).map(([file, load]) =>
[file.replace(/^(\.\.\/)+src\//, '').replace(/\.story\.ts$/, ''), load] as const),
);
async function resolveStory(storyId: string) {
const cut = storyId.lastIndexOf('/');
const path = storyId.slice(0, cut);
const exportName = storyId.slice(cut + 1);
const key = [...stories.keys()].find(id => id === path || id.endsWith(`/${path}`));
if (!key)
throw new Error(`No story file for "${storyId}". Known: ${[...stories.keys()].join(', ')}`);
const story = (await stories.get(key)!())[exportName];
if (!story)
throw new Error(`"${key}" has no export named "${exportName}"`);
return story;
}
const story = shallowRef<Component>();
const props = ref<Record<string, unknown>>({});
const host = defineComponent({
setup: () => () => (story.value ? h(story.value, props.value) : null),
});
let app: App | undefined;
window.mount = async params => {
story.value = markRaw(await resolveStory(params.story));
props.value = params.props ?? {};
if (!app) {
app = createApp(host);
app.mount('#root');
}
await nextTick();
};
window.unmount = async () => {
app?.unmount();
app = undefined;
};
declare global {
interface Window {
mount(params: { story: string, props?: Record<string, unknown> }): Promise<void>;
unmount(): Promise<void>;
}
}
Playwright 1.62.1 · playwright/gallery/main.ts · Vue 3.5.42 + Vite 8.2.2
The second question a lead asks is what happens to the config that already runs the
end-to-end suite. It gains a project and a webServer block, and nothing about the
existing one changes.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'e2e',
testDir: './tests/e2e',
use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5173' },
},
{
name: 'components',
testDir: './tests/components',
use: {
...devices['Desktop Chrome'],
baseURL: 'http://localhost:5173/playwright/gallery/index.html',
},
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173/playwright/gallery/index.html',
reuseExistingServer: !process.env.CI,
},
});
Playwright 1.62.1 · playwright.config.ts
The two projects share one dev server and one report.
reuseExistingServer: !process.env.CI decides what happens on each side of that:
on your machine the run uses whatever dev server is already listening, and in the pipeline,
where the variable is set, it starts its own instead of trusting what it finds. Screenshots
work against the locator mount() hands back, so the frame is the component, and
where one has to be generated for it to keep passing in CI is in
baselines, tolerances and what to
snapshot.
What this replaces, and what it does not
You have a Vitest or Jest suite against jsdom, running in seconds and mostly working. A real
browser brings in what jsdom leaves out: layout and stacking order, real CSS including
display: none and container queries, real events with actionability behind them,
and three engines rather than one.
The bill for that: a test run that starts a browser and navigates where the other one imports a module, a dev server that becomes something which has to be up on your machine and in the pipeline, a gallery module you maintain, a story file for every scenario you want covered, and a repository with two ways of writing a test in it. No number is attached to any of that on this page.
Needing a real browser is not a reason to leave the runner you have. Vitest
Browser Mode runs tests in one, and it can drive it with Playwright: the providers are
preview, playwright and webdriverio, and the Playwright
one installs as npm install -D vitest @vitest/browser-playwright. Storybook Test
renders your stories in a browser too, and its own documentation says it uses Playwright to do
that by default. A design-system team with either of those already has the browser.
So the difference is not fidelity, it is where a test is allowed to stand. Storybook's definition of a component test includes reaching into the implementation to mock things or manipulate data. Playwright's documentation says the opposite about its own model: reaching into the component from a test, for its instance or its internals, is neither recommended nor supported, and internal effects are recorded into the DOM through the story instead. Tests written only from the outside survive a refactor that renames everything inside the component, and they take more work to express.
The second difference is an operations one. A repository running an end-to-end suite in Playwright and a component suite somewhere else has two runners, two reports and two CI stories to keep working; moving the component layer into Playwright makes that one of each. That is worth something on a team that maintains its own pipeline and nothing on a team that does not.
What we would recommend: keep the fast jsdom suite, give a browser project to the handful of components whose failures only exist once something has been laid out, and leave whole journeys to the end-to-end suite. If Storybook Test or Vitest browser mode is already running green in your pipeline, the answer is often to leave it alone, and you will hear that on the first call, before anything is scoped.
Where this stops
- A component test is not coverage of a flow. It exercises one component inside its own boundary. Routing, auth, real data and anything that crosses a screen boundary stay in the end-to-end suite, and we will not sell you a component layer as a substitute for one.
- We do not delete the suite you have. If reading your repository says the jsdom suite should stay, that is the recommendation you get.
- Past React and Vue, somebody has to write the gallery contract. That framework-specific module is real work for Svelte, Solid, Angular or anything home-grown. The documentation's answer is to point a coding agent at the contract; ours is to make you name the framework before anybody quotes the job.
- Mobile means a mobile browser. Playwright emulates a phone-sized viewport, the user agent string that goes with it and touch events, so your components can be exercised at those widths. It does not drive an installed iOS or Android application, and no version of this engagement does.
- WebKit is not Safari on a phone. The WebKit project runs the engine build Playwright ships, which is not the browser your users open on an iPhone. Sign-off on a real iOS device is device work and we do not provide it.
- No load testing and no security testing. A component suite exercises one component in one browser, so nothing in a green run says anything about what happens under a thousand concurrent users. That work belongs to k6, JMeter or Gatling. Penetration testing is not offered here in any framework.
Handover ends at the written record and the pipeline. Being walked through that record with your team is not inside this price. Say so in the first email if you want it and we will scope it as its own thing, by the hour. The nearest thing already on sale is training, which is an engineer of ours going through a Playwright suite with the people who have to maintain it — the same shape of work, on the suite rather than on this record.
What it costs
Engineers are billed hourly, from $50 an hour, depending on where the engineer sits. The minimum engagement is one full-time engineer for one month.
The cost here turns on build work more than on test work. How many components are worth a browser at all. How many rendering paths the application has once theming, providers and global CSS are counted. Whether the dev server comes up clean somewhere that is not a developer's machine. How many frameworks live in the repository. And whether a Storybook or Vitest browser suite already exists that this has to sit beside rather than replace.
Read first
- Component testing in Playwright 1.62. What a story is, what the mount fixture does, and the same component green in jsdom and red in Chromium on the same interaction.
- Fixtures, and what belongs inside one. Where
mountcomes from and how the rest of a suite's setup is built the same way. - Baselines, tolerances and what to snapshot. What it takes for a screenshot of a component to keep passing in a pipeline.
- The page object model, and whether you still need one. Where that abstraction pays in an end-to-end suite, and why a spec scoped to one component's locator rarely needs it.
Questions
Do we have to replace our Vitest or Jest component tests?
No. The suite you have keeps every component whose behaviour a simulated DOM can see, which is most of them. What moves into a browser is the short list where the bug is in the layout, the stacking order or a stylesheet. Naming that list is part of the first phase, and on most repositories it comes out shorter than the team expected.
We already have Storybook. Do we need this?
Often not. Storybook Test renders your stories in a browser and uses Playwright to do it by default, so the browser is already real and fidelity is not the difference. The difference is where a test is allowed to stand: Playwright's model keeps it outside the component, with no access to the instance or the internals. If your Storybook run is green and people trust it, keep it.
Do you need access to our repository and our dev server?
Yes, and there is no version of this that runs from outside them. The gallery is a file in your repository and your own dev server serves it, so the work happens against your bundler config, your aliases and your CSS. We need a branch, the command that starts the dev server, and somebody who can approve a change to the pipeline.
Is Playwright component testing still experimental?
No. It became stable in 1.62. The mount fixture now comes from plain @playwright/test with no separate package to install, and the model supersedes the @playwright/experimental-ct-react and @playwright/experimental-ct-vue packages. Nothing published says when those two will be removed, so a suite sitting on them still runs, and moving it across is a scoping question with no deadline on it.
Do we need the audit before you start?
No. Most teams arrive knowing the job, and here it is usually a list of components somebody has already argued about. The audit is there for a team that wants to know what it has before it picks a direction. It is optional, it is billed by the hour, and how long it takes depends on the project. The minimum of one full-time engineer for one month does not apply to it.
Send the command that starts your dev server
That line, plus whether it comes up on a machine with nothing configured on it, is where the scoping starts. Add the components you would put in a browser first and say which framework they are written in. We do not need an audit to start. If the answer is that your Storybook or Vitest browser suite is already doing this, that is what you will be told.