Skip to content

Component tests with local react components are evaluated twice in headed mode (Vite 8) #33750

@petertellgren

Description

@petertellgren

Current behavior

When running component tests in headed mode cypress open --component with Vite 8 and @vitejs/plugin-react v6, any .cy.tsx spec file that defines a React component (e.g. a Subject wrapper) has all its describe/it blocks registered twice. The test sidebar shows duplicated entries and tests run twice.

This does NOT happen in headless mode cypress run --component

Desired behavior

Each describe/it block should be registered exactly once regardless of whether the spec file defines React components.

Test code to reproduce

Example code:

// src/components/Hello.cy.tsx
import { Hello } from "./Hello";

const Subject = () => (
  <div>
    <Hello />
  </div>
);

describe("Hello Component", () => {
  it("should only appear once", () => {
    cy.mount(<Subject />);
  });
});

Steps to reproduce

  1. Create a project with Vite 8.0.10 and @cypress/vite-dev-server
  2. Run cypress open --component
  3. Observe the describe block appear twice

Example repo here

Cypress Version

15.14.2

Debug Logs

Other

Root Cause (I think)**

In Vite 8, @vitejs/plugin-react v6 uses Rolldown's built-in viteReactRefreshWrapperPlugin for HMR. When a file contains a React component definition (any function returning JSX), the plugin wraps it with:

import.meta.hot.accept((nextExports) => { ... });

This makes the spec file a self-accepting HMR boundary.

In headed mode, @cypress/vite-dev-server leaves HMR enabled. Cypress loads the spec via dynamic import() in initCypressTests.js, which evaluates the module and registers all describe/it blocks. The HMR system then detects the self-accepting module and triggers a re-evaluation, registering all blocks a second time.

In headless mode, @cypress/vite-dev-server sets hmr: false (in resolveConfig.js:122), so the refresh wrapper has no effect.

Verified by running server.transformRequest() on spec files:

  • Spec files WITHOUT local component definitions → no import.meta.hot.accept() → no duplication
  • Spec files WITH local component definitions → import.meta.hot.accept() injected → duplication

Suggested fix

@cypress/vite-dev-server should exclude spec files from React Refresh in the Vite config it constructs. For example, adding jsxRefreshExclude for the spec pattern in makeCypressViteConfig:

const viteConfig = {
  // ... existing config
  oxc: {
    jsxRefreshExclude: specs.map(s => s.absolute),
  },
};

Or alternatively, passing the spec pattern to @vitejs/plugin-react's exclude option via a plugin that modifies the config.

Workaround

Users can work around this by excluding spec files from React Refresh in their own vite.config.ts:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react({ exclude: [/node_modules/, /\.cy\.[tj]sx?$/] })],
});

Note: the default exclude is /node_modules/, so it must be included when overriding.

This disables React Fast Refresh for spec files only. JSX transformation still works, and editing subject components still triggers HMR normally.

Versions

  • Cypress: 15.14.2
  • @cypress/vite-dev-server: 7.3.1
  • @vitejs/plugin-react: 6.0.1
  • Vite: 8.0.10
  • Node: 24.x
  • OS: macOS

Metadata

Metadata

Assignees

Labels

CTIssue related to component testingnpm: @cypress/react@cypress/react package issuesnpm: @cypress/vite-dev-server@cypress/vite-dev-server package issues

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions