7b85f4dff1
The whole point of driving a real browser instead of curling api/frontend separately: none of this session's prior "live verification" ever exercised the same-origin routing production relies on (Caddy: /ws*+/api/* -> api, else -> frontend), so a real browser's relative fetch()/WebSocket calls were never actually proven to resolve. e2e/proxy.ts mirrors that routing (no caddy binary available locally); global-setup.ts/global-teardown.ts orchestrate throwaway infra + seeded data + the api/frontend/proxy processes end to end. Getting the suite green surfaced two genuine bugs invisible to unit tests: - index.pug loaded map.js via two <script type="module"> tags (one moved to <head> to fix load-order, the original left in place by mistake), causing Alpine's x-init="init()" to run twice and Leaflet to throw "Map container is already initialized" on the second call. - map.js's exportRegion() passed the Alpine-reactive `regionBounds` object straight into worker.postMessage(); Alpine wraps assigned state in Proxies, which the structured clone algorithm can't clone, so every export silently failed. Fixed by spreading into a plain object first. Covers the two flows flagged all session as verified only at the unit/curl level: the marker click-to-place/edit popup (including that a marker created while linked shows up in a second browser context with the same session, proving server-side sync) and the region-select drag + glTF export (including a real triggered file download).
124 lines
5.9 KiB
TypeScript
124 lines
5.9 KiB
TypeScript
// Covers the click-to-place/edit marker popup flow (Leaflet L.popup() + Alpine's $refs
|
|
// DOM-node-reuse trick in map.js's init()/openMarkerForm) — flagged throughout MCMapper's
|
|
// development as verified only at the unit-test/manual-curl level, never actually clicked
|
|
// through in a real browser. See README's e2e section.
|
|
import { test, expect, type Page } from "@playwright/test";
|
|
import { readE2eState } from "../config";
|
|
|
|
const state = readE2eState();
|
|
const SESSION_STORAGE_KEY = "mcmapper_session";
|
|
|
|
async function waitForMapReady(page: Page) {
|
|
await page.goto("/");
|
|
await expect(page.getByTestId("place-marker-toggle")).toBeVisible();
|
|
// Leaflet adds this class to the same #map div passed to L.map() — waiting for it means the
|
|
// map/tile layer/view are fully initialized before we try to click on it.
|
|
await page.locator("#map.leaflet-container").waitFor();
|
|
}
|
|
|
|
async function placeMarkerAt(page: Page, offset: { x: number; y: number }) {
|
|
await page.getByTestId("place-marker-toggle").click();
|
|
const map = page.locator("#map");
|
|
const box = await map.boundingBox();
|
|
if (!box) throw new Error("#map has no bounding box");
|
|
await map.click({ position: offset });
|
|
// Not `.leaflet-popup` — Leaflet doesn't remove a previously-closed popup's container from the
|
|
// DOM, only the shared markerFormEl content node (moved between popups, see map.js's
|
|
// openMarkerForm doc comment) is guaranteed unique and reflects the currently-open popup.
|
|
await expect(page.getByTestId("marker-form")).toBeVisible();
|
|
return box;
|
|
}
|
|
|
|
test.describe("marker popup — anonymous (localStorage)", () => {
|
|
test.use({ storageState: { cookies: [], origins: [] } });
|
|
|
|
test("clicking the map opens a popup pre-filled with defaults", async ({ page }) => {
|
|
await waitForMapReady(page);
|
|
await placeMarkerAt(page, { x: 400, y: 300 });
|
|
|
|
await expect(page.getByTestId("marker-y")).toHaveValue("60");
|
|
await expect(page.getByTestId("marker-x")).not.toHaveValue("");
|
|
await expect(page.getByTestId("marker-z")).not.toHaveValue("");
|
|
|
|
const color = await page.getByTestId("marker-color-input").inputValue();
|
|
expect(color).toMatch(/^#[0-9a-f]{6}$/i);
|
|
});
|
|
|
|
test("creating a marker adds it to the list and survives a reload", async ({ page }) => {
|
|
await waitForMapReady(page);
|
|
await placeMarkerAt(page, { x: 420, y: 280 });
|
|
|
|
await page.getByTestId("marker-name-input").fill("Test Marker");
|
|
await page.getByTestId("marker-save").click();
|
|
await expect(page.locator('[data-testid="marker-row"][data-marker-name="Test Marker"]')).toBeVisible();
|
|
|
|
await page.reload();
|
|
await expect(page.getByTestId("place-marker-toggle")).toBeVisible();
|
|
await expect(page.locator('[data-testid="marker-row"][data-marker-name="Test Marker"]')).toBeVisible();
|
|
});
|
|
|
|
test("editing a marker via the popup updates its name", async ({ page }) => {
|
|
await waitForMapReady(page);
|
|
await placeMarkerAt(page, { x: 440, y: 260 });
|
|
await page.getByTestId("marker-name-input").fill("Before Edit");
|
|
await page.getByTestId("marker-save").click();
|
|
await expect(page.locator('[data-testid="marker-row"][data-marker-name="Before Edit"]')).toBeVisible();
|
|
|
|
await page.locator('[data-testid="marker-row"][data-marker-name="Before Edit"]').getByTestId("marker-edit").click();
|
|
await expect(page.getByTestId("marker-form")).toBeVisible();
|
|
await expect(page.getByTestId("marker-name-input")).toHaveValue("Before Edit");
|
|
|
|
await page.getByTestId("marker-name-input").fill("After Edit");
|
|
await page.getByTestId("marker-save").click();
|
|
|
|
await expect(page.locator('[data-testid="marker-row"][data-marker-name="After Edit"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="marker-row"][data-marker-name="Before Edit"]')).toHaveCount(0);
|
|
});
|
|
|
|
test("deleting a marker removes it from the list", async ({ page }) => {
|
|
await waitForMapReady(page);
|
|
await placeMarkerAt(page, { x: 460, y: 240 });
|
|
await page.getByTestId("marker-name-input").fill("To Delete");
|
|
await page.getByTestId("marker-save").click();
|
|
await expect(page.locator('[data-testid="marker-row"][data-marker-name="To Delete"]')).toBeVisible();
|
|
|
|
await page.locator('[data-testid="marker-row"][data-marker-name="To Delete"]').getByTestId("marker-delete").click();
|
|
await expect(page.locator('[data-testid="marker-row"][data-marker-name="To Delete"]')).toHaveCount(0);
|
|
});
|
|
});
|
|
|
|
test.describe("marker popup — linked account (server-synced across devices)", () => {
|
|
async function injectSession(page: Page) {
|
|
await page.addInitScript(
|
|
([key, token]) => localStorage.setItem(key, token),
|
|
[SESSION_STORAGE_KEY, state.sessionToken],
|
|
);
|
|
}
|
|
|
|
test("a marker created while linked is visible in a separate browser context with the same session", async ({ browser }) => {
|
|
const contextA = await browser.newContext();
|
|
const pageA = await contextA.newPage();
|
|
await injectSession(pageA);
|
|
await waitForMapReady(pageA);
|
|
await expect(pageA.getByTestId("account-linked")).toBeVisible();
|
|
|
|
await placeMarkerAt(pageA, { x: 400, y: 300 });
|
|
const markerName = `Synced Marker ${Date.now()}`;
|
|
await pageA.getByTestId("marker-name-input").fill(markerName);
|
|
await pageA.getByTestId("marker-save").click();
|
|
await expect(pageA.locator(`[data-testid="marker-row"][data-marker-name="${markerName}"]`)).toBeVisible();
|
|
await contextA.close();
|
|
|
|
// A fresh context has its own localStorage — the only way this marker can show up here is
|
|
// if it was actually persisted server-side (accounts.markers table), proving the
|
|
// cross-device sync the user asked for, not just localStorage.
|
|
const contextB = await browser.newContext();
|
|
const pageB = await contextB.newPage();
|
|
await injectSession(pageB);
|
|
await waitForMapReady(pageB);
|
|
await expect(pageB.getByTestId("account-linked")).toBeVisible();
|
|
await expect(pageB.locator(`[data-testid="marker-row"][data-marker-name="${markerName}"]`)).toBeVisible();
|
|
await contextB.close();
|
|
});
|
|
});
|