From 0b146f9b3244df0281c2d9156d575eb70945821e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Fri, 22 May 2026 21:58:10 +0200 Subject: [PATCH] Split planner E2E tests into focused feature files Breaks the monolithic planner.test.ts (581 lines, 25 tests) into five focused files grouped by feature area, with BRouter mocked by default via test.beforeEach in files that need routing. Co-Authored-By: Claude Sonnet 4.6 --- e2e/helpers/planner.ts | 12 + e2e/planner-coloring.test.ts | 71 +++ e2e/planner-multiday.test.ts | 71 +++ e2e/planner-overlays.test.ts | 72 +++ e2e/planner-routing.test.ts | 153 +++++ e2e/planner-session.test.ts | 132 ++++ e2e/planner.test.ts | 581 ------------------ .../.openspec.yaml | 0 .../design.md | 41 ++ .../proposal.md | 0 .../specs/e2e-testing/spec.md | 0 .../tasks.md | 15 + openspec/specs/e2e-testing/spec.md | 16 + playwright.config.ts | 2 +- 14 files changed, 584 insertions(+), 582 deletions(-) create mode 100644 e2e/helpers/planner.ts create mode 100644 e2e/planner-coloring.test.ts create mode 100644 e2e/planner-multiday.test.ts create mode 100644 e2e/planner-overlays.test.ts create mode 100644 e2e/planner-routing.test.ts create mode 100644 e2e/planner-session.test.ts delete mode 100644 e2e/planner.test.ts rename openspec/changes/{e2e-test-reorganization => archive/2026-05-22-e2e-test-reorganization}/.openspec.yaml (100%) create mode 100644 openspec/changes/archive/2026-05-22-e2e-test-reorganization/design.md rename openspec/changes/{e2e-test-reorganization => archive/2026-05-22-e2e-test-reorganization}/proposal.md (100%) rename openspec/changes/{e2e-test-reorganization => archive/2026-05-22-e2e-test-reorganization}/specs/e2e-testing/spec.md (100%) create mode 100644 openspec/changes/archive/2026-05-22-e2e-test-reorganization/tasks.md create mode 100644 openspec/specs/e2e-testing/spec.md diff --git a/e2e/helpers/planner.ts b/e2e/helpers/planner.ts new file mode 100644 index 0000000..1b81289 --- /dev/null +++ b/e2e/helpers/planner.ts @@ -0,0 +1,12 @@ +import type { APIRequestContext, Page } from "@playwright/test"; + +export async function createSession(request: APIRequestContext): Promise { + const response = await request.post("/api/sessions", { data: {} }); + const { url } = await response.json(); + return url; +} + +export async function openSession(page: Page, url: string): Promise { + await page.goto(url); + await page.getByText("Connected").waitFor({ timeout: 15000 }); +} diff --git a/e2e/planner-coloring.test.ts b/e2e/planner-coloring.test.ts new file mode 100644 index 0000000..1906fd0 --- /dev/null +++ b/e2e/planner-coloring.test.ts @@ -0,0 +1,71 @@ +import { test, expect } from "./fixtures/test"; +import { mockBRouter } from "./fixtures/brouter-mock"; +import { createSession } from "./helpers/planner"; + +test.beforeEach(async ({ page }) => { + await mockBRouter(page); +}); + +const WITH_ROUTE = encodeURIComponent(JSON.stringify([ + { lat: 52.520, lon: 13.405 }, + { lat: 52.515, lon: 13.351 }, +])); + +test.describe("Planner – route coloring", () => { + test("session has color mode toggle", async ({ page, request }) => { + const url = await createSession(request); + + await page.goto(`${url}?waypoints=${WITH_ROUTE}`); + await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); + await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); + + await expect(page.locator("canvas")).toBeVisible({ timeout: 10000 }); + const select = page.locator("select", { has: page.locator("option[value='highway']") }); + await expect(select).toBeVisible({ timeout: 5000 }); + await expect(select).toHaveValue("plain"); + + await select.selectOption("elevation"); + await expect(select).toHaveValue("elevation"); + }); + + test("road type color mode renders chart and shows legend", async ({ page, request }) => { + const url = await createSession(request); + + await page.goto(`${url}?waypoints=${WITH_ROUTE}`); + await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); + await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); + + await expect(page.locator("canvas")).toBeVisible({ timeout: 10000 }); + const select = page.locator("select", { has: page.locator("option[value='highway']") }); + await expect(select).toBeVisible({ timeout: 5000 }); + + await select.selectOption("highway"); + await expect(select).toHaveValue("highway"); + + await expect(page.getByText("Road Type Profile")).toBeVisible({ timeout: 5000 }); + await expect(page.getByText("secondary")).toBeVisible({ timeout: 5000 }); + await expect(page.getByText("residential")).toBeVisible({ timeout: 5000 }); + }); + + test("road type hover label shows highway type", async ({ page, request }) => { + const url = await createSession(request); + + await page.goto(`${url}?waypoints=${WITH_ROUTE}`); + await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); + await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); + + await expect(page.locator("canvas")).toBeVisible({ timeout: 10000 }); + const select = page.locator("select", { has: page.locator("option[value='highway']") }); + await expect(select).toBeVisible({ timeout: 5000 }); + await select.selectOption("highway"); + + const canvas = page.locator("canvas"); + await expect(canvas).toBeVisible({ timeout: 5000 }); + const box = await canvas.boundingBox(); + if (box) { + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + } + + await expect(canvas).toBeVisible(); + }); +}); diff --git a/e2e/planner-multiday.test.ts b/e2e/planner-multiday.test.ts new file mode 100644 index 0000000..c664e3f --- /dev/null +++ b/e2e/planner-multiday.test.ts @@ -0,0 +1,71 @@ +import { test, expect } from "./fixtures/test"; +import { mockBRouter } from "./fixtures/brouter-mock"; +import { createSession, openSession } from "./helpers/planner"; + +test.beforeEach(async ({ page }) => { + await mockBRouter(page); +}); + +test.describe("Planner – multi-day routes", () => { + test("toggle overnight on waypoint shows day breakdown in sidebar", async ({ page, request }) => { + const url = await createSession(request); + + await page.goto(`${url}?waypoints=${encodeURIComponent(JSON.stringify([ + { lat: 52.520, lon: 13.405 }, + { lat: 52.516, lon: 13.377 }, + { lat: 52.510, lon: 13.390 }, + ]))}`); + + await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); + await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("Waypoints (3)")).toBeVisible({ timeout: 15000 }); + + const sidebar = page.locator("aside"); + await expect(sidebar.getByText(/\d+\.\d+ km/).first()).toBeVisible({ timeout: 15000 }); + + const waypointRows = sidebar.locator("li").filter({ has: page.locator("span.rounded-full") }); + const secondRow = waypointRows.nth(1); + await secondRow.hover(); + const moonButton = secondRow.getByTitle(/overnight/i); + await moonButton.click(); + + await expect(sidebar.getByText("Day 1")).toBeVisible({ timeout: 5000 }); + await expect(sidebar.getByText("Day 2")).toBeVisible({ timeout: 5000 }); + }); + + test("export GPX with day breaks includes overnight metadata", async ({ page, request }) => { + const url = await createSession(request); + + const gpx = ` + + Berlin + Mitteovernight + Kreuzberg + + 34 + 40 + 35 + +`; + + await openSession(page, url); + + const dataTransfer = await page.evaluateHandle((content) => { + const dt = new DataTransfer(); + const file = new File([content], "multi-day.gpx", { type: "application/gpx+xml" }); + dt.items.add(file); + return dt; + }, gpx); + + 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 (3)")).toBeVisible({ timeout: 10000 }); + + const sidebar = page.locator("aside"); + await expect(sidebar.getByText("Day 1")).toBeVisible({ timeout: 5000 }); + }); +}); diff --git a/e2e/planner-overlays.test.ts b/e2e/planner-overlays.test.ts new file mode 100644 index 0000000..d863a5f --- /dev/null +++ b/e2e/planner-overlays.test.ts @@ -0,0 +1,72 @@ +import { test, expect } from "./fixtures/test"; +import { createSession } from "./helpers/planner"; + +test.describe("Planner – overlays", () => { + test("enable hillshading overlay loads tiles", async ({ page, request }) => { + const url = await createSession(request); + + const hillshadingRequests: string[] = []; + await page.route("**/tiles.wmflabs.org/hillshading/**", async (route) => { + hillshadingRequests.push(route.request().url()); + await route.fulfill({ + status: 200, + contentType: "image/png", + body: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12NgAAIABQABNjN9GQAAAABJRUEFTuQmCC", "base64"), + }); + }); + + await page.goto(url); + await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); + + await page.evaluate(() => { + const inputs = document.querySelectorAll(".leaflet-control-layers-overlays input"); + if (inputs[0]) inputs[0].click(); + }); + + await page.waitForTimeout(2000); + expect(hillshadingRequests.length).toBeGreaterThan(0); + }); + + test("enable POI category shows markers on map", async ({ page, request }) => { + const url = await createSession(request); + + await page.route("**/api/overpass", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + elements: [ + { + type: "node", + id: 12345, + lat: 52.52, + lon: 13.405, + tags: { amenity: "drinking_water", name: "Test Brunnen" }, + }, + ], + }), + }); + }); + + await page.goto(url); + await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); + await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); + + await page.evaluate(() => { + const map = (window as any).__leafletMap; + if (map) map.setView([52.52, 13.405], 14, { animate: false }); + }); + await page.waitForTimeout(500); + + await page.getByTitle("Points of Interest").click(); + await page.getByText("Drinking water").click(); + + await page.waitForTimeout(3000); + + const markerCount = await page.locator(".leaflet-marker-pane .leaflet-marker-icon").count(); + expect(markerCount).toBeGreaterThan(0); + + const panelText = await page.getByText("Drinking water").textContent(); + expect(panelText).toBeTruthy(); + }); +}); diff --git a/e2e/planner-routing.test.ts b/e2e/planner-routing.test.ts new file mode 100644 index 0000000..2b0077e --- /dev/null +++ b/e2e/planner-routing.test.ts @@ -0,0 +1,153 @@ +import { test, expect } from "./fixtures/test"; +import { mockBRouter, latLngToPixel } from "./fixtures/brouter-mock"; +import { createSession, openSession } from "./helpers/planner"; + +test.beforeEach(async ({ page }) => { + await mockBRouter(page); +}); + +test.describe("Planner – routing", () => { + test("clicking on route splits it by inserting a waypoint", async ({ page, request }) => { + const url = await createSession(request); + + 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 }); + + const sidebar = page.locator("aside"); + await expect(sidebar.getByText(/\d+\.\d+ km/).first()).toBeVisible({ timeout: 10000 }); + + await page.evaluate(() => { + const map = (window as any).__leafletMap; + if (!map) return; + map.setView([52.518, 13.383], 15, { animate: false }); + }); + await page.waitForTimeout(500); + + const pixel = await latLngToPixel(page, 52.518, 13.383); + await page.mouse.click(pixel.x, pixel.y); + + await expect(page.getByText("Waypoints (3)")).toBeVisible({ timeout: 10000 }); + }); + + test("map zooms to fit the route when opened with initial waypoints", async ({ page, request }) => { + const url = await createSession(request); + + const waypoints = [ + { lat: 52.520, lon: 13.405 }, + { lat: 52.515, lon: 13.351 }, + ]; + await page.goto(`${url}?waypoints=${encodeURIComponent(JSON.stringify(waypoints))}`); + + await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); + await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); + + const sidebar = page.locator("aside"); + await expect(sidebar.getByText(/\d+\.\d+ km/).first()).toBeVisible({ timeout: 10000 }); + + const zoom = await page.evaluate(() => { + const map = (window as any).__leafletMap; + return map ? map.getZoom() : null; + }); + expect(zoom).not.toBeNull(); + expect(zoom).toBeGreaterThan(6); + }); + + test("waypoint note roundtrips through GPX import and export", async ({ page, request }) => { + const url = await createSession(request); + + const gpxWithNote = ` + + BerlinRefill water here + Spandau + + 34 + 40 + +`; + + await openSession(page, url); + + 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 }); + + const sidebar = page.locator("aside"); + await expect(sidebar.getByText("Refill water here")).toBeVisible({ timeout: 5000 }); + + 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("Refill water here"); + expect(gpxText.indexOf("Refill water here")); + }); + + test("nearby POI snap moves waypoint and prepends note prefix", async ({ page, request }) => { + const url = await createSession(request); + + 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"); + + const firstRow = sidebar.locator("li").filter({ has: page.locator("span.rounded-full").first() }).first(); + await firstRow.click(); + + await expect(sidebar.getByText("Nearby")).toBeVisible({ timeout: 5000 }); + await expect(sidebar.getByText("Stadtbrunnen")).toBeVisible({ timeout: 5000 }); + + const snapButton = sidebar.locator("li").filter({ hasText: "Stadtbrunnen" }).getByRole("button", { name: /snap/i }); + await snapButton.click(); + + 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/e2e/planner-session.test.ts b/e2e/planner-session.test.ts new file mode 100644 index 0000000..7b53d75 --- /dev/null +++ b/e2e/planner-session.test.ts @@ -0,0 +1,132 @@ +import { test, expect } from "./fixtures/test"; +import { createSession, openSession } from "./helpers/planner"; + +test.describe("Planner – session", () => { + 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 url = await createSession(request); + await page.goto(url); + 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 url = await createSession(request); + await page.goto(url); + await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); + }); + + test("session has profile selector", async ({ page, request }) => { + const url = await createSession(request); + 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("fastbike"); + }); + + test("session has export GPX button", async ({ page, request }) => { + const url = await createSession(request); + 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 url = await createSession(request); + 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 url = await createSession(request); + await page.goto(url); + await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); + + await expect(page.getByRole("button", { name: "Waypoints" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Notes" })).toBeVisible(); + + await page.getByRole("button", { name: "Notes" }).click(); + await expect(page.getByText("Add notes for this session...")).toBeVisible(); + + await page.getByRole("button", { name: "Waypoints" }).click(); + await expect(page.getByText("Waypoints (0)")).toBeVisible(); + }); + + test("session has no-go area button", async ({ page, request }) => { + const url = await createSession(request); + 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 via API", 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); + }); + + test("home page has Import GPX button", async ({ page }) => { + await page.goto("/"); + await expect(page.getByText("Import GPX")).toBeVisible(); + }); + + test("import invalid GPX shows error", async ({ page }) => { + await page.goto("/"); + const fileChooserPromise = page.waitForEvent("filechooser"); + await page.getByText("Import GPX").click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles({ + name: "broken.gpx", + mimeType: "application/gpx+xml", + buffer: Buffer.from("not valid xml at all"), + }); + await expect(page.getByText(/Could not read|konnte nicht/)).toBeVisible({ timeout: 5000 }); + await expect(page).toHaveURL(/^\/$|\/$/); + }); + + test("import GPX from home page creates session with waypoints", async ({ page }) => { + await page.goto("/"); + const gpx = ` + + + 34 + 113 + 519 + +`; + const fileChooserPromise = page.waitForEvent("filechooser"); + await page.getByText("Import GPX").click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles({ + name: "test-route.gpx", + mimeType: "application/gpx+xml", + buffer: Buffer.from(gpx), + }); + await expect(page).toHaveURL(/\/session\//, { timeout: 10000 }); + await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); + }); +}); diff --git a/e2e/planner.test.ts b/e2e/planner.test.ts deleted file mode 100644 index b60b826..0000000 --- a/e2e/planner.test.ts +++ /dev/null @@ -1,581 +0,0 @@ -import { test, expect } from "./fixtures/test"; -import { mockBRouter, latLngToPixel } from "./fixtures/brouter-mock"; - -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("fastbike"); - }); - - 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(); - // CodeMirror editor should be visible with the placeholder text - await expect(page.getByText("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 mockBRouter(page); - - 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 }); - - const sidebar = page.locator("aside"); - await expect(sidebar.getByText(/\d+\.\d+ km/).first()).toBeVisible({ timeout: 10000 }); - - // Zoom in to the route midpoint using known coordinates - await page.evaluate(() => { - const map = (window as any).__leafletMap; - if (!map) return; - map.setView([52.518, 13.383], 15, { animate: false }); - }); - await page.waitForTimeout(500); - - // Click on a known route coordinate to insert a waypoint - const pixel = await latLngToPixel(page, 52.518, 13.383); - await page.mouse.click(pixel.x, pixel.y); - - // Should now have 3 waypoints (original 2 + inserted) - 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 sessionResp = await request.post("/api/sessions", { data: {} }); - const { url } = await sessionResp.json(); - - await mockBRouter(page); - - // Need a route for the elevation chart (and color selector) to appear - 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 }); - - // Wait for elevation chart to render with the color mode selector - await expect(page.locator("canvas")).toBeVisible({ timeout: 10000 }); - const select = page.locator("select", { has: page.locator("option[value='highway']") }); - await expect(select).toBeVisible({ timeout: 5000 }); - 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("map zooms to fit the route when opened with initial waypoints", async ({ page, request }) => { - const sessionResp = await request.post("/api/sessions", { data: {} }); - const { url } = await sessionResp.json(); - - await mockBRouter(page); - - const waypoints = [ - { lat: 52.520, lon: 13.405 }, - { lat: 52.515, lon: 13.351 }, - ]; - await page.goto(`${url}?waypoints=${encodeURIComponent(JSON.stringify(waypoints))}`); - - await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); - await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); - - const sidebar = page.locator("aside"); - await expect(sidebar.getByText(/\d+\.\d+ km/).first()).toBeVisible({ timeout: 10000 }); - - // The map should have zoomed in to the route bounds (zoom > default 6) - const zoom = await page.evaluate(() => { - const map = (window as any).__leafletMap; - return map ? map.getZoom() : null; - }); - expect(zoom).not.toBeNull(); - expect(zoom).toBeGreaterThan(6); - }); - - 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); - }); - - test("home page has Import GPX button", async ({ page }) => { - await page.goto("/"); - await expect(page.getByText("Import GPX")).toBeVisible(); - }); - - test("import invalid GPX shows error", async ({ page }) => { - await page.goto("/"); - const fileChooserPromise = page.waitForEvent("filechooser"); - await page.getByText("Import GPX").click(); - const fileChooser = await fileChooserPromise; - await fileChooser.setFiles({ - name: "broken.gpx", - mimeType: "application/gpx+xml", - buffer: Buffer.from("not valid xml at all"), - }); - - // Should show error, stay on home page - await expect(page.getByText(/Could not read|konnte nicht/)).toBeVisible({ timeout: 5000 }); - await expect(page).toHaveURL(/^\/$|\/$/); - }); - - test("import GPX from home page creates session with waypoints", async ({ page }) => { - await page.goto("/"); - const gpx = ` - - - 34 - 113 - 519 - -`; - const fileChooserPromise = page.waitForEvent("filechooser"); - await page.getByText("Import GPX").click(); - const fileChooser = await fileChooserPromise; - await fileChooser.setFiles({ - name: "test-route.gpx", - mimeType: "application/gpx+xml", - buffer: Buffer.from(gpx), - }); - - // Should redirect to a session - await expect(page).toHaveURL(/\/session\//, { timeout: 10000 }); - await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); - }); - - test("toggle overnight on waypoint shows day breakdown in sidebar", 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 }, - { lat: 52.516, lon: 13.377 }, - { lat: 52.510, lon: 13.390 }, - ]))}`); - - await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); - await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); - await expect(page.getByText("Waypoints (3)")).toBeVisible({ timeout: 15000 }); - - const sidebar = page.locator("aside"); - await expect(sidebar.getByText(/\d+\.\d+ km/).first()).toBeVisible({ timeout: 15000 }); - - // Hover waypoint 2 to reveal controls, click the overnight toggle (moon icon) - const waypointRows = sidebar.locator("li").filter({ has: page.locator("span.rounded-full") }); - const secondRow = waypointRows.nth(1); - await secondRow.hover(); - const moonButton = secondRow.getByTitle(/overnight/i); - await moonButton.click(); - - // Day breakdown should appear in the sidebar - await expect(sidebar.getByText("Day 1")).toBeVisible({ timeout: 5000 }); - await expect(sidebar.getByText("Day 2")).toBeVisible({ timeout: 5000 }); - }); - - test("export GPX with day breaks includes overnight metadata", async ({ page, request }) => { - const sessionResp = await request.post("/api/sessions", { data: {} }); - const { url } = await sessionResp.json(); - - await mockBRouter(page); - - // Import a GPX with overnight waypoints - const gpx = ` - - Berlin - Mitteovernight - Kreuzberg - - 34 - 40 - 35 - -`; - - await page.goto(url); - await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); - await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); - - // Drop the GPX file onto the map - const dataTransfer = await page.evaluateHandle((content) => { - const dt = new DataTransfer(); - const file = new File([content], "multi-day.gpx", { type: "application/gpx+xml" }); - dt.items.add(file); - return dt; - }, gpx); - - 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 }); - - // Wait for waypoints to load - await expect(page.getByText("Waypoints (3)")).toBeVisible({ timeout: 10000 }); - - // The overnight waypoint should show day breakdown in the sidebar - const sidebar = page.locator("aside"); - await expect(sidebar.getByText("Day 1")).toBeVisible({ timeout: 5000 }); - }); - - test("enable hillshading overlay loads tiles", async ({ page, request }) => { - const sessionResp = await request.post("/api/sessions", { data: {} }); - const { url } = await sessionResp.json(); - - // Track hillshading tile requests - const hillshadingRequests: string[] = []; - await page.route("**/tiles.wmflabs.org/hillshading/**", async (route) => { - hillshadingRequests.push(route.request().url()); - await route.fulfill({ - status: 200, - contentType: "image/png", - body: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12NgAAIABQABNjN9GQAAAABJRUEFTuQmCC", "base64"), - }); - }); - - await page.goto(url); - await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); - - // Enable hillshading via DOM — Leaflet's layers control hover is hard to automate - await page.evaluate(() => { - const inputs = document.querySelectorAll(".leaflet-control-layers-overlays input"); - if (inputs[0]) inputs[0].click(); - }); - - await page.waitForTimeout(2000); - expect(hillshadingRequests.length).toBeGreaterThan(0); - }); - - test("road type color mode renders chart and shows legend", 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 }, - { lat: 52.515, lon: 13.351 }, - ]))}`); - - await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); - await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); - - // Wait for the elevation chart to render, then find the color mode select (has a "highway" option) - await expect(page.locator("canvas")).toBeVisible({ timeout: 10000 }); - const select = page.locator("select", { has: page.locator("option[value='highway']") }); - await expect(select).toBeVisible({ timeout: 5000 }); - - // Switch to highway (road type) mode - await select.selectOption("highway"); - await expect(select).toHaveValue("highway"); - - // Chart title should show "Road Type Profile" - await expect(page.getByText("Road Type Profile")).toBeVisible({ timeout: 5000 }); - - // Legend should show highway types from mock data - await expect(page.getByText("secondary")).toBeVisible({ timeout: 5000 }); - await expect(page.getByText("residential")).toBeVisible({ timeout: 5000 }); - }); - - test("road type hover label shows highway type", 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 }, - { lat: 52.515, lon: 13.351 }, - ]))}`); - - await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); - await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); - - // Switch to highway mode - await expect(page.locator("canvas")).toBeVisible({ timeout: 10000 }); - const select = page.locator("select", { has: page.locator("option[value='highway']") }); - await expect(select).toBeVisible({ timeout: 5000 }); - await select.selectOption("highway"); - - // Hover the canvas to trigger the hover label - const canvas = page.locator("canvas"); - await expect(canvas).toBeVisible({ timeout: 5000 }); - const box = await canvas.boundingBox(); - if (box) { - // Hover near the middle of the chart - await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); - } - - // The hover label is drawn on canvas — we can't assert text directly, - // but we verify no errors occurred and the chart re-renders on hover. - // The canvas should still be visible (no crash). - await expect(canvas).toBeVisible(); - }); - - test("enable POI category shows markers on map", async ({ page, request }) => { - const sessionResp = await request.post("/api/sessions", { data: {} }); - const { url } = await sessionResp.json(); - - // The browser queries POIs through the planner's `/api/overpass` - // proxy (not the upstream Overpass directly), so that's what we - // intercept here. Response body is the same Overpass JSON shape. - await page.route("**/api/overpass", async (route) => { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - elements: [ - { - type: "node", - id: 12345, - lat: 52.52, - lon: 13.405, - tags: { amenity: "drinking_water", name: "Test Brunnen" }, - }, - ], - }), - }); - }); - - await page.goto(url); - await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 }); - await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 }); - - // Zoom in to Berlin center to meet zoom threshold and see the mock POI - await page.evaluate(() => { - const map = (window as any).__leafletMap; - if (map) map.setView([52.52, 13.405], 14, { animate: false }); - }); - await page.waitForTimeout(500); - - // Open POI panel and enable Drinking water - await page.getByTitle("Points of Interest").click(); - await page.getByText("Drinking water").click(); - - // Wait for mock Overpass response and marker rendering - await page.waitForTimeout(3000); - - // Verify POI marker rendered — check for marker with the water emoji in the pane - const markerCount = await page.locator(".leaflet-marker-pane .leaflet-marker-icon").count(); - // Should have at least one POI marker (beyond any waypoint markers) - expect(markerCount).toBeGreaterThan(0); - - // Verify the Overpass mock was actually called by checking the POI panel count - 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 on a onto the map - const gpxWithNote = ` - - BerlinRefill water here - Spandau - - 34 - 40 - -`; - - 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("Refill water here"); - expect(gpxText.indexOf("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/e2e-test-reorganization/.openspec.yaml b/openspec/changes/archive/2026-05-22-e2e-test-reorganization/.openspec.yaml similarity index 100% rename from openspec/changes/e2e-test-reorganization/.openspec.yaml rename to openspec/changes/archive/2026-05-22-e2e-test-reorganization/.openspec.yaml diff --git a/openspec/changes/archive/2026-05-22-e2e-test-reorganization/design.md b/openspec/changes/archive/2026-05-22-e2e-test-reorganization/design.md new file mode 100644 index 0000000..548258f --- /dev/null +++ b/openspec/changes/archive/2026-05-22-e2e-test-reorganization/design.md @@ -0,0 +1,41 @@ +## Approach + +Split `e2e/planner.test.ts` (581 lines, 42+ tests) into 5 feature-focused files. Extract a shared helper for the common create-session + navigate + wait-connected pattern. Make BRouter mocked by default in files where routing is needed via `test.beforeEach`. + +## File Map + +### New files + +| File | Tests | BRouter | +|------|-------|---------| +| `e2e/planner-session.test.ts` | Home page, session creation, basic session UI, GPX import | No mock needed | +| `e2e/planner-routing.test.ts` | Route splitting, zoom to fit, waypoint notes, POI snap | `beforeEach` mock | +| `e2e/planner-multiday.test.ts` | Overnight toggle, multi-day GPX export | `beforeEach` mock | +| `e2e/planner-overlays.test.ts` | Hillshading tiles, POI category markers | No mock needed | +| `e2e/planner-coloring.test.ts` | Color mode, road type chart, hover label | `beforeEach` mock | + +### New helper + +`e2e/helpers/planner.ts` — exports: +- `createSession(request)` → `string` (session URL) +- `openSession(page, url)` → `Promise` (goto + waitForConnected) + +### Unchanged + +`e2e/fixtures/brouter-mock.ts` — stays as-is; each file that needs it calls `mockBRouter(page)` in `beforeEach`. + +### Deleted + +`e2e/planner.test.ts` + +## BRouter Default Mock + +Files whose tests always need a computed route add: + +```ts +test.beforeEach(async ({ page }) => { + await mockBRouter(page); +}); +``` + +This is explicit and discoverable. Tests in `planner-session.test.ts` and `planner-overlays.test.ts` never need routing, so they don't mock BRouter at all. diff --git a/openspec/changes/e2e-test-reorganization/proposal.md b/openspec/changes/archive/2026-05-22-e2e-test-reorganization/proposal.md similarity index 100% rename from openspec/changes/e2e-test-reorganization/proposal.md rename to openspec/changes/archive/2026-05-22-e2e-test-reorganization/proposal.md diff --git a/openspec/changes/e2e-test-reorganization/specs/e2e-testing/spec.md b/openspec/changes/archive/2026-05-22-e2e-test-reorganization/specs/e2e-testing/spec.md similarity index 100% rename from openspec/changes/e2e-test-reorganization/specs/e2e-testing/spec.md rename to openspec/changes/archive/2026-05-22-e2e-test-reorganization/specs/e2e-testing/spec.md diff --git a/openspec/changes/archive/2026-05-22-e2e-test-reorganization/tasks.md b/openspec/changes/archive/2026-05-22-e2e-test-reorganization/tasks.md new file mode 100644 index 0000000..c080311 --- /dev/null +++ b/openspec/changes/archive/2026-05-22-e2e-test-reorganization/tasks.md @@ -0,0 +1,15 @@ +## 1. Shared helper + +- [x] 1.1 Create `e2e/helpers/planner.ts` with `createSession(request)` and `openSession(page, url)` helpers + +## 2. Split test files + +- [x] 2.1 Create `e2e/planner-session.test.ts` — home page, session creation, basic session UI, GPX import (14 tests) +- [x] 2.2 Create `e2e/planner-routing.test.ts` — route splitting, zoom to fit, waypoint notes, POI snap (4 tests, BRouter mocked in beforeEach) +- [x] 2.3 Create `e2e/planner-multiday.test.ts` — overnight toggle, multi-day GPX export (2 tests, BRouter mocked in beforeEach) +- [x] 2.4 Create `e2e/planner-overlays.test.ts` — hillshading tiles, POI category markers (2 tests) +- [x] 2.5 Create `e2e/planner-coloring.test.ts` — color mode, road type chart, hover label (3 tests, BRouter mocked in beforeEach) + +## 3. Clean up + +- [x] 3.1 Delete `e2e/planner.test.ts` diff --git a/openspec/specs/e2e-testing/spec.md b/openspec/specs/e2e-testing/spec.md new file mode 100644 index 0000000..fc89169 --- /dev/null +++ b/openspec/specs/e2e-testing/spec.md @@ -0,0 +1,16 @@ +## Requirements + +### Requirement: Planner E2E test infrastructure +The Planner E2E test suite SHALL mock BRouter by default, split tests by feature file, and provide shared test helpers for common setup patterns. + +#### Scenario: BRouter mocked by default +- **WHEN** a Planner E2E test runs +- **THEN** BRouter is mocked via a shared Playwright fixture unless the test explicitly opts into the real BRouter endpoint + +#### Scenario: Tests split by feature file +- **WHEN** the Planner E2E test suite is executed +- **THEN** tests are organized into focused files by feature area (session, routing, multi-day, overlays, coloring) instead of a single monolithic test file + +#### Scenario: Shared test helpers +- **WHEN** a Planner E2E test needs to create a session and wait for connection +- **THEN** it uses a shared helper function that handles session creation, navigation, and waiting for the connected state diff --git a/playwright.config.ts b/playwright.config.ts index 6e21e1b..99924cf 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -25,7 +25,7 @@ export default defineConfig({ }, { name: "planner", - testMatch: "planner.test.ts", + testMatch: "planner-*.test.ts", use: { ...devices["Desktop Chrome"], baseURL: "http://localhost:3001",