Files
KantSites/scripts/accordion-check.mjs
T
kantarci a3029966cb
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
fix: expose accordion contents and add clear insert actions (1.8.3)
2026-09-14 12:53:16 +02:00

137 lines
10 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.
import { chromium } from 'playwright';
import AxeBuilder from '@axe-core/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/accordion'; 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.unshift({ id: 'qa-accordion', type: 'accordion', children: [
{ id: 'qa-entry', type: 'accordionitem', props: { title: 'Was ist enthalten?', open: false }, children: [] },
{ id: 'qa-open', type: 'accordionitem', props: { title: 'Schon geöffnet', open: true }, children: [{ id: 'qa-existing', type: 'text', props: { text: '<p>Vorhandener Inhalt bleibt.</p>' } }] },
] }, { id: 'qa-empty-accordion', type: 'accordion', children: [] });
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}`);
const frame = page.frameLocator('#preview');
const ready = async () => page.locator('#preview-status').filter({ hasText: 'Entwurf ·' }).waitFor();
const select = async id => { await page.locator(`#structure [data-block-id="${id}"]`).click(); await page.locator('[data-panel="content"]').click(); };
await ready();
await frame.locator('#qa-entry > summary').click();
assert.equal(await frame.locator('#qa-entry').evaluate(el => el.open), true);
assert.equal(await page.getByLabel('Beim Laden geöffnet', { exact: true }).isChecked(), false);
assert.equal(await page.locator('#save').isDisabled(), true, 'Revealing the editor body must not change the document');
await frame.locator('#qa-entry [data-editor-insert]').focus();
await frame.locator('#qa-entry [data-editor-insert]').press('Enter');
assert.match(await page.locator('#insert-hint').innerText(), /Was ist enthalten/);
await page.locator('#library [data-add="text"]').click();
await ready(); await frame.locator('#qa-entry .block-text').waitFor();
const textId = await frame.locator('#qa-entry .block-text').getAttribute('id');
await page.getByLabel('Text', { exact: true }).fill('Text direkt im Akkordeon.'); await ready();
assert.equal(await frame.locator('#qa-entry .block-text').innerText(), 'Text direkt im Akkordeon.');
await page.locator('#undo').click(); await page.locator('#undo').click(); await ready();
assert.equal(await frame.locator(`#${textId}`).count(), 0);
await page.locator('#redo').click(); await page.locator('#redo').click(); await ready();
assert.equal(await frame.locator(`#qa-entry #${textId}`).count(), 1);
checks.push('closed entries reveal editable bodies and empty insert areas without changing public state; text insertion, editing and undo/redo');
await select('qa-accordion');
assert.equal(await page.locator('#library [data-add="text"]').isDisabled(), true, 'Group selection must not put text outside the accordion');
await page.getByRole('button', { name: 'Eintrag 1: Was ist enthalten? bearbeiten', exact: true }).click();
await page.getByRole('button', { name: '+ Inhalt hinzufügen', exact: true }).click();
await page.locator('#library [data-add="button"]').click(); await ready();
const firstButtonId = await frame.locator('#qa-entry a.button').getAttribute('id');
assert.equal(await frame.locator('#qa-entry .accordion-body > [id]').last().getAttribute('id'), firstButtonId, 'Add content appends after existing content');
await select('qa-open');
await page.locator('#library [data-add="button"]').dragTo(frame.locator('#qa-open .accordion-body'), { targetPosition: { x: 40, y: 20 } });
await ready(); await frame.locator('#qa-open a.button').waitFor();
const draggedId = await frame.locator('#qa-open a.button').getAttribute('id');
await page.locator('#undo').click(); await ready(); assert.equal(await frame.locator(`#${draggedId}`).count(), 0);
await page.locator('#redo').click(); await ready(); assert.equal(await frame.locator(`#qa-open #${draggedId}`).count(), 1);
checks.push('entry selection in inspector, append content and drag/drop into an open body with undo/redo');
await frame.locator('#qa-accordion > .editor-add-content [data-editor-add-entry]').click();
await page.getByLabel('Titel', { exact: true }).fill('Ein neuer Eintrag'); await ready();
const newEntryId = await frame.locator('#qa-accordion > details').last().getAttribute('id');
assert.equal(await frame.locator('#qa-accordion > details').count(), 3);
assert.equal(await frame.locator(`#${newEntryId}`).evaluate(el => el.open), true);
await select('qa-empty-accordion');
await page.getByRole('button', { name: '+ Eintrag hinzufügen', exact: true }).click(); await ready();
assert.equal(await frame.locator('#qa-empty-accordion > details').count(), 1);
await page.getByLabel('Titel', { exact: true }).fill('Erster Eintrag'); await ready();
await page.locator('#tree-search').fill('Ein neuer Eintrag');
assert.equal(await page.locator(`#structure [data-block-id="${newEntryId}"]`).count(), 1);
await page.locator('#tree-search').fill('');
await select('qa-entry'); await page.locator('[data-panel="design"]').click();
await page.locator('#library [data-add="accordion"]').click(); await ready();
assert.equal(await page.locator('#properties-title').innerText(), 'Akkordeon-Eintrag');
assert.equal(await page.getByLabel('Titel', { exact: true }).inputValue(), 'Eine Frage');
await page.locator('#undo').click(); await ready();
checks.push('add entries in canvas and inspector, recover empty accordion, search by title, new accordion starts with its editable entry selected');
await page.locator('#save').click(); await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor();
await page.reload(); await ready(); await select('qa-entry');
assert.equal(await page.getByLabel('Beim Laden geöffnet', { exact: true }).isChecked(), false);
assert.equal(await frame.locator('#qa-entry').evaluate(el => el.open), true);
const saved = await (await context.request.get(api)).json();
const accordion = saved.document.blocks.find(b => b.id === 'qa-accordion');
assert.equal(accordion.children[0].props.open, false); assert.equal(accordion.children[1].props.open, true);
assert.deepEqual(accordion.children[0].children.map(b => b.id), [textId, firstButtonId]);
assert.ok(!JSON.stringify(saved.document).includes('editor-empty'));
await page.locator('#publish').click(); await page.locator('#confirm-publish').click();
await page.locator('#save-status').filter({ hasText: 'Veröffentlicht' }).waitFor();
const plain = await browser.newContext({ javaScriptEnabled: false });
const visitor = await plain.newPage(); await visitor.goto(origin);
assert.equal(await visitor.locator('#qa-entry').evaluate(el => el.open), false);
assert.equal(await visitor.locator('#qa-open').evaluate(el => el.open), true);
assert.equal(await visitor.locator('[data-editor-insert],[data-editor-add-entry],.editor-empty').count(), 0);
await visitor.locator('#qa-entry > summary').click();
assert.equal(await visitor.locator('#qa-entry').evaluate(el => el.open), true);
assert.equal(await visitor.locator(`#qa-entry #${textId}`).isVisible(), true);
await visitor.locator('#qa-entry > summary').press('Enter');
assert.equal(await visitor.locator('#qa-entry').evaluate(el => el.open), false);
await plain.close();
checks.push('save/reload/publication keep children and original open flags; published accordion toggles without JavaScript and contains no editing aids');
for (const width of [1600, 390]) {
await page.setViewportSize({ width, height: 1050 }); await select('qa-entry'); await ready();
await page.locator('[data-viewport="mobile"]').click(); await ready();
await frame.locator('#qa-entry').scrollIntoViewIfNeeded();
const audit = await new AxeBuilder({ page }).exclude('#preview').analyze();
assert.deepEqual(audit.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) })), []);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
await page.screenshot({ path: `${report}/editor-${width}.png`, fullPage: true });
}
checks.push('desktop/mobile editor passes axe and layout checks with editable accordion preview');
assert.deepEqual(errors, []);
await writeFile(`${report}/result.json`, JSON.stringify({ checks, passed: true }, null, 2));
console.log('Accordion 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(); }