Files
KantSites/scripts/link-media-check.mjs
T
kantarci 25fd388f80
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
fix: reliably reveal image link fields on selection (1.7.4)
2026-09-14 11:56:40 +02:00

213 lines
17 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-empty-image', type: 'image', props: { ...original, click: 'keine' } },
{ 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-empty-image');
const clickAction = () => page.getByLabel('Beim Klick', { exact: true });
await page.locator('#property-href').waitFor({ state: 'hidden' });
const nativeSelect = await clickAction().elementHandle();
await clickAction().click(); await page.keyboard.press('End'); await page.keyboard.press('Enter');
await page.getByLabel('Linkziel', { exact: true }).waitFor();
assert.equal(await nativeSelect.evaluate(el => el.isConnected), true, 'Keep the native dropdown mounted while changing the action');
await nativeSelect.dispose();
await page.getByLabel('Linkziel', { exact: true }).fill('/kontakt');
await clickAction().focus(); await page.keyboard.press('Home');
await page.locator('#property-href').waitFor({ state: 'hidden' });
await page.keyboard.press('End'); await page.getByLabel('Linkziel', { exact: true }).waitFor();
assert.equal(await page.getByLabel('Linkziel', { exact: true }).inputValue(), '/kontakt');
await clickAction().selectOption('keine');
await clickAction().evaluate(el => { el.value = 'link'; el.dispatchEvent(new Event('change', { bubbles: true })); });
await page.getByLabel('Linkziel', { exact: true }).waitFor();
assert.equal(await page.getByLabel('Linkziel', { exact: true }).inputValue(), '/kontakt');
await page.locator('#undo').click();
assert.equal(await clickAction().inputValue(), 'keine'); await page.locator('#property-href').waitFor({ state: 'hidden' });
await page.locator('#redo').click();
assert.equal(await clickAction().inputValue(), 'link'); await page.getByLabel('Linkziel', { exact: true }).waitFor();
await clickAction().selectOption('vergroessern');
await page.locator('#property-href').waitFor({ state: 'hidden' }); await page.locator('#property-newTab').waitFor({ state: 'hidden' });
await frame.locator('#qa-empty-image a.image-zoom').waitFor();
await clickAction().selectOption('link'); await page.getByLabel('Linkziel', { exact: true }).waitFor();
await page.locator('.image-link-options').screenshot({ path: `${report}/image-link-fields.png` });
checks.push('fresh image URL appears after native dropdown and keyboard selection; URL retained; change-only event supported');
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 }).focus();
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 select('qa-empty-image');
assert.equal(await clickAction().inputValue(), 'link');
assert.equal(await page.getByLabel('Linkziel', { exact: true }).inputValue(), '/kontakt');
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');
assert.equal(await publicPage.locator('#qa-empty-image a').getAttribute('href'), '/kontakt');
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(); }