Add visual button presets, jump targets and menu controls
This commit is contained in:
1 parent
bb4e422af8
commit
3391fea58b
41 files changed
+723
-57
No files matched your search
@@ -0,0 +1,5 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
const file = new URL('../public/assets/buttons.css', import.meta.url);
|
||||
const source = (await readFile(file, 'utf8')).split('/* Responsive variants */')[0].trimEnd();
|
||||
const rules = source.split('\n').filter(line => line.includes('b-desktop-')).join('\n');
|
||||
await writeFile(file, `${source}\n/* Responsive variants */\n@media(max-width:1024px){\n${rules.replaceAll('b-desktop-', 'b-tablet-')}\n}\n@media(max-width:767px){\n${rules.replaceAll('b-desktop-', 'b-mobile-')}\n}\n`);
|
||||
@@ -0,0 +1,98 @@
|
||||
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 = [];
|
||||
|
||||
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 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');
|
||||
await publicPage.locator('#qa-buttons-section').screenshot({ path: `${report}/public-${width}.png` });
|
||||
}
|
||||
await plain.close(); checks.push('published global defaults 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(); }
|
||||
@@ -0,0 +1,92 @@
|
||||
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/navigation-ux'; 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.push({ id: 'qa-anchor', type: 'section', children: [{ id: 'qa-anchor-title', type: 'heading', props: { text: 'Kontaktbereich', level: 2 } }] });
|
||||
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}`);
|
||||
await page.locator('#structure [data-block-id="qa-anchor"]').click();
|
||||
await page.locator('#property-anchor').fill('#kontakt-check'); await page.locator('#property-anchor').blur();
|
||||
await page.locator('#save').click(); await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor();
|
||||
await page.locator('#publish').click(); await page.locator('#confirm-publish').click(); await page.locator('#save-status').filter({ hasText: 'Veröffentlicht' }).waitFor();
|
||||
const stored = (await (await context.request.get(api)).json()).document;
|
||||
assert.equal(stored.blocks.at(-1).anchor, 'kontakt-check'); assert.equal(stored.schemaVersion, 21);
|
||||
checks.push('named jump target created in editor and saved/published with stable block ID');
|
||||
|
||||
await page.goto(origin + '/admin/menus');
|
||||
const rows = page.locator('#menu-items > li');
|
||||
const originalFirst = await rows.first().locator('[data-key="label"]').inputValue();
|
||||
const handle = rows.first().locator('[data-menu-drag]');
|
||||
const start = await handle.boundingBox(), end = await rows.last().boundingBox();
|
||||
await page.mouse.move(start.x + start.width / 2, start.y + start.height / 2); await page.mouse.down();
|
||||
await page.mouse.move(start.x + start.width / 2, end.y + end.height - 2, { steps: 12 }); await page.mouse.up();
|
||||
assert.equal(await rows.last().locator('[data-key="label"]').inputValue(), originalFirst);
|
||||
await page.locator('#menu-undo').click(); assert.equal(await rows.first().locator('[data-key="label"]').inputValue(), originalFirst);
|
||||
await rows.first().locator('[data-menu-drag]').focus(); await page.keyboard.press('ArrowDown');
|
||||
assert.notEqual(await rows.first().locator('[data-key="label"]').inputValue(), originalFirst);
|
||||
await page.locator('#menu-undo').click();
|
||||
await rows.first().locator('summary').click();
|
||||
await rows.first().locator('[data-key="icon"]').selectOption('phone');
|
||||
await rows.first().locator('[data-key="iconPosition"]').selectOption('after');
|
||||
assert.equal(await rows.first().locator('.menu-entry-title > svg').count(), 1);
|
||||
await page.getByText('Eigener Link', { exact: true }).first().click();
|
||||
await page.locator('#link-label').fill('Zum Kontaktbereich'); await page.locator('#link-url').fill('#kontakt-check');
|
||||
await page.locator('#menu-add-url').click();
|
||||
await page.getByRole('button', { name: 'Menü speichern', exact: true }).click();
|
||||
await page.locator('#menu-status').filter({ hasText: 'Gespeicherter Stand' }).waitFor();
|
||||
await page.reload();
|
||||
assert.equal(await rows.first().locator('[data-key="icon"]').inputValue(), 'phone');
|
||||
assert.equal(await rows.first().locator('[data-key="iconPosition"]').inputValue(), 'after');
|
||||
await rows.first().locator('summary').click();
|
||||
await page.screenshot({ path: `${report}/menus.png`, fullPage: true });
|
||||
checks.push('pointer drag, keyboard ordering, undo, icon placement, fragment menu link and save/reload');
|
||||
const plain = await browser.newContext({ javaScriptEnabled: false }); const publicPage = await plain.newPage();
|
||||
await publicPage.goto(origin);
|
||||
assert.ok(await publicPage.locator('.nav-label > svg').count() > 0);
|
||||
await publicPage.getByRole('link', { name: 'Zum Kontaktbereich', exact: true }).first().click();
|
||||
assert.ok(publicPage.url().endsWith('#kontakt-check'));
|
||||
assert.equal(await publicPage.locator('#kontakt-check').count(), 1);
|
||||
assert.ok(await publicPage.evaluate(() => window.scrollY) > 0);
|
||||
await plain.close();
|
||||
execFileSync(php, ['-r', 'require "vendor/autoload.php"; $db=App\\Infrastructure\\ConnectionFactory::create($argv[1]); $db->executeStatement("UPDATE scheduled_task SET last_status = ?, last_message = ? WHERE name = ?", ["ok", "Fassung 99.0.0 ist verfügbar; installiert ist 1.8.4.", "update-check"]);', fixture.databaseUrl], { windowsHide: true });
|
||||
await page.reload();
|
||||
assert.equal(await page.getByRole('link', { name: 'Update 99.0.0 verfügbar. Aktualisierung öffnen.', exact: true }).count(), 1);
|
||||
assert.match(await page.locator('.version-update').getAttribute('href'), /system\?pruefen=1$/);
|
||||
await page.locator('.shell-foot').screenshot({ path: `${report}/update-hint.png` });
|
||||
checks.push('public icon and native anchor navigation without JavaScript; cached update notice by version');
|
||||
assert.deepEqual(errors, []);
|
||||
await writeFile(`${report}/result.json`, JSON.stringify({ checks, passed: true }, null, 2));
|
||||
console.log('Navigation 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(); }
|
||||
Reference in new issue
Block a user