Files
ServiceMap-Public/frontend/e2e/demo.spec.ts
T

171 lines
7.5 KiB
TypeScript

import { expect, test } from "@playwright/test";
type ServiceSnapshot = {
service: {
id: string;
name: string;
depends_on: string[];
};
latest_probe: { status: string } | null;
effective_status: string;
};
const topologyOf = (snapshots: ServiceSnapshot[]) =>
snapshots
.map(({ service }) => ({
id: service.id,
name: service.name,
depends_on: [...service.depends_on].sort(),
}))
.sort((left, right) => left.id.localeCompare(right.id));
const isDemoMutation = (path: string) =>
(response: {
url: () => string;
request: () => { method: () => string };
}) =>
response.url().endsWith(path) && response.request().method() === "POST";
test("Explore demo exposes evidence-backed root cause and reset-safe exit", async ({ page, request }) => {
test.setTimeout(60_000);
// The Playwright server uses a persistent temporary database, so restore any
// session left by an interrupted run before taking this test's baseline.
const cleanup = await request.post("/api/v1/demo/exit");
expect(cleanup.ok()).toBeTruthy();
const baselineResponse = await request.get("/api/v1/services");
expect(baselineResponse.ok()).toBeTruthy();
const baselineTopology = topologyOf(await baselineResponse.json());
await page.goto("/");
const explore = page.getByRole("button", { name: "Explore demo", exact: true });
await expect(explore).toBeEnabled({ timeout: 30_000 });
const loadResponsePromise = page.waitForResponse(isDemoMutation("/api/v1/demo"));
await explore.click();
if (baselineTopology.length > 0) {
const confirmation = page
.getByRole("alert")
.filter({ hasText: "temporarily replace this topology" });
await expect(confirmation).toBeVisible();
await confirmation.getByRole("button", { name: "Explore demo", exact: true }).click();
}
const loadResponse = await loadResponsePromise;
expect(loadResponse.ok()).toBeTruthy();
expect(await loadResponse.json()).toMatchObject({
active: true,
synthetic: true,
scenario_name: "Northstar Platform",
service_count: 9,
total: 9,
root_cause_service_name: "PostgreSQL",
affected_count: 5,
});
const demoServicesResponse = await request.get("/api/v1/services");
expect(demoServicesResponse.ok()).toBeTruthy();
const demoServices: ServiceSnapshot[] = await demoServicesResponse.json();
expect(demoServices).toHaveLength(9);
const postgres = demoServices.find(({ service }) => service.name === "PostgreSQL");
expect(postgres).toBeDefined();
expect(postgres!.latest_probe?.status).toBe("down");
expect(postgres!.effective_status).toBe("down");
const [incidentsResponse, historyResponse, availabilityResponse] =
await Promise.all([
request.get("/api/v1/incidents"),
request.get(`/api/v1/services/${postgres!.service.id}/history?limit=50`),
request.get(`/api/v1/services/${postgres!.service.id}/availability?hours=24`),
]);
expect(incidentsResponse.ok()).toBeTruthy();
const incident = (await incidentsResponse.json()) as {
active: boolean;
candidates: Array<{
service_name: string;
affected_service_ids: string[];
}>;
};
expect(incident.active).toBe(true);
expect(incident.candidates[0]?.service_name).toBe("PostgreSQL");
expect(incident.candidates[0]?.affected_service_ids).toHaveLength(5);
expect(historyResponse.ok()).toBeTruthy();
expect(await historyResponse.json()).toHaveLength(24);
expect(availabilityResponse.ok()).toBeTruthy();
expect(await availabilityResponse.json()).toMatchObject({
total_checks: 24,
down_checks: 2,
uptime_percent: 91.67,
});
const demoBanner = page.getByRole("status", { name: "Explore demo scenario" });
await expect(demoBanner).toBeVisible();
await expect(demoBanner).toHaveAttribute("aria-busy", "false", { timeout: 30_000 });
await expect(page.getByText("Demo scenario", { exact: false })).toBeVisible();
await expect(page.getByText("PostgreSQL", { exact: true }).first()).toBeVisible();
await expect(page.getByText("Likely root cause", { exact: true })).toBeVisible();
await expect(page.getByText("Health evidence", { exact: true })).toBeVisible();
const workspaces = page.getByRole("navigation", { name: "ServiceMap workspaces" });
await workspaces.getByRole("button", { name: "Overview" }).click();
await expect(page.getByRole("heading", { name: "9/9 with current health evidence" })).toBeVisible();
await expect(page.getByRole("button", { name: "Review unresolved" })).toBeDisabled();
await page.locator(".overview-lead").getByRole("button", { name: /1 down/i }).click();
await expect(page.getByRole("combobox", { name: "Status", exact: true })).toHaveValue("down");
await workspaces.getByRole("button", { name: "Incidents" }).click();
await expect(
page.getByRole("heading", { name: "Active and historical incidents" }),
).toBeVisible();
await page.getByLabel("Search incidents").fill("does not exist");
await expect(page.getByText("No incidents match these filters.")).toBeVisible();
await page.getByLabel("Search incidents").fill("");
await workspaces.getByRole("button", { name: "Topology", exact: true }).click();
await expect(page.getByRole("region", { name: "Topology commands" })).toBeVisible();
await page.getByRole("region", { name: "Topology commands" }).locator("summary").filter({ hasText: "Filters" }).click();
await expect(page.getByRole("combobox", { name: "Filter topology by status" })).toBeVisible();
const inspector = page.getByRole("complementary");
await expect(inspector.getByRole("heading", { name: "PostgreSQL" })).toBeVisible();
await expect(inspector.getByText("Likely root cause", { exact: true })).toBeVisible();
await expect(inspector.getByText("Health evidence", { exact: true })).toBeVisible();
const reset = page.locator("header").getByRole("button", { name: "Reset demo" });
const resetResponsePromise = page.waitForResponse(isDemoMutation("/api/v1/demo/reset"));
await reset.click();
const resetResponse = await resetResponsePromise;
expect(resetResponse.ok()).toBeTruthy();
expect(await resetResponse.json()).toMatchObject({
active: true,
root_cause_service_name: "PostgreSQL",
});
// A rendered PostgreSQL node can be left over from before Reset completed.
// Wait for the demo region to leave its busy state before attempting Exit.
await expect(demoBanner).toHaveAttribute("aria-busy", "false", { timeout: 30_000 });
await expect(page.getByText("PostgreSQL", { exact: true }).first()).toBeVisible();
const exit = page.getByRole("button", { name: "Exit demo", exact: true });
await expect(exit).toBeEnabled();
const exitResponsePromise = page.waitForResponse(isDemoMutation("/api/v1/demo/exit"));
await exit.click();
const exitResponse = await exitResponsePromise;
expect(exitResponse.ok()).toBeTruthy();
expect(await exitResponse.json()).toMatchObject({ active: false, restored: true });
await expect(demoBanner).toBeHidden({ timeout: 30_000 });
await expect(
page.getByText("Demo closed. Your previous topology was restored.", { exact: true }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Explore demo", exact: true }),
).toBeEnabled({ timeout: 30_000 });
const restoredDemoResponse = await request.get("/api/v1/demo");
expect(restoredDemoResponse.ok()).toBeTruthy();
expect(await restoredDemoResponse.json()).toMatchObject({ active: false });
const restoredServicesResponse = await request.get("/api/v1/services");
expect(restoredServicesResponse.ok()).toBeTruthy();
expect(topologyOf(await restoredServicesResponse.json())).toEqual(baselineTopology);
});