Skip to content

Fix: Playwright Component Testing Not Working — Mount Fixture, Vite Config, Styles, and TypeScript

FixDevs ·

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.ts is for E2E (full browser navigations). Component testing needs playwright-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 the mount fixture.
  • 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.json aren’t auto-honored by Vite; you need a Vite plugin (vite-tsconfig-paths) or duplicate them in vite.config.ts.

Fix 1: Install and Configure

For React:

npm install -D @playwright/experimental-ct-react @playwright/test
npx playwright install

Then:

// 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.ts

For Vue:

npm install -D @playwright/experimental-ct-vue
import { 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 @/components and 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 CSS

Tailwind’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-snapshots

For 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 in ctViteConfig.resolve.alias.
  • Tests pass locally, fail in CI. Workers count mismatch (CI may have less memory). Set workers: process.env.CI ? 1 : undefined and fullyParallel: false for 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 as optimizeDeps.include in ctViteConfig.
  • 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 --ui mode 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.body is empty in afterMount. Some frameworks (Solid) mount after mount returns. Use await 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles