"""Real Chromium DOM + real ASGI backend, bridged in-process (no network deployment test). Requires playwright. Browser cookie/CSRF behavior is covered separately in test_api.py. The bridge uses TestClient's cookie jar and supplies the configured same-origin header. """ import os from pathlib import Path import secrets import sys import tempfile ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "backend" / "src")) from fastapi.testclient import TestClient from playwright.sync_api import sync_playwright from apc.api import Settings, create_app def main(): with tempfile.TemporaryDirectory() as directory: token = secrets.token_urlsafe(32) app = create_app(Settings(token, str(Path(directory) / "smoke.db"))) with TestClient(app) as client, sync_playwright() as pw: browser = pw.chromium.launch(executable_path=os.environ.get("CHROMIUM_PATH") or None, args=["--no-sandbox"]) page = browser.new_page(viewport={"width": 1280, "height": 900}) errors = [] page.on("pageerror", lambda error: errors.append(str(error))) def request(path, options): response = client.request(options.get("method", "GET"), path, content=options.get("body"), headers={"Content-Type": "application/json", "Origin": "http://localhost:8000"}) return {"status": response.status_code, "body": response.text} page.expose_function("asgiRequest", request) html = client.get("/").text.replace('', "") html = html.replace('', "") page.set_content(html) page.add_style_tag(content=client.get("/static/style.css").text) page.evaluate("""() => { window.fetch = async (path, options = {}) => { const response = await window.asgiRequest(path, options); return new Response(response.body, {status: response.status, headers: {'Content-Type': 'application/json'}}); }; }""") page.add_script_tag(content=client.get("/static/app.js").text) page.get_by_label("Administration token").fill(token) page.get_by_role("button", name="Sign in", exact=True).click() page.locator("#dashboard").wait_for(state="visible") for name in ("Example A", "Example B"): page.get_by_label("Display name", exact=True).fill(name) page.get_by_role("button", name="Add child", exact=True).click() page.get_by_role("heading", name=name, exact=True).wait_for() page.locator("#policy [name=daily_minutes]").fill("40") page.get_by_role("button", name="Save shared policy").click() page.get_by_text("Shared policy saved.", exact=False).wait_for() assert page.locator(".child").count() == 2 first = page.locator(".child").first first.get_by_text("Individual exceptions", exact=True).click() first.locator("[name=daily_minutes]").fill("45") first.get_by_role("button", name="Save exceptions").click() page.get_by_text("Exceptions saved.", exact=False).wait_for() first = page.locator(".child").first assert "45 daily minutes" in first.inner_text() first.get_by_role("button", name="Pair phone").click() page.locator("#code-dialog").wait_for(state="visible") code = page.locator("#issued-code").inner_text() paired = client.post("/v1/pair", json={"code": code, "name": "Smoke test"}).json() headers = {"Authorization": f"Bearer {paired['token']}"} challenge = client.post("/v1/device/challenges", headers=headers).json() answers = [] for q in challenge["questions"]: a, op, b = q["prompt"].split() answers.append({"question_id": q["id"], "answer": int(a) + int(b) if op == "+" else int(a) - int(b)}) result = client.post(f"/v1/device/challenges/{challenge['id']}/submit", headers=headers, json={"answers": answers}) assert result.json()["awarded_minutes"] == 10 page.get_by_role("button", name="Close", exact=True).click() page.get_by_role("button", name="Refresh status").click() page.get_by_text("45 daily minutes + 10 earned today", exact=False).wait_for() if os.environ.get("APC_SCREENSHOT"): page.screenshot(path=os.environ["APC_SCREENSHOT"], full_page=True) page.get_by_role("button", name="Sign out").click() page.locator("#login-panel").wait_for(state="visible") assert not errors, errors browser.close() print("Browser/ASGI smoke passed: login, two children, policy, override, pairing, reward, logout") if __name__ == "__main__": main()