#!/usr/bin/env python3 """ capture-playground.py — Tar screenshots av Playground v3 i alle 4 surfaces × 2 themes. Bruker Playwright Python (sync API). Åpner playground/ms-ai-architect-playground.html direkte fra disk via file:// URL og populerer state programmatisk via window.__store før hvert screenshot. Dette gir reproduserbare screenshots uten å klikke gjennom hele brukerflyten manuelt. Output: playground/screenshots/*.png Kjøring: python3 scripts/screenshots/capture-playground.py Krav: Playwright + chromium installert. På macOS: pip3 install playwright playwright install chromium """ import json import sys from pathlib import Path from playwright.sync_api import sync_playwright, Page PLUGIN_ROOT = Path(__file__).resolve().parent.parent.parent HTML_PATH = PLUGIN_ROOT / "playground" / "ms-ai-architect-playground.html" FIXTURES_DIR = PLUGIN_ROOT / "playground" / "test-fixtures" SCREENSHOTS_DIR = PLUGIN_ROOT / "playground" / "screenshots" VIEWPORT = {"width": 1440, "height": 900} TALL_VIEWPORT = {"width": 1440, "height": 1200} # for surfaces med mer innhold # Demo-data — Statens vegvesen ANPR-eksempelet (matcher fixtures). DEMO_SHARED = { "organization": { "name": "Statens vegvesen", "description": "Trafikketat ansvarlig for vegnett, kjøretøy og sjåføropplæring", "sector": "Statlig", "size": "2000-10000", "regulatory_requirements": [ "Personopplysningsloven/GDPR", "Sikkerhetsloven", "Forvaltningsloven", "Offentleglova", ], }, "technology": { "cloud_platform": ["Azure", "M365"], "license_type": "E5", "ai_services_in_use": ["Azure OpenAI", "Copilot for M365", "Azure AI Search"], }, "security": { "data_classification": ["Intern", "Fortrolig"], "data_residency": "Norge", "dpia_practice": "Systematisk", "certifications": "ISO 27001, ISO/IEC 42001 (under etablering)", }, "architecture": { "preferred_platform": "Azure AI Foundry", "integration_needs": ["M365", "SharePoint", "Fagsystemer", "REST API-er"], "annual_ai_budget": "2M-10M", }, "business": { "governance_model": "Sentralisert", "doc_format_preferences": ["Markdown", "SharePoint Wiki"], "reference_architecture": "TOGAF + intern AI-styringsmodell", }, } DEMO_PROJECTS = [ { "id": "proj-anpr-2026", "name": "ANPR Trafikkanalyse", "description": "Automatisk skiltgjenkjenning for trafikkflyt-analyse på E18.", "createdAt": "2026-04-15T09:00:00Z", "local": { "system_name": "ANPR Trafikkanalyse", "system_description": "Sanntids skiltgjenkjenning av kjøretøy på utvalgte strekninger på E18 for trafikkflyt-analyse.", "interaction_type": "automatisering", "users": "Vegtrafikkavdelingen + analytikere", "risk_level_assumption": "Høy", "risk_classification": "Høy", "org_role": "Provider", "data_sources": "Kameraer langs E18, kjøretøyregisteret (begrenset), værdata fra met.no", }, "reports": {}, }, { "id": "proj-saksbehandler-2026", "name": "Saksbehandlerassistent", "description": "Copilot-basert assistent for førerkortssøknader og kjøretøy-registrering.", "createdAt": "2026-04-22T10:30:00Z", "local": { "system_name": "Saksbehandlerassistent v1", "system_description": "M365 Copilot Studio-agent som hjelper saksbehandlere med førerkort-søknader, henter relevant lovverk og foreslår sakssvar.", "interaction_type": "beslutningsstøtte", "users": "Saksbehandlere ved 22 trafikkstasjoner", "risk_level_assumption": "Begrenset", "risk_classification": "Begrenset", "org_role": "Deployer", }, "reports": {}, }, { "id": "proj-chatbot-2026", "name": "Brukerstøtte chatbot", "description": "Publikumsrettet chatbot på vegvesen.no for førerkort- og kjøretøyspørsmål.", "createdAt": "2026-05-01T13:15:00Z", "local": {}, "reports": {}, }, ] def read_fixture(name: str) -> str: """Les en av de 17 ANPR-fixture-filene.""" return (FIXTURES_DIR / f"{name}.md").read_text(encoding="utf-8") def seed_state(page: Page, *, projects=True): """Populerer __store.state med demo-data og trigger render.""" payload = { "shared": DEMO_SHARED, "projects": DEMO_PROJECTS if projects else [], } page.evaluate( """({shared, projects}) => { for (const k of Object.keys(shared)) { const target = window.__store.state.shared[k]; for (const f of Object.keys(shared[k])) target[f] = shared[k][f]; } if (projects.length > 0) { window.__store.state.projects.length = 0; for (const p of projects) window.__store.state.projects.push(p); } window.__scheduleRender(); }""", payload, ) page.wait_for_timeout(200) def import_reports(page: Page, project_id: str, command_ids: list): """Kaller __handlePasteImport på et aktivt prosjekt etter at project-surface er rendret. Må kalles ETTER navigate('project') siden handlePasteImport rendrer direkte til DOM-slottet og scheduleRender ville slette det.""" page.evaluate( """({pid}) => { window.__store.state.activeProjectId = pid; }""", {"pid": project_id}, ) for cmd in command_ids: markdown = read_fixture(cmd) page.evaluate( """({cmd, md}) => { window.__handlePasteImport(cmd, md); }""", {"cmd": cmd, "md": markdown}, ) page.wait_for_timeout(150) def navigate(page: Page, surface: str, project_id: str = None, project_tab: str = None): """Bytter aktiv surface (og evt. aktivt prosjekt + tab) og trigger render.""" page.evaluate( """({surface, pid}) => { if (pid) window.__store.state.activeProjectId = pid; window.__navigate(surface); }""", {"surface": surface, "pid": project_id}, ) page.wait_for_timeout(250) # Project-tab er module-local (currentProjectTab) — må klikkes via faktisk knapp if surface == "project" and project_tab: page.evaluate( """(tab) => { const btn = document.querySelector('[data-action=\"project-tab\"][data-tab=\"' + tab + '\"]'); if (btn) btn.click(); }""", project_tab, ) page.wait_for_timeout(200) def set_theme(page: Page, theme: str): """Setter tema (light/dark) før screenshot.""" page.evaluate( """(theme) => { document.documentElement.setAttribute('data-theme', theme); try { localStorage.setItem('ms-ai-architect-theme', theme); } catch(e){} // Re-render topbar slik at theme-toggle-label oppdateres const labels = document.querySelectorAll('[data-theme-label]'); for (const l of labels) l.textContent = theme === 'dark' ? 'Mørk' : 'Lys'; }""", theme, ) page.wait_for_timeout(100) def shoot(page: Page, name: str, *, full_page: bool = False, scroll_to: str = None): """Lagrer en screenshot. Default: viewport-only. full_page=True: hele scroll-høyden scroll_to: CSS-selektor som scrolles inn i view før shot """ out = SCREENSHOTS_DIR / f"{name}.png" if scroll_to: page.evaluate( """(sel) => { const el = document.querySelector(sel); if (el) el.scrollIntoView({block: 'start', behavior: 'instant'}); window.scrollBy(0, -20); }""", scroll_to, ) page.wait_for_timeout(120) page.screenshot(path=str(out), full_page=full_page) print(f" [{name}.png] {out.relative_to(PLUGIN_ROOT)}") def open_clean(browser, viewport=None): """Åpner playground med ren state og venter på bootstrap.""" context = browser.new_context(viewport=viewport or VIEWPORT) page = context.new_page() # Hopp til about:blank først for å rense localStorage/IDB fra forrige # kontekst-kjøring (file:// kan dele storage på samme browser-instans). page.goto("about:blank") page.evaluate( """async () => { try { localStorage.clear(); } catch(e){} try { sessionStorage.clear(); } catch(e){} if (typeof indexedDB !== 'undefined' && indexedDB.databases) { try { const dbs = await indexedDB.databases(); await Promise.all(dbs.map(d => new Promise(r => { const req = indexedDB.deleteDatabase(d.name); req.onsuccess = req.onerror = req.onblocked = r; }))); } catch(e){} } }""" ) page.goto(f"file://{HTML_PATH}") # Vent til __store er eksponert (bootstrap fullført) page.wait_for_function("() => window.__store && window.__CATALOG", timeout=10_000) # Skjul evt. error-banner som kan dukke opp fra bootstrap-kanter page.evaluate( """() => { const errs = document.querySelectorAll('[data-onboarding-errors]'); for (const e of errs) e.setAttribute('hidden', ''); }""" ) return context, page def main(): if not HTML_PATH.exists(): print(f"FEIL: {HTML_PATH} mangler", file=sys.stderr) sys.exit(1) SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True) print(f"Lagrer screenshots til: {SCREENSHOTS_DIR.relative_to(PLUGIN_ROOT)}") print() with sync_playwright() as pw: browser = pw.chromium.launch(headless=True) # NB: light-mode tokens er ikke implementert i det vendrede # design-systemet ennå (kun mørk-tema-vars). Theme-toggle bytter # data-theme + label korrekt, men CSS-fargene endres ikke før # tokens.css får [data-theme="light"]-overrides. Vi capture'r kun # mørk-modus for nå. Når light-tokens kommer, endre listen til # ("dark", "light") for å regenerere parsuffix-screenshots. for theme in ("dark",): suffix = "" if theme == "dark" else "-light" print(f"--- Theme: {theme} ---") # 1. Onboarding (utfylt) — TALL viewport for å vise sidebar + alle 5 grupper ctx, page = open_clean(browser, viewport=TALL_VIEWPORT) seed_state(page, projects=False) navigate(page, "onboarding") set_theme(page, theme) page.evaluate( """() => { const errs = document.querySelectorAll('[data-onboarding-errors]'); for (const e of errs) e.setAttribute('hidden', ''); }""" ) page.wait_for_timeout(150) shoot(page, f"01-onboarding{suffix}") ctx.close() # 2. Home (med 3 prosjekter) ctx, page = open_clean(browser) seed_state(page) navigate(page, "home") set_theme(page, theme) page.wait_for_timeout(150) shoot(page, f"02-home{suffix}") ctx.close() # 3. Catalog (alle grupper synlig) — utvid de første 2 gruppene ctx, page = open_clean(browser) seed_state(page) navigate(page, "catalog") set_theme(page, theme) page.evaluate( """() => { const exps = document.querySelectorAll('[data-action="catalog-toggle-group"]'); // Klikk de 2 første for å utvide for (let i = 0; i < Math.min(2, exps.length); i++) exps[i].click(); }""" ) page.wait_for_timeout(200) shoot(page, f"03-catalog{suffix}") ctx.close() # 4. Project — classify pyramide (scroll til classify report-slot) ctx, page = open_clean(browser) seed_state(page) navigate(page, "project", project_id="proj-anpr-2026", project_tab="regulatory") set_theme(page, theme) import_reports(page, "proj-anpr-2026", ["classify"]) shoot( page, f"04-project-classify-pyramide{suffix}", scroll_to='[data-report-slot="classify"]', ) ctx.close() # 5. Project — ROS matrix ctx, page = open_clean(browser) seed_state(page) navigate(page, "project", project_id="proj-anpr-2026", project_tab="security") set_theme(page, theme) import_reports(page, "proj-anpr-2026", ["ros"]) shoot( page, f"05-project-ros-matrix{suffix}", scroll_to='[data-report-slot="ros"]', ) ctx.close() # 6. Project — Cost distribution ctx, page = open_clean(browser) seed_state(page) navigate(page, "project", project_id="proj-anpr-2026", project_tab="economy") set_theme(page, theme) import_reports(page, "proj-anpr-2026", ["cost"]) shoot( page, f"06-project-cost-distribution{suffix}", scroll_to='[data-report-slot="cost"]', ) ctx.close() browser.close() print() print(f"Ferdig. Screenshots i {SCREENSHOTS_DIR.relative_to(PLUGIN_ROOT)}/") if __name__ == "__main__": main()