Cypress Component Testing for Vue: Mount API, Real Browser Tests, Visual Assertions

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 wizard

During 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 browser

When 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.

Read more