E2E Test Builder
Build reliable end-to-end tests for critical user flows.
Playwright Test Setup
// playwright.config.ts import { defineConfig } from "@playwright/test";
export default defineConfig({ testDir: "./e2e", fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: "html", use: { baseURL: "http://localhost:3000", trace: "on-first-retry", screenshot: "only-on-failure", }, projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, { name: "firefox", use: { ...devices["Desktop Firefox"] }, }, { name: "webkit", use: { ...devices["Desktop Safari"] }, }, { name: "Mobile Chrome", use: { ...devices["Pixel 5"] }, }, ], webServer: { command: "npm run dev", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, }, });
Critical Flow Tests
// e2e/checkout-flow.spec.ts import { test, expect } from "@playwright/test";
test.describe("Checkout Flow", () => { test.beforeEach(async ({ page }) => { // Navigate to home page await page.goto("/");
// Login
await page.getByRole("button", { name: "Login" }).click();
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Password").fill("password123");
await page.getByRole("button", { name: "Sign In" }).click();
// Wait for dashboard
await expect(page).toHaveURL("/dashboard");
});
test("should complete checkout successfully", async ({ page }) => { // 1. Browse products await page.getByRole("link", { name: "Products" }).click(); await expect(page).toHaveURL("/products");
// 2. Add product to cart
await page.getByRole("button", { name: "Add to Cart" }).first().click();
await expect(page.getByText("Added to cart")).toBeVisible();
// 3. Go to cart
await page.getByRole("link", { name: "Cart" }).click();
await expect(page).toHaveURL("/cart");
await expect(
page.getByRole("heading", { name: "Shopping Cart" })
).toBeVisible();
// 4. Proceed to checkout
await page.getByRole("button", { name: "Checkout" }).click();
await expect(page).toHaveURL("/checkout");
// 5. Fill shipping information
await page.getByLabel("Full Name").fill("John Doe");
await page.getByLabel("Address").fill("123 Main St");
await page.getByLabel("City").fill("New York");
await page.getByLabel("ZIP Code").fill("10001");
// 6. Fill payment information
await page.getByLabel("Card Number").fill("4242424242424242");
await page.getByLabel("Expiry Date").fill("12/25");
await page.getByLabel("CVC").fill("123");
// 7. Place order
await page.getByRole("button", { name: "Place Order" }).click();
// 8. Verify success
await expect(page).toHaveURL(/\/order\/\d+/);
await expect(page.getByText("Order confirmed!")).toBeVisible();
await expect(page.getByText(/Order #\d+/)).toBeVisible();
});
test("should show validation errors for empty fields", async ({ page }) => { // Navigate to checkout await page.goto("/checkout");
// Try to submit without filling fields
await page.getByRole("button", { name: "Place Order" }).click();
// Verify validation errors
await expect(page.getByText("Name is required")).toBeVisible();
await expect(page.getByText("Address is required")).toBeVisible();
await expect(page.getByText("Card number is required")).toBeVisible();
});
test("should handle payment failure", async ({ page }) => { // Add product and go to checkout await page.goto("/products"); await page.getByRole("button", { name: "Add to Cart" }).first().click(); await page.goto("/checkout");
// Fill with failing card number
await page.getByLabel("Card Number").fill("4000000000000002");
await page.getByLabel("Expiry Date").fill("12/25");
await page.getByLabel("CVC").fill("123");
// Submit
await page.getByRole("button", { name: "Place Order" }).click();
// Verify error message
await expect(page.getByText("Payment failed")).toBeVisible();
await expect(page.getByText("Please try a different card")).toBeVisible();
}); });
Page Object Pattern
// e2e/pages/LoginPage.ts export class LoginPage { constructor(private page: Page) {}
async goto() { await this.page.goto("/login"); }
async login(email: string, password: string) { await this.page.getByLabel("Email").fill(email); await this.page.getByLabel("Password").fill(password); await this.page.getByRole("button", { name: "Sign In" }).click(); }
async expectLoginSuccess() { await expect(this.page).toHaveURL("/dashboard"); }
async expectLoginError(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } }
// e2e/pages/ProductPage.ts export class ProductPage { constructor(private page: Page) {}
async goto() { await this.page.goto("/products"); }
async addToCart(productName: string) {
const product = this.page.locator([data-product="${productName}"]);
await product.getByRole("button", { name: "Add to Cart" }).click();
}
async expectProductVisible(productName: string) { await expect( this.page.getByRole("heading", { name: productName }) ).toBeVisible(); } }
// Usage in tests test("should login and add product", async ({ page }) => { const loginPage = new LoginPage(page); const productPage = new ProductPage(page);
await loginPage.goto(); await loginPage.login("test@example.com", "password123"); await loginPage.expectLoginSuccess();
await productPage.goto(); await productPage.addToCart("MacBook Pro"); });
Selector Strategy
// Preferred selector priority: // 1. Role-based (most resilient) await page.getByRole("button", { name: "Submit" }); await page.getByRole("link", { name: "Products" }); await page.getByRole("textbox", { name: "Email" });
// 2. Label-based (semantic) await page.getByLabel("Email address"); await page.getByLabel("Password");
// 3. Test ID (for complex cases) await page.getByTestId("user-menu"); await page.getByTestId("product-card-123");
// 4. Text content (for unique text) await page.getByText("Welcome back!"); await page.getByText(/Order #\d+/);
// ❌ Avoid: CSS selectors (brittle) // await page.locator('.btn.btn-primary'); // await page.locator('#submit-button');
Test Data Management
// e2e/fixtures/test-data.ts export const testData = { users: { admin: { email: "admin@example.com", password: "admin123", }, customer: { email: "customer@example.com", password: "customer123", }, }, products: { laptop: { name: "MacBook Pro", price: 2499.99, }, phone: { name: "iPhone 15", price: 999.99, }, }, cards: { valid: "4242424242424242", declined: "4000000000000002", insufficientFunds: "4000000000009995", }, };
// e2e/setup/seed-test-data.ts export async function seedTestData() { const prisma = new PrismaClient();
// Create test users await prisma.user.upsert({ where: { email: testData.users.customer.email }, create: { email: testData.users.customer.email, password: await hash(testData.users.customer.password), }, update: {}, });
// Create test products await prisma.product.upsert({ where: { name: testData.products.laptop.name }, create: testData.products.laptop, update: {}, });
await prisma.$disconnect(); }
Visual Regression Testing
// e2e/visual/homepage.spec.ts test("homepage should match screenshot", async ({ page }) => { await page.goto("/");
// Take full page screenshot await expect(page).toHaveScreenshot("homepage.png", { fullPage: true, maxDiffPixels: 100, // Allow minor differences }); });
test("product card should match screenshot", async ({ page }) => { await page.goto("/products");
const productCard = page.locator('[data-testid="product-card"]').first();
// Take element screenshot await expect(productCard).toHaveScreenshot("product-card.png"); });
Mobile Testing
// e2e/mobile/checkout-mobile.spec.ts test.use({ viewport: { width: 375, height: 667 } }); // iPhone SE
test("should complete mobile checkout", async ({ page }) => { await page.goto("/");
// Open mobile menu await page.getByRole("button", { name: "Menu" }).click(); await page.getByRole("link", { name: "Products" }).click();
// Add to cart await page.getByRole("button", { name: "Add to Cart" }).first().click();
// Continue with checkout // ... });
Network Mocking
// e2e/mocked/payment-api.spec.ts test("should handle payment API timeout", async ({ page }) => { // Mock slow payment API await page.route("**/api/payment", async (route) => { await new Promise((resolve) => setTimeout(resolve, 5000)); await route.fulfill({ status: 200, body: JSON.stringify({ success: true }), }); });
// Proceed with checkout await page.goto("/checkout"); // ... fill form ... await page.getByRole("button", { name: "Place Order" }).click();
// Should show loading state await expect(page.getByText("Processing payment...")).toBeVisible(); });
Best Practices
-
Test user flows: Not individual components
-
Use role-based selectors: More resilient
-
Page objects: Reusable and maintainable
-
Wait for elements: Don't use fixed sleeps
-
Test critical paths: Login, checkout, signup
-
Manage test data: Isolated per test
-
Visual regression: Key pages only
Output Checklist
-
Playwright/Cypress configured
-
Critical flows identified
-
Page objects created
-
Selector strategy defined (role-based)
-
Test data management
-
Setup/teardown hooks
-
Authentication flow tested
-
Error states tested
-
Mobile viewport tests
-
CI integration configured