1. Introduction: Why Component Testing Matters
Modern frontend applications are built from reusable components.
A single application may contain hundreds of components:
- Buttons
- Forms
- Tables
- Modals
- Navigation menus
- Product cards
- Shopping carts
- Date pickers
- Authentication components
- Data grids
Testing every component only through end-to-end workflows is expensive and slow. Testing only implementation details with unit tests can also miss important browser behavior.
This is where Playwright component testing advanced techniques become useful.
Playwright’s current component-testing model uses stories and a gallery. A story represents one component scenario, while the gallery is a browser page that renders those stories. The built-in mount() fixture then mounts a story and returns a locator scoped to the component.
The architecture looks like:
React Component
|
v
Component Story
|
v
Playwright Gallery
|
v
Real Browser
|
v
|
+—- Props
+—- State
+—- Events
+—- API mocking
+—- Accessibility
+—- Visual testing
This provides faster feedback than complete E2E workflows while still exercising the component in a real browser.
Important: Current Playwright component testing uses the story/gallery model. The older @playwright/experimental-ct-react packages have been removed from current Playwright releases; older projects should follow the migration path rather than starting new work with those packages.
2. What Is Playwright Component Testing?
Playwright Component Testing tests individual UI components in isolation while running them inside a real browser.
Unlike a traditional unit test, the component is rendered in an actual browser environment.
That means you can test:
- Real DOM behavior
- CSS
- Browser events
- Layout
- Keyboard interaction
- Accessibility
- Screenshots
- Network behavior
- User interactions
Playwright describes the current model as a regular Playwright test running against a small story gallery page served by your own development server.
The basic flow is:
test()
|
| mount(“Button/Primary”)
v
Gallery
|
v
React Component
|
v
Locator
|
v
Click / Fill / Assert
This makes component tests especially valuable for frontend-heavy applications.
3. Component Testing vs Unit vs Integration vs E2E
A senior automation engineer should understand where each testing layer fits.
| Test type | Scope | Browser | Speed | Main purpose |
| Unit | Function/class | No | Very fast | Logic |
| Component | UI component | Yes | Fast | UI behavior |
| Integration | Multiple components/services | Sometimes | Medium | Integration |
| E2E | Complete application | Yes | Slower | User workflow |
Consider a shopping cart.
Unit test
Tests:
calculateTotal()
Component test
Tests:
Cart component
+
quantity controls
+
remove button
+
total display
E2E test
Tests:
Login
→ Product
→ Add to cart
→ Cart
→ Checkout
→ Payment
A scalable Playwright framework should use all appropriate layers rather than forcing every test into E2E.
4. When Should You Use Playwright Component Testing?
Component testing is a strong choice when you need fast feedback about:
- Component rendering
- Props
- State transitions
- Events
- Form behavior
- Loading states
- Error states
- Empty states
- Accessibility
- Visual appearance
- Responsive behavior
For example:
ProductCard
|
+– Default
+– Discounted
+– Out of stock
+– Loading
+– Error
Testing these states individually is much faster than reaching every state through a full application workflow.
Use E2E tests for critical workflows that require real application integration.
5. Playwright Component Testing Project Setup
A current Playwright setup uses normal @playwright/test rather than the former experimental React component-testing package. The gallery is served by your application’s development server.
A practical project might look like:
playwright-component/
├── src/
│ ├── components/
│ │ ├── ProductCard.tsx
│ │ └── ProductCard.story.tsx
│ └── main.tsx
├── playwright/
│ └── gallery/
│ └── index.html
├── tests/
│ └── components/
│ └── product-card.spec.ts
├── playwright.config.ts
├── package.json
└── tsconfig.json
npm install -D @playwright/test
Playwright’s installation documentation provides the current browser installation commands and project setup guidance.
6. Configuring Playwright Component Testing for React
The key configuration is a component project whose baseURL points to the gallery.
// playwright.config.ts
import {
defineConfig,
devices
} from ‘@playwright/test’;
export default defineConfig({
testDir: ‘./tests’,
projects: [
{
name: ‘components’,
testDir: ‘./tests/components’,
use: {
…devices[‘Desktop Chrome’],
baseURL:
‘http://localhost:5173/playwright/gallery/index.html’,
serviceWorkers: ‘block’
}
}
],
webServer: {
command: ‘npm run dev’,
url:
‘http://localhost:5173/playwright/gallery/index.html’,
reuseExistingServer: !process.env.CI
}
});
Playwright’s current documentation shows this gallery-based configuration, including baseURL, webServer, and blocking service workers when route mocking needs to control requests.
7. Creating a React Component Story
Suppose we have:
// src/components/Button.tsx
type ButtonProps = {
title: string;
disabled?: boolean;
onClick?: () => void;
};
export function Button({
title,
disabled = false,
onClick
}: ButtonProps) {
return (
<button
disabled={disabled}
onClick={onClick}
>
{title}
</button>
);
}
Create a story:
// src/components/Button.story.tsx
import { Button } from ‘./Button’;
export const Primary = () => (
<Button title=”Submit” />
);
export const Disabled = () => (
<Button
title=”Submit”
disabled
/>
);
A story represents one specific scenario.
Playwright’s documentation recommends this model because the story owns scenario-specific props, mock data, providers, and callbacks.
8. Mounting and Testing a React Component
The test mounts the story:
// tests/components/button.spec.ts
import {
test,
expect
} from ‘@playwright/test’;
test(‘renders primary button’, async ({
mount
}) => {
const component =
await mount(
‘components/Button/Primary’
);
await expect(
component.getByRole(‘button’)
).toHaveText(‘Submit’);
});
The important sequence is:
Component
↓
Story
↓
mount()
↓
Locator
↓
Interaction
↓
Assertion
The mount() fixture returns a locator scoped to the mounted story, so component queries should generally start from that locator.
9. Testing Props With Playwright Component Testing
Props are one of the most important component-test dimensions.
Current Playwright component testing allows props to be passed to a story and can type-check them when you provide the story type.
Example story:
export const WithTitle = ({
title
}: {
title: string;
}) => (
<Button title={title} />
);
Test:
import type {
WithTitle
} from ‘../../src/components/Button.story’;
test(‘renders supplied title’, async ({
mount
}) => {
const component =
await mount<typeof WithTitle>(
‘components/Button/WithTitle’,
{
title: ‘Save’
}
);
await expect(
component.getByRole(‘button’)
).toHaveText(‘Save’);
});
This is safer than constructing arbitrary JSX inside every test.
10. Testing Prop Changes With update()
Advanced component tests often need to verify what happens when a prop changes.
Playwright’s current component-testing model provides component.update(). It re-renders the existing story while preserving component state.
Example:
test(‘updates component props’, async ({
mount
}) => {
const component =
await mount<typeof WithTitle>(
‘components/Button/WithTitle’,
{
title: ‘Save’
}
);
await expect(
component.getByRole(‘button’)
).toHaveText(‘Save’);
await component.update({
title: ‘Submit’
});
await expect(
component.getByRole(‘button’)
).toHaveText(‘Submit’);
});
The lifecycle is:
Mount
↓
Initial props
↓
Assertion
↓
update()
↓
New props
↓
Assertion
This is particularly useful for components driven by changing application state.
11. Testing Component State
Consider an expandable component:
import {
useState
} from ‘react’;
export function Expandable() {
const [
expanded,
setExpanded
] = useState(false);
return (
<>
<button
onClick={() =>
setExpanded(!expanded)
}
>
Details
</button>
{expanded && (
<div>
Additional information
</div>
)}
</>
);
}
A story can expose observable state:
export const Stateful = () => {
const [
expanded,
setExpanded
] = useState(false);
return (
<>
<Expandable
expanded={expanded}
setExpanded={setExpanded}
/>
<input
data-testid=”expanded”
hidden
readOnly
value={String(expanded)}
/>
</>
);
};
Test:
test(‘expands on click’, async ({
mount
}) => {
const component =
await mount(
‘components/Expandable/Stateful’
);
await component
.getByRole(‘button’, {
name: ‘Details’
})
.click();
await expect(
component.getByTestId(‘expanded’)
).toHaveValue(‘true’);
});
This follows an important Playwright principle: test observable behavior rather than accessing internal component instances.
12. Testing Buttons, Forms, and Events
A component test should behave like a user.
For a login form:
test(‘login form submits credentials’, async ({
mount
}) => {
const component =
await mount(
‘components/LoginForm/Default’
);
await component
.getByLabel(‘Email’)
.fill(‘qa@example.com’);
await component
.getByLabel(‘Password’)
.fill(‘secret’);
await component
.getByRole(‘button’, {
name: ‘Sign in’
})
.click();
await expect(
component.getByText(
‘Signing in…’
)
).toBeVisible();
});
This tests:
Mount
↓
Fill
↓
Click
↓
Component event
↓
State update
↓
UI assertion
Avoid directly calling React methods or component instances.
13. Testing Reusable UI Components
Large frontend applications often have shared components:
components/
├── Button
├── Input
├── Modal
├── Dropdown
├── DataTable
├── DatePicker
└── Toast
Each reusable component can have stories representing meaningful scenarios.
For example:
Button/
├── Button.tsx
├── Button.story.tsx
└── button.spec.ts
Stories:
Primary
Disabled
Loading
Danger
WithIcon
LongText
This gives the team a visual and behavioral catalog of supported component states.
14. Component Testing With API Mocking
A component may depend on:
/api/products
/api/cart
/api/profile
Component testing should not require those services to be available for every scenario.
Playwright’s network interception can mock requests.
test(‘renders products from mocked API’, async ({
page,
mount
}) => {
await page.route(
‘**/api/products’,
async route => {
await route.fulfill({
status: 200,
contentType:
‘application/json’,
body: JSON.stringify({
products: [
{
id: 1,
name: ‘Laptop’,
price: 999
}
]
})
});
}
);
const component =
await mount(
‘components/ProductList/Default’
);
await expect(
component.getByText(‘Laptop’)
).toBeVisible();
});
The flow is:
Component
↓
API request
↓
page.route()
↓
Mock response
↓
Component state
↓
Rendered UI
↓
Assertion
This makes component tests deterministic and fast.
15. Testing Loading, Error, and Empty States
These states are frequently missed by E2E suites.
Create stories:
ProductList/Loading
ProductList/Empty
ProductList/Error
ProductList/Success
Then test them independently.
test(‘shows loading state’, async ({
mount
}) => {
const component =
await mount(
‘components/ProductList/Loading’
);
await expect(
component.getByText(
‘Loading products…’
)
).toBeVisible();
});
Error:
test(‘shows error state’, async ({
mount
}) => {
const component =
await mount(
‘components/ProductList/Error’
);
await expect(
component.getByRole(‘alert’)
).toContainText(
‘Unable to load products’
);
});
This provides excellent coverage without manipulating a real backend.
16. Component Testing With Fixtures
Fixtures can centralize reusable setup.
For example:
// fixtures/component.fixture.ts
import {
test as base,
expect
} from ‘@playwright/test’;
type Fixtures = {
authenticatedMount: typeof base;
};
export const test =
base.extend({});
export { expect };
In practice, the most valuable reusable abstraction is usually the story itself:
Story
|
+– Provider
+– Mock data
+– Authentication context
+– Theme
+– State
+– Callbacks
The current Playwright model explicitly positions the story as the place where component-specific setup belongs.
This is different from creating huge fixture layers that hide what the component actually needs.
17. Component Testing With Authentication and Application Context
Real components often require providers:
AuthProvider
ThemeProvider
Router
QueryClient
FeatureFlagProvider
A story can compose them:
export const Authenticated = () => (
<AuthProvider
user={{
id: ‘1’,
name: ‘Test User’,
role: ‘admin’
}}
>
<ThemeProvider>
<DashboardWidget />
</ThemeProvider>
</AuthProvider>
);
Test:
test(‘shows admin actions’, async ({
mount
}) => {
const component =
await mount(
‘components/DashboardWidget/Authenticated’
);
await expect(
component.getByRole(‘button’, {
name: ‘Manage users’
})
).toBeVisible();
});
This approach avoids reproducing the entire login flow for every component test.
18. Testing Responsive and Accessible Components
Component testing is useful for accessibility and responsive behavior because components execute in a real browser.
Test accessible names:
test(‘button is accessible’, async ({
mount
}) => {
const component =
await mount(
‘components/Button/Primary’
);
await expect(
component.getByRole(‘button’)
).toHaveAccessibleName(‘Submit’);
});
You can also run component tests under mobile projects.
{
name: ‘mobile-components’,
testDir: ‘./tests/components’,
use: {
…devices[‘Pixel 7’],
baseURL:
‘http://localhost:5173/playwright/gallery/index.html’
}
}
For deeper accessibility coverage, combine this with Playwright Accessibility Testing Advanced and @axe-core/playwright.
19. Component Testing Across Browsers
A major advantage of browser-based component testing is that the component can execute in different rendering engines.
projects: [
{
name: ‘components-chromium’,
testDir: ‘./tests/components’,
use: {
browserName: ‘chromium’,
baseURL:
‘http://localhost:5173/playwright/gallery/index.html’
}
},
{
name: ‘components-firefox’,
testDir: ‘./tests/components’,
use: {
browserName: ‘firefox’,
baseURL:
‘http://localhost:5173/playwright/gallery/index.html’
}
},
{
name: ‘components-webkit’,
testDir: ‘./tests/components’,
use: {
browserName: ‘webkit’,
baseURL:
‘http://localhost:5173/playwright/gallery/index.html’
}
}
]
This is valuable for:
- CSS differences
- Browser events
- Focus behavior
- Responsive UI
- Accessibility
- Visual rendering
20. Component Screenshot Testing
Component testing also enables visual regression.
test(‘product card visual regression‘, async ({
mount
}) => {
const component =
await mount(
‘components/ProductCard/Default’
);
await expect(component)
.toHaveScreenshot(
‘product-card.png’
);
});
The workflow becomes:
Component
↓
Mount story
↓
Stable state
↓
Screenshot
↓
Baseline comparison
↓
Visual regression
This is particularly effective for design-system components.
Use masking or stable data when components contain:
- Dates
- Random IDs
- User names
- Dynamic prices
- Animations
For broader visual testing strategies, see Playwright Visual Regression Advanced Setup.
21. Debugging Component Test Failures
Playwright’s normal debugging capabilities apply to component tests because they execute in real browsers.
Run UI Mode:
npx playwright test –ui
Playwright’s UI Mode provides test exploration, step inspection, and trace integration.
Configure tracing:
use: {
trace: ‘on-first-retry’
}
When a component test fails, inspect:
Story
↓
DOM
↓
Locator
↓
User action
↓
Network
↓
Assertion
For example:
Expected:
button “Add to Cart”
Actual:
button “Loading…”
The likely issue is not the locator. The component may be waiting for an API response.
This is why component tests should use deterministic stories and network mocks.
22. Component Testing in Parallel Execution and CI/CD
Playwright Test supports parallel execution using worker processes. By default, test files run in parallel while tests within a file run sequentially; configuration can enable broader parallelism.
For CI:
export default defineConfig({
testDir: ‘./tests’,
workers:
process.env.CI ? 2 : undefined,
reporter: process.env.CI
? ‘dot’
: ‘html’,
projects: [
{
name: ‘components’,
testDir:
‘./tests/components’,
use: {
baseURL:
‘http://localhost:5173/playwright/gallery/index.html’
}
}
],
webServer: {
command: ‘npm run dev’,
url:
‘http://localhost:5173/playwright/gallery/index.html’
}
});
The important goal is predictable isolation.
If tests depend on global mutable state, parallel execution can expose hidden coupling.
23. GitHub Actions Component Testing
A production pipeline can use:
name: Component Tests
on:
pull_request:
push:
branches: [main]
jobs:
components:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v6
– uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
– run: npm ci
– run: npx playwright install –with-deps chromium
– name: Component tests
run: npx playwright test –project=components
– name: Upload report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v5
with:
name: component-test-report
path: playwright-report/
Playwright’s CI guidance demonstrates the same general pattern: install dependencies, install browsers, execute tests, and upload the generated report.
24. Real-World E-Commerce Component Testing Project
Consider an e-commerce application with:
ProductCard
ProductGrid
SearchBox
Cart
QuantitySelector
CheckoutForm
PaymentForm
OrderSummary
A scalable component-testing matrix might be:
| Component | Scenarios |
| ProductCard | Available, sale, unavailable |
| ProductGrid | Loading, empty, populated |
| Cart | Empty, one item, many items |
| CheckoutForm | Valid, invalid, server error |
| PaymentForm | Success, decline, timeout |
| SearchBox | Empty, results, no results |
Instead of creating E2E tests for every state, component tests handle the state matrix.
E2E tests then verify the most important complete journeys:
Search
↓
Product
↓
Cart
↓
Checkout
↓
Order
This creates a healthy test pyramid:
E2E
/ \
Integration
/ \
Component Tests
/ \
Unit Tests
25. Common Playwright Component Testing Errors and Solutions
| Error | Likely cause | Solution |
| mount is not defined | Component project/gallery not configured | Verify current gallery setup |
| Story not found | Incorrect story ID | Check story path/name |
| Component renders blank | Gallery issue | Verify dev server and #root |
| Props not updating | Wrong story signature | Type story and use update() |
| API mock ignored | Service worker/cache | Block service workers where needed |
| Test is flaky | Dynamic component state | Use deterministic stories |
| Screenshot differs | Unstable content | Mock data and animations |
| Browser test fails | Browser-specific behavior | Run cross-browser projects |
| CI cannot mount | Gallery server unavailable | Check webServer configuration |
| Tests interfere | Shared state | Isolate stories and test data |
The current Playwright gallery contract requires the gallery to expose mounting and unmounting behavior, and an unknown story or rendering error should surface through the mount() call.
26. Playwright Component Testing Best Practices
1. Test behavior, not implementation
Prefer:
component.getByRole(‘button’).click()
over accessing React internals.
2. Create meaningful stories
Good:
ProductCard/OutOfStock
Weak:
ProductCard/Test1
3. Keep stories deterministic
Control:
- Dates
- Random data
- API responses
- Feature flags
- Authentication
4. Use real browser interactions
Component tests are valuable because they exercise real browser behavior.
5. Use update() for prop transitions
Don’t remount unnecessarily when the scenario requires state preservation.
6. Mock external dependencies
Components should not require production APIs for basic component-state testing.
7. Use accessibility-aware locators
Prefer:
getByRole()
getByLabel()
8. Keep E2E coverage
Component testing does not replace complete application workflows.
9. Run critical components in CI
Component tests are fast enough to provide excellent pull-request feedback.
10. Avoid giant component fixtures
The story should clearly show the providers, data, and setup required by the scenario.
11. Treat visual testing as complementary
Use screenshots for layout and visual regressions, not as the only component assertion.
12. Keep the gallery close to the application
The current architecture intentionally lets the application own the dev server, bundler configuration, CSS, aliases, and plugins.
27. Advanced Playwright Component Testing Interview Questions
1. What is Playwright Component Testing?
It is browser-based testing of individual application components using Playwright’s component-testing model.
2. How is component testing different from E2E testing?
Component tests isolate a component and its immediate dependencies. E2E tests validate complete workflows through the application.
3. How does the current Playwright component model work?
A story defines a component scenario, a gallery renders the story, and mount() loads that story in the browser.
4. Why use stories?
Stories make component scenarios explicit and reusable.
They can contain:
Props
Mock data
Providers
Callbacks
State
5. How do you test prop changes?
Use:
await component.update({
title: ‘New title’
});
The current implementation preserves component state while updating props.
6. Can Playwright component tests mock APIs?
Yes. Use Playwright network interception such as page.route() or use an in-browser mocking solution such as MSW inside the story.
7. Can component tests run in multiple browsers?
Yes. Configure Playwright projects for Chromium, Firefox, and WebKit.
8. Can component tests perform visual testing?
Yes. A mounted component can be used with Playwright screenshot assertions.
9. Should component tests replace unit tests?
No. Unit tests remain valuable for pure business logic. Component tests provide browser-level confidence for UI behavior.
10. What should not be tested?
Avoid testing private component implementation details or internal React instances. Playwright recommends observing behavior through user-facing interaction and the rendered page.
28. Playwright Component Testing Learning Roadmap
Level 1: Playwright Fundamentals
Learn:
- TypeScript
- Locators
- Assertions
- Fixtures
- Projects
- Test isolation
Level 2: React Testing
Learn:
- Props
- State
- Events
- Context
- Hooks
- Conditional rendering
Level 3: Component Testing
Learn:
- Stories
- Gallery
- mount()
- update()
- Component locators
- API mocking
Level 4: Advanced Testing
Learn:
- Authentication context
- Accessibility
- Visual testing
- Responsive testing
- Cross-browser testing
- Error/loading states
Level 5: Enterprise Architecture
Learn:
- Component test organization
- CI/CD
- Parallel execution
- Reporting
- Design-system testing
- Test ownership
- Quality gates
Related areas worth learning include Advanced Playwright Automation Techniques, Playwright Test Architecture for Large Projects, Playwright Multi-Tenant Testing Strategy, Playwright Network Mocking Advanced, Playwright Custom Reporter Development, Playwright Visual Regression Advanced Setup, Playwright Accessibility Testing Advanced, Playwright Performance Testing Techniques, Playwright Custom Fixtures Advanced, Playwright Test Sharding Advanced, Playwright Authentication Tutorial, Playwright API Testing, Playwright Page Object Model, Playwright Fixtures Tutorial, Playwright Parallel Execution Tutorial, Playwright Reporting Tutorial, Playwright CI/CD Tutorial, Playwright GitHub Actions Tutorial, Playwright Docker Tutorial, Playwright TypeScript Tutorial, Playwright Framework Design, Playwright Best Practices, and Playwright Interview Questions.
29. FAQs: Playwright Component Testing
What is Playwright Component Testing?
Playwright Component Testing runs individual UI components in a real browser and verifies their rendered behavior, interactions, state, accessibility, and visual output.
Does Playwright support React component testing?
Yes. The current Playwright model supports component testing through the story/gallery architecture and uses the standard @playwright/test package. The former experimental React component-testing packages are no longer published in current releases.
How do I mount a React component in Playwright?
Create a story for the component and mount the story:
const component =
await mount(
‘components/Button/Primary’
);
What is a Playwright component story?
A story is a small wrapper that renders a component in one defined scenario, including props, mock data, providers, and state.
Can Playwright component tests test state?
Yes. Component stories can own state and expose observable outcomes that the Playwright test can assert.
How do I test component props?
Define a typed story and pass serializable props to mount().
Can I update component props without remounting?
Yes. Use component.update(). The current gallery model preserves component state when updating props.
Can Playwright component testing mock API responses?
Yes. Use Playwright’s network interception or an in-browser mocking approach such as MSW.
Can component tests run in CI?
Yes. They are regular Playwright tests and can use Playwright’s CI, reporting, parallel execution, tracing, and artifact capabilities.
Are Playwright component tests faster than E2E tests?
Generally, yes, because the component is tested in isolation rather than navigating through an entire application workflow. However, execution time depends on browser startup, component setup, project configuration, and CI infrastructure.
Should I use component testing instead of unit testing?
No. Use unit tests for pure logic and component tests for browser-rendered UI behavior. The two layers complement each other.
