Skip to main content
โšก Calmops

End-to-End Testing: Cypress, Playwright

End-to-End Testing: Cypress, Playwright

End-to-end testing verifies complete user workflows. This article covers E2E testing frameworks and patterns.

Introduction

E2E testing provides:

  • User workflow verification
  • Cross-browser testing
  • Visual regression testing
  • Real browser testing
  • Complete application testing

Understanding E2E testing helps you:

  • Test complete workflows
  • Verify user interactions
  • Test across browsers
  • Catch integration issues
  • Ensure user experience

Cypress Testing

Cypress Setup

# โœ… Good: Install Cypress
npm install --save-dev cypress

# โœ… Good: Open Cypress
npx cypress open

# โœ… Good: Run Cypress tests
npx cypress run
npx cypress run --browser chrome
npx cypress run --spec "cypress/e2e/login.cy.js"

Cypress Tests

// โœ… Good: Basic Cypress test
describe('Login Page', () => {
  beforeEach(() => {
    cy.visit('http://localhost:3000/login');
  });

  it('displays login form', () => {
    cy.get('form').should('be.visible');
    cy.get('input[type="email"]').should('exist');
    cy.get('input[type="password"]').should('exist');
    cy.get('button[type="submit"]').should('exist');
  });

  it('logs in with valid credentials', () => {
    cy.get('input[type="email"]').type('[email protected]');
    cy.get('input[type="password"]').type('password123');
    cy.get('button[type="submit"]').click();

    cy.url().should('include', '/dashboard');
    cy.get('h1').should('contain', 'Dashboard');
  });

  it('shows error with invalid credentials', () => {
    cy.get('input[type="email"]').type('[email protected]');
    cy.get('input[type="password"]').type('wrongpassword');
    cy.get('button[type="submit"]').click();

    cy.get('.error-message').should('contain', 'Invalid credentials');
  });
});

// โœ… Good: Test user workflow
describe('User Registration Workflow', () => {
  it('completes registration', () => {
    cy.visit('http://localhost:3000/register');

    cy.get('input[name="name"]').type('John Doe');
    cy.get('input[name="email"]').type('[email protected]');
    cy.get('input[name="password"]').type('password123');
    cy.get('input[name="confirmPassword"]').type('password123');

    cy.get('button[type="submit"]').click();

    cy.url().should('include', '/verify-email');
    cy.get('.success-message').should('be.visible');
  });
});

// โœ… Good: Test with fixtures
describe('User Profile', () => {
  beforeEach(() => {
    cy.fixture('user.json').then((user) => {
      cy.visit(`http://localhost:3000/profile/${user.id}`);
    });
  });

  it('displays user information', () => {
    cy.fixture('user.json').then((user) => {
      cy.get('h1').should('contain', user.name);
      cy.get('.email').should('contain', user.email);
    });
  });
});

Playwright Testing

Playwright Setup

# โœ… Good: Install Playwright
npm install --save-dev @playwright/test

# โœ… Good: Run Playwright tests
npx playwright test
npx playwright test --headed
npx playwright test --project=chromium
npx playwright test --debug

Playwright Tests

// โœ… Good: Basic Playwright test
import { test, expect } from '@playwright/test';

test.describe('Login Page', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('http://localhost:3000/login');
  });

  test('displays login form', async ({ page }) => {
    await expect(page.locator('form')).toBeVisible();
    await expect(page.locator('input[type="email"]')).toBeVisible();
    await expect(page.locator('input[type="password"]')).toBeVisible();
  });

  test('logs in with valid credentials', async ({ page }) => {
    await page.fill('input[type="email"]', '[email protected]');
    await page.fill('input[type="password"]', 'password123');
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL(/.*dashboard/);
    await expect(page.locator('h1')).toContainText('Dashboard');
  });

  test('shows error with invalid credentials', async ({ page }) => {
    await page.fill('input[type="email"]', '[email protected]');
    await page.fill('input[type="password"]', 'wrongpassword');
    await page.click('button[type="submit"]');

    await expect(page.locator('.error-message')).toContainText('Invalid credentials');
  });
});

// โœ… Good: Test with multiple browsers
test.describe('Cross-browser Login', () => {
  test('works on Chrome', async ({ browser }) => {
    const context = await browser.newContext();
    const page = await context.newPage();
    await page.goto('http://localhost:3000/login');
    // Test
  });

  test('works on Firefox', async ({ browser }) => {
    const context = await browser.newContext();
    const page = await context.newPage();
    await page.goto('http://localhost:3000/login');
    // Test
  });
});

// โœ… Good: Test with screenshots
test('takes screenshot on failure', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await page.screenshot({ path: 'screenshot.png' });
});

Visual Regression Testing

Visual Testing

// โœ… Good: Cypress visual testing
describe('Visual Regression', () => {
  it('matches homepage snapshot', () => {
    cy.visit('http://localhost:3000');
    cy.matchImageSnapshot('homepage');
  });

  it('matches login page snapshot', () => {
    cy.visit('http://localhost:3000/login');
    cy.matchImageSnapshot('login-page');
  });
});

// โœ… Good: Playwright visual testing
test('matches homepage snapshot', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await expect(page).toHaveScreenshot('homepage.png');
});

test('matches login page snapshot', async ({ page }) => {
  await page.goto('http://localhost:3000/login');
  await expect(page).toHaveScreenshot('login-page.png');
});

Best Practices

  1. Use page objects:

    // โœ… Good: Page object pattern
    class LoginPage {
      constructor(page) {
        this.page = page;
        this.emailInput = page.locator('input[type="email"]');
        this.passwordInput = page.locator('input[type="password"]');
        this.submitButton = page.locator('button[type="submit"]');
      }
    
      async login(email, password) {
        await this.emailInput.fill(email);
        await this.passwordInput.fill(password);
        await this.submitButton.click();
      }
    }
    
    // Usage
    test('logs in', async ({ page }) => {
      const loginPage = new LoginPage(page);
      await loginPage.login('[email protected]', 'password123');
    });
    
  2. Wait for elements:

    // โœ… Good: Wait for element
    cy.get('.loading').should('not.exist');
    cy.get('.content').should('be.visible');
    
    // โŒ Bad: No waiting
    cy.get('.content').click();
    
  3. Test user workflows:

    // โœ… Good: Complete workflow
    it('completes purchase', () => {
      cy.visit('/products');
      cy.get('[data-product="1"]').click();
      cy.get('button[aria-label="Add to cart"]').click();
      cy.get('a[href="/cart"]').click();
      cy.get('button[aria-label="Checkout"]').click();
    });
    

Summary

E2E testing is essential. Key takeaways:

  • Test complete user workflows
  • Use page objects
  • Wait for elements
  • Test across browsers
  • Verify visual appearance
  • Test error scenarios
  • Keep tests maintainable
  • Run tests regularly

Next Steps

Comments