End-to-End Testing Concepts

Understanding the principles and strategies for comprehensive E2E testing

What is End-to-End Testing?

End-to-End (E2E) testing examines the entire application workflow from start to finish, ensuring all components work together as expected. It's like test-driving a car before purchase rather than just checking individual parts in isolation.

graph LR A[User Interface] -->|Interacts with| B[Frontend Logic] B -->|Makes requests to| C[API/Backend] C -->|Queries| D[Database] C -->|May call| E[External Services] style A fill:#d1e7dd,stroke:#0f5132 style B fill:#d1e7dd,stroke:#0f5132 style C fill:#d1e7dd,stroke:#0f5132 style D fill:#d1e7dd,stroke:#0f5132 style E fill:#d1e7dd,stroke:#0f5132

Unlike unit or integration tests that focus on specific components, E2E tests validate the entire system from the user's perspective. They answer questions like:

Real-World Analogy

Think of testing a car:

  • Unit Tests: Testing individual parts (brakes, engine, lights)
  • Integration Tests: Testing how parts work together (engine + transmission)
  • E2E Tests: Actually driving the car through different roads and conditions

E2E tests validate the experience as a whole, not just the components.

The Test Pyramid and E2E Testing

graph TD A[E2E Tests] --> B[Integration Tests] B --> C[Unit Tests] style A fill:#f8d7da,stroke:#721c24 style B fill:#fff3cd,stroke:#856404 style C fill:#d1e7dd,stroke:#0f5132 classDef pyramidLabel text-align:center,fill:none,stroke:none; class D,E,F pyramidLabel D["Few, \nSlow, \nExpensive"] E["Some, \nModerate Speed, \nModerate Cost"] F["Many, \nFast, \nCheap"] D --- A E --- B F --- C

The Test Pyramid, introduced by Mike Cohn, illustrates the recommended proportions of different types of tests in a software testing strategy:

Why E2E Tests Are at the Top

Practical Test Distribution

A well-balanced testing strategy might look like:

  • 70% Unit Tests: Testing individual functions and components
  • 20% Integration Tests: Testing interactions between components
  • 10% E2E Tests: Testing complete user workflows

This balance provides good coverage while keeping test suites maintainable and reasonably fast.

E2E Testing Approaches

Graphical User Interface (GUI) Testing

Focuses on testing the application through its user interface, simulating actual user interactions.

API-Based Testing

Tests the application by making direct API calls and verifying responses.

Headless Browser Testing

Uses browsers without a graphical interface to test web applications.

Visual Regression Testing

Compares screenshots of the application to detect visual changes.

Choosing the Right Approach

Consider these factors when selecting your E2E testing approach:

  • Application Type: Web, mobile, desktop, or API-based?
  • Team Skills: What technologies is your team familiar with?
  • Test Environment: Can you run browsers in your CI/CD pipeline?
  • Test Speed: How fast do tests need to run?
  • Coverage Needs: What aspects of the application are most critical?

Many teams use a combination of approaches for comprehensive coverage.

Key Components of E2E Testing

Test Scenarios

High-level descriptions of the workflows to test.

Example Scenario: E-commerce Checkout

"User adds items to cart, proceeds to checkout, enters shipping information, selects payment method, confirms order, and receives order confirmation."

Test Cases

Specific conditions to test with expected outcomes.

Example Test Cases for Checkout

  • User can add multiple items to cart
  • Cart updates quantity correctly
  • User can remove items from cart
  • Cart calculates totals correctly
  • Checkout form validates required fields
  • Order is processed when valid payment is provided

Test Steps

Detailed actions to execute during the test.

// Example steps for testing adding an item to cart
// 1. Navigate to product page
// 2. Select product options (size, color, etc.)
// 3. Click "Add to Cart" button
// 4. Verify cart icon updates
// 5. Navigate to cart page
// 6. Verify correct item appears in cart
// 7. Verify price and quantity are correct

Assertions

Statements that verify the expected outcomes.

// Example assertions for adding an item to cart
expect(cartItemCount).toBe(1);
expect(cartItemName).toBe('Expected Product Name');
expect(cartItemPrice).toBe('$19.99');
expect(cartItemQuantity).toBe(1);
expect(cartTotal).toBe('$19.99');

Test Data

The information used during test execution.

Test Environment

The system configuration where tests run.

Popular E2E Testing Tools

Cypress

A modern JavaScript-based testing framework that runs directly in the browser.

graph TD A[Cypress] --> B[Runs in Browser] A --> C[Time Travel & Debugging] A --> D[Auto-waiting Mechanism] A --> E[Real-time Reloading] A --> F[Network Traffic Control] style A fill:#d1e7dd,stroke:#0f5132
// Example Cypress test
describe('Product Page', () => {
  it('adds item to cart', () => {
    cy.visit('/products/sample-product');
    cy.get('.size-selector').select('Medium');
    cy.get('.color-selector').select('Blue');
    cy.get('.add-to-cart-button').click();
    
    // Verify cart update
    cy.get('.cart-count').should('contain', '1');
    cy.get('.cart-icon').click();
    cy.get('.cart-item').should('have.length', 1);
    cy.get('.item-name').should('contain', 'Sample Product');
    cy.get('.item-price').should('contain', '$19.99');
  });
});

Playwright

A Microsoft-backed testing framework that supports multiple browsers and languages.

graph TD A[Playwright] --> B[Multi-browser Support] A --> C[Auto-waiting] A --> D[Mobile Emulation] A --> E[Network Interception] A --> F[Multiple Language Support] style A fill:#cfe2ff,stroke:#084298
// Example Playwright test
const { test, expect } = require('@playwright/test');

test('adds item to cart', async ({ page }) => {
  await page.goto('/products/sample-product');
  await page.selectOption('.size-selector', 'Medium');
  await page.selectOption('.color-selector', 'Blue');
  await page.click('.add-to-cart-button');
  
  // Verify cart update
  await expect(page.locator('.cart-count')).toContainText('1');
  await page.click('.cart-icon');
  await expect(page.locator('.cart-item')).toHaveCount(1);
  await expect(page.locator('.item-name')).toContainText('Sample Product');
  await expect(page.locator('.item-price')).toContainText('$19.99');
});

Selenium

One of the oldest and most widely used browser automation tools.

graph TD A[Selenium] --> B[Wide Browser Support] A --> C[Multiple Language Bindings] A --> D[Large Community] A --> E[Grid for Parallel Testing] A --> F[Mature Ecosystem] style A fill:#fff3cd,stroke:#856404
// Example Selenium test with JavaScript
const { Builder, By, until } = require('selenium-webdriver');

(async function example() {
  let driver = await new Builder().forBrowser('chrome').build();
  
  try {
    await driver.get('/products/sample-product');
    await driver.findElement(By.css('.size-selector')).sendKeys('Medium');
    await driver.findElement(By.css('.color-selector')).sendKeys('Blue');
    await driver.findElement(By.css('.add-to-cart-button')).click();
    
    // Verify cart update
    let cartCount = await driver.findElement(By.css('.cart-count'));
    await driver.wait(until.elementTextIs(cartCount, '1'), 5000);
    await driver.findElement(By.css('.cart-icon')).click();
    
    let cartItems = await driver.findElements(By.css('.cart-item'));
    assert.strictEqual(cartItems.length, 1);
    
    let itemName = await driver.findElement(By.css('.item-name')).getText();
    assert.strictEqual(itemName, 'Sample Product');
    
    let itemPrice = await driver.findElement(By.css('.item-price')).getText();
    assert.strictEqual(itemPrice, '$19.99');
  } finally {
    await driver.quit();
  }
})();

TestCafe

A Node.js tool for automated web testing with a focus on ease of use.

graph TD A[TestCafe] --> B[No WebDriver Dependency] A --> C[Built-in Waiting] A --> D[Parallel Testing] A --> E[Role-based Authentication] A --> F[Runs on any Modern Browser] style A fill:#d1ecf1,stroke:#0c5460
// Example TestCafe test
import { Selector } from 'testcafe';

fixture `Product Page`
    .page `http://localhost:3000/products/sample-product`;

test('Adds item to cart', async t => {
    await t
        .click('.size-selector')
        .click(Selector('.size-option').withText('Medium'))
        .click('.color-selector')
        .click(Selector('.color-option').withText('Blue'))
        .click('.add-to-cart-button');
    
    // Verify cart update
    await t
        .expect(Selector('.cart-count').innerText).eql('1')
        .click('.cart-icon')
        .expect(Selector('.cart-item').count).eql(1)
        .expect(Selector('.item-name').innerText).eql('Sample Product')
        .expect(Selector('.item-price').innerText).eql('$19.99');
});

Tool Comparison

Tool Strengths Best For
Cypress Developer-friendly, great debugging, strong documentation Modern web applications, JavaScript developers
Playwright Multi-browser, mobile testing, multiple languages Teams needing cross-browser support, various platforms
Selenium Mature, flexible, widely supported Enterprise applications, multi-language teams
TestCafe Easy setup, no WebDriver dependency Quick test implementation, simpler web applications

E2E Testing Best Practices

Test Critical Paths First

Focus on the most important user journeys through your application.

Keep Tests Independent

Each test should be self-contained and not rely on the state from other tests.

Use Stable Selectors

Choose element selectors that are less likely to change with UI updates.

Selector Stability Comparison

More Stable Less Stable
data-testid="login-button" button.btn.btn-primary
aria-label="Search" #search-box
role="navigation" .navbar.navbar-dark

Dedicated test attributes like data-testid provide the most stability.

Manage Test Data Effectively

Control the test data to create consistent and predictable tests.

Handle Asynchronous Operations

Web applications often have asynchronous operations that need special handling in tests.

// Bad practice: arbitrary delay
await page.click('.submit-button');
await page.waitForTimeout(2000); // Hope 2 seconds is enough
await expect(page.locator('.success-message')).toBeVisible();

// Good practice: wait for specific condition
await page.click('.submit-button');
await page.waitForSelector('.success-message');
await expect(page.locator('.success-message')).toBeVisible();

Implement Retry Logic

Make tests more resilient by retrying operations that might fail due to timing issues.

// Example retry implementation
async function retryOperation(operation, maxRetries = 3, delay = 1000) {
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      console.log(`Attempt ${attempt} failed: ${error.message}`);
      lastError = error;
      
      if (attempt < maxRetries) {
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
  
  throw lastError;
}

Parallelize Test Execution

Run tests in parallel to reduce execution time, especially in CI/CD pipelines.

Monitor Test Flakiness

Track and reduce tests that produce inconsistent results.

Handling Common E2E Testing Challenges

Authentication

Testing authenticated flows can be tricky, especially with modern authentication systems.

Approaches:

  1. UI Login: Navigate through the login form (slowest but most thorough)
  2. Programmatic Login: Use API calls to log in and set cookies/tokens
  3. Mock Authentication: Bypass authentication for testing protected routes
// Cypress example: Programmatic login
// In cypress/support/commands.js
Cypress.Commands.add('loginByApi', (email, password) => {
  cy.request({
    method: 'POST',
    url: '/api/login',
    body: { email, password }
  }).then((response) => {
    window.localStorage.setItem('authToken', response.body.token);
  });
});

// In test
it('tests authenticated feature', () => {
  cy.loginByApi('test@example.com', 'password123');
  cy.visit('/dashboard');
  cy.get('.user-greeting').should('contain', 'Welcome, Test User');
});

Third-Party Integrations

External services can complicate E2E testing by introducing dependencies outside your control.

Approaches:

  1. Test Environments: Use sandbox/test environments provided by the third-party
  2. Service Mocking: Mock third-party API responses
  3. Request Interception: Intercept and modify HTTP requests/responses
// Playwright example: Mocking a payment gateway
test('completes checkout with mocked payment', async ({ page }) => {
  // Mock payment gateway response
  await page.route('https://api.payment-provider.com/v1/payments', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        id: 'test-payment-123',
        status: 'succeeded',
        amount: 1999,
        currency: 'usd'
      })
    });
  });
  
  // Proceed with checkout flow
  await page.goto('/checkout');
  // Fill in details and submit payment...
});

Handling Dates and Times

Time-dependent tests can be unreliable due to changing dates and times.

Approaches:

  1. Mock Date/Time: Override JavaScript's Date object
  2. Relative Assertions: Test relative to current time rather than absolute values
  3. Time Freezing: Use libraries to freeze time during tests
// Cypress example: Mocking date
cy.clock(new Date(2025, 0, 15).getTime()); // Set fixed date: Jan 15, 2025
cy.visit('/calendar');
cy.get('.current-date').should('contain', 'January 15, 2025');

Testing File Uploads and Downloads

File operations often require special handling in E2E tests.

Approaches:

  1. Test File Preparation: Create test files in the test setup
  2. Bypass UI: Use direct API calls for file operations when possible
  3. Download Verification: Check file existence and content after download
// Cypress example: File upload
cy.fixture('test-image.jpg', 'binary')
  .then(Cypress.Blob.binaryStringToBlob)
  .then(blob => {
    const file = new File([blob], 'test-image.jpg', { type: 'image/jpeg' });
    const dataTransfer = new DataTransfer();
    dataTransfer.items.add(file);
    
    cy.get('input[type="file"]').then(input => {
      input[0].files = dataTransfer.files;
      cy.wrap(input).trigger('change', { force: true });
    });
  });

cy.get('.upload-success').should('be.visible');

Handling Animations and Transitions

Modern UIs often include animations that can interfere with test timing.

Approaches:

  1. Disable Animations: Add a CSS rule to disable animations in test mode
  2. Wait for Transitions: Explicitly wait for animations to complete
  3. Animation-Aware Selectors: Use selectors that account for animation states
// CSS to disable animations in test mode
// Add to your application when running tests
if (process.env.NODE_ENV === 'test') {
  const style = document.createElement('style');
  style.textContent = `
    *, *::before, *::after {
      transition-duration: 0s !important;
      animation-duration: 0s !important;
      animation-delay: 0s !important;
    }
  `;
  document.head.appendChild(style);
}

Real-World E2E Testing Example

Let's walk through a complete E2E test for an e-commerce checkout flow using Cypress.

Scenario: Complete Checkout Flow

Test the entire process from product selection to order confirmation.

// cypress/e2e/checkout-flow.cy.js
describe('Checkout Flow', () => {
  let testUser;
  
  before(() => {
    // Create test data
    cy.task('createTestUser').then(user => {
      testUser = user;
    });
    cy.task('createTestProducts');
  });
  
  it('completes the checkout process successfully', () => {
    // Login
    cy.visit('/login');
    cy.get('#email').type(testUser.email);
    cy.get('#password').type(testUser.password);
    cy.get('button[type="submit"]').click();
    cy.url().should('include', '/dashboard');
    
    // Browse products
    cy.visit('/products');
    cy.get('.product-card').first().click();
    
    // Select options and add to cart
    cy.get('.size-selector').select('Medium');
    cy.get('.color-selector').select('Blue');
    cy.get('.quantity-input').clear().type('2');
    cy.get('.add-to-cart-button').click();
    
    // Verify cart update
    cy.get('.cart-notification').should('be.visible');
    cy.get('.cart-count').should('contain', '2');
    
    // Go to cart
    cy.get('.cart-icon').click();
    cy.url().should('include', '/cart');
    
    // Verify cart contents
    cy.get('.cart-item').should('have.length', 1);
    cy.get('.item-quantity').should('contain', '2');
    
    // Proceed to checkout
    cy.get('.checkout-button').click();
    cy.url().should('include', '/checkout');
    
    // Fill shipping information
    cy.get('#shipping-address').select('Add new address');
    cy.get('#street').type('123 Test St');
    cy.get('#city').type('Test City');
    cy.get('#state').select('California');
    cy.get('#zip').type('90210');
    cy.get('#shipping-method').select('Standard Shipping');
    cy.get('.continue-button').click();
    
    // Fill payment information
    cy.get('#card-number').type('4242424242424242');
    cy.get('#card-expiry').type('1235');
    cy.get('#card-cvc').type('123');
    cy.get('#billing-same-as-shipping').check();
    cy.get('.continue-button').click();
    
    // Review order
    cy.get('.order-summary').should('be.visible');
    cy.get('.order-items').children().should('have.length', 1);
    cy.get('.subtotal').should('exist');
    cy.get('.shipping-cost').should('exist');
    cy.get('.tax').should('exist');
    cy.get('.total').should('exist');
    
    // Place order
    cy.get('.place-order-button').click();
    
    // Verify order confirmation
    cy.url().should('include', '/order-confirmation');
    cy.get('.confirmation-message').should('contain', 'Thank you for your order');
    cy.get('.order-number').should('exist');
    
    // Verify order exists in user's order history
    cy.visit('/account/orders');
    cy.get('.order-item').should('have.length.at.least', 1);
    cy.get('.order-item').first().should('contain', 'Processing');
  });
  
  it('validates required fields during checkout', () => {
    // Add item to cart first
    cy.visit('/products');
    cy.get('.product-card').first().click();
    cy.get('.add-to-cart-button').click();
    cy.get('.cart-icon').click();
    cy.get('.checkout-button').click();
    
    // Try to continue without filling required fields
    cy.get('.continue-button').click();
    
    // Check for validation messages
    cy.get('#street-error').should('be.visible');
    cy.get('#city-error').should('be.visible');
    cy.get('#state-error').should('be.visible');
    cy.get('#zip-error').should('be.visible');
    
    // Fill one field and check that its error disappears
    cy.get('#street').type('123 Test St');
    cy.get('#street-error').should('not.exist');
  });
  
  it('shows error message for invalid payment details', () => {
    // Setup cart and get to payment page
    cy.visit('/products');
    cy.get('.product-card').first().click();
    cy.get('.add-to-cart-button').click();
    cy.get('.cart-icon').click();
    cy.get('.checkout-button').click();
    
    // Fill shipping information
    cy.get('#shipping-address').select('Add new address');
    cy.get('#street').type('123 Test St');
    cy.get('#city').type('Test City');
    cy.get('#state').select('California');
    cy.get('#zip').type('90210');
    cy.get('#shipping-method').select('Standard Shipping');
    cy.get('.continue-button').click();
    
    // Enter invalid card number
    cy.get('#card-number').type('4242424242424241'); // Invalid last digit
    cy.get('#card-expiry').type('1235');
    cy.get('#card-cvc').type('123');
    cy.get('#billing-same-as-shipping').check();
    cy.get('.continue-button').click();
    
    // Verify error message
    cy.get('.payment-error').should('be.visible');
    cy.get('.payment-error').should('contain', 'Your card number is invalid');
  });
});

Setup for Test Isolation

// cypress/support/tasks.js
module.exports = {
  async createTestUser() {
    // Connect to test database
    const db = require('../../db/testConnection');
    
    // Create a unique test user
    const testUser = {
      email: `test-${Date.now()}@example.com`,
      password: 'TestPassword123',
      name: 'Test User'
    };
    
    const result = await db.collection('users').insertOne(testUser);
    testUser.id = result.insertedId;
    
    return testUser;
  },
  
  async createTestProducts() {
    const db = require('../../db/testConnection');
    
    // Create test products
    const products = [
      {
        name: 'Test Product 1',
        description: 'This is a test product',
        price: 19.99,
        inventory: 100,
        options: {
          sizes: ['Small', 'Medium', 'Large'],
          colors: ['Red', 'Blue', 'Green']
        }
      },
      {
        name: 'Test Product 2',
        description: 'Another test product',
        price: 29.99,
        inventory: 50,
        options: {
          sizes: ['Small', 'Medium', 'Large'],
          colors: ['Black', 'White']
        }
      }
    ];
    
    await db.collection('products').insertMany(products);
  }
};

Cypress Configuration

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

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    setupNodeEvents(on, config) {
      on('task', tasks);
    },
    env: {
      apiUrl: 'http://localhost:3001/api'
    },
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 10000,
    videoCompression: 15
  }
});

Integrating E2E Tests into CI/CD Pipelines

E2E tests provide the most value when integrated into your continuous integration and deployment process.

GitHub Actions Example

// .github/workflows/e2e-tests.yml
name: E2E Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  cypress-run:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        browser: [chrome, firefox]
    
    services:
      # Start MongoDB for the test database
      mongodb:
        image: mongo:5.0
        ports:
          - 27017:27017
    
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 16
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build the app
        run: npm run build
      
      - name: Start the app and API server
        run: |
          npm run start:api &
          npm run start &
        env:
          NODE_ENV: test
          DATABASE_URL: mongodb://localhost:27017/test-db
          API_PORT: 3001
          PORT: 3000
      
      - name: Wait for servers to start
        run: |
          npx wait-on http://localhost:3000
          npx wait-on http://localhost:3001/api/health
      
      - name: Run Cypress tests
        uses: cypress-io/github-action@v5
        with:
          browser: ${{ matrix.browser }}
          record: true
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Upload screenshots if tests fail
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: cypress-screenshots-${{ matrix.browser }}
          path: cypress/screenshots
          if-no-files-found: ignore
      
      - name: Upload videos
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: cypress-videos-${{ matrix.browser }}
          path: cypress/videos
          if-no-files-found: ignore

Test Execution Strategy

Consider when and how to run E2E tests in your pipeline:

Managing Test Flakiness in CI

// cypress.config.js with CI-specific settings
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    retries: {
      runMode: 2,         // Retry failed tests in CI
      openMode: 0          // Don't retry in development
    },
    // Additional CI settings
    trashAssetsBeforeRuns: true,
    video: true,
    screenshotOnRunFailure: true
  }
});

Analyzing and Improving E2E Test Results

Test Reports and Dashboards

Visualize test results to identify patterns and issues:

graph TD A[Test Execution] --> B[Generate Reports] B --> C[Analyze Results] C --> D[Identify Issues] D --> E[Improve Tests] E --> A style A fill:#cce5ff,stroke:#004085 style B fill:#d1e7dd,stroke:#0f5132 style C fill:#fff3cd,stroke:#856404 style D fill:#f8d7da,stroke:#721c24 style E fill:#d1ecf1,stroke:#0c5460

Common Metrics to Track

Optimizing Test Performance

Maintaining E2E Tests Over Time

// Example Page Object Pattern
// pages/ProductPage.js
class ProductPage {
  visit(productId) {
    cy.visit(`/products/${productId}`);
    return this;
  }
  
  selectSize(size) {
    cy.get('.size-selector').select(size);
    return this;
  }
  
  selectColor(color) {
    cy.get('.color-selector').select(color);
    return this;
  }
  
  setQuantity(quantity) {
    cy.get('.quantity-input').clear().type(quantity);
    return this;
  }
  
  addToCart() {
    cy.get('.add-to-cart-button').click();
    return this;
  }
  
  verifyAddedToCart() {
    cy.get('.cart-notification').should('be.visible');
    return this;
  }
}

export default new ProductPage();

// Using the Page Object in a test
import ProductPage from '../pages/ProductPage';

it('adds product to cart', () => {
  ProductPage
    .visit('sample-product')
    .selectSize('Medium')
    .selectColor('Blue')
    .setQuantity('2')
    .addToCart()
    .verifyAddedToCart();
});

Practice Activities

Activity 1: Write Your First E2E Test

Set up Cypress and write an E2E test for a simple to-do application:

  1. Create a test that navigates to the application
  2. Add a new to-do item
  3. Mark the item as completed
  4. Delete the item
  5. Verify each action has the expected result

Use this as a starting point:

// cypress/e2e/todo.cy.js
describe('Todo Application', () => {
  beforeEach(() => {
    cy.visit('http://localhost:3000');
  });
  
  it('adds, completes, and deletes a todo item', () => {
    // Your code here
  });
});

Activity 2: Test Authentication Flow

Create E2E tests for a user authentication flow:

  1. Test successful registration with valid data
  2. Test registration validation (password requirements, email format)
  3. Test successful login
  4. Test login with incorrect credentials
  5. Test password reset flow

Implement proper test isolation and data management.

Activity 3: Refactor Tests Using Page Objects

Take an existing E2E test and refactor it using the Page Object pattern:

  1. Identify pages or components used in the test
  2. Create Page Object classes for each one
  3. Move selectors and actions into the Page Objects
  4. Refactor the test to use the Page Objects
  5. Add a second test that reuses the Page Objects

Compare the readability and maintainability before and after.

Further Reading