jobhunt-platform/scripts/screenshots.py
hermes aa858bd2c1
Some checks failed
CI / api-tests (push) Failing after 32s
CI / package-tests (push) Failing after 38s
CI / web-tests (push) Failing after 36s
Screenshot pipeline + fixes found under real render (CORS, batch state stomp, prompt-aware cv_tailor mock)
- scripts/screenshots.py + scripts/Dockerfile.shots: playwright-in-compose
  runner, 7 demo shots against the seeded stack (DinD-safe, output volume)
- docker-compose.yml: api + web services (nginx SPA + /api proxy-less via
  build-arg VITE_API_BASE), shots service with volume output
- apps/api: add CORSMiddleware (SPA origin web:80 could not read api:8000),
  batch scoring now refuses to rewrite applications beyond
  discovered/scored (kanban page load previously reset sent/interviewing
  to scored), prompt-aware cv_tailor mock so demo passes hallucination
  guard, hallucination-guard tests repointed to monkeypatched run_task
- apps/web: Applications kanban batches only unscored applications, red
  flag display falls back to stored rationale
- 159 api tests + 14 web tests green after fixes
2026-07-30 21:40:14 +00:00

84 lines
3 KiB
Python

"""Screenshot runner: drives the seed demo UI and saves PNGs to /out.
Runs inside the compose network; browser resolves 'web' and 'api' directly.
"""
import json
import sys
import time
import urllib.request
from playwright.sync_api import sync_playwright
BASE = "http://web"
API = "http://api:8000/api"
OUT = "/out"
def api(method, path, body=None):
req = urllib.request.Request(
API + path,
data=json.dumps(body).encode() if body else None,
method=method,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
def main():
# Idempotent: ensures full demo dataset exists
seed = api("POST", "/concierge/seed-demo")
print("seed:", json.dumps(seed)[:200])
apps = api("GET", "/applications")
interview = next((a for a in apps if a["state"] == "interviewing"), apps[0])
detail_id = interview["id"]
print("detail id:", detail_id)
shots = [
("welcome", "/", {}), # first-run wizard may redirect away; force below
("welcome", "/welcome", {}),
("today", "/today", {}),
("cv", "/cv", {}),
("research", "/research", {}),
("applications", "/applications", {}),
("detail", f"/applications/{detail_id}", {}),
]
with sync_playwright() as p:
browser = p.chromium.launch(args=["--disable-dev-shm-usage"])
page = browser.new_page(viewport={"width": 1440, "height": 900},
device_scale_factor=2)
for name, path, _opts in shots[1:]: # skip the "/" duplicate
try:
page.goto(BASE + path, wait_until="networkidle", timeout=30000)
page.wait_for_timeout(1200)
# close possible wizard redirect back
if name != "welcome" and page.url.endswith("/welcome"):
page.goto(BASE + path, wait_until="networkidle", timeout=30000)
page.wait_for_timeout(800)
page.screenshot(path=f"{OUT}/{name}.png")
print("shot:", name, "<-", page.url)
except Exception as e:
print("FAILED:", name, type(e).__name__, str(e)[:150])
# Interaction shot: open the Tailor CV panel on an approved application
approved = next((a for a in apps if a["state"] in ("approved", "interviewing")), apps[0])
try:
page.goto(f"{BASE}/applications/{approved['id']}", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(1000)
btn = page.locator('[data-testid="tailor-cv-btn"]')
btn.click(timeout=8000)
page.wait_for_selector('[data-testid="tailor-panel"]', timeout=20000)
page.wait_for_timeout(800)
page.screenshot(path=f"{OUT}/detail-tailor.png")
print("shot: detail-tailor")
except Exception as e:
print("FAILED: detail-tailor", type(e).__name__, str(e)[:150])
browser.close()
print("DONE")
if __name__ == "__main__":
sys.exit(main())