Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ClientApps/trip-editor/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions tests/e2e/trip-editor/tripEditorRealCrudContracts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -296,6 +298,13 @@ async function cleanupSegments(page: Page, segmentIds: string[]): Promise<void>
}
}

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<void> {
await expect(page.locator('.trip-editor-save-state').filter({ hasText: /saved/i }).first()).toBeVisible();
}
Expand Down
7 changes: 3 additions & 4 deletions tests/e2e/trip-editor/tripEditorRemainingParity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 10 additions & 5 deletions tests/e2e/trip-editor/tripEditorRichNotes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -270,11 +269,11 @@ test.describe.serial('Trip Editor rich notes parity', () => {

await pasteDataImage(editor, '<p><img src=" DATA:IMAGE/png;base64,iVBORw0KGgo="></p>');
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, '<p><img src="\r\ndata : image/png;base64,iVBORw0KGgo="></p>');
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=');

Expand All @@ -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', '<script>alert("x")</script><a href="javascript:alert(1)" onclick="alert(2)">Unsafe link</a><img src="javascript:alert(3)" onerror="alert(4)">');
Expand Down Expand Up @@ -585,6 +584,12 @@ async function rejectImageDialogUrl(form: Locator, url: string): Promise<void> {
await dialog.getByRole('button', { name: 'Cancel' }).click();
}

async function expectNoDataImages(editor: Locator): Promise<void> {
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<void> {
const clickPoint = await editor.evaluate(element => {
element.scrollIntoView({ block: 'center', inline: 'nearest' });
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/trip-editor/tripEditorRichNotesPersistence.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
type EditorState = Record<string, any>;

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));
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/trip-editor/tripEditorSegmentEditing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions tests/e2e/trip-editor/tripEditorTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { id: string; name: string; isShadow: boolean }>;
regionsById: Record<string, { id: string; name: string; isShadow: boolean; capabilities?: { canEdit?: boolean } }>;
regionOrder: string[];
placesById: Record<string, { id: string; name: string; address: string; regionId: string }>;
placeOrderByRegionId: Record<string, string[]>;
Expand Down Expand Up @@ -113,7 +113,10 @@ export async function loadEditorStateFixture(page: Page): Promise<EditorTripFixt

// Derives stable sidebar search examples from the configured trip fixture.
export function sidebarSearchFixture(state: EditorTripFixture): SidebarSearchFixture {
const place = Object.values(state.placesById)[0];
const place = Object.values(state.placesById).find(candidate => {
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.');
}
Expand Down
17 changes: 14 additions & 3 deletions tests/e2e/trip-editor/tripEditorVisualPolish.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -233,6 +234,16 @@ async function openVisits(page: Page): Promise<void> {
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<void> {
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<void> {
const map = page.getByLabel('Read-only trip map');
await map.evaluate((element, point) => {
Expand Down
Loading