import { expect, test } from "@playwright/test"; import { markerKinds, projectEmail, projectText, registerAndConfirm, selectOptionContaining, waitForLiveViewConnected, } from "./helpers"; async function positionCount(page: import("@playwright/test").Page): Promise { const raw = await page.locator("#e2e-runtime-node").getAttribute("data-position-count"); return Number(raw ?? "-1"); } async function runtimeSignature( page: import("@playwright/test").Page, ): Promise<{ node: string; bootID: string } | null> { const runtime = page.locator("#e2e-runtime-node"); const [node, bootID] = await Promise.all([ runtime.getAttribute("data-node"), runtime.getAttribute("data-boot-id"), ]); return node && bootID ? { node, bootID } : null; } test("active chat and browser tracking recover after the serving BEAM node restarts", async ({ browser, request, }, testInfo) => { const requester = await registerAndConfirm( browser, request, projectEmail("failover-requester", testInfo.project.name), "Failover Requester", ); const helper = await registerAndConfirm( browser, request, projectEmail("failover-helper", testInfo.project.name), "Failover Helper", ); const requestTitle = projectText("Failover medicine pickup", testInfo.project.name); await requester.page.goto("/requests/new"); await selectOptionContaining(requester.page, "Category", "Medicine pickup"); await requester.page.getByLabel("Medicine pickup status").selectOption("reserved"); await requester.page.getByLabel("Short title").fill(requestTitle); await requester.page .getByLabel("What help do you need?") .fill("A run-scoped reserved medicine pickup for failover verification."); await requester.page.getByLabel("Urgency").selectOption("now"); await requester.page .getByLabel("Request expires (UTC)") .fill(new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString().slice(0, 16)); await requester.page.getByLabel("Approximate area").fill("Failover district"); await requester.page.getByLabel("Location visibility").selectOption("approximate_public"); await requester.page.getByLabel("Latitude").fill("50.4501"); await requester.page.getByLabel("Longitude").fill("30.5234"); await requester.page.locator("#request-form input[type=checkbox]").check(); await requester.page.getByRole("button", { name: "Publish request" }).click(); await expect(requester.page).toHaveURL(/\/requests\/[0-9a-f-]+$/); const requestURL = requester.page.url(); const origin = new URL(process.env.BASE_URL!).origin; await helper.context.grantPermissions(["geolocation"], { origin }); await helper.context.setGeolocation({ latitude: 50.4504, longitude: 30.5237, accuracy: 5, }); await helper.page.goto(requestURL); await helper.page.getByRole("button", { name: "I can help" }).click(); await expect(helper.page.getByRole("heading", { name: "Private match chat" })).toBeVisible(); await expect(requester.page.getByRole("heading", { name: "Private match chat" })).toBeVisible(); const browserPosition = await helper.page.evaluate( () => new Promise<{ latitude: number; longitude: number }>((resolve, reject) => { navigator.geolocation.getCurrentPosition( ({ coords }) => resolve({ latitude: coords.latitude, longitude: coords.longitude, }), ({ code, message }) => reject(new Error(`geolocation ${code}: ${message}`)), ); }), ); expect(browserPosition.latitude).toBeCloseTo(50.4504, 4); expect(browserPosition.longitude).toBeCloseTo(30.5237, 4); await helper.page.getByRole("button", { name: "Share location" }).click(); await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible(); await expect.poll(() => positionCount(helper.page)).toBe(1); await expect.poll(() => positionCount(requester.page)).toBe(1); await expect.poll(() => markerKinds(requester.page)).toContain("Shared live location"); await helper.page .getByPlaceholder("Write a safe coordination message…") .fill("Message before the serving node restart."); await helper.page.getByRole("button", { name: "Send", exact: true }).click(); await expect(requester.page.getByText("Message before the serving node restart.")).toBeVisible(); await helper.page.evaluate(() => { const state = { sawDisconnected: false }; ( window as typeof window & { __wnhFailoverState?: { sawDisconnected: boolean }; } ).__wnhFailoverState = state; window.addEventListener("phx:page-loading-start", (event) => { const detail = (event as CustomEvent<{ kind?: string }>).detail; if (detail?.kind === "error") { state.sawDisconnected = true; } }); }); const originalRuntime = await runtimeSignature(helper.page); expect(originalRuntime).not.toBeNull(); const crash = await request.post("/__e2e__/crash-node", { data: { confirmation: "crash-exact-e2e-node", node: originalRuntime!.node, }, }); expect(crash.status()).toBe(202); expect(await crash.json()).toMatchObject({ status: "scheduled", node: originalRuntime!.node, }); await expect .poll( () => helper.page.evaluate( () => ( window as typeof window & { __wnhFailoverState?: { sawDisconnected: boolean }; } ).__wnhFailoverState?.sawDisconnected ?? false, ), { timeout: 30_000 }, ) .toBe(true); await waitForLiveViewConnected(helper.page); let recoveredRuntime: Awaited> = null; await expect .poll( async () => { recoveredRuntime = await runtimeSignature(helper.page); return recoveredRuntime ? `${recoveredRuntime.node}/${recoveredRuntime.bootID}` : null; }, { timeout: 30_000 }, ) .not.toBe(`${originalRuntime!.node}/${originalRuntime!.bootID}`); await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible(); await expect.poll(() => positionCount(helper.page), { timeout: 30_000 }).toBe(1); await expect.poll(() => positionCount(requester.page), { timeout: 30_000 }).toBe(1); await expect.poll(() => markerKinds(requester.page), { timeout: 30_000 }).toContain( "Shared live location", ); await helper.page .getByPlaceholder("Write a safe coordination message…") .fill("Message after LiveView recovered on the available replicas."); await helper.page.getByRole("button", { name: "Send", exact: true }).click(); await expect( requester.page.getByText("Message after LiveView recovered on the available replicas."), ).toBeVisible(); await helper.page.getByRole("button", { name: "Stop and delete position" }).click(); await expect(helper.page.getByRole("button", { name: "Share location" })).toBeVisible(); await expect.poll(() => positionCount(helper.page)).toBe(0); await expect.poll(() => positionCount(requester.page)).toBe(0); await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared live location"); await testInfo.attach("active-failover-evidence", { contentType: "application/json", body: Buffer.from( JSON.stringify( { browser: testInfo.project.name, original_runtime: originalRuntime, recovered_runtime: recoveredRuntime, saw_liveview_disconnect: true, tracking_recovered: true, chat_recovered: true, tracking_position_deleted: true, }, null, 2, ), ), }); await requester.context.close(); await helper.context.close(); });