#!/bin/bash # test-playground-projectview.sh — Playground v3 renderProjectView integration # # Verifiserer at renderProjectView + sub-renderers produserer korrekt HTML # (som strenger — ingen DOM-deps) mot en inline demo-state-snippet. # # Dekker 4 view-tilstander: # - overview (selectedArtifactId null) # - artifact (filled) (selectedArtifactId = 'classify') # - empty (sidebar-treff) (selectedArtifactId = 'frimpact' — mangler) # - import-modal-open (top-state, orthogonal) # # Pluss: # - renderArtifactNav-søk filtrerer # - renderProjectOverview har 4 verdict-tiles, top-risks, next-actions, missing # - renderImportModal har 17 dropdown-options + prefill # # Bash 3.2-kompatibel. Bruker node til JS-eval. Ingen npm-deps. set -euo pipefail PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)" HTML_FILE="$PLUGIN_ROOT/playground/ms-ai-architect-playground.html" # shellcheck disable=SC1091 source "$PLUGIN_ROOT/tests/lib/e2e-helpers.sh" init_suite "Playground v3 — renderProjectView integration" if [ ! -f "$HTML_FILE" ]; then fail "HTML-fila finnes ikke: $HTML_FILE" print_summary; exit 1 fi pass "HTML-fil finnes: $(basename "$HTML_FILE")" if grep -q "PROJECT_VIEW_V2_BEGIN" "$HTML_FILE" && grep -q "PROJECT_VIEW_V2_END" "$HTML_FILE"; then pass "PROJECT_VIEW_V2_BEGIN/END markører finnes" else fail "Mangler PROJECT_VIEW_V2-markører i HTML" print_summary; exit 1 fi NODE_OUT=$(node -e ' const fs = require("fs"); const htmlPath = process.argv[1]; const htmlSrc = fs.readFileSync(htmlPath, "utf8"); const beginMarker = "// === PROJECT_VIEW_V2_BEGIN ==="; const endMarker = "// === PROJECT_VIEW_V2_END ==="; const beginIdx = htmlSrc.indexOf(beginMarker); const endIdx = htmlSrc.indexOf(endMarker); if (beginIdx < 0 || endIdx < 0) { console.error("MARKER_MISSING"); process.exit(2); } const block = htmlSrc.substring(beginIdx, endIdx + endMarker.length); // 17 produces_report-renderers per PROJECT_VIEW_CONFIG.renderers. // Hver stub returnerer en deterministisk HTML-streng vi kan asserte mot. const RENDERER_IDS = ["renderAiActPyramid","renderRequirements","renderTransparency","renderFria","renderConformity","renderDpia","renderSecurity","renderRos","renderReview","renderCost","renderLicense","renderMigrate","renderAdr","renderSummary","renderPoc","renderUtredning","renderCompare"]; const rendererStubs = RENDERER_IDS.map(function (n) { return n + ": function (data, slot) { if (slot) slot.innerHTML = \"
\" + (data && data.title || \"stub\") + \"
\"; }"; }).join(", "); const COMMANDS = [ { id: "classify", category: "regulatory", label: "EU AI Act — Klassifisering", description: "EU AI Act klass.", renderer: "renderAiActPyramid", produces_report: true, report_archetype: "aiact" }, { id: "requirements", category: "regulatory", label: "EU AI Act — Krav", description: "Krav per risiko", renderer: "renderRequirements", produces_report: true, report_archetype: "requirements-list" }, { id: "transparency", category: "regulatory", label: "Transparensnotis (Art. 13/50)", description: "Art. 13/50", renderer: "renderTransparency", produces_report: true, report_archetype: "text-document" }, { id: "frimpact", category: "regulatory", label: "FRIA (Art. 27)", description: "FRIA", renderer: "renderFria", produces_report: true, report_archetype: "fria" }, { id: "conformity", category: "regulatory", label: "Samsvarsvurdering (Art. 43)", description: "Annex IV", renderer: "renderConformity", produces_report: true, report_archetype: "conformity-checklist" }, { id: "dpia", category: "regulatory", label: "DPIA / PVK", description: "PVK", renderer: "renderDpia", produces_report: true, report_archetype: "matrix-risk" }, { id: "security", category: "security", label: "Sikkerhetsvurdering (6×5)", description: "6×5 scoring", renderer: "renderSecurity", produces_report: true, report_archetype: "matrix-risk-6x5" }, { id: "ros", category: "security", label: "ROS-analyse", description: "NS 5814", renderer: "renderRos", produces_report: true, report_archetype: "matrix-risk" }, { id: "review", category: "security", label: "Arkitekturgjennomgang", description: "Digdir/NSM", renderer: "renderReview", produces_report: true, report_archetype: "findings" }, { id: "cost", category: "economy", label: "Kostnadsestimat", description: "P10/P50/P90 NOK", renderer: "renderCost", produces_report: true, report_archetype: "cost-distribution" }, { id: "license", category: "economy", label: "Lisenskartlegging", description: "M365-lisenser", renderer: "renderLicense", produces_report: true, report_archetype: "scenario-comparison" }, { id: "migrate", category: "economy", label: "Migrasjonsplan", description: "Fase-plan", renderer: "renderMigrate", produces_report: true, report_archetype: "phase-plan" }, { id: "adr", category: "documentation", label: "ADR", description: "MADR v3.0", renderer: "renderAdr", produces_report: true, report_archetype: "adr" }, { id: "summary", category: "documentation", label: "Beslutningsnotat", description: "Sammendrag", renderer: "renderSummary", produces_report: true, report_archetype: "verdict" }, { id: "poc", category: "documentation", label: "POC-plan", description: "Suksesskriterier", renderer: "renderPoc", produces_report: true, report_archetype: "phase-plan" }, { id: "utredning", category: "documentation", label: "Utredning", description: "Utredningsinstr.", renderer: "renderUtredning", produces_report: true, report_archetype: "utredning" }, { id: "compare", category: "documentation", label: "Plattformsammenligning", description: "Plattformer", renderer: "renderCompare", produces_report: true, report_archetype: "scenario-comparison" } ]; // Demo state med 5 fylte artifacts + ui i forskjellige tilstander. function buildDemoState(opts) { const o = opts || {}; const now = "2026-05-15T10:00:00.000Z"; return { dataVersion: 3, schemaVersion: 1, activeProjectId: "p1", projects: [{ id: "p1", name: "Acme: Kunde-chatbot", description: "AI-system for objekt-deteksjon i sensordata.", createdAt: "2026-04-01T08:00:00.000Z", artifacts: { classify: { commandId: "classify", raw_markdown: "# EU AI Act — Klassifisering", parsed: { title: "Klassifisering", risk_level: "Høy", role: "Provider og Deployer" }, verdict: "warning", keyStats: [{ label: "Risikonivå", value: "Høy" }], importedAt: now, updatedAt: now }, ros: { commandId: "ros", raw_markdown: "# ROS-analyse", parsed: { title: "ROS", threats: [ { id: "T1", description: "Modell-bias", severity: "Høy" }, { id: "T2", description: "Privacy leak", severity: "Kritisk" }, { id: "T3", description: "Hallusinerte fakta", severity: "Medium" } ]}, verdict: "warning", keyStats: [], importedAt: now, updatedAt: now }, security: { commandId: "security", raw_markdown: "# Sikkerhetsvurdering", parsed: { title: "Security", findings: [ { id: "S1", finding: "Manglende DLP", severity: "Kritisk" }, { id: "S2", finding: "Audit ufullstendig", severity: "Høy" } ]}, verdict: "block", keyStats: [], importedAt: now, updatedAt: now }, dpia: { commandId: "dpia", raw_markdown: "# DPIA", parsed: { title: "DPIA", threats: [] }, verdict: "go-with-conditions", keyStats: [], importedAt: now, updatedAt: now }, cost: { commandId: "cost", raw_markdown: "# Kostnadsestimat", parsed: { title: "Kostnad", p10: 78000, p50: 142000, p90: 285000 }, verdict: "approved", keyStats: [{ label: "P50/mnd", value: "142 000 NOK" }], importedAt: now, updatedAt: now } }, reports: {} }], ui: { projectView: { selectedArtifactId: o.selectedArtifactId === undefined ? null : o.selectedArtifactId, searchQuery: o.searchQuery || "" }, importModal: { open: !!o.importOpen, prefillCommandId: o.prefillCommandId || "", prefillMarkdown: o.prefillMarkdown || "" } } }; } const stubs = ` const window = {}; function escapeHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function escapeAttr(s) { return escapeHtml(s); } function renderPageShell(opts, body) { const t = (opts && opts.title) || ""; const e = (opts && opts.eyebrow) || ""; const v = (opts && opts.verdict) || ""; return "

" + escapeHtml(t) + "

" + (body || ""); } function renderVerdictPill(v) { return "" + escapeHtml(String(v || "").toUpperCase()) + ""; } function renderKeyStatsGrid(s) { return "
" + (Array.isArray(s) ? s.length : 0) + "
"; } function inferVerdict() { return "n-a"; } function inferKeyStats() { return []; } const PARSERS = {}; const RENDERERS = { ` + rendererStubs + ` }; const CATALOG = { commands: ` + JSON.stringify(COMMANDS) + ` }; const ACTIONS = {}; let __STORE_STATE = null; const store = { get state() { return __STORE_STATE; }, save: function () {} }; function setStoreState(s) { __STORE_STATE = s; } function findProject(id) { const list = (store.state && store.state.projects) || []; for (let i = 0; i < list.length; i++) if (list[i].id === id) return list[i]; return null; } function scheduleRender() {} `; const wrapped = stubs + block + "\nreturn { renderProjectView, renderProjectHeader, renderArtifactNav, renderArtifactNavItem, renderProjectMain, renderProjectOverview, renderProjectArtifact, renderEmptyArtifactPrompt, renderImportModal, PROJECT_VIEW_CONFIG, setStoreState };"; let api; try { api = (new Function(wrapped))(); } catch (e) { console.error("EVAL_FAILED: " + e.message); process.exit(3); } function emit(ok, desc) { console.log((ok ? "PASS" : "FAIL") + "\t" + desc); } // ---- API-eksponering ---- emit(typeof api.renderProjectView === "function", "renderProjectView er funksjon"); emit(typeof api.renderProjectOverview === "function", "renderProjectOverview er funksjon"); emit(typeof api.renderImportModal === "function", "renderImportModal er funksjon"); emit(typeof api.PROJECT_VIEW_CONFIG === "object" && api.PROJECT_VIEW_CONFIG !== null, "PROJECT_VIEW_CONFIG eksponert"); // ---- View 1: overview (selectedArtifactId null) ---- let demo = buildDemoState({}); api.setStoreState(demo); let project = demo.projects[0]; let html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG); emit(html.indexOf("class=\"project-view\"") !== -1 && html.indexOf("data-view=\"overview\"") !== -1, "overview: .project-view rot med data-view=\"overview\""); emit(html.indexOf("Acme: Kunde-chatbot") !== -1, "overview: prosjekt-navn rendres i header"); emit(html.indexOf("data-action=\"import-open\"") !== -1, "overview: Importer-rapport-knapp finnes"); // Alle 4 kategori-grupper i sidebaren emit(html.indexOf("Regulatorisk") !== -1 && html.indexOf("Risiko & sikkerhet") !== -1 && html.indexOf("Økonomi") !== -1 && html.indexOf("Dokumentasjon") !== -1, "overview: alle 4 kategori-labels rendres i nav"); // Overview-tiles (verdict-grid) emit(html.indexOf("project-overview__verdict-tile") !== -1, "overview: project-overview__verdict-tile finnes"); const tileCount = (html.match(/project-overview__verdict-tile/g) || []).length; emit(tileCount >= 4, "overview: minst 4 verdict-tiles (got " + tileCount + ")"); // Top-risks (3 fra ros + 2 fra security = 5) emit(html.indexOf("top-risks") !== -1 && html.indexOf("Privacy leak") !== -1, "overview: top-risks-listen inkluderer ROS-trussel"); // Next-actions / missing reports emit(html.indexOf("project-overview__next-actions") !== -1 || html.indexOf("empty-hint") !== -1, "overview: next-actions-seksjon rendres"); emit(html.indexOf("project-overview__missing-reports") !== -1 || html.indexOf("Alle må-ha-rapporter er importert") !== -1, "overview: missing-reports-seksjon rendres"); // ---- View 2: artifact (filled) ---- demo = buildDemoState({ selectedArtifactId: "classify" }); api.setStoreState(demo); project = demo.projects[0]; html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG); emit(html.indexOf("data-view=\"artifact\"") !== -1, "artifact: data-view=\"artifact\""); emit(html.indexOf("project-view__artifact") !== -1 && html.indexOf("data-artifact=\"classify\"") !== -1, "artifact: .project-view__artifact med data-artifact=\"classify\""); emit(html.indexOf("project-view__artifact-title") !== -1 && html.indexOf("EU AI Act — Klassifisering") !== -1, "artifact: command-label vises i artifact-header"); emit(html.indexOf("data-action=\"artifact-reimport\"") !== -1 && html.indexOf("data-action=\"artifact-delete\"") !== -1, "artifact: reimport + delete-action-knapper finnes"); // ---- View 3: empty (sidebar-treff uten artifact) ---- demo = buildDemoState({ selectedArtifactId: "frimpact" }); api.setStoreState(demo); project = demo.projects[0]; html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG); emit(html.indexOf("data-view=\"empty\"") !== -1, "empty: data-view=\"empty\""); emit(html.indexOf("empty-artifact-prompt") !== -1 && html.indexOf("data-command=\"frimpact\"") !== -1, "empty: empty-artifact-prompt for frimpact"); emit(html.indexOf("data-prefill-command=\"frimpact\"") !== -1, "empty: Importer-knapp har data-prefill-command=\"frimpact\""); // ---- View 4: import-modal-open ---- demo = buildDemoState({ importOpen: true, prefillCommandId: "ros", prefillMarkdown: "# ROS-analyse\nDemo" }); api.setStoreState(demo); project = demo.projects[0]; html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG); emit(html.indexOf("data-import-modal") !== -1, "import-modal: [data-import-modal] rendres når open=true"); // Dropdown har 17 options + tom default = 18