PracticeQAAutomation logoPracticeQAAutomation
Pw

TypeScript · @playwright/test

E-commerce with Playwright

Login with test users, search/filter/sort products, cart & coupons, a validated checkout, a declined card, order history with cancel and return, a store-manager role and a buggy build.

App under test

🛍️ Nova Shop

URL
/shop
User
demo_user
Password
Demo@123
Manager
admin_user
Coupons
SAVE10 · FLAT20
Declined card
4000 0000 0000 0002

Scenarios to automate

Work top to bottom — each one adds a new technique.

  1. 1

    Valid & invalid login

    Beginner

    demo_user / Demo@123 succeeds; wrong password and empty fields show login-error.

  2. 2

    Locked-out user

    Beginner

    locked_user gets “Sorry, this user has been locked out.”

  3. 3

    Slow login

    Intermediate

    slow_user takes ~3s — wait for the URL or product grid, never sleep.

  4. 4

    Route guard

    Beginner

    Visiting /shop/cart signed out redirects to /shop.

  5. 5

    Search, filter & sort

    Intermediate

    Assert product-count text, no-results state, and that product-price values are sorted.

  6. 6

    Cart badge & quantities

    Intermediate

    Add/remove from the grid, change qty in the cart, check line-total and totals maths.

  7. 7

    Coupons

    Intermediate

    SAVE10 = 10% off, FLAT20 = $20 off above $100, anything else is rejected.

  8. 8

    Checkout validation

    Intermediate

    Submit empty to collect error-* messages; fix them one by one.

  9. 9

    Declined card

    Advanced

    4000 0000 0000 0002 shows payment-error; switch to Cash on delivery and succeed.

  10. 10

    Order history

    Advanced

    Order id on the success page must appear in orders-table after navigation.

  11. 11

    Cancel, return & buy again

    Intermediate

    Cancel a Processing order in cancel-modal; request a return on a Delivered one (a reason is required); reorder-* refills the cart.

  12. 12

    Store manager role

    Advanced

    admin_user lands on /shop/admin, ships and delivers orders and decides returns; demo_user sees access-denied there.

  13. 13

    Bug hunt

    Advanced

    buggy_user has 5 seeded defects (sort, search, badge, tax, order total) — write tests that fail for it and pass for demo_user.

  14. 14

    UI vs API

    Advanced

    POST /api/shop/quote prices the same cart; assert subtotal, discount, tax and total match the cart page.

Sample Playwright suite

A working starting point that finds elements every way Playwright can — by role, label, text, id, name, CSS, XPath and data-testid — so you practice them all. Pick your language, switch to data-testid only to compare, or try your own in the Locator Playground.

Language
nova-shop.spec.ts
import { test, expect } from '@playwright/test';

// Each line uses a different kind of locator on purpose — the comments name them.
test.beforeEach(async ({ page }) => {
  await page.goto('/shop');
  await page.getByLabel('Username').fill('demo_user');                  // by its <label>
  await page.getByLabel('Password').fill('Demo@123');
  await page.getByRole('button', { name: 'Sign in' }).click();          // by role + accessible name
  await expect(page).toHaveURL(/\/shop\/products/);
});

test('buys headphones with a coupon', async ({ page }) => {
  await page.getByPlaceholder('Search products…').fill('headphones');   // by placeholder
  await expect(page.getByTestId('product-card')).toHaveCount(1);        // by data-testid
  await page.getByTestId('product-card').filter({ hasText: 'Aurora Wireless Headphones' })
    .getByRole('button', { name: 'Add to cart' }).click();              // chained: a button inside one card
  await expect(page.getByTestId('cart-badge')).toHaveText('1');

  await page.getByRole('link', { name: /^Cart with/ }).click();         // aria-label, matched with a regex
  await page.getByLabel('Coupon code').fill('SAVE10');                  // getByLabel also reads aria-label
  await page.getByRole('button', { name: 'Apply' }).click();
  await expect(page.getByTestId('discount')).toContainText('$13.00');

  await page.getByRole('link', { name: 'Checkout' }).click();
  await page.getByLabel('First name').fill('Ada');
  await page.getByLabel('Last name').fill('Lovelace');
  await page.locator('#email').fill('ada@example.com');                 // CSS: id
  await page.locator('input[name="phone"]').fill('5551234567');         // CSS: attribute
  await page.getByLabel('Street address').fill('1 Analytical Way');
  await page.locator('xpath=//input[@id="city"]').fill('Austin');       // XPath
  await page.getByRole('combobox', { name: 'State' }).selectOption('Texas');
  await page.getByLabel('ZIP code').fill('73301');
  await page.getByLabel('Name on card').fill('Ada Lovelace');
  await page.getByPlaceholder('4242 4242 4242 4242').fill('4242424242424242');
  await page.getByPlaceholder('MM/YY').fill('12/30');
  await page.getByLabel('CVV').fill('123');
  await page.getByRole('checkbox', { name: /I agree to the terms/ }).check();
  await page.getByRole('button', { name: /^Place order/ }).click();     // the button text ends with the total

  // Placing the order takes ~2s — web-first assertions wait for it.
  await expect(page.getByRole('heading', { name: 'Thank you for your order!' })).toBeVisible();
  await expect(page.getByTestId('order-id')).toHaveText(/^NS-\d+$/);
});

test('sorts by price, low to high', async ({ page }) => {
  await page.getByLabel('Sort by').selectOption('price-asc');
  const prices = (await page.getByTestId('product-price').allTextContents())
    .map((p) => Number(p.replace('$', '')));
  expect(prices).toEqual([...prices].sort((a, b) => a - b));
});

Practicing another tool?