Fix: Playwright Component Testing Not Working — Mount Fixture, Vite Config, Styles, and TypeScript
Quick Answer
How to fix Playwright component testing errors — playwright-ct.config not found, mount fixture undefined, CSS not loaded in tests, Vite alias for imports, TypeScript paths, hooks (beforeMount), and snapshot strategy.
The Error
You install Playwright CT and the config isn’t found:
$ npx playwright test -c playwright-ct.config.ts
# Error: No tests found. Did you mean to run them in a different mode?Or mount is undefined:
test("Button renders", async ({ mount }) => {
await mount(<Button>Click</Button>);
// TypeError: mount is not a function
});Or styles don’t load in the test snapshot:
test("themed button", async ({ mount }) => {
const button = await mount(<Button variant="primary" />);
await expect(button).toHaveScreenshot(); // No background color in snapshot
});Or TypeScript path aliases don’t resolve:
import { Button } from "@/components/Button";
// Cannot find module '@/components/Button' or its corresponding type declarations.Why This Happens
Playwright Component Testing is a separate test runner from regular Playwright. It mounts components in a real browser via a small Vite server, then drives them with the standard Playwright API.
- Two configs. Regular
playwright.config.tsis for E2E (full browser navigations). Component testing needsplaywright-ct.config.ts(or similarly named). They use different fixtures and different file globs. - Framework adapter is required.
@playwright/experimental-ct-react,-vue,-svelte,-solid— pick the one that matches your framework. They provide themountfixture. - Vite serves your components. Your project’s CSS, aliases, and plugins need to be replicated in the CT Vite config — otherwise styles and resolution differ from your real app.
- TypeScript paths in
tsconfig.jsonaren’t auto-honored by Vite; you need a Vite plugin (vite-tsconfig-paths) or duplicate them invite.config.ts.
Fix 1: Install and Configure
For React:
npm install -D @playwright/experimental-ct-react @playwright/test
npx playwright installThen:
// playwright-ct.config.ts
import { defineConfig, devices } from "@playwright/experimental-ct-react";
export default defineConfig({
testDir: "./",
testMatch: /.*\.spec\.tsx?$/,
snapshotDir: "./__snapshots__",
timeout: 10_000,
fullyParallel: true,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
trace: "on-first-retry",
ctPort: 3100,
ctViteConfig: {
// Vite config — see Fix 3
},
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
],
});Run:
npx playwright test -c playwright-ct.config.tsFor Vue:
npm install -D @playwright/experimental-ct-vueimport { defineConfig } from "@playwright/experimental-ct-vue";
// Same shape, different framework.For Svelte / Solid: same pattern with their respective packages.
Pro Tip: Use a separate package.json script:
{
"scripts": {
"test:ct": "playwright test -c playwright-ct.config.ts",
"test:e2e": "playwright test -c playwright.config.ts"
}
}Avoids confusion about which config runs.
Fix 2: Write a Component Test
// Button.spec.tsx
import { test, expect } from "@playwright/experimental-ct-react";
import { Button } from "./Button";
test("renders children", async ({ mount }) => {
const component = await mount(<Button>Click me</Button>);
await expect(component).toContainText("Click me");
});
test("fires onClick", async ({ mount }) => {
let clicks = 0;
const component = await mount(
<Button onClick={() => clicks++}>Click</Button>
);
await component.click();
expect(clicks).toBe(1);
});
test("primary variant", async ({ mount }) => {
const component = await mount(<Button variant="primary">Save</Button>);
await expect(component).toHaveCSS("background-color", "rgb(0, 122, 255)");
});The mount fixture returns a Locator rooted at your component. All Playwright assertions and actions work on it.
Common Mistake: Importing test from @playwright/test instead of @playwright/experimental-ct-react. The latter has the mount fixture; the former doesn’t.
Fix 3: Configure Vite for Your Project
CT uses Vite to bundle your components. To match your real app’s behavior:
// playwright-ct.config.ts
import { defineConfig } from "@playwright/experimental-ct-react";
import path from "path";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
// ...
use: {
ctPort: 3100,
ctViteConfig: {
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
plugins: [tsconfigPaths()],
css: {
postcss: "./postcss.config.cjs", // For Tailwind / PostCSS
},
},
},
});Three things to replicate from your vite.config.ts:
- Aliases — for
@/componentsand similar. - PostCSS / Tailwind config — so styles process correctly.
- Plugins — anything you depend on at build time (mdx, svgr, etc.).
If you have a complex vite.config.ts, import and reuse:
import sharedConfig from "./vite.config";
export default defineConfig({
use: {
ctViteConfig: { ...sharedConfig, /* CT-specific overrides */ },
},
});Fix 4: Load Global Styles
Components that depend on global CSS need to import it in the test entry point:
// playwright/index.ts (CT's setup file)
import "../src/styles/globals.css"; // Tailwind, fonts, resets
import "../src/styles/tokens.css";Configure the entry in playwright-ct.config.ts:
export default defineConfig({
// ...
use: {
ctPort: 3100,
ctTemplateDir: "./playwright", // Directory with index.html and index.ts
},
});CT looks for playwright/index.html and playwright/index.ts (or index.tsx) by default. The index.ts is your setup; the index.html is the page template (usually a minimal <div id="root"></div>).
For Tailwind:
<!-- playwright/index.html -->
<!DOCTYPE html>
<html>
<head><title>CT</title></head>
<body>
<div id="root"></div>
<script type="module" src="./index.ts"></script>
</body>
</html>// playwright/index.ts
import "../src/styles/globals.css"; // Tailwind directives + your CSSTailwind’s content config must include the test files:
// tailwind.config.js
module.exports = {
content: [
"./src/**/*.{ts,tsx}",
"./**/*.spec.tsx", // CT test files
],
};Without the spec files in content, Tailwind purges classes that only appear in tests.
Fix 5: Hooks — beforeMount and afterMount
For provider context (theme, query client, router), wrap your mounts:
// playwright/index.ts
import { beforeMount, afterMount } from "@playwright/experimental-ct-react/hooks";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter } from "react-router-dom";
beforeMount(async ({ App, hooksConfig }) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={hooksConfig?.routes ?? ["/"]}>
<App />
</MemoryRouter>
</QueryClientProvider>
);
});In your test, pass hooksConfig:
test("home route", async ({ mount }) => {
const component = await mount(<HomePage />, {
hooksConfig: { routes: ["/home"] },
});
});beforeMount wraps every component with shared providers. afterMount runs after mount (e.g. for cleanup or DOM inspection).
Common Mistake: Trying to render multiple components in beforeMount. The function receives the test’s component as <App /> and must return a tree with <App /> at some point.
Fix 6: Locators and Interactions
Once mounted, use standard Playwright locators:
test("filter list", async ({ mount }) => {
const component = await mount(<UserList />);
await component.getByPlaceholder("Search").fill("alice");
await component.getByRole("button", { name: "Search" }).click();
await expect(component.getByText("Alice")).toBeVisible();
await expect(component.getByText("Bob")).not.toBeVisible();
});For shadow DOM:
await component.locator("custom-element").locator(":scope >> .inner").click();For attached elements outside the component root (portals, tooltips):
// component is the root mounted element.
// For portals that render to document.body:
const tooltip = component.page().getByRole("tooltip");
await expect(tooltip).toBeVisible();component.page() returns the full page Playwright object, useful for portals, modals, and any DOM outside the component subtree.
Fix 7: Snapshot Testing
For visual regression:
test("button screenshot", async ({ mount }) => {
const component = await mount(<Button>Click</Button>);
await expect(component).toHaveScreenshot("button.png");
});The first run creates button.png next to the test file (or under __snapshots__). Subsequent runs compare.
To update snapshots:
npx playwright test -c playwright-ct.config.ts --update-snapshotsFor DOM snapshot:
test("button DOM", async ({ mount }) => {
const component = await mount(<Button>Click</Button>);
expect(await component.innerHTML()).toMatchSnapshot("button.html");
});Pro Tip: Visual snapshots are sensitive to fonts, antialiasing, and animations. Disable animations for stable snapshots:
/* In playwright/global.css */
*, *::before, *::after {
animation-duration: 0s !important;
transition-duration: 0s !important;
}Import in playwright/index.ts.
Fix 8: TypeScript Setup
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*", "**/*.spec.tsx", "playwright/**/*"]
}For per-test-type tsconfig:
// tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["@playwright/experimental-ct-react"]
},
"include": ["**/*.spec.tsx", "playwright/**/*"]
}The types array adds Playwright CT’s ambient declarations, so editor autocomplete works for mount, hooksConfig, etc.
Common Mistake: Mixing CT’s test import with regular Playwright’s. The CT one has extra fixtures. ESLint can’t catch this — verify your imports.
Still Not Working?
A few less-obvious failures:
Cannot find module 'vite-tsconfig-paths'. Install it:npm install -D vite-tsconfig-paths. The fallback is duplicating paths inctViteConfig.resolve.alias.- Tests pass locally, fail in CI. Workers count mismatch (CI may have less memory). Set
workers: process.env.CI ? 1 : undefinedandfullyParallel: falsefor stability. - Slow test startup. Vite is doing a fresh build on each run. Cache the Vite build artifacts between CI runs.
SyntaxError: Cannot use import statement outside a module. Your component imports a CJS-only package. Mark it asoptimizeDeps.includeinctViteConfig.- State leaks between tests. Component is mounted in a single page; some state (localStorage, cookies) persists. Clear in
beforeEach:await page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); }). - Hot reload doesn’t trigger. CT doesn’t watch by default. Use
--uimode for interactive debugging:npx playwright test -c playwright-ct.config.ts --ui. - Wrong viewport size. Playwright CT mounts at default viewport. Set per-project:
use: { viewport: { width: 1280, height: 720 } }. document.bodyis empty inafterMount. Some frameworks (Solid) mount aftermountreturns. Useawait page.waitForSelector("[data-mounted]")with a marker.
For related testing and Playwright issues, see Playwright not working, Vitest setup not working, Jest mock not working, and React testing library not finding element.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Inertia.js Not Working — Shared Data, Lazy Props, Versioning, Forms, and SSR
How to fix Inertia.js errors — Inertia.render not returning a component, shared data missing on every page, lazy props not deferring, asset versioning forcing reloads, useForm helper, and SSR setup.
Fix: React Compiler Not Working — ESLint Plugin, Babel Setup, Bail-Outs, and Vite/Next.js Config
How to fix React Compiler issues — eslint-plugin-react-compiler not flagging, babel-plugin-react-compiler not running, 'Function contains a code construct that prevents compilation', Next.js 15 config, and removing useMemo/useCallback safely.
Fix: React Router 7 Not Working — Framework Mode, Loaders, Type Safety, and Remix Migration
How to fix React Router v7 errors — framework mode vs library mode setup, loader/action data type narrowing, route module exports missing, single-fetch revalidation, hydration mismatch, and Remix v2 migration paths.
Fix: Storybook Not Working — Addon Conflicts, Component Not Rendering, or Build Fails After Upgrade
How to fix Storybook issues — CSF3 story format, addon configuration, webpack vs Vite builder, decorator setup, args not updating component, and Storybook 8 migration problems.