Component Testing with Playwright
Playwright's experimental Component Testing (CT) packages — @playwright/experimental-ct-react, -vue, and -svelte — let you mount a single UI component in isolation inside a real browser and interact with it using the exact same locator and assertion API as full end-to-end tests, rather than testing the entire application through a URL. This sits between unit tests (which typically run in a simulated DOM like jsdom and never load real CSS or a real browser engine) and full end-to-end tests (which exercise an entire running application including routing, backend calls, and authentication), giving you fast feedback on a component's rendering and interaction behavior without needing the whole app running.
Cricket analogy: It's like a batting coach running a player through throwdowns in an indoor net facility to isolate and fix a specific shot, rather than needing a full live match to evaluate that one technique.
Setting Up Component Testing
Running npm init playwright@latest --ct scaffolds a project with a dedicated playwright-ct.config.ts (separate from the regular playwright.config.ts), a testDir pointing at component spec files, and a Vite-powered dev server that Playwright starts automatically to actually render your framework's components — this is the key architectural difference from tools like React Testing Library, since Playwright CT renders inside a genuine browser via Vite rather than a simulated DOM, so real CSS, real layout, and real browser APIs are all present during the test. Component test files use the .ct.tsx (or .ct.ts, .ct.jsx) extension by convention so Playwright's test runner and build tooling can distinguish them from full end-to-end specs in the same repository.
Cricket analogy: It's like setting up a dedicated indoor academy with real turf pitches instead of a gym mat, so a batter's footwork drills happen under conditions close enough to a real match surface to actually transfer.
Writing Component Tests
A component test imports mount from @playwright/experimental-ct-react (or the equivalent Vue/Svelte package) and calls it with JSX, which renders the component into the real browser page and returns a locator-like handle you interact with exactly as you would in an end-to-end test — clicking buttons, filling inputs, and asserting with expect(component).toContainText() or expect(component.getByRole('button')).toBeEnabled(). Because mount() accepts props and slots directly, you can test a component across many prop combinations (loading state, error state, empty state) in separate, fast test cases without navigating any application routes or standing up a backend, and event handlers passed as props (like onClick) can be plain test-double functions asserted against afterward.
Cricket analogy: It's like a bowling coach feeding a specific delivery — say a yorker at leg stump — and immediately checking the batter's exact foot movement in response, rather than waiting for that exact match situation to occur naturally in a live game.
Component Testing vs E2E Testing
Component tests trade completeness for speed and isolation: because there's no real backend, routing, or authentication layer involved, a component test that mounts a single ProductCard with mock props runs in milliseconds and fails only when that component's own rendering or interaction logic breaks, making it far easier to pinpoint the root cause than an end-to-end failure that could stem from the frontend, backend, network, or any layer in between. The tradeoff is that component tests can't catch integration bugs — a ProductCard component test passing tells you nothing about whether the real API actually returns the props shape the component expects — so mature test suites use component tests for exhaustive coverage of a component's states and edge cases, while reserving a smaller number of true end-to-end tests to verify the critical user journeys actually work when every real layer is wired together.
Cricket analogy: It's like a fielding coach running isolated slip-catching drills that pinpoint exactly which hand position causes a drop, versus a full live match where a dropped catch could stem from bad light, footwork, or the bowler's seam position — harder to diagnose but the only true test of match conditions.
// ProductCard.ct.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { ProductCard } from './ProductCard';
test('renders price and add-to-cart button', async ({ mount }) => {
const component = await mount(
<ProductCard name="Wireless Mouse" price={29.99} inStock={true} />
);
await expect(component).toContainText('Wireless Mouse');
await expect(component).toContainText('$29.99');
await expect(component.getByRole('button', { name: 'Add to cart' })).toBeEnabled();
});
test('disables button and shows badge when out of stock', async ({ mount }) => {
const component = await mount(
<ProductCard name="Wireless Mouse" price={29.99} inStock={false} />
);
await expect(component.getByText('Out of stock')).toBeVisible();
await expect(component.getByRole('button', { name: 'Add to cart' })).toBeDisabled();
});Component tests run through a real Vite dev server that Playwright manages automatically, so actual CSS, actual browser layout, and real DOM APIs are present during the test — this is why Playwright CT catches CSS/layout bugs that jsdom-based unit testing tools structurally cannot detect.
The @playwright/experimental-ct-* packages carry 'experimental' in their name for a reason — the mount() API and configuration surface have changed across Playwright releases, so pin your Playwright version deliberately and review the changelog before upgrading a component-testing-heavy suite.
- Playwright Component Testing mounts a single framework component in a real browser via mount(), using the same locator/assertion API as E2E tests.
- It sits between jsdom-based unit tests and full end-to-end tests: real browser rendering, but no backend, routing, or auth layer involved.
- npm init playwright@latest --ct scaffolds a dedicated playwright-ct.config.ts and a Vite-powered dev server for rendering components.
- Component spec files conventionally use the .ct.tsx/.ct.ts extension to distinguish them from full E2E specs.
- mount() accepts props directly, making it fast to test many component states (loading, error, empty) without a real backend.
- Component tests isolate failures to the component itself, making root-causing far faster than a full E2E failure that could stem from any layer.
- Component tests can't catch integration bugs (e.g., API shape mismatches), so mature suites pair them with a smaller set of true E2E tests.
Practice what you learned
1. What is the key architectural difference between Playwright Component Testing and jsdom-based unit testing tools?
2. What API does a Playwright component test use to render a component before interacting with it?
3. What is a key limitation of component tests compared to end-to-end tests?
4. What file extension convention distinguishes Playwright component test files from full end-to-end specs?
5. Why should teams pin their Playwright version carefully when relying heavily on component testing?
Was this page helpful?
You May Also Like
Fixtures and Test Hooks
How Playwright's fixture system and lifecycle hooks provide reusable, isolated setup and teardown for tests.
Cross-Browser and Device Emulation
How Playwright's project configuration runs the same suite across Chromium, Firefox, and WebKit, and emulates mobile devices, locales, and permissions.
Tracing and Debugging with Playwright Inspector
How to use Playwright's Trace Viewer and Inspector to diagnose flaky and failing tests, both locally and in CI.