Cypress Component Testing for Vue: Complete Guide 2026
Complete guide to Cypress Component Testing for Vue 3 in 2026. Mount, slots, Pinia, Vue Router, fixtures, intercepts, CI patterns, and best practices.
Cypress Component Testing for Vue: Complete Guide 2026
Cypress Component Testing supports Vue 3 (Composition API and Options API) with a mount API that closely mirrors @vue/test-utils. Where @vue/test-utils plus Vitest gives you fast unit-style tests in jsdom, Cypress Component Testing renders your component in a real browser with real CSS, real fonts, real animations, and a Cypress UI for interactive debugging.
This guide is the complete 2026 reference for Vue 3 teams running, or evaluating, Cypress Component Testing. We cover the mental model, cy.mount for Vue, Pinia store injection, Vue Router setup, slots and named slots, scoped slots, fixtures, network interception, MSW integration, theming with CSS variables, debugging with the Cypress UI, CI configuration, and the gotchas distilled from running real Vue suites.
For broader Cypress references, browse the blog index. For Cypress skills you can install into Claude Code, see the QA Skills directory.
When to use Cypress Component Testing for Vue
Cypress Component Testing for Vue shines when:
- Real CSS matters. Components depending on
@mediaqueries, container queries, or grid layouts behave correctly only in a real browser. - Animations and transitions.
<transition>and<transition-group>execute with real timing. - Layout fidelity. Tooltips, modals, popovers, charts.
- Visual regression. Combined with
cypress-image-snapshotor Percy.
Where Vitest plus @vue/test-utils is a better fit:
- Pure logic composables. Tens of milliseconds per test.
- Large suites. Vitest parallelizes more aggressively.
- Snapshot-heavy workflows.
Most teams use both.
Setup
npm install --save-dev cypress @cypress/vue @cypress/vite-dev-server
cypress.config.ts:
import { defineConfig } from 'cypress';
export default defineConfig({
component: {
devServer: {
framework: 'vue',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{ts,vue}',
},
});
Add a cypress/support/component.ts file that imports global styles and registers cy.mount:
import { mount } from 'cypress/vue';
import '../../src/assets/main.css';
Cypress.Commands.add('mount', mount);
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}
}
Your first component test
import Button from './Button.vue';
describe('<Button />', () => {
it('renders the slot and fires click', () => {
const onClick = cy.stub();
cy.mount(Button, {
props: { onClick },
slots: { default: 'Click me' },
});
cy.contains('Click me').click().then(() => {
expect(onClick).to.have.been.calledOnce;
});
});
});
cy.mount(Component, options) mirrors the @vue/test-utils mount signature. props, slots, and global are the most common options.
Mounting with Pinia
Most real components depend on stores. Pinia integrates cleanly.
import { createPinia, setActivePinia } from 'pinia';
import { mount } from 'cypress/vue';
import UserCard from './UserCard.vue';
import { useUserStore } from '@/stores/user';
it('shows the current user', () => {
const pinia = createPinia();
setActivePinia(pinia);
const store = useUserStore();
store.setUser({ id: 1, name: 'Alice' });
cy.mount(UserCard, {
global: { plugins: [pinia] },
});
cy.contains('Alice').should('be.visible');
});
For a reusable wrapper:
export const mountWithPinia = (component, options = {}) => {
const pinia = createPinia();
return cy.mount(component, {
...options,
global: {
...(options.global || {}),
plugins: [pinia, ...(options.global?.plugins || [])],
},
});
};
Mounting with Vue Router
import { createMemoryHistory, createRouter } from 'vue-router';
import { routes } from '@/router';
it('navigates on click', () => {
const router = createRouter({
history: createMemoryHistory(),
routes,
});
router.push('/users');
cy.mount(UserList, {
global: { plugins: [router] },
});
cy.contains('View profile').click();
cy.location('pathname').should('eq', '/users/1');
});
Slots and named slots
The slots option accepts strings, render functions, or full Vue components.
cy.mount(Card, {
slots: {
default: '<p>Body content</p>',
header: '<h2>Title</h2>',
footer: () => h('button', 'Close'),
},
});
For scoped slots, pass a render function:
cy.mount(UserList, {
props: { users: [{ id: 1, name: 'Alice' }] },
slots: {
item: (slotProps) => h('span', `Hello ${slotProps.user.name}`),
},
});
cy.contains('Hello Alice').should('be.visible');
Network interception
cy.intercept works identically to E2E tests. Real network calls hit the interceptor.
it('loads users from the API', () => {
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
cy.mount(UserList);
cy.wait('@getUsers');
cy.get('[data-testid=user-row]').should('have.length', 3);
});
Composables testing
Composables that wrap reactive state can be tested in isolation with a tiny wrapper component, or directly with @vue/test-utils's renderComposable pattern. For composables that depend on a DOM API (useElementBounding, useResizeObserver), Cypress Component Testing produces more accurate results than Vitest plus jsdom.
import { defineComponent, h } from 'vue';
import { useCounter } from './useCounter';
const HarnessComponent = defineComponent({
setup() {
return useCounter(0);
},
template: '<button @click="increment">{{ count }}</button>',
});
it('counts up on click', () => {
cy.mount(HarnessComponent);
cy.get('button').click().should('contain', '1');
cy.get('button').click().should('contain', '2');
});
Custom commands
Cypress.Commands.add('byTestId', (id) => cy.get(`[data-testid="${id}"]`));
Cypress.Commands.add('mountWithStore', (component, options = {}) => {
const pinia = createPinia();
return cy.mount(component, {
...options,
global: { ...(options.global || {}), plugins: [pinia] },
});
});
CSS variables and theming
Vue 3 teams often use CSS variables for theming. Cypress Component Tests inherit any imported global stylesheets, including variable definitions.
/* main.css */
:root {
--color-primary: #0d6efd;
--color-background: #ffffff;
}
[data-theme='dark'] {
--color-background: #1a1a1a;
}
Toggle theme per test:
it('renders correctly in dark mode', () => {
document.documentElement.setAttribute('data-theme', 'dark');
cy.mount(Card);
cy.get('[data-testid=card]').should('have.css', 'background-color', 'rgb(26, 26, 26)');
});
Debugging with the Cypress UI
npx cypress open --component opens the interactive UI. Each command in the test log is replayable, the DOM snapshot shows the component before and after each step, and the network panel shows intercepted requests. For Vue components with reactive state, this is materially better than the Vitest UI.
CI configuration
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- uses: cypress-io/github-action@v6
with:
component: true
record: true
parallel: true
group: 'component-tests'
Best practices
- Co-locate specs.
Button.cy.tsnext toButton.vue. - Share mount wrappers.
mountWithPinia,mountWithRouterreduce boilerplate. - Prefer text and role selectors. Use
data-testidonly when accessibility cannot guide. - Intercept all network calls. No real backend during component tests.
- One assertion per behavior. Multi-assertion tests obscure failure points.
- Stub callback props with
cy.stub. Assert oncalledOnce,calledWith. - Test what the user sees and does. Not the component's internal state.
- Co-locate fixtures. Or share via
cypress/fixtures/. - Run on every PR. Parallelize with Cypress Cloud if needed.
- Keep tests under one second each. Slower tests are warning signs.
Gotchas
@vue/test-utilsmount differs slightly fromcypress/vuemount. Names and options are similar but not identical.- Pinia must be installed per test or via custom command. Stale state between tests is a common bug.
- Vue Router needs memory history in tests.
createMemoryHistoryrather thancreateWebHistory. - Reactive props may not update after mount. Use
cy.get(...).vue()to access the component instance. - Async
setuprequires await in mount. Wrap inSuspense. - Global directives must be registered.
global.directivesin mount options. - TypeScript autocompletion for
cy.mountrequires thedeclareblock. Otherwise IntelliSense fails. - Hot reload can cause stale tests. Restart on code edits.
Comparison: Cypress Component vs Vitest + Test Utils
| Dimension | Cypress Component | Vitest + @vue/test-utils |
|---|---|---|
| Speed (per test) | ~1 second | ~10 milliseconds |
| Fidelity | Real browser | jsdom or happy-dom |
| Network mocking | cy.intercept | MSW or module mocks |
| Visual debugging | Excellent | Limited |
| CSS support | Full | Partial |
| Animation support | Real | Stubbed |
| Snapshot testing | Visual via plugin | Built-in HTML |
| Parallelism | Cloud or DIY | Built-in workers |
Conclusion and next steps
Cypress Component Testing for Vue 3 is the right choice for layout-sensitive, visually-rich, and animation-heavy components in 2026. The cost is slower execution; the benefit is fidelity and debugging that Vitest plus jsdom cannot match. Use it alongside Vitest plus @vue/test-utils, not as a replacement.
Start with one component. Build the mount wrapper with Pinia and Router. Add intercepts and fixtures. Migrate visually complex components first.
Next read: explore the QA Skills directory for Cypress skills, and the blog index for visual testing and CI guides.