mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
e9393e9e6c
The duplicate-trip guide dropped its copy by position, keeping whatever the trips endpoint happened to answer first. The copy survived, and three files later the journey dialog offered two entries starting "Weekend in Lisbon" and the create-journey guide could not say which one it meant. The copy is now dropped by name and the delete is checked, and the journey dialog picks its trip by accessible name rather than by a substring another trip shares. Each file passed on its own before this; only the whole suite in one run showed it.
800 lines
28 KiB
TypeScript
800 lines
28 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test'
|
|
import { clearNotices } from '../screenshots/shot'
|
|
import { captureGuide, captureHero, dismissReleaseNotice, beat, typeInto, settle, VIEWPORT, type GuideScript } from './guide'
|
|
import { ensureExtraTrips, ensureJourneyFixtures, coverFixture, JOURNEY_TITLE, SECOND_JOURNEY, SPARE_TRIP } from './fixtures'
|
|
import { journeyGuides, journeyContext, journalGuides, journalContext, studioGuides, studioContext } from '../../src/help/contexts/journey'
|
|
import type { HelpGuide } from '../../src/help/types'
|
|
|
|
/**
|
|
* Actions for the two Journey screens, keyed by the ids in
|
|
* `src/help/contexts/journey.ts`: the list at `/journey` and the open journal
|
|
* at `/journey/:id`. The fixtures write three entries into the seeded
|
|
* "Autumn in Japan" journey and leave the rest of its timeline as suggestions.
|
|
*/
|
|
|
|
const ALL = [...journeyGuides, ...journalGuides, ...studioGuides]
|
|
|
|
const guide = (id: string): HelpGuide => {
|
|
const g = ALL.find(x => x.id === id)
|
|
if (!g) throw new Error(`no registered guide "${id}"`)
|
|
return g
|
|
}
|
|
|
|
let journeyId = 0
|
|
|
|
async function openJourneys(page: Page): Promise<void> {
|
|
await page.goto('/journey')
|
|
await clearNotices(page)
|
|
await dismissReleaseNotice(page)
|
|
await expect(createCard(page)).toBeVisible({ timeout: 20_000 })
|
|
await expect(journeyCard(page, SECOND_JOURNEY.title)).toBeVisible({ timeout: 20_000 })
|
|
await settle(page)
|
|
}
|
|
|
|
async function openJournal(page: Page): Promise<void> {
|
|
await page.goto(`/journey/${journeyId}`)
|
|
await clearNotices(page)
|
|
await dismissReleaseNotice(page)
|
|
await expect(page.getByRole('button', { name: 'Add Entry' }).first()).toBeVisible({ timeout: 20_000 })
|
|
await expect(entryCard(page, 'First morning in Asakusa')).toBeVisible({ timeout: 20_000 })
|
|
await settle(page)
|
|
await page.waitForTimeout(600) // the map tiles
|
|
}
|
|
|
|
const journeyCard = (page: Page, title: string) => page.locator('.vg-card').filter({ hasText: title }).first()
|
|
/** The + card at the end of the grid. */
|
|
const createCard = (page: Page) => page.getByRole('button', { name: /Create a new Journey/ })
|
|
/** Full-screen dialogs of this screen (create, settings, add trip, invite) all sit at z-200. */
|
|
const anyDialog = (page: Page) => page.locator('div.fixed.inset-0.z-\\[200\\]')
|
|
const dialog = (page: Page) => anyDialog(page).last()
|
|
const dialogCard = (page: Page) => dialog(page).locator('> div').first()
|
|
/** The entry editor. */
|
|
const editor = (page: Page) => page.locator('div.fixed.inset-0.z-\\[9999\\]')
|
|
|
|
/** A written entry's card in the timeline, and the ⋯ menu button on its cover. */
|
|
const entryCard = (page: Page, title: string) => page.locator('div[class*="rounded-[20px]"]').filter({ hasText: title }).first()
|
|
// Two builds of the button: on a photo cover (rounded-[10px]) and on a plain card (w-7 h-7 rounded-md).
|
|
const entryMenu = (page: Page, title: string) => entryCard(page, title).locator('button[class*="rounded-[10px]"], button.w-7.rounded-md').first()
|
|
/** A suggestion card: the one kind of card that carries a dismiss button. */
|
|
const suggestion = (page: Page) =>
|
|
page.locator('[role="button"]').filter({ has: page.getByRole('button', { name: 'Dismiss this suggestion' }) }).first()
|
|
const settingsButton = (page: Page) => page.getByRole('button', { name: 'Journey Settings' }).first()
|
|
const mapPanel = (page: Page) => page.locator('aside').filter({ has: page.locator('.leaflet-container, .maplibregl-map, canvas') }).first()
|
|
|
|
async function openSettings(page: Page): Promise<void> {
|
|
await settingsButton(page).click()
|
|
await expect(dialog(page).getByText('Journey Settings')).toBeVisible()
|
|
await settle(page)
|
|
}
|
|
|
|
/** Close whatever dialogs are stacked: Escape first, then the backdrop, until none is left. */
|
|
async function closeDialog(page: Page): Promise<void> {
|
|
await page.keyboard.press('Escape')
|
|
for (let i = 0; i < 3 && (await anyDialog(page).count()); i++) {
|
|
await page.mouse.click(20, VIEWPORT.height / 2)
|
|
await page.waitForTimeout(300)
|
|
}
|
|
await expect(anyDialog(page)).toHaveCount(0)
|
|
await settle(page)
|
|
}
|
|
|
|
async function openEditor(page: Page): Promise<void> {
|
|
await page.getByRole('button', { name: 'Add Entry' }).first().click()
|
|
await expect(editor(page)).toBeVisible()
|
|
await settle(page)
|
|
}
|
|
|
|
// ── Studio ────────────────────────────────────────────────────────────────────
|
|
|
|
const rail = (page: Page) => page.locator('.st-rail')
|
|
const railButton = (page: Page, name: string) => rail(page).getByRole('button', { name, exact: true })
|
|
const sidePanel = (page: Page) => page.locator('.st-panel.st-side')
|
|
const inspector = (page: Page) => page.locator('.st-inspector')
|
|
const studioMenu = (page: Page) => page.locator('.st-menu').last()
|
|
const exportDialog = (page: Page) => page.locator('.st-ex')
|
|
const thumbs = (page: Page) => page.locator('.st-thumb-row')
|
|
/** The hit targets over the spread, one per element. */
|
|
const sheetElements = (page: Page) => page.locator('.st-sheet > div[style*="left:"]')
|
|
|
|
async function studioSection(page: Page, name: string): Promise<void> {
|
|
await railButton(page, name).click()
|
|
await expect(sidePanel(page)).toBeVisible()
|
|
await settle(page)
|
|
}
|
|
|
|
async function relayoutBook(page: Page): Promise<void> {
|
|
await page.getByRole('button', { name: 'Auto layout' }).click()
|
|
await studioMenu(page).getByText('The whole book').click()
|
|
await expect.poll(async () => thumbs(page).count(), { timeout: 30_000 }).toBeGreaterThan(6)
|
|
await settle(page)
|
|
await page.waitForTimeout(800)
|
|
}
|
|
|
|
/**
|
|
* Studio over the journal. A journey with no book yet opens on an empty one,
|
|
* so the first visit lays the book out; every guide then finds pages to work
|
|
* on, and the overview picture shows a book rather than blank covers.
|
|
*/
|
|
async function openStudio(page: Page): Promise<void> {
|
|
await page.goto(`/journey/${journeyId}/studio`)
|
|
await clearNotices(page)
|
|
await dismissReleaseNotice(page)
|
|
await expect(page.locator('.st-bar')).toBeVisible({ timeout: 30_000 })
|
|
await expect(rail(page)).toBeVisible({ timeout: 30_000 })
|
|
await studioSection(page, 'Pages')
|
|
// An untouched book is cover, first page, one empty spread, last page and back: five thumbnails.
|
|
if ((await thumbs(page).count()) < 7) await relayoutBook(page)
|
|
await settle(page)
|
|
await page.waitForTimeout(800)
|
|
}
|
|
|
|
const SCRIPTS: Record<string, GuideScript> = {
|
|
// ── /journey ────────────────────────────────────────────────────────────
|
|
'create-journey': {
|
|
guide: guide('create-journey'),
|
|
start: openJourneys,
|
|
steps: [
|
|
{
|
|
target: createCard,
|
|
act: async p => {
|
|
await createCard(p).click()
|
|
await expect(dialog(p)).toBeVisible()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
target: dialogCard,
|
|
act: async p => {
|
|
await typeInto(p, dialog(p).locator('input').first(), 'Lisbon weekend')
|
|
await typeInto(p, dialog(p).locator('input').nth(1), 'Two days of tiles and custard tarts')
|
|
// By accessible name, not by a substring: a copy of the trip made by
|
|
// another screen's guide is also "Weekend in Lisbon something".
|
|
await dialog(p).getByRole('checkbox', { name: /^Weekend in Lisbon \d+ days/ }).click()
|
|
await beat(p, 400)
|
|
},
|
|
},
|
|
{
|
|
target: p => dialog(p).getByRole('button', { name: 'Create Journey' }),
|
|
act: async p => {
|
|
await dialog(p).getByRole('button', { name: 'Create Journey' }).click()
|
|
await expect(p).toHaveURL(/\/journey\/\d+/, { timeout: 15_000 })
|
|
await expect(p.getByRole('button', { name: 'Add Entry' }).first()).toBeVisible({ timeout: 20_000 })
|
|
await settle(p)
|
|
await p.waitForTimeout(600)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
'open-journey': {
|
|
guide: guide('open-journey'),
|
|
start: openJourneys,
|
|
steps: [
|
|
{
|
|
target: p => journeyCard(p, SECOND_JOURNEY.title),
|
|
},
|
|
],
|
|
},
|
|
|
|
'continue-writing': {
|
|
guide: guide('continue-writing'),
|
|
start: openJourneys,
|
|
steps: [
|
|
{
|
|
target: p => p.getByText('Continue writing').first(),
|
|
},
|
|
],
|
|
},
|
|
|
|
// ── /journey/:id ────────────────────────────────────────────────────────
|
|
'add-entry': {
|
|
guide: guide('add-entry'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
target: p => p.getByRole('button', { name: 'Add Entry' }).first(),
|
|
act: openEditor,
|
|
},
|
|
{
|
|
target: p => editor(p).getByPlaceholder('Write your story...'),
|
|
act: async p => {
|
|
await typeInto(p, editor(p).getByPlaceholder('Give this moment a name...'), 'Night market on the river')
|
|
await typeInto(
|
|
p,
|
|
editor(p).getByPlaceholder('Write your story...'),
|
|
'Lanterns along the Kamo, grilled sweetfish on sticks and a jazz trio nobody had booked. We stayed until the last stall packed up.',
|
|
)
|
|
await beat(p, 400)
|
|
},
|
|
},
|
|
{
|
|
target: p => editor(p).getByRole('button', { name: 'Amazing' }).locator('xpath=../..'),
|
|
act: async p => {
|
|
await editor(p).getByRole('button', { name: 'Amazing' }).click()
|
|
await editor(p).getByRole('button', { name: 'Partly cloudy' }).click()
|
|
await beat(p, 400)
|
|
},
|
|
},
|
|
{
|
|
target: p => editor(p).getByRole('button', { name: 'Save', exact: true }),
|
|
act: async p => {
|
|
await editor(p).getByRole('button', { name: 'Save', exact: true }).click()
|
|
await expect(editor(p)).toHaveCount(0, { timeout: 15_000 })
|
|
await expect(entryCard(p, 'Night market on the river')).toBeVisible()
|
|
await settle(p)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
'entry-photos': {
|
|
guide: guide('entry-photos'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
target: p => entryMenu(p, 'Barefoot through the light'),
|
|
act: async p => {
|
|
await entryMenu(p, 'Barefoot through the light').click()
|
|
await p.getByRole('button', { name: 'Edit', exact: true }).first().click()
|
|
await expect(editor(p)).toBeVisible()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
target: p => editor(p).getByRole('button', { name: 'Upload photos' }),
|
|
act: async p => {
|
|
const file = await coverFixture()
|
|
await editor(p).locator('input[type="file"][multiple]').setInputFiles(file)
|
|
await expect(editor(p).locator('img').first()).toBeVisible({ timeout: 15_000 })
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
prepare: async p => { await editor(p).locator('img').first().hover() },
|
|
target: p => editor(p).getByRole('button', { name: 'Save', exact: true }),
|
|
act: async p => {
|
|
await editor(p).getByRole('button', { name: 'Save', exact: true }).click()
|
|
await expect(editor(p)).toHaveCount(0, { timeout: 30_000 })
|
|
await expect(entryCard(p, 'Barefoot through the light').locator('img').first()).toBeVisible({ timeout: 15_000 })
|
|
await settle(p)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
suggestions: {
|
|
guide: guide('suggestions'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
target: suggestion,
|
|
},
|
|
{
|
|
target: p => suggestion(p).getByRole('button', { name: 'Dismiss this suggestion' }),
|
|
act: async p => {
|
|
const before = await p.getByRole('button', { name: 'Dismiss this suggestion' }).count()
|
|
await suggestion(p).getByRole('button', { name: 'Dismiss this suggestion' }).click()
|
|
// A place kept across two days is two suggestions, so one click can take two away.
|
|
await expect.poll(() => p.getByRole('button', { name: 'Dismiss this suggestion' }).count()).toBeLessThan(before)
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
prepare: async p => {
|
|
await openSettings(p)
|
|
await dialog(p).getByText('Bring back dismissed suggestions').scrollIntoViewIfNeeded()
|
|
},
|
|
target: p => dialog(p).getByText('Bring back dismissed suggestions').locator('xpath=../..'),
|
|
act: async p => {
|
|
await dialog(p).getByRole('button', { name: 'Bring back dismissed suggestions' }).click()
|
|
await beat(p, 500)
|
|
await closeDialog(p)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
'add-on-day': {
|
|
guide: guide('add-on-day'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
target: p => p.getByRole('button', { name: 'Add an entry on this day' }).nth(1),
|
|
act: async p => {
|
|
await p.getByRole('button', { name: 'Add an entry on this day' }).nth(1).click()
|
|
await expect(editor(p)).toBeVisible()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
target: p => editor(p).getByText('Date', { exact: true }).locator('xpath=..'),
|
|
act: async p => {
|
|
await editor(p).getByRole('button', { name: 'Cancel' }).click()
|
|
await expect(editor(p)).toHaveCount(0)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
'pros-cons': {
|
|
guide: guide('pros-cons'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
prepare: async p => {
|
|
await entryMenu(p, 'First morning in Asakusa').click()
|
|
await p.getByRole('button', { name: 'Edit', exact: true }).first().click()
|
|
await expect(editor(p)).toBeVisible()
|
|
await editor(p).getByText('Pros & Cons').scrollIntoViewIfNeeded()
|
|
await settle(p)
|
|
},
|
|
target: p => editor(p).getByText('Pros & Cons').locator('xpath=../..'),
|
|
act: async p => {
|
|
await typeInto(p, editor(p).getByPlaceholder('Something great...').last(), 'Empty courtyard at seven')
|
|
await typeInto(p, editor(p).getByPlaceholder('Not so great...').last(), 'Coffee only after nine')
|
|
await beat(p, 400)
|
|
},
|
|
},
|
|
{
|
|
target: p => editor(p).getByRole('button', { name: 'Save', exact: true }),
|
|
act: async p => {
|
|
await editor(p).getByRole('button', { name: 'Save', exact: true }).click()
|
|
await expect(editor(p)).toHaveCount(0, { timeout: 15_000 })
|
|
// The card folds a long entry, so the saved verdict is checked at the source.
|
|
await expect.poll(async () => {
|
|
const res = await p.request.get(`/api/journeys/${journeyId}/entries`)
|
|
const { entries } = (await res.json()) as { entries: { title: string | null; pros_cons?: { pros?: string[] } | null }[] }
|
|
return entries.find(e => e.title === 'First morning in Asakusa')?.pros_cons?.pros ?? []
|
|
}).toContain('Empty courtyard at seven')
|
|
await settle(p)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
'search-journey': {
|
|
guide: guide('search-journey'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
target: p => p.getByPlaceholder('Search this journey'),
|
|
act: async p => {
|
|
await typeInto(p, p.getByPlaceholder('Search this journey'), 'crossing')
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
prepare: async p => {
|
|
await p.getByPlaceholder('Search this journey').fill('')
|
|
await settle(p)
|
|
},
|
|
target: p => p.getByRole('button', { name: /^(Hide|Show) suggestions$/ }).first(),
|
|
},
|
|
],
|
|
},
|
|
|
|
'gallery-map': {
|
|
guide: guide('gallery-map'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
target: p => p.getByRole('button', { name: 'Gallery' }).first(),
|
|
act: async p => {
|
|
await p.getByRole('button', { name: 'Gallery' }).first().click()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
prepare: async p => {
|
|
await p.getByRole('button', { name: 'Timeline', exact: true }).first().click()
|
|
await settle(p)
|
|
},
|
|
target: mapPanel,
|
|
},
|
|
],
|
|
},
|
|
|
|
'entry-fields': {
|
|
guide: guide('entry-fields'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
target: settingsButton,
|
|
act: openSettings,
|
|
},
|
|
{
|
|
prepare: async p => { await dialog(p).getByText('Entry fields').scrollIntoViewIfNeeded() },
|
|
target: p => dialog(p).getByText('Entry fields').locator('xpath=..'),
|
|
act: closeDialog,
|
|
},
|
|
],
|
|
},
|
|
|
|
'link-trip': {
|
|
guide: guide('link-trip'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
target: settingsButton,
|
|
act: openSettings,
|
|
},
|
|
{
|
|
prepare: async p => { await dialog(p).getByRole('button', { name: 'Add Trip' }).scrollIntoViewIfNeeded() },
|
|
target: p => dialog(p).getByRole('button', { name: 'Add Trip' }),
|
|
act: async p => {
|
|
await dialog(p).getByRole('button', { name: 'Add Trip' }).click()
|
|
await expect(dialog(p).getByPlaceholder('Trip name or destination...')).toBeVisible()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
target: p => dialog(p).getByText(SPARE_TRIP.title).locator('xpath=../..'),
|
|
act: async p => {
|
|
await dialog(p).getByText(SPARE_TRIP.title).locator('xpath=../..').getByRole('button', { name: 'Link' }).click()
|
|
await beat(p, 600)
|
|
if (await anyDialog(p).count()) await closeDialog(p)
|
|
await settle(p)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
'share-public': {
|
|
guide: guide('share-public'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
prepare: async p => {
|
|
await openSettings(p)
|
|
await dialog(p).getByText('Public Share').scrollIntoViewIfNeeded()
|
|
},
|
|
target: p => dialog(p).getByText('Public Share').locator('xpath=..'),
|
|
},
|
|
{
|
|
target: p => dialog(p).getByRole('button', { name: 'Create share link' }),
|
|
act: async p => {
|
|
await dialog(p).getByRole('button', { name: 'Create share link' }).click()
|
|
await expect(dialog(p).getByRole('button', { name: 'Copy' })).toBeVisible({ timeout: 15_000 })
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
target: p => dialog(p).getByText('Public Share').locator('xpath=..'),
|
|
act: closeDialog,
|
|
},
|
|
],
|
|
},
|
|
|
|
contributors: {
|
|
guide: guide('contributors'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
prepare: async p => {
|
|
await openSettings(p)
|
|
await dialog(p).getByRole('button', { name: 'Invite Contributor' }).scrollIntoViewIfNeeded()
|
|
},
|
|
target: p => dialog(p).getByRole('button', { name: 'Invite Contributor' }).locator('xpath=..'),
|
|
},
|
|
{
|
|
target: p => dialog(p).getByRole('button', { name: 'Invite Contributor' }),
|
|
act: async p => {
|
|
await dialog(p).getByRole('button', { name: 'Invite Contributor' }).click()
|
|
const search = p.getByPlaceholder('Username or email...')
|
|
await expect(search).toBeVisible()
|
|
await typeInto(p, search, 'jonas')
|
|
await dialog(p).locator('button').filter({ hasText: 'jonas' }).first().click()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
target: p => dialog(p).getByRole('button', { name: 'Invite', exact: true }),
|
|
act: async p => {
|
|
await dialog(p).getByRole('button', { name: 'Invite', exact: true }).click()
|
|
await expect(dialog(p).getByText('jonas')).toBeVisible({ timeout: 15_000 })
|
|
await beat(p, 500)
|
|
await closeDialog(p)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
studio: {
|
|
guide: guide('studio'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
target: p => p.getByRole('button', { name: 'Studio', exact: true }).first(),
|
|
act: async p => {
|
|
await p.getByRole('button', { name: 'Studio', exact: true }).first().click()
|
|
await expect(p).toHaveURL(/\/studio/, { timeout: 15_000 })
|
|
await expect(p.locator('.st-bar')).toBeVisible({ timeout: 30_000 })
|
|
await settle(p)
|
|
await p.waitForTimeout(1200)
|
|
},
|
|
},
|
|
{
|
|
target: p => p.locator('.st-back'),
|
|
},
|
|
],
|
|
},
|
|
|
|
'archive-journey': {
|
|
guide: guide('archive-journey'),
|
|
start: openJournal,
|
|
steps: [
|
|
{
|
|
target: settingsButton,
|
|
act: openSettings,
|
|
},
|
|
{
|
|
prepare: async p => { await dialog(p).getByRole('button', { name: 'Archive Journey' }).scrollIntoViewIfNeeded() },
|
|
target: p => dialog(p).getByRole('button', { name: 'Archive Journey' }).locator('xpath=..'),
|
|
act: closeDialog,
|
|
},
|
|
],
|
|
},
|
|
|
|
// ── /journey/:id/studio ─────────────────────────────────────────────────
|
|
'studio-auto-layout': {
|
|
guide: guide('studio-auto-layout'),
|
|
start: openStudio,
|
|
steps: [
|
|
{
|
|
target: p => p.getByRole('button', { name: 'Auto layout' }),
|
|
act: async p => {
|
|
await p.getByRole('button', { name: 'Auto layout' }).click()
|
|
await expect(studioMenu(p)).toBeVisible()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
target: studioMenu,
|
|
act: async p => {
|
|
await studioMenu(p).getByText('The whole book').click()
|
|
await expect.poll(async () => thumbs(p).count(), { timeout: 30_000 }).toBeGreaterThan(6)
|
|
await settle(p)
|
|
await p.waitForTimeout(800)
|
|
},
|
|
},
|
|
{
|
|
target: p => p.getByRole('button', { name: 'Undo' }),
|
|
},
|
|
],
|
|
},
|
|
|
|
'studio-pages': {
|
|
guide: guide('studio-pages'),
|
|
start: openStudio,
|
|
steps: [
|
|
{
|
|
target: p => railButton(p, 'Pages'),
|
|
act: async p => { await studioSection(p, 'Pages') },
|
|
},
|
|
{
|
|
target: p => sidePanel(p).getByRole('button', { name: 'Add spread' }),
|
|
act: async p => {
|
|
const before = await thumbs(p).count()
|
|
await sidePanel(p).getByRole('button', { name: 'Add spread' }).click()
|
|
await expect(thumbs(p)).toHaveCount(before + 1)
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
prepare: async p => {
|
|
await thumbs(p).nth(2).scrollIntoViewIfNeeded()
|
|
await thumbs(p).nth(2).hover()
|
|
},
|
|
target: p => thumbs(p).nth(2),
|
|
},
|
|
],
|
|
},
|
|
|
|
'studio-layouts': {
|
|
guide: guide('studio-layouts'),
|
|
start: openStudio,
|
|
steps: [
|
|
{
|
|
prepare: async p => {
|
|
await studioSection(p, 'Pages')
|
|
await thumbs(p).nth(2).locator('.st-thumb').click()
|
|
await settle(p)
|
|
},
|
|
target: p => railButton(p, 'Layouts'),
|
|
act: async p => { await studioSection(p, 'Layouts') },
|
|
},
|
|
{
|
|
target: p => sidePanel(p).locator('.st-tile, .st-thumb').nth(3),
|
|
act: async p => {
|
|
await sidePanel(p).locator('.st-tile, .st-thumb').nth(3).click()
|
|
await settle(p)
|
|
await p.waitForTimeout(600)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
'studio-content': {
|
|
guide: guide('studio-content'),
|
|
start: openStudio,
|
|
steps: [
|
|
{
|
|
target: p => railButton(p, 'Content'),
|
|
act: async p => { await studioSection(p, 'Content') },
|
|
},
|
|
{
|
|
target: p => sidePanel(p).locator('.st-photo-grid').first(),
|
|
act: async p => {
|
|
const add = sidePanel(p).locator('button[title="Add to this page"]').first()
|
|
await add.hover()
|
|
await add.click()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
prepare: async p => {
|
|
await sidePanel(p).locator('.st-tabs button').filter({ hasText: 'Entries' }).click()
|
|
await expect(sidePanel(p).locator('.st-entry').first()).toBeVisible()
|
|
await settle(p)
|
|
},
|
|
target: p => sidePanel(p).locator('.st-entry').first(),
|
|
act: async p => {
|
|
await sidePanel(p).locator('.st-entry').first().getByRole('button', { name: 'Title', exact: true }).click()
|
|
await settle(p)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
'studio-elements': {
|
|
guide: guide('studio-elements'),
|
|
start: openStudio,
|
|
steps: [
|
|
{
|
|
target: p => railButton(p, 'Elements'),
|
|
act: async p => { await studioSection(p, 'Elements') },
|
|
},
|
|
{
|
|
target: sidePanel,
|
|
},
|
|
],
|
|
},
|
|
|
|
'studio-travel': {
|
|
guide: guide('studio-travel'),
|
|
start: openStudio,
|
|
steps: [
|
|
{
|
|
target: p => railButton(p, 'Travel'),
|
|
act: async p => { await studioSection(p, 'Travel') },
|
|
},
|
|
{
|
|
target: sidePanel,
|
|
},
|
|
],
|
|
},
|
|
|
|
'studio-properties': {
|
|
guide: guide('studio-properties'),
|
|
start: openStudio,
|
|
steps: [
|
|
{
|
|
prepare: async p => {
|
|
await studioSection(p, 'Pages')
|
|
await thumbs(p).nth(2).locator('.st-thumb').click()
|
|
await settle(p)
|
|
await expect(sheetElements(p).first()).toBeVisible()
|
|
},
|
|
target: p => sheetElements(p).first(),
|
|
act: async p => {
|
|
await sheetElements(p).first().click()
|
|
await expect(inspector(p).getByRole('button', { name: 'Delete' })).toBeVisible()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
target: inspector,
|
|
},
|
|
{
|
|
target: p => inspector(p).getByRole('button', { name: 'Delete' }).locator('xpath=..'),
|
|
},
|
|
],
|
|
},
|
|
|
|
'studio-format': {
|
|
guide: guide('studio-format'),
|
|
start: openStudio,
|
|
steps: [
|
|
{
|
|
target: p => p.getByRole('button', { name: 'Page format' }),
|
|
act: async p => {
|
|
await p.getByRole('button', { name: 'Page format' }).click()
|
|
await expect(studioMenu(p)).toBeVisible()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
target: studioMenu,
|
|
act: async p => {
|
|
await p.keyboard.press('Escape')
|
|
if (await studioMenu(p).isVisible()) await p.mouse.click(VIEWPORT.width / 2, VIEWPORT.height / 2)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
'studio-export': {
|
|
guide: guide('studio-export'),
|
|
start: openStudio,
|
|
steps: [
|
|
{
|
|
target: p => p.getByRole('button', { name: 'Export' }),
|
|
act: async p => {
|
|
await p.getByRole('button', { name: 'Export' }).click()
|
|
await expect(exportDialog(p)).toBeVisible()
|
|
await settle(p)
|
|
},
|
|
},
|
|
{
|
|
target: exportDialog,
|
|
},
|
|
{
|
|
target: p => exportDialog(p).getByRole('button', { name: 'Print view' }),
|
|
act: async p => {
|
|
await exportDialog(p).getByRole('button', { name: 'Close' }).click()
|
|
await expect(exportDialog(p)).toHaveCount(0)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
'studio-spread-file': {
|
|
guide: guide('studio-spread-file'),
|
|
start: openStudio,
|
|
steps: [
|
|
{
|
|
target: p => p.getByRole('button', { name: 'Download this spread' }),
|
|
},
|
|
{
|
|
prepare: async p => { await studioSection(p, 'Pages') },
|
|
target: p => sidePanel(p).getByRole('button', { name: 'Import', exact: true }),
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
// ── Run ───────────────────────────────────────────────────────────────────────
|
|
|
|
test.describe.configure({ mode: 'serial' })
|
|
|
|
test.beforeAll(async ({ request }) => {
|
|
await ensureExtraTrips(request)
|
|
journeyId = await ensureJourneyFixtures(request)
|
|
})
|
|
|
|
test('every registered journey guide has a script, and only those', async () => {
|
|
expect(Object.keys(SCRIPTS).sort()).toEqual([...journeyContext.guides, ...journalContext.guides, ...studioContext.guides].sort())
|
|
})
|
|
|
|
test('hero: journey', async ({ page }) => {
|
|
await page.setViewportSize(VIEWPORT)
|
|
await captureHero(page, journeyContext.id, openJourneys)
|
|
})
|
|
|
|
test('hero: journey-detail', async ({ page }) => {
|
|
await page.setViewportSize(VIEWPORT)
|
|
await captureHero(page, journalContext.id, openJournal)
|
|
})
|
|
|
|
test('hero: journey-studio', async ({ page }) => {
|
|
await page.setViewportSize(VIEWPORT)
|
|
await captureHero(page, studioContext.id, openStudio)
|
|
})
|
|
|
|
for (const id of [...journeyContext.guides, ...journalContext.guides, ...studioContext.guides]) {
|
|
test(`pictures: ${id}`, async ({ page }) => {
|
|
await page.setViewportSize(VIEWPORT)
|
|
await captureGuide(page, SCRIPTS[id])
|
|
})
|
|
}
|