QA How-To
Run Playwright Component Tests with Vite in CI: Tutorial
Playwright component tests Vite CI tutorial for React: configure mounting, coverage-ready tests, GitHub Actions, reports, caching, and reliable builds.
18 min read | 2,568 words
TL;DR
Install Playwright Component Testing for React, create a CT config that passes your Vite settings through ctViteConfig, mount real components, and run the suite in GitHub Actions after installing Chromium with dependencies. Preserve reports as artifacts so every failure is diagnosable.
Key Takeaways
- Use @playwright/experimental-ct-react with a dedicated playwright-ct.config.ts file.
- Import the existing Vite aliases and plugins through ctViteConfig instead of rebuilding app configuration by hand.
- Mount components in a real browser and assert behavior with accessible locators.
- Install only the CI browser and its operating system dependencies to keep the job focused.
- Upload the HTML report and test results even when a CI test fails.
- Use retries only in CI, and investigate flaky tests instead of hiding them.
- Keep component tests isolated from backend services by passing props or intercepting network requests deliberately.
A reliable playwright component tests vite ci tutorial needs to solve three connected problems: render React components with the same Vite behavior as the application, exercise them in a real browser, and reproduce those checks on every pull request. This guide gives you a paste-ready setup for all three.
If you are choosing what belongs at the component layer or need the broader architecture first, read the Playwright Component Testing for React complete guide. Here, you will focus on one outcome: a React component test that passes locally and in GitHub Actions with useful failure artifacts.
The examples use TypeScript, React, Vite, and Playwright's React component testing package. Playwright Component Testing remains labeled experimental, so pin compatible package versions, review release notes during upgrades, and commit the lockfile.
What You Will Build
By the end, you will have:
- A dedicated Playwright CT configuration that reuses Vite aliases and React plugins.
- A small React component with browser-level interaction tests.
- A consistent local command for headless and interactive runs.
- A GitHub Actions workflow that installs Chromium and runs component tests.
- HTML and machine-readable artifacts retained after failures.
The final path is short: Vite compiles the component test harness, Playwright opens Chromium, the test mounts the component, and CI runs the same command from a clean checkout.
Prerequisites
Use Node.js 20 or 22 LTS, npm with a committed package-lock.json, React 18 or newer, and Vite 5 or newer. You also need a GitHub repository if you want to run the workflow unchanged. The commands assume an existing Vite React TypeScript project.
Confirm your tools:
node --version
npm --version
Install the component test runner and Chromium. Keep @playwright/test and @playwright/experimental-ct-react on the same exact version in your real project. Omitting a version below lets npm resolve the current compatible release and record it in the lockfile.
npm install --save-dev @playwright/test @playwright/experimental-ct-react
npx playwright install chromium
On Linux CI, use npx playwright install --with-deps chromium. That command installs the browser plus required operating system libraries. Do not commit downloaded browsers.
Step 1: Create a Component That Exposes User Behavior
Create src/components/QuantityPicker.tsx. The component has visible state, accessible buttons, a boundary, and a callback that a test can observe.
import { useState } from 'react';
type QuantityPickerProps = {
initialValue?: number;
maximum?: number;
onChange?: (value: number) => void;
};
export function QuantityPicker({
initialValue = 1,
maximum = 5,
onChange,
}: QuantityPickerProps) {
const [quantity, setQuantity] = useState(initialValue);
const update = (nextValue: number) => {
setQuantity(nextValue);
onChange?.(nextValue);
};
return (
<section aria-label="Quantity picker">
<output aria-live="polite">Quantity: {quantity}</output>
<button
type="button"
onClick={() => update(quantity - 1)}
disabled={quantity <= 1}
>
Decrease
</button>
<button
type="button"
onClick={() => update(quantity + 1)}
disabled={quantity >= maximum}
>
Increase
</button>
</section>
);
}
This API makes the test meaningful without exposing implementation details. The browser clicks buttons and reads accessible output. A disabled state prevents invalid input, while onChange gives the parent a normal integration point.
Verify the step: import QuantityPicker into an existing app page and run npm run dev. You should see Quantity: 1; Decrease should be disabled, and Increase should update the output. Remove the temporary page import after checking it.
Step 2: Add the Playwright CT Vite Configuration
Create playwright-ct.config.ts in the repository root. The test directory is separate from end-to-end tests, and ctViteConfig carries the alias used by application imports.
import { defineConfig, devices } from '@playwright/experimental-ct-react';
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
testDir: './src',
testMatch: '**/*.ct.tsx',
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: process.env.CI
? [['line'], ['html', { open: 'never' }], ['junit', { outputFile: 'test-results/ct-junit.xml' }]]
: [['list'], ['html', { open: 'never' }]],
use: {
...devices['Desktop Chrome'],
trace: 'on-first-retry',
screenshot: 'only-on-failure',
ctViteConfig: {
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
},
},
outputDir: 'test-results/component',
});
testMatch prevents ordinary unit or end-to-end files from entering this suite. forbidOnly protects CI from an accidentally committed test.only. Two CI workers are a conservative default, not a universal optimum. Increase or remove that limit after observing your runner's memory and duration.
If your vite.config.ts contains essential plugins, CSS preprocessors, or aliases, put their shared values in a small Vite configuration module and import them into both configs. Avoid importing a configuration that starts services or depends on development-only environment variables.
Verify the step: run npx playwright test --config=playwright-ct.config.ts --list. The command should load the configuration without syntax or module resolution errors. Seeing zero tests is expected until Step 3.
Step 3: Mount the React Component and Assert Behavior
Create src/components/QuantityPicker.ct.tsx beside the component. JSX in the test makes the mounted props easy to read.
import { expect, test } from '@playwright/experimental-ct-react';
import { QuantityPicker } from '@/components/QuantityPicker';
test('increments and enforces the maximum', async ({ mount }) => {
const component = await mount(
<QuantityPicker initialValue={1} maximum={2} />
);
const output = component.getByRole('status');
const increase = component.getByRole('button', { name: 'Increase' });
await expect(output).toHaveText('Quantity: 1');
await increase.click();
await expect(output).toHaveText('Quantity: 2');
await expect(increase).toBeDisabled();
});
test('notifies the parent when quantity changes', async ({ mount }) => {
let receivedValue: number | undefined;
const component = await mount(
<QuantityPicker onChange={(value) => { receivedValue = value; }} />
);
await component.getByRole('button', { name: 'Increase' }).click();
await expect.poll(() => receivedValue).toBe(2);
});
Playwright maps <output> to the status role, so the first locator reflects the accessibility tree. Component-scoped locators prevent accidental matches outside the mounted root. Web-first assertions retry while the browser updates, which is safer than fixed sleeps.
Callbacks passed across the mount boundary are supported by Playwright's component test plumbing. Keep callback effects serializable and observable. For a detailed foundation on mounting, props, events, and hooks, use the step-by-step React component mounting tutorial.
Verify the step: run npx playwright test --config=playwright-ct.config.ts. Expect two passed tests. If getByRole('status') fails, inspect the accessibility snapshot and confirm the component still renders an output element.
Step 4: Load Global CSS and Providers Through Hooks
Real components often require global CSS, a router, a theme, or application context. Playwright CT discovers playwright/index.tsx and playwright/index.html as the test harness entry files. Create the directory and add the React hook file.
// playwright/index.tsx\nimport '../src/index.css';\nimport { beforeMount } from '@playwright/experimental-ct-react/hooks';\n\nexport type HooksConfig = {\n useSection?: boolean;\n};\n\nbeforeMount<HooksConfig>(async ({ App, hooksConfig }) => {\n if (hooksConfig?.useSection) {\n return <section aria-label=\"Test wrapper\"><App /></section>;\n }\n\n return <App />;\n});
Then add the host document:
<!-- playwright/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Component tests</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./index.ts"></script>
</body>
</html>
Keep the default hook light. If every component needs a provider, wrap App there. If providers vary by test, pass typed hooksConfig options when mounting. The React context mocking with Playwright component tests guide covers provider wrappers and per-test context values without leaking state.
Verify the step: rerun the tests, then run them interactively with npx playwright test --config=playwright-ct.config.ts --ui. Open the mounted component and confirm global fonts or layout rules from src/index.css apply. Pass { hooksConfig: { useSection: true } } as the second argument to mount and confirm the mounted root appears inside the Test wrapper region.
Step 5: Add Stable npm Scripts and Ignore Artifacts
Add focused scripts to package.json. Keep the existing scripts intact.
{
"scripts": {
"test:ct": "playwright test --config=playwright-ct.config.ts",
"test:ct:ui": "playwright test --config=playwright-ct.config.ts --ui",
"test:ct:debug": "playwright test --config=playwright-ct.config.ts --debug",
"test:ct:report": "playwright show-report"
}
}
Add generated output to .gitignore:
playwright-report/
test-results/
blob-report/
Use scripts as the public interface for developers and CI. This prevents the workflow from drifting into a subtly different command. Do not put --update-snapshots in the normal CI script because CI should detect visual changes, not approve them.
Verify the step: run npm run test:ct, then npm run test:ct:report. The first command should pass. The second should open or serve the HTML report created in playwright-report. Stop the report server with Ctrl+C if it does not exit automatically.
Step 6: Playwright Component Tests Vite CI Tutorial Workflow
Create .github/workflows/component-tests.yml. It installs dependencies deterministically, installs only Chromium with Linux libraries, runs the same npm script, and retains diagnostics.
name: Component tests
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
playwright-ct:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install JavaScript dependencies
run: npm ci
- name: Install Chromium and system dependencies
run: npx playwright install --with-deps chromium
- name: Run component tests
run: npm run test:ct
env:
CI: true
- name: Upload HTML report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-component-report
path: playwright-report/
if-no-files-found: ignore
retention-days: 14
- name: Upload raw test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-component-results
path: test-results/
if-no-files-found: ignore
retention-days: 14
npm ci enforces the lockfile. The Playwright install command downloads the browser version expected by the installed package. Artifact steps use !cancelled() so they run after success or failure, but not after a manually cancelled job. For a deeper workflow breakdown, read GitHub Actions for Playwright. Adjust retention to your organization\x27s policy.
Do not cache Playwright browser binaries by default. Browser downloads are versioned, caches become stale, and restoring a large cache can erase the expected gain. The npm cache in setup-node caches package downloads while npm ci still creates a clean dependency tree.
Verify the step: commit the workflow on a branch and open a pull request. The Component tests / playwright-ct check should pass. Open its artifacts and confirm both the HTML report and JUnit file are available.
Step 7: Make the CI Suite Production-Ready
Add boundaries before the suite grows. Use tags to separate slow or platform-specific checks, and use Playwright projects only when browser coverage is worth the additional time. Component tests usually get fast feedback from Chromium; a smaller scheduled job can cover Firefox and WebKit if cross-browser rendering matters.
| Decision | Recommended default | Expand when |
|---|---|---|
| Browser | Chromium | CSS, browser APIs, or customer usage require more |
| Workers | 2 in CI | Memory is stable and timing data supports more |
| Retries | 2 in CI, 0 locally | Never increase merely to silence failures |
| Trace | First retry | Use retain-on-failure for rare non-reproducible failures |
| Network | Mock at component boundary | Add a contract or E2E test for real integration |
| Visual snapshots | Small, stable surfaces | Avoid entire responsive pages at component scope |
For API-backed components, create a small component and intercept its known endpoint before mounting:\n\ntsx\nimport { useEffect, useState } from 'react';\n\ntype Profile = { name: string };\n\nexport function ProfileCard() {\n const [profile, setProfile] = useState<Profile | null>(null);\n\n useEffect(() => {\n void fetch('/api/profile')\n .then((response) => response.json() as Promise<Profile>)\n .then(setProfile);\n }, []);\n\n return profile ? <h2>{profile.name}</h2> : <p>Loading profile</p>;\n}\n\n\nThen import it in the component test and register the route before mount:\n\n```tsx\nimport { expect, test } from '@playwright/experimental-ct-react';\nimport { ProfileCard } from './ProfileCard';\n\ntest('shows a loaded profile', async ({ mount, page }) => {
await page.route('**/api/profile', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ name: 'Ada' }),
});
});
const component = await mount(
Register the route before mounting so the initial request cannot race ahead. Test success, empty, and error states with explicit responses. For rendering failures, follow the [React error boundary component testing guide](/resources/test-react-error-boundaries-with-playwright) and assert the fallback users actually see.
### Split feedback without duplicating configuration
As the suite grows, keep one configuration and filter work through projects, tags, or file patterns. Do not copy the whole config into a second CI file. Duplicate configurations drift in reporters, aliases, browser revisions, and retry policy.
For example, mark a visual check in its title and exclude it from the pull request command when snapshots run on a scheduled workflow:
```tsx
test('visual: quantity picker default state', async ({ mount }) => {
const component = await mount(<QuantityPicker />);
await expect(component).toHaveScreenshot('quantity-picker.png');
});
{
"scripts": {
"test:ct:pr": "playwright test --config=playwright-ct.config.ts --grep-invert visual:",
"test:ct:visual": "playwright test --config=playwright-ct.config.ts --grep visual:"
}
}
Snapshot files must be generated and reviewed on the same operating system and browser family used by CI. Font rendering and native controls can differ across platforms. Prefer a Linux container for intentional snapshot updates if GitHub's Ubuntu runner is the source of truth. Keep screenshot regions small, disable animation through component props or CSS, and wait for fonts or data explicitly before asserting.
Protect the main branch
Once the workflow is stable, configure the repository to require the component test check before merge. Give the workflow and job durable names because branch protection identifies the resulting check by name. If you rename either one, update the branch rule.
Do not place secrets in component tests unless a genuine external dependency requires them. Controlled route responses and provider values are faster and safer. If a component test needs a secret to render, reconsider the boundary because browser-delivered code cannot keep a secret anyway.
Track duration and failure categories over time. A sudden increase in harness startup time can indicate a heavy Vite plugin or dependency. Repeated first-attempt failures with successful retries indicate flakiness that deserves a ticket and an owner, not a higher retry count.
Verify the step: temporarily change the mocked status to 500 and confirm the test fails with a useful assertion. Restore 200, rerun npm run test:ct, and ensure the suite is green before committing.
Before enabling the required check, run it on several representative pull requests. Confirm dependency-only changes trigger it, failed assertions preserve artifacts, and a cancelled run does not block artifact steps indefinitely. Document the local reproduction command in your contributor guide. Developers should be able to copy the exact CI command, run it from a clean checkout, and inspect the same report without guessing which flags the server used.
Troubleshooting
Problem: Vite cannot resolve an @/ import -> Add the same alias to ctViteConfig, or import a shared alias object into both Vite configurations. Confirm letter case because Linux CI is case-sensitive even when a local file system is not.
Problem: Tests pass locally but CI cannot launch Chromium -> Run npx playwright install --with-deps chromium after npm ci. Do not use an arbitrary system Chromium unless you intentionally maintain that compatibility.
Problem: The browser shows an unstyled component -> Import the global stylesheet in playwright/index.tsx. Also reproduce required PostCSS, Tailwind, CSS module, and preprocessor settings through the CT Vite configuration.
Problem: A provider-dependent component throws during mount -> Wrap the mounted app in beforeMount or a small test wrapper. Reset mutable provider state per mount so parallel tests do not share it.
Problem: CI hangs after tests complete -> Look for application intervals, open sockets, or third-party scripts created by the component. Avoid starting a Vite dev server yourself because Playwright CT manages its own Vite-powered harness. A webServer block is for a separately served application, not the normal CT setup.
Problem: The HTML artifact is empty after failure -> Keep open: 'never', upload playwright-report/ with if: ${{ !cancelled() }}, and confirm no later command deletes the directory. Upload test-results/ separately for traces, screenshots, and JUnit output.
Where To Go Next
You now have a clean component pipeline, so deepen it according to the risk in your UI:
- Use the complete Playwright Component Testing for React guide to decide suite boundaries and long-term structure.
- Practice mounting React components step by step when components need complex props, slots, or callbacks.
- Add controlled dependencies with the React context mocking tutorial.
- Protect failure UX by learning to test React error boundaries with Playwright.
Treat component tests as one layer. Unit tests still fit pure functions, and end-to-end tests still prove routing, deployed configuration, authentication, and backend integration.
Interview Questions and Answers
Q: Why does Playwright Component Testing use Vite?
Vite bundles and serves the component harness quickly while supporting modern TypeScript, JSX, CSS, and plugins. Playwright controls a real browser against that harness. The test therefore gets browser behavior without launching the full deployed application.
Q: Should a component test configuration include webServer?
Normally, no. Playwright CT starts and manages its Vite-powered component environment. Use webServer for a separate application or service only when the test genuinely requires it, and prefer isolated component dependencies.
Q: Why install browsers after npm ci in CI?
The installed Playwright package determines the expected browser revision. Running the browser installation afterward aligns the executable with that package and lockfile. --with-deps also supplies Linux libraries absent from many clean runners.
Q: How do you control flaky component tests?
Use accessible locators, web-first assertions, isolated state, deterministic network mocks, and traces. Keep retries CI-only and inspect every retry. A retry is diagnostic resilience, not proof that a flaky test is acceptable.
Q: When should component tests run across three browsers?
Add browsers when the component depends on browser-specific APIs, complex CSS, or a supported-browser commitment. Chromium-only pull request checks are a practical default. Run broader coverage on high-risk changes or a schedule if feedback time matters.
Q: How should CI retain Playwright failures?
Generate an HTML report plus raw test results. Upload them under an always-after-completion condition such as !cancelled(). Traces, screenshots, and JUnit XML serve different debugging and reporting needs. The Playwright trace on retry guide shows how to turn a retained trace into a root-cause timeline.
Best Practices
- Pin Playwright packages together and commit the lockfile.
- Keep test files close to components, but isolate them with a
.ct.tsxsuffix. - Prefer roles, names, and visible behavior over CSS selectors or React internals.
- Register network routes before mounting.
- Reuse Vite configuration deliberately, without importing development side effects.
- Run zero retries locally so failures remain visible during development.
- Upload reports on failed CI jobs and inspect traces before rerunning.
- Review experimental CT release notes before dependency upgrades.
Avoid testing every prop combination. Choose states that represent meaningful user risk: defaults, boundaries, loading, errors, accessibility, and callbacks. If a test requires half the application to mount, it may belong at the end-to-end layer or indicate that the component needs a smaller boundary.
Conclusion: Playwright Component Tests Vite CI Tutorial
This playwright component tests vite ci tutorial gives you a repeatable route from a React component to a protected pull request. Configure the CT runner once, mount through the browser, keep Vite aliases and styles aligned, and run the identical npm command on a clean Linux worker.
Start with Chromium and a small behavior-focused suite. Preserve the HTML report, traces, screenshots, and JUnit output, then expand browsers and coverage only when product risk justifies the cost.
Interview Questions and Answers
What is the difference between a Playwright component test and an end-to-end test?
A component test mounts a UI component in a browser harness and controls its dependencies, while an end-to-end test starts from the deployed or fully served application. Component tests give faster, narrower feedback. End-to-end tests prove routing, services, configuration, and complete user journeys.
How does Vite participate in Playwright Component Testing?
Vite compiles and serves the mount harness, including React JSX, TypeScript, CSS, and configured plugins. Playwright then drives a real browser against that harness. ctViteConfig supplies settings needed specifically by component tests.
What makes a Playwright component test CI-safe?
Use a locked dependency install, install the matching browser and Linux dependencies, forbid focused tests, isolate state, and avoid uncontrolled services. Use web-first assertions instead of sleeps. Always retain reports and traces when failures occur.
Why should Playwright packages use matching versions?
The runner, component adapter, and browser binaries evolve together. Matching versions reduce protocol and fixture compatibility risks. Pin them through the package manifest and lockfile, then install browsers after dependencies.
How would you test a component that requires React context?
Wrap the mounted component with the real provider configured with controlled test values, either in a reusable wrapper or the beforeMount hook. Reset provider state for every mount. Assert visible behavior instead of inspecting context internals.
How do you diagnose a component test that fails only in CI?
First compare Node, package, browser, environment, file casing, and worker settings. Open the uploaded HTML report and trace, then inspect console and network activity. Reproduce in a Linux container if the evidence points to an operating system difference.
Frequently Asked Questions
Can Playwright component tests run with a Vite React app?
Yes. Install @playwright/experimental-ct-react and configure the runner with playwright-ct.config.ts. The component harness uses Vite, and ctViteConfig can carry aliases and other required Vite settings.
Do Playwright component tests need a running Vite dev server?
No, not in the normal setup. Playwright Component Testing manages its own Vite-powered harness, so you should not start npm run dev or add webServer merely to mount components.
How do I run Playwright component tests in GitHub Actions?
Run npm ci, then npx playwright install --with-deps chromium, followed by your component test script. Upload playwright-report and test-results with a condition that still runs after test failure.
Why do Vite aliases fail only in Playwright component tests?
The CT harness has its own Vite configuration context. Add aliases through ctViteConfig or import shared alias settings into both your application and component test configurations, and verify path casing on Linux.
Should I cache Playwright browsers in CI?
Usually not. Browser caches are large and tightly coupled to Playwright versions, so stale restores can create confusing failures. Cache npm downloads and install the matching Chromium revision during the job.
Is Playwright Component Testing stable in 2026?
It remains distributed under experimental component testing packages. It can be useful in production pipelines, but you should pin versions, commit the lockfile, and review Playwright release notes before upgrading.
Related Guides
- How to Run tests in headed mode in Playwright (2026)
- How to Run tests in headed mode in Cypress (2026)
- How to Run tests in headed mode in Selenium (2026)
- Mock React Context in Playwright Component Tests
- How to Debug a failing test in VS Code in Playwright (2026)
- How to Run a single test in Playwright (2026)