Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Build generated dashboard fixture
run: python tests/build_browser_dashboard_fixture.py
- name: Set up Node.js
uses: actions/setup-node@v4
with:
Expand Down
8 changes: 4 additions & 4 deletions generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,16 +616,16 @@ def _job_row(j):
<div class="panel"><h2>Scheduled jobs &amp; health</h2><table><tbody>{job_rows}</tbody></table></div>
<div class="panel"><h2>Recent agent actions</h2>
<div class="chips" role="group" aria-label="Time range filter">
<button class="chip active" data-range="7">Last 7 days</button>
<button class="chip" data-range="1">Last 24h</button>
<button class="chip active" data-range="7" aria-pressed="true">Last 7 days</button>
<button class="chip" data-range="1" aria-pressed="false">Last 24h</button>
</div>
<table id="actions-table"><tbody>{act_rows}</tbody></table></div>
<script>
(function(){{
var chips=document.querySelectorAll('.chip');
chips.forEach(function(c){{c.addEventListener('click',function(){{
chips.forEach(function(x){{x.classList.remove('active')}});
c.classList.add('active');
chips.forEach(function(x){{x.classList.remove('active');x.setAttribute('aria-pressed','false')}});
c.classList.add('active');c.setAttribute('aria-pressed','true');
var cutoff=Date.now()-(parseInt(c.dataset.range)*864e5);
document.querySelectorAll('#actions-table tr[data-ts]').forEach(function(tr){{
var ts=Date.parse(tr.dataset.ts);
Expand Down
46 changes: 46 additions & 0 deletions tests/build_browser_dashboard_fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from datetime import datetime, timedelta, timezone
import json
import sqlite3
from pathlib import Path
import sys

root = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(root))
import generate
site = root / "site"
fixtures = root / ".browser-fixtures"
fixtures.mkdir(exist_ok=True)

now = datetime.now(timezone.utc)
jobs_path = fixtures / "jobs.json"
jobs_path.write_text(json.dumps([{
"id": "job-disabled",
"name": "Disabled required job",
"schedule": {"display": "every 6h"},
"enabled": False,
"required": True,
"created_at": (now - timedelta(days=3)).isoformat(),
}], ensure_ascii=False), encoding="utf-8")

db_path = fixtures / "executions.db"
if db_path.exists():
db_path.unlink()
with sqlite3.connect(db_path) as db:
db.execute("""CREATE TABLE executions (
job_id TEXT, status TEXT, started_at TEXT, finished_at TEXT, error TEXT
)""")
db.executemany(
"INSERT INTO executions VALUES (?, ?, ?, ?, ?)",
[
("job-disabled", "succeeded", (now - timedelta(days=2)).isoformat(), None, None),
("job-disabled", "succeeded", (now - timedelta(hours=2)).isoformat(), None, None),
],
)

cfg = json.loads(json.dumps(generate.DEFAULT_CONFIG))
cfg["title"] = "Browser acceptance dashboard"
cfg["theme"] = "light"
cfg["sources"] = {key: None for key in cfg["sources"]}
cfg["sources"]["cron_jobs_glob"] = str(jobs_path)
cfg["sources"]["executions_db"] = str(db_path)
generate.build(cfg, str(site / "generated-dashboard-test.html"))
40 changes: 38 additions & 2 deletions tests/ui_browser_test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ async function main() {
{ path: "/", name: "home" },
{ path: "/demo.html", name: "demo" },
{ path: "/proofs.html", name: "proofs" },
{ path: "/generated-dashboard-test.html", name: "generated-dashboard" },
];

try {
Expand Down Expand Up @@ -122,13 +123,48 @@ async function main() {
assert(focused, "proof explorer: keyboard Tab does not focus the skip link");
}


if (item.name === "generated-dashboard" && width === 320) {
await page.keyboard.press("Tab");
const focused = await page.evaluate(() => document.activeElement?.classList.contains("skip-link"));
assert(focused, "generated dashboard: keyboard Tab does not focus the skip link");
const last24 = page.locator('.chip[data-range="1"]');
await last24.click();
assert(await last24.getAttribute("aria-pressed") === "true", "generated dashboard: 24-hour filter state missing");
const rows = page.locator("#actions-table tr[data-ts]");
assert(await rows.count() === 2, "generated dashboard: expected recent and historical actions");
assert(await rows.nth(1).evaluate(el => getComputedStyle(el).display === "none"),
"generated dashboard: 24-hour filter did not hide historical action");
const last7 = page.locator('.chip[data-range="7"]');
await last7.click();
assert(await last7.getAttribute("aria-pressed") === "true", "generated dashboard: 7-day filter state missing");
assert(await rows.nth(1).evaluate(el => getComputedStyle(el).display !== "none"),
"generated dashboard: 7-day filter did not restore historical action");
const ack = page.locator(".ackbtn");
assert(await ack.count() === 1, "generated dashboard: expected one acknowledge control");
await ack.click();
assert((await ack.textContent()).trim() === "Seen" && await ack.isDisabled(),
"generated dashboard: acknowledge action did not update state");
await page.reload({ waitUntil: "networkidle" });
const persistedAck = page.locator(".ackbtn");
assert((await persistedAck.textContent()).trim() === "Seen" && await persistedAck.isDisabled(),
"generated dashboard: acknowledgement did not persist locally");
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
const copy = page.locator("#sharebtn");
await copy.click();
await page.waitForFunction(() => document.getElementById("sharebtn")?.textContent === "Copied");
const copied = await page.evaluate(() => navigator.clipboard.readText());
assert(copied.includes("Browser acceptance dashboard"), "generated dashboard: copied summary omitted title");
assert(copied.includes("Disabled required job"), "generated dashboard: copied summary omitted alert");
}

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");
console.log("Browser UI acceptance passed: 4 pages × 5 widths; public-page navigation, generated-dashboard filters/acknowledgement/copy, mobile table visibility, keyboard navigation and both Habitat proof bundles verified.");
console.log("Screenshots: artifacts/{home,demo,proofs,generated-dashboard}-{320,1440}.png");
} finally {
await browser.close();
await new Promise(resolve => server.close(resolve));
Expand Down
Loading