146 lines
12 KiB
JavaScript
146 lines
12 KiB
JavaScript
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';
|
|
import { mediaProps, upgradeDocument } from '../public/assets/editor-model.js';
|
|
|
|
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/image-alignment'; await mkdir(report, { recursive: true });
|
|
let browser, page;
|
|
const checks = [], errors = [];
|
|
|
|
// Geometry of the visible image as well as its figure: centering an empty, full-width
|
|
// figure alone must not pass while the logo inside it stays at the left edge.
|
|
const geometry = figure => {
|
|
const img = figure.querySelector('img');
|
|
const parent = figure.parentElement, style = getComputedStyle(parent);
|
|
const p = parent.getBoundingClientRect(), f = figure.getBoundingClientRect(), i = img.getBoundingClientRect();
|
|
const left = p.left + parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft);
|
|
const width = parent.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
|
const top = p.top + parseFloat(style.borderTopWidth) + parseFloat(style.paddingTop);
|
|
const height = parent.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
|
|
return { left: f.left - left, right: left + width - f.right, width: f.width, available: width,
|
|
center: i.left + i.width / 2 - left - width / 2, innerCenter: i.left + i.width / 2 - f.left - f.width / 2,
|
|
verticalCenter: f.top + f.height / 2 - top - height / 2, imageWidth: i.width, imageHeight: i.height,
|
|
align: getComputedStyle(figure).alignSelf };
|
|
};
|
|
const closeTo = (actual, expected, message) => assert.ok(Math.abs(actual - expected) < 2, `${message}: ${actual} vs ${expected}`);
|
|
|
|
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();
|
|
upgradeDocument(data.document, 20);
|
|
const media = (await (await context.request.get(origin + '/admin/api/media')).json()).items;
|
|
const chosen = media.find(item => /teamviewer/i.test(item.title)); assert.ok(chosen);
|
|
const original = mediaProps(chosen); original.alt ||= chosen.title;
|
|
const small = { ...original, width: 180, height: Math.round(180 * original.height / original.width) };
|
|
const image = (id, style, props = original) => ({ id, type: 'image', props: structuredClone(props), style });
|
|
const centered = { desktop: { width: '40%', alignSelf: 'center' } };
|
|
const container = (id, style, child) => ({ id, type: 'column', style, children: [child] });
|
|
const section = { id: 'qa-alignment-section', type: 'section', layout: { width: 'full', tone: 'paper' }, style: { desktop: { maxWidth: '800px', padding: { left: '20px', right: '20px' } } }, children: [
|
|
container('qa-flow', { desktop: { padding: { left: '18px', right: '18px' } } }, image('qa-center', { desktop: { width: '50%' } })),
|
|
image('qa-natural', { desktop: { alignSelf: 'center' } }, small),
|
|
image('qa-linked', { desktop: { width: '60%', alignSelf: 'center', margin: { left: '11px', right: '7px' } }, tablet: { alignSelf: 'flex-end' }, mobile: { alignSelf: 'auto' } }, { ...small, click: 'link', href: '/kontakt', newTab: true }),
|
|
container('qa-grid', { desktop: { display: 'grid', gridColumns: '1', height: '160px' } }, image('qa-grid-image', centered)),
|
|
container('qa-flex-column', { desktop: { display: 'flex', flexDirection: 'column', height: '160px' } }, image('qa-column-image', centered)),
|
|
container('qa-flex-row', { desktop: { display: 'flex', height: '160px' } }, image('qa-row-image', centered)),
|
|
container('qa-responsive-parent', { desktop: { display: 'flex', height: '160px' }, tablet: { flexDirection: 'column' }, mobile: { display: 'block' } }, image('qa-responsive-image', centered)),
|
|
image('qa-zoom', { desktop: { maxWidth: '300px', alignSelf: 'center' } }, { ...small, click: 'vergroessern' }),
|
|
] };
|
|
data.document.blocks.unshift(section);
|
|
const created = await context.request.post(api + '/draft', { headers: { 'X-CSRF-Token': data.csrfToken }, data: { document: data.document, expectedVersion: data.version } });
|
|
assert.equal(created.status(), 200, await created.text());
|
|
await page.goto(`${origin}/admin/pages/${fixture.homeId}`);
|
|
const select = async () => {
|
|
await page.locator('#structure [data-block-id="qa-center"]').click(); await page.locator('[data-panel="design"]').click();
|
|
const group = page.locator('.design-group').filter({ has: page.locator('summary').filter({ hasText: /^Layout$/ }) });
|
|
if (await group.getAttribute('open') === null) await group.locator('summary').click();
|
|
};
|
|
const preview = () => page.frameLocator('#preview');
|
|
const aligned = async value => {
|
|
await page.waitForFunction(value => {
|
|
const doc = document.querySelector('#preview')?.contentDocument, figure = doc?.getElementById('qa-center');
|
|
return figure && doc.defaultView.getComputedStyle(figure).alignSelf === value;
|
|
}, value);
|
|
return preview().locator('#qa-center').evaluate(geometry);
|
|
};
|
|
await select(); closeTo((await aligned('auto')).left, 0, 'Initial left edge');
|
|
await page.locator('#style-alignSelf').selectOption('center');
|
|
closeTo((await aligned('center')).center, 0, 'Center in normal column');
|
|
await page.locator('#undo').click(); closeTo((await aligned('auto')).left, 0, 'Undo centering');
|
|
await page.locator('#redo').click(); closeTo((await aligned('center')).center, 0, 'Redo centering');
|
|
await page.locator('#style-alignSelf').focus(); await page.locator('#style-alignSelf').press('ArrowDown');
|
|
closeTo((await aligned('flex-end')).right, 0, 'Keyboard end alignment');
|
|
await page.locator('#style-alignSelf').selectOption('flex-start'); closeTo((await aligned('flex-start')).left, 0, 'Start alignment');
|
|
await page.locator('#style-alignSelf').selectOption('center'); await aligned('center');
|
|
checks.push('actual Design control: center, start, end, keyboard, undo and redo in a normal padded column');
|
|
|
|
const checkLayout = async (root, device) => {
|
|
for (const id of ['qa-center', 'qa-natural', 'qa-grid-image', 'qa-column-image', 'qa-zoom']) {
|
|
closeTo((await root.locator(`#${id}`).evaluate(geometry)).center, 0, `${device} ${id} centered`);
|
|
}
|
|
const normal = await root.locator('#qa-center').evaluate(geometry);
|
|
closeTo(normal.width / normal.available * 100, 50, 'Percentage width preserved');
|
|
const natural = await root.locator('#qa-natural').evaluate(geometry);
|
|
closeTo(natural.imageWidth, 180, 'Natural logo is not stretched');
|
|
const row = await root.locator('#qa-row-image').evaluate(geometry);
|
|
closeTo(row.left, 0, 'Flex row keeps its main-axis position'); closeTo(row.verticalCenter, 0, 'Flex row retains vertical centering');
|
|
const changing = await root.locator('#qa-responsive-image').evaluate(geometry);
|
|
closeTo(device === 'desktop' ? changing.left : changing.center, 0, 'Responsive parent layout');
|
|
const linked = await root.locator('#qa-linked').evaluate(geometry);
|
|
if (device === 'desktop') closeTo(linked.center, 0, 'Linked image centered');
|
|
if (device === 'tablet') { closeTo(linked.right, 0, 'Tablet figure at end'); const img = await root.locator('#qa-linked img').boundingBox(); const fig = await root.locator('#qa-linked').boundingBox(); closeTo(img.x + img.width, fig.x + fig.width, 'Small linked image at end'); }
|
|
if (device === 'mobile') { closeTo(linked.left, 11, 'Auto restores authored margin'); closeTo(linked.innerCenter, -(linked.width - linked.imageWidth) / 2, 'Auto clears inherited internal alignment'); }
|
|
assert.equal(await root.locator('#qa-linked a').getAttribute('target'), '_blank');
|
|
closeTo(normal.imageWidth / normal.imageHeight, original.width / original.height, 'Aspect ratio preserved');
|
|
};
|
|
for (const device of ['desktop', 'tablet', 'mobile']) {
|
|
await page.locator(`[data-viewport="${device}"]`).click(); await aligned('center');
|
|
await checkLayout(preview(), device);
|
|
}
|
|
await page.locator('[data-viewport="desktop"]').click(); await aligned('center');
|
|
await preview().locator('#qa-flow').screenshot({ path: `${report}/centered-column.png` });
|
|
await page.locator('#style-alignSelf').scrollIntoViewIfNeeded(); await page.screenshot({ path: `${report}/editor-desktop.png`, fullPage: true });
|
|
checks.push('preview: natural, percentage and maximum widths; links/zoom; grid and flex axes; responsive parent and auto reset');
|
|
await page.locator('#save').click(); await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor();
|
|
await page.reload(); await select(); closeTo((await aligned('center')).center, 0, 'Reload preserves alignment');
|
|
const stored = await (await context.request.get(api)).json();
|
|
const block = stored.document.blocks[0].children[0].children[0];
|
|
assert.deepEqual(block.props, original); assert.deepEqual(block.style, { desktop: { width: '50%', alignSelf: 'center' } });
|
|
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, device] of [[1600, 'desktop'], [900, 'tablet'], [390, 'mobile']]) {
|
|
await publicPage.setViewportSize({ width, height: 1000 }); await publicPage.goto(origin);
|
|
await checkLayout(publicPage, device);
|
|
await publicPage.locator('#qa-alignment-section').screenshot({ path: `${report}/published-${device}.png` });
|
|
}
|
|
await plain.close(); checks.push('save/reload, unchanged media metadata, published desktop/tablet/mobile with JavaScript disabled');
|
|
assert.deepEqual(errors, []);
|
|
await writeFile(`${report}/result.json`, JSON.stringify({ checks, passed: true }, null, 2));
|
|
console.log('Image alignment 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(); }
|