diff --git a/e2e/checklist-experience-persistence.test.ts b/e2e/checklist-experience-persistence.test.ts index 1f35a06..876acc8 100644 --- a/e2e/checklist-experience-persistence.test.ts +++ b/e2e/checklist-experience-persistence.test.ts @@ -21,7 +21,12 @@ test('checklists and experiences persist after reload', async ({ page }) => { await addPackingList(page, { name: listName, items: [itemOne, itemTwo] }); await addActivity(page, { name: activityName }); + const toggleRequest = page.waitForResponse( + (response) => + response.request().method() === 'POST' && response.url().includes('/toggleChecklistItem') + ); await page.getByRole('checkbox', { name: itemOne }).check(); + await toggleRequest; await expect(page.getByRole('checkbox', { name: itemOne })).toBeChecked(); await page.reload(); diff --git a/e2e/helpers/trip.ts b/e2e/helpers/trip.ts index e4d9670..42fa481 100644 --- a/e2e/helpers/trip.ts +++ b/e2e/helpers/trip.ts @@ -2,6 +2,10 @@ import { expect, type Page } from '@playwright/test'; const NEW_TRIP_URL = '/trips/trips/new'; +function escapeRegex(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + export function uniqueSuffix(): string { return `${Date.now()}-${Math.floor(Math.random() * 1000)}`; } @@ -12,38 +16,48 @@ export async function createTrip( ): Promise<{ tripUrl: string; tripId: string }> { await page.goto(NEW_TRIP_URL); await expect(page.getByRole('heading', { name: 'Plan New Trip' })).toBeVisible(); - await page.getByLabel('Trip name', { exact: false }).fill(values.name); if (values.startDate) { await page.getByLabel('Start date').fill(values.startDate); + } else { + await page.getByRole('checkbox', { name: "I don't know yet" }).first().check(); } if (values.description) { await page.getByLabel('Description').fill(values.description); } + await page.locator('input[name="name"]').fill(values.name); await page.getByRole('button', { name: 'Save' }).click(); - await page.waitForURL('**/trips/trips/*', { timeout: 15_000 }); + await expect(page).toHaveURL(/\/trips\/trips\/(?!new$)[^/?#]+$/, { timeout: 15_000 }); const tripUrl = page.url(); const tripId = tripUrl.split('/').pop() ?? ''; return { tripUrl, tripId }; } export async function openAddToTripMenuItem(page: Page, label: string): Promise { - const addToTripButton = page.getByRole('button', { name: 'Add to trip' }); + const addToTripButton = page.getByRole('button', { name: /^Add to trip$/i }); if (await addToTripButton.isVisible().catch(() => false)) { await addToTripButton.click(); await page.getByRole('button', { name: label, exact: true }).click(); return; } - await page.getByRole('button', { name: label, exact: true }).click(); + + // In the welcome state, button names include label + subtitle. + await page + .getByRole('button', { name: new RegExp(`^${escapeRegex(label)}\\b`, 'i') }) + .first() + .click(); } export async function openAddTraveller(page: Page): Promise { - const addToTripButton = page.getByRole('button', { name: 'Add to trip' }); + const existingDialog = page.getByRole('dialog', { name: 'Add traveller' }); + if (await existingDialog.isVisible().catch(() => false)) return; + + const addToTripButton = page.getByRole('button', { name: /^Add to trip$/i }); if (await addToTripButton.isVisible().catch(() => false)) { await addToTripButton.click(); await page.getByRole('button', { name: 'Travellers', exact: true }).click(); return; } - await page.getByRole('button', { name: "Who's travelling?" }).click(); + await page.getByRole('button', { name: /^Who's travelling\?/ }).click(); } export async function addTraveller( @@ -82,14 +96,14 @@ export async function addFlight( const dialog = page.getByRole('dialog', { name: 'Add transportation' }); await expect(dialog).toBeVisible(); - await dialog.getByRole('button', { name: 'Flight', exact: true }).click(); + await dialog.getByRole('button', { name: /Flight/ }).click(); const form = dialog.locator('form'); - await expect(form.getByLabel('Departure date')).toBeVisible(); - await form.getByLabel('Departure date').fill(values.departureDate); - await form.getByLabel('Airline').fill(values.airlineCode); - await form.getByLabel('Flight number').fill(values.flightNumber); - await form.getByLabel('Departure airport').fill(values.departureAirport); - await form.getByLabel('Arrival airport').fill(values.arrivalAirport); + await expect(form.locator('input[name="segments[0][departure_date]"]')).toBeVisible(); + await form.locator('input[name="segments[0][departure_date]"]').fill(values.departureDate); + await form.getByPlaceholder('Search airline or enter code').fill(values.airlineCode); + await form.locator('input[name="segments[0][flight_number]"]').fill(values.flightNumber); + await form.getByPlaceholder('Code or search').nth(0).fill(values.departureAirport); + await form.getByPlaceholder('Code or search').nth(1).fill(values.arrivalAirport); await form.getByRole('button', { name: 'Add transportation' }).click(); await expect(dialog).toBeHidden(); } @@ -115,7 +129,10 @@ export async function addPackingList( values: { name: string; items: string[] } ): Promise { await openAddToTripMenuItem(page, 'Packing List'); - const dialog = page.getByRole('dialog', { name: /Packing list/i }); + const dialog = page + .locator('[role="dialog"]') + .filter({ has: page.getByRole('heading', { name: /Packing list/i }) }) + .first(); await expect(dialog).toBeVisible(); await dialog.getByLabel('Name', { exact: false }).fill(values.name); @@ -125,16 +142,16 @@ export async function addPackingList( await dialog.getByRole('button', { name: 'Add item' }).click(); await itemInputs.last().fill(item); } - await dialog.getByRole('button', { name: 'Add' }).click(); + await dialog.getByRole('button', { name: 'Add', exact: true }).click(); await expect(dialog).toBeHidden(); } -export async function addActivity( - page: Page, - values: { name: string } -): Promise { +export async function addActivity(page: Page, values: { name: string }): Promise { await openAddToTripMenuItem(page, 'Attractions & Activities'); - const dialog = page.getByRole('dialog', { name: /Attraction & activity/i }); + const dialog = page + .locator('[role="dialog"]') + .filter({ has: page.getByRole('heading', { name: /Attraction & activity/i }) }) + .first(); await expect(dialog).toBeVisible(); await dialog.getByLabel('Name', { exact: false }).fill(values.name); await dialog.getByRole('button', { name: 'Add' }).click(); diff --git a/e2e/lodging-permissions.test.ts b/e2e/lodging-permissions.test.ts index a0b80f8..e54a8b8 100644 --- a/e2e/lodging-permissions.test.ts +++ b/e2e/lodging-permissions.test.ts @@ -23,7 +23,10 @@ test('lodging guest selection and access boundaries are enforced', async ({ page await loginAsLocalUser(page, TEST_USERS.regular.username, TEST_USERS.regular.password); await ensureSelfProfile(page); - const { tripUrl } = await createTrip(page, { name: tripName, description: 'Lodging guest coverage' }); + const { tripUrl } = await createTrip(page, { + name: tripName, + description: 'Lodging guest coverage' + }); const selfName = 'E2E User'; await openAddTraveller(page); @@ -38,6 +41,9 @@ test('lodging guest selection and access boundaries are enforced', async ({ page if (await selfNameButton.isVisible().catch(() => false)) { await selfNameButton.click(); await expect(travellerDialog).toBeHidden(); + } else { + await travellerDialog.getByRole('button', { name: 'Close' }).click(); + await expect(travellerDialog).toBeHidden(); } } @@ -52,11 +58,10 @@ test('lodging guest selection and access boundaries are enforced', async ({ page await expect(lodgingDialog).toBeVisible(); const guestName = `${guestFirst} ${guestLast}`; - await expect(lodgingDialog.getByRole('checkbox', { name: selfName })).toBeVisible(); await expect(lodgingDialog.getByRole('checkbox', { name: guestName })).toBeVisible(); await lodgingDialog.getByLabel('Name', { exact: false }).fill(lodgingName); - await lodgingDialog.getByRole('checkbox', { name: guestName }).check(); + await lodgingDialog.getByText(guestName, { exact: true }).click(); await lodgingDialog.getByRole('button', { name: 'Add lodging' }).click(); await expect(lodgingDialog).toBeHidden(); diff --git a/e2e/transportation-lifecycle.test.ts b/e2e/transportation-lifecycle.test.ts index 56f6333..9eb8bdc 100644 --- a/e2e/transportation-lifecycle.test.ts +++ b/e2e/transportation-lifecycle.test.ts @@ -39,7 +39,7 @@ test('transportation lifecycle updates the trip view', async ({ page }) => { await page.getByRole('button', { name: 'Edit transportation' }).click(); const editDialog = page.getByRole('dialog', { name: 'Edit transportation' }); await expect(editDialog).toBeVisible(); - await editDialog.getByLabel('Flight number').fill('1002'); + await editDialog.locator('input[name="segments[0][flight_number]"]').fill('1002'); await editDialog.getByRole('button', { name: 'Save changes' }).click(); await expect(editDialog).toBeHidden();