Mounting and Testing Components — Props, Events, Slots and Rendering

The core of component testing is the mount() function — it takes your component, renders it in a real browser, and returns a Cypress chainable that you interact with using the same cy.get(), .click(), .type(), and .should() commands you already know from E2E tests. The power lies in what you pass to mount(): different props, event handlers, context providers, and slots — each creating a different test scenario without any backend or navigation.

Mounting Components — Props, Events and Rendering Variations

Every component test follows the same pattern: mount with specific props, interact, assert on the rendered output.

// ── Example component: ProductCard.tsx ──

/*
interface ProductCardProps {
  name: string;
  price: number;
  imageUrl: string;
  inStock: boolean;
  onAddToCart: (productName: string) => void;
}

export function ProductCard({ name, price, imageUrl, inStock, onAddToCart }: ProductCardProps) {
  return (
    <div className="product-card" data-cy="product-card">
      <img src={imageUrl} alt={name} data-cy="product-image" />
      <h3 data-cy="product-name">{name}</h3>
      <p data-cy="product-price">${price.toFixed(2)}</p>
      {inStock ? (
        <button data-cy="add-to-cart" onClick={() => onAddToCart(name)}>
          Add to Cart
        </button>
      ) : (
        <span data-cy="out-of-stock" className="out-of-stock">Out of Stock</span>
      )}
    </div>
  );
}
*/


// ── Component test: ProductCard.cy.tsx ──

import { ProductCard } from './ProductCard';

describe('ProductCard', () => {
  // ── Test 1: Renders basic product information ──
  it('should display product name, price and image', () => {
    cy.mount(
      
    );

    cy.get('[data-cy="product-name"]').should('have.text', 'Sauce Labs Backpack');
    cy.get('[data-cy="product-price"]').should('have.text', '$29.99');
    cy.get('[data-cy="product-image"]').should('have.attr', 'alt', 'Sauce Labs Backpack');
  });

  // ── Test 2: Shows "Add to Cart" when in stock ──
  it('should show Add to Cart button when in stock', () => {
    cy.mount(
      
    );

    cy.get('[data-cy="add-to-cart"]').should('be.visible').and('contain.text', 'Add to Cart');
    cy.get('[data-cy="out-of-stock"]').should('not.exist');
  });

  // ── Test 3: Shows "Out of Stock" when not in stock ──
  it('should show Out of Stock when not in stock', () => {
    cy.mount(
      
    );

    cy.get('[data-cy="out-of-stock"]').should('be.visible');
    cy.get('[data-cy="add-to-cart"]').should('not.exist');
  });

  // ── Test 4: Fires onAddToCart event when clicked ──
  it('should call onAddToCart with product name when button is clicked', () => {
    const onAddToCartStub = cy.stub().as('addToCartHandler');

    cy.mount(
      
    );

    cy.get('[data-cy="add-to-cart"]').click();

    // Verify the stub was called with the correct argument
    cy.get('@addToCartHandler').should('have.been.calledOnceWith', 'Bike Light');
  });

  // ── Test 5: Handles edge case — zero price ──
  it('should display $0.00 for free products', () => {
    cy.mount(
      
    );

    cy.get('[data-cy="product-price"]').should('have.text', '$0.00');
  });

  // ── Test 6: Handles edge case — very long name ──
  it('should handle very long product names without layout break', () => {
    const longName = 'A'.repeat(200);

    cy.mount(
      
    );

    cy.get('[data-cy="product-card"]').should('be.visible');
    // Verify the card does not overflow its container
    cy.get('[data-cy="product-card"]').then(($card) => {
      const cardWidth = $card[0].offsetWidth;
      const parentWidth = $card[0].parentElement!.offsetWidth;
      expect(cardWidth).to.be.at.most(parentWidth);
    });
  });
});
Note: cy.stub() is Cypress’s built-in function spy/stub. When passed as an event handler prop (onAddToCart={cy.stub().as('handler')}), it records every call with its arguments. You can then assert: cy.get('@handler').should('have.been.calledOnceWith', 'Bike Light'). This is how you verify that a component fires the correct events without actually implementing the handler logic. Stubs replace the real function with a recording — no side effects, full visibility into what was called.
Tip: Test edge cases that are difficult to reach through E2E: zero prices, extremely long names, empty strings, null values, missing images, and maximum quantities. In E2E, creating these conditions requires specific backend state. In component testing, you simply pass the edge-case value as a prop: <ProductCard price={0} />, <ProductCard name="" />. This is where component testing provides the highest unique value — testing rendering edge cases that E2E cannot easily reproduce.
Warning: Always use cy.stub() for event handler props, not anonymous functions like () => {}. An anonymous function works (the component does not crash), but you cannot assert on whether it was called or with what arguments. cy.stub().as('name') gives you a named reference that you can assert on: cy.get('@name').should('have.been.called'). Without the stub, you verify rendering but not behaviour.

Common Mistakes

Mistake 1 — Not testing event handler callbacks

❌ Wrong: Mounting a button component, clicking it, but not verifying that the onClick prop was called with the correct arguments.

✅ Correct: Passing cy.stub().as('onClick') as the prop, clicking the button, then asserting: cy.get('@onClick').should('have.been.calledOnceWith', expectedValue).

Mistake 2 — Only testing the happy-path props

❌ Wrong: Testing ProductCard only with valid name, price, and inStock=true — missing all edge cases.

✅ Correct: Testing with inStock=false, price=0, empty name, very long name, missing image URL, and negative price to verify the component handles every prop variation defensively.

🧠 Test Yourself

How do you verify that a component’s button click fires the correct event with the correct argument?