Files
ForgeFlow/tests/browser/forgeflow.spec.mjs
T
NuklearRabbit 4c21616e72
ForgeFlow quality gate / quality (push) Canceled after 0s
fix: harden deployment discovery and preflight
2026-08-08 23:59:27 +02:00

215 lines
11 KiB
JavaScript

import { test, expect } from "@playwright/test";
import { writeFile } from "node:fs/promises";
const consoleEntries = new WeakMap();
const runtimeErrors = new WeakMap();
test.beforeEach(async ({ page }, testInfo) => {
const logs = [];
const errors = [];
consoleEntries.set(page, logs);
runtimeErrors.set(page, errors);
await page.emulateMedia({ colorScheme: testInfo.project.metadata.theme, reducedMotion: testInfo.project.metadata.reduced ? "reduce" : "no-preference" });
page.on("console", (message) => logs.push({ type: message.type(), text: message.text() }));
page.on("pageerror", (error) => errors.push({ name: error.name, message: error.message, stack: error.stack }));
await page.goto("/");
await expect(page.locator(".app-shell")).toBeVisible();
const theme = testInfo.project.metadata.theme;
await page.evaluate((requested) => {
document.documentElement.dataset.theme = requested;
localStorage.setItem("forgeflow-demo-theme", requested);
}, theme);
});
test.afterEach(async ({ page }, testInfo) => {
const logs = consoleEntries.get(page) || [];
const errors = runtimeErrors.get(page) || [];
if (testInfo.status !== testInfo.expectedStatus) {
const prefix = testInfo.outputPath("failure");
await writeFile(`${prefix}-console.json`, JSON.stringify({ test: testInfo.title, project: testInfo.project.name, metadata: testInfo.project.metadata, logs, errors }, null, 2));
await writeFile(`${prefix}-dom.html`, await page.content());
await writeFile(`${prefix}-fixture.json`, JSON.stringify({ url: page.url(), viewport: page.viewportSize(), theme: await page.locator("html").getAttribute("data-theme") }, null, 2));
}
expect(errors, "page errors").toEqual([]);
expect(logs.filter((entry) => entry.type === "error"), "console errors").toEqual([]);
});
async function assertSurface(page) {
const audit = await page.evaluate(() => {
const interactive = [...document.querySelectorAll('button,input,select,textarea,a[href],[role="button"]')].filter((element) => {
const style = getComputedStyle(element);
return style.display !== "none" && style.visibility !== "hidden" && element.getBoundingClientRect().width > 0;
});
const unnamed = interactive.filter((element) => !String(element.getAttribute("aria-label") || element.getAttribute("title") || element.labels?.[0]?.textContent || element.textContent || element.value || "").trim());
const outside = interactive.filter((element) => { const rect = element.getBoundingClientRect(); const fixed = ["fixed", "sticky"].includes(getComputedStyle(element).position) || Boolean(element.closest('[role="dialog"]')); return rect.left < -1 || rect.right > innerWidth + 1 || (fixed && (rect.top < -1 || rect.bottom > innerHeight + 1)); });
const text = document.body.innerText;
return {
horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
unnamed: unnamed.map((element) => element.outerHTML.slice(0, 180)),
outside: outside.map((element) => element.outerHTML.slice(0, 180)),
badTokens: ["undefined", "[object Object]", "â", "Â", "Ã"].filter((token) => text.includes(token)),
nullText: /(^|\s)null($|\s)/i.test(text),
headings: [...document.querySelectorAll("h1,h2,h3")].map((heading) => Number(heading.tagName[1])),
};
});
expect(audit.horizontalOverflow).toBe(false);
expect(audit.unnamed).toEqual([]);
expect(audit.outside).toEqual([]);
expect(audit.badTokens).toEqual([]);
expect(audit.nullText).toBe(false);
expect(audit.headings.length).toBeGreaterThan(0);
}
async function assertScrollableWhenOverflowing(page, selector) {
const target = page.locator(selector);
await expect(target).toBeVisible();
await expect(target).toHaveCSS("overflow-y", /auto|scroll/);
let metrics;
await expect.poll(async () => {
metrics = await target.evaluate((element) => ({
connected: element.isConnected,
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
}));
return metrics.connected && metrics.clientHeight > 0;
}).toBe(true);
if (metrics.scrollHeight > metrics.clientHeight + 1) {
await target.evaluate((element) => { element.scrollTop = element.scrollHeight; });
await expect.poll(() => target.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
}
}
test("shell, overview, repositories and settings remain responsive and accessible", async ({ page }, testInfo) => {
await assertSurface(page);
for (const view of ["overview", "deployments", "settings"]) {
await page.locator(`.nav-button[data-action="navigate"][data-view="${view}"]`).click();
await expect(page.locator("main")).toBeVisible();
await assertSurface(page);
}
await expect(page.locator("html")).toHaveAttribute("data-theme", String(testInfo.project.metadata.theme));
if (testInfo.project.metadata.reduced) {
expect(await page.evaluate(() => matchMedia("(prefers-reduced-motion: reduce)").matches)).toBe(true);
}
});
test("repository changes, Git tools and Git Validator complete their primary flow", async ({ page }) => {
await page.locator('[data-action="select-repo"]').first().click();
await expect(page.locator('[data-action="repo-tab"]')).toHaveCount(6);
for (const tab of ["changes", "history", "deployments", "gittools", "validator", "settings"]) {
const control = page.locator(`[data-action="repo-tab"][data-tab="${tab}"]`);
if (await control.count()) {
await control.click();
await expect(control).toHaveClass(/active/);
await assertSurface(page);
}
}
await page.locator('[data-action="repo-tab"][data-tab="validator"]').click();
await expect(page.locator(".validator-score")).toBeVisible();
await assertScrollableWhenOverflowing(page, ".validator-page");
await expect(page.locator("#validator-policy")).toBeVisible();
await page.locator("#validator-policy").selectOption("production");
await expect(page.locator(".validator-hero")).toContainText(/Production policy/i);
await expect(page.locator(".validator-hero")).toContainText(/review required/i);
await page.keyboard.press("Tab");
await expect(page.locator(":focus")).toBeVisible();
});
test("every long application surface retains a working vertical scroll owner", async ({ page }) => {
for (const view of ["overview", "deployments", "diagnostics", "settings"]) {
await test.step(`${view} view scrolls`, async () => {
const navigation = page.locator(`.nav-button[data-view="${view}"]`);
await navigation.click();
await expect(navigation).toHaveClass(/active/);
await assertScrollableWhenOverflowing(page, ".main-canvas");
});
}
await page.locator('[data-action="select-repo"]').first().click();
for (const tab of ["history", "deployments", "gittools", "validator", "settings"]) {
await page.locator(`[data-action="repo-tab"][data-tab="${tab}"]`).click();
const scrollRoot = page.locator(".repo-content > .tab-page, .repo-content > .validator-page");
if (await scrollRoot.count())
await assertScrollableWhenOverflowing(page, ".repo-content > .tab-page, .repo-content > .validator-page");
}
});
test("deployment inventory supports dense workloads without ambiguous blank cards", async ({ page }) => {
await page.locator('.nav-button[data-action="navigate"][data-view="deployments"]').click();
await expect(page.locator(".deploy-card, .server-inventory-panel .tool-row").first()).toBeVisible();
const cards = page.locator(".deploy-card, .server-inventory-panel .tool-row");
const count = await cards.count();
expect(count).toBeGreaterThan(0);
for (let index = 0; index < Math.min(count, 25); index += 1) {
await expect(cards.nth(index)).not.toHaveText(/^\s*$/);
}
const unresolved = page.locator(".tool-row", { hasText: "Legacy Worker" });
await expect(unresolved).toContainText("Link unresolved");
await expect(unresolved).not.toContainText(/^Linked$/);
await expect(page.locator(".server-inventory-panel").first()).toContainText("1 unresolved");
const repositoryLink = page.locator('[data-action="open-deployment-link"]');
if (await repositoryLink.count()) {
await repositoryLink.first().click();
await expect(page.locator('.repo-row.active')).toHaveAttribute("data-deployment-count", /^[1-9]/);
await expect(page.locator('.repo-row.active .deployment-badge')).toBeVisible();
await expect(page.locator('.tab[data-action="repo-tab"][data-tab="deployments"]')).toHaveClass(/active/);
await expect(page.locator(".repository-workloads")).toBeVisible();
await expect(page.locator(".repository-workload-row").first()).toContainText("Repository linked");
}
await assertSurface(page);
});
test("dialogs expose semantics, labels, keyboard close and focus restoration", async ({ page }) => {
await page.locator('[data-action="select-repo"]').first().click();
const trigger = page.locator('[data-action="edit-deployment-profile"], [data-action="add-deployment-profile"]').first();
if (await trigger.count()) {
await trigger.focus();
await trigger.click();
const dialog = page.locator('[role="dialog"]');
await expect(dialog).toBeVisible();
await expect(dialog.locator("button").first()).toBeVisible();
await page.keyboard.press("Escape");
await expect(dialog).toHaveCount(0);
}
await assertSurface(page);
});
test("onboarding and updater states remain usable without an existing configuration", async ({ page }) => {
await page.evaluate(() => localStorage.setItem("forgeflow-demo-setup", "false"));
await page.reload();
await expect(page.locator(".setup-window")).toBeVisible();
await expect(page.getByRole("heading", { name: "Check this computer" })).toBeVisible();
await page.locator('[data-action="setup-run-preflight"]').click();
await expect(page.locator('[data-action="setup-continue"]')).toBeEnabled();
await assertSurface(page);
await page.evaluate(() => localStorage.setItem("forgeflow-demo-setup", "true"));
await page.reload();
await page.locator('.nav-button[data-view="settings"]').click();
await page.locator('[data-action="check-updates"]').first().click();
await expect(page.locator(".update-card")).toContainText(/ForgeFlow/i);
await assertSurface(page);
});
test("inventory, deployment safety and failure evidence dialogs are reviewable", async ({ page }) => {
await page.locator('.nav-button[data-view="deployments"]').click();
const reconciliation = page.locator('[data-action="plan-server-reconciliation"]').first();
if (await reconciliation.count()) {
await reconciliation.click();
await expect(page.locator('[role="dialog"]')).toContainText(/reconciliation/i);
await page.keyboard.press("Escape");
}
const keyLifecycle = page.locator('[data-action="manage-deploy-key"]').first();
if (await keyLifecycle.count()) {
await keyLifecycle.click();
await expect(page.locator('[role="dialog"]')).toContainText(/Deploy key lifecycle/i);
await page.keyboard.press("Escape");
}
const preflight = page.locator('[data-action="run-deployment-preflight"]').first();
await preflight.click();
await expect(page.locator('[role="dialog"]')).toContainText(/preflight/i);
await page.keyboard.press("Escape");
const failed = page.locator('[data-action="open-operation"]').last();
if (await failed.count()) {
await failed.click();
await expect(page.locator('[role="dialog"]')).toContainText(/failed|failure|healthcheck/i);
await page.keyboard.press("Escape");
}
await assertSurface(page);
});