from __future__ import annotations
import os
import threading
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from playwright.sync_api import expect, sync_playwright
def assert_catalog_contract(site_dir: Path) -> None:
import json
payload = json.loads((site_dir / "groundwork.json").read_text(encoding="utf-8"))
capabilities = payload["capabilities"]
assert len(capabilities) == 53
node_exporter = next(item for item in capabilities if item["role"] == "node_exporter")
assert node_exporter["adoption_status"] == "bundled"
assert node_exporter["release_gate"] == "standard"
def start_server(directory: Path) -> tuple[ThreadingHTTPServer, str]:
handler = partial(SimpleHTTPRequestHandler, directory=str(directory))
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
host, port = server.server_address
return server, f"http://{host}:{port}/"
def wait_for_service_worker_control(page) -> None:
controlled = page.evaluate(
"""
async () => {
if (!("serviceWorker" in navigator)) return false;
await navigator.serviceWorker.ready;
if (navigator.serviceWorker.controller) return true;
await new Promise((resolve) => {
navigator.serviceWorker.addEventListener("controllerchange", resolve, { once: true });
});
return Boolean(navigator.serviceWorker.controller);
}
"""
)
assert controlled, "page must be controlled by the service worker before offline checks"
def main() -> None:
site_dir = Path(os.environ.get("NIDO_PAGES_SITE_DIR", "target/pages-site"))
base_url = os.environ.get("NIDO_PAGES_BASE_URL")
server = None
if not base_url:
if not site_dir.exists():
raise SystemExit(f"missing site artifact: {site_dir}")
assert_catalog_contract(site_dir)
server, base_url = start_server(site_dir)
try:
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
context = browser.new_context(viewport={"width": 1440, "height": 960})
page = context.new_page()
console_errors: list[str] = []
page.on("console", lambda msg: console_errors.append(msg.text) if msg.type == "error" else None)
page.goto(base_url, wait_until="networkidle")
wait_for_service_worker_control(page)
expect(page).to_have_title("Nido Groundwork")
expect(page.locator("[data-total-count]")).to_have_text("53")
expect(page.locator("[data-gated-count]")).to_have_text("7")
expect(page.locator("[data-layer-count]")).not_to_have_text("0")
expect(page.locator("[data-role='minio']")).to_be_visible()
page.evaluate("localStorage.setItem('nido-groundwork-compare', 'not-json')")
page.reload(wait_until="networkidle")
expect(page.locator("[data-total-count]")).to_have_text("53")
page.locator("#search").fill("minio")
expect(page.locator("[data-visible-count]")).to_have_text("1")
expect(page.locator(".detail-title")).to_have_text("minio")
expect(page.locator(".command-box code")).to_contain_text("nido ansible show-groundwork minio --json")
page.locator("button[data-gate='license-review']").click()
page.locator("#search").fill("")
expect(page.locator("[data-visible-count]")).to_have_text("7")
expect(page.locator("[data-role='grafana']")).to_be_visible()
expect(page.locator("[data-role='keycloak']")).to_have_count(0)
page.locator("#clearFilters").click()
page.locator("button[data-adoption='bundled']").click()
expect(page.locator("[data-visible-count]")).to_have_text("1")
expect(page.locator("[data-role='node_exporter']")).to_be_visible()
page.locator("#clearFilters").click()
page.locator("[data-role='grafana']").click()
page.get_by_role("button", name="Pin").click()
expect(page.locator(".compare-table")).to_contain_text("grafana")
expect(page.locator(".compare-table")).to_contain_text("Observability")
page.reload(wait_until="networkidle")
expect(page.locator(".compare-table")).to_contain_text("grafana")
manifest = page.goto(f"{base_url}manifest.webmanifest")
assert manifest and manifest.ok, "manifest must load"
sw = page.goto(f"{base_url}sw.js")
assert sw and sw.ok, "service worker must load"
page.goto(base_url, wait_until="networkidle")
wait_for_service_worker_control(page)
console_errors.clear()
context.set_offline(True)
page.reload(wait_until="domcontentloaded")
expect(page.locator("[data-total-count]")).to_have_text("53")
expect(page.locator("[data-role='minio']")).to_be_visible()
context.set_offline(False)
assert not console_errors, "offline reload console errors: " + " | ".join(console_errors)
mobile = browser.new_page(viewport={"width": 390, "height": 844}, is_mobile=True)
mobile.goto(base_url, wait_until="networkidle")
expect(mobile.locator(".brand-name")).to_have_text("Nido")
expect(mobile.locator("[data-role='minio']")).to_be_visible()
overflow = mobile.evaluate("document.documentElement.scrollWidth - window.innerWidth")
assert overflow <= 1, f"mobile layout has horizontal overflow: {overflow}px"
mobile.close()
context.close()
browser.close()
assert not console_errors, "console errors: " + " | ".join(console_errors)
finally:
if server:
server.shutdown()
if __name__ == "__main__":
main()