test(ui): browser check for Architecture back and forward (K27)

tools/handtest-fixes-k27.mjs drives the hand-test flow on django-demo in a
headless browser: Overview, open django, Routes, Endpoints, open the /edit
route group, System structure, expand a group, Behavior, start handle in
loaddata.py, follow a call. It then goes back step by step to the start and
forward again, checking at every step the subtab, location trail, route
perspective and filter, system focus and expanded groups, Behavior start
and the Back and Forward tooltips. It also checks Alt+Left and Alt+Right
(and that they do nothing while typing in the filter), a jump from Recent,
that a new navigation after Back drops Forward, that Alt+Left in Galaxy
leaves the Architecture history alone, and that switching to cbm starts a
fresh history. Every step writes a screenshot and index.md.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
Bernhard Jackiewicz
2026-10-03 22:25:26 +02:00
parent 9901f9776f
commit d354983e84
+353
View File
@@ -0,0 +1,353 @@
#!/usr/bin/env node
/*
* Browserpruefung zu K27 (verification/call-feedback-2026-10-02/KORREKTURPLAN.md):
* ein Zurueck und ein Vor fuer ganz Architecture, mit demselben Verlaufsmodell
* wie Galaxy (K2).
*
* node tools/handtest-fixes-k27.mjs --origin http://127.0.0.1:4375 \
* --out /tmp/handtest/k27 [--project django-demo] [--control cbm] [--profile /tmp/profile]
*
* Der Ablauf auf django-demo: Overview, Bereich "django" oeffnen, Routes,
* Endpoints, Gruppe "/edit" oeffnen ("Show these N routes"), System
* structure, eine Gruppe aufklappen, Behavior (der vorgeschlagene Start ist
* kein eigener Schritt), Start "handle · .../loaddata.py" waehlen, einem
* Aufruf folgen. Dann Schritt fuer Schritt zurueck bis zum Anfang und wieder
* vor; an jedem Schritt werden Untertab, Brotkruemel, Routen-Perspektive und
* -Filter, System-Fokus und aufgeklappte Gruppen, Behavior-Start und die
* Tooltips von Zurueck und Vor gemessen. Danach Alt+Links/Rechts (auch beim
* Tippen im Filter, wo sie nichts tun duerfen), ein Sprung aus "Recent",
* eine neue Navigation nach Zurueck (Vor faellt weg), Galaxy als eigener
* Arbeitsbereich und der Projektwechsel nach cbm (frischer Verlauf).
*
* Jeder Schritt schreibt ein Bild <out>/NN-name.png; <out>/index.md fuehrt
* jedes Bild mit dem, was es zeigt, und dem Messwert, <out>/report.json alle
* Pruefungen. Der Browser laeuft ohne Fenster. Das Skript startet keinen
* Server und schreibt nichts in das Protokoll des Servers: POST /api/ui-log
* wird im Browser beantwortet und navigator.sendBeacon bleibt in der Seite.
*/
import { chromium } from 'playwright';
import { mkdir, rm, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
const argv = process.argv.slice(2);
const arg = (name, fallback) => {
const index = argv.indexOf(`--${name}`);
if (index < 0) return fallback;
const value = argv[index + 1];
return value === undefined || value.startsWith('--') ? true : value;
};
const ORIGIN = String(arg('origin', 'http://127.0.0.1:4375')).replace(/\/$/, '');
const OUT = resolve(String(arg('out', 'verification/handtest-fixes/k27')));
const PROFILE = resolve(String(arg('profile', join(OUT, '..', 'profile-k27'))));
const PROJECT = String(arg('project', 'django-demo'));
const CONTROL = String(arg('control', 'cbm'));
const VIEWPORT = { width: 1600, height: 1000 };
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
const checks = [];
const images = [];
let counter = 0;
function check(id, title, pass, measured) {
checks.push({ id, title, pass: Boolean(pass), measured });
console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${title} ${JSON.stringify(measured)}`);
}
async function shot(page, name, note, measured) {
counter += 1;
const file = `${String(counter).padStart(2, '0')}-${name}.png`;
await page.evaluate(() => { const section = document.querySelector('.atlas-architecture'); if (section) section.scrollTop = 0; });
await page.screenshot({ path: join(OUT, file) });
images.push({ file, note, measured });
}
/* ------------------------------------------------------------------ */
/* Was die Seite gerade zeigt */
async function state(page) {
return page.evaluate(() => {
const root = document.querySelector('.atlas-architecture');
if (!root) return null;
const text = (element) => element?.textContent?.replace(/\s+/g, ' ').trim() ?? null;
const history = root.querySelector('[role="group"][aria-label="Architecture history"]');
const button = (label) => { const element = history?.querySelector(`button[aria-label="${label}"]`); return element ? { disabled: element.disabled, title: element.getAttribute('title') } : null; };
const location = root.querySelector('[aria-label="Architecture location"]');
const select = root.querySelector('select[aria-label="Behavior entry point"]');
const systemLabels = [...root.querySelectorAll('.system-scene-node-label')];
return {
view: root.querySelector('.atlas-arch-tab[aria-pressed="true"]')?.getAttribute('data-view') ?? null,
trail: location ? [...location.children].filter((element) => element.tagName === 'BUTTON' || (element.tagName === 'SPAN' && element.textContent !== '/')).map((element) => element.textContent) : null,
camera: text(root.querySelector('[aria-label="Map camera"] button[aria-pressed="true"]')),
perspective: text(root.querySelector('[aria-label="Routes perspective"] button[aria-pressed="true"]')),
filter: root.querySelector('input[type="search"]')?.value ?? null,
routeChips: root.querySelectorAll('button.architecture-node-label[data-node-id^="route"]').length,
focus: systemLabels.filter((element) => element.dataset.focus === 'true' && element.dataset.presentation === 'system').map((element) => text(element.querySelector('strong'))),
expanded: systemLabels.filter((element) => element.dataset.expanded === 'true').map((element) => text(element.querySelector('strong'))),
heading: text(root.querySelector('.behavior-heading h2')),
start: select ? text(select.selectedOptions[0]) : null,
back: button('Back'), forward: button('Forward'),
position: history?.getAttribute('data-position') ?? null,
recent: [...(history?.querySelectorAll('[aria-label="Recently visited places"] li button') ?? [])].map((element) => ({
name: text(element.querySelector('strong')), detail: text(element.querySelector('span')) ?? '', current: element.getAttribute('aria-current') === 'true' })),
};
});
}
/** Wartet, bis die Seite den erwarteten Ort zeigt; liefert den letzten Messwert. */
async function settle(page, expected, timeout = 90000) {
const started = Date.now();
let last = null;
while (Date.now() - started < timeout) {
last = await state(page);
if (last && matches(last, expected).length === 0) { await wait(900); return await state(page); }
await wait(400);
}
return last;
}
/** Was vom Erwarteten abweicht. Ein fehlendes Feld im Erwarteten wird nicht geprueft. */
function matches(actual, expected) {
const misses = [];
for (const [field, value] of Object.entries(expected)) {
const got = actual?.[field];
const same = value instanceof RegExp ? typeof got === 'string' && value.test(got) : JSON.stringify(got) === JSON.stringify(value);
if (!same) misses.push(`${field}: ${JSON.stringify(got)} != ${value instanceof RegExp ? value : JSON.stringify(value)}`);
}
return misses;
}
const tab = (page, view) => page.locator(`.atlas-architecture button.atlas-arch-tab[data-view="${view}"]`).first();
const historyButton = (page, label) => page.locator(`.atlas-architecture [aria-label="Architecture history"] button[aria-label="${label}"]`).first();
/* ------------------------------------------------------------------ */
/* Ablauf */
await rm(PROFILE, { recursive: true, force: true });
await mkdir(OUT, { recursive: true });
const context = await chromium.launchPersistentContext(PROFILE, {
headless: true, viewport: VIEWPORT, deviceScaleFactor: 2,
args: ['--enable-unsafe-webgpu', '--ignore-gpu-blocklist'],
});
await context.addInitScript(() => {
try { localStorage.setItem('cbm.workspace.setup', 'done'); } catch { /* ohne Speicher geht es auch */ }
window.__beacons = [];
navigator.sendBeacon = (url, data) => { window.__beacons.push({ url: String(url), data }); return true; };
});
await context.route('**/api/ui-log', async (route) => {
if (route.request().method() !== 'POST') return route.continue();
return route.fulfill({ status: 200, contentType: 'application/json', body: '{"accepted":0}' });
});
const page = context.pages()[0] ?? await context.newPage();
const pageErrors = [];
page.on('pageerror', (error) => { pageErrors.push(error.message); console.error(`[pageerror] ${error.message}`); });
/** Die Schritte des Hinwegs: was jeder zeigt und wie sein Tooltip ihn nennt. */
const steps = [];
async function record(name, note, expected, label) {
const measured = await settle(page, expected);
const misses = matches(measured, expected);
steps.push({ name, expected, label });
check(`F${steps.length - 1}`, `Hinweg ${name}: ${note}`, misses.length === 0, { misses, measured });
await shot(page, `hin-${name}`, `Hinweg, Schritt ${steps.length - 1}: ${note}`, measured);
return measured;
}
try {
await page.goto(`${ORIGIN}/?project=${encodeURIComponent(PROJECT)}&workspace=architecture`, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.atlas-shell', { timeout: 30000 });
await page.waitForFunction(() => document.querySelectorAll('.atlas-architecture button.architecture-node-label').length > 0, null, { timeout: 180000 });
await wait(1500);
let measured = await record('overview', 'Overview an der Wurzel, Zurueck und Vor gesperrt',
{ view: 'overview', trail: [PROJECT], position: '1/1' }, 'Overview');
check('C0', 'Am Anfang: Zurueck und Vor gesperrt, mit Tooltip', measured?.back?.disabled && measured?.forward?.disabled
&& measured.back.title === 'Nothing to go back to yet' && measured.forward.title === 'Nothing to go forward to', { back: measured?.back, forward: measured?.forward });
// 1. Bereich django oeffnen
await page.locator('.atlas-architecture button.architecture-node-label', { hasText: /^\W*django\s*$/ }).first().click();
await wait(600);
await page.locator('.atlas-architecture').getByRole('button', { name: /^Open area/ }).first().click();
await record('django', 'Bereich django geoeffnet, Brotkruemel django-demo / django', { view: 'overview', trail: [PROJECT, 'django'] }, 'Overview · django');
// 2. Routes, Service map
await tab(page, 'routes').click();
await record('routes', 'Routes, Service map', { view: 'routes', perspective: 'Service map', filter: '' }, 'Routes · Service map');
// 3. Endpoints
await page.locator('.atlas-architecture [aria-label="Routes perspective"] button', { hasText: 'Endpoints' }).click();
await page.waitForFunction(() => document.querySelectorAll('.atlas-architecture button.architecture-node-label[data-node-id="route-group:/edit"]').length > 0, null, { timeout: 120000 });
const groups = await record('endpoints', 'Routes, Endpoints mit Routengruppen', { view: 'routes', perspective: 'Endpoints', filter: '' }, 'Routes · Endpoints');
const allRoutes = groups?.routeChips ?? 0;
// 4. Gruppe /edit oeffnen
await page.locator('.atlas-architecture button.architecture-node-label[data-node-id="route-group:/edit"]').click();
await wait(600);
const showRoutes = page.locator('.atlas-architecture button', { hasText: /^Show these \d+ routes/ }).first();
const showText = (await showRoutes.textContent())?.trim();
await showRoutes.click();
measured = await record('edit', `Gruppe /edit geoeffnet (${showText})`, { view: 'routes', perspective: 'Endpoints', filter: '/edit' }, 'Routes · Endpoints · /edit');
const editRoutes = measured?.routeChips ?? 0;
// 5. System structure
await tab(page, 'structure').click();
await page.waitForFunction(() => document.querySelectorAll('.atlas-architecture .system-scene-node-label[data-kind="group"]').length > 0, null, { timeout: 180000 });
await record('structure', 'System structure, ganzes System', { view: 'structure', focus: [], expanded: [] }, 'System structure');
// 6. Eine Gruppe aufklappen (Doppelklick: aufklappen und fokussieren, wie bisher)
const groupLabels = await page.locator('.atlas-architecture .system-scene-node-label[data-kind="group"]').evaluateAll((elements) =>
elements.map((element) => ({ id: element.dataset.nodeId, name: element.querySelector('strong')?.textContent?.trim() ?? '' })));
const group = groupLabels.find((item) => item.name === 'django') ?? groupLabels[0];
await page.locator(`.atlas-architecture .system-scene-node-label[data-node-id="${group.id}"]`).dblclick();
await record('group', `Gruppe ${group.name} aufgeklappt und fokussiert`, { view: 'structure', focus: [group.name], expanded: [group.name] }, `System structure · ${group.name} · 1 group open`);
// 7. Behavior: der vorgeschlagene Start fuellt den Schritt, statt einen eigenen abzulegen
await tab(page, 'behavior').click();
await page.waitForFunction(() => /What can .+ call\?/.test(document.querySelector('.behavior-heading h2')?.textContent ?? ''), null, { timeout: 180000 });
measured = await record('behavior', 'Behavior mit dem vorgeschlagenen Start main', { view: 'behavior', heading: 'What can main call?' }, 'Behavior · main');
check('C7', 'Der automatische Start main ist kein eigener Schritt (Zurueck fuehrt zur System structure)',
measured?.back?.title === `Back to System structure · ${group.name} · 1 group open (Alt+Left)`, { back: measured?.back, position: measured?.position });
// 8. Start handle · .../loaddata.py
const select = page.locator('.atlas-architecture select[aria-label="Behavior entry point"]');
const options = await select.locator('option').allTextContents();
const index = options.findIndex((option) => /^handle · .*loaddata\.py$/.test(option.trim()));
await select.selectOption({ index });
await page.waitForFunction(() => document.querySelector('.behavior-heading h2')?.textContent === 'What can handle call?', null, { timeout: 180000 });
await page.waitForFunction(() => document.querySelectorAll('.atlas-architecture .system-scene-node-label[data-node-id^="journey-choice:"]').length > 1, null, { timeout: 120000 });
measured = await record('handle', `Start ${options[index]?.trim()}`, { view: 'behavior', heading: 'What can handle call?', start: /^handle · .*loaddata\.py$/ }, 'Behavior · handle');
// 9. Einem Aufruf folgen (Doppelklick)
const callees = await page.locator('.atlas-architecture .system-scene-node-label[data-node-id^="journey-choice:"]').evaluateAll((elements) =>
elements.map((element) => ({ id: element.dataset.nodeId, name: element.querySelector('strong')?.textContent?.trim() ?? '' })));
const handleId = callees[0]?.id;
const callee = callees.find((item) => item.id !== handleId && item.name && !/^handle$/.test(item.name)) ?? callees[1];
await page.locator(`.atlas-architecture .system-scene-node-label[data-node-id="${callee.id}"]`).dblclick();
await record('follow', `Aufruf ${callee.name} gefolgt`, { view: 'behavior', heading: `What can ${callee.name} call?` }, `Behavior · ${callee.name} · followed from handle`);
const last = steps.length - 1;
const labels = steps.map((step) => step.label);
check('C-routes', 'Die Gruppe /edit zeigt weniger Routen als alle Gruppen zusammen', editRoutes > 0 && allRoutes > 0, { allRouteChips: allRoutes, editRouteChips: editRoutes, button: showText });
// Zurueck Schritt fuer Schritt bis zum Anfang
for (let at = last - 1; at >= 0; at--) {
await historyButton(page, 'Back').click();
const expected = { ...steps[at].expected, back: at > 0 ? { disabled: false, title: `Back to ${labels[at - 1]} (Alt+Left)` } : { disabled: true, title: 'Nothing to go back to yet' },
forward: { disabled: false, title: `Forward to ${labels[at + 1]} (Alt+Right)` }, position: `${at + 1}/${last + 1}` };
measured = await settle(page, expected);
const misses = matches(measured, expected);
check(`B${at}`, `Zurueck auf ${labels[at]}`, misses.length === 0, { misses, measured });
await shot(page, `zurueck-${String(at).padStart(2, '0')}-${steps[at].name}`, `Zurueck, Schritt ${at}: ${labels[at]}; Tooltips nennen ${at > 0 ? labels[at - 1] : 'nichts'} und ${labels[at + 1]}`, measured);
}
// Und wieder vor bis zum Ende
for (let at = 1; at <= last; at++) {
await historyButton(page, 'Forward').click();
const expected = { ...steps[at].expected, back: { disabled: false, title: `Back to ${labels[at - 1]} (Alt+Left)` },
forward: at < last ? { disabled: false, title: `Forward to ${labels[at + 1]} (Alt+Right)` } : { disabled: true, title: 'Nothing to go forward to' }, position: `${at + 1}/${last + 1}` };
measured = await settle(page, expected);
const misses = matches(measured, expected);
check(`V${at}`, `Vor auf ${labels[at]}`, misses.length === 0, { misses, measured });
await shot(page, `vor-${String(at).padStart(2, '0')}-${steps[at].name}`, `Vor, Schritt ${at}: ${labels[at]}`, measured);
}
// Alt+Links und Alt+Rechts
await page.locator('.atlas-architecture .behavior-heading h2').click();
await page.keyboard.press('Alt+ArrowLeft');
measured = await settle(page, { ...steps[last - 1].expected, position: `${last}/${last + 1}` });
check('K-left', 'Alt+Links geht einen Schritt zurueck', matches(measured, { ...steps[last - 1].expected, position: `${last}/${last + 1}` }).length === 0, measured);
await shot(page, 'alt-links', `Alt+Links: zurueck auf ${labels[last - 1]}`, measured);
await page.keyboard.press('Alt+ArrowRight');
measured = await settle(page, { ...steps[last].expected, position: `${last + 1}/${last + 1}` });
check('K-right', 'Alt+Rechts geht einen Schritt vor', matches(measured, { ...steps[last].expected, position: `${last + 1}/${last + 1}` }).length === 0, measured);
await shot(page, 'alt-rechts', `Alt+Rechts: vor auf ${labels[last]}`, measured);
// Beim Tippen im Routen-Filter gehoeren die Pfeile dem Feld
for (let step = 0; step < 5; step++) await historyButton(page, 'Back').click().then(() => wait(250));
measured = await settle(page, { ...steps[4].expected, position: '5/10' });
const search = page.locator('.atlas-architecture input[type="search"]');
await search.focus();
const typingFocus = await page.evaluate(() => document.activeElement?.getAttribute('type') === 'search');
await page.keyboard.press('Alt+ArrowLeft');
await wait(1200);
const typing = await state(page);
check('K-typing', 'Alt+Links im Routen-Filter blaettert nicht', typingFocus && typing?.view === 'routes' && typing.filter === '/edit' && typing.position === '5/10', { typingFocus, ...typing });
await shot(page, 'alt-links-beim-tippen', 'Alt+Links im Filterfeld: Routes · Endpoints · /edit bleibt stehen', typing);
await page.evaluate(() => (document.activeElement instanceof HTMLElement ? document.activeElement.blur() : undefined));
for (let step = 0; step < 5; step++) await historyButton(page, 'Forward').click().then(() => wait(250));
measured = await settle(page, { ...steps[last].expected, position: '10/10' });
// Ein Sprung aus "Recent" ist eine neue Navigation
await page.locator('.atlas-architecture details.atlas-arch-recent > summary').click();
await wait(400);
measured = await state(page);
await shot(page, 'recent-offen', 'Recent geoeffnet: die zuletzt besuchten Orte, der aktuelle ausgegraut', measured);
// Begrenzt auf 8 Orte: nach Hin- und Rueckweg stehen die acht zuletzt besuchten darin, der aktuelle zuerst.
check('R-list', 'Recent listet hoechstens 8 Orte, neueste zuerst, den aktuellen markiert',
measured?.recent?.length === 8 && measured.recent[0]?.current && measured.recent.some((item) => item.name === 'Routes' && item.detail === 'Endpoints · /edit'), measured?.recent);
await page.locator('.atlas-architecture [aria-label="Recently visited places"] li button', { has: page.locator('span', { hasText: /^Endpoints · \/edit$/ }) }).first().click();
const jumped = { view: 'routes', perspective: 'Endpoints', filter: '/edit', forward: { disabled: true, title: 'Nothing to go forward to' },
back: { disabled: false, title: `Back to ${labels[last]} (Alt+Left)` }, position: `${last + 2}/${last + 2}` };
measured = await settle(page, jumped);
check('R-jump', 'Recent: Sprung nach Routes · Endpoints · /edit als neuer Schritt', matches(measured, jumped).length === 0, { misses: matches(measured, jumped), measured });
await shot(page, 'recent-sprung', 'Nach dem Sprung aus Recent: Routes · Endpoints · /edit, Vor gesperrt', measured);
// Eine neue Navigation nach Zurueck verwirft den Vorwaertszweig
await historyButton(page, 'Back').click();
measured = await settle(page, { ...steps[last].expected, forward: { disabled: false, title: 'Forward to Routes · Endpoints · /edit (Alt+Right)' } });
await shot(page, 'zurueck-vor-neuer-navigation', `Zurueck auf ${labels[last]}, Vor nennt Routes · Endpoints · /edit`, measured);
await tab(page, 'hotspots').click();
const dropped = { view: 'hotspots', forward: { disabled: true, title: 'Nothing to go forward to' }, back: { disabled: false, title: `Back to ${labels[last]} (Alt+Left)` } };
measured = await settle(page, dropped);
check('N-drop', 'Neue Navigation nach Zurueck: Vor faellt weg', matches(measured, dropped).length === 0, { misses: matches(measured, dropped), measured });
await shot(page, 'neue-navigation', 'Hotspots nach Zurueck: der Vorwaertszweig ist verworfen', measured);
// Galaxy hat einen eigenen Verlauf; Alt+Pfeile wirken nur im aktiven Arbeitsbereich
const before = measured?.position;
await page.locator('[data-workspace-tab="galaxy"]').first().click();
await wait(2500);
// Der Fokus verlaesst die Reiterleiste: dort sind die Pfeile ihre eigene Tastenfuehrung zwischen den Arbeitsbereichen.
await page.evaluate(() => (document.activeElement instanceof HTMLElement ? document.activeElement.blur() : undefined));
await page.keyboard.press('Alt+ArrowLeft');
await wait(800);
const workspace = await page.locator('.atlas-shell').first().getAttribute('data-workspace');
await shot(page, 'galaxy-alt-links', `Galaxy aktiv (${workspace}), Alt+Links: Architecture bleibt unberuehrt`, null);
await page.locator('[data-workspace-tab="architecture"]').first().click();
await wait(1500);
measured = await state(page);
check('G-separate', 'Alt+Links in Galaxy aendert den Architecture-Verlauf nicht', workspace === 'galaxy' && measured?.view === 'hotspots' && measured.position === before, { workspace, before, after: measured?.position, view: measured?.view });
// Projektwechsel nach cbm: frischer Verlauf
await page.locator('.atlas-shell details summary', { hasText: PROJECT }).first().click();
const entry = page.locator('.atlas-project-results button', { has: page.locator('.atlas-project-result-name', { hasText: new RegExp(`^${CONTROL}$`) }) }).first();
await entry.waitFor({ timeout: 20000 });
await entry.click();
const fresh = { position: '1/1', back: { disabled: true, title: 'Nothing to go back to yet' }, forward: { disabled: true, title: 'Nothing to go forward to' }, recent: [] };
measured = await settle(page, fresh, 60000);
await wait(2500);
measured = await state(page);
check('P-fresh', `Projektwechsel nach ${CONTROL}: frischer Verlauf ohne Zurueck, Vor und Recent`, matches(measured, fresh).length === 0, { misses: matches(measured, fresh), measured });
await shot(page, 'cbm-frisch', `Architecture in ${CONTROL} nach dem Projektwechsel: frischer Verlauf`, measured);
await tab(page, 'routes').click();
const startedOn = architectureName(measured?.view);
measured = await settle(page, { view: 'routes', position: '2/2', back: { disabled: false, title: `Back to ${startedOn} (Alt+Left)` } }, 60000);
check('P-step', `${CONTROL}: der erste Schritt fuehrt nur in ${CONTROL} zurueck`, measured?.position === '2/2' && measured?.back?.title === `Back to ${startedOn} (Alt+Left)`, { position: measured?.position, back: measured?.back });
await shot(page, 'cbm-erster-schritt', `${CONTROL}: Routes, Zurueck nennt den Start in ${CONTROL}`, measured);
} catch (error) {
check('X0', 'Ablauf abgebrochen', false, String(error?.stack ?? error));
await shot(page, 'abbruch', 'Stand beim Abbruch', await state(page).catch(() => null)).catch(() => {});
}
function architectureName(view) {
return { overview: 'Overview', routes: 'Routes · Service map', hotspots: 'Hotspots', structure: 'System structure', behavior: 'Behavior' }[view] ?? 'Overview';
}
check('X1', 'Keine Seitenfehler im Lauf', pageErrors.length === 0, { pageErrors });
await context.close();
const lines = ['# K27: Zurueck und Vor in Architecture', '', `Ursprung ${ORIGIN}, Projekt ${PROJECT}, Kontrolle ${CONTROL}, Ansicht ${VIEWPORT.width}x${VIEWPORT.height}, ohne Fenster.`, '',
...checks.map((item) => `- ${item.pass ? 'PASS' : 'FAIL'} ${item.id}: ${item.title}`), '', '| Bild | Zeigt | Gemessen |', '|---|---|---|',
...images.map((item) => `| ${item.file} | ${item.note} | ${item.measured ? `${item.measured.view ?? ''} · ${JSON.stringify(item.measured.trail ?? '')} · ${item.measured.perspective ?? ''} · filter ${JSON.stringify(item.measured.filter)} · focus ${JSON.stringify(item.measured.focus)} · ${item.measured.heading ?? ''} · ${item.measured.position ?? ''} · ${item.measured.back?.title ?? ''} / ${item.measured.forward?.title ?? ''}`.replaceAll('|', '/') : ''} |`), ''];
await writeFile(join(OUT, 'index.md'), lines.join('\n'));
await writeFile(join(OUT, 'report.json'), JSON.stringify({ origin: ORIGIN, project: PROJECT, control: CONTROL, checks }, null, 2));
const failed = checks.filter((item) => !item.pass);
console.log(`${checks.length - failed.length}/${checks.length} bestanden`);
if (failed.length) process.exitCode = 1;