180 lines
14 KiB
JavaScript
180 lines
14 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/link-media'; 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-link-section', type: 'section', children: [
|
|
{ id: 'qa-link-image', type: 'image', props: { ...original, href: '/impressum', click: 'link' } },
|
|
{ id: 'qa-link-button', type: 'button', props: { text: 'QA Neuer Tab', href: '/impressum' } },
|
|
{ id: 'qa-link-text', type: 'text', props: { text: '<p>Textlink prüfen</p>' } },
|
|
{ id: 'qa-link-gallery', type: 'gallery', children: [{ id: 'qa-gallery-image', type: 'image', props: original }] },
|
|
] });
|
|
data.document.blocks.unshift({ id: 'qa-background', type: 'section', style: { desktop: { backgroundImage: chosen.id, minHeight: '200px' } }, children: [] });
|
|
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 frame = page.frameLocator('#preview');
|
|
const select = async (id, panel = 'content') => { await page.locator(`#structure [data-block-id="${id}"]`).click(); await page.locator(`[data-panel="${panel}"]`).click(); };
|
|
const thumbnail = () => page.locator('#properties > .selected-media-preview img');
|
|
const background = async hasImage => page.waitForFunction(hasImage => {
|
|
const doc = document.querySelector('#preview')?.contentDocument, block = doc?.getElementById('qa-background');
|
|
return block && doc.defaultView.getComputedStyle(block).backgroundImage.includes('url(') === hasImage;
|
|
}, hasImage);
|
|
const save = async () => { await page.locator('#save').click(); await page.locator('#save-status').filter({ hasText: 'Entwurf gespeichert' }).waitFor(); };
|
|
await select('qa-link-image'); await thumbnail().waitFor({ state: 'visible' });
|
|
assert.ok(await thumbnail().evaluate(img => img.naturalWidth > 0));
|
|
await page.getByLabel('In neuem Tab öffnen', { exact: true }).check();
|
|
await frame.locator('#qa-link-image a[target="_blank"][rel="noopener"]').waitFor();
|
|
await page.getByLabel('Beim Klick', { exact: true }).selectOption('keine');
|
|
await frame.locator('#qa-link-image a').waitFor({ state: 'detached' });
|
|
await page.getByLabel('Beim Klick', { exact: true }).selectOption('link');
|
|
assert.equal(await page.getByLabel('Beim Klick', { exact: true }).evaluate(el => el === document.activeElement), true);
|
|
await page.getByLabel('Linkziel', { exact: true }).waitFor();
|
|
await page.getByLabel('Bildadresse', { exact: true }).fill('/nicht-vorhanden.png');
|
|
await page.getByLabel('Bildadresse', { exact: true }).blur();
|
|
await page.locator('#properties > .selected-media-preview').getByText(/nicht verfügbar/).waitFor();
|
|
await page.locator('#undo').click(); await thumbnail().waitFor({ state: 'visible' });
|
|
await page.getByRole('button', { name: 'Bild entfernen', exact: true }).click();
|
|
await frame.locator('#qa-link-image').waitFor({ state: 'detached' });
|
|
assert.equal((await context.request.get(origin + '/admin/api/media/' + chosen.id)).status(), 200);
|
|
await page.locator('#undo').click(); await select('qa-link-image'); await thumbnail().waitFor({ state: 'visible' });
|
|
await page.locator('.right-panel').screenshot({ path: `${report}/image-inspector.png` });
|
|
await select('qa-link-gallery'); await page.locator('.selected-media-list img').waitFor({ state: 'visible' });
|
|
await page.locator('.selected-media-list').getByRole('button', { name: /entfernen$/ }).click();
|
|
await frame.locator('#qa-gallery-image').waitFor({ state: 'detached' });
|
|
await page.locator('#undo').click(); await frame.locator('#qa-gallery-image').waitFor();
|
|
checks.push('image and gallery thumbnails, missing-image feedback, remove/undo without deleting media; image link/no-action toggle');
|
|
|
|
await select('qa-background', 'design');
|
|
const group = page.locator('.design-group').filter({ has: page.locator('summary').filter({ hasText: /^Hintergrund$/ }) });
|
|
if (!(await group.getAttribute('open'))) await group.locator('summary').click();
|
|
await group.locator('.selected-media-preview img').waitFor({ state: 'visible' });
|
|
await page.locator('[data-viewport="tablet"]').click();
|
|
await group.locator('.selected-media-preview img').waitFor({ state: 'visible' }); await background(true);
|
|
await group.getByRole('button', { name: 'Bild entfernen', exact: true }).click(); await background(false);
|
|
assert.equal(await page.locator('#style-backgroundImage').evaluate(el => el === document.activeElement), true);
|
|
await page.locator('[data-viewport="mobile"]').click(); await background(false);
|
|
await page.locator('[data-viewport="desktop"]').click(); await background(true);
|
|
await page.locator('[data-viewport="tablet"]').click();
|
|
await page.getByRole('button', { name: 'Hintergrundbild zurücksetzen', exact: true }).click(); await background(true);
|
|
await page.locator('#undo').click(); await background(false);
|
|
await page.locator('[data-viewport="desktop"]').click();
|
|
checks.push('inherited background thumbnail, remove on tablet/mobile, preserve desktop, reset and undo');
|
|
|
|
await select('qa-link-button'); await page.getByLabel('In neuem Tab öffnen', { exact: true }).check();
|
|
await frame.locator('#qa-link-button[target="_blank"]').waitFor();
|
|
await select('qa-link-text');
|
|
const surface = page.locator('.rt-surface');
|
|
await surface.evaluate(el => { const range = document.createRange(); range.selectNodeContents(el.querySelector('p')); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); el.focus(); });
|
|
await page.getByRole('button', { name: 'Link setzen oder entfernen' }).click();
|
|
await page.getByLabel('Linkziel', { exact: true }).fill('/impressum');
|
|
await page.getByLabel('In neuem Tab öffnen', { exact: true }).check();
|
|
await page.getByRole('button', { name: 'Linkziel übernehmen' }).click();
|
|
await frame.locator('#qa-link-text a[target="_blank"][rel="noopener"]').waitFor();
|
|
const editExisting = async () => {
|
|
await surface.locator('a').evaluate(el => { const range = document.createRange(); range.selectNodeContents(el); range.collapse(true); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); el.closest('[contenteditable]').focus(); });
|
|
await page.getByRole('button', { name: 'Link setzen oder entfernen' }).click();
|
|
assert.equal(await page.getByLabel('Linkziel', { exact: true }).inputValue(), '/impressum');
|
|
};
|
|
await editExisting(); assert.equal(await page.getByLabel('In neuem Tab öffnen', { exact: true }).isChecked(), true);
|
|
await page.getByLabel('In neuem Tab öffnen', { exact: true }).uncheck(); await page.getByRole('button', { name: 'Linkziel übernehmen' }).click();
|
|
await frame.locator('#qa-link-text a:not([target])').waitFor();
|
|
await editExisting(); await page.getByLabel('In neuem Tab öffnen', { exact: true }).check();
|
|
await page.locator('.rt-link').screenshot({ path: `${report}/text-link.png` });
|
|
const audit = await new AxeBuilder({ page }).exclude('#preview').analyze(); assert.deepEqual(audit.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) })), []);
|
|
await page.getByRole('button', { name: 'Linkziel übernehmen' }).click();
|
|
await save(); await page.reload(); await select('qa-link-image');
|
|
assert.equal(await page.getByLabel('In neuem Tab öffnen', { exact: true }).isChecked(), true);
|
|
await thumbnail().waitFor({ state: 'visible' });
|
|
await page.locator('#publish').click(); await page.locator('#confirm-publish').click();
|
|
await page.locator('#save-status').filter({ hasText: 'Veröffentlicht' }).waitFor();
|
|
checks.push('button and rich-text target creation/edit/uncheck, safe attributes, save/reload and publication');
|
|
|
|
await page.goto(origin + '/admin/menus');
|
|
await page.getByText('Eigener Link', { exact: true }).filter({ has: page.locator('xpath=self::summary') }).click();
|
|
await page.locator('#link-label').fill('QA Menülink'); await page.locator('#link-url').fill('/impressum');
|
|
await page.locator('#link-new-tab').check(); await page.locator('#menu-add-url').click();
|
|
const entry = () => page.locator('#menu-items > li').last();
|
|
await entry().locator('summary').click();
|
|
assert.equal(await entry().getByLabel('In neuem Tab öffnen', { exact: true }).isChecked(), true);
|
|
await entry().getByLabel('In neuem Tab öffnen', { exact: true }).uncheck();
|
|
await page.locator('#menu-undo').click();
|
|
await entry().locator('summary').click();
|
|
assert.equal(await entry().getByLabel('In neuem Tab öffnen', { exact: true }).isChecked(), true);
|
|
await page.getByRole('button', { name: 'Menü speichern', exact: true }).click();
|
|
await page.locator('#menu-status').filter({ hasText: 'Gespeicherter Stand' }).waitFor();
|
|
await page.reload(); await entry().locator('summary').click();
|
|
assert.equal(await entry().getByLabel('In neuem Tab öffnen', { exact: true }).isChecked(), true);
|
|
await entry().screenshot({ path: `${report}/menu-link.png` });
|
|
checks.push('menu creation, checkbox undo, save and reload');
|
|
|
|
const publicPage = await context.newPage(); await publicPage.goto(origin);
|
|
for (const selector of ['#qa-link-image a', '#qa-link-button', '#qa-link-text a']) {
|
|
assert.equal(await publicPage.locator(selector).getAttribute('target'), '_blank');
|
|
assert.equal(await publicPage.locator(selector).getAttribute('rel'), 'noopener');
|
|
}
|
|
assert.equal(await publicPage.getByRole('link', { name: 'QA Menülink', exact: true }).getAttribute('target'), '_blank');
|
|
const popupPromise = publicPage.waitForEvent('popup'); await publicPage.locator('#qa-link-button').click();
|
|
const popup = await popupPromise; await popup.waitForLoadState();
|
|
assert.equal(new URL(popup.url()).pathname, '/impressum'); assert.equal(await popup.evaluate(() => window.opener), null); await popup.close();
|
|
assert.equal(new URL(publicPage.url()).pathname, '/');
|
|
const publicAudit = await new AxeBuilder({ page: publicPage }).analyze();
|
|
assert.deepEqual(publicAudit.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) })), []);
|
|
await publicPage.close();
|
|
const noScriptContext = await browser.newContext({ javaScriptEnabled: false });
|
|
const noScript = await noScriptContext.newPage();
|
|
for (const [width, hasImage] of [[1600, true], [900, false], [390, false]]) {
|
|
await noScript.setViewportSize({ width, height: 1000 }); await noScript.goto(origin);
|
|
assert.equal(await noScript.locator('#qa-background').evaluate(el => getComputedStyle(el).backgroundImage.includes('url(')), hasImage);
|
|
assert.equal(await noScript.locator('#qa-link-image a').getAttribute('target'), '_blank');
|
|
}
|
|
await noScriptContext.close();
|
|
await page.goto(`${origin}/admin/pages/${fixture.homeId}`); await select('qa-link-image');
|
|
for (const viewport of [1600, 390]) {
|
|
await page.setViewportSize({ width: viewport, height: 1050 }); await thumbnail().scrollIntoViewIfNeeded();
|
|
const result = await new AxeBuilder({ page }).exclude('#preview').analyze();
|
|
assert.deepEqual(result.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) })), []);
|
|
assert.equal(await page.locator('.selected-media-preview').evaluate(el => el.scrollWidth <= el.clientWidth), true);
|
|
await page.locator('#properties').screenshot({ path: `${report}/image-properties-${viewport}.png` });
|
|
}
|
|
checks.push('public links really open a separate tab without opener; public page and desktop/mobile image controls pass axe; background removal persists without JavaScript at 1600/900/390 px');
|
|
assert.deepEqual(errors, []);
|
|
await writeFile(`${report}/result.json`, JSON.stringify({ checks, passed: true }, null, 2));
|
|
console.log('Link and media 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(); }
|