Files
KantSites/scripts/navigation-ux-check.mjs
kantarciandClaude Opus 5 ace6c50743 Raise loaded documents to the schema their blocks need, and bring the old browser checks back
The kant-it.de header used the hamburger switch at schema 19, so the server refused its
preview and save; the editor now raises a loaded document to the version its blocks and
properties require. The admin, button and navigation checks select blocks through the
structure tab, find the motion button by id and expect what the site's motion setting says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:21:35 +02:00

96 lines
7.3 KiB
JavaScript

import { chromium } from 'playwright';
import assert from 'node:assert/strict';
import { execFileSync, spawn } from 'node:child_process';
import { mkdir, writeFile } from 'node:fs/promises';
import { randomBytes } from 'node:crypto';
import net from 'node:net';
const php = process.env.PHP_BINARY || 'php';
const fixture = JSON.parse(execFileSync(php, ['tests/pilot-fixture.php'], { encoding: 'utf8', windowsHide: true }));
const env = { ...process.env, APP_ENV: 'prod', APP_DEBUG: '0', APP_SECRET: randomBytes(32).toString('hex'), DATABASE_URL: fixture.databaseUrl, MEDIA_DIRECTORY: fixture.mediaDirectory };
execFileSync(php, ['bin/console', 'cache:clear', '--env=prod', '--no-debug'], { stdio: 'ignore', windowsHide: true, env });
const probe = net.createServer(); await new Promise(resolve => probe.listen(0, '127.0.0.1', resolve));
const port = probe.address().port; await new Promise(resolve => probe.close(resolve));
const origin = `http://127.0.0.1:${port}`;
const server = spawn(php, ['-d', 'memory_limit=256M', '-S', `127.0.0.1:${port}`, '-t', 'public', 'bin/dev-router.php'], { windowsHide: true, stdio: 'ignore', env: { ...env, SITE_ORIGIN: origin } });
const report = 'reports/navigation-ux'; await mkdir(report, { recursive: true });
let browser, page;
const checks = [], errors = [];
try {
for (let attempt = 0; attempt < 60; attempt++) {
try { if ((await fetch(origin)).ok) break; } catch {}
if (attempt === 59) throw new Error('Pilot test server did not start');
await new Promise(resolve => setTimeout(resolve, 200));
}
browser = await chromium.launch();
const context = await browser.newContext({ viewport: { width: 1600, height: 1050 } });
page = await context.newPage(); page.setDefaultTimeout(15000); page.on('pageerror', error => errors.push(error.message));
await page.goto(origin + '/admin/login');
await page.getByLabel('Benutzername').fill(fixture.username); await page.getByLabel('Passwort', { exact: true }).fill(fixture.password);
await page.getByRole('button', { name: 'Anmelden', exact: true }).click(); await page.waitForURL(origin + '/admin');
const api = `${origin}/admin/api/pages/${fixture.homeId}`;
const data = await (await context.request.get(api)).json();
data.document.blocks.push({ id: 'qa-anchor', type: 'section', children: [{ id: 'qa-anchor-title', type: 'heading', props: { text: 'Kontaktbereich', level: 2 } }] });
assert.equal((await context.request.post(api + '/draft', { headers: { 'X-CSRF-Token': data.csrfToken }, data: { document: data.document, expectedVersion: data.version } })).status(), 200);
await page.goto(`${origin}/admin/pages/${fixture.homeId}`);
// Im Reiter „Struktur“ wählen und mit „Bearbeiten“ öffnen, wie im Editor selbst.
await page.getByRole('tab', { name: 'Struktur', exact: true }).click();
await page.locator('#structure [data-block-id="qa-anchor"]').click();
await page.locator('#structure [data-edit-block="qa-anchor"]').click();
await page.locator('#property-anchor').fill('#kontakt-check'); await page.locator('#property-anchor').blur();
await page.locator('#save').click(); await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor();
await page.locator('#publish').click(); await page.locator('#confirm-publish').click(); await page.locator('#save-status').filter({ hasText: 'Veröffentlicht' }).waitFor();
const stored = (await (await context.request.get(api)).json()).document;
assert.equal(stored.blocks.at(-1).anchor, 'kontakt-check'); assert.equal(stored.schemaVersion, 21);
checks.push('named jump target created in editor and saved/published with stable block ID');
await page.goto(origin + '/admin/menus');
const rows = page.locator('#menu-items > li');
const originalFirst = await rows.first().locator('[data-key="label"]').inputValue();
const handle = rows.first().locator('[data-menu-drag]');
const start = await handle.boundingBox(), end = await rows.last().boundingBox();
await page.mouse.move(start.x + start.width / 2, start.y + start.height / 2); await page.mouse.down();
await page.mouse.move(start.x + start.width / 2, end.y + end.height - 2, { steps: 12 }); await page.mouse.up();
assert.equal(await rows.last().locator('[data-key="label"]').inputValue(), originalFirst);
await page.locator('#menu-undo').click(); assert.equal(await rows.first().locator('[data-key="label"]').inputValue(), originalFirst);
await rows.first().locator('[data-menu-drag]').focus(); await page.keyboard.press('ArrowDown');
assert.notEqual(await rows.first().locator('[data-key="label"]').inputValue(), originalFirst);
await page.locator('#menu-undo').click();
await rows.first().locator('summary').click();
await rows.first().locator('[data-key="icon"]').selectOption('phone');
await rows.first().locator('[data-key="iconPosition"]').selectOption('after');
assert.equal(await rows.first().locator('.menu-entry-title > svg').count(), 1);
await page.getByText('Eigener Link', { exact: true }).first().click();
await page.locator('#link-label').fill('Zum Kontaktbereich'); await page.locator('#link-url').fill('#kontakt-check');
await page.locator('#menu-add-url').click();
await page.getByRole('button', { name: 'Menü speichern', exact: true }).click();
await page.locator('#menu-status').filter({ hasText: 'Gespeicherter Stand' }).waitFor();
await page.reload();
assert.equal(await rows.first().locator('[data-key="icon"]').inputValue(), 'phone');
assert.equal(await rows.first().locator('[data-key="iconPosition"]').inputValue(), 'after');
await rows.first().locator('summary').click();
await page.screenshot({ path: `${report}/menus.png`, fullPage: true });
checks.push('pointer drag, keyboard ordering, undo, icon placement, fragment menu link and save/reload');
const plain = await browser.newContext({ javaScriptEnabled: false }); const publicPage = await plain.newPage();
await publicPage.goto(origin);
assert.ok(await publicPage.locator('.nav-label > svg').count() > 0);
await publicPage.getByRole('link', { name: 'Zum Kontaktbereich', exact: true }).first().click();
assert.ok(publicPage.url().endsWith('#kontakt-check'));
assert.equal(await publicPage.locator('#kontakt-check').count(), 1);
assert.ok(await publicPage.evaluate(() => window.scrollY) > 0);
await plain.close();
execFileSync(php, ['-r', 'require "vendor/autoload.php"; $db=App\\Infrastructure\\ConnectionFactory::create($argv[1]); $db->executeStatement("UPDATE scheduled_task SET last_status = ?, last_message = ? WHERE name = ?", ["ok", "Fassung 99.0.0 ist verfügbar; installiert ist 1.8.4.", "update-check"]);', fixture.databaseUrl], { windowsHide: true });
await page.reload();
assert.equal(await page.getByRole('link', { name: 'Update 99.0.0 verfügbar. Aktualisierung öffnen.', exact: true }).count(), 1);
assert.match(await page.locator('.version-update').getAttribute('href'), /system\?pruefen=1$/);
await page.locator('.shell-foot').screenshot({ path: `${report}/update-hint.png` });
checks.push('public icon and native anchor navigation without JavaScript; cached update notice by version');
assert.deepEqual(errors, []);
await writeFile(`${report}/result.json`, JSON.stringify({ checks, passed: true }, null, 2));
console.log('Navigation checks passed:', checks.length, 'groups.');
} catch (error) {
if (page) await page.screenshot({ path: `${report}/failure.png`, fullPage: true }).catch(() => {});
throw error;
} finally { await browser?.close(); server.kill(); }