TypeScript ยท @playwright/test
Ticket booking with Playwright
Automate a movie booking โ dates, filters, a seat map, snacks, promos, OTP payment and a seat-hold timer.
App under test
ShowTime
- URL
- /tickets
- Login
- not required
- Promos
- MOVIE50 ยท FIRSTSHOW
- Card OTP
- 123456
- Seat hold
- 5 minutes
Scenarios to automate
Work top to bottom โ each one adds a new technique.
- 1
Date strip
BeginnerSeven date-option chips from today; dates are computed from the browser clock.
- 2
Filters
BeginnerCombine genre-* toggles, language-filter and movie-search; assert movie-card count.
- 3
Showtime states
Intermediatedata-status is available / filling / soldout / started โ disabled ones can't be clicked.
- 4
Seat map
IntermediatePick seats by data-status, verify selected-seat chips and seat-total per tier.
- 5
Seat limit
IntermediateThe 9th seat shows seat-limit-error.
- 6
Snacks & promo codes
IntermediateMOVIE50 halves tickets (max $10); FIRSTSHOW only works on a first booking.
- 7
OTP payment
AdvancedCard payments open otp-modal; 123456 passes, anything else shows otp-error.
- 8
Async UPI approval
AdvancedUPI shows payment-processing for ~3s before the confirmation page.
- 9
Hold timer
AdvancedSeats are held for 5:00. Mock the clock to hit timer-expired without waiting.
- 10
Cancel a booking
AdvancedCancel from My bookings, confirm the dialog, and find it under the Cancelled tab.
Sample Playwright suite
A working starting point written against this app's real data-testids.
import { test, expect } from '@playwright/test';
test('book two seats and pay by card with OTP', async ({ page }) => {
await page.goto('/tickets');
await page.getByTestId('date-option').nth(1).click(); // tomorrow: no "started" shows
await page.getByTestId('city-select').selectOption('London');
const movie = page.getByTestId('movie-card').filter({ hasText: 'Nebula Drift' });
await movie.locator('[data-testid="showtime"]:enabled').first().click();
await expect(page).toHaveURL(/\/tickets\/show\//);
// Re-evaluated on every use, so the second click gets the next free seat.
const freeSeat = page.locator('[data-testid^="seat-"][data-status="available"]').first();
await freeSeat.click();
await freeSeat.click();
await expect(page.getByTestId('selected-seat')).toHaveCount(2);
await page.getByTestId('proceed-button').click();
await page.getByTestId('contact-name').fill('Grace Hopper');
await page.getByTestId('contact-email').fill('grace@example.com');
await page.getByTestId('contact-phone').fill('5559876543');
await page.getByTestId('snack-popcorn-inc').click();
await page.getByTestId('promo-input').fill('MOVIE50');
await page.getByTestId('apply-promo').click();
await expect(page.getByTestId('promo-message')).toContainText('applied');
await page.getByTestId('card-number').fill('4242424242424242');
await page.getByTestId('card-expiry').fill('12/30');
await page.getByTestId('card-cvv').fill('123');
await page.getByTestId('accept-terms').check();
await page.getByTestId('pay-button').click();
await page.getByTestId('otp-input').fill('123456');
await page.getByTestId('otp-submit').click();
await expect(page.getByTestId('booking-success')).toBeVisible();
await expect(page.getByTestId('booking-ref')).toHaveText(/^ST[A-Z0-9]{6}$/);
});
test('hold timer expires (mocked clock)', async ({ page }) => {
await page.clock.install();
await page.goto('/tickets');
await page.getByTestId('date-option').nth(1).click();
await page.locator('[data-testid="showtime"]:enabled').first().click();
await page.locator('[data-testid^="seat-"][data-status="available"]').first().click();
await page.getByTestId('proceed-button').click();
await page.clock.fastForward('05:01');
await expect(page.getByTestId('timer-expired')).toBeVisible();
});