import { test, expect } from "@playwright/test"; test.describe("Planner", () => { test("loads the home page", async ({ page }) => { await page.goto("/"); await expect(page).toHaveTitle(/trails\.cool Planner/); await expect(page.getByRole("link", { name: "Start Planning" })).toBeVisible(); }); test("can create a session via API", async ({ request }) => { const response = await request.post("/api/sessions", { data: {} }); expect(response.ok()).toBeTruthy(); const body = await response.json(); expect(body.sessionId).toBeTruthy(); expect(body.url).toContain("/session/"); }); test("session page loads with map", async ({ page, request }) => { const response = await request.post("/api/sessions", { data: {} }); const { url } = await response.json(); await page.goto(url); // Title is now a home link await expect(page.getByRole("link", { name: "trails.cool Planner" })).toBeVisible(); await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); }); test("session shows connection status", async ({ page, request }) => { const response = await request.post("/api/sessions", { data: {} }); const { url } = await response.json(); await page.goto(url); await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); }); test("session has profile selector", async ({ page, request }) => { const response = await request.post("/api/sessions", { data: {} }); const { url } = await response.json(); await page.goto(url); await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); const profileSelect = page.getByLabel("Profile:"); await expect(profileSelect).toBeVisible(); await expect(profileSelect).toHaveValue("trekking"); }); test("session has export GPX button", async ({ page, request }) => { const response = await request.post("/api/sessions", { data: {} }); const { url } = await response.json(); await page.goto(url); await expect(page.getByRole("button", { name: "Export GPX" })).toBeVisible({ timeout: 10000 }); }); test("session shows empty waypoints sidebar", async ({ page, request }) => { const response = await request.post("/api/sessions", { data: {} }); const { url } = await response.json(); await page.goto(url); await expect(page.getByText("Waypoints (0)")).toBeVisible({ timeout: 10000 }); await expect(page.getByText("Click on the map to add waypoints")).toBeVisible(); }); test("session has sidebar tabs (waypoints and notes)", async ({ page, request }) => { const response = await request.post("/api/sessions", { data: {} }); const { url } = await response.json(); await page.goto(url); await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); // Waypoints tab is active by default await expect(page.getByRole("button", { name: "Waypoints" })).toBeVisible(); await expect(page.getByRole("button", { name: "Notes" })).toBeVisible(); // Switch to Notes tab await page.getByRole("button", { name: "Notes" }).click(); await expect(page.getByPlaceholder("Add notes for this session...")).toBeVisible(); // Switch back to Waypoints tab await page.getByRole("button", { name: "Waypoints" }).click(); await expect(page.getByText("Waypoints (0)")).toBeVisible(); }); test("clicking on route splits it by inserting a waypoint", async ({ page, request }) => { const sessionResp = await request.post("/api/sessions", { data: {} }); const { url } = await sessionResp.json(); await page.goto(`${url}?waypoints=${encodeURIComponent(JSON.stringify([ { lat: 52.520, lon: 13.405 }, { lat: 52.515, lon: 13.351 }, ]))}`); 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 }); await expect(page.getByText(/\d+\.\d+ km/)).toBeVisible({ timeout: 20000 }); // Zoom in and click on the route midpoint to split it // RouteInteraction uses map-level mousemove, but click on the ghost marker // inserts the waypoint. We'll use Leaflet events to simulate. await page.evaluate(() => { const map = (window as any).__leafletMap; if (!map) return; map.setView([52.5175, 13.378], 14, { animate: false }); }); await page.waitForTimeout(1000); // Click on the route via the visible polyline's bounding box const routePath = page.locator(".leaflet-overlay-pane path").first(); await expect(routePath).toBeAttached({ timeout: 5000 }); const box = await routePath.boundingBox(); if (!box) throw new Error("Route polyline not visible"); // Click the center of the route path bounding box await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); // The click on the map near the route should trigger RouteInteraction // which shows the ghost and inserts a waypoint. If the click was within // snap tolerance, a waypoint is inserted. Otherwise, a new waypoint is // appended (map click). Either way we get 3 waypoints. await expect(page.getByText("Waypoints (3)")).toBeVisible({ timeout: 10000 }); }); // Note: Ghost marker hover interaction is verified visually via cmux browser. // Playwright's mouse simulation doesn't reliably trigger Leaflet's SVG // pointer events needed for the distance-based snap detection. test("session has color mode toggle", async ({ page, request }) => { const response = await request.post("/api/sessions", { data: {} }); const { url } = await response.json(); await page.goto(url); await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); const select = page.getByTitle("Route Color"); await expect(select).toBeVisible(); await expect(select).toHaveValue("plain"); // Switch to elevation await select.selectOption("elevation"); await expect(select).toHaveValue("elevation"); }); test("session has no-go area button", async ({ page, request }) => { const response = await request.post("/api/sessions", { data: {} }); const { url } = await response.json(); await page.goto(url); await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); await expect(page.getByTitle("Draw no-go area")).toBeVisible(); }); test("can create session with initial waypoints", async ({ request }) => { const response = await request.post("/api/sessions", { data: { gpx: 'BerlinMunich', }, }); const body = await response.json(); expect(body.initialWaypoints).toHaveLength(2); expect(body.initialWaypoints[0].name).toBe("Berlin"); }); test("expired session returns 404", async ({ page }) => { const response = await page.goto("/session/nonexistent-id"); expect(response?.status()).toBe(404); }); });