Files
Maurice 3901aecbc0 feat(trips): Share and Save to list in the new dialog look
Share opens on the shared dialog frame: the trip's name and the number of
people in the head band, members and guests on the left, the public link
and the invite link as two cards on the right. Rows carry their actions as
icon buttons named by their tooltips, and removing a member, leaving,
handing over the trip or removing a guest asks in the app's own dialog
instead of the browser's. The invite link can no longer be created twice by
a double click. The phone, which opens the same dialog from its More sheet,
keeps the sheet it had; both run on one set of hooks.

Save to list has the place's name in the head band, Mark visited as a pill
beside it, the lists as rows with a tick, and View at its foot.

The public link's hint loses its dash in every language, and the help
guides and wiki pages of the Share dialog show the new look.
2026-09-28 13:19:04 +02:00

235 lines
9.9 KiB
TypeScript

import type { Page } from '@playwright/test'
import { test, clearNotices, expect, seed } from './shot'
import { TOKYO_HOTEL } from '../help/fixtures'
/**
* The Plan tab and the dialogs of the trip page, as the wiki pages for the day
* plan, places, bookings, transports and costs show them. Every picture drives
* the real UI by its roles and names, so a renamed control fails here instead
* of quietly photographing the wrong thing.
*/
test.beforeEach(async ({ page }) => {
await page.goto(`/trips/${seed.tripId}`)
await clearNotices(page)
await page.waitForTimeout(800)
})
const dialog = (page: Page) => page.getByRole('dialog').last()
const firstDayCard = (page: Page) => page.locator('.dp-day-header').first().locator('xpath=..')
async function openTab(page: Page, label: string) {
// A tab can carry a longer name for screen readers than its label ("Book" for the bookings).
await page.getByRole('button', { name: new RegExp(`^${label}`) }).first().click()
await page.waitForTimeout(800)
}
// On the day's number at the left edge: the middle of the header is the booked
// night's pill once the day has a stay, and a click there opens the hotel instead.
async function selectDay(page: Page, index: number) {
await page.locator('.dp-day-header').nth(index).click({ position: { x: 22, y: 20 } })
await page.waitForTimeout(900)
}
const selectFirstDay = (page: Page) => selectDay(page, 0)
/** Out of the way, so no tooltip of the button just clicked stays in the picture. */
const parkPointer = (page: Page) => page.mouse.move(1, 1)
/** A row of the places column, by the place's name. */
const placeOption = (page: Page, name: string) => page.getByRole('option', { name: new RegExp(`^${name}`) }).first()
test('plan: a day card', async ({ page, shot }) => {
await shot.element('PlanDayCard', firstDayCard(page))
})
test('plan: the day "+" menu', async ({ page, shot }) => {
await selectFirstDay(page)
await firstDayCard(page).locator('button[aria-label="Add to day"]').click()
await expect(page.getByRole('button', { name: 'Add Note' })).toBeVisible()
await shot.page_('PlanDayAddMenu')
})
test('plan: the route bar', async ({ page, shot }) => {
await selectFirstDay(page)
const bar = page.locator('[data-dp="route-tools"]').first()
await expect(bar).toBeVisible()
await shot.element('PlanRouteBar', bar)
})
test('plan: the places column', async ({ page, shot }) => {
const column = page.getByPlaceholder('Search').first().locator('xpath=ancestor::div[contains(@class,"flex-col")][2]')
await shot.element('PlacesColumn', column)
})
test('plan: the place inspector', async ({ page, shot }) => {
await placeOption(page, 'Senso-ji Temple').click()
await page.waitForTimeout(1200)
await shot.page_('PlaceInspector')
})
test('plan: the save to list dialog', async ({ page, shot }) => {
await placeOption(page, 'Senso-ji Temple').click()
await page.waitForTimeout(1000)
await page.getByRole('button', { name: /^Save to Collection/ }).first().click()
await expect(dialog(page)).toBeVisible()
await expect(dialog(page).getByText('Kyoto shortlist')).toBeVisible()
await page.waitForTimeout(500)
await shot.element('SaveToList', dialog(page))
})
test('plan: the edit place dialog', async ({ page, shot }) => {
await placeOption(page, 'Senso-ji Temple').click()
await page.waitForTimeout(1000)
await page.getByRole('button', { name: 'Edit', exact: true }).first().click()
await expect(dialog(page)).toBeVisible()
await page.waitForTimeout(2500)
await shot.element('PlaceForm', dialog(page))
})
test('plan: the day details', async ({ page, shot }) => {
await selectFirstDay(page)
await shot.page_('DayDetails')
})
test('plan: the stay editor', async ({ page, shot }) => {
// Day 9 has no stay yet, so its details offer Add accommodation.
await selectDay(page, 8)
await page.getByRole('button', { name: 'Add accommodation' }).first().click()
await expect(dialog(page)).toBeVisible()
// A hotel picked, so the head band carries its name.
await dialog(page).getByText('Hotel Granvia Kyoto', { exact: true }).click()
await page.waitForTimeout(500)
await shot.element('StayEditor', dialog(page))
})
test('plan: the map hover card', async ({ page, shot }) => {
// With no day open every stop is folded into a cluster; the open day's stops are
// pins. Its details panel stays: closing it would close the day as well.
await selectFirstDay(page)
// A place pin, not a cluster bubble or an explore result (only those carry a
// name). The columns float over the map's edges and pins stack, so the first
// one in the DOM is often one the pointer cannot reach.
const pins = page.locator('#trek-map .leaflet-marker-icon:not(:has(.marker-cluster-custom)):not([aria-label])')
await expect(pins.first()).toBeAttached()
const total = await pins.count()
for (let i = 0; i < total; i++) {
if (await pins.nth(i).hover({ trial: true, timeout: 1500 }).then(() => true, () => false)) {
await pins.nth(i).hover()
break
}
}
const card = page.getByTestId('tooltip')
await expect(card).toBeVisible()
await page.waitForTimeout(600)
// The card and the pin it belongs to with some map around them, not the whole planner.
const box = (await card.boundingBox())!
const width = 760
const height = 440
const x = Math.max(0, Math.min(1440 - width, box.x + box.width / 2 - width / 2))
const y = Math.max(0, Math.min(900 - height, box.y + box.height / 2 - height / 2))
await shot.region('MapHoverCard', { x, y, width, height })
})
test('plan: the export dialog', async ({ page, shot }) => {
await page.getByRole('button', { name: 'Export' }).first().click()
await expect(dialog(page)).toBeVisible()
await shot.element('ExportDialog', dialog(page))
})
test('plan: the subscribe to calendar dialog', async ({ page, shot }) => {
await page.getByRole('button', { name: 'Export' }).first().click()
await dialog(page).getByRole('button', { name: /^Subscribe to calendar/ }).click()
const subscribe = page.getByRole('dialog').filter({ hasText: 'Subscribe to calendar' }).last()
await subscribe.getByRole('button', { name: 'Enable calendar subscription' }).click()
// Enabled, the dialog shows the ways to subscribe and its Regenerate and Turn off.
await expect(subscribe.getByRole('button', { name: 'Turn off' })).toBeVisible()
await shot.element('IcsSubscribe', subscribe)
await subscribe.getByRole('button', { name: 'Turn off' }).click()
})
test('plan: the PDF preview', async ({ page, shot }) => {
await page.getByRole('button', { name: 'Export' }).first().click()
await dialog(page).getByRole('button', { name: /^PDF/ }).click()
const preview = page.locator('#pdf-preview-overlay > div')
await expect(preview).toBeVisible({ timeout: 30_000 })
await parkPointer(page)
// The document is a srcdoc frame; its photos and the route map load after it.
await page.waitForTimeout(4000)
await shot.element('PDFPreview', preview)
await page.locator('#pdf-close-btn').click()
})
// The Bookings tab holds a booking of every kind here (shots.setup.ts), so the
// views are shown on it; each test puts the view back to the cards.
async function bookingsView(page: Page, view: 'Cards' | 'List' | 'Timeline') {
await openTab(page, 'Book')
await page.getByRole('button', { name: view, exact: true }).first().click()
await parkPointer(page)
await page.waitForTimeout(800)
}
test('bookings: list view', async ({ page, shot }) => {
await bookingsView(page, 'List')
await shot.page_('BookingsList')
await page.getByRole('button', { name: 'Cards', exact: true }).first().click()
})
test('bookings: timeline view', async ({ page, shot }) => {
await bookingsView(page, 'Timeline')
await shot.page_('BookingsTimeline')
await page.getByRole('button', { name: 'Cards', exact: true }).first().click()
})
test('bookings: the detail popup and the editor', async ({ page, shot }) => {
await bookingsView(page, 'Cards')
await page.locator('article').filter({ hasText: TOKYO_HOTEL }).first().click()
await expect(dialog(page)).toBeVisible()
await page.waitForTimeout(600)
await shot.element('BookingDetail', dialog(page))
await dialog(page).getByRole('button', { name: 'Edit', exact: true }).click()
await page.waitForTimeout(800)
await shot.element('BookingEditor', dialog(page))
})
test('transports: timeline view', async ({ page, shot }) => {
await openTab(page, 'Transports')
await page.getByRole('button', { name: 'Timeline', exact: true }).first().click()
await parkPointer(page)
await page.waitForTimeout(800)
await shot.page_('TransportsTimeline')
await page.getByRole('button', { name: 'Cards', exact: true }).first().click()
})
test('transports: the detail popup', async ({ page, shot }) => {
await openTab(page, 'Transports')
await page.getByRole('button', { name: 'Cards', exact: true }).first().click()
await page.locator('article').filter({ hasText: 'LH716' }).first().click()
await expect(dialog(page)).toBeVisible()
await page.waitForTimeout(600)
await shot.element('TransportDetail', dialog(page))
})
test('transports: the add transport dialog', async ({ page, shot }) => {
await openTab(page, 'Transports')
await page.getByRole('button', { name: /^(Add transport|Transport)$/ }).first().click()
await expect(dialog(page)).toBeVisible()
await shot.element('TransportEditor', dialog(page))
})
test('costs: the add expense dialog', async ({ page, shot }) => {
await openTab(page, 'Costs')
await page.getByRole('button', { name: 'Add expense' }).first().click()
await expect(dialog(page)).toBeVisible()
await shot.element('ExpenseDialog', dialog(page))
})
test('costs: the table view', async ({ page, shot }) => {
await openTab(page, 'Costs')
await page.getByRole('button', { name: 'Table', exact: true }).click()
await expect(page.getByRole('table')).toBeVisible()
await parkPointer(page)
await shot.page_('CostsTable')
// Back to the list for every other picture of the tab.
await page.getByRole('button', { name: 'List', exact: true }).click()
})