diff --git a/ClientApps/trip-editor/src/styles.css b/ClientApps/trip-editor/src/styles.css index ced9ec14..03797492 100644 --- a/ClientApps/trip-editor/src/styles.css +++ b/ClientApps/trip-editor/src/styles.css @@ -399,6 +399,18 @@ body { padding: 0.5rem 0.65rem; } +.trip-editor-segments .trip-editor-segment-row { + align-items: start; + display: grid; + gap: 0.45rem; + grid-template-columns: auto auto minmax(0, 1fr) auto; +} + +.trip-editor-segments .trip-editor-segment-row > .trip-editor-surface { + grid-column: 1 / -1; + min-width: 0; +} + .trip-editor-place-row, .trip-editor-area-row { align-items: center; diff --git a/tests/e2e/trip-editor/tripEditorRealCrudContracts.spec.ts b/tests/e2e/trip-editor/tripEditorRealCrudContracts.spec.ts index 17c08768..d11b99e0 100644 --- a/tests/e2e/trip-editor/tripEditorRealCrudContracts.spec.ts +++ b/tests/e2e/trip-editor/tripEditorRealCrudContracts.spec.ts @@ -169,7 +169,9 @@ test.describe.serial('Trip Editor real endpoint CRUD persistence contracts', () }); await deleteSegmentViaEndpoint(page, secondId); + removeDisposableId(disposableSegmentIds, secondId); await deleteSegmentViaEndpoint(page, firstId); + removeDisposableId(disposableSegmentIds, firstId); await page.reload(); await expectMountedWorkspace(page); await expect(segmentRow(page, secondId)).toHaveCount(0); @@ -296,6 +298,13 @@ async function cleanupSegments(page: Page, segmentIds: string[]): Promise } } +function removeDisposableId(ids: string[], id: string): void { + const index = ids.indexOf(id); + if (index >= 0) { + ids.splice(index, 1); + } +} + async function expectSaved(page: Page): Promise { await expect(page.locator('.trip-editor-save-state').filter({ hasText: /saved/i }).first()).toBeVisible(); } diff --git a/tests/e2e/trip-editor/tripEditorRemainingParity.spec.ts b/tests/e2e/trip-editor/tripEditorRemainingParity.spec.ts index 767eeb40..ce12d266 100644 --- a/tests/e2e/trip-editor/tripEditorRemainingParity.spec.ts +++ b/tests/e2e/trip-editor/tripEditorRemainingParity.spec.ts @@ -89,13 +89,12 @@ test.describe.serial('Trip Editor remaining parity verification', () => { const copyFeedback = page.locator('.trip-editor-map-copy-feedback'); await expect(copyFeedback).toHaveText('Map link copied to clipboard'); await expect(copyFeedback).toHaveCount(1); - await page.waitForTimeout(900); + await expect(utilityButton(page, 'Map link copied')).toBeVisible(); await utilityButton(page, 'Map link copied').click(); await expect(copyFeedback).toHaveCount(1); await expect(copyFeedback).toBeVisible(); - await page.waitForTimeout(900); - await expect(copyFeedback).toBeVisible(); - await expect(copyFeedback).toHaveCount(0, { timeout: 1500 }); + await expect(utilityButton(page, 'Copy map link')).toBeVisible({ timeout: 2500 }); + await expect(copyFeedback).toHaveCount(0, { timeout: 2500 }); const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); expect(clipboardText).toContain(editorPath); diff --git a/tests/e2e/trip-editor/tripEditorRichNotes.spec.ts b/tests/e2e/trip-editor/tripEditorRichNotes.spec.ts index ec1b006d..82a0f21e 100644 --- a/tests/e2e/trip-editor/tripEditorRichNotes.spec.ts +++ b/tests/e2e/trip-editor/tripEditorRichNotes.spec.ts @@ -147,8 +147,7 @@ test.describe.serial('Trip Editor rich notes parity', () => { const form = page.locator('#trip-editor-metadata-form'); await routeRichNoteImages(page, 'https://images.example.test/rich-note.png'); await insertImageUrl(form, 'https://images.example.test/rich-note.png'); - const image = richEditor(form).locator('.ql-editor img'); - await expect(image).toHaveAttribute('src', /\/Public\/ProxyImage\?url=https%3A%2F%2Fimages\.example\.test%2Frich-note\.png$/); + await expect(richEditor(form).locator('.ql-editor img[src="/Public/ProxyImage?url=https%3A%2F%2Fimages.example.test%2Frich-note.png"]')).toHaveCount(1); await pasteDataImage(richEditor(form).locator('.ql-editor')); await expect(form.getByRole('status')).toContainText('Embedded data images are not allowed'); @@ -270,11 +269,11 @@ test.describe.serial('Trip Editor rich notes parity', () => { await pasteDataImage(editor, '

'); await expect(form.getByRole('status')).toContainText('Embedded data images are not allowed'); - await expect(editor.locator('img')).toHaveCount(0); + await expectNoDataImages(editor); await dropDataImage(editor, '

'); await expect(form.getByRole('status')).toContainText('Embedded data images are not allowed'); - await expect(editor.locator('img')).toHaveCount(0); + await expectNoDataImages(editor); await rejectImageDialogUrl(form, '\tDaTa : ImAgE/png;base64,iVBORw0KGgo='); @@ -285,7 +284,7 @@ test.describe.serial('Trip Editor rich notes parity', () => { element.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste' })); }); await expect(form.getByRole('status')).toContainText('Embedded data images are not allowed'); - await expect(editor.locator('img')).toHaveCount(0); + await expectNoDataImages(editor); await editor.evaluate(element => { element.insertAdjacentHTML('beforeend', 'Unsafe link'); @@ -585,6 +584,12 @@ async function rejectImageDialogUrl(form: Locator, url: string): Promise { await dialog.getByRole('button', { name: 'Cancel' }).click(); } +async function expectNoDataImages(editor: Locator): Promise { + await expect.poll(async () => editor.locator('img').evaluateAll(images => + images.filter(image => (image.getAttribute('src') ?? '').replace(/\s+/g, '').toLowerCase().startsWith('data:image')).length + )).toBe(0); +} + async function clickEditableTrailingBlankLine(editor: Locator): Promise { const clickPoint = await editor.evaluate(element => { element.scrollIntoView({ block: 'center', inline: 'nearest' }); diff --git a/tests/e2e/trip-editor/tripEditorRichNotesPersistence.spec.ts b/tests/e2e/trip-editor/tripEditorRichNotesPersistence.spec.ts index 89ea55cd..7e516bd4 100644 --- a/tests/e2e/trip-editor/tripEditorRichNotesPersistence.spec.ts +++ b/tests/e2e/trip-editor/tripEditorRichNotesPersistence.spec.ts @@ -12,6 +12,8 @@ import { type EditorState = Record; test.describe.serial('Trip Editor rich notes real persistence contract', () => { + test.describe.configure({ timeout: 60_000 }); + test('metadata rich notes save through the real endpoint and reload as canonical HTML', async ({ page }) => { await signIn(page); await page.goto(absoluteUrl(editorPath)); diff --git a/tests/e2e/trip-editor/tripEditorSegmentEditing.spec.ts b/tests/e2e/trip-editor/tripEditorSegmentEditing.spec.ts index 2a4c581d..9308670d 100644 --- a/tests/e2e/trip-editor/tripEditorSegmentEditing.spec.ts +++ b/tests/e2e/trip-editor/tripEditorSegmentEditing.spec.ts @@ -123,6 +123,7 @@ test.describe.serial('Trip Editor segment editing', () => { }); test('client-session visibility hides map route without changing API or reload defaults', async ({ page }) => { + test.setTimeout(60_000); await signIn(page); await loadWorkspaceWithSegmentFixture(page); const routeCount = async () => page.locator('.leaflet-overlay-pane path').count(); @@ -136,6 +137,7 @@ test.describe.serial('Trip Editor segment editing', () => { }); test('reorders segments and applies the mocked order response after reload', async ({ page }) => { + test.setTimeout(60_000); await signIn(page); const state = await loadWorkspaceWithSegmentFixture(page); await page.unroute(editorApiMatcher); diff --git a/tests/e2e/trip-editor/tripEditorTestUtils.ts b/tests/e2e/trip-editor/tripEditorTestUtils.ts index 5fd88cc5..518fe399 100644 --- a/tests/e2e/trip-editor/tripEditorTestUtils.ts +++ b/tests/e2e/trip-editor/tripEditorTestUtils.ts @@ -9,7 +9,7 @@ export const editorApiPath = `/api/trips/${config.tripId}/editor`; const forbiddenSidebarSearchRequest = /nominatim|geosearch|search-add|searchadd|\/search(?:[/?#]|$)/i; export type EditorTripFixture = { - regionsById: Record; + regionsById: Record; regionOrder: string[]; placesById: Record; placeOrderByRegionId: Record; @@ -113,7 +113,10 @@ export async function loadEditorStateFixture(page: Page): Promise { + const region = state.regionsById[candidate.regionId]; + return region && !region.isShadow && region.capabilities?.canEdit !== false; + }) ?? Object.values(state.placesById)[0]; if (!place) { throw new Error('Configured Trip Editor fixture must contain at least one loaded place for sidebar search coverage.'); } diff --git a/tests/e2e/trip-editor/tripEditorVisualPolish.spec.ts b/tests/e2e/trip-editor/tripEditorVisualPolish.spec.ts index d7e5ce33..b640240b 100644 --- a/tests/e2e/trip-editor/tripEditorVisualPolish.spec.ts +++ b/tests/e2e/trip-editor/tripEditorVisualPolish.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page, type Route, type TestInfo } from '@playwright/test'; +import { expect, test, type Locator, type Page, type Route, type TestInfo } from '@playwright/test'; import { absoluteUrl, editorApiPath, @@ -39,7 +39,7 @@ test.describe.serial('Trip Editor issue 275 visual polish evidence', () => { await capture(page, testInfo, `${viewport.name}-light-docked-metadata`); note(testInfo, 'docked metadata, tags/share progress, map navigation toolbar', viewport.name, 'light', 'data-bs-theme', 'pass'); - await page.getByRole('button', { name: 'Expand Editor' }).click(); + await expandDockedEditor(page); await expect(page.getByRole('dialog', { name: /Edit Trip -/ })).toBeVisible(); await expectDialogFitsViewport(page); await capture(page, testInfo, `${viewport.name}-light-expanded-metadata`); @@ -75,7 +75,8 @@ test.describe.serial('Trip Editor issue 275 visual polish evidence', () => { await openPlace(page); await capture(page, testInfo, `${viewport.name}-dark-place-edit-docked`); note(testInfo, 'child entity edit docked', viewport.name, 'dark', 'data-bs-theme', 'pass'); - await page.getByRole('button', { name: 'Expand Editor' }).click(); + await expandDockedEditor(page); + await expect(page.getByRole('dialog', { name: /Edit Place -/ })).toBeVisible(); await expectDialogFitsViewport(page); await capture(page, testInfo, `${viewport.name}-dark-place-edit-expanded`); note(testInfo, 'child entity edit expanded', viewport.name, 'dark', 'data-bs-theme', 'pass'); @@ -233,6 +234,16 @@ async function openVisits(page: Page): Promise { await expect(page.getByRole('dialog', { name: 'Visit progress and history' })).toBeVisible(); } +function dockedEditor(page: Page): Locator { + return page.locator('.trip-editor-surface--docked').first(); +} + +async function expandDockedEditor(page: Page): Promise { + const button = dockedEditor(page).getByRole('button', { name: 'Expand Editor' }); + await button.scrollIntoViewIfNeeded(); + await button.click(); +} + async function clickMap(page: Page, position: { xRatio: number; yRatio: number }): Promise { const map = page.getByLabel('Read-only trip map'); await map.evaluate((element, point) => {