ms-ai-architect/tests/test-playground-projectview.sh
Kjell Tore Guttormsen 544934dc57
refactor(examples): replace sector-specific example material with generic, fictitious examples
Reference files, test fixtures, the playground demo project and one design
document now use generic, fictitious examples (buildings, energy, water,
grants, municipal services). The playground demo (17 fixtures plus the
embedded demo state) tells one consistent story: a municipal customer
chatbot that pre-screens housing-benefit applications, classified under
Annex III point 5(a). The embedded demo copies were edited in place rather
than regenerated, because they already carry newer AI Act dates than the
fixture files.

Legal text is unchanged. Test semantics are unchanged. Four dark-theme
onboarding screenshots with outdated placeholder text are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:03:53 +02:00

323 lines
16 KiB
Bash
Executable file
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.

#!/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 = \"<div class=\\\"stub-\" + " + JSON.stringify(n) + " + \"\\\">\" + (data && data.title || \"stub\") + \"</div>\"; }";
}).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: "Innbyggerchatbot som forhåndsvurderer søknader om bostøtte.",
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
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 "<header class=\\"page__header\\" data-eyebrow=\\"" + escapeAttr(e) + "\\" data-verdict=\\"" + escapeAttr(v) + "\\"><h1>" + escapeHtml(t) + "</h1></header>" + (body || "");
}
function renderVerdictPill(v) {
return "<span class=\\"verdict-pill\\" data-verdict=\\"" + escapeAttr(String(v || "").toLowerCase()) + "\\">" + escapeHtml(String(v || "").toUpperCase()) + "</span>";
}
function renderKeyStatsGrid(s) { return "<div class=\\"key-stats\\">" + (Array.isArray(s) ? s.length : 0) + "</div>"; }
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 &amp; 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 <option>-tags
const optionMatches = html.match(/<option /g) || [];
emit(optionMatches.length >= 17,
"import-modal: dropdown har 17+ options (got " + optionMatches.length + ")");
emit(html.indexOf("value=\"ros\" selected") !== -1 || html.indexOf("value=\"ros\" selected") !== -1,
"import-modal: prefillCommandId=\"ros\" gir selected option");
emit(html.indexOf("# ROS-analyse") !== -1 || html.indexOf("ROS-analyse") !== -1,
"import-modal: prefillMarkdown rendres i textarea");
// ---- Modal IKKE rendret når open=false ----
demo = buildDemoState({});
api.setStoreState(demo);
project = demo.projects[0];
html = api.renderProjectView(project, api.PROJECT_VIEW_CONFIG);
emit(html.indexOf("data-import-modal") === -1,
"import-modal: ikke rendret når importModal.open=false");
// ---- renderArtifactNav-søk filtrerer ----
demo = buildDemoState({ searchQuery: "ros" });
api.setStoreState(demo);
project = demo.projects[0];
const navHtml = api.renderArtifactNav(project, api.PROJECT_VIEW_CONFIG, null, "ros");
emit(navHtml.indexOf("data-artifact-id=\"ros\"") !== -1,
"nav-filter: ROS-element finnes ved søk=\"ros\"");
emit(navHtml.indexOf("data-artifact-id=\"cost\"") === -1,
"nav-filter: cost-element filtrert ut ved søk=\"ros\"");
// ---- renderProjectView med null project ----
emit(api.renderProjectView(null, api.PROJECT_VIEW_CONFIG).indexOf("Ingen prosjekt valgt") !== -1,
"guard: null project → empty-hint");
' "$HTML_FILE" 2>&1) || NODE_RC=$?
if [ "${NODE_RC:-0}" -ne 0 ]; then
fail "node-eval feilet (rc=${NODE_RC:-0}): $NODE_OUT"
print_summary; exit 1
fi
while IFS=$'\t' read -r status desc; do
case "$status" in
PASS) pass "$desc" ;;
FAIL) fail "$desc" ;;
WARN) warn "$desc" ;;
esac
done <<< "$NODE_OUT"
print_summary