Erstatter v2 project-surface (screen-tabs + category-tabs + per-command paste-cards)
med v3 renderProjectView (sidebar med 17 artifacts + main-area + import-modal overlay).
renderActive() ruter project-surface til renderProjectSurfaceV3() som wrapper
renderProjectView + topbar + app-shell.
V2-surface helt fjernet:
- renderProjectSurface (152 linjer)
- renderCommandSubCard (87 linjer)
- rehydratePasteImports (15 linjer)
- ACTIONS['project-screen'], currentProjectScreen
- 5 v2-CSS-klasser: .project-tabs, .project-tab*, .sub-zone, .paste-import-row, .project-header__*, .command-cards
Zombie-handlers beholdt for test-back-compat:
currentProjectTab, ACTIONS['project-tab'], ACTIONS['parse'],
handlePasteImport, window.__handlePasteImport. Unreachable fra v3 DOM
men nødvendige for test-playground-v3.sh + test-playground-parsers.sh.
2 fingerprint-gap lukket:
- requirements.headers: utvidet med "EU AI Act — Krav" pattern
- license.headers: utvidet med "Lisens-kapabilitetsmatrise" pattern
- KNOWN_GAP_FIXTURES = {} i test-playground-fingerprints.sh
migrateDataVersion utvidet med parserFor (3. arg):
- Demo-state med kun raw_markdown auto-parses til project.artifacts[cid]
- defaultParserFor(cmdId) resolverer PARSERS[archetypeFor(cmdId)]
- 3 bootstrap-callsites oppdatert (cold-load, import, load-demo)
Ship-QA bugfixes funnet via browser-dogfood:
- components-tier4-project-view.css lagt til i <link>-kjeden (var ikke loaded
-> modal-overlay og two-column layout virket ikke)
- renderImportModal setter data-open="true" (DS-kontrakt for display: flex)
Bundler også sesjon 2-4 deliverables som ikke ble committed tidligere:
- shared/playground-design-system v0.6.0 (Tier 4 project-view CSS + 6 tokens)
- ms-ai-architect/playground/vendor/ re-sync til DS v0.6.0
- tests/test-playground-fingerprints.sh (sesjon 4 NY - 32 PASS)
- tests/test-playground-projectview.sh (sesjon 4 NY - 30 PASS)
- tests/test-playground-actions.sh (sesjon 4 NY - 19 PASS)
- tests/test-playground-migrations.sh utvidet (7 -> 16 PASS)
- tests/run-e2e.sh wirer alle 6 playground-suiter
Stats:
- bash tests/run-e2e.sh --playground: 386 PASS, 0 FAIL, 2 WARN (pre-eks)
- bash tests/run-e2e.sh (full): All E2E suites passed
- bash tests/validate-plugin.sh: 219 PASS
Screenshots regenerert til playground/screenshots/v1.15.0/ (24 PNG-er, 12
surfaces x 2 tema). Nye v3-surfaces: project-overview, project-artifact-*,
project-import-modal (viewport-only), project-search.
Docs oppdatert (3 nivåer): README.md (badge + version history),
CHANGELOG.md, CLAUDE.md (playground-seksjon + valideringstabell),
rot-README.md + rot-CLAUDE.md (marketplace-landingen + plugin-index).
.gitignore: ny pattern *.local.html + *.local.json for sesjon-state-filer.
Co-Authored-By: Claude Opus 4.7 <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: "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, ">").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
|