Cypress Component Testing for Vue: Mount API, Real Browser Tests, Visual Assertions
Cypress component testing runs Vue components in a real browser — not jsdom, not a virtual DOM. This means your CSS is applied, browser APIs work correctly, and user interactions behave exactly as they do in production. Unlike Vitest + Vue Test Utils which runs in Node, Cypress component tests give you visual feedback, real rendering, and the ability to test CSS-dependent behavior like animations and responsive layouts.
Key Takeaways
Cypress component tests run in a real browser. CSS is applied. getBoundingClientRect() works. IntersectionObserver fires. This is the main difference from Vitest/jsdom testing.
cy.mount() is the entry point. It works like mount() from Vue Test Utils but returns a Cypress chainable. Pass props, slots, and global plugins the same way.
You can import your application's CSS and fonts. Add import '@/assets/style.css' to your component support file and every component test renders with real styles applied.
cy.get() waits automatically. Unlike Vue Test Utils, Cypress retries element queries until they appear (default 4 seconds). You rarely need await or flushPromises().
Cypress Component Testing and E2E testing share the same API. Skills transfer directly — cy.get(), cy.click(), cy.should() work identically in both test types.
Why Cypress Component Testing?
Vitest + Vue Test Utils tests components in jsdom — a JavaScript DOM simulation. It's fast and works well for logic and event testing. But jsdom has limitations:
- No real CSS rendering (computed styles are wrong or empty)
- No real browser APIs (
IntersectionObserver,ResizeObserver, etc.) - No visual output — you can't see the component to debug
Cypress component testing solves this by mounting components in a real Chrome, Firefox, or Edge instance. You get a visual preview in the Cypress app, real CSS application, and accurate browser API behavior.
Use Vitest for fast feedback on logic; use Cypress component tests for visual behavior and CSS-dependent assertions.
Installation
npm install --save-dev cypress @cypress/vue
npx cypress open # runs setup wizardDuring setup, select "Component Testing" and then "Vue" + "Vite".
Cypress creates cypress.config.ts:
import { defineConfig } from 'cypress';
export default defineConfig({
component: {
devServer: {
framework: 'vue',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{ts,js}',
supportFile: 'cypress/support/component.ts',
},
});Support File
// cypress/support/component.ts
import { mount } from 'cypress/vue';
import '@/assets/main.css'; // import your app's global CSS
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}
}
Cypress.Commands.add('mount', mount);Writing Component Tests
Test files use the .cy.ts suffix and live alongside components:
// src/components/UserCard.cy.ts
import UserCard from './UserCard.vue';
describe('UserCard', () => {
it('renders user information', () => {
cy.mount(UserCard, {
props: {
user: { id: '1', name: 'Alice Johnson', email: 'alice@example.com' }
}
});
cy.get('[data-testid="user-name"]').should('have.text', 'Alice Johnson');
cy.get('[data-testid="user-email"]').should('have.text', 'alice@example.com');
});
it('shows the avatar with correct initials', () => {
cy.mount(UserCard, {
props: {
user: { id: '1', name: 'Alice Johnson', email: 'alice@example.com' }
}
});
cy.get('[data-testid="avatar"]').should('have.text', 'AJ');
});
});Testing with Props and Events
// src/components/DeleteButton.cy.ts
import DeleteButton from './DeleteButton.vue';
describe('DeleteButton', () => {
it('emits confirm event on click', () => {
const onConfirm = cy.stub().as('onConfirm');
cy.mount(DeleteButton, {
props: {
itemId: '123',
onConfirm, // Vue 3: prop name for @confirm listener
}
});
cy.get('[data-testid="delete-btn"]').click();
cy.get('@onConfirm').should('have.been.calledOnce');
cy.get('@onConfirm').should('have.been.calledWith', '123');
});
it('shows confirmation dialog before emitting', () => {
cy.mount(DeleteButton, { props: { itemId: '123' } });
cy.get('[data-testid="delete-btn"]').click();
// Dialog appears first
cy.get('[data-testid="confirm-dialog"]').should('be.visible');
cy.get('[data-testid="confirm-btn"]').should('contain', 'Yes, delete');
});
});Testing with Global Plugins
Mount with Pinia and Vue Router:
import { createPinia } from 'pinia';
import { createRouter, createMemoryHistory } from 'vue-router';
import UserDashboard from './UserDashboard.vue';
import { useUserStore } from '@/stores/user';
describe('UserDashboard', () => {
it('renders users from Pinia store', () => {
const pinia = createPinia();
cy.mount(UserDashboard, {
global: {
plugins: [pinia],
},
}).then(() => {
// Set store state after mounting
const store = useUserStore(pinia);
store.$patch({
users: [
{ id: '1', name: 'Alice', active: true },
{ id: '2', name: 'Bob', active: true },
],
});
});
cy.get('[data-testid="user-card"]').should('have.length', 2);
});
});Testing CSS and Visual Behavior
This is where Cypress component testing shines over Vitest:
it('applies active class to the selected tab', () => {
cy.mount(TabGroup, {
props: {
tabs: ['Overview', 'Settings', 'Activity'],
activeTab: 'Settings',
}
});
cy.get('[data-testid="tab-Settings"]').should('have.class', 'tab--active');
cy.get('[data-testid="tab-Overview"]').should('not.have.class', 'tab--active');
});
it('renders correctly on mobile viewport', () => {
cy.viewport(375, 667); // iPhone SE
cy.mount(AppHeader);
cy.get('[data-testid="mobile-menu-btn"]').should('be.visible');
cy.get('[data-testid="desktop-nav"]').should('not.be.visible');
});
it('applies error styles to invalid input', () => {
cy.mount(ValidatedInput, {
props: { value: '', required: true, label: 'Name' }
});
cy.get('input').blur(); // trigger validation
cy.get('.input-wrapper').should('have.class', 'input-wrapper--error');
cy.get('[data-testid="error-message"]').should('be.visible');
});Testing Slots
import Card from '@/components/Card.vue';
it('renders slot content in the correct section', () => {
cy.mount(Card, {
slots: {
header: '<h1 data-testid="card-title">My Card</h1>',
default: '<p data-testid="card-body">Card content here</p>',
footer: '<button data-testid="card-action">Submit</button>',
}
});
cy.get('[data-testid="card-title"]').should('have.text', 'My Card');
cy.get('[data-testid="card-body"]').should('be.visible');
cy.get('[data-testid="card-action"]').should('be.visible');
});Intercepting API Calls
it('shows loading state while fetching users', () => {
cy.intercept('GET', '/api/users', { delay: 1000, body: [] }).as('getUsers');
cy.mount(UserList);
cy.get('[data-testid="loading-spinner"]').should('be.visible');
cy.wait('@getUsers');
cy.get('[data-testid="loading-spinner"]').should('not.exist');
});
it('displays users from the API', () => {
cy.intercept('GET', '/api/users', {
body: [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
]
});
cy.mount(UserList);
cy.get('[data-testid="user-card"]').should('have.length', 1);
cy.get('[data-testid="user-card"]').first().should('contain', 'Alice');
});Running Cypress Component Tests
npx cypress open --component # interactive mode with browser UI
npx cypress run --component <span class="hljs-comment"># headless (CI)
npx cypress run --component --browser chrome <span class="hljs-comment"># specific browserWhen to Use Cypress vs Vitest
| Scenario | Vitest + VTU | Cypress Component |
|---|---|---|
| Testing computed props | ✅ Faster | |
| Testing emit payloads | ✅ Cleaner | |
| Testing CSS classes | ✅ Real styles | |
| Testing animations | ✅ Real browser | |
| Testing responsive layout | ✅ Real viewport | |
| Testing third-party components | ✅ Real render | |
| CI feedback loop | ✅ Much faster |
Production Monitoring
Even Cypress component tests run in isolation — they don't test your deployed application. Real users encounter network failures, server-side auth issues, and third-party service outages that no component test can catch.
HelpMeTest monitors your live Vue application continuously — testing real user flows in real browsers, 24/7. Start free with 10 tests.