ktg-plugin-marketplace/plugins/ms-ai-architect/scripts/screenshots/capture-playground.py
Kjell Tore Guttormsen 9664bf1b1c feat(ms-ai-architect): release v1.9.0 with playground v3 + screenshot suite
Version bump: v1.8.0 -> v1.9.0 (minor — plugin API surface unchanged).

Version sync:
- .claude-plugin/plugin.json (canonical), README.md badge,
  CHANGELOG.md (full v1.9.0 entry with playground v3 architecture,
  validation suite, A11Y artifacts, SemVer rationale),
  marketplace root README.md listing.

Screenshot suite (new):
- scripts/screenshots/capture-playground.py — Playwright Python automation
  that opens playground from file://, populates __store with Statens vegvesen
  ANPR demo data, navigates each surface, paste-imports fixtures, scrolls to
  the relevant report-slot, and saves viewport screenshots.
- 6 PNG screenshots in playground/screenshots/ covering: onboarding (18/18
  filled), home (3 projects), catalog (24 commands across 5 expansion groups),
  classify pyramid (high-risk Annex III), ROS 5x5 matrix + 7-dim radar,
  cost P10/P50/P90 distribution.

Doc updates (3 levels per repo policy):
- Plugin README: new "Screenshots" subsection embeds all 6 with description
  columns, plus reproduce command.
- Plugin CLAUDE.md: new "Screenshot-suite (v1.9.0)" subsection documenting
  the automation, demo-state seeding, and re-run trigger conditions.
- Marketplace root README: ms-ai-architect listing now mentions the
  screenshot suite + reproduce command.

Reproduce screenshots: python3 scripts/screenshots/capture-playground.py.

Notes:
- Light-mode tokens are not in the vendored design-system yet. The toggle
  swaps data-theme + label correctly (Step 13 mechanics intact), but the
  CSS palette only ships dark. Captured dark-mode only; light-mode capture
  re-enables when shared/playground-design-system gains [data-theme="light"]
  overrides.
- Local CSS fix in playground HTML: added `[hidden] { display: none !important; }`
  in the inline app-shell <style> block. The vendored .error-summary rule
  sets display: flex which overrode HTML's [hidden] default, leaking the
  onboarding error banner on cold start. Plugin-local for now; a proper
  fix belongs in shared/playground-design-system/components-tier3.css.

Verified post-bump:
- bash tests/validate-plugin.sh -> 215/215 PASS
- bash tests/run-e2e.sh --playground -> 240/240 PASS
2026-05-03 20:40:07 +02:00

360 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()