Add BRouter mock for deterministic E2E tests

- brouter-mock.ts: Canned route responses for 2 and 3 waypoint sessions,
  plus latLngToPixel helper for pixel-precise map interactions
- planner.test.ts: Route split, map zoom, and overnight tests now use
  mockBRouter() for instant deterministic responses instead of real
  BRouter — fixes all 3 flaky tests
- Used .first() for sidebar km locator to avoid strict mode violations
  when both header summary and stats footer match

All 38 E2E tests pass in 9.7s (was 34s+ with real BRouter).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-11 01:07:39 +02:00
parent f90571fc2c
commit e6c68fb71b
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
2 changed files with 118 additions and 23 deletions

View file

@ -1,4 +1,5 @@
import { test, expect } from "@playwright/test";
import { mockBRouter, latLngToPixel } from "./fixtures/brouter-mock";
test.describe("Planner", () => {
test("loads the home page", async ({ page }) => {
@ -86,6 +87,8 @@ test.describe("Planner", () => {
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 },
@ -94,31 +97,23 @@ test.describe("Planner", () => {
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.
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.5175, 13.378], 14, { animate: false });
map.setView([52.518, 13.383], 15, { animate: false });
});
await page.waitForTimeout(1000);
await page.waitForTimeout(500);
// 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 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);
// 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.
// Should now have 3 waypoints (original 2 + inserted)
await expect(page.getByText("Waypoints (3)")).toBeVisible({ timeout: 10000 });
});
@ -155,6 +150,8 @@ test.describe("Planner", () => {
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 },
@ -163,8 +160,9 @@ test.describe("Planner", () => {
await expect(page.locator(".leaflet-container")).toBeVisible({ timeout: 10000 });
await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 });
// Wait for route to compute and fit
await expect(page.getByText(/\d+\.\d+ km/)).toBeVisible({ timeout: 20000 });
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(() => {
@ -240,7 +238,8 @@ test.describe("Planner", () => {
const sessionResp = await request.post("/api/sessions", { data: {} });
const { url } = await sessionResp.json();
// Create session with 3 nearby waypoints (short route for fast BRouter response)
await mockBRouter(page);
await page.goto(`${url}?waypoints=${encodeURIComponent(JSON.stringify([
{ lat: 52.520, lon: 13.405 },
{ lat: 52.516, lon: 13.377 },
@ -251,9 +250,8 @@ test.describe("Planner", () => {
await expect(page.getByText("Connected")).toBeVisible({ timeout: 15000 });
await expect(page.getByText("Waypoints (3)")).toBeVisible({ timeout: 5000 });
// Wait for route to compute — the header summary shows distance
const sidebar = page.locator("aside");
await expect(sidebar.getByText(/\d+\.\d+ km/)).toBeVisible({ timeout: 20000 });
await expect(sidebar.getByText(/\d+\.\d+ km/).first()).toBeVisible({ timeout: 10000 });
// Hover waypoint 2 to reveal controls, click the overnight toggle (moon icon)
const waypointRows = sidebar.locator("li").filter({ has: page.locator("span.rounded-full") });