TypeScript · @playwright/test
Food delivery with Playwright
Filter restaurants, add dishes with quantity steppers, customise a pizza in a modal, handle a replace-cart prompt, add tip and coupon, then watch the order move live and rate it.
App under test
🍔 FoodDash
- URL
- /apps/food
- Login
- not required
- Coupon
- FEAST50 (min ₹299)
- Free delivery
- above ₹499
Scenarios to automate
Work top to bottom — each one adds a new technique.
- 1
Filters & switch
BeginnerCombine cuisine-* chips, veg-only (role=switch), food-search and restaurant-sort; assert restaurant-count.
- 2
ADD becomes a stepper
Beginneradd-<item> turns into <item>-dec / <item>-qty / <item>-inc; decreasing to 0 brings ADD back.
- 3
Customisation modal
IntermediatePick size-* and topping-* (max 3 — the 4th is disabled); custom-total updates live.
- 4
Replace cart prompt
IntermediateAdding from a second restaurant opens replace-cart-modal; keep-cart vs confirm-replace.
- 5
Coupon rules
IntermediateFEAST50 under ₹299 shows how much more to add; above it caps the discount at ₹100.
- 6
Bill maths
AdvancedVerify bill-items, bill-discount, bill-delivery (free ≥ ₹499), bill-taxes (5%) and bill-total with a tip.
- 7
Live tracking
Advancedorder-status moves Order placed → Preparing → Out for delivery → Delivered every ~3s.
- 8
Rate & reorder
IntermediateAfter delivery, rate-* + submit-rating shows rating-thanks; reorder-<id> refills the cart.
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.
import { test, expect } from '@playwright/test';
// playwright.config.ts → use: { baseURL: 'https://www.practiceqaautomation.com' }
// Locators in this suite: getByRole ×2 · getByLabel ×1 · getByPlaceholder ×1 · XPath ×1 · getByTestId ×21
test('orders a customised pizza and tracks it', async ({ page }) => {
await page.goto('/apps/food');
await page.getByRole('button', { name: 'Pizza' }).click();
await expect(page.getByTestId('restaurant-count')).toContainText('1 restaurant');
await page.getByTestId('open-restaurant-r3').click();
await expect(page.getByTestId('restaurant-title')).toContainText('Slice of Napoli');
// Customise a Margherita: medium + olives
await page.getByTestId('add-r3i1').click();
await page.getByTestId('size-medium').click();
await page.getByTestId('topping-olives').check();
await expect(page.getByTestId('custom-total')).toContainText('₹489');
await page.getByTestId('add-customized').click();
await page.getByTestId('add-r3i4').click();
await page.getByRole('button', { name: 'Add one Garlic Bread' }).click();
await expect(page.getByTestId('r3i4-qty')).toContainText('2');
await expect(page.getByTestId('cart-count')).toContainText('3');
await page.getByTestId('view-cart').click();
await expect(page).toHaveURL(/\/apps\/food\/checkout/);
await page.getByPlaceholder('Coupon code', { exact: true }).fill('FEAST50');
await page.locator('xpath=//button[normalize-space()="Apply"]').click();
await expect(page.getByTestId('promo-message')).toContainText('applied');
await page.getByTestId('tip-20').click();
await expect(page.getByTestId('bill-delivery')).toContainText('FREE');
await expect(page.getByTestId('bill-total')).toContainText('₹741');
await page.getByTestId('address-home').click();
await page.getByLabel('Phone for the rider').fill('9876543210');
await page.getByTestId('pay-upi').click();
await page.getByTestId('place-order').click();
await expect(page.getByTestId('order-placed')).toBeVisible();
// The status advances on its own — wait for text, never sleep
await expect(page.getByTestId('order-status')).toContainText('Preparing');
});