From 0f5058a1be49ea6e8a95218063ffbfd3b4d12ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rama=20=E2=80=94=20Buttercode=20Systems?= Date: Thu, 12 Nov 2026 16:24:32 +0200 Subject: [PATCH 1/5] test: add Playwright browser acceptance for all public UI pages --- tests/ui_browser_test.cjs | 140 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 tests/ui_browser_test.cjs diff --git a/tests/ui_browser_test.cjs b/tests/ui_browser_test.cjs new file mode 100644 index 0000000..d7b450e --- /dev/null +++ b/tests/ui_browser_test.cjs @@ -0,0 +1,140 @@ +const http = require("node:http"); +const fs = require("node:fs"); +const path = require("node:path"); +const { chromium } = require("playwright"); + +const root = path.resolve(__dirname, ".."); +const artifacts = path.join(root, "artifacts"); +fs.mkdirSync(artifacts, { recursive: true }); + +const mime = { + ".html": "text/html; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", +}; + +const server = http.createServer((req, res) => { + let pathname; + try { + pathname = decodeURIComponent(new URL(req.url, "http://127.0.0.1").pathname); + } catch { + res.writeHead(400).end("Bad request"); + return; + } + if (pathname.endsWith("/")) pathname += "index.html"; + const file = path.resolve(root, "." + pathname); + if (!file.startsWith(root + path.sep)) { + res.writeHead(403).end("Forbidden"); + return; + } + fs.readFile(file, (error, data) => { + if (error) { + res.writeHead(404).end("Not found"); + return; + } + res.writeHead(200, { + "Content-Type": mime[path.extname(file)] || "application/octet-stream", + "Cache-Control": "no-store", + }); + res.end(data); + }); +}); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +async function main() { + await new Promise(resolve => server.listen(4173, "127.0.0.1", resolve)); + const browser = await chromium.launch({ headless: true, args: ["--no-sandbox"] }); + const errors = []; + const widths = [320, 375, 768, 1120, 1440]; + const pages = [ + { path: "/", name: "home" }, + { path: "/demo.html", name: "demo" }, + { path: "/proofs.html", name: "proofs" }, + ]; + + try { + for (const item of pages) { + for (const width of widths) { + const page = await browser.newPage({ viewport: { width, height: 900 }, deviceScaleFactor: 1 }); + page.on("pageerror", error => errors.push(item.name + "@" + width + ": " + error.message)); + await page.goto("http://127.0.0.1:4173" + item.path, { waitUntil: "networkidle" }); + await page.waitForTimeout(100); + + const metrics = await page.evaluate(() => ({ + viewport: document.documentElement.clientWidth, + document: document.documentElement.scrollWidth, + title: document.title, + })); + assert(metrics.document <= metrics.viewport + 1, + item.name + "@" + width + ": horizontal page overflow (" + metrics.document + "px > " + metrics.viewport + "px)"); + assert(metrics.title.length > 0, item.name + "@" + width + ": missing title"); + + if (width === 320 || width === 1440) { + await page.screenshot({ + path: path.join(artifacts, item.name + "-" + width + ".png"), + fullPage: true, + }); + } + + if (item.name === "home" && width === 320) { + await page.keyboard.press("Tab"); + const focused = await page.evaluate(() => document.activeElement?.classList.contains("skip-link")); + assert(focused, "homepage: keyboard Tab does not focus the skip link"); + assert(await page.locator('a[href="/demo.html"]').count() === 1, "homepage: demo navigation link missing"); + assert(await page.locator('a[href="/proofs.html"]').count() === 1, "homepage: proof navigation link missing"); + } + + if (item.name === "demo" && width === 320) { + const cadenceVisible = await page.locator(".table th").nth(1).evaluate(el => getComputedStyle(el).display !== "none"); + const statusVisible = await page.locator(".act span:last-child").first().evaluate(el => getComputedStyle(el).display !== "none"); + const tableScroll = await page.locator(".table-scroll").evaluate(el => el.scrollWidth > el.clientWidth); + assert(cadenceVisible, "demo@320: cadence column is hidden"); + assert(statusVisible, "demo@320: action outcome is hidden"); + assert(tableScroll, "demo@320: narrow table has no contained horizontal scroll"); + const last24 = page.locator('.filter[data-days="1"]'); + await last24.click(); + assert(await last24.getAttribute("aria-pressed") === "true", "demo: 24-hour filter did not expose pressed state"); + assert(await page.locator('.act[data-age="26"]').evaluate(el => getComputedStyle(el).display === "none"), + "demo: 24-hour filter did not hide older activity"); + await page.locator('.filter[data-days="7"]').click(); + assert(await page.locator('.act[data-age="26"]').evaluate(el => getComputedStyle(el).display !== "none"), + "demo: 7-day filter did not restore older activity"); + } + + if (item.name === "proofs" && width === 320) { + await page.waitForFunction(() => document.querySelectorAll("#proof-list .proof").length === 2); + const cards = page.locator("#proof-list .proof"); + assert(await cards.count() === 2, "proof explorer: expected two historical proof cards"); + for (let i = 0; i < await cards.count(); i++) { + const card = cards.nth(i); + const result = (await card.locator(".check").first().textContent()).trim(); + assert(result === "BUNDLE CHECKS PASSED", "proof explorer: card " + i + " failed: " + result); + assert((await card.locator(".check").nth(1).textContent()).includes("LEDGER EXPORT: INTACT"), + "proof explorer: card " + i + " ledger export status missing"); + } + await page.keyboard.press("Tab"); + const focused = await page.evaluate(() => document.activeElement?.classList.contains("skip-link")); + assert(focused, "proof explorer: keyboard Tab does not focus the skip link"); + } + + await page.close(); + } + } + + assert(errors.length === 0, "browser runtime errors:\n" + errors.join("\n")); + console.log("Browser UI acceptance passed: 3 pages × 5 widths; mobile filters, table visibility, keyboard navigation and both Habitat proof bundles verified."); + console.log("Screenshots: artifacts/{home,demo,proofs}-{320,1440}.png"); + } finally { + await browser.close(); + await new Promise(resolve => server.close(resolve)); + } +} + +main().catch(error => { + console.error(error); + process.exitCode = 1; +}); From 5a81383003da51c96e52c8c82e00b1753e25e370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rama=20=E2=80=94=20Buttercode=20Systems?= Date: Thu, 12 Nov 2026 16:24:45 +0200 Subject: [PATCH 2/5] ci: run responsive Playwright acceptance and retain screenshots --- .github/workflows/test.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 57138b9..ecc0b14 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,3 +17,26 @@ jobs: run: pip install -e ".[test]" - name: Run tests run: python -m pytest -q + + ui-browser: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + - name: Install Playwright + run: npm install --no-save --no-package-lock playwright + - name: Install Chromium + run: npx playwright install --with-deps chromium + - name: Run responsive browser acceptance + run: node tests/ui_browser_test.cjs + - name: Upload UI screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: agentscope-ui-browser-screenshots + path: artifacts/*.png + if-no-files-found: ignore From d7378f7553ffb8a993751d6030c2f6abf1fdbc61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rama=20=E2=80=94=20Buttercode=20Systems?= Date: Thu, 12 Nov 2026 16:25:45 +0200 Subject: [PATCH 3/5] fix: serve the actual Vercel site output directory in browser tests --- tests/ui_browser_test.cjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ui_browser_test.cjs b/tests/ui_browser_test.cjs index d7b450e..01e09b7 100644 --- a/tests/ui_browser_test.cjs +++ b/tests/ui_browser_test.cjs @@ -4,6 +4,7 @@ const path = require("node:path"); const { chromium } = require("playwright"); const root = path.resolve(__dirname, ".."); +const siteRoot = path.join(root, "site"); const artifacts = path.join(root, "artifacts"); fs.mkdirSync(artifacts, { recursive: true }); @@ -23,8 +24,8 @@ const server = http.createServer((req, res) => { return; } if (pathname.endsWith("/")) pathname += "index.html"; - const file = path.resolve(root, "." + pathname); - if (!file.startsWith(root + path.sep)) { + const file = path.resolve(siteRoot, "." + pathname); + if (!file.startsWith(siteRoot + path.sep)) { res.writeHead(403).end("Forbidden"); return; } From c6af40d073abe7002c6d4e794f337512ed752e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rama=20=E2=80=94=20Buttercode=20Systems?= Date: Thu, 12 Nov 2026 16:26:47 +0200 Subject: [PATCH 4/5] test: accept multiple homepage entry points to public pages --- tests/ui_browser_test.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ui_browser_test.cjs b/tests/ui_browser_test.cjs index 01e09b7..3f311de 100644 --- a/tests/ui_browser_test.cjs +++ b/tests/ui_browser_test.cjs @@ -85,8 +85,8 @@ async function main() { await page.keyboard.press("Tab"); const focused = await page.evaluate(() => document.activeElement?.classList.contains("skip-link")); assert(focused, "homepage: keyboard Tab does not focus the skip link"); - assert(await page.locator('a[href="/demo.html"]').count() === 1, "homepage: demo navigation link missing"); - assert(await page.locator('a[href="/proofs.html"]').count() === 1, "homepage: proof navigation link missing"); + assert(await page.locator('a[href="/demo.html"]').count() >= 1, "homepage: demo navigation link missing"); + assert(await page.locator('a[href="/proofs.html"]').count() >= 1, "homepage: proof navigation link missing"); } if (item.name === "demo" && width === 320) { From eaa0992cb324566a3813451f39eb6515a45ae8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rama=20=E2=80=94=20Buttercode=20Systems?= Date: Thu, 12 Nov 2026 16:28:19 +0200 Subject: [PATCH 5/5] fix: restore line breaks in browser proof fetch script --- site/proofs.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/proofs.html b/site/proofs.html index 5b5cb9e..be2b606 100644 --- a/site/proofs.html +++ b/site/proofs.html @@ -160,7 +160,9 @@ if(proofUrl.origin!==location.origin)throw Error("Proof URL must remain on this site"); const proofResponse=await fetch(proofUrl.href,{cache:"no-store"}); if(!proofResponse.ok)throw Error("Could not load proof ("+proofResponse.status+")"); - const raw=await proofResponse.text();\n if(new TextEncoder().encode(raw).length>16*1024*1024)throw Error("Proof bundle exceeds 16 MiB");\n const bundle=JSON.parse(raw); + const raw=await proofResponse.text(); + if(new TextEncoder().encode(raw).length>16*1024*1024)throw Error("Proof bundle exceeds 16 MiB"); + const bundle=JSON.parse(raw); const result=await verify(bundle,item.content_sha256); card.append(el("p","claim",bundle.claim&&bundle.claim.claim?bundle.claim.claim:"Claim text unavailable")); const checks=el("div","checks");