Schema v25: a button takes an optional icon (any name from the shared set or "none") and an icon position (start or end). Without a choice it keeps the diagonal arrow behind the text, still with the button-arrow class, so stored pages and their own CSS look exactly as before. Thirteen everyday icons join the set (arrow down, chevron, calendar, message, send, download, document, play, search, heart, info, plus, person). Button, icon and icon card choose from a tile picker with a drawing per icon, grouped, keyboard navigable, instead of a select with English keys. icons.js holds the paths for the editor; a test keeps them equal to the Twig drawings and the validator list. ADR 108. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
8.0 KiB
JavaScript
123 lines
8.0 KiB
JavaScript
// 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();
|
|
}
|