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 page.locator(`.nav-button[data-view="${view}"]`).click(); 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('.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); });