Merge pull request #393 from trails-cool/stigi/waypoint-notes-e2e
Add E2E tests for waypoint notes + nearby POI snap; archive waypoint-notes
This commit is contained in:
commit
353dcb2c13
10 changed files with 164 additions and 3 deletions
|
|
@ -25,6 +25,7 @@ function getWaypoints(yjs: YjsState) {
|
|||
lon: yMap.get("lon") as number,
|
||||
name: yMap.get("name") as string | undefined,
|
||||
isDayBreak: yMap.get("overnight") === true ? true : undefined,
|
||||
note: yMap.get("note") as string | undefined,
|
||||
osmId: yMap.get("osmId") as number | undefined,
|
||||
poiTags: yMap.get("poiTags") as WaypointPoiTags | undefined,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export function useGpxDrop(yjs: YjsState, onImportError?: (message: string) => v
|
|||
yMap.set("lon", wp.lon);
|
||||
if (wp.name) yMap.set("name", wp.name);
|
||||
if (wp.isDayBreak) yMap.set("overnight", true);
|
||||
if (wp.note) yMap.set("note", wp.note);
|
||||
yjs.waypoints.push([yMap]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -469,4 +469,113 @@ test.describe("Planner", () => {
|
|||
const panelText = await page.getByText("Drinking water").textContent();
|
||||
expect(panelText).toBeTruthy();
|
||||
});
|
||||
|
||||
test("waypoint note roundtrips through GPX import and export", async ({ page, request }) => {
|
||||
const sessionResp = await request.post("/api/sessions", { data: {} });
|
||||
const { url } = await sessionResp.json();
|
||||
|
||||
await mockBRouter(page);
|
||||
|
||||
// Drop a GPX with a <desc> on a <wpt> onto the map
|
||||
const gpxWithNote = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
|
||||
<wpt lat="52.52" lon="13.405"><name>Berlin</name><desc>Refill water here</desc></wpt>
|
||||
<wpt lat="52.515" lon="13.351"><name>Spandau</name></wpt>
|
||||
<trk><trkseg>
|
||||
<trkpt lat="52.520" lon="13.405"><ele>34</ele></trkpt>
|
||||
<trkpt lat="52.515" lon="13.351"><ele>40</ele></trkpt>
|
||||
</trkseg></trk>
|
||||
</gpx>`;
|
||||
|
||||
await page.goto(url);
|
||||
await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 });
|
||||
|
||||
const dataTransfer = await page.evaluateHandle((content) => {
|
||||
const dt = new DataTransfer();
|
||||
const file = new File([content], "noted.gpx", { type: "application/gpx+xml" });
|
||||
dt.items.add(file);
|
||||
return dt;
|
||||
}, gpxWithNote);
|
||||
|
||||
const map = page.locator(".leaflet-container");
|
||||
await map.dispatchEvent("dragenter", { dataTransfer });
|
||||
await page.getByText("Drop GPX file here").waitFor({ timeout: 3000 });
|
||||
page.on("dialog", (dialog) => dialog.accept());
|
||||
await map.dispatchEvent("drop", { dataTransfer });
|
||||
|
||||
await expect(page.getByText("Waypoints (2)")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// The note should be visible in the sidebar
|
||||
const sidebar = page.locator("aside");
|
||||
await expect(sidebar.getByText("Refill water here")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Export as plan and verify the note is preserved
|
||||
await page.getByRole("button", { name: "▾" }).click();
|
||||
await expect(page.getByText("Export Plan")).toBeVisible({ timeout: 3000 });
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
await page.getByText("Export Plan").first().click();
|
||||
const download = await downloadPromise;
|
||||
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");
|
||||
|
||||
expect(gpxText).toContain("<desc>Refill water here</desc>");
|
||||
expect(gpxText.indexOf("<wpt")).toBeLessThan(gpxText.indexOf("<desc>Refill water here</desc>"));
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
## 6. Testing (Notes)
|
||||
|
||||
- [x] 6.1 Unit tests: per-waypoint `<desc>` in GPX generation; `<desc>` inside `<wpt>` 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 `<desc>` inside `<wpt>`
|
||||
- [x] 6.2 E2E test: type a note in sidebar, blur, verify note persists; verify GPX export contains `<desc>` inside `<wpt>`
|
||||
|
||||
## 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
|
||||
50
openspec/specs/waypoint-notes/spec.md
Normal file
50
openspec/specs/waypoint-notes/spec.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
## Purpose
|
||||
|
||||
Per-waypoint plain-text notes in the Planner, with nearby POI discovery and snap-to-POI functionality.
|
||||
|
||||
## 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 `<desc>` elements in GPX output.
|
||||
|
||||
#### Scenario: Export notes
|
||||
- **WHEN** a user exports a plan with waypoint notes
|
||||
- **THEN** each waypoint's note appears as a `<desc>` 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")
|
||||
|
|
@ -5,7 +5,7 @@ import type { GpxData } from "./types.ts";
|
|||
* Uses explicit <wpt> elements if present, otherwise simplifies the
|
||||
* track using Douglas-Peucker to find significant turning points.
|
||||
*/
|
||||
export function extractWaypoints(gpxData: GpxData): Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean }> {
|
||||
export function extractWaypoints(gpxData: GpxData): Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean; note?: string }> {
|
||||
if (gpxData.waypoints.length > 0) return gpxData.waypoints;
|
||||
if (gpxData.tracks.length === 0) return [];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue