diff --git a/client/e2e/help/trip-places.guide.ts b/client/e2e/help/trip-places.guide.ts index 9a27fa3d0..aca703f59 100644 --- a/client/e2e/help/trip-places.guide.ts +++ b/client/e2e/help/trip-places.guide.ts @@ -1,7 +1,7 @@ import { test, expect, type Page, type Locator } from '@playwright/test' import { captureGuide, captureHero, beat, typeInto, settle, VIEWPORT, type GuideScript } from './guide' import { seededTrip, gpxFixture, ensureTrack } from './fixtures' -import { openTrip, modal, dialog } from './trip-shared' +import { openTrip, modal, dialog, confirmDialog, portalDialog } from './trip-shared' import { tripPlacesContext, tripPlacesGuides } from '../../src/help/contexts/tripPlaces' import type { HelpGuide } from '../../src/help/types' @@ -19,6 +19,8 @@ const guide = (id: string): HelpGuide => { } const SPARE = { name: 'Tsukiji Outer Market', lat: 35.6654, lng: 139.7707, address: '4 Chome Tsukiji, Chuo City, Tokyo' } +/** What create-place looks up. See the note in its second step for why this one. */ +const SEARCHED = { query: 'Osaka Castle', match: /^Osaka Castle/ } /** A row of the places column by the place's name (the rows are options of a list). */ const row = (page: Page, name: string) => page.getByRole('option', { name: new RegExp(`^${name}`) }).first() @@ -29,6 +31,13 @@ const addButton = (page: Page) => page.getByRole('button', { name: /^(Add Place\ /** The filter row's controls. */ const filterSelect = (page: Page) => page.getByTestId('places-filter').getByRole('button').first() const searchBox = (page: Page) => page.getByPlaceholder('Search places...').last() +/** FileImportModal: its own portal, recognised by the hidden file input and its heading. */ +const importFileDialog = (page: Page) => + portalDialog(page, page.locator('input[type="file"][accept*=".gpx"]')) +/** ListImportModal: its own portal, recognised by the link box. */ +const listImportDialog = (page: Page) => portalDialog(page, page.getByPlaceholder(/goo\.gl|naver/)) +/** The drop box of the file dialog: the dashed button that opens the file picker. */ +const dropBox = (page: Page) => importFileDialog(page).getByRole('button').first() /** The column itself: the header block around the search box. */ const column = (page: Page) => searchBox(page).locator('xpath=ancestor::div[contains(@class,"scroll") or @data-places-sidebar][1]') @@ -74,29 +83,37 @@ const SCRIPTS: Record = { { prepare: async p => { const box = modal(p).getByPlaceholder('Search places...') - await typeInto(p, box, 'Tokyo Tower') - // The suggestions drop down under the box; the first one is the tower. - const suggestion = modal(p).locator('.shadow-dropdown button').filter({ hasText: /Tokyo Tower/ }).first() + // Osaka Castle on purpose: its entry in the index carries a picture, + // opening hours and a description, so Place details shows what the + // text says it shows. Most results have none of that and the column + // would be an empty state in the picture. + await typeInto(p, box, SEARCHED.query) + const suggestion = modal(p).locator('.shadow-dropdown button').filter({ hasText: SEARCHED.match }).first() await expect(suggestion).toBeVisible({ timeout: 20_000 }) await suggestion.click() - await expect(modal(p).getByPlaceholder('e.g. Eiffel Tower')).toHaveValue(/Tokyo Tower/, { timeout: 20_000 }) + await expect(modal(p).getByPlaceholder('e.g. Eiffel Tower')).toHaveValue(SEARCHED.match, { timeout: 20_000 }) + // The details column answers after the form; wait for it or the + // picture catches the spinner. + await expect(modal(p).getByText('Pick a picture', { exact: false }).first()).toBeVisible({ timeout: 30_000 }) await settle(p) }, target: p => modal(p).getByPlaceholder('Search places...').locator('xpath=../..'), }, - only(p => modal(p).getByText('Place details', { exact: true }).locator('xpath=ancestor::div[2]')), - only(p => modal(p).getByPlaceholder('e.g. Eiffel Tower').locator('xpath=ancestor::div[3]')), + // The details column is an