Initial Watermaps import

This commit is contained in:
BuTzZ
2026-07-24 11:29:24 +02:00
commit 57f7b4dedb
129 changed files with 43136 additions and 0 deletions
+219
View File
@@ -0,0 +1,219 @@
import { expect, test } from "@playwright/test";
test("renders the iPhone PWA shell without overlapping core controls", async ({ page, context }) => {
await context.grantPermissions(["geolocation"]);
await context.setGeolocation({ latitude: 54.18, longitude: 12.09 });
await page.goto("/");
await page.getByRole("button", { name: "GPS starten" }).click();
await expect(page.getByText("Watermaps")).toBeVisible();
await expect(page.getByTestId("map-container")).toBeVisible();
await expect(page.getByRole("button", { name: "Layer" })).toBeVisible();
await expect(page.getByRole("complementary", { name: "Routenplanung" })).toBeVisible();
await expect(page.getByRole("button", { name: "Start auf Karte setzen" })).toBeVisible();
await expect(page.getByRole("button", { name: "Ziel auf Karte setzen" })).toBeVisible();
await page.getByText("Routenoptionen: Boot · Zwischenziele").click();
await expect(page.getByRole("button", { name: "Zwischenziel auf der Karte hinzufügen" })).toBeVisible();
await expect(page.getByLabel("Abfahrt")).toBeVisible();
await page.getByText("Unterwegs: GPX · Offline · Kursalarm").click();
await expect(page.getByRole("region", { name: "Navigation und Offline-Route" })).toBeVisible();
await page.getByRole("button", { name: "Layer" }).click();
await expect(page.getByLabel("Layer Auswahl").getByText("Brücken")).toBeVisible();
await expect(page.getByLabel("Layer Auswahl").getByText("Tiefen")).toBeVisible();
await expect(page.getByLabel("Layer Auswahl").getByText("Schleusen")).toBeVisible();
await expect(page.getByLabel("Layer Auswahl").getByText("Häfen")).toBeVisible();
});
test("sets start and destination only after explicit map picking actions", async ({ page, context }) => {
await context.grantPermissions(["geolocation"]);
await context.setGeolocation({ latitude: 54.18, longitude: 12.09 });
await page.goto("/");
await expect(page.getByText("Start und Ziel setzen")).toBeVisible();
await expect(page.getByRole("button", { name: "Layer" })).toBeVisible();
const mapBox = await page.getByTestId("map-container").boundingBox();
if (!mapBox) {
throw new Error("Map container not visible");
}
const mapTapY = mapBox.y + Math.min(132, mapBox.height * 0.2);
await page.touchscreen.tap(mapBox.x + mapBox.width / 2, mapTapY);
await expect(page.getByText("Start und Ziel setzen")).toBeVisible();
await page.getByRole("button", { name: "Start auf Karte setzen" }).click();
await expect(page.getByText("Startpunkt auf der Karte anklicken")).toBeVisible();
await page.touchscreen.tap(mapBox.x + mapBox.width / 3, mapTapY);
await expect(page.getByText(/^Start \d/)).toBeVisible();
await page.getByRole("button", { name: "Ziel auf Karte setzen" }).click();
await expect(page.getByText("Ziel auf der Karte anklicken")).toBeVisible();
await page.touchscreen.tap(mapBox.x + (mapBox.width * 2) / 3, mapTapY);
await expect(page.getByText(/^Ziel \d/)).toBeVisible();
await expect(page.getByText("Route bereit zur Prüfung")).toBeVisible();
await page.getByText("Routenoptionen: Boot · Zwischenziele").click();
await page.getByRole("button", { name: "Zwischenziel auf der Karte hinzufügen" }).click();
await expect(page.getByText("Zwischenziel auf der Karte anklicken")).toBeVisible();
await page.touchscreen.tap(mapBox.x + mapBox.width / 2, mapTapY);
await expect(page.getByText(/^Z1 /)).toBeVisible();
});
test("does not expose Borkum or Hamm as direct route shortcuts", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("button", { name: /Borkum/i })).toHaveCount(0);
await expect(page.getByRole("button", { name: /Hamm/i })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Start auf Karte setzen" })).toBeVisible();
await expect(page.getByRole("button", { name: "Ziel auf Karte setzen" })).toBeVisible();
});
test("can hide and restore the route planner for a map-only view", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "Routenfenster ausblenden" }).click();
await expect(page.getByRole("complementary", { name: "Routenplanung" })).toBeHidden();
await expect(page.getByRole("button", { name: "Routenfenster einblenden" })).toBeVisible();
await expect(page.getByRole("button", { name: "Start auf Karte setzen" })).toBeHidden();
await page.getByRole("button", { name: "Routenfenster einblenden" }).click();
await expect(page.getByRole("complementary", { name: "Routenplanung" })).toBeVisible();
await expect(page.getByRole("button", { name: "Start auf Karte setzen" })).toBeVisible();
});
test("cycles the mobile route sheet through half, full, and compact heights", async ({ page }) => {
await page.goto("/");
const routePanel = page.getByRole("complementary", { name: "Routenplanung" });
await expect(routePanel).toHaveAttribute("data-sheet-state", "half");
const halfBox = await routePanel.boundingBox();
await page
.getByRole("button", { name: "Routenfenster auf volle Höhe vergrößern" })
.click();
await expect(routePanel).toHaveAttribute("data-sheet-state", "full");
const fullBox = await routePanel.boundingBox();
expect(fullBox && halfBox && fullBox.height > halfBox.height + 80).toBe(true);
await page
.getByRole("button", { name: "Routenfenster auf kompakte Höhe verkleinern" })
.click();
await expect(routePanel).toHaveAttribute("data-sheet-state", "compact");
const compactBox = await routePanel.boundingBox();
expect(compactBox && halfBox && compactBox.height < halfBox.height - 80).toBe(true);
await expect(page.getByRole("button", { name: "Start auf Karte setzen" })).toBeHidden();
await page.setViewportSize({ width: 900, height: 844 });
await expect(routePanel).toHaveAttribute("data-sheet-state", "half");
await expect(page.getByRole("button", { name: "Start auf Karte setzen" })).toBeVisible();
});
test("starts a visible dynamic course assistant only after route planning", async ({ page, context }) => {
await context.grantPermissions(["geolocation"]);
await context.setGeolocation({ latitude: 54.18, longitude: 12.09 });
await page.route("**/api/routes", async (request) => {
await request.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
id: "guidance-e2e",
name: "Testkurs Ost",
geometry: { type: "LineString", coordinates: [[12.09, 54.18], [12.11, 54.18]] },
distanceNm: 0.7,
eta: null,
warnings: [],
minKnownDepthM: null,
unknownDepthRatio: 1,
dataSources: ["E2E"],
routingMode: "fairway"
})
});
});
await page.goto("/");
await expect(page.getByRole("button", { name: "Layer" })).toBeVisible();
const mapBox = await page.getByTestId("map-container").boundingBox();
if (!mapBox) throw new Error("Map container not visible");
const tapY = mapBox.y + Math.min(132, mapBox.height * 0.2);
await page.getByRole("button", { name: "Start auf Karte setzen" }).click();
await page.touchscreen.tap(mapBox.x + mapBox.width / 3, tapY);
await page.getByRole("button", { name: "Ziel auf Karte setzen" }).click();
await page.touchscreen.tap(mapBox.x + (mapBox.width * 2) / 3, tapY);
await page.getByRole("button", { name: "Route berechnen" }).click();
const startAssistant = page.getByRole("button", { name: "Navigation starten" });
await expect(startAssistant).toBeVisible();
await startAssistant.click();
const assistant = page.getByRole("complementary", { name: "Kursassistent" });
await expect(assistant).toBeVisible();
await expect(assistant.getByText("SOLL ÜBER GRUND")).toBeVisible();
await expect(assistant.getByRole("button", { name: "Kursassistent stoppen" })).toBeVisible();
await expect(page.getByRole("complementary", { name: "Routenplanung" })).toBeHidden();
const assistantBox = await assistant.boundingBox();
const statusBox = await page.getByLabel("Navigationsstatus").boundingBox();
expect(assistantBox && statusBox && assistantBox.y + assistantBox.height <= statusBox.y + 1).toBe(true);
});
test("sets and arms the anchor watch only through the explicit two-step flow", async ({ page, context }) => {
const now = Date.now();
await context.grantPermissions(["geolocation"]);
await context.setGeolocation({ latitude: 53.2159, longitude: 6.5766 });
await page.route("**/api/tides/nearest?*", async (request) => {
await request.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
station: "Testpegel Emden",
distanceKm: 4.2,
nextHigh: null,
nextLow: null,
waterLevelCurve: [
{ time: new Date(now - 60 * 60_000).toISOString(), predictedM: 1 },
{ time: new Date(now + 6 * 60 * 60_000).toISOString(), predictedM: 2 },
{ time: new Date(now + 30 * 60 * 60_000).toISOString(), predictedM: 0.5 }
],
source: "E2E",
updatedAt: new Date(now).toISOString()
})
});
});
await page.goto("/");
await expect(page.getByRole("complementary", { name: "Ankerwache" })).toHaveCount(0);
await page.getByRole("button", { name: "GPS starten" }).click();
await page.getByRole("button", { name: "Ankerwache öffnen" }).click();
const panel = page.getByRole("complementary", { name: "Ankerwache" });
await expect(panel).toBeVisible();
await expect(panel.getByText(/GPS ±\d+ m/)).toBeVisible();
await expect(page.getByRole("complementary", { name: "Routenplanung" })).toBeHidden();
await panel.getByRole("button", { name: "Anker gefallen Position jetzt setzen" }).click();
await expect(panel.getByText("Ankerpunkt gespeichert")).toBeVisible();
await panel.getByLabel("Tiefe beim Setzen").fill("4");
await panel.getByLabel("Bugrolle über Wasser").fill("1");
await panel.getByLabel("Kette / Leine draußen").fill("40");
await panel.getByLabel("Gewähltes Verhältnis").selectOption("5");
await expect(panel.getByText(/Testpegel Emden · 4\.2 km entfernt/)).toBeVisible();
await expect(panel.getByText(/m Reserve/)).toBeVisible();
await panel.getByRole("button", { name: "Wache starten" }).click();
await expect(panel.getByText("Ankerwache aktiv")).toBeVisible();
await expect(panel.getByText("Im überwachten Schwojkreis")).toBeVisible();
const panelBox = await panel.boundingBox();
const statusBox = await page.getByLabel("Navigationsstatus").boundingBox();
expect(panelBox && statusBox && panelBox.y + panelBox.height <= statusBox.y + 1).toBe(true);
await panel.getByRole("button", { name: "Ankerwache beenden" }).click();
await expect(panel.getByRole("button", { name: "Wirklich beenden" })).toBeVisible();
await panel.getByRole("button", { name: "Wirklich beenden" }).click();
await expect(panel).toBeHidden();
await expect(page.getByRole("complementary", { name: "Routenplanung" })).toBeVisible();
});
+123
View File
@@ -0,0 +1,123 @@
import { expect, test } from "@playwright/test";
test("docks the route planner beside the map and releases the full map when collapsed", async ({
page
}) => {
await page.goto("/");
const routePanel = page.getByRole("complementary", { name: "Routenplanung" });
const map = page.getByTestId("map-container");
const status = page.getByLabel("Navigationsstatus");
await expect(routePanel).toBeVisible();
await expect.poll(async () => (await map.boundingBox())?.x).toBe(400);
const dockedPanelBox = await routePanel.boundingBox();
const dockedMapBox = await map.boundingBox();
const statusBox = await status.boundingBox();
expect(dockedPanelBox).not.toBeNull();
expect(dockedMapBox).not.toBeNull();
expect(statusBox).not.toBeNull();
expect(dockedPanelBox!.x).toBe(0);
expect(dockedPanelBox!.width).toBe(400);
expect(dockedPanelBox!.x + dockedPanelBox!.width).toBeLessThanOrEqual(dockedMapBox!.x);
expect(dockedPanelBox!.y + dockedPanelBox!.height).toBeLessThanOrEqual(statusBox!.y + 1);
await routePanel.getByRole("button", { name: "Routenfenster ausblenden" }).click();
await expect(routePanel).toBeHidden();
await expect.poll(async () => (await map.boundingBox())?.x).toBe(0);
await expect.poll(async () => {
return page.locator(".maplibregl-canvas").evaluate((node) => {
const canvas = node as HTMLCanvasElement;
return Math.abs(canvas.width / window.devicePixelRatio - canvas.clientWidth);
});
}).toBeLessThan(2);
await page.getByRole("button", { name: "Routenfenster einblenden" }).click();
await expect.poll(async () => (await map.boundingBox())?.x).toBe(400);
});
test("switches from floating tablet panel to docked desktop sidebar at 1100 pixels", async ({
page
}) => {
await page.goto("/");
const routePanel = page.getByRole("complementary", { name: "Routenplanung" });
const map = page.getByTestId("map-container");
await page.setViewportSize({ width: 1099, height: 900 });
await expect.poll(async () => (await map.boundingBox())?.x).toBe(0);
const floatingPanelBox = await routePanel.boundingBox();
expect(floatingPanelBox?.x).toBe(10);
expect(floatingPanelBox?.width).toBe(360);
await page.setViewportSize({ width: 1100, height: 900 });
await expect.poll(async () => (await map.boundingBox())?.x).toBe(400);
const dockedPanelBox = await routePanel.boundingBox();
expect(dockedPanelBox?.x).toBe(0);
expect(dockedPanelBox?.width).toBe(400);
});
test("shows marine contact details as a right-hand desktop drawer", async ({ page, context }) => {
await context.grantPermissions(["geolocation"]);
await context.setGeolocation({ latitude: 54.18, longitude: 12.09 });
const requestedFeatureLayers: string[][] = [];
await page.route("**/api/features**", async (route) => {
const requestedLayers =
new URL(route.request().url()).searchParams.get("layers")?.split(",") ?? [];
requestedFeatureLayers.push(requestedLayers);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
type: "FeatureCollection",
features: requestedLayers.includes("harbours")
? [
{
type: "Feature",
id: "e2e-test-harbour",
geometry: { type: "Point", coordinates: [12.09, 54.18] },
properties: {
layer: "harbours",
name: "Testhafen Rostock",
phone: "+49 381 123456",
website: "https://example.com",
vhf: "Kanal 12",
source: "E2E"
}
}
]
: []
})
});
});
await page.goto("/");
await expect(page.locator(".data-badge")).toHaveAttribute("data-ready", "true");
await page.getByRole("button", { name: "GPS starten" }).click();
await page.getByRole("button", { name: "Position zentrieren" }).click();
await expect.poll(() => requestedFeatureLayers.flat().sort().join(",")).toContain("harbours");
const featureButton = page.getByRole("button", {
name: "Informationen zu Hafen Testhafen Rostock",
includeHidden: true
});
await expect(featureButton).toBeAttached({ timeout: 10_000 });
await featureButton.focus();
await featureButton.press("Enter");
const drawer = page.getByRole("dialog", { name: "Testhafen Rostock" });
await expect(drawer).toBeVisible();
await expect(drawer.getByRole("link", { name: /Testhafen Rostock anrufen/ })).toBeVisible();
const drawerBox = await drawer.boundingBox();
const statusBox = await page.getByLabel("Navigationsstatus").boundingBox();
expect(drawerBox).not.toBeNull();
expect(statusBox).not.toBeNull();
expect(drawerBox!.x + drawerBox!.width).toBeGreaterThanOrEqual(1426);
expect(drawerBox!.y).toBeGreaterThanOrEqual(62);
expect(drawerBox!.y + drawerBox!.height).toBeLessThanOrEqual(statusBox!.y);
await page.keyboard.press("Escape");
await expect(drawer).toBeHidden();
await expect(featureButton).toBeFocused();
});