// Prüft die Symbolwahl am Button im echten Editor: Kachel wählen, Stellung umschalten, kein Symbol, // Tastatur, Rückgängig, Speichern und Neuladen - auf einer isolierten Kopie der Pilotdaten. 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/button-icons'; 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)); const selectBlock = async id => { await page.getByRole('tab', { name: 'Struktur', exact: true }).click(); await page.locator(`#structure [data-block-id="${id}"]`).click(); await page.locator(`#structure [data-edit-block="${id}"]`).click(); }; 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(); const startVersion = data.document.schemaVersion; data.document.blocks.unshift({ id: 'qa-icons', type: 'section', children: [ { id: 'qa-icon-button', type: 'button', props: { text: 'Jetzt anrufen', href: 'tel:+4972296992826' } }, ] }); assert.equal((await context.request.post(api + '/draft', { headers: { 'X-CSRF-Token': data.csrfToken }, data: { document: data.document, expectedVersion: data.version } })).status(), 200); // Was die Vorschau im Button zeigt: Reihenfolge von Text und Symbol und die Zeichnung. const preview = () => page.evaluate(() => { const el = document.querySelector('#preview')?.contentDocument?.getElementById('qa-icon-button'); if (!el) return null; return { first: el.firstElementChild?.className ?? null, text: el.textContent.trim(), path: el.querySelector('svg path')?.getAttribute('d') ?? null, firstNode: el.firstChild?.nodeType }; }); const waitPreview = async expected => page.waitForFunction(expected => { const el = document.querySelector('#preview')?.contentDocument?.getElementById('qa-icon-button'); if (!el) return false; const path = el.querySelector('svg path')?.getAttribute('d') ?? null; const iconFirst = el.firstChild?.nodeType === 1; return (expected.path === null ? path === null : path?.startsWith(expected.path)) && (expected.first === undefined || iconFirst === expected.first); }, expected); await page.goto(`${origin}/admin/pages/${fixture.homeId}`); await selectBlock('qa-icon-button'); const picker = page.locator('.icon-picker'); await picker.waitFor(); // Ohne Wahl: der bisherige Pfeil ist markiert und steht in der Vorschau hinter dem Text. assert.equal(await picker.locator('[aria-pressed="true"]').getAttribute('data-icon'), 'arrow-up-right'); await waitPreview({ path: 'M6.5 17.5', first: false }); assert.equal(await picker.locator('.icon-tile').count(), 25, 'Kein Symbol plus 24 Symbole in drei Gruppen'); assert.deepEqual(await picker.locator('.icon-group-title').allTextContents(), ['Pfeile', 'Kontakt', 'Allgemein']); assert.equal(await page.locator('#property-icon').count(), 0, 'Keine Auswahlliste mehr'); await page.locator('.properties-panel, #properties, aside').first().screenshot({ path: `${report}/vorher.png` }).catch(() => {}); checks.push('ohne Wahl: Pfeil ↗ markiert und hinter dem Text, Kacheln statt Liste'); await picker.getByRole('button', { name: 'Telefon', exact: true }).click(); await waitPreview({ path: 'M6 3h4', first: false }); assert.equal(await page.locator('.icon-tile[data-icon="phone"]').getAttribute('aria-pressed'), 'true'); assert.equal(await page.evaluate(() => document.activeElement?.dataset.icon), 'phone', 'Fokus bleibt auf der gewählten Kachel'); await page.getByRole('button', { name: 'Vor dem Text', exact: true }).click(); await waitPreview({ path: 'M6 3h4', first: true }); checks.push('Telefon gewählt, dann vor den Text gestellt - Vorschau folgt'); // Tastatur: Pfeiltasten wandern durch die Kacheln, Enter wählt. await page.locator('.icon-tile[data-icon="phone"]').focus(); await page.keyboard.press('ArrowRight'); assert.equal(await page.evaluate(() => document.activeElement?.dataset.icon), 'mail'); await page.keyboard.press('Enter'); await waitPreview({ path: 'M3 6h18', first: true }); checks.push('Pfeiltaste und Enter wählen E-Mail'); await page.locator('#undo').click(); await waitPreview({ path: 'M6 3h4', first: true }); await page.locator('#redo').click(); await waitPreview({ path: 'M3 6h18', first: true }); checks.push('Rückgängig und Wiederholen'); await page.getByRole('button', { name: 'Kein Symbol', exact: true }).click(); await waitPreview({ path: null }); assert.equal(await page.getByRole('button', { name: 'Vor dem Text', exact: true }).count(), 0, 'Ohne Symbol keine Stellung'); assert.equal((await preview()).text, 'Jetzt anrufen'); await page.locator('#undo').click(); await waitPreview({ path: 'M3 6h18', first: true }); await picker.getByRole('button', { name: 'Pfeil nach rechts', exact: true }).click(); await waitPreview({ path: 'M4 12h15.5', first: true }); await page.locator('.icon-picker').screenshot({ path: `${report}/auswahl.png` }); await page.locator('.icon-position').screenshot({ path: `${report}/stellung.png` }); checks.push('kein Symbol, dann Pfeil nach rechts'); await page.locator('#save').click(); await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor(); const stored = (await (await context.request.get(api)).json()).document; assert.equal(stored.schemaVersion, 25, `vorher v${startVersion}`); assert.deepEqual(stored.blocks[0].children[0].props, { text: 'Jetzt anrufen', href: 'tel:+4972296992826', icon: 'arrow-right', iconPosition: 'start' }); await page.reload(); await selectBlock('qa-icon-button'); await page.locator('.icon-picker').waitFor(); assert.equal(await page.locator('.icon-tile[aria-pressed="true"]').getAttribute('data-icon'), 'arrow-right'); assert.equal(await page.locator('.icon-position [aria-pressed="true"]').getAttribute('data-position'), 'start'); await waitPreview({ path: 'M4 12h15.5', first: true }); checks.push('gespeichert als Schema v25, nach Neuladen unverändert'); assert.deepEqual(errors, []); await writeFile(`${report}/report.json`, JSON.stringify({ origin, checks }, null, 2)); console.log(checks.map(check => `✓ ${check}`).join('\n')); } finally { await browser?.close(); server.kill(); }