Frontend Engineering/
Lesson

Your unit tests pass. Your integration tests pass. You deploy. A user opens the site on Safari, clicks "Sign Up," and the button does nothing. The form submission handler was bound to a click event that Safari processes differently. No amount of unit testing would have caught this because the bug only exists in a real browser rendering real HTML.

End-to-end testsWhat is e2e test?An automated check that drives the full application the way a user would, clicking buttons and filling forms in a real browser. close this gap. They launch an actual browser, navigate your application, interact with the UI the way a user would, and verify that the full stack works together.

What E2E testsWhat is e2e test?An automated check that drives the full application the way a user would, clicking buttons and filling forms in a real browser. do

An E2E test automates what a human QA tester would do manually:

import { test, expect } from '@playwright/test';

test('user can purchase a product', async ({ page }) => {
  await page.goto('/shop');
  await page.click('[data-testid="product-1"]');
  await page.click('text=Add to Cart');
  await page.click('text=Checkout');
  await page.fill('#email', 'test@example.com');
  await page.fill('#card-number', '4242424242424242');
  await page.click('text=Pay Now');

  await expect(page).toHaveURL('/order-success');
  await expect(page.locator('text=Thank you')).toBeVisible();
});

This single test verifies frontend routing, APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. communication, payment processing, database persistence, and success page rendering. If any layer breaks, the test fails.

02

Choosing an E2E tool

FeaturePlaywrightCypress
Browser supportChromium, Firefox, WebKitChromium-family only
SpeedFast parallel executionSlower for large suites
Auto-waitingBuilt-in, reliableBuilt-in, good
DebuggingTrace viewer, video recordingTime-travel debugger
Mobile testingNative supportLimited
TypeScriptFirst-classRequires configuration
Test generationRecord and playbackLimited

Playwright is the recommended choice for new projects. It handles the most browsers, runs the fastest, and has the best debugging tools. When AI generates E2E testWhat is e2e test?An automated check that drives the full application the way a user would, clicking buttons and filling forms in a real browser. code, it often targets Cypress syntax, make sure the framework matches your project.

03

What to E2E testWhat is e2e test?An automated check that drives the full application the way a user would, clicking buttons and filling forms in a real browser.

E2E tests are expensive, slow to run, complex to maintain, and brittle when the UI changes. Spend them wisely on flows that would cost real money or users if broken.

Always E2E testDo not E2E test
User registration and loginIndividual button styling
Checkout and paymentForm field validation (unit test this)
Core feature workflowsAPI response parsing (integration test this)
Data persistence across sessionsEdge cases in business logic (unit test this)
Cross-browser critical pathsEvery possible user interaction

Rule of thumb: if this flow breaks in production, does the business lose money or users? If yes, it deserves an E2E test.

04

Writing reliable E2E testsWhat is e2e test?An automated check that drives the full application the way a user would, clicking buttons and filling forms in a real browser.

Stable selectors

CSS classes change during redesigns. Text content changes with copy updates. Use dedicated test attributes:

// Fragile - breaks when styling changes
await page.click('.btn.btn-primary.mt-4');

// Fragile - breaks when copy changes
await page.click('text=Submit Your Order Now');

// Stable - dedicated test attribute
await page.click('[data-testid="checkout-submit"]');

In your components:

<button
  data-testid="checkout-submit"
  className="btn btn-primary mt-4"
  onClick={handleCheckout}
>
  Submit Your Order Now
</button>

AI pitfall
When you ask AI to generate E2E tests, it invents selectors based on what it imagines your HTML looks like. It will confidently write page.click('[data-testid="submit-btn"]') when your actual attribute is data-testid="checkout-submit". Always verify that every selector in AI-generated E2E tests actually exists in your markup. Hallucinated selectors cause silent test failures that waste hours of debugging.

Handle timing correctly

Never use arbitrary timeouts. Modern E2E tools wait for elements automatically:

// Wrong - race condition
await page.click('#submit');
await page.waitForTimeout(2000);
const msg = await page.textContent('#result');

// Right - waits for the element to appear
await page.click('#submit');
await expect(page.locator('#success-message')).toBeVisible();
AI pitfall
AI-generated E2E tests almost always include waitForTimeout() calls with arbitrary delays, 1000ms, 2000ms, 3000ms. These create flaky tests: they pass on fast machines and fail in CI where environments are slower. Replace every waitForTimeout() with a proper wait condition like waitForSelector(), toBeVisible(), or waitForURL().

Isolate test data

Each test should create its own data. Tests that share state break when run in parallel or in different order:

test('user can create a post', async ({ page }) => {
  const uniqueEmail = `test-${Date.now()}@example.com`;
  await page.fill('#email', uniqueEmail);
  await page.fill('#title', `Test Post ${Date.now()}`);
  await page.click('#submit');

  await expect(page.locator('[data-testid="post-title"]')).toBeVisible();
});
05

The Page Object Model

For larger test suites, encapsulate page interactions into reusable classes:

// pages/LoginPage.js
export class LoginPage {
  constructor(page) {
    this.page = page;
    this.emailInput = page.locator('#email');
    this.passwordInput = page.locator('#password');
    this.submitButton = page.locator('[data-testid="login-submit"]');
  }

  async navigate() {
    await this.page.goto('/login');
  }

  async login(email, password) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }
}

// tests/login.spec.js
test('user can login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.navigate();
  await loginPage.login('alice@example.com', 'password123');

  await expect(page).toHaveURL('/dashboard');
});

When the login form changes, you update one file. Every test that uses LoginPage benefits from the fix.

AI pitfall
When you ask AI to generate E2E tests, it often writes long, linear scripts that duplicate selectors across every test. If a selector changes, you have to update 20 files. Always ask AI to generate Page Objects alongside the tests, and review that the Page Object methods actually match your current UI structure, AI frequently hallucinates selectors and attributes that do not exist in your app.
06

AuthenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. shortcuts

Logging in through the UI for every test wastes time. Use authentication fixtures:

// Save auth state once
test('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.fill('#email', 'test@example.com');
  await page.fill('#password', 'password123');
  await page.click('[data-testid="login-submit"]');
  await page.waitForURL('/dashboard');

  await page.context().storageState({ path: 'auth.json' });
});

// All subsequent tests start authenticated
test('can access settings', async ({ page }) => {
  await page.goto('/settings');
  await expect(page.locator('h1')).toHaveText('Settings');
});
07

Debugging failures

When an E2E testWhat is e2e test?An automated check that drives the full application the way a user would, clicking buttons and filling forms in a real browser. fails, you need to see what happened:

// playwright.config.js
export default {
  use: {
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'on-first-retry'
  }
};
TechniqueWhen to useCommand
Headed modeSee the browser while test runsnpx playwright test --headed
Pause executionStep through interactivelyawait page.pause() in test
Trace viewerInspect failed test timelinenpx playwright show-trace trace.zip
ScreenshotsQuick visual check of failure stateAutomatic on failure
Video recordingSee exactly what happenedAutomatic on failure
08

E2E testing best practices

DoDo not
Test critical user paths onlyTest every feature with E2E
Use data-testid attributesRely on CSS classes or text content
Wait for elements properlyUse arbitrary waitForTimeout calls
Create isolated test dataShare state between tests
Use Page Object ModelDuplicate selectors across files
Investigate flaky tests immediatelyIgnore intermittent failures
Run tests in CI on every PROnly run locally before deploy

E2E testsWhat is e2e test?An automated check that drives the full application the way a user would, clicking buttons and filling forms in a real browser. are the most expensive tests to write and maintain. But for critical user flows, the paths that generate revenue, retain users, or protect data, they are irreplaceable. A single E2E test that catches a broken checkout before deployment pays for itself thousands of times over.