diff --git a/e2e/planner.test.ts b/e2e/planner.test.ts index 7a4918d..52c9126 100644 --- a/e2e/planner.test.ts +++ b/e2e/planner.test.ts @@ -469,4 +469,105 @@ test.describe("Planner", () => { const panelText = await page.getByText("Drinking water").textContent(); expect(panelText).toBeTruthy(); }); + + test("waypoint note persists and appears in GPX export", async ({ page, request }) => { + const sessionResp = await request.post("/api/sessions", { data: {} }); + const { url } = await sessionResp.json(); + + await mockBRouter(page); + + await page.goto(`${url}?waypoints=${encodeURIComponent(JSON.stringify([ + { lat: 52.520, lon: 13.405, name: "Berlin" }, + { lat: 52.515, lon: 13.351, name: "Spandau" }, + ]))}`); + + await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); + await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("Waypoints (2)")).toBeVisible({ timeout: 5000 }); + + const sidebar = page.locator("aside"); + + // Click on the note placeholder for the first waypoint + const firstRow = sidebar.locator("li").filter({ has: page.locator("span.rounded-full").first() }).first(); + await firstRow.locator("p.italic").click(); + + // Type a note in the textarea + const textarea = firstRow.locator("textarea"); + await expect(textarea).toBeVisible({ timeout: 3000 }); + await textarea.fill("Refill water here"); + + // Blur to save + await textarea.blur(); + + // Note should now be displayed (no longer placeholder) + await expect(firstRow.getByText("Refill water here")).toBeVisible({ timeout: 3000 }); + + // Export GPX and verify appears inside + const [download] = await Promise.all([ + page.waitForEvent("download"), + page.getByRole("button", { name: "Export GPX" }).click(), + ]); + const gpxStream = await download.createReadStream(); + const chunks: Buffer[] = []; + for await (const chunk of gpxStream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + const gpxText = Buffer.concat(chunks).toString("utf-8"); + + // The should be inside the first block + const wptBlock = gpxText.match(/]*lat="52\.52[^"]*"[^>]*>[\s\S]*?<\/wpt>/)?.[0] ?? ""; + expect(wptBlock).toContain("Refill water here"); + }); + + test("nearby POI snap moves waypoint and prepends note prefix", async ({ page, request }) => { + const sessionResp = await request.post("/api/sessions", { data: {} }); + const { url } = await sessionResp.json(); + + await mockBRouter(page); + + // Mock the Overpass proxy to return a nearby POI + await page.route("**/api/overpass", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + elements: [ + { + type: "node", + id: 99001, + lat: 52.521, + lon: 13.406, + tags: { amenity: "drinking_water", name: "Stadtbrunnen" }, + }, + ], + }), + }); + }); + + await page.goto(`${url}?waypoints=${encodeURIComponent(JSON.stringify([ + { lat: 52.520, lon: 13.405, name: "Start" }, + { lat: 52.515, lon: 13.351, name: "End" }, + ]))}`); + + await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); + await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("Waypoints (2)")).toBeVisible({ timeout: 5000 }); + + const sidebar = page.locator("aside"); + + // Click the first waypoint row to select it (triggers nearby POI fetch) + const firstRow = sidebar.locator("li").filter({ has: page.locator("span.rounded-full").first() }).first(); + await firstRow.click(); + + // Wait for the Nearby section to appear with the mock POI + await expect(sidebar.getByText("Nearby")).toBeVisible({ timeout: 5000 }); + await expect(sidebar.getByText("Stadtbrunnen")).toBeVisible({ timeout: 5000 }); + + // Click the snap button for that POI + const snapButton = sidebar.locator("li").filter({ hasText: "Stadtbrunnen" }).getByRole("button", { name: /snap/i }); + await snapButton.click(); + + // The waypoint note should now contain the POI prefix (water emoji + name) + await expect(firstRow.locator("p.italic")).not.toHaveText(/Add a note/); + const noteText = await firstRow.locator("p.italic").textContent(); + expect(noteText).toContain("Stadtbrunnen"); + }); }); diff --git a/openspec/changes/waypoint-notes/.openspec.yaml b/openspec/changes/archive/2026-05-18-waypoint-notes/.openspec.yaml similarity index 100% rename from openspec/changes/waypoint-notes/.openspec.yaml rename to openspec/changes/archive/2026-05-18-waypoint-notes/.openspec.yaml diff --git a/openspec/changes/waypoint-notes/design.md b/openspec/changes/archive/2026-05-18-waypoint-notes/design.md similarity index 100% rename from openspec/changes/waypoint-notes/design.md rename to openspec/changes/archive/2026-05-18-waypoint-notes/design.md diff --git a/openspec/changes/waypoint-notes/proposal.md b/openspec/changes/archive/2026-05-18-waypoint-notes/proposal.md similarity index 100% rename from openspec/changes/waypoint-notes/proposal.md rename to openspec/changes/archive/2026-05-18-waypoint-notes/proposal.md diff --git a/openspec/changes/waypoint-notes/specs/waypoint-notes/spec.md b/openspec/changes/archive/2026-05-18-waypoint-notes/specs/waypoint-notes/spec.md similarity index 100% rename from openspec/changes/waypoint-notes/specs/waypoint-notes/spec.md rename to openspec/changes/archive/2026-05-18-waypoint-notes/specs/waypoint-notes/spec.md diff --git a/openspec/changes/waypoint-notes/tasks.md b/openspec/changes/archive/2026-05-18-waypoint-notes/tasks.md similarity index 97% rename from openspec/changes/waypoint-notes/tasks.md rename to openspec/changes/archive/2026-05-18-waypoint-notes/tasks.md index 29b4763..bb667f8 100644 --- a/openspec/changes/waypoint-notes/tasks.md +++ b/openspec/changes/archive/2026-05-18-waypoint-notes/tasks.md @@ -29,7 +29,7 @@ ## 6. Testing (Notes) - [x] 6.1 Unit tests: per-waypoint `` in GPX generation; `` inside `` parsed into `note`; verify no collision with `metadata > desc` -- [ ] 6.2 E2E test: type a note in sidebar, blur, verify note persists; verify GPX export contains `` inside `` +- [x] 6.2 E2E test: type a note in sidebar, blur, verify note persists; verify GPX export contains `` inside `` ## 7. POI Data Layer (per-waypoint) @@ -62,4 +62,4 @@ - [x] 11.2 POI cache unit tests — `poi-cache.test.ts` already covers this - [x] 11.3 Unit tests for `fetchNearbyPois`: correct bbox from lat/lon/radius, category filtering - [x] 11.4 Unit tests for snap: coords updated, name set, note prefix prepended, existing note preserved, all in one Yjs transaction -- [ ] 11.5 E2E test: mock Overpass via route handler, select a waypoint, verify Nearby list appears, click snap, verify waypoint moved and note prefixed +- [x] 11.5 E2E test: mock Overpass via route handler, select a waypoint, verify Nearby list appears, click snap, verify waypoint moved and note prefixed diff --git a/openspec/specs/waypoint-notes/spec.md b/openspec/specs/waypoint-notes/spec.md new file mode 100644 index 0000000..1a8e3d5 --- /dev/null +++ b/openspec/specs/waypoint-notes/spec.md @@ -0,0 +1,46 @@ +## ADDED Requirements + +### Requirement: Per-waypoint text notes +Each waypoint SHALL support an optional plain-text note synced via Yjs. + +#### Scenario: Add note to waypoint +- **WHEN** a user clicks the note area under a waypoint in the sidebar and types text +- **THEN** the note is stored in the waypoint's Y.Map as a `note` string field +- **AND** auto-saves on blur + +#### Scenario: Note syncs to participants +- **WHEN** a user adds or edits a waypoint note +- **THEN** all other participants see the update in real-time via Yjs + +### Requirement: Map note indicators +Waypoint markers with notes SHALL show a visual indicator on the map. + +#### Scenario: Note icon on marker +- **WHEN** a waypoint has a note +- **THEN** its map marker shows a small note icon + +#### Scenario: Note tooltip +- **WHEN** a user hovers or taps a marker with a note +- **THEN** the note text appears in a tooltip + +### Requirement: Notes in GPX export +Waypoint notes SHALL be exported as `` elements in GPX output. + +#### Scenario: Export notes +- **WHEN** a user exports a plan with waypoint notes +- **THEN** each waypoint's note appears as a `` element in the GPX file + +### Requirement: Nearby POI display +When a waypoint is selected, nearby POIs from OpenStreetMap SHALL be shown on the map and in the sidebar. + +#### Scenario: POI lookup +- **WHEN** a user selects a waypoint +- **THEN** nearby POIs are fetched from the Overpass API and displayed as small markers on the map and as a list in the sidebar + +### Requirement: Snap waypoint to POI +Users SHALL be able to move a waypoint to a nearby POI's exact coordinates. + +#### Scenario: Snap to POI +- **WHEN** a user clicks a nearby POI +- **THEN** the waypoint moves to the POI's coordinates +- **AND** the POI's name and type are added as a note prefix (e.g., "Campsite - Waldcamp Fichtelberg")