From 0dd07befa76e3b97ac5371e4f0f8fd4c0f99b6bf Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 27 Sep 2026 21:00:19 +0200 Subject: [PATCH] feat(planner): bring the plan tab and every trip dialog into the new look The Plan tab was the last trip tab still in the old design. It now reads like Bookings, Costs, Lists, Files and Collab, without losing a function: - Day plan: each day is a card with a head band (day tile with weather, stay and rental pills, a "+" menu for place, stay, transport, transit and note), rows with "..." menus, white badges and a white route bar. - Places column: add, search and filters in three rows, a show dropdown, category and rating filters, and a selection bar at the foot. - Place inspector, day details and the stay editor on the shared dialog frame; the place form in the editor look with the name in the head band. - Clicking a booking or transport anywhere on the plan opens the booking detail popup first; Edit leads to the editor. The popup is one host shared with the Bookings and Transports tabs. - Files and Collab dialogs, notes, polls, links and chat moved to the same frame; shared DialogShell and dialogParts hold the building blocks. - The map hover card shows the place photo and follows the theme. Every new and reworded string is translated into all locales, and a few older English leftovers (help center, cost split, import hints) with them. Phone screens keep their own look. --- client/e2e/help/trip-bookings.guide.ts | 48 +- client/e2e/help/trip-day-detail.guide.ts | 17 +- client/e2e/help/trip-map.guide.ts | 7 +- client/e2e/help/trip-place.guide.ts | 13 +- client/e2e/help/trip-places.guide.ts | 4 +- client/e2e/help/trip-transports.guide.ts | 21 +- .../src/components/Budget/CostsPanel.test.tsx | 184 +-- client/src/components/Budget/CostsPanel.tsx | 1100 ++++++------- client/src/components/Budget/CostsToolbar.tsx | 167 +- .../Budget/ReceiptScanModal.test.tsx | 5 +- .../components/Budget/ReceiptScanModal.tsx | 60 +- client/src/components/Budget/expenseFx.ts | 4 +- .../src/components/Collab/CollabChat.test.tsx | 6 +- client/src/components/Collab/CollabChat.tsx | 6 +- .../Collab/CollabChatLinkPreview.tsx | 10 +- .../Collab/CollabChatMessages.test.tsx | 6 +- .../components/Collab/CollabChatMessages.tsx | 30 +- .../Collab/CollabChatSmall.test.tsx | 13 +- .../components/Collab/CollabLinks.test.tsx | 12 +- client/src/components/Collab/CollabLinks.tsx | 102 +- .../components/Collab/CollabNotes.test.tsx | 266 ++- client/src/components/Collab/CollabNotes.tsx | 262 ++- .../src/components/Collab/CollabNotesCard.tsx | 271 ++-- .../CollabNotesCategorySettingsModal.tsx | 165 +- .../Collab/CollabNotesEditableCatName.tsx | 27 +- .../Collab/CollabNotesFilePreviewPortal.tsx | 106 +- .../Collab/CollabNotesFormModal.test.tsx | 8 +- .../Collab/CollabNotesFormModal.tsx | 303 +--- .../Collab/CollabNotesWebsiteThumbnail.tsx | 7 +- client/src/components/Collab/CollabPanel.tsx | 26 +- .../src/components/Collab/CollabPanelHead.tsx | 29 + .../components/Collab/CollabPolls.test.tsx | 42 +- client/src/components/Collab/CollabPolls.tsx | 305 ++-- .../src/components/Collab/WhatsNextWidget.tsx | 83 +- .../Collections/CollectionPicker.test.tsx | 52 +- .../Collections/CollectionPicker.tsx | 131 +- .../src/components/Files/FileManager.test.tsx | 67 +- .../Files/FileManagerAssignModal.test.tsx | 4 +- .../Files/FileManagerAssignModal.tsx | 60 +- .../components/Files/FileManagerFilesView.tsx | 10 +- .../Files/FileManagerImageLightbox.test.tsx | 4 +- .../Files/FileManagerImageLightbox.tsx | 38 +- .../Files/FileManagerMarkdownPreviewModal.tsx | 75 +- .../Files/FileManagerPdfPreviewModal.tsx | 93 +- .../Files/FileManagerPreviewModals.test.tsx | 21 +- .../src/components/Files/FileManagerRow.tsx | 80 +- .../Files/FileManagerSourceBadge.tsx | 22 +- .../components/Files/FileManagerToolbar.tsx | 97 +- .../components/Files/FileManagerTrashView.tsx | 15 +- .../Files/docsync/DocSyncBinding.test.tsx | 2 +- .../components/Files/docsync/DocSyncBits.tsx | 20 +- .../Files/docsync/DocSyncConnectModal.tsx | 64 +- .../components/Files/docsync/DocSyncPanel.tsx | 35 +- .../Files/docsync/DocSyncScopeModal.tsx | 22 +- client/src/components/Map/MapView.tsx | 11 +- client/src/components/Map/MapViewGL.tsx | 11 +- client/src/components/Map/PlaceHoverCard.tsx | 71 +- client/src/components/Map/mapHover.ts | 2 + client/src/components/Map/placePhoto.ts | 25 + .../Packing/PackingListPanel.test.tsx | 15 +- .../Packing/PackingListPanelBagCard.tsx | 18 +- .../Packing/PackingListPanelHeader.tsx | 4 +- .../Packing/PackingListPanelImportModal.tsx | 119 +- .../Packing/PackingListPanelItemRow.test.tsx | 3 +- .../Packing/PackingListPanelItemRow.tsx | 19 +- .../Packing/packingPopoverStyles.ts | 6 +- .../Planner/BookingCostsSection.test.tsx | 36 +- .../Planner/BookingCostsSection.tsx | 61 +- .../src/components/Planner/DayAddFooter.tsx | 97 +- .../Planner/DayDetailPanel.test.tsx | 473 ++++-- .../src/components/Planner/DayDetailPanel.tsx | 1088 +++++++------ .../Planner/DayPlanSidebar.test.tsx | 232 ++- .../src/components/Planner/DayPlanSidebar.tsx | 925 +++++------ .../Planner/DayPlanSidebarFooter.tsx | 13 +- .../Planner/DayPlanSidebarNoteModal.test.tsx | 178 ++ .../Planner/DayPlanSidebarNoteModal.tsx | 264 ++- .../Planner/DayPlanSidebarRouteConnector.tsx | 70 +- .../DayPlanSidebarTimeConfirmModal.test.tsx | 47 + .../DayPlanSidebarTimeConfirmModal.tsx | 59 +- .../Planner/DayPlanSidebarToolbar.test.tsx | 34 +- .../Planner/DayPlanSidebarToolbar.tsx | 270 ++-- ...ayPlanSidebarTransportDetailModal.test.tsx | 120 +- .../DayPlanSidebarTransportDetailModal.tsx | 448 +++--- .../Planner/DayReorderPopup.test.tsx | 26 +- .../components/Planner/DayReorderPopup.tsx | 67 +- .../Planner/FileImportModal.test.tsx | 32 +- .../components/Planner/FileImportModal.tsx | 269 ++-- .../Planner/IcsSubscribeModal.test.tsx | 77 +- .../components/Planner/IcsSubscribeModal.tsx | 102 +- .../Planner/PlaceDetailsColumn.test.tsx | 154 ++ .../components/Planner/PlaceDetailsColumn.tsx | 247 ++- .../Planner/PlaceFormModal.helpers.test.ts | 87 +- .../Planner/PlaceFormModal.helpers.ts | 87 +- .../Planner/PlaceFormModal.test.tsx | 418 ++++- .../src/components/Planner/PlaceFormModal.tsx | 1202 +++++++------- .../Planner/PlaceInspector.test.tsx | 296 +++- .../src/components/Planner/PlaceInspector.tsx | 1432 +++++++++-------- .../Planner/PlacesBulkCategoryModal.test.tsx | 51 + .../Planner/PlacesBulkCategoryModal.tsx | 103 +- .../components/Planner/PlacesSidebar.test.tsx | 360 ++++- .../src/components/Planner/PlacesSidebar.tsx | 31 +- .../Planner/PlacesSidebarHeader.tsx | 801 +++++---- .../components/Planner/PlacesSidebarList.tsx | 19 +- .../Planner/PlacesSidebarListImportModal.tsx | 164 +- .../components/Planner/PlacesSidebarRow.tsx | 140 +- .../PlacesSidebarSelectionBar.test.tsx | 48 +- .../Planner/PlacesSidebarSelectionBar.tsx | 181 +-- .../Planner/ReservationModal.test.tsx | 16 +- .../components/Planner/ReservationModal.tsx | 418 +++-- .../Planner/ReservationsPanel.test.tsx | 277 ++-- .../components/Planner/ReservationsPanel.tsx | 87 +- .../Planner/TransitJourneyModal.test.tsx | 14 +- .../Planner/TransitJourneyModal.tsx | 122 +- .../Planner/TransportModal.test.tsx | 62 +- .../src/components/Planner/TransportModal.tsx | 926 +++++------ .../src/components/Planner/TravelerPicker.tsx | 2 +- .../Planner/TripExportModal.test.tsx | 76 +- .../components/Planner/TripExportModal.tsx | 137 +- .../Planner/bookings/BookingCard.tsx | 128 +- .../Planner/bookings/BookingDetailDialog.tsx | 461 +++--- .../bookings/BookingDetailHost.test.tsx | 270 ++++ .../Planner/bookings/BookingDetailHost.tsx | 109 ++ .../bookings/BookingDialogShell.test.tsx | 171 ++ .../Planner/bookings/BookingDialogShell.tsx | 42 + .../Planner/bookings/BookingsHeader.tsx | 27 +- .../Planner/bookings/BookingsList.tsx | 2 +- .../Planner/bookings/BookingsTimeline.tsx | 22 +- .../Planner/bookings/bookingFacts.ts | 19 +- .../Planner/bookings/bookingParts.tsx | 114 +- .../Planner/bookings/bookingsModel.ts | 35 +- .../Planner/bookings/transitParts.tsx | 70 + .../Planner/bookings/useBookingActions.tsx | 46 + .../Planner/bookings/useBookingsView.ts | 9 +- .../bookings/useReservationDetailPlugins.ts | 6 + .../components/Planner/placeDialogParts.tsx | 45 + client/src/components/Planner/planParts.tsx | 124 ++ .../src/components/Planner/transitDisplay.tsx | 8 +- client/src/components/Planner/useDayDetail.ts | 154 +- .../Planner/usePlacesSidebar.test.tsx | 69 +- .../components/Planner/usePlacesSidebar.ts | 81 +- .../Plugins/PluginContributions.tsx | 1 - .../Roadtrip/RoadtripModeSwitch.tsx | 8 +- .../Roadtrip/ServiceStopSection.test.tsx | 46 +- .../Roadtrip/ServiceStopSection.tsx | 42 +- client/src/components/Roadtrip/StopChips.tsx | 29 +- .../components/Todo/TodoListPanel.test.tsx | 6 +- client/src/components/Todo/TodoListPanel.tsx | 79 +- client/src/components/Todo/TodoTaskFields.tsx | 108 +- .../src/components/Weather/WeatherWidget.tsx | 15 +- .../src/components/shared/ConfirmDialog.tsx | 119 +- client/src/components/shared/CustomSelect.tsx | 4 + client/src/components/shared/DialogShell.tsx | 284 ++++ .../src/components/shared/GuestBadge.test.tsx | 11 +- client/src/components/shared/GuestBadge.tsx | 8 +- client/src/components/shared/NameDialog.tsx | 82 +- .../shared/NoteFormatToolbar.test.tsx | 25 +- .../components/shared/NoteFormatToolbar.tsx | 40 +- client/src/components/shared/Tooltip.tsx | 2 +- .../components/shared/dialogParts.test.tsx | 157 ++ client/src/components/shared/dialogParts.tsx | 220 +++ client/src/index.css | 36 +- .../src/pages/DashboardPage.desktop.test.tsx | 2 +- client/src/pages/TripPlannerPage.test.tsx | 33 +- client/src/pages/TripPlannerPage.tsx | 181 +-- .../src/pages/TripPlannerPage.wiring.test.tsx | 156 +- .../pages/tripPlanner/useTripPlanner.test.tsx | 198 ++- .../src/pages/tripPlanner/useTripPlanner.ts | 69 + .../unit/mobile/dashboard/MDashboard.test.tsx | 4 + shared/src/i18n/ar/budget.ts | 17 +- shared/src/i18n/ar/dayplan.ts | 4 +- shared/src/i18n/ar/help.ts | 48 +- shared/src/i18n/ar/places.ts | 8 +- shared/src/i18n/ar/reservations.ts | 63 +- shared/src/i18n/ar/transport.ts | 1 + shared/src/i18n/az/budget.ts | 44 +- shared/src/i18n/az/help.ts | 33 +- shared/src/i18n/az/places.ts | 63 +- shared/src/i18n/az/reservations.ts | 170 +- shared/src/i18n/az/transport.ts | 3 +- shared/src/i18n/br/budget.ts | 17 +- shared/src/i18n/br/dayplan.ts | 4 +- shared/src/i18n/br/help.ts | 47 +- shared/src/i18n/br/places.ts | 8 +- shared/src/i18n/br/reservations.ts | 63 +- shared/src/i18n/br/transport.ts | 1 + shared/src/i18n/ca/budget.ts | 12 +- shared/src/i18n/ca/help.ts | 32 +- shared/src/i18n/ca/places.ts | 7 +- shared/src/i18n/ca/reservations.ts | 57 + shared/src/i18n/ca/transport.ts | 1 + shared/src/i18n/cs/budget.ts | 17 +- shared/src/i18n/cs/dayplan.ts | 4 +- shared/src/i18n/cs/help.ts | 48 +- shared/src/i18n/cs/places.ts | 8 +- shared/src/i18n/cs/reservations.ts | 63 +- shared/src/i18n/cs/transport.ts | 1 + shared/src/i18n/de/budget.ts | 12 +- shared/src/i18n/de/help.ts | 32 +- shared/src/i18n/de/places.ts | 4 + shared/src/i18n/de/reservations.ts | 13 +- shared/src/i18n/en/budget.ts | 12 +- shared/src/i18n/en/help.ts | 33 +- shared/src/i18n/en/places.ts | 7 + shared/src/i18n/en/reservations.ts | 7 +- shared/src/i18n/es/budget.ts | 17 +- shared/src/i18n/es/dayplan.ts | 4 +- shared/src/i18n/es/help.ts | 47 +- shared/src/i18n/es/places.ts | 11 +- shared/src/i18n/es/reservations.ts | 63 +- shared/src/i18n/es/transport.ts | 1 + shared/src/i18n/et/budget.ts | 9 +- shared/src/i18n/et/help.ts | 33 +- shared/src/i18n/et/places.ts | 4 + shared/src/i18n/et/reservations.ts | 57 + shared/src/i18n/et/transport.ts | 1 + shared/src/i18n/fr/budget.ts | 20 +- shared/src/i18n/fr/dayplan.ts | 4 +- shared/src/i18n/fr/help.ts | 47 +- shared/src/i18n/fr/places.ts | 13 +- shared/src/i18n/fr/reservations.ts | 63 +- shared/src/i18n/fr/transport.ts | 1 + shared/src/i18n/gr/budget.ts | 20 +- shared/src/i18n/gr/help.ts | 47 +- shared/src/i18n/gr/places.ts | 11 +- shared/src/i18n/gr/reservations.ts | 63 +- shared/src/i18n/gr/transport.ts | 1 + shared/src/i18n/hu/budget.ts | 20 +- shared/src/i18n/hu/dayplan.ts | 4 +- shared/src/i18n/hu/help.ts | 47 +- shared/src/i18n/hu/places.ts | 8 +- shared/src/i18n/hu/reservations.ts | 63 +- shared/src/i18n/hu/transport.ts | 1 + shared/src/i18n/id/budget.ts | 17 +- shared/src/i18n/id/dayplan.ts | 4 +- shared/src/i18n/id/help.ts | 46 +- shared/src/i18n/id/places.ts | 8 +- shared/src/i18n/id/reservations.ts | 63 +- shared/src/i18n/id/transport.ts | 1 + shared/src/i18n/it/budget.ts | 20 +- shared/src/i18n/it/dayplan.ts | 4 +- shared/src/i18n/it/help.ts | 46 +- shared/src/i18n/it/places.ts | 8 +- shared/src/i18n/it/reservations.ts | 63 +- shared/src/i18n/it/transport.ts | 1 + shared/src/i18n/ja/budget.ts | 20 +- shared/src/i18n/ja/help.ts | 47 +- shared/src/i18n/ja/places.ts | 8 +- shared/src/i18n/ja/reservations.ts | 63 +- shared/src/i18n/ja/transport.ts | 1 + shared/src/i18n/ko/budget.ts | 17 +- shared/src/i18n/ko/help.ts | 48 +- shared/src/i18n/ko/places.ts | 8 +- shared/src/i18n/ko/reservations.ts | 63 +- shared/src/i18n/ko/transport.ts | 1 + shared/src/i18n/nl/budget.ts | 20 +- shared/src/i18n/nl/dayplan.ts | 4 +- shared/src/i18n/nl/help.ts | 48 +- shared/src/i18n/nl/places.ts | 8 +- shared/src/i18n/nl/reservations.ts | 63 +- shared/src/i18n/nl/transport.ts | 1 + shared/src/i18n/pl/budget.ts | 20 +- shared/src/i18n/pl/dayplan.ts | 4 +- shared/src/i18n/pl/help.ts | 47 +- shared/src/i18n/pl/places.ts | 8 +- shared/src/i18n/pl/reservations.ts | 63 +- shared/src/i18n/pl/transport.ts | 1 + shared/src/i18n/ru/budget.ts | 17 +- shared/src/i18n/ru/dayplan.ts | 4 +- shared/src/i18n/ru/help.ts | 45 +- shared/src/i18n/ru/places.ts | 8 +- shared/src/i18n/ru/reservations.ts | 63 +- shared/src/i18n/ru/transport.ts | 1 + shared/src/i18n/sk/budget.ts | 9 +- shared/src/i18n/sk/help.ts | 35 +- shared/src/i18n/sk/places.ts | 4 + shared/src/i18n/sk/reservations.ts | 57 + shared/src/i18n/sk/transport.ts | 1 + shared/src/i18n/sv/budget.ts | 20 +- shared/src/i18n/sv/help.ts | 49 +- shared/src/i18n/sv/places.ts | 8 +- shared/src/i18n/sv/reservations.ts | 57 + shared/src/i18n/sv/transport.ts | 1 + shared/src/i18n/th/budget.ts | 341 ++-- shared/src/i18n/th/help.ts | 1135 ++++++++----- shared/src/i18n/th/places.ts | 258 +-- shared/src/i18n/th/reservations.ts | 401 +++-- shared/src/i18n/th/transport.ts | 11 +- shared/src/i18n/tr/budget.ts | 20 +- shared/src/i18n/tr/help.ts | 47 +- shared/src/i18n/tr/places.ts | 10 +- shared/src/i18n/tr/reservations.ts | 63 +- shared/src/i18n/tr/transport.ts | 1 + shared/src/i18n/uk/budget.ts | 20 +- shared/src/i18n/uk/dayplan.ts | 4 +- shared/src/i18n/uk/help.ts | 47 +- shared/src/i18n/uk/places.ts | 8 +- shared/src/i18n/uk/reservations.ts | 63 +- shared/src/i18n/uk/transport.ts | 1 + shared/src/i18n/vi/budget.ts | 9 +- shared/src/i18n/vi/help.ts | 32 +- shared/src/i18n/vi/places.ts | 8 +- shared/src/i18n/vi/reservations.ts | 57 + shared/src/i18n/vi/transport.ts | 1 + shared/src/i18n/zh-TW/budget.ts | 17 +- shared/src/i18n/zh-TW/dayplan.ts | 4 +- shared/src/i18n/zh-TW/help.ts | 50 +- shared/src/i18n/zh-TW/places.ts | 8 +- shared/src/i18n/zh-TW/reservations.ts | 63 +- shared/src/i18n/zh-TW/transport.ts | 1 + shared/src/i18n/zh/budget.ts | 9 +- shared/src/i18n/zh/help.ts | 34 +- shared/src/i18n/zh/places.ts | 4 + shared/src/i18n/zh/reservations.ts | 57 + shared/src/i18n/zh/transport.ts | 1 + 314 files changed, 17106 insertions(+), 10400 deletions(-) create mode 100644 client/src/components/Collab/CollabPanelHead.tsx create mode 100644 client/src/components/Planner/DayPlanSidebarNoteModal.test.tsx create mode 100644 client/src/components/Planner/DayPlanSidebarTimeConfirmModal.test.tsx create mode 100644 client/src/components/Planner/PlacesBulkCategoryModal.test.tsx create mode 100644 client/src/components/Planner/bookings/BookingDetailHost.test.tsx create mode 100644 client/src/components/Planner/bookings/BookingDetailHost.tsx create mode 100644 client/src/components/Planner/bookings/BookingDialogShell.test.tsx create mode 100644 client/src/components/Planner/bookings/BookingDialogShell.tsx create mode 100644 client/src/components/Planner/bookings/transitParts.tsx create mode 100644 client/src/components/Planner/bookings/useBookingActions.tsx create mode 100644 client/src/components/Planner/bookings/useReservationDetailPlugins.ts create mode 100644 client/src/components/Planner/placeDialogParts.tsx create mode 100644 client/src/components/Planner/planParts.tsx create mode 100644 client/src/components/shared/DialogShell.tsx create mode 100644 client/src/components/shared/dialogParts.test.tsx create mode 100644 client/src/components/shared/dialogParts.tsx 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