Cypress is a next generation front end testing tool built for the modern web
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Mount and test Vue components with Vue Test Utils integration and Cypress commands. The @cypress/vue package provides seamless integration between Vue components and Cypress's powerful testing capabilities.
Mount Vue components for isolated testing with props, slots, and full Cypress command support.
// From cypress/vue
import { mount } from '@cypress/vue';
/**
* Mount a Vue component for testing
* @param component - Vue component to mount
* @param options - Vue Test Utils mounting options plus Cypress-specific options
* @returns Cypress chainable with wrapper and component access
*/
function mount<T>(
component: T,
options?: VueMountOptions<T>
): Cypress.Chainable<VueMountReturn<T>>;
interface VueMountOptions<T> {
/** Props to pass to the component */
props?: Record<string, any>;
/** Slots content */
slots?: Record<string, any>;
/** Global configuration */
global?: {
plugins?: any[];
components?: Record<string, any>;
directives?: Record<string, any>;
mixins?: any[];
provide?: Record<string, any>;
config?: {
globalProperties?: Record<string, any>;
errorHandler?: (err: Error) => void;
};
};
/** Whether to log the mount command */
log?: boolean;
}
interface VueMountReturn<T> {
/** Vue Test Utils wrapper for advanced interactions */
wrapper: VueWrapper<ComponentPublicInstance>;
/** Direct access to the component instance */
component: ComponentPublicInstance;
}Usage Examples:
// cypress/component/Button.cy.js
import { mount } from '@cypress/vue';
import Button from './Button.vue';
describe('Button Component', () => {
it('renders with text', () => {
mount(Button, {
slots: {
default: 'Click me'
}
});
cy.contains('Click me').should('be.visible');
});
it('emits click events', () => {
const onClickSpy = cy.stub();
mount(Button, {
props: {
onClick: onClickSpy
},
slots: {
default: 'Click me'
}
});
cy.contains('Click me').click();
cy.wrap(onClickSpy).should('have.been.called');
});
it('supports different variants', () => {
mount(Button, {
props: {
variant: 'primary'
},
slots: {
default: 'Primary Button'
}
});
cy.get('button').should('have.class', 'btn-primary');
});
});Test components with props and watch for reactive updates.
// Testing reactive props
describe('Counter Component', () => {
it('displays initial count', () => {
mount(Counter, {
props: {
initialCount: 5
}
});
cy.get('[data-cy=count]').should('contain', '5');
});
it('updates when props change', () => {
mount(Counter, {
props: {
initialCount: 0
}
}).then(({ wrapper }) => {
cy.get('[data-cy=count]').should('contain', '0');
// Update props programmatically
wrapper.setProps({ initialCount: 10 });
cy.get('[data-cy=count]').should('contain', '10');
});
});
it('handles user interactions', () => {
mount(Counter, {
props: {
initialCount: 0
}
});
cy.get('[data-cy=increment]').click();
cy.get('[data-cy=count]').should('contain', '1');
cy.get('[data-cy=decrement]').click();
cy.get('[data-cy=count]').should('contain', '0');
});
});Test components that use Vue slots for content projection.
import { mount } from '@cypress/vue';
import Card from './Card.vue';
describe('Card Component', () => {
it('renders default slot content', () => {
mount(Card, {
slots: {
default: '<p>Card content</p>'
}
});
cy.contains('Card content').should('be.visible');
});
it('renders named slots', () => {
mount(Card, {
slots: {
header: '<h2>Card Title</h2>',
default: '<p>Card body content</p>',
footer: '<button>Action</button>'
}
});
cy.contains('Card Title').should('be.visible');
cy.contains('Card body content').should('be.visible');
cy.contains('Action').should('be.visible');
});
it('renders scoped slots', () => {
mount(Card, {
slots: {
default: `
<template #default="{ user }">
<p>Hello, {{ user.name }}!</p>
</template>
`
},
props: {
user: { name: 'John Doe' }
}
});
cy.contains('Hello, John Doe!').should('be.visible');
});
});Test components that depend on Vue plugins like Vue Router or Vuex/Pinia.
import { mount } from '@cypress/vue';
import { createRouter, createWebHistory } from 'vue-router';
import Navigation from './Navigation.vue';
describe('Navigation Component', () => {
const routes = [
{ path: '/', component: { template: '<div>Home</div>' } },
{ path: '/about', component: { template: '<div>About</div>' } },
{ path: '/contact', component: { template: '<div>Contact</div>' } }
];
it('highlights active route', () => {
const router = createRouter({
history: createWebHistory(),
routes
});
mount(Navigation, {
global: {
plugins: [router]
}
});
cy.get('[data-cy=nav-home]').should('have.class', 'router-link-active');
});
it('navigates to different routes', () => {
const router = createRouter({
history: createWebHistory(),
routes
});
mount(Navigation, {
global: {
plugins: [router]
}
});
cy.get('[data-cy=nav-about]').click();
cy.location('pathname').should('eq', '/about');
});
});Test components that use Pinia for state management.
import { mount } from '@cypress/vue';
import { createPinia, setActivePinia } from 'pinia';
import { useUserStore } from './stores/user';
import UserProfile from './UserProfile.vue';
describe('UserProfile Component', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it('displays user information from store', () => {
const pinia = createPinia();
mount(UserProfile, {
global: {
plugins: [pinia]
}
}).then(() => {
// Access store and set user data
const userStore = useUserStore();
userStore.setUser({
name: 'Jane Doe',
email: 'jane@example.com'
});
cy.contains('Jane Doe').should('be.visible');
cy.contains('jane@example.com').should('be.visible');
});
});
it('updates when store state changes', () => {
const pinia = createPinia();
mount(UserProfile, {
global: {
plugins: [pinia]
}
}).then(() => {
const userStore = useUserStore();
// Initial state
userStore.setUser({ name: 'John', email: 'john@example.com' });
cy.contains('John').should('be.visible');
// Update state
userStore.updateName('Johnny');
cy.contains('Johnny').should('be.visible');
});
});
});Test components that depend on globally registered components or directives.
import { mount } from '@cypress/vue';
import MyComponent from './MyComponent.vue';
import GlobalButton from './GlobalButton.vue';
describe('MyComponent', () => {
it('uses global components', () => {
mount(MyComponent, {
global: {
components: {
'global-button': GlobalButton
}
}
});
cy.get('global-button').should('exist');
});
it('uses custom directives', () => {
const focusDirective = {
mounted(el) {
el.focus();
}
};
mount(MyComponent, {
global: {
directives: {
focus: focusDirective
}
}
});
cy.get('[data-cy=input-with-focus]').should('be.focused');
});
});Test components that use Vue 3's Composition API.
import { mount } from '@cypress/vue';
import CompositionComponent from './CompositionComponent.vue';
describe('CompositionComponent', () => {
it('handles reactive refs', () => {
mount(CompositionComponent);
cy.get('[data-cy=counter]').should('contain', '0');
cy.get('[data-cy=increment]').click();
cy.get('[data-cy=counter]').should('contain', '1');
});
it('handles computed properties', () => {
mount(CompositionComponent, {
props: {
items: ['apple', 'banana', 'cherry']
}
});
cy.get('[data-cy=item-count]').should('contain', '3');
cy.get('[data-cy=first-item]').should('contain', 'apple');
});
it('handles watchers', () => {
mount(CompositionComponent);
cy.get('[data-cy=search-input]').type('test');
cy.get('[data-cy=search-results]').should('contain', 'Searching for: test');
});
});Access Vue Test Utils functionality through the wrapper returned by mount.
// Re-exported Vue Test Utils (excluding mount and shallowMount)
import { VueTestUtils } from '@cypress/vue';
// Available Vue Test Utils methods:
interface VueWrapper {
/** Find component by selector */
find(selector: string): DOMWrapper;
/** Find component by component definition */
findComponent(component: any): VueWrapper;
/** Find all matching elements */
findAll(selector: string): DOMWrapper[];
/** Set component props */
setProps(props: Record<string, any>): Promise<void>;
/** Set component data */
setData(data: Record<string, any>): Promise<void>;
/** Trigger an event */
trigger(event: string, options?: any): Promise<void>;
/** Get component's emitted events */
emitted(): Record<string, any[]>;
/** Check if component exists */
exists(): boolean;
/** Get component HTML */
html(): string;
/** Get component text */
text(): string;
/** Unmount the component */
unmount(): void;
}Advanced Testing with Vue Test Utils:
describe('Advanced Component Testing', () => {
it('uses wrapper methods for detailed testing', () => {
mount(ComplexComponent, {
props: {
items: ['item1', 'item2', 'item3']
}
}).then(({ wrapper }) => {
// Use Vue Test Utils methods
expect(wrapper.findAll('[data-cy=item]')).to.have.length(3);
// Check emitted events
wrapper.find('[data-cy=delete-button]').trigger('click');
expect(wrapper.emitted()).to.have.property('delete');
// Update props and verify changes
wrapper.setProps({ items: ['item1', 'item2'] });
cy.get('[data-cy=item]').should('have.length', 2);
});
});
});Configure Vue component testing in your Cypress configuration:
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
component: {
devServer: {
framework: 'vue',
bundler: 'vite', // or 'webpack'
},
specPattern: 'src/**/*.cy.{js,ts}',
supportFile: 'cypress/support/component.js'
}
});Create custom commands for Vue-specific testing patterns:
// cypress/support/component.js
import { mount } from '@cypress/vue';
Cypress.Commands.add('mountVue', (component, options = {}) => {
// Add default global configuration
const defaultOptions = {
global: {
stubs: {
// Stub router-link by default
'router-link': true
}
}
};
const mergedOptions = { ...defaultOptions, ...options };
return mount(component, mergedOptions);
});
// Usage in tests
cy.mountVue(MyComponent, {
props: { title: 'Test Title' }
});The Vue component testing integration provides comprehensive support for testing Vue.js components in isolation while maintaining access to Vue's reactive system, component lifecycle, and ecosystem tools.
Install with Tessl CLI
npx tessl i tessl/npm-cypress