Fix: Playwright Component Testing Not Working — Mount Fixture, Vite Config, Styles, and TypeScript
Part of: React & Frontend Errors
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.
A second layer of failure comes from the package name. Every component-testing entry point still ships under @playwright/experimental-ct-*. That experimental- prefix is a deliberate Playwright signal: the API can change between releases, sometimes in non-obvious ways. The 1.30 → 1.40 transition added new fixture properties, 1.45 expanded Solid and Svelte 5 support, and several minor releases altered how ctViteConfig merges with your project’s vite.config.ts. If you upgraded Playwright but kept the same playwright-ct.config.ts from a 2023 tutorial, fields that used to be valid may now log warnings, and mount behaviour may differ. Always check the Playwright release notes for the exact version installed (npx playwright --version).
A third common cause is dependency mismatch between Playwright core and the CT adapter. @playwright/test and @playwright/experimental-ct-react must be on the same minor version. If package.json pins @playwright/test to ^1.45 but the CT adapter is at 1.40, the mount fixture’s TypeScript signature can drift from what the runtime provides, producing the “mount is not a function” error at runtime even though the import compiles. Always upgrade them together: npm install @playwright/test@latest @playwright/experimental-ct-react@latest.
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.
Version History: Playwright CT From 1.30 to 1.45 and Where It’s Going
Playwright Component Testing has been “experimental” since it shipped — and that label is honest. Knowing which release introduced which feature is the difference between a working CT setup and three hours debugging “why does my Vite alias work in dev but not in tests.”
Playwright 1.30 (January 2023) — initial CT release. React and Vue 3 only. The mount fixture was minimal: no hooksConfig, no beforeMount/afterMount. You wrapped providers manually by passing JSX from the test. ctViteConfig accepted only a small subset of Vite options and silently ignored unknown fields. Snapshot tests worked but the diff renderer was crude.
Playwright 1.31–1.34 (Spring 2023) — Svelte 3 support. @playwright/experimental-ct-svelte arrived. Tests for Svelte components used .svelte files mounted similarly. Vue 3’s adapter was rewritten to use app.mount directly, fixing a class of “missing root element” errors.
Playwright 1.35–1.39 (Summer-Autumn 2023) — hooksConfig and providers. beforeMount / afterMount hooks (Fix 5) landed. The hooksConfig object on mount(component, { hooksConfig }) made it possible to vary provider props per test without rewriting the whole hook. This is the version that made CT actually useful for real apps that depend on React Query, React Router, or Vue Router.
Playwright 1.40 (November 2023) — Solid support, stable JSX-in-CT. @playwright/experimental-ct-solid joined the lineup. The mount fixture’s return type became a proper Locator across all frameworks, so chaining .locator() and .getByRole() on the mounted component is consistent.
Playwright 1.41–1.44 (early-mid 2024) — Vite 5, Vue SFC props, snapshot diff. CT’s bundled Vite was updated to v5. Vue SFC <script setup> props parsing improved. The screenshot diff UI got the new pixel-by-pixel renderer that’s now standard across Playwright.
Playwright 1.45 (July 2024) — Svelte 5 + framework-config flexibility. Svelte 5’s runes-based components are supported. ctViteConfig accepts a function that returns a Vite config, which makes it easier to share logic with your app’s vite.config.ts.
Later releases. Subsequent minor versions have continued the same incremental track: more framework versions supported, better TypeScript inference on the mount fixture, faster startup. The experimental- prefix remains because the team has signalled at least one more API revision before 1.0-ing the feature.
vs Storybook test-runner
Storybook’s test-runner uses Jest + Playwright to drive existing .stories.tsx files in a real browser. Compared to Playwright CT:
- Storybook’s story files double as both interactive docs and tests. CT’s
.spec.tsxfiles are test-only. - Storybook test-runner is slower to boot (it loads the full Storybook UI in a hidden frame). CT’s Vite server is leaner.
- Storybook works without writing tests — you get visual regression “for free” from existing stories via the Chromatic add-on. CT requires explicit test code.
- Use Storybook test-runner if you already have Storybook for docs and want tests on top. Use Playwright CT if you want tests directly and don’t want a Storybook dependency.
vs Cypress Component Testing
Cypress also offers component testing. The differences:
- Cypress CT uses Cypress’s bundled browser harness with its own iframe model. Playwright CT uses Vite’s dev server and a near-stock Chromium.
- Cypress’s
cy.mount()is more mature — it shipped earlier (2022) and has fewer breaking changes per release. - Playwright’s parallelism is significantly better. CT runs tests across files in parallel by default; Cypress component tests parallelise per-spec only.
- Pick Cypress CT if your team already runs Cypress E2E. Pick Playwright CT for a smaller installed footprint and faster CI.
Pinning strategy
Pin both @playwright/test and the framework adapter to the same exact version (not ^) in production projects:
{
"devDependencies": {
"@playwright/test": "1.45.0",
"@playwright/experimental-ct-react": "1.45.0"
}
}Upgrade with npm install -E @playwright/test@latest @playwright/experimental-ct-react@latest simultaneously. Misaligned minors are the #1 cause of “worked yesterday, broken today” CT failures.
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.- TypeScript types for
mountregress after upgrade. When the CT adapter ships a new minor with refined fixture types, code that compiled cleanly may now flag a type error onmount(<Foo prop={undefined} />). Update your component prop types — usually the CT type is now stricter (and correct). - Mixing two CT adapters in a monorepo. If one package depends on
@playwright/experimental-ct-reactand another on@playwright/experimental-ct-vue, hoisting can install duplicate Playwright cores. Pin both to the same version and use your package manager’sresolutions/overrides. page.route()calls don’t intercept CT-mounted requests. CT’smountruns after the page has loaded the harness HTML, so routes you set up aftermountmay miss the initial component fetch. Set uppage.route()beforemountor use thebeforeEachhook.
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.