Files
KantSites/scripts/editor-check.mjs
T
kantarciandClaude Opus 5 ba56add678 Browsertests wiederhergestellt; drei echte Fehler gefunden
Der Playwright-Editortest war durch den Editorumbau kaputt: er
steuerte die abgeschafften Auswahllisten. Umgeschrieben auf die
freien Bedienelemente - Einheitenfelder, Vererbung über drei Geräte,
einzelnes Zurücksetzen, Palettenfarbe, Abstand je Seite, Hover-Regel,
freie Rasterspalten und die weiter vorhandenen Bewegungseffekte.

Dabei kamen drei Fehler heraus, die ohne Browserlauf unsichtbar sind:

1. Die frei gestalteten Werte wirkten in der Editorvorschau gar
   nicht. Ein srcdoc-Iframe erbt die Richtlinie des einbettenden
   Dokuments, und die Verwaltung verbietet Inline-Styles - das
   erzeugte style-Element wurde stillschweigend verworfen.
   Ausgeliefert wurde es korrekt, nur die Vorschau log. Jetzt als
   inertes template plus adoptedStyleSheets.
2. Die Einrückung der Unterseiten in der Seitenliste hat nie
   funktioniert; sie lief über ein style-Attribut, das dieselbe
   Richtlinie verwirft. Jetzt über Klassen.
3. Das Theme setzte alle Links auf die Akzentfarbe und überschrieb
   die kontextabhängigen Farben: 3,08:1 auf dunklem Grund. Die Regel
   gilt jetzt nur für Links in Textblöcken, die voreingestellte
   Linkfarbe liegt auf allen hellen Tönen über 4,5:1.

Geprüft: 155 PHP-Tests / 579 Assertions unter PHP 8.2.12 und 8.4.22,
15 JavaScript-Modelltests, vollständiger Editortest mit axe (null
Verstöße) und Lighthouse 13.4.1 mit je drei Läufen mobil und Desktop,
alle fünf Kategorien 100.

Offen: npm run test:browser schlägt wegen eines Inhalts fehl -
/grundlagen trägt einen im Editor gesetzten Textblock mit 2,75:1.
Nicht verändert, weil es eine Nutzerentscheidung ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:16:08 +02:00

386 lines
26 KiB
JavaScript

import { chromium } from 'playwright';
import AxeBuilder from '@axe-core/playwright';
import assert from 'node:assert/strict';
import { mkdir, writeFile } from 'node:fs/promises';
import { spawn, execFileSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import net from 'node:net';
const php = process.env.PHP_BINARY || 'php';
execFileSync(php, ['bin/console', 'cache:clear', '--env=prod', '--no-debug'], { stdio: 'ignore' });
const fixture = JSON.parse(execFileSync(php, ['tests/browser-fixture.php'], { encoding: 'utf8' }));
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', 'upload_max_filesize=8M', '-d', 'post_max_size=10M', '-d', 'memory_limit=256M', '-S', `127.0.0.1:${port}`, '-t', 'public'], {
windowsHide: true,
env: { ...process.env, APP_ENV: 'prod', APP_DEBUG: '0', APP_SECRET: randomBytes(32).toString('hex'), DATABASE_URL: fixture.databaseUrl, MEDIA_DIRECTORY: fixture.mediaDirectory, SITE_ORIGIN: origin },
stdio: 'ignore',
});
server.on('error', error => { throw error; });
let browser;
let activePage;
await mkdir('reports/editor', { recursive: true });
const checks = [];
try {
for (let i = 0; i < 60; i++) {
try { if ((await fetch(origin)).ok) break; } catch {}
if (i === 59) throw new Error('Isolated PHP server did not start.');
await new Promise(resolve => setTimeout(resolve, 200));
}
browser = await chromium.launch();
const context = await browser.newContext({ viewport: { width: 1600, height: 1100 }, acceptDownloads: true });
context.setDefaultTimeout(15000);
const page = await context.newPage();
activePage = page;
const errors = [];
page.on('pageerror', error => { errors.push(error.message); console.error('Editor script error:', error.message); });
page.on('console', message => { if (message.type() === 'error' && /Content Security Policy/.test(message.text())) errors.push(message.text()); });
page.on('dialog', dialog => dialog.accept());
const axe = async name => {
console.log('Accessibility check:', name);
// The no-script preview sandbox cannot host axe's frame worker. Audit the identical
// published renderer separately; retain the sandbox in the real editor.
const result = await new AxeBuilder({ page }).exclude('#preview').withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'best-practice']).analyze();
assert.deepEqual(result.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) })), [], name);
checks.push({ name, violations: 0 });
};
await page.goto(origin + '/admin');
assert.equal(new URL(page.url()).pathname, '/admin/login');
await axe('login');
await page.screenshot({ path: 'reports/editor/login.png', fullPage: true });
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');
console.log('Authenticated in isolated test installation.');
await axe('page list');
await page.screenshot({ path: 'reports/editor/pages.png', fullPage: true });
await page.locator('a[href="/admin/pages/test-home"]').first().click();
await page.locator('#save-status').filter({ hasText: 'Veröffentlicht' }).waitFor();
console.log('Editor data loaded.');
console.log('Preview status:', await page.locator('#preview-status').innerText());
const frame = page.frameLocator('#preview');
await frame.locator('h1').waitFor();
await page.locator('#preview-status').filter({ hasText: 'Zum Auswählen' }).waitFor();
await frame.locator('h1').click();
assert.equal(await page.locator('#properties-title').innerText(), 'Überschrift');
await page.getByLabel('Text', { exact: true }).fill('Meine bearbeitete Website');
await frame.getByRole('heading', { name: 'Meine bearbeitete Website', exact: true }).waitFor();
assert.equal(await page.locator('#publish').isDisabled(), true);
await page.getByRole('button', { name: 'Rückgängig', exact: true }).click();
await frame.getByRole('heading', { name: /Deine Inhalte/, level: 1 }).waitFor();
await page.getByRole('button', { name: 'Wiederholen', exact: true }).click();
await frame.getByRole('heading', { name: 'Meine bearbeitete Website', exact: true }).waitFor();
checks.push({ name: 'preview selection, property edit, undo and redo', passed: true });
console.log('Preview edits and history passed.');
await page.getByRole('button', { name: 'Einklappen', exact: true }).click();
assert.equal(await page.locator('#structure .tree-item').count(), 3);
await page.getByLabel('Element suchen', { exact: true }).fill('Meine bearbeitete');
assert.equal(await page.locator('#structure .tree-item').count(), 2);
await page.locator('#structure [data-block-id="headline"]').focus();
await page.keyboard.press('Enter');
await page.getByLabel('Element suchen', { exact: true }).fill('');
assert.equal(await page.locator('#structure [data-block-id="headline"]').isVisible(), true);
await page.getByRole('button', { name: 'Alle öffnen', exact: true }).click();
await page.getByRole('button', { name: 'Design', exact: true }).click();
const frameValue = async (selector, property, expected) => {
await page.waitForFunction(({ selector, property, expected }) => {
const doc = document.querySelector('#preview').contentDocument;
const element = doc?.querySelector(selector);
return element && doc.defaultView.getComputedStyle(element)[property] === expected;
}, { selector, property, expected });
};
// Free values: a number plus a unit, not a fixed list. Filling and blurring fires the change.
const setLength = async (id, value, unit = 'px') => {
await page.locator(`#${id}`).fill(String(value));
await page.locator(`#${id}`).blur();
if (unit !== 'px') await page.locator(`#${id}`).locator('xpath=following-sibling::select').selectOption(unit);
};
const openGroup = async name => {
const group = page.locator('.design-group').filter({ has: page.locator('summary', { hasText: name }) });
if (!(await group.first().evaluate(element => element.open))) await group.first().locator('summary').click();
};
await setLength('style-fontSize', 72);
await frameValue('#headline', 'fontSize', '72px');
assert.equal(await page.locator('#preview').evaluate(element => element.contentWindow.innerWidth), 1240);
// Tablet and mobile inherit until they get an own value, and a reset makes them inherit again.
await page.getByRole('button', { name: 'Tablet', exact: true }).click();
await frameValue('#headline', 'fontSize', '72px');
await setLength('style-fontSize', 52);
await frameValue('#headline', 'fontSize', '52px');
await page.getByRole('button', { name: 'Mobil', exact: true }).click();
await frameValue('#headline', 'fontSize', '52px');
await setLength('style-fontSize', 36);
await frameValue('#headline', 'fontSize', '36px');
await page.getByRole('button', { name: 'Mobil zurücksetzen', exact: true }).click();
await frameValue('#headline', 'fontSize', '52px');
await page.getByRole('button', { name: 'Rückgängig', exact: true }).click();
await frameValue('#headline', 'fontSize', '36px');
// A single value can be reset on its own, without touching the rest of the device.
await page.getByLabel('Ausrichtung', { exact: true }).selectOption('center');
await frameValue('#headline', 'textAlign', 'center');
await page.getByRole('button', { name: 'Ausrichtung zurücksetzen', exact: true }).click();
await frameValue('#headline', 'textAlign', 'start');
await page.getByRole('button', { name: 'Desktop', exact: true }).click();
await frameValue('#headline', 'fontSize', '72px');
// Colours: a palette reference resolves to the theme variable, a hex value is taken as it is.
await page.locator('#style-color').locator('xpath=following-sibling::select').selectOption('@link');
await frameValue('#headline', 'color', 'rgb(164, 81, 15)');
const contrast = page.locator('.contrast-note');
await contrast.waitFor();
assert.match(await contrast.textContent(), /Kontrast \d/, 'The panel reports a contrast ratio.');
// Per side spacing on the section, and a hover rule that also covers keyboard focus.
await page.locator('#structure [data-block-id="hero"]').click();
await page.getByRole('button', { name: 'Design', exact: true }).click();
await setLength('style-padding-top', 96);
await frameValue('#hero', 'paddingTop', '96px');
await page.getByRole('button', { name: 'Mobil', exact: true }).click();
await setLength('style-padding-top', 32);
await frameValue('#hero', 'paddingTop', '32px');
await page.getByRole('button', { name: 'Desktop', exact: true }).click();
// The background group is collapsed by default, and the readable field next to the swatch is
// what a person types into.
await openGroup('Hintergrund');
const hoverColor = page.locator('#style-backgroundColor-hover').locator('xpath=following-sibling::input[contains(@class,"color-text")]');
await hoverColor.fill('#112233');
await hoverColor.blur();
// The preview carries its stylesheet as template text, applied by the editor as a constructed
// stylesheet; the hover rule has to arrive through that path, not as a style element.
await page.waitForFunction(() => {
const doc = document.querySelector('#preview').contentDocument;
const template = doc?.getElementById('kant-styles')?.content.textContent ?? '';
const adopted = [...(doc?.adoptedStyleSheets ?? [])].some(sheet => [...sheet.cssRules].some(rule => rule.selectorText?.includes('#hero:hover')));
return template.includes('#hero:hover,#hero:focus-within') && adopted;
});
checks.push({ name: 'free styles: units, inheritance, single reset, palette colour, per-side spacing, hover rule', passed: true });
await page.locator('[data-preset="columns"]').click();
const presetSection = await page.locator('#structure .tree-item[aria-pressed="true"]').getAttribute('data-block-id');
const presetRow = `#${presetSection} .block-row`;
await frame.locator(presetRow).waitFor();
// The preset carries its own responsive tokens: one column on mobile, two from tablet up.
await page.getByRole('button', { name: 'Mobil', exact: true }).click();
const columnCount = async expected => {
await page.waitForFunction(({ selector, expected }) => {
const doc = document.querySelector('#preview').contentDocument;
const element = doc?.querySelector(selector);
return element && doc.defaultView.getComputedStyle(element).gridTemplateColumns.split(' ').length === expected;
}, { selector: presetRow, expected });
};
await columnCount(1);
await page.getByRole('button', { name: 'Desktop', exact: true }).click();
await columnCount(2);
await page.getByRole('button', { name: 'Rückgängig', exact: true }).click();
await frame.locator(presetRow).waitFor({ state: 'detached' });
await page.getByRole('button', { name: 'Wiederholen', exact: true }).click();
await columnCount(2);
await page.locator('#structure [data-block-id="headline"]').click();
await page.getByRole('button', { name: 'Inhalt', exact: true }).click();
checks.push({ name: 'keyboard tree search and collapse, responsive inheritance/reset/undo, true viewport widths, spacing and responsive section preset', passed: true });
console.log('Responsive design, tree tools and presets passed.');
await page.locator('#library [data-add="gallery"]').click();
const galleryId = await page.locator('#structure .tree-item[aria-pressed="true"]').getAttribute('data-block-id');
await page.getByRole('button', { name: 'Galeriebilder auswählen', exact: true }).click();
await page.locator('#media-status').filter({ hasText: '0 Bilder' }).waitFor();
await page.locator('#media-upload').setInputFiles(fixture.images);
await page.locator('#media-status').filter({ hasText: '2 Bilder' }).waitFor();
const mediaItems = (await (await context.request.get(origin + '/admin/api/media')).json()).items;
const mediaResponse = await context.request.get(origin + mediaItems[0].variants[0].src);
assert.equal(mediaResponse.status(), 200, `Media delivery: ${(await mediaResponse.text()).slice(0, 160)}`);
for (const [index, item] of mediaItems.entries()) {
await page.locator(`[data-media-id="${item.id}"]`).focus();
await page.keyboard.press('Space');
assert.equal(await page.locator(`[data-media-id="${item.id}"]`).evaluate(button => button === document.activeElement), true);
await page.getByLabel('Bildtitel', { exact: true }).fill(index === 0 ? 'Bergblick' : 'Küstenblick');
await page.getByLabel('Alternativtext des Mediums', { exact: true }).fill(index === 0 ? 'Grüne Berglandschaft mit warmer Abendsonne' : 'Küstenlandschaft mit Hügeln und Abendsonne');
await page.getByRole('button', { name: 'Bildangaben speichern', exact: true }).click();
await page.getByRole('button', { name: 'Bildangaben speichern', exact: true }).waitFor({ state: 'visible' });
await page.waitForFunction(() => !document.querySelector('#media-save').disabled);
}
await axe('media picker');
await page.screenshot({ path: 'reports/editor/media-library.png', fullPage: true });
await page.getByRole('button', { name: 'Auswahl übernehmen (2)', exact: true }).click();
await frame.locator(`#${galleryId} img`).first().waitFor();
assert.equal(await frame.locator(`#${galleryId} img`).count(), 2);
await frame.locator(`#${galleryId} img`).first().scrollIntoViewIfNeeded();
await page.waitForFunction(id => document.querySelector('#preview').contentDocument?.querySelector(`#${id} img`)?.naturalWidth > 0, galleryId);
const imageId = await frame.locator(`#${galleryId} figure`).first().getAttribute('id');
await page.locator(`#structure [data-block-id="${imageId}"]`).click();
await page.getByLabel('Bildunterschrift', { exact: true }).fill('Ein Ausblick zum Durchatmen.');
await frame.getByText('Ein Ausblick zum Durchatmen.', { exact: true }).waitFor();
await page.getByRole('button', { name: 'Nach unten verschieben', exact: true }).click();
await page.getByRole('button', { name: 'Rückgängig', exact: true }).click();
await page.locator(`#structure [data-block-id="${galleryId}"]`).click();
await page.getByRole('button', { name: 'Design', exact: true }).click();
await openGroup('Layout');
await page.locator('#style-gridColumns').fill('2');
await page.locator('#style-gridColumns').blur();
const columnsOfGallery = async expected => {
await page.waitForFunction(({ id, expected }) => {
const doc = document.querySelector('#preview').contentDocument;
const element = doc?.querySelector(`#${id}`);
return element && doc.defaultView.getComputedStyle(element).gridTemplateColumns.split(' ').length === expected;
}, { id: galleryId, expected });
};
await columnsOfGallery(2);
// Motion presets are keyframes, which the free system cannot express, so they stay available.
await openGroup('Bewegung und Effekte');
await page.getByLabel('Schatten', { exact: true }).selectOption('soft');
await page.getByLabel('Hover-Effekt', { exact: true }).selectOption('lift');
await page.getByLabel('Einblendung beim Laden', { exact: true }).selectOption('rise');
await frame.locator(`#${galleryId}.fx-entrance-rise`).waitFor();
await page.getByRole('button', { name: 'Mobil', exact: true }).click();
await openGroup('Layout');
await page.locator('#style-gridColumns').fill('1');
await page.locator('#style-gridColumns').blur();
await columnsOfGallery(1);
await page.getByRole('button', { name: 'Desktop', exact: true }).click();
await columnsOfGallery(2);
await page.emulateMedia({ reducedMotion: 'reduce' });
await frameValue(`#${galleryId}`, 'animationName', 'none');
await frameValue(`#${galleryId}`, 'transform', 'none');
await page.emulateMedia({ reducedMotion: 'no-preference' });
await page.locator('#open-media-library').click();
await page.locator('#media-status').filter({ hasText: '2 Bilder' }).waitFor();
await page.locator(`[data-media-id="${mediaItems[0].id}"]`).click();
await page.getByRole('button', { name: 'Archivieren', exact: true }).click();
await page.locator('#media-status').filter({ hasText: '1 Bilder' }).waitFor();
assert.equal((await context.request.get(origin + mediaItems[0].variants[0].src)).status(), 200);
await page.getByLabel('Archivierte Bilder anzeigen', { exact: true }).check();
await page.locator(`[data-media-id="${mediaItems[0].id}"]`).click();
await page.getByRole('button', { name: 'Wiederherstellen', exact: true }).click();
await page.locator('#media-status').filter({ hasText: '2 Bilder' }).waitFor();
await page.getByRole('button', { name: 'Schließen', exact: true }).click();
await page.locator('#structure [data-block-id="headline"]').click();
await page.getByRole('button', { name: 'Inhalt', exact: true }).click();
checks.push({ name: 'image upload, alt metadata, multi-select gallery, caption, sorting undo, archive/restore, responsive variants and reduced-motion effects', passed: true });
console.log('Media library, gallery and effects passed.');
await page.locator('#library [data-add="text"]').click();
await page.getByLabel('Text', { exact: true }).fill('Dieser Absatz wurde im Editor eingefügt.');
await page.getByRole('button', { name: 'Duplizieren', exact: true }).click();
await page.getByLabel('Text', { exact: true }).fill('Das ist eine unabhängige Kopie.');
await page.getByRole('button', { name: 'Nach oben verschieben', exact: true }).click();
await page.getByRole('button', { name: 'Löschen', exact: true }).click();
await page.getByRole('button', { name: 'Rückgängig', exact: true }).click();
await page.locator('#toggle-library').click();
await page.locator(`#structure [data-toggle="${presetSection}"]`).click();
await page.locator(`#structure [data-toggle="${galleryId}"]`).click();
await page.getByRole('button', { name: 'Text: Dieser Absatz wurde im Editor eingefügt.', exact: true }).dragTo(page.locator('#structure [data-block-id="principles"]'));
await page.waitForFunction(() => document.querySelector('#structure [data-block-id="principles"]')?.parentElement.textContent.includes('Dieser Absatz wurde im Editor eingefügt.'));
await page.getByRole('button', { name: 'Rückgängig', exact: true }).click();
await page.locator('#toggle-library').click();
await page.route('**/admin/api/pages/test-home/draft', route => route.abort(), { times: 1 });
await page.getByRole('button', { name: 'Entwurf speichern', exact: true }).click();
await page.locator('#message').filter({ hasText: 'Verbindung ist unterbrochen' }).waitFor();
assert.equal(await page.getByRole('button', { name: 'Entwurf speichern', exact: true }).isEnabled(), true);
checks.push({ name: 'network failure retains edits and allows retry', passed: true });
await page.getByRole('button', { name: 'Entwurf speichern', exact: true }).click();
await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor();
const stored = await (await context.request.get(origin + '/admin/api/pages/test-home')).json();
console.log('Draft stored.');
assert.equal((await (await context.request.get(origin)).text()).includes('Meine bearbeitete Website'), false);
await page.reload();
await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor();
await frame.getByRole('heading', { name: 'Meine bearbeitete Website', exact: true }).waitFor();
const reloaded = await (await context.request.get(origin + '/admin/api/pages/test-home')).json();
assert.deepEqual(reloaded.document, stored.document);
await page.getByRole('button', { name: 'Veröffentlichen', exact: true }).click();
await page.getByRole('button', { name: 'Abbrechen', exact: true }).first().focus();
await page.keyboard.press('Escape');
assert.equal(await page.locator('#publish-dialog').isVisible(), false);
assert.equal(await page.locator('#publish').evaluate(element => element === document.activeElement), true);
await page.getByRole('button', { name: 'Veröffentlichen', exact: true }).click();
await page.getByRole('button', { name: 'Jetzt veröffentlichen', exact: true }).click();
await page.locator('#save-status').filter({ hasText: 'Veröffentlicht' }).waitFor();
assert.equal((await (await context.request.get(origin)).text()).includes('Meine bearbeitete Website'), true);
const publicPage = await context.newPage();
await publicPage.goto(origin);
assert.equal(await publicPage.locator(`#${galleryId} img`).count(), 2);
await publicPage.locator(`#${galleryId} img`).first().scrollIntoViewIfNeeded();
await publicPage.waitForFunction(id => document.querySelector(`#${id} img`)?.naturalWidth > 0, galleryId);
assert.equal(await publicPage.locator('#headline').evaluate(element => getComputedStyle(element).fontSize), '72px');
await publicPage.setViewportSize({ width: 768, height: 1000 });
assert.equal(await publicPage.locator('#headline').evaluate(element => getComputedStyle(element).fontSize), '52px');
await publicPage.setViewportSize({ width: 390, height: 844 });
assert.equal(await publicPage.locator('#headline').evaluate(element => getComputedStyle(element).fontSize), '36px');
assert.equal(await publicPage.locator('#hero').evaluate(element => getComputedStyle(element).paddingTop), '32px');
assert.equal(await publicPage.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
await publicPage.screenshot({ path: 'reports/editor/designed-public-mobile.png', fullPage: true });
const contentAudit = await new AxeBuilder({ page: publicPage }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'best-practice']).analyze();
assert.deepEqual(contentAudit.violations.map(v => v.id), [], JSON.stringify(contentAudit.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => ({ target: n.target, summary: n.failureSummary?.split('\n')[1] })) })), null, 1));
checks.push({ name: 'edited public renderer accessibility', violations: 0 });
await publicPage.close();
checks.push({ name: 'add, duplicate, move, delete, undo, exact reload, separate publication, dialog focus', passed: true });
console.log('Draft reload and publication passed.');
// A second server save must never be silently overwritten by the open editor.
const latest = await (await context.request.get(origin + '/admin/api/pages/test-home')).json();
const external = structuredClone(latest.document); external.title = 'Parallel gespeicherter Titel';
const externalSave = await context.request.post(origin + '/admin/api/pages/test-home/draft', { headers: { 'X-CSRF-Token': latest.csrfToken }, data: { document: external, expectedVersion: latest.version } });
assert.equal(externalSave.status(), 200);
await page.locator('#select-page').click();
await page.getByLabel('Seitentitel', { exact: true }).fill('Meine lokalen Änderungen');
await page.getByRole('button', { name: 'Entwurf speichern', exact: true }).click();
await page.locator('#message').filter({ hasText: 'neuerer Stand' }).waitFor();
console.log('Conflict displayed.');
assert.equal(await page.getByLabel('Seitentitel', { exact: true }).inputValue(), 'Meine lokalen Änderungen');
const download = page.waitForEvent('download');
console.log('Testing conflict export.');
await page.getByRole('button', { name: 'Änderungen als JSON sichern' }).click();
assert.equal((await download).suggestedFilename(), 'kant-entwurf.json');
await page.getByRole('button', { name: 'Serverstand laden', exact: true }).click();
await page.getByRole('button', { name: 'Serverstand übernehmen', exact: true }).click();
await page.getByLabel('Seitentitel', { exact: true }).filter({ visible: true }).waitFor();
await page.waitForFunction(() => document.querySelector('#property-title')?.value === 'Parallel gespeicherter Titel');
checks.push({ name: 'conflict retains local edits, JSON export, explicit server reload', passed: true });
await page.locator('#structure [data-block-id="headline"]').click();
await page.getByRole('button', { name: 'Design', exact: true }).click();
await page.locator('#preview-status').filter({ hasText: 'Zum Auswählen' }).waitFor();
await axe('desktop editor');
await page.screenshot({ path: 'reports/editor/editor-desktop.png', fullPage: true });
await page.getByRole('button', { name: 'Mobil', exact: true }).click();
assert.equal(await page.locator('#preview').evaluate(element => Math.round(element.getBoundingClientRect().width)), 390);
await page.screenshot({ path: 'reports/editor/editor-mobile-preview.png', fullPage: true });
await page.setViewportSize({ width: 390, height: 844 });
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
await axe('mobile editor');
await page.screenshot({ path: 'reports/editor/editor-phone.png', fullPage: true });
await page.locator('#open-media-library').click();
await page.locator('#media-status').filter({ hasText: '2 Bilder' }).waitFor();
await axe('mobile media library');
await page.screenshot({ path: 'reports/editor/media-phone.png', fullPage: true });
await page.getByRole('button', { name: 'Schließen', exact: true }).click();
assert.deepEqual(errors, []);
await writeFile('reports/editor/result.json', JSON.stringify({ checks, publicDataUntouched: true }, null, 2));
console.log('Editor end-to-end checks passed; isolated database, production data unchanged.');
if (process.argv.includes('--audit')) {
console.log('Auditing the published v3 document with responsive design, gallery images and effects.');
await browser.close(); browser = null;
await new Promise((resolve, reject) => {
const audit = spawn(process.execPath, ['scripts/audit.mjs'], { windowsHide: true, stdio: 'inherit', env: { ...process.env, CMS_URL: origin, CMS_AUDIT_DIR: 'reports/lighthouse-editor' } });
audit.on('error', reject);
audit.on('exit', code => code === 0 ? resolve() : reject(new Error(`Designed-page audit failed (${code}).`)));
});
}
} catch (error) {
console.error('Check failed:', error.message);
await activePage?.screenshot({ path: 'reports/editor/failure.png', fullPage: true, timeout: 5000 }).catch(() => {});
throw error;
} finally {
await browser?.close();
server.kill();
}