Cypress Component Testing for Vue and Angular

Cypress Component Testing for Vue and Angular

Cypress component testing works across frameworks. React gets the most coverage in tutorials, but Vue 3 and Angular both have first-class support. This guide covers the setup, idiomatic patterns, and real-world considerations for each.

Vue 3 Component Testing Setup

npm install --save-dev cypress
npx cypress open

Cypress detects Vue and generates a config. Confirm it looks like:

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  component: {
    devServer: {
      framework: 'vue',
      bundler: 'vite',
    },
    specPattern: 'src/**/*.cy.{js,ts,jsx,tsx,vue}',
    supportFile: 'cypress/support/component.ts',
  },
});

Support file:

// cypress/support/component.ts
import { mount } from 'cypress/vue';

declare global {
  namespace Cypress {
    interface Chainable {
      mount: typeof mount;
    }
  }
}

Cypress.Commands.add('mount', mount);

Mounting Vue 3 Components

// src/components/ProductCard.cy.ts
import ProductCard from './ProductCard.vue';

describe('ProductCard', () => {
  it('renders product information', () => {
    cy.mount(ProductCard, {
      props: {
        product: {
          id: 'prod-1',
          name: 'Widget Pro',
          price: 29.99,
          inStock: true,
        },
      },
    });

    cy.contains('Widget Pro').should('be.visible');
    cy.contains('$29.99').should('be.visible');
  });
});

Vue component mounting accepts props, slots, global plugins, and attrs through the mount options object.

Testing Vue Composition API and Reactive State

For components using ref, computed, and watch:

<!-- src/components/QuantityPicker.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue';

const props = defineProps<{
  min: number;
  max: number;
  modelValue?: number;
}>();

const emit = defineEmits<{
  'update:modelValue': [value: number];
}>();

const quantity = ref(props.modelValue ?? props.min);

const canDecrement = computed(() => quantity.value > props.min);
const canIncrement = computed(() => quantity.value < props.max);

function decrement() {
  if (canDecrement.value) {
    quantity.value--;
    emit('update:modelValue', quantity.value);
  }
}

function increment() {
  if (canIncrement.value) {
    quantity.value++;
    emit('update:modelValue', quantity.value);
  }
}
</script>
// src/components/QuantityPicker.cy.ts
import QuantityPicker from './QuantityPicker.vue';

describe('QuantityPicker', () => {
  it('increments and decrements within bounds', () => {
    const onUpdate = cy.stub().as('onUpdate');

    cy.mount(QuantityPicker, {
      props: {
        min: 1,
        max: 5,
        modelValue: 1,
        'onUpdate:modelValue': onUpdate,
      },
    });

    cy.get('[data-cy="qty-display"]').should('have.text', '1');

    cy.get('[data-cy="increment"]').click();
    cy.get('[data-cy="qty-display"]').should('have.text', '2');
    cy.get('@onUpdate').should('have.been.calledWith', 2);

    cy.get('[data-cy="decrement"]').click();
    cy.get('[data-cy="qty-display"]').should('have.text', '1');

    // At minimum, decrement is disabled
    cy.get('[data-cy="decrement"]').should('be.disabled');
  });

  it('cannot exceed max', () => {
    cy.mount(QuantityPicker, {
      props: { min: 1, max: 3, modelValue: 3 },
    });

    cy.get('[data-cy="increment"]').should('be.disabled');
  });
});

Vue Composables and provide/inject

Testing components that use provide/inject requires passing the provided values through mount options:

// Component uses inject('cartService')
cy.mount(AddToCartButton, {
  props: { productId: 'prod-42' },
  global: {
    provide: {
      cartService: {
        addItem: cy.stub().as('addItem').resolves({ success: true }),
      },
    },
  },
});

cy.get('[data-cy="add-btn"]').click();
cy.get('@addItem').should('have.been.calledWith', 'prod-42', 1);

For components using Vue Router:

import { createRouter, createMemoryHistory } from 'vue-router';

const router = createRouter({
  history: createMemoryHistory(),
  routes: [
    { path: '/', component: { template: '<div>Home</div>' } },
    { path: '/products/:id', component: ProductDetail },
  ],
});

cy.mount(ProductLink, {
  props: { productId: 'prod-42' },
  global: {
    plugins: [router],
  },
});

For Pinia state:

import { createPinia, setActivePinia } from 'pinia';
import { useCartStore } from '@/stores/cart';

const pinia = createPinia();
setActivePinia(pinia);

// Pre-populate the store
const cart = useCartStore();
cart.items = [{ id: 'prod-1', quantity: 2, price: 29.99 }];

cy.mount(CartSummary, {
  global: {
    plugins: [pinia],
  },
});

cy.get('[data-cy="cart-total"]').should('contain', '$59.98');

Angular Component Testing Setup

Cypress supports Angular through its own devServer integration. For a standard Angular CLI project:

ng add @cypress/schematic

This runs the Angular Cypress schematic, which:

  • Installs Cypress and configures it
  • Sets up the component testing support file
  • Creates cypress.config.ts with Angular-specific settings

Manual setup:

npm install --save-dev cypress
// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  component: {
    devServer: {
      framework: 'angular',
      bundler: 'webpack',
    },
    specPattern: '**/*.cy.ts',
    supportFile: 'cypress/support/component.ts',
  },
});
// cypress/support/component.ts
import { mount } from 'cypress/angular';

declare global {
  namespace Cypress {
    interface Chainable {
      mount: typeof mount;
    }
  }
}

Cypress.Commands.add('mount', mount);

Angular Component Mounting with cy.mount()

// src/app/components/product-card/product-card.component.cy.ts
import { ProductCardComponent } from './product-card.component';
import { RouterTestingModule } from '@angular/router/testing';
import { By } from '@angular/platform-browser';

describe('ProductCardComponent', () => {
  it('renders product name and price', () => {
    cy.mount(ProductCardComponent, {
      componentProperties: {
        product: {
          id: 'prod-1',
          name: 'Widget Pro',
          price: 29.99,
          inStock: true,
        },
      },
      imports: [RouterTestingModule],
    });

    cy.contains('Widget Pro').should('be.visible');
    cy.contains('$29.99').should('be.visible');
  });

  it('emits addToCart event when button clicked', () => {
    const onAddToCart = cy.stub().as('addToCart');

    cy.mount(ProductCardComponent, {
      componentProperties: {
        product: { id: 'prod-42', name: 'Widget', price: 9.99, inStock: true },
        addToCart: {
          emit: onAddToCart,
        } as any,
      },
    });

    cy.get('[data-cy="add-to-cart"]').click();
    cy.get('@addToCart').should('have.been.calledWith', 'prod-42');
  });
});

Angular Dependency Injection in Cypress Mounts

Angular's DI system is the trickiest part of component testing. Cypress's mount() for Angular accepts Angular TestBed configuration through providers and imports:

import { ProductCardComponent } from './product-card.component';
import { ProductService } from '../../services/product.service';
import { of } from 'rxjs';

describe('ProductCardComponent with service', () => {
  it('loads product from service', () => {
    const mockProductService = {
      getProduct: cy.stub().returns(of({
        id: 'prod-42',
        name: 'Service Widget',
        price: 19.99,
        inStock: true,
      })),
    };

    cy.mount(ProductCardComponent, {
      componentProperties: {
        productId: 'prod-42',
      },
      providers: [
        { provide: ProductService, useValue: mockProductService },
      ],
    });

    cy.contains('Service Widget').should('be.visible');
  });

  it('uses HttpClient with intercepted requests', () => {
    cy.mount(ProductListComponent, {
      imports: [HttpClientModule],
    });

    cy.intercept('GET', '/api/products', {
      statusCode: 200,
      body: [
        { id: '1', name: 'Widget A', price: 9.99 },
        { id: '2', name: 'Widget B', price: 14.99 },
      ],
    });

    cy.get('[data-cy="product-item"]').should('have.length', 2);
    cy.contains('Widget A').should('be.visible');
  });
});

For reactive forms, import ReactiveFormsModule:

cy.mount(LoginFormComponent, {
  imports: [ReactiveFormsModule],
});

cy.get('[data-cy="email"]').type('user@example.com');
cy.get('[data-cy="password"]').type('password123');
cy.get('[data-cy="submit"]').click();
cy.get('[data-cy="error-message"]').should('not.exist');

Cypress vs @angular/core/testing (TestBed)

Angular ships with its own testing utilities: TestBed, ComponentFixture, and DebugElement. How do Cypress component tests compare?

Aspect Cypress component @angular/core/testing
Browser Real Chromium jsdom (simulated)
Visual debugging Time-travel, screenshots Text output only
CSS rendering Real (catches CSS bugs) None
Test runner Cypress Jest/Karma
Assertion style .should('be.visible') expect(el.nativeElement.textContent)
Setup complexity Medium Lower (built into Angular CLI)
Animation testing Real browser animations Requires fakeAsync + tick()
Existing tests New spec format Compatible with existing Jasmine specs

@angular/core/testing is the Angular default for a reason — it's deeply integrated with the Angular CLI and has excellent support for Angular-specific patterns (change detection, reactive forms, DI). Most Angular teams already have thousands of tests written this way.

Cypress component tests add value for:

  • Visual inspection of Angular component rendering
  • Testing interactions that involve real CSS transitions
  • Teams that already use Cypress for E2E and want a unified workflow

Migrating from Karma to Cypress

Angular projects created before Angular 16 used Karma + Jasmine as the default test runner. Angular 16+ switched to Jest (via ng generate @angular/core:jest), but many projects are still on Karma.

Migrate step by step, not all at once:

  1. Add Cypress component testing alongside Karma (they can coexist)
  2. Write new component tests in Cypress
  3. When you refactor a component, migrate its Karma tests to Cypress
  4. Remove Karma once the migration is complete

Keep your existing Jasmine/Karma tests running throughout. Don't delete them until you've verified equivalent Cypress coverage.

// package.json — run both test suites in CI
{
  "scripts": {
    "test": "ng test --watch=false",
    "test:component": "cypress run --component",
    "test:all": "npm run test && npm run test:component"
  }
}

Vue vs React vs Angular: Cypress Differences

The Cypress test syntax is identical across frameworks — cy.get(), cy.click(), cy.should() work the same everywhere. The differences are in the mount API:

Framework Mount import Props Events
React cypress/react18 JSX props JSX event props (onClick={stub})
Vue 3 cypress/vue { props: {} } { props: { 'onUpdate:modelValue': stub } }
Angular cypress/angular { componentProperties: {} } Stub via EventEmitter

The underlying principle is the same: mount the component, interact with it through the DOM, assert on what the user sees. Framework differences are in configuration, not in the test style.

Read more

Start now free