Files
TREK/client/e2e/help/trip-shared.ts
T
Maurice 0ed0d98c32 feat(help): ten more screens of a trip, registered with their guide scripts
Place details, Day details, Map, Transports, Bookings, Lists, Costs, Files,
Collab and Road Trip: 77 guides over 360 steps, each one a task a reader can
follow, with the Playwright script that will photograph it. The fixtures they
need go in beside ensureDaysFixtures, and every screen is a child of the trip,
so the panel folds them under it.

The capture now pins the browser's clock to the day the seeded trip ends. A
picture has to look the same whoever takes it and whenever: a day later the
trip is over, My Trips moves it out of the boarding pass, What's Next has
nothing left to list, and a forecast for a day in the past is not a forecast.
Only `Date` is pinned, so timers keep running.

trip-days is green end to end after its locators were checked against the
running app; its route guides read day 6, whose two stops sit next to each
other, because day 1's are separated by the flight and have no leg of their own.
2026-09-24 14:05:44 +02:00

93 lines
4.2 KiB
TypeScript

import { expect, type Locator, type Page } from '@playwright/test'
import { clearNotices } from '../screenshots/shot'
import { dismissReleaseNotice, settle } from './guide'
import { seededTrip } from './fixtures'
/**
* What every trip screen's script needs: the way into the seeded trip, the
* day selection and the dialogs. Not a `.guide.ts`, so importing it registers
* no tests twice.
*/
export const modal = (page: Page) => page.locator('.trek-modal-backdrop').last()
export const dialog = (page: Page) => modal(page).locator('> [role="presentation"], > div').first()
/**
* ConfirmDialog is its own portal and carries `trek-backdrop-enter` without the
* `trek-modal-backdrop` of `Modal`, so the yes/no questions need their own
* locator: `modal` would never see them.
*/
export const confirmBackdrop = (page: Page) => page.locator('.trek-backdrop-enter:not(.trek-modal-backdrop)').last()
export const confirmDialog = (page: Page) => confirmBackdrop(page).locator('.trek-modal-enter').last()
/**
* The import dialogs of the places column are portals of their own with no
* shared backdrop class at all, so they are found by what stands inside them.
* Both the overlay and the card carry `role="presentation"`, and the card comes
* second in document order, which is the one worth a picture.
*/
export const portalDialog = (page: Page, contains: Locator): Locator =>
page.locator('div[role="presentation"]').filter({ has: contains }).last()
export async function openTrip(page: Page, opts: { tab?: string; day?: number | null } = {}): Promise<void> {
const { tripId } = seededTrip()
const { tab, day = 1 } = opts
await page.goto(`/trips/${tripId}${tab ? `?tab=${tab}` : ''}`)
await clearNotices(page)
await dismissReleaseNotice(page)
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible({ timeout: 30_000 })
await settle(page)
if (!tab && day) {
// The trip is running, so the plan opens on today, an empty day at the
// bottom. Day 1 has the places; its details panel would cover the map.
await selectDay(page, day)
await closeDayDetails(page)
}
// The map tiles land after the page says it is idle.
await page.waitForTimeout(1200)
}
/** Click the day's header: selects it and opens its details panel. */
export async function selectDay(page: Page, n: number): Promise<void> {
const header = page.getByRole('button', { name: new RegExp(`^${n} .*Day ${n} `) })
// The header carries buttons of its own: the booked night in the middle, the
// transit, note and fold actions on the right. A click on the centre lands on
// the booked night as soon as a day has one, which selects nothing, so the
// click goes to the day's number at the left edge.
await header.click({ position: { x: 22, y: 20 } })
// The details panel is the selection: without it the day's route tools, the
// + on a place row and To day are all absent, and a guide fails far from here.
await expect(dayDetails(page)).toBeVisible({ timeout: 15_000 })
await settle(page)
}
/** The day details panel: the one fixed, floating card over the map. */
export const dayDetails = (page: Page) => page.locator('div.fixed.z-50').first()
/** The details panel's close is the one unlabelled X button over the map. */
export async function closeDayDetails(page: Page): Promise<void> {
const close = page.locator('button:has(svg.lucide-x)').last()
if (await close.isVisible().catch(() => false)) {
await close.click()
await settle(page)
}
}
/**
* Open the trip with day `n` selected and its details panel standing. The panel
* IS the selection — closing it deselects the day — and the day's route tools
* and the "+" at the end of a place row render for a selected day only, so a
* guide about either of them starts here rather than at `openTrip`.
*/
export async function openTripOnDay(page: Page, n = 1): Promise<void> {
await openTrip(page, { day: null })
await selectDay(page, n)
}
/**
* Dismiss a `ContextMenu`. It closes on a document click and ignores Escape,
* and a real click would land on the map or on the row underneath.
*/
export async function closeMenu(page: Page): Promise<void> {
await page.evaluate(() => document.body.click())
await settle(page)
}