"use strict"; const { chromium } = require("playwright"); const assert = require("assert"); const fs = require("fs"); const path = require("path"); const root = process.cwd(); const fileUrl = "file://" + path.join(root, "index.html"); const profile = path.join(root, "verification", "browser-profile-release"); const results = []; const failures = []; fs.mkdirSync(profile, { recursive: true }); function sameIds(actual, expected) { assert.deepStrictEqual([...actual].sort(), [...expected].sort()); } async function run() { const context = await chromium.launchPersistentContext(profile, { headless: true, executablePath: "/usr/bin/google-chrome", viewport: { width: 1440, height: 1000 }, reducedMotion: "reduce", args: ["--no-sandbox", "--disable-dev-shm-usage"] }); const page = context.pages()[0] || await context.newPage(); const consoleErrors = []; const pageErrors = []; const requestFailures = []; const requests = []; page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); page.on("pageerror", (error) => pageErrors.push(String(error))); page.on("requestfailed", (request) => { requestFailures.push(`${request.url()} :: ${request.failure()?.errorText}`); }); page.on("request", (request) => requests.push(request.url())); async function ready(url = fileUrl) { await page.goto(url, { waitUntil: "load" }); await page.waitForFunction(() => ( window.__TIMELINE_APP__ && document.querySelectorAll('[data-testid="arc-card"]').length > 0 )); } async function ids() { return page.locator('[data-testid="arc-card"][data-arc-id]').evaluateAll( (nodes) => nodes.map((node) => node.dataset.arcId) ); } async function check(name, assertion) { try { await assertion(); results.push({ name, status: "PASS" }); } catch (error) { const detail = String(error?.stack || error); failures.push({ name, detail }); results.push({ name, status: "FAIL", detail: detail.split("\n")[0] }); } } try { await ready(); await check("default STORY + Alle = 10", async () => { sameIds(await ids(), [ "ARC-01", "ARC-02", "ARC-03", "ARC-04", "ARC-05", "ARC-06", "ARC-07", "ARC-08", "ARC-09", "ARC-10" ]); assert.strictEqual( (await page.evaluate(() => window.__TIMELINE_APP__.getState())).view, "STORY" ); assert.strictEqual( await page.locator('button[data-view="STORY"]').getAttribute("aria-pressed"), "true" ); const overlaps = await page.locator(".thread-row").evaluateAll((rows) => { const collisions = []; rows.forEach((row, rowIndex) => { const boxes = [...row.querySelectorAll(".thread-node")] .map((node) => ({ id: node.dataset.openArc, rect: node.getBoundingClientRect() })) .sort((left, right) => left.rect.left - right.rect.left); for (let index = 1; index < boxes.length; index += 1) { if (boxes[index - 1].rect.right > boxes[index].rect.left + 0.5) { collisions.push(`${rowIndex}:${boxes[index - 1].id}/${boxes[index].id}`); } } }); return collisions; }); assert.deepStrictEqual(overlaps, []); }); await check("Codex path exact set", async () => { await page.locator('[data-harness="Codex"]').click(); sameIds(await ids(), ["ARC-01", "ARC-06", "ARC-09"]); }); await check("Codex OR Hermes exact union", async () => { await page.locator('[data-harness="Hermes / HAI"]').click(); sameIds(await ids(), [ "ARC-01", "ARC-06", "ARC-07", "ARC-08", "ARC-09", "ARC-10" ]); }); await check("empty state + reset", async () => { await page.locator('[data-harness="Alle"]').click(); await page.locator('[data-harness="OpenCode"]').click(); await page.locator("#stage-filter").selectOption("SYSTEMATISIERT"); sameIds(await ids(), []); assert.strictEqual(await page.locator('[data-state="empty"]').isVisible(), true); await page.locator( '[data-state="empty"] [data-action="reset-filters"]' ).click(); sameIds(await ids(), [ "ARC-01", "ARC-02", "ARC-03", "ARC-04", "ARC-05", "ARC-06", "ARC-07", "ARC-08", "ARC-09", "ARC-10" ]); }); let savedHash = ""; await check("hash roundtrip preserves full state", async () => { await page.locator('button[data-view="INSIGHTS"]').click(); await page.locator('[data-harness="Codex"]').click(); await page.locator('[data-harness="Hermes / HAI"]').click(); await page.locator("#stage-filter").selectOption("TRANSFERIERT"); await page.locator("#confidence-filter").selectOption("BELEGT"); await page.locator("#concept-filter").selectOption("Orchestrator / Worker"); const expected = { view: "INSIGHTS", paths: ["Codex", "Hermes / HAI"], stage: "TRANSFERIERT", confidence: "BELEGT", concept: "Orchestrator / Worker" }; assert.deepStrictEqual( await page.evaluate(() => window.__TIMELINE_APP__.getState()), expected ); savedHash = await page.evaluate(() => location.hash); await page.reload({ waitUntil: "load" }); await page.waitForFunction(() => window.__TIMELINE_APP__); assert.deepStrictEqual( await page.evaluate(() => window.__TIMELINE_APP__.getState()), expected ); await page.locator('button[data-view="STORY"]').click(); sameIds(await ids(), ["ARC-09"]); const after = await page.evaluate(() => window.__TIMELINE_APP__.getState()); assert.deepStrictEqual( { paths: after.paths, stage: after.stage, confidence: after.confidence, concept: after.concept }, { paths: expected.paths, stage: expected.stage, confidence: expected.confidence, concept: expected.concept } ); assert.ok( savedHash.includes("view=INSIGHTS") && savedHash.includes("paths=Codex%2CHermes") ); }); await check("invalid hash notice + safe default", async () => { await ready(fileUrl + "#view=NOPE"); assert.strictEqual( await page.locator('[data-state="invalid-hash"]').isVisible(), true ); sameIds(await ids(), [ "ARC-01", "ARC-02", "ARC-03", "ARC-04", "ARC-05", "ARC-06", "ARC-07", "ARC-08", "ARC-09", "ARC-10" ]); assert.strictEqual( (await page.evaluate(() => window.__TIMELINE_APP__.getState())).view, "STORY" ); }); await check("HAI depth contract, month interaction, Goal and Permissions", async () => { await page.locator('button[data-view="INSIGHTS"]').click(); assert.strictEqual(await page.locator('[data-testid="hai-month-card"]').count(), 7); assert.strictEqual(await page.locator('[data-zoom-id]').count(), 5); assert.strictEqual(await page.locator('[data-rule-id]').count(), 11); assert.strictEqual(await page.locator('[data-metric-id]').count(), 3); assert.ok( (await page.locator('.hai-owner-thesis__question').innerText()) .startsWith("Wenn heutiges HAI in jedem Monat neben Samuel gesessen hätte") ); const january = page.locator('[data-hai-month="MHAI-01"]'); const february = page.locator('[data-hai-month="MHAI-02"]'); assert.ok((await january.innerText()).includes("NO EVIDENCE")); assert.ok((await february.innerText()).includes("NO EVIDENCE")); await page.locator('[data-hai-month="MHAI-05"]').click(); const mayText = await page.locator('#hai-month-detail').textContent(); assert.ok(mayText.includes("/goal")); assert.ok(mayText.includes("11931")); assert.ok(mayText.includes("19.05.2026 23:08:59")); assert.ok(mayText.includes("Permissions")); assert.ok(mayText.includes("Human Gates")); const march = page.locator('[data-hai-month="MHAI-03"]'); await march.focus(); await page.keyboard.press("ArrowRight"); assert.strictEqual( await page.locator('[data-hai-month="MHAI-04"]').getAttribute("aria-selected"), "true" ); assert.strictEqual( await page.locator('#hai-month-detail').getAttribute("data-month-id"), "MHAI-04" ); await page.locator('button[data-view="STORY"]').click(); }); await check("keyboard dialog, evidence, Escape, focus return", async () => { await page.locator('button[data-view="STORY"]').click(); const trigger = page.locator('.arc-card[data-arc-id="ARC-06"] .arc-open'); await trigger.focus(); await page.keyboard.press("Enter"); await page.locator("#arc-dialog[open]").waitFor(); assert.strictEqual( await page.locator('[data-action="close-detail"]').evaluate( (element) => element === document.activeElement ), true ); const dialogText = (await page.locator("#arc-dialog").innerText()).toLowerCase(); for (const label of [ "Ausgangslage", "Trigger", "Neue Fähigkeit / Verhalten", "Positiver Effekt", "Negativer Effekt", "Adaption", "Transfer", "Bleibendes Prinzip", "Evidenz & Unsicherheit" ]) { assert.ok(dialogText.includes(label.toLowerCase()), label); } const summary = page.locator("#arc-dialog .evidence-drawer summary"); await summary.focus(); await page.keyboard.press("Enter"); assert.strictEqual( await page.locator("#arc-dialog .evidence-drawer").getAttribute("open") !== null, true ); await page.keyboard.press("Escape"); await page.waitForFunction(() => !document.querySelector("#arc-dialog").open); assert.strictEqual( await page.evaluate(() => document.activeElement?.dataset?.openArc), "ARC-06" ); }); await check("reduced motion active", async () => { assert.strictEqual( await page.evaluate(() => matchMedia("(prefers-reduced-motion: reduce)").matches), true ); const animation = await page.locator(".arc-card").first().evaluate( (element) => getComputedStyle(element).animationDuration ); assert.ok( animation === "0s" || animation === "0.001s" || animation === "none", animation ); }); await check("offline request boundary + clean console", async () => { const remote = requests.filter((url) => !/^(file:|data:|blob:)/.test(url)); assert.deepStrictEqual(remote, []); assert.deepStrictEqual(consoleErrors, []); assert.deepStrictEqual(pageErrors, []); assert.deepStrictEqual(requestFailures, []); }); await ready(); await page.evaluate(() => scrollTo(0, 0)); await page.screenshot({ path: path.join(root, "verification", "all-paths-desktop.png"), fullPage: true }); await page.locator('button[data-view="INSIGHTS"]').click(); await page.locator('[data-hai-month="MHAI-05"]').click(); await page.evaluate(() => scrollTo(0, 0)); await page.screenshot({ path: path.join(root, "verification", "insights-all-desktop.png"), fullPage: true }); await ready(); await page.locator('[data-harness="Codex"]').click(); sameIds(await ids(), ["ARC-01", "ARC-06", "ARC-09"]); await page.evaluate(() => scrollTo(0, 0)); await page.screenshot({ path: path.join(root, "verification", "codex-only-desktop.png"), fullPage: true }); await check("mobile layout, dialog fullscreen, no overflow", async () => { await page.setViewportSize({ width: 390, height: 844 }); await page.locator('[data-harness="Harness-agnostisch"]').click(); sameIds(await ids(), ["ARC-01", "ARC-06", "ARC-09"]); const overflow = await page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, innerWidth })); assert.ok( overflow.scrollWidth <= overflow.innerWidth + 1, JSON.stringify(overflow) ); await page.locator('.arc-card[data-arc-id="ARC-06"] .arc-open').click(); const rect = await page.locator("#arc-dialog").evaluate((element) => { const bounds = element.getBoundingClientRect(); return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, viewportWidth: innerWidth, viewportHeight: innerHeight }; }); assert.ok( rect.width >= rect.viewportWidth - 2 && rect.height >= rect.viewportHeight - 2, JSON.stringify(rect) ); await page.keyboard.press("Escape"); await page.waitForFunction(() => !document.querySelector("#arc-dialog").open); await page.evaluate(() => scrollTo(0, 0)); await page.evaluate(() => document.activeElement?.blur()); await page.screenshot({ path: path.join(root, "verification", "mobile.png"), fullPage: true }); await page.locator('button[data-view="INSIGHTS"]').click(); await page.locator('[data-hai-month="MHAI-07"]').click(); await page.evaluate(() => scrollTo(0, 0)); await page.evaluate(() => document.activeElement?.blur()); await page.screenshot({ path: path.join(root, "verification", "insights-mobile.png"), fullPage: true }); const insightsOverflow = await page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, innerWidth })); assert.ok( insightsOverflow.scrollWidth <= insightsOverflow.innerWidth + 1, JSON.stringify(insightsOverflow) ); }); await check("screenshot files written", async () => { for (const name of [ "all-paths-desktop.png", "codex-only-desktop.png", "mobile.png", "insights-all-desktop.png", "insights-mobile.png" ]) { const screenshot = path.join(root, "verification", name); assert.ok( fs.existsSync(screenshot) && fs.statSync(screenshot).size > 10000, name ); } }); console.log(JSON.stringify({ results, observations: { savedHash, requests: [...new Set(requests)], consoleErrors, pageErrors, requestFailures }, failures }, null, 2)); } finally { await context.close(); } if (failures.length) process.exitCode = 1; } run().catch((error) => { console.error(error); process.exitCode = 1; });