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
- Create a project with Vite 8.0.10 and @cypress/vite-dev-server
- Run
cypress open --component
- 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
Current behavior
When running component tests in headed mode
cypress open --componentwith 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 --componentDesired 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:
Steps to reproduce
cypress open --componentExample repo here
Cypress Version
15.14.2
Debug Logs
Other
Root Cause (I think)**
In Vite 8,
@vitejs/plugin-reactv6 uses Rolldown's built-inviteReactRefreshWrapperPluginfor HMR. When a file contains a React component definition (any function returning JSX), the plugin wraps it with:This makes the spec file a self-accepting HMR boundary.
In headed mode,
@cypress/vite-dev-serverleaves HMR enabled. Cypress loads the spec via dynamicimport()ininitCypressTests.js, which evaluates the module and registers alldescribe/itblocks. 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-serversetshmr: false(inresolveConfig.js:122), so the refresh wrapper has no effect.Verified by running
server.transformRequest()on spec files:import.meta.hot.accept()→ no duplicationimport.meta.hot.accept()injected → duplicationSuggested fix
@cypress/vite-dev-servershould exclude spec files from React Refresh in the Vite config it constructs. For example, addingjsxRefreshExcludefor the spec pattern inmakeCypressViteConfig:Or alternatively, passing the spec pattern to
@vitejs/plugin-react'sexcludeoption 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:Note: the default
excludeis/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