ms-ai-architect/tests/screenshot/run.mjs
Kjell Tore Guttormsen 2ed24add12
fix(screenshots): regenerate screenshots from the current demo, remove older sets
tests/screenshot/run.mjs now clears PNGs a previous run left in its output directory and writes playground/screenshots/MANIFEST.json (output directory, sha256 per PNG, sha256 of the demo state rendered). One run produced the 24 PNGs in playground/screenshots/v1.15.0/ (12 surfaces x 2 themes, including the dark onboarding view removed in 1.18.1).

Removed the v1.10.0, v1.11.0, v1.14.0 and v2-mockup sets (77 PNGs) and tests/screenshot/shoot-mockup.local.mjs. Chose regenerate for the set the runner writes and delete for the rest, because the current runner cannot reproduce the older layouts, and the mockup script reads an HTML file that is not in the repository, so its output could never be regenerated from a clone.

README gallery, docs/playground.md and tests/screenshot/README.md now point at v1.15.0 and describe the 12 surfaces the runner captures. The manifest gate goes green: 4/4.

OCR (Apple Vision, local): 0 of 24 PNGs in the tree carry text from the older demo; known positives from 715950b were found (2 of 2).

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

235 lines
8.5 KiB
JavaScript
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.

#!/usr/bin/env node
// Capture playground screenshots for v1.15.0 documentation.
//
// v1.15.0: v2 project-surface (renderProjectSurface med screen-tabs +
// category-tabs) erstattet av renderProjectView (sidebar med 17 artifacts +
// main-area med per-artifact view + import-modal). Skjermbilder oppdatert
// til å fange v3-surfaces.
//
// Output: playground/screenshots/v1.15.0/<surface>-<theme>.png, plus
// playground/screenshots/MANIFEST.json (sha256 per PNG + sha256 of the demo
// state rendered). tests/kb-update/test-screenshots-manifest.test.mjs fails
// when a PNG in the tree is not output of this run or the demo changed since.
//
// Usage:
// cd tests/screenshot
// npm install
// npx playwright install chromium # ~150MB download, one-time
// node run.mjs
import { chromium } from 'playwright';
import { fileURLToPath } from 'node:url';
import { dirname, resolve, join } from 'node:path';
import { mkdirSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PLUGIN_ROOT = resolve(__dirname, '..', '..');
const HTML_PATH = join(PLUGIN_ROOT, 'playground', 'ms-ai-architect-playground.html');
const OUT_DIR = join(PLUGIN_ROOT, 'playground', 'screenshots', 'v1.15.0');
const HTML_URL = 'file://' + HTML_PATH;
const MANIFEST_PATH = join(PLUGIN_ROOT, 'playground', 'screenshots', 'MANIFEST.json');
const VIEWPORT = { width: 1440, height: 900 };
const FULL_PAGE = true;
function ensureOutDir() {
if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true });
// The set is exactly one run's output: drop PNGs a previous run left behind.
for (const f of readdirSync(OUT_DIR)) if (f.endsWith('.png')) rmSync(join(OUT_DIR, f));
}
const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');
function writeManifest() {
const html = readFileSync(HTML_PATH, 'utf8');
const demo = html.match(/<script type="application\/json" id="demo-state-v1">([\s\S]*?)<\/script>/);
if (!demo) throw new Error('demo-state-v1 block not found in ' + HTML_PATH);
const files = {};
for (const f of readdirSync(OUT_DIR).filter((n) => n.endsWith('.png')).sort()) {
files[f] = sha256(readFileSync(join(OUT_DIR, f)));
}
const manifest = {
generator: 'tests/screenshot/run.mjs',
dir: 'playground/screenshots/' + OUT_DIR.split('/').pop(),
demoStateSha256: sha256(demo[1]),
files
};
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
console.log(' → MANIFEST.json (' + Object.keys(files).length + ' PNGs)');
}
async function setTheme(page, theme) {
await page.evaluate((t) => {
document.documentElement.setAttribute('data-theme', t);
try { localStorage.setItem('ms-ai-architect-theme', t); } catch (e) {}
const labels = document.querySelectorAll('[data-theme-label]');
for (const l of labels) l.textContent = t === 'dark' ? 'Mørk' : 'Lys';
}, theme);
await page.waitForTimeout(150);
}
async function clearState(page) {
await page.evaluate(() => {
try { localStorage.clear(); } catch (e) {}
try {
const dbs = ['ms-ai-architect-state-v1', 'ms-ai-architect-playground'];
dbs.forEach((n) => indexedDB.deleteDatabase(n));
} catch (e) {}
});
}
async function loadDemo(page) {
await page.evaluate(() => {
const action = document.querySelector('[data-action="load-demo"]');
if (action) action.click();
});
await page.waitForSelector('[data-surface="project"]:not([hidden])', { timeout: 5000 });
// Settle migrasjon (v2→v3 auto-parse) + render.
await page.waitForTimeout(1200);
}
async function clickAction(page, action) {
await page.evaluate((a) => {
const el = document.querySelector('[data-action="' + a + '"]');
if (el) el.click();
}, action);
await page.waitForTimeout(400);
}
async function selectArtifact(page, artifactId) {
await page.evaluate((id) => {
const el = document.querySelector('[data-action="project-select-artifact"][data-artifact-id="' + id + '"]');
if (el) el.click();
}, artifactId);
await page.waitForTimeout(500);
}
async function openImportModal(page, prefillCmd) {
await page.evaluate((cid) => {
// Foretrukket: artifact-reimport-knappen (har eksisterende markdown).
if (cid) {
const el = document.querySelector('[data-action="artifact-reimport"][data-command="' + cid + '"]');
if (el) { el.click(); return; }
}
// Fallback: generisk "Importer rapport"-knapp.
const open = document.querySelector('[data-action="import-open"]');
if (open) open.click();
}, prefillCmd);
await page.waitForSelector('[data-import-modal]', { timeout: 3000 });
await page.waitForTimeout(400);
}
async function setSearchQuery(page, query) {
await page.evaluate((q) => {
const input = document.querySelector('[data-project-search]');
if (!input) return;
input.value = q;
input.dispatchEvent(new Event('input', { bubbles: true }));
}, query);
await page.waitForTimeout(400);
}
async function shoot(page, name, opts) {
const path = join(OUT_DIR, name + '.png');
const useFullPage = (opts && opts.fullPage != null) ? opts.fullPage : FULL_PAGE;
await page.screenshot({ path, fullPage: useFullPage });
console.log(' → ' + name + '.png');
}
async function captureAllSurfaces(page, theme) {
console.log('\n[' + theme + ' theme]');
// 1. Onboarding (empty state)
await clearState(page);
await page.goto(HTML_URL);
await page.waitForSelector('[data-surface="onboarding"]:not([hidden])', { timeout: 5000 });
await setTheme(page, theme);
await shoot(page, '01-onboarding-empty-' + theme);
// 2. Load demo → project-view overview (default — ingen artifact valgt)
await loadDemo(page);
await setTheme(page, theme);
await shoot(page, '02-project-overview-' + theme);
// 3-7. 5 sample artifacts som dekker arketype-bredden
const SAMPLE_ARTIFACTS = [
{ id: 'classify', label: 'classify' }, // AI Act-pyramide
{ id: 'security', label: 'security' }, // 6×5 sikkerhets-matrise
{ id: 'ros', label: 'ros' }, // ROS matrise + radar
{ id: 'cost', label: 'cost' }, // P10/P50/P90 distribusjon
{ id: 'summary', label: 'summary' } // Beslutningsnotat
];
for (let i = 0; i < SAMPLE_ARTIFACTS.length; i++) {
const a = SAMPLE_ARTIFACTS[i];
await selectArtifact(page, a.id);
const num = String(3 + i).padStart(2, '0');
await shoot(page, num + '-project-artifact-' + a.label + '-' + theme);
}
// 8. Import-modal åpen (med prefill fra eksisterende ros-artifact)
// Viewport-only (ikke fullPage) — modal er position:fixed; fullPage
// skroller forbi overlay-en og kaster bort kontekst.
await openImportModal(page, 'ros');
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(200);
await shoot(page, '08-project-import-modal-' + theme, { fullPage: false });
await clickAction(page, 'import-close');
await page.waitForTimeout(300);
// 9. Sidebar-søk aktivt (filtrer på "ros")
await setSearchQuery(page, 'ros');
await shoot(page, '09-project-search-' + theme);
await setSearchQuery(page, ''); // reset
// 10. Home surface
await clickAction(page, 'goto-home');
await page.waitForSelector('[data-surface="home"]:not([hidden])');
await page.waitForTimeout(300);
await shoot(page, '10-home-' + theme);
// 11. Catalog surface
await clickAction(page, 'goto-catalog');
await page.waitForSelector('[data-surface="catalog"]:not([hidden])');
await page.waitForTimeout(300);
await shoot(page, '11-catalog-' + theme);
// 12. Onboarding prefilled (post-demo med org-felter fylt)
await clickAction(page, 'goto-onboarding');
await page.waitForSelector('[data-surface="onboarding"]:not([hidden])');
await page.waitForTimeout(300);
await shoot(page, '12-onboarding-prefilled-' + theme);
}
async function main() {
ensureOutDir();
console.log('[screenshot] launching Chromium…');
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: VIEWPORT,
deviceScaleFactor: 2
});
const page = await context.newPage();
page.on('console', (msg) => {
const t = msg.type();
if (t === 'error' || t === 'warning') {
console.warn(' [browser ' + t + '] ' + msg.text());
}
});
try {
for (const theme of ['dark', 'light']) {
await captureAllSurfaces(page, theme);
}
writeManifest();
console.log('\n[screenshot] done — output: ' + OUT_DIR);
} finally {
await browser.close();
}
}
main().catch((err) => {
console.error('[screenshot] FAILED:', err);
process.exit(1);
});