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>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-23 14:43:42 +02:00
commit 2ed24add12
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
107 changed files with 98 additions and 81 deletions

View file

@ -11,30 +11,38 @@ npx playwright install chromium # one-time, ~150 MB
node run.mjs
```
Output goes to `../../playground/screenshots/v1.10.0/`.
Output goes to `../../playground/screenshots/v1.15.0/`. The runner first deletes
PNGs a previous run left in that directory, then writes
`../../playground/screenshots/MANIFEST.json`: the output directory, the sha256 of
every PNG, and the sha256 of the demo state block it rendered.
`tests/kb-update/test-screenshots-manifest.test.mjs` fails when a PNG in the tree
is not in the manifest, a PNG changed, the demo state changed after the run, or a
doc points at another screenshot directory. Fix: run `node run.mjs` again.
## What it captures
For each theme (dark, light):
| # | Surface | Screen / tab |
|---|---------|--------------|
| # | Surface | View |
|---|---------|------|
| 01 | Onboarding | Empty state |
| 02 | Project | Rapporter / Regulatory (default) |
| 03 | Project | Rapporter / each of 4 other tabs |
| 04-06 | Project | Oversikt / Kontekst / Eksport |
| 07 | Home | Project list with demo-prosjekt |
| 08 | Catalog | All 5 expansion-grupper |
| 09 | Onboarding | Prefilled from demo-state |
| 02 | Project | Overview (no artifact selected) |
| 03-07 | Project | Artifacts: classify, security, ros, cost, summary |
| 08 | Project | Import modal (viewport only) |
| 09 | Project | Sidebar search |
| 10 | Home | Project list with the demo project |
| 11 | Catalog | Command catalog |
| 12 | Onboarding | Prefilled from demo state |
= ~18 PNGs, captured with `deviceScaleFactor: 2` (retina-crisp), `fullPage: true`.
= 24 PNGs, captured with `deviceScaleFactor: 2` (retina-crisp), `fullPage: true`
except the import modal.
## How the demo state works
The screenshot script clicks `[data-action="load-demo"]` which reads the
inline `<script type="application/json" id="demo-state-v1">` block from the
playground HTML. That block is generated by `scripts/build-demo-state.mjs`
and includes one demo project ("Demo: Innbygger-chatbot for byggesak") with
and includes one demo project ("Acme: Kunde-chatbot") with
all 17 fixture markdowns pre-loaded as `raw_markdown`. After load, the
project surface re-runs `handlePasteImport` for each report so the
visualizations render automatically.
@ -48,9 +56,10 @@ node scripts/build-demo-state.mjs
```
This rewrites the `<script id="demo-state-v1">` block in the playground HTML.
Regenerate the screenshots afterwards; the manifest gate fails until you do.
## Commit policy
- Commit `playground/screenshots/v1.10.0/*.png` so forkers see what the
- Commit `playground/screenshots/v1.15.0/*.png` and `MANIFEST.json` so forkers see what the
plugin looks like without running anything.
- Don't commit `node_modules/` (gitignored).

View file

@ -6,7 +6,10 @@
// main-area med per-artifact view + import-modal). Skjermbilder oppdatert
// til å fange v3-surfaces.
//
// Output: playground/screenshots/v1.15.0/<surface>-<theme>.png
// 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
@ -17,7 +20,8 @@
import { chromium } from 'playwright';
import { fileURLToPath } from 'node:url';
import { dirname, resolve, join } from 'node:path';
import { mkdirSync, existsSync } from 'node:fs';
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);
@ -25,12 +29,35 @@ 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) {
@ -195,6 +222,7 @@ async function main() {
for (const theme of ['dark', 'light']) {
await captureAllSurfaces(page, theme);
}
writeManifest();
console.log('\n[screenshot] done — output: ' + OUT_DIR);
} finally {
await browser.close();

View file

@ -1,53 +0,0 @@
#!/usr/bin/env node
// Mockup verification screenshots — sesjon 2 (DS-hoist).
// Captures the 4 mockup states × 2 themes to confirm visual identity
// after hoisting project-view CSS to shared DS.
//
// Output: playground/screenshots/v2-mockup/<state>-<theme>.png
import { chromium } from 'playwright';
import { fileURLToPath } from 'node:url';
import { dirname, resolve, join } from 'node:path';
import { mkdirSync, existsSync } from 'node:fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PLUGIN_ROOT = resolve(__dirname, '..', '..');
const HTML_PATH = join(PLUGIN_ROOT, 'playground', 'v2-mockup.local.html');
const OUT_DIR = join(PLUGIN_ROOT, 'playground', 'screenshots', 'v2-mockup');
const HTML_URL = 'file://' + HTML_PATH;
const VIEWPORT = { width: 1440, height: 1200 };
const STATES = ['overview', 'artifact', 'empty', 'import'];
const THEMES = ['dark', 'light'];
if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true });
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: VIEWPORT, deviceScaleFactor: 2 });
const page = await ctx.newPage();
await page.goto(HTML_URL, { waitUntil: 'load' });
for (const theme of THEMES) {
await page.evaluate((t) => {
document.documentElement.setAttribute('data-theme', t);
const btns = document.querySelectorAll('[data-action="set-theme"]');
btns.forEach((b) => b.setAttribute('aria-pressed', b.getAttribute('data-target') === t ? 'true' : 'false'));
}, theme);
await page.waitForTimeout(200);
for (const state of STATES) {
await page.evaluate((s) => {
const btn = document.querySelector(`[data-action="set-state"][data-target="${s}"]`);
if (btn) btn.click();
}, state);
await page.waitForTimeout(300);
const outPath = join(OUT_DIR, `${state}-${theme}.png`);
await page.screenshot({ path: outPath, fullPage: true });
console.log(`${state}-${theme}.png`);
}
}
await browser.close();
console.log('\nDone. 8 screenshots → ' + OUT_DIR);