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:
parent
f90571fc2c
commit
e6c68fb71b
2 changed files with 118 additions and 23 deletions
97
e2e/fixtures/brouter-mock.ts
Normal file
97
e2e/fixtures/brouter-mock.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Canned enriched route for 3 nearby Berlin waypoints:
|
||||
* [52.520, 13.405] → [52.516, 13.377] → [52.510, 13.390]
|
||||
*
|
||||
* ~3.2km total, 6 track points with elevation.
|
||||
*/
|
||||
const MOCK_ROUTE_3WP = {
|
||||
geojson: {
|
||||
type: "FeatureCollection",
|
||||
features: [{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[13.405, 52.520, 34], [13.398, 52.519, 36], [13.391, 52.518, 38],
|
||||
[13.384, 52.517, 40], [13.377, 52.516, 42],
|
||||
[13.379, 52.514, 40], [13.382, 52.513, 38],
|
||||
[13.385, 52.512, 36], [13.388, 52.511, 35], [13.390, 52.510, 34],
|
||||
],
|
||||
},
|
||||
properties: {},
|
||||
}],
|
||||
},
|
||||
coordinates: [
|
||||
[13.405, 52.520, 34], [13.398, 52.519, 36], [13.391, 52.518, 38],
|
||||
[13.384, 52.517, 40], [13.377, 52.516, 42],
|
||||
[13.379, 52.514, 40], [13.382, 52.513, 38],
|
||||
[13.385, 52.512, 36], [13.388, 52.511, 35], [13.390, 52.510, 34],
|
||||
],
|
||||
segmentBoundaries: [0, 4],
|
||||
totalLength: 3200,
|
||||
totalAscend: 8,
|
||||
surfaces: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Canned enriched route for 2 nearby Berlin waypoints:
|
||||
* [52.520, 13.405] → [52.515, 13.351]
|
||||
*
|
||||
* ~3.8km total, 6 track points with elevation.
|
||||
*/
|
||||
const MOCK_ROUTE_2WP = {
|
||||
geojson: {
|
||||
type: "FeatureCollection",
|
||||
features: [{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
[13.405, 52.520, 34], [13.394, 52.519, 36], [13.383, 52.518, 40],
|
||||
[13.372, 52.517, 38], [13.361, 52.516, 36], [13.351, 52.515, 35],
|
||||
],
|
||||
},
|
||||
properties: {},
|
||||
}],
|
||||
},
|
||||
coordinates: [
|
||||
[13.405, 52.520, 34], [13.394, 52.519, 36], [13.383, 52.518, 40],
|
||||
[13.372, 52.517, 38], [13.361, 52.516, 36], [13.351, 52.515, 35],
|
||||
],
|
||||
segmentBoundaries: [0],
|
||||
totalLength: 3800,
|
||||
totalAscend: 6,
|
||||
surfaces: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock the BRouter route API to return instant, deterministic responses.
|
||||
* Selects the appropriate canned response based on waypoint count.
|
||||
*/
|
||||
export async function mockBRouter(page: Page) {
|
||||
await page.route("**/api/route", async (route) => {
|
||||
const body = route.request().postDataJSON();
|
||||
const wpCount = body?.waypoints?.length ?? 0;
|
||||
const mock = wpCount >= 3 ? MOCK_ROUTE_3WP : MOCK_ROUTE_2WP;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(mock),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a lat/lon to pixel coordinates on the Leaflet map.
|
||||
* Requires MapExposer to have set window.__leafletMap.
|
||||
*/
|
||||
export async function latLngToPixel(page: Page, lat: number, lon: number): Promise<{ x: number; y: number }> {
|
||||
return page.evaluate(([la, ln]) => {
|
||||
const map = (window as any).__leafletMap;
|
||||
if (!map) throw new Error("__leafletMap not available");
|
||||
const point = map.latLngToContainerPoint([la, ln]);
|
||||
return { x: point.x, y: point.y };
|
||||
}, [lat, lon]);
|
||||
}
|
||||
|
|
@ -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") });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue