With page objects and fixtures in place, writing tests becomes almost trivial. The hard architectural work is done. Now tests are short, readable, and focused on WHAT they verify -- not HOW they interact with the page.
import { test, expect } from '../../fixtures/test-fixtures';
import { testUsers } from '../../test-data/users';
test.describe('Authentication', () => {
test('login with valid credentials @smoke @critical', async ({ loginPage }) => {
await loginPage.goto();
await loginPage.login(testUsers.standard.email, testUsers.standard.password);
await loginPage.expectLoggedIn();
});
test('show error for invalid credentials @smoke', async ({ loginPage }) => {
await loginPage.goto();
await loginPage.login(testUsers.invalid.email, testUsers.invalid.password);
await loginPage.expectError('Invalid credentials');
});
test('show error for empty email', async ({ loginPage }) => {
await loginPage.goto();
await loginPage.login('', 'somepassword');
await loginPage.expectError('Email is required');
});
test('show error for empty password', async ({ loginPage }) => {
await loginPage.goto();
await loginPage.login('user@example.com', '');
await loginPage.expectError('Password is required');
});
});This is the most important test in the suite. It covers the entire critical path from login to order confirmation. If this test passes, the core business flow works.
import { test, expect } from '../../fixtures/test-fixtures';
import { testUsers, shippingAddresses } from '../../test-data/users';
test.describe('Purchase Flow', () => {
test.beforeEach(async ({ loginPage }) => {
await loginPage.goto();
await loginPage.login(testUsers.standard.email, testUsers.standard.password);
});
test('complete purchase: search → cart → checkout @critical', async ({
productListPage,
productDetailPage,
cartPage,
checkoutPage,
}) => {
// Step 1: Find a product
await productListPage.goto();
await productListPage.search('laptop');
const resultCount = await productListPage.getProductCount();
expect(resultCount).toBeGreaterThan(0);
// Step 2: View product details and add to cart
await productListPage.openProduct(0);
const productName = await productDetailPage.getProductName();
await productDetailPage.setQuantity(2);
await productDetailPage.addToCart();
// Step 3: Verify cart
await productDetailPage.goToCart();
const itemCount = await cartPage.getItemCount();
expect(itemCount).toBe(1);
// Step 4: Checkout with shipping and payment
await cartPage.checkout();
await checkoutPage.fillShipping(shippingAddresses.valid);
await checkoutPage.fillPayment({
cardNumber: '4242424242424242',
expiry: '12/28',
cvv: '123',
});
await checkoutPage.placeOrder();
// Step 5: Verify order confirmation
await checkoutPage.expectConfirmation();
});
test('checkout with missing shipping info shows validation errors', async ({
productListPage,
productDetailPage,
cartPage,
checkoutPage,
}) => {
// Add any product to cart
await productListPage.goto();
await productListPage.openProduct(0);
await productDetailPage.addToCart();
await productDetailPage.goToCart();
await cartPage.checkout();
// Try to place order without filling shipping
await checkoutPage.placeOrder();
await checkoutPage.expectValidationErrors(5);
});
});import { test, expect } from '../../fixtures/test-fixtures';
test.describe('Product Search', () => {
test.beforeEach(async ({ authenticatedPage, productListPage }) => {
await productListPage.goto();
});
test('search returns matching products @smoke', async ({ productListPage }) => {
await productListPage.search('laptop');
const count = await productListPage.getProductCount();
expect(count).toBeGreaterThan(0);
});
test('search with no results shows empty state', async ({ productListPage }) => {
await productListPage.search('xyznonexistent123');
await productListPage.expectNoResults();
});
test('filter by category shows only matching products', async ({ productListPage }) => {
await productListPage.filterByCategory('Electronics');
const count = await productListPage.getProductCount();
expect(count).toBeGreaterThan(0);
});
test('empty search returns all products', async ({ productListPage }) => {
const allCount = await productListPage.getProductCount();
await productListPage.search('laptop');
const filteredCount = await productListPage.getProductCount();
expect(filteredCount).toBeLessThanOrEqual(allCount);
});
});import { test, expect } from '../../fixtures/test-fixtures';
test.describe('Cart Management', () => {
test.beforeEach(async ({ authenticatedPage }) => {
// authenticatedPage handles login
});
test('add product to cart @smoke', async ({
productListPage,
productDetailPage,
cartPage,
}) => {
await productListPage.goto();
await productListPage.openProduct(0);
await productDetailPage.addToCart();
await productDetailPage.goToCart();
await expect(cartPage.cartItems).toHaveCount(1);
});
test('remove product from cart', async ({
productListPage,
productDetailPage,
cartPage,
}) => {
// Add item first
await productListPage.goto();
await productListPage.openProduct(0);
await productDetailPage.addToCart();
await productDetailPage.goToCart();
await expect(cartPage.cartItems).toHaveCount(1);
// Remove it
await cartPage.removeItem(0);
await cartPage.expectEmpty();
});
test('update quantity in cart', async ({
productListPage,
productDetailPage,
cartPage,
}) => {
await productListPage.goto();
await productListPage.openProduct(0);
await productDetailPage.addToCart();
await productDetailPage.goToCart();
await cartPage.updateQuantity(0, 3);
// Verify the quantity updated -- implementation depends on UI
});
test('empty cart shows appropriate message', async ({ cartPage }) => {
await cartPage.goto();
await cartPage.expectEmpty();
});
test('cart persists across page navigation', async ({
productListPage,
productDetailPage,
cartPage,
}) => {
await productListPage.goto();
await productListPage.openProduct(0);
await productDetailPage.addToCart();
// Navigate away
await productListPage.goto();
// Come back to cart
await productListPage.openCart();
await expect(cartPage.cartItems).toHaveCount(1);
});
});import { test, expect } from '../../fixtures/test-fixtures';
test.describe('Product Detail Edge Cases', () => {
test('show out-of-stock message when API returns zero inventory', async ({
page,
authenticatedPage,
}) => {
// Mock the product API to return out-of-stock
await page.route('**/api/products/*', async (route) => {
const response = await route.fetch();
const body = await response.json();
body.inventory = 0;
body.inStock = false;
await route.fulfill({ json: body });
});
await page.goto('/shopping/products/1');
await expect(page.getByText('Out of Stock')).toBeVisible();
await expect(page.getByRole('button', { name: 'Add to Cart' })).toBeDisabled();
});
test('show error when product API fails', async ({
page,
authenticatedPage,
}) => {
await page.route('**/api/products/*', (route) =>
route.fulfill({ status: 500, body: 'Internal Server Error' })
);
await page.goto('/shopping/products/1');
await expect(page.getByText('Something went wrong')).toBeVisible();
});
});import { test, expect } from '../../fixtures/test-fixtures';
test.describe('Visual Regression', () => {
test('login page matches baseline', async ({ loginPage }) => {
await loginPage.goto();
await expect(loginPage.page).toHaveScreenshot('login-page.png', {
maxDiffPixelRatio: 0.01,
});
});
test('product list page matches baseline', async ({
authenticatedPage,
productListPage,
}) => {
await productListPage.goto();
await expect(productListPage.page).toHaveScreenshot('product-list.png', {
maxDiffPixelRatio: 0.01,
});
});
test('responsive: mobile product list', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto('/shopping');
await expect(page).toHaveScreenshot('product-list-mobile.png', {
maxDiffPixelRatio: 0.02,
});
});
});Visual tests generate baseline screenshots on the first run. Run npx playwright test --update-snapshots in CI to create baselines in the CI environment. Commit the CI-generated screenshots, not your local ones -- font rendering differs between macOS and Linux.
Q: How would you structure end-to-end tests for a critical purchase flow?
A: I write one long end-to-end test covering the entire flow: search → product detail → add to cart → checkout → confirmation. This catches integration issues between steps that isolated tests miss. I also write separate tests for edge cases at each step: empty cart, invalid payment, missing shipping info. The critical flow test is tagged @critical and runs on every PR. Edge case tests run in nightly regression. I use page objects and fixtures so the flow test reads like a user story, and API mocking for scenarios that are hard to trigger via UI (out-of-stock, payment failure).
Key Point: With page objects and fixtures in place, tests become short and readable. Write one long P0 flow test for the critical path, then separate tests for edge cases. Use @tags for selective CI runs.
Key Point: Write one critical flow test for the happy path, separate tests for edge cases, and use tags for selective CI execution