| name | e2e-testing-automation |
| description | Build end-to-end automated tests that simulate real user interactions across the full application stack. Use for E2E test, Selenium, Cypress, Playwright, browser automation, and user journey testing. |
E2E Testing Automation
Overview
End-to-end (E2E) testing validates complete user workflows from the UI through all backend systems, ensuring the entire application stack works together correctly from a user's perspective. E2E tests simulate real user interactions with browsers, handling authentication, navigation, form submissions, and validating results.
When to Use
- Testing critical user journeys (signup, checkout, login)
- Validating multi-step workflows
- Testing across different browsers and devices
- Regression testing for UI changes
- Verifying frontend-backend integration
- Testing with real user interactions (clicks, typing, scrolling)
- Smoke testing deployments
Instructions
1. Playwright E2E Tests
import { test, expect, Page } from '@playwright/test';
test.describe('E-commerce Checkout Flow', () => {
let page: Page;
test.beforeEach(async ({ page: p }) => {
page = p;
await page.goto('/');
});
test('complete checkout flow as guest user', async () => {
await page.click('text=Shop Now');
await page.click('[data-testid="product-1"]');
await expect(page.locator('h1')).toContainText('Product Name');
await page.click('button:has-text("Add to Cart")');
await expect(page.locator('.cart-count')).toHaveText('1');
await page.click('[data-testid="cart-icon"]');
await expect(page.locator('.cart-item')).toHaveCount(1);
await page.click('text=Proceed to Checkout');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="firstName"]', 'John');
await page.fill('[name="lastName"]', 'Doe');
await page.fill('[name="address"]', '123 Main St');
await page.fill('[name="city"]', 'San Francisco');
await page.selectOption('[name="state"]', 'CA');
await page.fill('[name="zip"]', '94105');
await page.click('text=Continue to Payment');
const paymentFrame = page.frameLocator('iframe[name="payment-frame"]');
await paymentFrame.locator('[name="cardNumber"]').fill('4242424242424242');
await paymentFrame.locator('[name="expiry"]').fill('12/25');
await paymentFrame.locator('[name="cvc"]').fill('123');
await page.click('button:has-text("Place Order")');
await expect(page).toHaveURL(/\/order\/confirmation/);
await expect(page.locator('.confirmation-message')).toContainText('Order placed successfully');
const orderNumber = await page.locator('[data-testid="order-number"]').textContent();
expect(orderNumber).toMatch(/^ORD-\d+$/);
});
test('checkout with existing user account', async () => {
await page.click('text=Sign In');
await page.fill('[name="email"]', 'existing@example.com');
await page.fill('[name="password"]', 'Password123!');
await page.click('button[type="submit"]');
await expect(page.locator('.user-menu')).toContainText('existing@example.com');
await page.click('[data-testid="product-2"]');
await page.click('button:has-text("Add to Cart")');
await page.click('[data-testid="cart-icon"]');
await page.click('text=Checkout');
await expect(page.locator('[name="address"]')).toHaveValue(/./);
await page.click('button:has-text("Use Saved Payment")');
await page.click('button:has-text("Place Order")');
await expect(page).toHaveURL(/\/order\/confirmation/);
});
test('handle out of stock product', async () => {
await page.click('[data-testid="product-out-of-stock"]');
const addToCartButton = page.locator('button:has-text("Add to Cart")');
await expect(addToCartButton).toBeDisabled();
await expect(page.locator('.stock-status')).toHaveText('Out of Stock');
});
});
2. Cypress E2E Tests
describe('User Authentication Flow', () => {
beforeEach(() => {
cy.visit('/');
});
it('should register a new user account', () => {
cy.get('[data-cy="signup-button"]').click();
cy.url().should('include', '/signup');
const timestamp = Date.now();
cy.get('[name="email"]').type(`user${timestamp}@example.com`);
cy.get('[name="password"]').type('SecurePass123!');
cy.get('[name="confirmPassword"]').type('SecurePass123!');
cy.get('[name="firstName"]').type('Test');
cy.get('[name="lastName"]').type('User');
cy.get('[name="acceptTerms"]').check();
cy.().();
cy.().(, );
cy.().(, );
cy.()
.()
.(, , );
});
(, {
cy.().();
cy.().();
cy.().(, );
cy.()
.()
.(, );
cy.().();
cy.().();
cy.().();
cy.()
.()
.(, );
cy.()
.()
.(, );
});
(, {
cy.(, , {
: ,
: ,
:
});
cy.().();
cy.().();
cy.().();
cy.().();
cy.().(, );
cy.().();
cy.().();
cy.().(, );
});
(, {
cy.(, );
cy.();
cy.().();
cy.();
cy.().();
cy.().();
});
(, {
cy.(, );
cy.();
cy.().();
cy.().();
cy.().(, .(). + );
cy.().();
});
});
..(, {
cy.(, , { email, password })
.( {
..(, response..);
});
});
3. Selenium with Python (pytest)
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
class TestSearchFunctionality:
@pytest.fixture
def driver(self):
"""Setup and teardown browser."""
options = webdriver.ChromeOptions()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(10)
yield driver
driver.quit()
def test_search_with_results(self, driver):
"""Test search functionality returns relevant results."""
driver.get('http://localhost:3000')
search_box = driver.find_element(By.NAME, 'search')
search_box.send_keys('laptop')
search_box.send_keys(Keys.RETURN)
wait = WebDriverWait(driver, 10)
results = wait.until(
EC.presence_of_all_elements_located((By.CLASS_NAME, 'search-result'))
)
assert len(results) > 0
assert 'laptop' driver.page_source.lower()
first_result = results[]
first_result.find_element(By.CLASS_NAME, )
first_result.find_element(By.CLASS_NAME, )
first_result.find_element(By.CLASS_NAME, )
():
driver.get()
wait = WebDriverWait(driver, )
wait.until(
EC.presence_of_element_located((By.CLASS_NAME, ))
)
initial_count = (driver.find_elements(By.CLASS_NAME, ))
price_filter = driver.find_element(By.ID, )
price_filter.click()
wait.until(
EC.staleness_of(driver.find_element(By.CLASS_NAME, ))
)
wait.until(
EC.presence_of_element_located((By.CLASS_NAME, ))
)
filtered_count = (driver.find_elements(By.CLASS_NAME, ))
filtered_count <= initial_count
prices = driver.find_elements(By.CLASS_NAME, )
price_elem prices:
price = (price_elem.text.replace(, ).replace(, ))
<= price <=
():
driver.get()
wait = WebDriverWait(driver, )
first_page_results = driver.find_elements(By.CLASS_NAME, )
first_result_title = first_page_results[].find_element(
By.CLASS_NAME,
).text
next_button = driver.find_element(By.CSS_SELECTOR, )
next_button.click()
wait.until(EC.staleness_of(first_page_results[]))
driver.current_url
second_page_results = driver.find_elements(By.CLASS_NAME, )
second_result_title = second_page_results[].find_element(
By.CLASS_NAME,
).text
first_result_title != second_result_title
():
driver.get()
search_box = driver.find_element(By.NAME, )
search_box.send_keys()
search_box.send_keys(Keys.RETURN)
wait = WebDriverWait(driver, )
no_results = wait.until(
EC.presence_of_element_located((By.CLASS_NAME, ))
)
no_results.text.lower()
(driver.find_elements(By.CLASS_NAME, )) ==
4. Page Object Model Pattern
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('[name="email"]');
this.passwordInput = page.locator('[name="password"]');
this.loginButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('.error-message');
}
async goto() {
await this..();
}
() {
..(email);
..(password);
..();
}
(): <> {
..();
}
}
{ test, expect } ;
{ } ;
(, ({ page }) => {
loginPage = (page);
loginPage.();
loginPage.(, );
error = loginPage.();
(error).();
});
Best Practices
✅ DO
- Use data-testid attributes for stable selectors
- Implement Page Object Model for maintainability
- Test critical user journeys thoroughly
- Run tests in multiple browsers (cross-browser testing)
- Use explicit waits instead of sleep/timeouts
- Clean up test data after each test
- Take screenshots on failures
- Parallelize test execution where possible
❌ DON'T
- Use brittle CSS selectors (like nth-child)
- Test every possible UI combination (focus on critical paths)
- Share state between tests
- Use fixed delays (sleep/timeout)
- Ignore flaky tests
- Run E2E tests for unit-level testing
- Test third-party UI components in detail
- Skip mobile/responsive testing
Tools & Frameworks
- Playwright: Modern, fast, reliable (Node.js, Python, Java, .NET)
- Cypress: Developer-friendly, fast feedback loop (JavaScript)
- Selenium: Cross-browser, mature ecosystem (multiple languages)
- Puppeteer: Chrome DevTools Protocol automation (Node.js)
- WebDriverIO: Next-gen browser automation (Node.js)
Configuration Examples
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
timeout: 30000,
retries: 2,
workers: process.env.CI ? 2 : 4,
use: {
baseURL: 'http://localhost:3000',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
{ name: 'firefox', use: { browserName: 'firefox' } },
{ name: 'webkit', use: { browserName: 'webkit' } },
],
webServer: {
command: 'npm run start',
port: 3000,
reuseExistingServer: !process.env.CI,
},
});
Examples
See also: integration-testing, visual-regression-testing, accessibility-testing, test-automation-framework skills.