Component testing and advanced configuration
Mount components in isolation with Playwright CT, write custom reporters, use global setup, and shard a large suite across CI machines.
Component testing
npm init playwright@latest -- --ct
# a separate config and a separate test directory are created
npx playwright test -c playwright-ct.config.ts// src/components/Button.test.tsx
import { test, expect } from "@playwright/experimental-ct-react";
import { Button } from "./Button";
test("shows a busy state while the action is pending", async ({ mount }) => {
const component = await mount(
<Button onClick={() => new Promise(() => {})}>Save</Button>
);
await component.getByRole("button", { name: "Save" }).click();
await expect(component.getByRole("button")).toBeDisabled();
await expect(component.getByTestId("spinner")).toBeVisible();
});
test("emits the click event", async ({ mount }) => {
let clicks = 0;
const component = await mount(<Button onClick={() => clicks++}>Save</Button>);
await component.getByRole("button").click();
expect(clicks).toBe(1);
});| Component test | End-to-end test | |
|---|---|---|
| Speed | Very fast | Slow |
| Setup | A mount call | A running application and data |
| Catches | Rendering, props, local state, a11y | Integration, routing, real data |
| Misses | Wiring, routing, API contracts | Almost nothing, except the interior |
| Best for | States and variants | Journeys |
💡
Component testing is the right place for the state matrix - loading, empty, error, disabled, long text - because each state costs one mount instead of a journey through the application. Keep end-to-end tests for the flows a user completes, and stop duplicating state coverage in both.
Global setup and teardown
// global-setup.ts
import { chromium, type FullConfig } from "@playwright/test";
export default async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(process.env.BASE_URL! + "/health");
await page.waitForSelector("[data-ready=true]");
await browser.close();
process.env.RUN_ID = String(Date.now());
}// playwright.config.ts
export default defineConfig({
globalSetup: "./global-setup.ts",
globalTeardown: "./global-teardown.ts",
webServer: {
command: "npm run start:test",
url: "http://localhost:3000/health",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});globalSetupruns once before all workers; use it for environment checks, not for per-test data.- Workers do not share memory with the global setup process, so pass values through the environment or a file.
webServerstarts your application and waits for the URL to respond, which removes a whole class of 'connection refused' failures in CI.reuseExistingServerkeeps local development fast by reusing the dev server you already have running.
Reporters and sharding
// my-reporter.ts
import type { Reporter, TestCase, TestResult } from "@playwright/test/reporter";
class SummaryReporter implements Reporter {
private failures: string[] = [];
onTestEnd(test: TestCase, result: TestResult) {
if (result.status !== "passed") {
this.failures.push(test.title + " -> " + result.status);
}
}
onEnd() {
if (this.failures.length) {
console.log("Failed tests:");
for (const f of this.failures) console.log(" " + f);
}
}
}
export default SummaryReporter;reporter: [
["list"],
["html", { open: "never", outputFolder: "playwright-report" }],
["junit", { outputFile: "reports/junit.xml" }],
["./my-reporter.ts"],
],
workers: process.env.CI ? 2 : undefined,npx playwright test --shard=1/4 --reporter=blob
npx playwright merge-reports --reporter=html ./blob-report ./blob-report-2- Emit JUnit XML for the pipeline and HTML for a human; they answer different questions.
- Set workers explicitly in CI. The default is based on the host's CPU count, which can over-commit a shared runner.
- Use
blobreporters when sharding, then merge into one report so the failure view stays unified. - Keep the HTML report as an artefact on failure; it is the fastest way for someone else to see what broke.
FAQ
When should I use component testing instead of end-to-end?
When the behaviour you are testing is inside one component: a state that renders differently, a prop that changes the output, a keyboard interaction, an accessible name. If the behaviour depends on routing, real data or several components together, it is an end-to-end test.
How many workers should CI use?
Depends on the runner's CPU count and how heavy the tests are. Start at two on a standard hosted runner, watch for timeouts caused by resource contention, and raise it only while the suite gets faster rather than flakier.
Related
Multiple browsers, devices and emulation Visual comparisons, screenshots and video
Last refreshed 2026-09-18.