import { expect, Locator, Page } from '@playwright/test'; /** The bookings list. */ export class BookingsPage { readonly rows: Locator; readonly emptyState: Locator; readonly filterAll: Locator; readonly filterConfirmed: Locator; readonly filterCancelled: Locator; constructor(private readonly page: Page) { this.rows = page.locator('table.data tbody tr'); this.emptyState = page.locator('.empty'); this.filterAll = page.locator('.filter', { hasText: 'All' }); this.filterConfirmed = page.locator('.filter', { hasText: 'Confirmed' }); this.filterCancelled = page.locator('.filter', { hasText: 'Cancelled' }); } async goto(): Promise { await this.page.goto('/bookings'); await this.settled(); } /** Waits for either a populated table or the empty state. */ async settled(): Promise { await expect(this.page.locator('.filters')).toBeVisible(); await expect(this.page.locator('.skeleton')).toHaveCount(0); } rowFor(reference: string): Locator { return this.rows.filter({ hasText: reference }); } async open(reference: string): Promise { await this.rowFor(reference).getByRole('link', { name: 'View' }).click(); await this.page.waitForURL(/\/bookings\/[0-9a-f-]+/); } async filterBy(status: 'all' | 'confirmed' | 'cancelled'): Promise { const button = { all: this.filterAll, confirmed: this.filterConfirmed, cancelled: this.filterCancelled, }[status]; await button.click(); await this.settled(); } } /** A single booking, with its cancel flow. */ export class BookingDetailPage { readonly confirmationBanner: Locator; readonly reference: Locator; readonly statusBadge: Locator; readonly travellerRows: Locator; readonly totalPaid: Locator; readonly cancelButton: Locator; readonly confirmCancelButton: Locator; readonly keepButton: Locator; readonly cancelledNotice: Locator; constructor(page: Page) { this.confirmationBanner = page.locator('.confirmed-banner'); this.reference = page.locator('.sum-list div', { hasText: 'Reference' }).locator('dd'); this.statusBadge = page.locator('.head-actions .badge'); this.travellerRows = page.locator('table.data tbody tr'); this.totalPaid = page.locator('.total-row strong'); this.cancelButton = page.getByRole('button', { name: 'Cancel booking' }); this.confirmCancelButton = page.getByRole('button', { name: /Yes, cancel booking|Cancelling/ }); this.keepButton = page.getByRole('button', { name: 'Keep it' }); this.cancelledNotice = page.locator('.alert-info'); } async bookingReference(): Promise { return (await this.reference.innerText()).trim(); } /** * Clicks through the two-step cancel confirmation. * * The caller must name the booking it means to void. Cancelling is * destructive and these tests share a database with seeded demo data, so the * page object refuses to act on whatever booking happens to be on screen * after a mis-step earlier in the test. */ async cancel(expectedReference: string): Promise { await expect( this.reference, `refusing to cancel: expected booking ${expectedReference} on screen`, ).toHaveText(expectedReference); await this.cancelButton.click(); await this.confirmCancelButton.click(); await expect(this.cancelledNotice).toBeVisible(); } }