diff --git a/client/e2e/help/trip-bookings.guide.ts b/client/e2e/help/trip-bookings.guide.ts index f82053b4b..cd857928b 100644 --- a/client/e2e/help/trip-bookings.guide.ts +++ b/client/e2e/help/trip-bookings.guide.ts @@ -58,13 +58,24 @@ const travellers = (page: Page) => page.locator('[aria-label="Travelers"]') /** The card's delete question is its own portal, with no backdrop class to find it by. */ const deleteAsk = (page: Page) => portalDialog(page, page.getByText('Delete booking?', { exact: true })) -/** The Booking Type field at the head of the form is a CustomSelect; this opens it. */ +/** The Booking Type is a pill in the head of the form; its name reads the type after the label. */ +const typePill = (page: Page) => modal(page).getByRole('button', { name: /^Booking Type:/ }) +/** The pill's list is its own portal, named after the pill's label. */ +const typeMenu = (page: Page) => page.getByRole('group', { name: 'Booking Type' }) +/** The status pill beside it says what a click turns it into. */ +const statusPill = (page: Page) => modal(page).getByRole('button', { name: /^Set to / }) + async function openTypeMenu(page: Page): Promise { - await block(page, /^Booking Type$/).getByRole('button').first().click() - await expect(selectMenu(page)).toBeVisible() + await typePill(page).click() + await expect(typeMenu(page)).toBeVisible() await beat(page, 300) } +async function pickType(page: Page, type: RegExp): Promise { + await typeMenu(page).getByRole('button', { name: type }).first().click() + await expect(typeMenu(page)).toHaveCount(0) +} + /** The Travelers field opens its members as a list under it, inside the form. */ const travelerList = (page: Page) => block(page, /^Travelers$/).getByRole('listbox') @@ -162,17 +173,16 @@ const SCRIPTS: Record = { prepare: async p => { await openTypeMenu(p) }, - target: selectMenu, + target: typeMenu, act: async p => { - await selectMenu(p).getByRole('button', { name: /^Event$/ }).first().click() - await expect(selectMenu(p)).toHaveCount(0) - await expect(block(p, /^Booking Type$/).getByRole('button', { name: /Event/ })).toBeVisible() + await pickType(p, /^Event$/) + await expect(typePill(p)).toHaveAccessibleName(/^Booking Type:\s*Event$/) await settle(p) }, }, { prepare: async p => { await typeInto(p, titleBox(p), 'Kabuki at the Minamiza') }, - target: p => block(p, /^Title \*$/), + target: titleBox, act: async p => { await expect(saveButton(p, 'Add')).toBeEnabled() await settle(p) @@ -195,7 +205,8 @@ const SCRIPTS: Record = { { prepare: async p => { await typeInto(p, codeBox(p), 'MZ-5521') - await chooseIn(p, /^Status$/, /^Confirmed$/) + await statusPill(p).click() + await expect(statusPill(p)).toHaveAccessibleName('Set to Pending') }, target: p => row(p, /^Booking Code$/), act: settle, @@ -222,10 +233,9 @@ const SCRIPTS: Record = { await expect(modal(p).getByRole('heading', { name: 'New Reservation' })).toBeVisible() await openTypeMenu(p) }, - target: selectMenu, + target: typeMenu, act: async p => { - await selectMenu(p).getByRole('button', { name: /^Accommodation$/ }).first().click() - await expect(selectMenu(p)).toHaveCount(0) + await pickType(p, /^Accommodation$/) await expect(label(p, /^Check-in until$/)).toBeVisible() await settle(p) }, @@ -583,16 +593,12 @@ const SCRIPTS: Record = { act: settle, }, { - prepare: async p => { - await block(p, /^Status$/).getByRole('button').first().click() - await expect(selectMenu(p)).toBeVisible() - await beat(p, 300) - }, - target: p => block(p, /^Status$/), + target: statusPill, act: async p => { - await selectMenu(p).getByRole('button', { name: /^Confirmed$/ }).click() - await expect(selectMenu(p)).toHaveCount(0) - await expect(block(p, /^Status$/).getByRole('button', { name: 'Confirmed' })).toBeVisible() + await expect(statusPill(p)).toHaveAccessibleName('Set to Confirmed') + await statusPill(p).click() + await expect(statusPill(p)).toHaveAccessibleName('Set to Pending') + await expect(statusPill(p)).toHaveText('Confirmed') await settle(p) }, }, diff --git a/client/e2e/help/trip-day-detail.guide.ts b/client/e2e/help/trip-day-detail.guide.ts index d2ca63b17..a918ad56c 100644 --- a/client/e2e/help/trip-day-detail.guide.ts +++ b/client/e2e/help/trip-day-detail.guide.ts @@ -42,8 +42,10 @@ const forecast = (page: Page) => panel(page).getByText(/^Forecast for /) const chips = (page: Page) => panel(page).locator('svg.lucide-sunrise').locator('xpath=ancestor::div[2]') /** The day's reservations block. */ const bookings = (page: Page) => panel(page).getByText('Reservations', { exact: true }).locator('xpath=..') -/** One booking row: the title sits in a wrapper, the row is its grandparent. */ -const bookingRow = (page: Page, title: string) => panel(page).getByText(title).locator('xpath=ancestor::div[2]') +/** One booking row: on the desktop a button that opens the booking, named with its title. */ +const bookingRow = (page: Page, title: string) => panel(page).getByRole('button', { name: title }) +/** The booking a row opens, named by its title. */ +const bookingDetail = (page: Page, title: string) => page.getByRole('dialog', { name: title }) /** * Select a day by its title rather than by the middle of its header: a day with @@ -305,8 +307,15 @@ const SCRIPTS: Record = { only(bookings), only(p => bookingRow(p, EVENING_WALK)), only(p => bookingRow(p, 'Nozomi 21')), - // Only pointed at: clicking it would leave the plan for the Bookings tab. - only(p => p.getByRole('button', { name: 'Bookings', exact: true })), + { + // A row opens its booking over the plan; Edit at its foot is where it changes. + prepare: async p => { + await bookingRow(p, 'Nozomi 21').click() + await expect(bookingDetail(p, 'Nozomi 21')).toBeVisible() + await settle(p) + }, + target: p => bookingDetail(p, 'Nozomi 21').getByRole('button', { name: 'Edit', exact: true }), + }, ], }, } diff --git a/client/e2e/help/trip-map.guide.ts b/client/e2e/help/trip-map.guide.ts index 354ed0a3e..80f0bb326 100644 --- a/client/e2e/help/trip-map.guide.ts +++ b/client/e2e/help/trip-map.guide.ts @@ -1,7 +1,7 @@ import { test, expect, type Locator, type Page } from '@playwright/test' import { captureGuide, captureHero, settle, typeInto, VIEWPORT, type GuideScript } from './guide' import { seededTrip, ensureMapFixtures } from './fixtures' -import { openTrip, openTripOnDay, selectDay, modal } from './trip-shared' +import { openTrip, openTripOnDay, selectDay, modal, dialog } from './trip-shared' import { ensureDawarichConnection, disconnectDawarich } from './external' import { tripMapContext, tripMapGuides } from '../../src/help/contexts/tripMap' import type { HelpGuide } from '../../src/help/types' @@ -367,7 +367,8 @@ const SCRIPTS: Record = { await settle(p) }, }, - { target: p => modal(p).getByPlaceholder('e.g. Eiffel Tower').locator('xpath=ancestor::form[1]') }, + // The whole dialog: the name is typed in its head band, outside the form. + { target: p => dialog(p) }, { target: p => modal(p).getByRole('button', { name: 'Add', exact: true }), act: async p => { @@ -413,7 +414,7 @@ const SCRIPTS: Record = { await typeInto(p, modal(p).getByPlaceholder('e.g. Eiffel Tower'), NEW_PLACE) await settle(p) }, - target: p => modal(p).getByPlaceholder('e.g. Eiffel Tower').locator('xpath=ancestor::form[1]'), + target: p => dialog(p), }, { target: p => modal(p).getByRole('button', { name: 'Add', exact: true }), diff --git a/client/e2e/help/trip-place.guide.ts b/client/e2e/help/trip-place.guide.ts index 8964150e0..781ae32bb 100644 --- a/client/e2e/help/trip-place.guide.ts +++ b/client/e2e/help/trip-place.guide.ts @@ -53,8 +53,10 @@ const member = (page: Page, name: string) => participants(page).getByRole('butto const addMember = (page: Page) => participants(page).getByRole('button', { name: '+', exact: true }) /** "Files" while the place has none, " files" once it has. */ const filesToggle = (page: Page) => card(page).getByRole('button', { name: /^(Files|\d+ files)$/ }) -/** The booking strip is a button only for a viewer who may open the booking. */ -const bookingStrip = (page: Page) => card(page).getByRole('button', { name: 'Edit Reservation' }) +/** The booking strip opens the booking; it is a button named after it. */ +const bookingStrip = (page: Page) => card(page).getByRole('button', { name: `Open booking: ${BOOKING}` }) +/** The booking the strip opens, named by its title. Its form is one Edit further. */ +const bookingDetail = (page: Page) => page.getByRole('dialog', { name: BOOKING }) /** The strip's lower row: Date, Time and Booking Code beside each other. */ const bookingFields = (page: Page) => bookingStrip(page).locator('> div').nth(1) const dayHeader = (page: Page, n: number) => page.getByRole('button', { name: new RegExp(`^${n} .*Day ${n} `) }) @@ -407,11 +409,16 @@ const SCRIPTS: Record = { target: bookingStrip, act: async p => { await bookingStrip(p).click() - await expect(modal(p).getByRole('heading', { name: 'Edit Reservation' })).toBeVisible() + await expect(bookingDetail(p)).toBeVisible() await settle(p) }, }, { + prepare: async p => { + await bookingDetail(p).getByRole('button', { name: 'Edit', exact: true }).click() + await expect(modal(p).getByRole('heading', { name: 'Edit Reservation' })).toBeVisible() + await settle(p) + }, target: p => modal(p).getByText('Link to day assignment', { exact: true }).locator('xpath=..'), act: closeModal, }, diff --git a/client/e2e/help/trip-places.guide.ts b/client/e2e/help/trip-places.guide.ts index 0ffad6e3f..561653ede 100644 --- a/client/e2e/help/trip-places.guide.ts +++ b/client/e2e/help/trip-places.guide.ts @@ -102,7 +102,9 @@ const SCRIPTS: Record = { // The details column is an