mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
abff834d80
The isolated Coverage fixture cannot establish whether resource-provider and WebSocket replacement preserves application interaction in issue #1869. Exercise the actual shell, scoped paginated resources, windowed Coverage and By date, and the real PBS History drawer with synthetic snapshots. Retain desktop and narrow evidence without claiming a deployed-release or Brave fix. Change-source: pulse-maintainer
319 lines
11 KiB
JavaScript
319 lines
11 KiB
JavaScript
// Run with pulse-heavy-run -- node scripts/check-backup-app-refresh.mjs
|
|
// Full application shell with synthetic API and WebSocket snapshots; no live deployment.
|
|
import { createServer } from "../frontend-modern/node_modules/vite/dist/node/index.js";
|
|
import { chromium, expect } from "@playwright/test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = fileURLToPath(new URL("../frontend-modern", import.meta.url));
|
|
process.chdir(root); // PostCSS/Tailwind resolve their config from the frontend root.
|
|
const server = await createServer({
|
|
root,
|
|
configFile: `${root}/vite.config.ts`,
|
|
server: { host: "127.0.0.1", port: 5198, strictPort: true },
|
|
});
|
|
let browser;
|
|
try {
|
|
await server.listen();
|
|
browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({
|
|
viewport: {
|
|
width: Number(process.env.PULSE_BROWSER_WIDTH || 1440),
|
|
height: 900,
|
|
},
|
|
});
|
|
// Never allow this synthetic qualification to contact a real service.
|
|
await page.route("**/*", (route) => {
|
|
const url = new URL(route.request().url());
|
|
if (url.origin !== "http://127.0.0.1:5198") return route.abort();
|
|
return route.continue();
|
|
});
|
|
const errors = [];
|
|
const consoleErrors = [];
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error") consoleErrors.push(message.text());
|
|
});
|
|
page.on("requestfailed", (request) =>
|
|
console.error(request.url(), request.failure()),
|
|
);
|
|
let generation = 0;
|
|
const workloads = () =>
|
|
Array.from({ length: 240 }, (_, index) => ({
|
|
id: `ct-${112 + index}`,
|
|
type: "system-container",
|
|
name: `guest-${String(index).padStart(2, "0")}-snapshot-${generation}`,
|
|
displayName: `guest-${String(index).padStart(2, "0")}-snapshot-${generation}`,
|
|
platformId: "pve-a",
|
|
platformType: "proxmox-pve",
|
|
sourceType: "api",
|
|
status: "running",
|
|
lastSeen: Date.now(),
|
|
proxmox: { vmid: 112 + index, node: "pve-a", instance: "pve-a" },
|
|
}));
|
|
const pbs = () => ({
|
|
id: "pbs-1",
|
|
type: "pbs",
|
|
name: `pbs-main-snapshot-${generation}`,
|
|
displayName: "PBS Main",
|
|
platformId: "pbs-main",
|
|
platformType: "proxmox-pbs",
|
|
sourceType: "hybrid",
|
|
metricsTarget: { resourceType: "agent", resourceId: "agent-pbs-1" },
|
|
agent: { agentId: "agent-pbs-1", hostname: "pbs-main" },
|
|
status: "online",
|
|
lastSeen: Date.now(),
|
|
cpu: { current: 12 + generation },
|
|
memory: { current: 40, total: 8000, used: 3200 },
|
|
pbs: {
|
|
instanceId: "pbs-main",
|
|
version: `3.2.${generation}`,
|
|
datastores: [],
|
|
},
|
|
platformData: { pbs: { instanceId: "pbs-main", hostname: "pbs-main" } },
|
|
});
|
|
const state = () => ({
|
|
resources: [...workloads(), pbs()],
|
|
connectedInfrastructure: [],
|
|
activeAlerts: [],
|
|
recentlyResolved: [],
|
|
metrics: [],
|
|
lastUpdate: Date.now(),
|
|
stats: { version: "6.5.0-test", startTime: new Date().toISOString() },
|
|
});
|
|
let socket;
|
|
await page.routeWebSocket(/\/ws(?:\?|$)/, (ws) => {
|
|
socket = ws;
|
|
ws.send(JSON.stringify({ type: "initialState", data: state() }));
|
|
});
|
|
await page.route(
|
|
(url) => url.pathname.startsWith("/api/"),
|
|
(route) => {
|
|
const path = new URL(route.request().url()).pathname;
|
|
if (path === "/api/alerts/active" || path === "/api/orgs")
|
|
return route.fulfill({ json: [] });
|
|
if (path === "/api/version")
|
|
return route.fulfill({
|
|
json: { version: "6.5.0-test", isDevelopment: true },
|
|
});
|
|
if (path === "/api/updates/check")
|
|
return route.fulfill({
|
|
json: {
|
|
available: false,
|
|
currentVersion: "6.5.0-test",
|
|
latestVersion: "6.5.0-test",
|
|
},
|
|
});
|
|
if (path === "/api/updates/release-notes")
|
|
return route.fulfill({
|
|
json: { version: "6.5.0-test", releaseNotes: "" },
|
|
});
|
|
if (path === "/api/security/status")
|
|
return route.fulfill({
|
|
json: { hasAuthentication: true, sessionCapabilities: {} },
|
|
});
|
|
if (path === "/api/state") return route.fulfill({ json: state() });
|
|
if (path === "/api/state/summary") return route.fulfill({ json: {} });
|
|
if (path === "/api/health")
|
|
return route.fulfill({ json: { status: "healthy" } });
|
|
if (path === "/api/resources") {
|
|
const types = new URL(route.request().url()).searchParams
|
|
.get("type")
|
|
?.split(",");
|
|
const resources = state().resources.filter(
|
|
(r) => !types || types.includes(r.type),
|
|
);
|
|
const query = new URL(route.request().url()).searchParams;
|
|
const limit = Number(query.get("limit") || 100);
|
|
const pageNumber = Number(query.get("page") || 1);
|
|
return route.fulfill({
|
|
json: {
|
|
resources: resources.slice(
|
|
(pageNumber - 1) * limit,
|
|
pageNumber * limit,
|
|
),
|
|
meta: { totalPages: Math.ceil(resources.length / limit) },
|
|
},
|
|
});
|
|
}
|
|
if (path === "/api/backups/pve")
|
|
return route.fulfill({
|
|
json: {
|
|
data: {
|
|
guestSnapshots: workloads().map((w) => ({
|
|
id: `snap-${w.id}`,
|
|
name: "pre-upgrade",
|
|
node: "pve-a",
|
|
instance: "pve-a",
|
|
type: "ct",
|
|
vmid: w.proxmox.vmid,
|
|
time: new Date().toISOString(),
|
|
vmstate: false,
|
|
})),
|
|
storageBackups: [],
|
|
backupTasks: [],
|
|
},
|
|
},
|
|
});
|
|
if (path === "/api/backups/pbs")
|
|
return route.fulfill({ json: { data: { backups: [] } } });
|
|
return route.fulfill({ json: { data: [], policy: {} } });
|
|
},
|
|
);
|
|
await page.goto("http://127.0.0.1:5198/proxmox/backups/coverage");
|
|
await expect(
|
|
page.getByText("pbs-main-snapshot-0", { exact: true }),
|
|
).toBeVisible({ timeout: 60000 });
|
|
const toggle = page.getByRole("button", {
|
|
name: /expand details for guest-20-/i,
|
|
});
|
|
// The narrow layout exposes this control on keyboard focus, not pointer hover.
|
|
await toggle.waitFor({ timeout: 60_000 }).catch(async (e) => {
|
|
console.error(await page.locator("body").innerText());
|
|
throw e;
|
|
});
|
|
await toggle.focus();
|
|
await toggle.press("Enter").catch(async (error) => {
|
|
console.error(errors, await page.locator("body").innerText());
|
|
throw error;
|
|
});
|
|
const expanded = page.getByRole("button", {
|
|
name: /collapse details for guest-20-/i,
|
|
});
|
|
await expanded.focus();
|
|
await expect(
|
|
page.getByText("Restore evidence", { exact: true }),
|
|
).toBeVisible();
|
|
// Finish layout/scroll synchronisation before measuring refresh effects.
|
|
await page.waitForTimeout(500);
|
|
const visibleCoverageRows = await page
|
|
.locator('[data-proxmox-backup-row="coverage"]')
|
|
.count();
|
|
expect(visibleCoverageRows).toBeLessThan(240);
|
|
expect(visibleCoverageRows).toBeGreaterThan(20);
|
|
const baseline = await page.evaluate(() => ({
|
|
y: document.querySelector(".app-scroll-shell").scrollTop,
|
|
url: location.href,
|
|
}));
|
|
expect(baseline.y).toBeGreaterThan(100);
|
|
const samples = [];
|
|
for (generation = 1; generation <= 3; generation++) {
|
|
expect(socket).toBeTruthy();
|
|
socket.send(JSON.stringify({ type: "rawData", data: state() }));
|
|
await expect(
|
|
page
|
|
.getByText(`guest-20-snapshot-${generation}`, { exact: true })
|
|
.first(),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByText("Restore evidence", { exact: true }),
|
|
).toBeVisible();
|
|
const sample = await page.evaluate(() => ({
|
|
y: document.querySelector(".app-scroll-shell").scrollTop,
|
|
url: location.href,
|
|
}));
|
|
console.log(
|
|
JSON.stringify({
|
|
generation,
|
|
...sample,
|
|
focused: await expanded.evaluate((el) => el === document.activeElement),
|
|
}),
|
|
);
|
|
await expect(expanded).toBeFocused();
|
|
expect(sample.url).toBe(baseline.url);
|
|
expect(Math.abs(sample.y - baseline.y)).toBeLessThanOrEqual(1);
|
|
samples.push({ generation, ...sample });
|
|
}
|
|
|
|
// Navigate through the real router, then hold a scrolled By date view.
|
|
await page.getByRole("link", { name: "By date", exact: true }).click();
|
|
const dateRow = page
|
|
.locator('[data-proxmox-backup-row="recoverable"]')
|
|
.filter({ hasText: "guest-20-" })
|
|
.first();
|
|
await dateRow.scrollIntoViewIfNeeded();
|
|
const dateTab = page.getByRole("link", { name: "By date", exact: true });
|
|
await dateTab.evaluate((el) => el.focus({ preventScroll: true }));
|
|
const capture = () =>
|
|
page.evaluate(() => ({
|
|
y: document.querySelector(".app-scroll-shell").scrollTop,
|
|
url: location.href,
|
|
active:
|
|
document.activeElement?.getAttribute("aria-label") ||
|
|
document.activeElement?.textContent,
|
|
}));
|
|
const dateBaseline = await capture();
|
|
expect(dateBaseline.y).toBeGreaterThan(100);
|
|
const dateSamples = [];
|
|
for (generation = 4; generation <= 6; generation++) {
|
|
socket.send(JSON.stringify({ type: "rawData", data: state() }));
|
|
await expect(dateRow).toContainText(`guest-20-snapshot-${generation}`);
|
|
await expect(dateTab).toBeFocused();
|
|
const sample = await capture();
|
|
expect(sample.url).toBe(dateBaseline.url);
|
|
expect(Math.abs(sample.y - dateBaseline.y)).toBeLessThanOrEqual(1);
|
|
dateSamples.push(sample);
|
|
}
|
|
console.log(
|
|
JSON.stringify({
|
|
view: "date",
|
|
baseline: dateBaseline,
|
|
samples: dateSamples,
|
|
}),
|
|
);
|
|
|
|
const pbsToggle = page.getByRole("button", {
|
|
name: /Expand details for pbs-main-snapshot-/,
|
|
});
|
|
await pbsToggle.focus();
|
|
await pbsToggle.press("Enter");
|
|
const history = page.getByRole("tab", { name: "History", exact: true });
|
|
await history.click();
|
|
await history.focus();
|
|
await expect(page.getByTestId("resource-metrics-history-tab")).toBeVisible();
|
|
await page.waitForTimeout(500);
|
|
const historyBaseline = await capture();
|
|
const historySamples = [];
|
|
for (generation = 7; generation <= 9; generation++) {
|
|
socket.send(JSON.stringify({ type: "rawData", data: state() }));
|
|
await expect(
|
|
page
|
|
.getByText(`pbs-main-snapshot-${generation}`, { exact: true })
|
|
.first(),
|
|
).toBeVisible();
|
|
await expect(history).toHaveAttribute("aria-selected", "true");
|
|
await expect(history).toBeFocused();
|
|
await expect(
|
|
page.getByTestId("resource-metrics-history-tab"),
|
|
).toBeVisible();
|
|
const sample = await capture();
|
|
expect(sample.url).toBe(historyBaseline.url);
|
|
expect(Math.abs(sample.y - historyBaseline.y)).toBeLessThanOrEqual(1);
|
|
historySamples.push(sample);
|
|
}
|
|
console.log(
|
|
JSON.stringify({
|
|
view: "PBS History",
|
|
baseline: historyBaseline,
|
|
samples: historySamples,
|
|
}),
|
|
);
|
|
expect(errors).toEqual([]);
|
|
expect(consoleErrors).toEqual([]);
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
baseline,
|
|
samples,
|
|
visibleCoverageRows,
|
|
pageErrors: errors,
|
|
consoleErrors,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} finally {
|
|
await browser?.close();
|
|
await server.close();
|
|
}
|