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/buttons'; await mkdir(report, { recursive: true }); let browser, page; const checks = [], errors = []; const buttonStyle = node => { const style = getComputedStyle(node); return Object.fromEntries(['backgroundColor', 'color', 'borderRadius', 'borderColor', 'borderWidth', 'borderStyle', 'paddingTop', 'paddingRight', 'fontFamily', 'fontSize', 'fontWeight', 'letterSpacing', 'textTransform', 'lineHeight', 'textAlign'].map(key => [key, style[key]])); }; 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-buttons-section', type: 'section', children: [ { id: 'qa-button-default', type: 'button', props: { text: 'Kontakt aufnehmen', href: '#kontakt' } }, { id: 'qa-button', type: 'button', props: { text: 'Mehr erfahren', href: '/kontakt' }, style: { desktop: { margin: { left: '12px' }, fontSize: '17px' } } } ] }); 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/theme'); await page.locator('#buttons-fontSize').fill('18'); await page.getByRole('button', { name: 'Standardbutton Rahmen · Pille', exact: true }).click(); assert.equal(await page.locator('#buttons-shape').inputValue(), 'pill'); assert.equal(await page.locator('#buttons-appearance').inputValue(), 'outline'); assert.equal(await page.locator('#button-defaults .button-state .cms-button').first().evaluate(el => getComputedStyle(el).borderRadius), '999px'); await page.locator('#button-defaults').screenshot({ path: `${report}/theme.png` }); await page.getByRole('button', { name: 'Design speichern', exact: true }).click(); assert.equal(await page.locator('#buttons-shape').inputValue(), 'pill'); assert.equal(await page.locator('#buttons-fontSize').inputValue(), '18'); checks.push('global typography, visual presets, normal/hover preview and actual form save including empty palette rows'); await page.goto(`${origin}/admin/pages/${fixture.homeId}`); await page.locator('#structure [data-block-id="qa-button"]').click(); await page.getByRole('button', { name: 'Button-Vorlage Abgerundet', exact: true }).click(); const waitVariant = async (id, radius, background) => page.waitForFunction(({id,radius,background}) => { const doc = document.querySelector('#preview')?.contentDocument, el = doc?.getElementById(id); if (!el) return false; const s = doc.defaultView.getComputedStyle(el); return s.borderRadius === radius && (background === undefined || (s.backgroundColor === 'rgba(0, 0, 0, 0)') === background); }, { id, radius, background }); await waitVariant('qa-button', '8px', false); await page.locator('#undo').click(); await waitVariant('qa-button', '999px', true); await page.locator('#redo').click(); await waitVariant('qa-button', '8px', false); await page.locator('[data-viewport="tablet"]').click(); await page.getByRole('button', { name: 'Button-Vorlage Rahmen · eckig', exact: true }).click(); await waitVariant('qa-button', '0px', true); await page.locator('[data-viewport="mobile"]').click(); await page.getByRole('button', { name: 'Button-Vorlage Pille', exact: true }).click(); await waitVariant('qa-button', '999px', false); await page.locator('#button-reset').click(); await waitVariant('qa-button', '0px', true); await page.locator('#undo').click(); await waitVariant('qa-button', '999px', false); await page.locator('[data-viewport="desktop"]').click(); await waitVariant('qa-button', '8px', false); await page.locator('.button-designer').screenshot({ path: `${report}/editor-controls.png` }); await page.locator('#save').click(); await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor(); await page.reload(); await page.locator('#structure [data-block-id="qa-button"]').click(); await waitVariant('qa-button', '8px', false); const stored = (await (await context.request.get(api)).json()).document; assert.equal(stored.schemaVersion, 21); assert.deepEqual(stored.blocks[0].children[1].props, { text: 'Mehr erfahren', href: '/kontakt' }); assert.equal(stored.blocks[0].children[1].style.desktop.margin.left, '12px'); assert.equal(stored.blocks[0].children[1].style.desktop.fontSize, '17px'); checks.push('preset selection, device inheritance/reset, undo/redo, save/reload, preserved content and unrelated styling'); 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 publicPage = await plain.newPage(); for (const [width, radius, transparent] of [[1600, '8px', false], [900, '0px', true], [390, '999px', false]]) { await publicPage.setViewportSize({ width, height: 900 }); await publicPage.goto(origin); const necessaryCookies = publicPage.getByRole('button', { name: 'Nur notwendige', exact: true }); if (await necessaryCookies.count()) await necessaryCookies.click(); const values = await publicPage.locator('#qa-button').evaluate(el => { const s=getComputedStyle(el);return { radius:s.borderRadius, bg:s.backgroundColor, font:s.fontSize }; }); assert.equal(values.radius, radius); assert.equal(values.bg === 'rgba(0, 0, 0, 0)', transparent); assert.equal(values.font, '17px'); assert.equal(await publicPage.locator('#qa-button-default').evaluate(el => getComputedStyle(el).fontSize), '18px'); const submit = publicPage.locator('.block-form button[type=submit]').first(); const standard = publicPage.locator('#qa-button-default'); assert.deepEqual(await submit.evaluate(buttonStyle), await standard.evaluate(buttonStyle), 'Form submit uses global shape, appearance, size, typography and colors'); assert.ok(await submit.evaluate(node => node.getBoundingClientRect().width < node.form.getBoundingClientRect().width), 'Submit keeps natural button width'); await submit.hover(); await publicPage.waitForTimeout(350); const hoverStyle = await submit.evaluate(buttonStyle); await standard.hover(); await publicPage.waitForTimeout(350); assert.deepEqual(hoverStyle, await standard.evaluate(buttonStyle), 'Submit inherits the global hover colors'); await submit.focus(); assert.equal(await submit.evaluate(node => getComputedStyle(node).outlineStyle), 'solid', 'Submit has a keyboard focus indicator'); await submit.screenshot({ path: `${report}/submit-${width}.png` }); await publicPage.locator('#qa-buttons-section').screenshot({ path: `${report}/public-${width}.png` }); } await page.goto(origin + '/admin/theme'); await page.locator('#buttons-shape').selectOption('rounded'); await page.locator('#buttons-appearance').selectOption('solid'); await page.locator('#buttons-size').selectOption('large'); await page.locator('#buttons-fontSize').fill('20'); for (const [key, hex] of [['backgroundColor', '#124b36'], ['color', '#ffffff'], ['hoverBackgroundColor', '#092c20'], ['hoverColor', '#fff4cc']]) { await page.locator(`#buttons-${key}`).click(); await page.locator(`#buttons-${key}-hex`).fill(hex); await page.locator(`#buttons-${key}-hex`).press('Enter'); await page.keyboard.press('Escape'); } await page.getByRole('button', { name: 'Design speichern', exact: true }).click(); await publicPage.goto(origin); const updatedSubmit = publicPage.locator('.block-form button[type=submit]').first(); const updatedDefault = publicPage.locator('#qa-button-default'); assert.deepEqual(await updatedSubmit.evaluate(buttonStyle), await updatedDefault.evaluate(buttonStyle), 'A later change to solid global defaults updates existing forms'); assert.equal(await updatedSubmit.evaluate(el => getComputedStyle(el).backgroundColor), 'rgb(18, 75, 54)'); await updatedSubmit.hover(); await publicPage.waitForTimeout(350); const updatedHover = await updatedSubmit.evaluate(buttonStyle); await updatedDefault.hover(); await publicPage.waitForTimeout(350); assert.deepEqual(updatedHover, await updatedDefault.evaluate(buttonStyle), 'Custom global hover colors update existing forms'); await publicPage.setViewportSize({ width: 390, height: 1600 }); await updatedSubmit.scrollIntoViewIfNeeded(); await publicPage.locator('.block-form').first().screenshot({ path: `${report}/form-standard-button.png` }); await plain.close(); checks.push('published global defaults, form submit including hover/focus/natural width, solid and outline theme changes, and local responsive variants without JavaScript'); assert.deepEqual(errors, []); await writeFile(`${report}/result.json`, JSON.stringify({ checks, passed: true }, null, 2)); console.log('Button 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(); }