#!/bin/bash # test-playground-actions.sh — Playground v3 ACTIONS handler-state-effekter # # Verifiserer at de 6 pure-state-ACTIONS-handlerne (project-select-artifact, # project-show-overview, import-open, import-close, artifact-reimport, # artifact-delete) muterer state korrekt. # # Handlerne import-detect og import-save krever document/DOM og dekkes ikke # her — de testes implisitt via browser-walkthrough før release og via # manuell QA per playground/MANUAL-CHECKLIST.md. # # 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 — ACTIONS handler state-effekter" if [ ! -f "$HTML_FILE" ]; then fail "HTML-fila finnes ikke: $HTML_FILE" print_summary; exit 1 fi pass "HTML-fil finnes: $(basename "$HTML_FILE")" NODE_OUT=$(node -e ' const fs = require("fs"); const htmlPath = process.argv[1]; const htmlSrc = fs.readFileSync(htmlPath, "utf8"); // Ekstraher ACTIONS-blokken fra "// PROJECT-VIEW V2 ACTIONS" (sesjon 3-kommentaren) // til starten av smart-detect-blokken. Tar med projectViewUiState + 6 handlere. const startMarker = "// PROJECT-VIEW V2 ACTIONS (Sesjon 3)"; const endMarker = "// Smart-detect på textarea-input i import-modal."; const startIdx = htmlSrc.indexOf(startMarker); const endIdx = htmlSrc.indexOf(endMarker); if (startIdx < 0 || endIdx < 0) { console.error("MARKERS_MISSING start=" + startIdx + " end=" + endIdx); process.exit(2); } const block = htmlSrc.substring(startIdx, endIdx); // Stubs som gir handlerne et minimums-miljø uten document/window. const stubs = ` 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; } let __renderCount = 0; function scheduleRender() { __renderCount++; } let __confirmAnswer = true; function confirm() { return __confirmAnswer; } function setConfirmAnswer(b) { __confirmAnswer = b; } function renderCount() { return __renderCount; } function resetRenderCount() { __renderCount = 0; } const ACTIONS = {}; `; const wrapped = stubs + block + "\nreturn { ACTIONS, setStoreState, setConfirmAnswer, renderCount, resetRenderCount, projectViewUiState };"; 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); } function freshState(opts) { const o = opts || {}; return { schemaVersion: 1, dataVersion: 3, activeProjectId: "p1", projects: [{ id: "p1", name: "Demo", artifacts: { classify: { commandId: "classify", raw_markdown: "RAW_CLASSIFY", parsed: { risk_level: "minimal" }, verdict: "go", keyStats: [], importedAt: "x", updatedAt: "x" }, ros: { commandId: "ros", raw_markdown: "RAW_ROS", parsed: { threats: [] }, verdict: "approved", keyStats: [], importedAt: "x", updatedAt: "x" } }, reports: { classify: { raw_markdown: "RAW_CLASSIFY", parsed: { risk_level: "minimal" } }, ros: { raw_markdown: "RAW_ROS", parsed: { threats: [] } } } }], ui: o.ui || {} }; } // ---- API ---- emit(typeof api.ACTIONS === "object" && api.ACTIONS !== null, "ACTIONS-objekt eksponert"); emit(typeof api.ACTIONS["project-select-artifact"] === "function", "project-select-artifact handler finnes"); emit(typeof api.ACTIONS["project-show-overview"] === "function", "project-show-overview handler finnes"); emit(typeof api.ACTIONS["import-open"] === "function", "import-open handler finnes"); emit(typeof api.ACTIONS["import-close"] === "function", "import-close handler finnes"); emit(typeof api.ACTIONS["artifact-reimport"] === "function", "artifact-reimport handler finnes"); emit(typeof api.ACTIONS["artifact-delete"] === "function", "artifact-delete handler finnes"); // ---- project-select-artifact ---- (function () { const state = freshState(); api.setStoreState(state); api.resetRenderCount(); api.ACTIONS["project-select-artifact"]({}, { dataset: { artifactId: "classify" } }); const ok = state.ui.projectView && state.ui.projectView.selectedArtifactId === "classify" && api.renderCount() === 1; emit(ok, "project-select-artifact setter selectedArtifactId og trigger scheduleRender (got=" + (state.ui.projectView && state.ui.projectView.selectedArtifactId) + ")"); })(); // ---- project-select-artifact uten artifactId — no-op ---- (function () { const state = freshState({ ui: { projectView: { selectedArtifactId: "ros", searchQuery: "" }, importModal: { open: false, prefillCommandId: "", prefillMarkdown: "" } } }); api.setStoreState(state); api.resetRenderCount(); api.ACTIONS["project-select-artifact"]({}, { dataset: {} }); const ok = state.ui.projectView.selectedArtifactId === "ros" && api.renderCount() === 0; emit(ok, "project-select-artifact uten dataset.artifactId → no-op (selectedArtifactId beholdt, ingen render)"); })(); // ---- project-show-overview ---- (function () { const state = freshState({ ui: { projectView: { selectedArtifactId: "classify", searchQuery: "" }, importModal: { open: false, prefillCommandId: "", prefillMarkdown: "" } } }); api.setStoreState(state); api.resetRenderCount(); api.ACTIONS["project-show-overview"]({}, {}); const ok = state.ui.projectView.selectedArtifactId === null && api.renderCount() === 1; emit(ok, "project-show-overview tømmer selectedArtifactId"); })(); // ---- import-open med prefill-command (eksisterende artifact gir prefillMarkdown) ---- (function () { const state = freshState(); api.setStoreState(state); api.resetRenderCount(); api.ACTIONS["import-open"]({}, { dataset: { prefillCommand: "ros" } }); const m = state.ui.importModal; const ok = m && m.open === true && m.prefillCommandId === "ros" && m.prefillMarkdown === "RAW_ROS" && api.renderCount() === 1; emit(ok, "import-open med prefillCommand=\"ros\" åpner modal + prefyller raw_markdown"); })(); // ---- import-open uten prefill-command ---- (function () { const state = freshState(); api.setStoreState(state); api.resetRenderCount(); api.ACTIONS["import-open"]({}, { dataset: {} }); const m = state.ui.importModal; const ok = m && m.open === true && m.prefillCommandId === "" && m.prefillMarkdown === ""; emit(ok, "import-open uten prefill → modal åpnet, ingen prefyll"); })(); // ---- import-close ---- (function () { const state = freshState({ ui: { projectView: { selectedArtifactId: null, searchQuery: "" }, importModal: { open: true, prefillCommandId: "ros", prefillMarkdown: "RAW_ROS" } } }); api.setStoreState(state); api.resetRenderCount(); api.ACTIONS["import-close"]({}, {}); const m = state.ui.importModal; const ok = m.open === false && m.prefillCommandId === "" && m.prefillMarkdown === "" && api.renderCount() === 1; emit(ok, "import-close tilbakestiller modal-state"); })(); // ---- artifact-reimport prefyller modal med eksisterende markdown ---- (function () { const state = freshState(); api.setStoreState(state); api.resetRenderCount(); api.ACTIONS["artifact-reimport"]({}, { dataset: { command: "classify" } }); const m = state.ui.importModal; const ok = m.open === true && m.prefillCommandId === "classify" && m.prefillMarkdown === "RAW_CLASSIFY"; emit(ok, "artifact-reimport åpner modal med prefill fra eksisterende artifact"); })(); // ---- artifact-delete med confirm=false → no-op ---- (function () { const state = freshState(); api.setStoreState(state); api.setConfirmAnswer(false); api.resetRenderCount(); api.ACTIONS["artifact-delete"]({}, { dataset: { command: "classify" } }); const ok = state.projects[0].artifacts.classify !== undefined && api.renderCount() === 0; emit(ok, "artifact-delete med confirm=false → artifact bevart, ingen render"); })(); // ---- artifact-delete med confirm=true → sletter + clearer selectedArtifactId ---- (function () { const state = freshState({ ui: { projectView: { selectedArtifactId: "classify", searchQuery: "" }, importModal: { open: false, prefillCommandId: "", prefillMarkdown: "" } } }); api.setStoreState(state); api.setConfirmAnswer(true); api.resetRenderCount(); api.ACTIONS["artifact-delete"]({}, { dataset: { command: "classify" } }); const p = state.projects[0]; const ok = p.artifacts.classify === undefined && p.reports.classify === undefined && state.ui.projectView.selectedArtifactId === null && api.renderCount() === 1; emit(ok, "artifact-delete med confirm=true sletter artifact + reports + clearer selectedArtifactId"); })(); // ---- artifact-delete bevarer andre artifacts ---- (function () { const state = freshState(); api.setStoreState(state); api.setConfirmAnswer(true); api.ACTIONS["artifact-delete"]({}, { dataset: { command: "ros" } }); const p = state.projects[0]; const ok = p.artifacts.classify !== undefined && p.artifacts.ros === undefined; emit(ok, "artifact-delete sletter bare den ene — andre artifacts bevart"); })(); // ---- projectViewUiState initialiserer state-grener idempotent ---- (function () { const state = { projects: [], ui: {} }; api.setStoreState(state); const ui1 = api.projectViewUiState(); const ui2 = api.projectViewUiState(); const ok = ui1 === ui2 && ui1.projectView && ui1.projectView.selectedArtifactId === null && ui1.projectView.searchQuery === "" && ui1.importModal && ui1.importModal.open === false; emit(ok, "projectViewUiState initialiserer projectView + importModal idempotent"); })(); ' "$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