who_need_help/e2e/tests/active-failover.spec.ts

215 lines
7.6 KiB
TypeScript

import { expect, test } from "@playwright/test";
import {
clickUntilVisible,
gotoLiveView,
markerKinds,
projectEmail,
projectText,
registerAndConfirm,
selectOptionContaining,
waitForLiveViewConnected,
} from "./helpers";
async function positionCount(page: import("@playwright/test").Page): Promise<number> {
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 gotoLiveView(requester.page, "/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 gotoLiveView(helper.page, requestURL);
await clickUntilVisible(
helper.page.getByRole("button", { name: "I can help" }),
helper.page.getByRole("heading", { name: "Private match chat" }),
);
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<ReturnType<typeof runtimeSignature>> = 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();
});