125 lines
9.6 KiB
JavaScript
125 lines
9.6 KiB
JavaScript
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';
|
|
import { mediaProps } 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-size'; 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();
|
|
const media = (await (await context.request.get(origin + '/admin/api/media')).json()).items;
|
|
const chosen = media.find(item => /teamviewer/i.test(item.title)) || media.find(item => item.variants.at(-1).width >= 800);
|
|
assert.ok(chosen); const original = mediaProps(chosen); original.alt ||= chosen.title;
|
|
data.document.blocks.unshift({ id: 'qa-image-size-section', type: 'section', layout: { width: 'full', tone: 'paper' }, style: { desktop: { maxWidth: '800px' } }, children: [{ id: 'qa-image-size', type: 'image', props: original }] });
|
|
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 page.goto(`${origin}/admin/pages/${fixture.homeId}`);
|
|
const select = async () => { await page.locator('#structure [data-block-id="qa-image-size"]').click(); await page.locator('[data-panel="content"]').click(); };
|
|
await select();
|
|
const exact = () => page.getByLabel('Bildbreite (%)', { exact: true });
|
|
const slider = () => page.getByRole('slider', { name: 'Bildbreite Schieberegler' });
|
|
const width = async percent => page.waitForFunction(percent => {
|
|
const doc = document.querySelector('#preview')?.contentDocument;
|
|
const figure = doc?.getElementById('qa-image-size'); if (!figure) return false;
|
|
const parent = figure.parentElement; const style = doc.defaultView.getComputedStyle(parent);
|
|
const available = parent.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
|
return Math.abs(figure.getBoundingClientRect().width / available * 100 - percent) < .1;
|
|
}, percent);
|
|
const saved = async () => { await page.locator('#save').click(); await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor(); };
|
|
const changeDevice = async device => page.locator(`[data-viewport="${device}"]`).click();
|
|
assert.equal(await exact().inputValue(), '');
|
|
await exact().fill('50'); await width(50);
|
|
assert.equal(await exact().evaluate(input => input === document.activeElement), true);
|
|
await exact().blur();
|
|
await slider().focus(); await slider().press('ArrowRight'); await width(50.1);
|
|
await page.locator('#undo').click(); await width(50); await page.locator('#redo').click(); await width(50.1);
|
|
const box = await slider().boundingBox();
|
|
await page.mouse.move(box.x + box.width * .2, box.y + box.height / 2); await page.mouse.down();
|
|
await page.mouse.move(box.x + box.width * .6, box.y + box.height / 2, { steps: 8 }); await page.mouse.up();
|
|
const dragged = Number(await exact().inputValue()); assert.ok(dragged > 50.1); await width(dragged);
|
|
await page.locator('#undo').click(); await width(50.1);
|
|
await page.locator('#redo').click(); await width(dragged);
|
|
await exact().fill('0'); assert.equal(await exact().getAttribute('aria-invalid'), 'true'); await width(dragged);
|
|
await exact().fill('50'); await width(50); await exact().blur();
|
|
checks.push('number, keyboard and drag update preview; one undo per gesture; invalid values do not change the document');
|
|
|
|
await changeDevice('tablet'); assert.equal(await exact().inputValue(), '50');
|
|
assert.match(await page.locator('#image-width-state').innerText(), /Von Desktop übernommen/);
|
|
await exact().fill('75'); await width(75); await exact().blur();
|
|
await changeDevice('mobile'); assert.equal(await exact().inputValue(), '75');
|
|
await exact().fill('30'); await width(30); await exact().blur();
|
|
await page.locator('#image-width-reset').click(); await width(75);
|
|
assert.match(await page.locator('#image-width-state').innerText(), /Von Tablet übernommen/);
|
|
await page.locator('#undo').click(); await width(30);
|
|
await changeDevice('desktop'); assert.equal(await exact().inputValue(), '50'); await width(50);
|
|
await saved(); await page.reload(); await select(); assert.equal(await exact().inputValue(), '50'); await width(50);
|
|
const stored = await (await context.request.get(api)).json();
|
|
const image = stored.document.blocks[0].children[0];
|
|
assert.deepEqual(image.style, { desktop: { width: '50%' }, tablet: { width: '75%' }, mobile: { width: '30%' } });
|
|
assert.deepEqual(image.props, original, 'original dimensions, media variants, caption and alt text are unchanged');
|
|
checks.push('device inheritance, reset, undo, save/reload and unchanged image metadata');
|
|
|
|
await page.locator('[data-panel="design"]').click();
|
|
const size = page.locator('.design-group').filter({ has: page.locator('summary').filter({ hasText: /^Größe$/ }) });
|
|
if (!(await size.getAttribute('open'))) await size.locator('summary').click();
|
|
assert.equal(await page.locator('#style-width').inputValue(), '50');
|
|
await page.locator('#style-width').fill('60'); await page.locator('#style-width').blur();
|
|
await page.locator('[data-panel="content"]').click(); assert.equal(await exact().inputValue(), '60'); await width(60);
|
|
await page.screenshot({ path: `${report}/editor-desktop.png`, fullPage: true });
|
|
for (const viewport of [1600, 390]) {
|
|
await page.setViewportSize({ width: viewport, height: 1050 });
|
|
await exact().scrollIntoViewIfNeeded();
|
|
const results = await new AxeBuilder({ page }).include('.image-width-control').analyze();
|
|
assert.deepEqual(results.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) })), []);
|
|
assert.equal(await page.locator('.image-width-control').evaluate(el => el.scrollWidth <= el.clientWidth), true);
|
|
await page.locator('.image-width-control').screenshot({ path: `${report}/control-${viewport}.png` });
|
|
}
|
|
await page.setViewportSize({ width: 1600, height: 1050 }); await saved();
|
|
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 [viewport, percent] of [[1600, 60], [900, 75], [390, 30]]) {
|
|
await publicPage.setViewportSize({ width: viewport, height: 900 }); await publicPage.goto(origin);
|
|
const geometry = await publicPage.locator('#qa-image-size').evaluate(async figure => {
|
|
const img = figure.querySelector('img'); await img.decode();
|
|
const parent = figure.parentElement, style = getComputedStyle(parent);
|
|
return { figure: figure.getBoundingClientRect().width, available: parent.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight), width: img.getBoundingClientRect().width, height: img.getBoundingClientRect().height };
|
|
});
|
|
assert.ok(Math.abs(geometry.figure / geometry.available * 100 - percent) < .1);
|
|
assert.ok(Math.abs(geometry.width / geometry.height - original.width / original.height) < .02, 'image aspect ratio is preserved');
|
|
await publicPage.locator('#qa-image-size-section').screenshot({ path: `${report}/published-${viewport}.png` });
|
|
}
|
|
await plain.close(); checks.push('shared Design width; desktop/mobile accessible controls; published responsive percentages and aspect ratio without JavaScript');
|
|
assert.deepEqual(errors, []);
|
|
await writeFile(`${report}/result.json`, JSON.stringify({ checks, passed: true }, null, 2));
|
|
console.log('Image size 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(); }
|