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>
323 lines
16 KiB
Bash
Executable file
323 lines
16 KiB
Bash
Executable file
#!/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, "&").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 "<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 & 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
|