- Image widths are 480, 720, 960, 1280, 1600 and 2400. A phone needed 721 pixels and got 960. Existing images receive the new steps from a task; stored pages pick them up at render time, so no document is rewritten. Mobile backgrounds use 720. - A variant served once is mirrored to public/media, and a rewrite rule lets the web server deliver it from there. var/media stays the source. - The hero preload follows the image each device really shows. - The post navigation reads the order and then two posts instead of up to 500 documents. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
185 lines
13 KiB
JavaScript
185 lines
13 KiB
JavaScript
// Prüft das Symbol am Button im echten Editor auf einer isolierten Kopie der Pilotdaten: Auswahl-
|
|
// fenster mit Kategorien, Tastatur, Stellung, Bewegung, nur Symbol, Farbe und Größe unter Design,
|
|
// Rückgängig, Speichern und Neuladen. Screenshots unter reports/button-icons/.
|
|
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, MEDIA_MIRROR_DIRECTORY: fixture.mediaDirectory + '-abbild' };
|
|
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();
|
|
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);
|
|
|
|
const inPreview = fn => page.evaluate(fn);
|
|
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('.button-icon svg path')?.getAttribute('d') ?? null;
|
|
const classes = el.className;
|
|
return (expected.path === undefined || (expected.path === null ? path === null : path?.startsWith(expected.path)))
|
|
&& (expected.first === undefined || ((el.firstChild?.nodeType === 1 && el.firstChild.classList.contains('button-icon')) === expected.first))
|
|
&& (expected.has ?? []).every(name => classes.includes(name))
|
|
&& (expected.not ?? []).every(name => !classes.includes(name));
|
|
}, expected);
|
|
|
|
await page.goto(`${origin}/admin/pages/${fixture.homeId}`);
|
|
await selectBlock('qa-icon-button');
|
|
const field = page.locator('.icon-field');
|
|
await field.waitFor();
|
|
assert.equal(await field.locator('.icon-field-name').textContent(), 'Pfeil schräg');
|
|
assert.equal(await page.locator('.icon-choice').count(), 0, 'Im Panel steht keine Kachelwand.');
|
|
await waitPreview({ path: 'M6.5 17.5', first: false });
|
|
await page.locator('.properties, #properties, aside').first().screenshot({ path: `${report}/panel-vorher.png` }).catch(() => {});
|
|
checks.push('Panel: nur das gewählte Symbol (Pfeil schräg) mit „Ändern …“');
|
|
|
|
// Das Fenster: Kategorien links, Symbole mit Namen, geöffnet bei der Kategorie des Pfeils.
|
|
await page.getByRole('button', { name: /Symbol ändern, aktuell: Pfeil schräg/ }).click();
|
|
const dialog = page.getByRole('dialog', { name: 'Symbol wählen' });
|
|
await dialog.waitFor();
|
|
assert.equal(await dialog.getByRole('tab').count(), 7);
|
|
assert.equal(await dialog.getByRole('tab', { selected: true }).textContent(), 'Pfeile');
|
|
assert.equal(await page.evaluate(() => document.activeElement?.dataset.icon), 'arrow-up-right', 'Fokus auf dem gewählten Symbol');
|
|
const axe = await new AxeBuilder({ page }).include('.icon-dialog').analyze();
|
|
assert.deepEqual(axe.violations.map(v => v.id), [], 'axe im Auswahlfenster');
|
|
await dialog.getByRole('tab', { name: 'Haus & Handwerk' }).click();
|
|
await dialog.screenshot({ path: `${report}/fenster.png` });
|
|
// Escape schließt ohne Änderung, der Fokus geht zurück zum Knopf.
|
|
await page.keyboard.press('Escape');
|
|
await dialog.waitFor({ state: 'detached' });
|
|
assert.match(await page.evaluate(() => document.activeElement?.getAttribute('aria-label') ?? ''), /Symbol ändern/);
|
|
await waitPreview({ path: 'M6.5 17.5' });
|
|
checks.push('Fenster: 7 Kategorien, Fokus auf dem Gewählten, axe ohne Befund, Escape ändert nichts');
|
|
|
|
// Tastatur: Kategorie mit Pfeiltaste wechseln, Symbol wählen.
|
|
await page.getByRole('button', { name: /Symbol ändern/ }).click();
|
|
await dialog.getByRole('tab', { name: 'Pfeile' }).focus();
|
|
await page.keyboard.press('ArrowDown');
|
|
assert.equal(await dialog.getByRole('tab', { selected: true }).textContent(), 'Kontakt & Ort');
|
|
await dialog.getByRole('button', { name: 'Telefon', exact: true }).click();
|
|
await dialog.waitFor({ state: 'detached' });
|
|
await waitPreview({ path: 'M6 3h4', first: false });
|
|
assert.equal(await field.locator('.icon-field-name').textContent(), 'Telefon');
|
|
checks.push('Telefon über die Kategorie „Kontakt & Ort“ gewählt');
|
|
|
|
await page.getByRole('button', { name: 'Vor dem Text', exact: true }).click();
|
|
await waitPreview({ first: true, has: ['icon-start'] });
|
|
await page.getByRole('button', { name: 'Rückt', exact: true }).click();
|
|
await waitPreview({ has: ['icon-motion-slide'] });
|
|
await page.getByRole('button', { name: 'Erscheint', exact: true }).click();
|
|
await waitPreview({ has: ['icon-motion-reveal'], not: ['icon-motion-slide'] });
|
|
await page.locator('#undo').click(); await waitPreview({ has: ['icon-motion-slide'] });
|
|
await page.locator('#redo').click(); await waitPreview({ has: ['icon-motion-reveal'] });
|
|
checks.push('Stellung und Bewegung, Rückgängig und Wiederholen');
|
|
|
|
await page.getByLabel('Nur das Symbol zeigen').check();
|
|
await waitPreview({ has: ['icon-only'] });
|
|
const label = await inPreview(() => document.querySelector('#preview').contentDocument.querySelector('#qa-icon-button .button-label')?.textContent);
|
|
assert.equal(label, 'Jetzt anrufen', 'Beschriftung bleibt für Vorlesesoftware');
|
|
await page.locator('.properties, #properties, aside').first().screenshot({ path: `${report}/panel-nachher.png` }).catch(() => {});
|
|
checks.push('Nur Symbol: Beschriftung unsichtbar im Link');
|
|
|
|
// Design: Gruppe „Symbol“ mit Farbe, Fläche, Größe, Drehung.
|
|
await page.getByRole('button', { name: 'Farbe, Größe und Fläche des Symbols → Design' }).click();
|
|
const group = page.locator('.design-group[data-group="icon"]');
|
|
await group.waitFor();
|
|
assert.equal(await group.evaluate(el => el.open), true);
|
|
for (const name of ['Symbolfarbe', 'Fläche hinter dem Symbol', 'Symbolgröße', 'Innenabstand der Fläche', 'Ecken der Fläche', 'Drehung', 'Abstand zum Text']) {
|
|
assert.ok(await group.getByText(name, { exact: true }).count() > 0, `${name} fehlt in der Gruppe Symbol`);
|
|
}
|
|
await group.locator('#style-iconSize').fill('24');
|
|
await group.locator('#style-iconSize').press('Enter');
|
|
await page.waitForFunction(() => {
|
|
const doc = document.querySelector('#preview')?.contentDocument, svg = doc?.querySelector('#qa-icon-button .button-icon svg');
|
|
return svg && Math.round(svg.getBoundingClientRect().width) === 24;
|
|
});
|
|
await group.screenshot({ path: `${report}/design-symbol.png` });
|
|
checks.push('Design → Symbol: alle sieben Werte da, Größe 24 px wirkt in der Vorschau');
|
|
|
|
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;
|
|
const button = stored.blocks[0].children[0];
|
|
assert.equal(stored.schemaVersion, 26);
|
|
assert.deepEqual(button.props, { text: 'Jetzt anrufen', href: 'tel:+4972296992826', icon: 'phone', iconPosition: 'start', iconMotion: 'reveal', iconOnly: true });
|
|
assert.equal(button.style.desktop.iconSize, '24px');
|
|
await page.reload(); await selectBlock('qa-icon-button');
|
|
await page.locator('.icon-field').waitFor();
|
|
assert.equal(await page.locator('.icon-field-name').textContent(), 'Telefon');
|
|
assert.equal(await page.getByLabel('Nur das Symbol zeigen').isChecked(), true);
|
|
checks.push('gespeichert als Schema v26 mit Größe unter Design, nach Neuladen gleich');
|
|
|
|
// So sieht es aus: Pfeil in einer runden Fläche, die sich beim Überfahren dreht und umfärbt,
|
|
// daneben ein Button nur aus Symbol.
|
|
const look = (await (await context.request.get(api)).json());
|
|
look.document.blocks[0].children = [
|
|
{ id: 'qa-look-a', type: 'button', props: { text: 'Projekt anfragen', href: '/kontakt', icon: 'arrow-right', iconMotion: 'slide' }, style: { desktop: { iconColor: '#ffffff', iconBackgroundColor: '#b71c22', iconPadding: '6px', iconRadius: '999px', iconSize: '16px', hover: { iconBackgroundColor: '#1e2a20', iconRotate: '-45deg' } } } },
|
|
{ id: 'qa-look-b', type: 'button', props: { text: 'Anrufen', href: 'tel:+4972296992826', icon: 'phone', iconOnly: true } },
|
|
{ id: 'qa-look-c', type: 'button', props: { text: 'Prospekt laden', href: '/', icon: 'download', iconPosition: 'start' } },
|
|
];
|
|
assert.equal((await context.request.post(api + '/draft', { headers: { 'X-CSRF-Token': look.csrfToken }, data: { document: look.document, expectedVersion: look.version } })).status(), 200);
|
|
await page.reload();
|
|
const frame = page.frameLocator('#preview');
|
|
await frame.locator('#qa-look-a .button-icon').waitFor();
|
|
const section = frame.locator('#qa-icons');
|
|
await section.screenshot({ path: `${report}/aussehen-normal.png` });
|
|
const styles = await frame.locator('#qa-look-a .button-icon').evaluate(el => { const s = getComputedStyle(el); return { bg: s.backgroundColor, radius: s.borderRadius, color: s.color }; });
|
|
assert.deepEqual(styles, { bg: 'rgb(183, 28, 34)', radius: '999px', color: 'rgb(255, 255, 255)' });
|
|
await frame.locator('#qa-look-a').hover();
|
|
await page.waitForTimeout(400);
|
|
await section.screenshot({ path: `${report}/aussehen-hover.png` });
|
|
assert.equal(await frame.locator('#qa-look-a .button-icon').evaluate(el => getComputedStyle(el).rotate), '-45deg');
|
|
const only = await frame.locator('#qa-look-b').evaluate(el => ({ name: el.textContent.trim(), w: Math.round(el.getBoundingClientRect().width), h: Math.round(el.getBoundingClientRect().height) }));
|
|
assert.equal(only.name, 'Anrufen');
|
|
assert.ok(Math.abs(only.w - only.h) <= 2, `Nur-Symbol-Button ist nicht quadratisch: ${only.w}x${only.h}`);
|
|
checks.push('Aussehen: farbige runde Fläche, Drehung beim Überfahren, quadratischer Nur-Symbol-Button, Symbol vor dem Text');
|
|
await selectBlock('qa-look-c');
|
|
|
|
// Kein Symbol: Nur-Symbol fällt weg, die Einstellungen verschwinden.
|
|
await page.getByRole('button', { name: /Symbol ändern/ }).click();
|
|
await dialog.getByRole('button', { name: 'Kein Symbol', exact: true }).click();
|
|
await page.waitForFunction(() => { const el = document.querySelector('#preview')?.contentDocument?.getElementById('qa-look-c'); return el && !el.querySelector('.button-icon'); });
|
|
assert.equal(await page.getByRole('button', { name: 'Vor dem Text', exact: true }).count(), 0);
|
|
checks.push('Kein Symbol entfernt auch „nur Symbol“');
|
|
|
|
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();
|
|
}
|