Fix image centering in normal and responsive layouts
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

This commit is contained in:
kantarci committed 2026-09-14 13:12:59 +02:00
1 parent d300eae241
commit 81276ddd1a
9 files changed
+288 -8

No files matched your search

+2
View File
@@ -30,6 +30,8 @@ Ohne veröffentlichte Auswahl erscheint automatisch eine Standard-404 im Website
## Anmelden und bearbeiten
**Bildausrichtung (ab 1.8.4):** Bild auswählen → Design → Layout → **Eigene Ausrichtung quer: Mitte**. Zentriert den Bildblock und auch kleinere Bilder innerhalb des Rahmens. Funktioniert mit Prozentbreite, maximaler Breite und Bildlinks; Anfang/Ende und Gerätewerte werden ebenfalls berücksichtigt. In nebeneinander angeordneten Flex-Gruppen bleibt die Querachse vertikal. Prüfen: `npm run test:image-alignment` auf einer isolierten Kant-IT-Kopie; Screenshots unter `reports/image-alignment/`.
**Akkordeon-Inhalte (ab 1.8.3):** Einträge bleiben im Editor geöffnet. Im Eintrag auf **Inhalt hinzufügen** klicken und links Text, Bild oder ein anderes Element auswählen; alternativ direkt in die sichtbare Inhaltsfläche ziehen. **Eintrag hinzufügen** steht unter dem Akkordeon und in dessen Inhaltsoptionen. Die Eintragsübersicht rechts und die Struktur links zeigen die Titel. **Beim Laden geöffnet** bestimmt ausschließlich den Zustand der veröffentlichten Seite. Prüfen: `npm run test:accordion`, Screenshots unter `reports/accordion/`.
**Farbauswahl im Editor (ab 1.8.2):** Unter Design zeigt jedes Farbfeld eine Vorschau mit Palettenname und Hexwert. Anklicken öffnet die Website-Farben als Farbkacheln. Eigene Farben über den Farbwähler oder als Hexwert eingeben und mit Enter/✓ übernehmen. Transparente Hexfarben werden auf einem Schachbrett dargestellt. „Geerbten Wert verwenden“ entfernt die eigene Überschreibung für das aktive Gerät. Prüfen: `npm run test:colors` mit isolierten Kant-IT-Daten; Screenshots unter `reports/color-picker/`.
+1 -1
View File
@@ -1 +1 @@
1.8.3
1.8.4
+10
View File
@@ -2,6 +2,16 @@
Stand: 14.09.2026. Arbeitstitel KantSites; Produktname offen.
## ADR 086 – Bildausrichtung berücksichtigt Rahmen, Bild und Layoutachse (14.09.2026)
`align-self` allein hat in normalen Inhaltsbereichen keine Wirkung. Ein prozentual schmaler Bildrahmen blieb links; bei voller Rahmenbreite konnte zusätzlich das kleinere Bild im Rahmen links stehen. Der Browserfall mit einem 50-Prozent-Bild in einer gepolsterten Spalte reproduzierte die Abweichung trotz ausgewählter Mitte.
Der gemeinsame StyleCompiler ergänzt bei Bildern mit eigener Ausrichtung horizontale Außenabstände am Rahmen und am enthaltenen Bild sowie `justify-self` für Raster. Verlinkte und vergrößerbare Bilder werden über dasselbe innere Bild erreicht. In horizontalen Flex-Gruppen bleiben die Außenabstände am Rahmen erhalten: Dort wirkt die vorhandene Einstellung auf der vertikalen Querachse und soll die horizontale Verteilung nicht verändern. Sektionen, Akkordeon-Einträge und Slider haben innere Inhaltswrapper, deren Blockfluss berücksichtigt wird.
Bild- und Elterneinstellungen werden je Gerät geerbt. Änderungen der Flexrichtung werden deshalb auch bei unverändertem Bildwert wirksam. Auto, Strecken und Grundlinie entfernen die zusätzliche horizontale Bildausrichtung und stellen die jeweiligen selbst gesetzten Außenabstände wieder her. Originalmaße, Bildquellen, Bildbreite, Beschriftungen und Links werden nicht umgeschrieben. Beliebiges benutzerdefiniertes CSS kann diese Layoutannahmen weiterhin überschreiben.
**Migration und Rückweg:** Keine Änderung am Dokument- oder Datenbankschema. Bestehende gespeicherte Ausrichtungswerte werden beim Rendern korrigiert, ohne erneutes Speichern. Vorschau und öffentliche Ausgabe verwenden denselben Compiler; kein zusätzliches öffentliches JavaScript. Ein Rückweg auf 1.8.3 erhält alle Daten, zeigt jedoch dessen bisherige Ausrichtung.
## ADR 085 – Akkordeon-Inhalte sind im Editor direkt zugänglich (14.09.2026)
Die bisherige Vorschau verhinderte Standard-Klickaktionen einschließlich des Öffnens von `<details>`. Einträge mit `props.open: false` blieben deshalb geschlossen; für leere Akkordeon-Einträge fehlten zusätzlich die Einfügehilfen. Ein Browserfall reproduzierte den geschlossenen Zustand nach Anklicken des Titels.
+10
View File
@@ -1,5 +1,15 @@
# Projektstatus
## 1.8.4 – Bildausrichtung sichtbar korrigiert (14.09.2026)
**Behoben:** „Eigene Ausrichtung quer: Mitte“ zentriert Bilder jetzt auch in normalen Sektionen und Spalten. Zuvor blieb das Bild trotz `align-self:center` links; der Browserfall mit 50-Prozent-Breite zeigte 181 Pixel Abweichung von der Mitte (`var/image-alignment-before.log`, `reports/image-alignment/before.png`). Der gemeinsame Compiler richtet Rahmen und enthaltenes Bild aus, einschließlich kleiner Logos, Prozent-/Maximalbreiten und Bildlinks. Anfang/Ende, Gerätevererbung und Rücksetzen sind berücksichtigt; horizontale Flex-Gruppen behalten ihre vertikale Querachse. Keine Datenmigration, keine Änderung an gespeicherten Bilddaten, kein zusätzliches öffentliches Skript (ADR 086).
**Browser geprüft:** `npm run test:image-alignment` mit drei Prüfgruppen auf isolierter Kant-IT-Kopie erfolgreich: echte Layout-Auswahl, Tastatur, Anfang/Mitte/Ende, Undo/Redo, gepolsterte Inhaltsbereiche, kleine Originalbilder, Prozent-/Maximalbreiten, Link/Vergrößerung, Grid, beide Flexachsen, responsive Elterneinstellungen und Rücksetzen auf Auto mit wiederhergestelltem Außenabstand. Speichern/Laden erhält Maße und Medienmetadaten. Öffentliche Ausgabe ohne JavaScript auf Desktop 1600, Tablet 900 und Mobil 390 geprüft. Screenshots unter `reports/image-alignment/` visuell geprüft; Originaldaten unverändert.
**Zusätzlich geprüft:** Bestehendes `npm run test:image-size` mit drei Prüfgruppen erfolgreich, einschließlich Größenregler, Gerätewerten, Undo/Redo, Speichern/Laden, Veröffentlichung und Desktop-/Mobil-Barrierefreiheit der Bedienelemente. 32 JavaScript-Tests und Produktionscontainer erfolgreich; PHP-Stilprüfung der drei betroffenen Dateien ohne Befund. Die PHP-Gesamtprüfung (481 Tests / 3858 Assertions) zeigte einen von CRLF/LF abhängigen Quelltextvergleich der Layout-Gruppen; alle anderen Fälle bestanden. Vergleich zeilenumbruchunabhängig korrigiert, anschließend fünf gezielte PHP-Tests / 29 Assertions erfolgreich. Gesamtsuite wird zur abschließenden Bestätigung mit `php -d max_execution_time=0 vendor/bin/phpunit` wiederholt; der erste Lauf mit lokaler Standardkonfiguration war am 80-Sekunden-Limit abgebrochen.
**Nächster Schritt:** PHP-Gesamtprüfung abschließen, Paket aus dem exakten Commit bauen, prüfen und veröffentlichen. Keine neue Lighthouse-Messung oder vollständige Editor-Gesamtprüfung für die gezielte CSS-Compiler-Korrektur; eigene CSS-Regeln können weiterhin Layoutvorgaben überschreiben.
## 1.8.3 – Inhalte in Akkordeons sichtbar bearbeiten (14.09.2026)
**Behoben:** Geschlossene Akkordeon-Einträge ließen sich in der Editorvorschau nicht öffnen, weil die Klickbehandlung die native Aktion verhinderte. Der Fehler wurde vor der Korrektur im Browser reproduziert (`var/accordion-before.log`, `reports/accordion/before.png`). Einträge sind im Editor jetzt geöffnet, leere Inhaltsbereiche sichtbar und als Ablageziel nutzbar. „Inhalt hinzufügen“ im Eintrag und rechts im Feinschliff hängt neue Elemente an. „Eintrag hinzufügen“ steht an der Gruppe und in deren Inhaltsansicht; auch ein vollständig leeres Akkordeon ist wieder befüllbar.
+1
View File
@@ -12,6 +12,7 @@
"test:admin": "node scripts/admin-quality-check.mjs",
"test:search": "node scripts/live-search-check.mjs",
"test:image-size": "node scripts/image-size-check.mjs",
"test:image-alignment": "node scripts/image-alignment-check.mjs",
"test:link-media": "node scripts/link-media-check.mjs",
"test:404": "node scripts/not-found-check.mjs",
"test:colors": "node scripts/color-picker-check.mjs",
+145
View File
@@ -0,0 +1,145 @@
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(); }
+49 -2
View File
@@ -97,12 +97,15 @@ final class StyleCompiler
public function compileDocument(array $document): string
{
$css = '';
$walk = function (array $blocks) use (&$walk, &$css): void {
$walk = function (array $blocks, array $parent = []) use (&$walk, &$css): void {
foreach ($blocks as $block) {
if (is_array($block['style'] ?? null) && 1 === preg_match('/^[a-zA-Z0-9_-]{1,64}$/D', (string) ($block['id'] ?? ''))) {
$css .= $this->compileBlock((string) $block['id'], $block['style']);
if ('image' === ($block['type'] ?? null)) {
$css .= $this->imageAlignment((string) $block['id'], $block['style'], $parent);
}
}
$walk($block['children'] ?? []);
$walk($block['children'] ?? [], $block);
}
};
$walk($document['blocks'] ?? []);
@@ -110,6 +113,50 @@ final class StyleCompiler
return $css;
}
/**
* In a normal section/column, align-self cannot position a figure. Align its box
* and the possibly smaller image inside it, including linked images. A horizontal
* flex group must keep align-self's vertical axis without consuming its main-axis
* space with automatic margins. Resolve both styles at each breakpoint so a parent
* layout change or "auto" on a phone cannot leave desktop alignment behind.
*/
private function imageAlignment(string $id, array $style, array $parent): string
{
$css = $previous = '';
$values = $group = [];
// These block types render children in an unstyled inner wrapper.
$wrapped = in_array($parent['type'] ?? '', ['section', 'accordionitem', 'slider'], true);
foreach (['desktop', 'tablet', 'mobile'] as $device) {
$values = array_replace_recursive($values, $style[$device] ?? []);
$group = array_replace_recursive($group, $parent['style'][$device] ?? []);
if (!isset($values['alignSelf'])) {
continue;
}
$alignment = match ($values['alignSelf']) {
'center' => ['auto', 'auto', 'center'],
'flex-start' => ['0', 'auto', 'start'],
'flex-end' => ['auto', '0', 'end'],
default => null,
};
$horizontalFlex = !$wrapped
&& in_array($group['display'] ?? '', ['flex', 'inline-flex'], true)
&& in_array($group['flexDirection'] ?? 'row', ['row', 'row-reverse'], true);
[$left, $right, $justify] = $horizontalFlex || null === $alignment
? [$values['margin']['left'] ?? '0', $values['margin']['right'] ?? '0', 'auto']
: $alignment;
$rules = sprintf('#%s{margin-left:%s;margin-right:%s;justify-self:%s}#%s img{margin-left:%s;margin-right:%s}',
$id, $left, $right, $justify, $id, $alignment[0] ?? '0', $alignment[1] ?? '0');
if ($rules !== $previous) {
$css .= isset(self::BREAKPOINTS[$device])
? sprintf('@media (max-width:%dpx){%s}', self::BREAKPOINTS[$device], $rules)
: $rules;
$previous = $rules;
}
}
return $css;
}
private function compileBlock(string $id, array $style): string
{
$css = '';
+4 -5
View File
@@ -3554,7 +3554,7 @@ final class CmsTest extends TestCase
yield 'corners' => [['borderRadius' => ['topLeft' => '18px']], '#b1{border-top-left-radius:18px}'];
yield 'outline' => [['outlineWidth' => '3px', 'outlineColor' => '#2c6b4f', 'outlineOffset' => '2px'], '#b1{outline-width:3px;outline-color:#2c6b4f;outline-offset:2px}'];
yield 'position and layer' => [['position' => 'absolute', 'top' => '0', 'right' => '12px', 'zIndex' => '5'], '#b1{position:absolute;top:0;right:12px;z-index:5}'];
yield 'flex item' => [['alignSelf' => 'center', 'flexGrow' => '2', 'flexBasis' => '40%'], '#b1{align-self:center;flex-grow:2;flex-basis:40%}'];
yield 'flex item' => [['alignSelf' => 'center', 'flexGrow' => '2', 'flexBasis' => '40%'], '#b1{align-self:center;flex-grow:2;flex-basis:40%}', 'column'];
yield 'separate gaps' => [['rowGap' => '8px', 'columnGap' => '24px'], '#b1{row-gap:8px;column-gap:24px}'];
yield 'inner shadow' => [['shadowY' => '4px', 'shadowBlur' => '12px', 'shadowInset' => 'innen'], '#b1{box-shadow:inset 0 4px 12px 0 #00000026}'];
yield 'outer shadow stays outer' => [['shadowY' => '4px'], '#b1{box-shadow:0 4px 0 0 #00000026}'];
@@ -3570,11 +3570,11 @@ final class CmsTest extends TestCase
}
#[DataProvider('styleDeclarations')]
public function testEveryStylePropertyCompilesToTheDeclarationItPromises(array $style, string $expected): void
public function testEveryStylePropertyCompilesToTheDeclarationItPromises(array $style, string $expected, string $type = 'image'): void
{
$compiler = new StyleCompiler();
self::assertSame($expected, $compiler->compileDocument([
'blocks' => [['id' => 'b1', 'type' => 'image', 'style' => ['desktop' => $style]]],
'blocks' => [['id' => 'b1', 'type' => $type, 'style' => ['desktop' => $style]]],
]));
// And what compiles has to survive the validator, or the editor could offer it in vain.
$document = DemoContent::starter('Mit Stil');
@@ -3593,8 +3593,7 @@ final class CmsTest extends TestCase
$panel = (string) file_get_contents(__DIR__.'/../public/assets/design-panel.js');
foreach (array_keys($groups) as $group) {
// A group the panel does not know would hold its properties in an unnamed accordion.
self::assertMatchesRegularExpression('/
'.$group.': /', $panel, sprintf('Die Gruppe „%s“ fehlt im Panel.', $group));
self::assertMatchesRegularExpression('/^\h*'.preg_quote($group, '/').':\h/m', $panel, sprintf('Die Gruppe „%s“ fehlt im Panel.', $group));
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Tests;
use App\Content\StyleCompiler;
use PHPUnit\Framework\TestCase;
final class ImageAlignmentTest extends TestCase
{
public function testAlignmentOnlyAddsRulesToImagesWithAnExplicitSetting(): void
{
$compiler = new StyleCompiler();
self::assertSame('#plain{width:50%}#text{align-self:center}', $compiler->compileDocument(['blocks' => [
['id' => 'plain', 'type' => 'image', 'style' => ['desktop' => ['width' => '50%']]],
['id' => 'text', 'type' => 'text', 'style' => ['desktop' => ['alignSelf' => 'center']]],
]]));
self::assertSame('', $compiler->compileDocument(['blocks' => [
['id' => 'bad{selector}', 'type' => 'image', 'style' => ['desktop' => ['alignSelf' => 'center']]],
]]));
}
public function testResponsiveResetRestoresAuthoredMarginsAndClearsInnerAlignment(): void
{
foreach (['auto', 'stretch', 'baseline'] as $reset) {
$css = (new StyleCompiler())->compileDocument(['blocks' => [[
'id' => 'logo', 'type' => 'image', 'style' => [
'desktop' => ['width' => '50%', 'alignSelf' => 'center', 'margin' => ['left' => '11px', 'right' => '7px', 'top' => '20px']],
'tablet' => ['alignSelf' => 'flex-end'],
'mobile' => ['alignSelf' => $reset, 'margin' => ['left' => '15px']],
],
]]]);
self::assertStringContainsString('#logo{margin-left:auto;margin-right:auto;justify-self:center}#logo img{margin-left:auto;margin-right:auto}', $css);
self::assertStringContainsString('@media (max-width:1024px){#logo{margin-left:auto;margin-right:0;justify-self:end}#logo img{margin-left:auto;margin-right:0}}', $css);
self::assertStringContainsString('@media (max-width:767px){#logo{margin-left:15px;margin-right:7px;justify-self:auto}#logo img{margin-left:0;margin-right:0}}', $css);
self::assertSame(1, substr_count($css, 'margin-top:20px'));
}
}
public function testChangingParentFlexAxisRecomputesInheritedImageAlignment(): void
{
$css = (new StyleCompiler())->compileDocument(['blocks' => [[
'id' => 'group', 'type' => 'column', 'style' => [
'desktop' => ['display' => 'inline-flex', 'flexDirection' => 'row-reverse'],
'tablet' => ['flexDirection' => 'column-reverse'],
'mobile' => ['flexDirection' => 'row'],
], 'children' => [[
'id' => 'logo', 'type' => 'image', 'style' => ['desktop' => ['alignSelf' => 'center', 'margin' => ['left' => '9px']]],
]],
]]]);
self::assertStringContainsString('#logo{align-self:center;margin-left:9px}', $css);
self::assertStringContainsString('#logo{margin-left:9px;margin-right:0;justify-self:auto}#logo img{margin-left:auto;margin-right:auto}', $css);
self::assertStringContainsString('@media (max-width:1024px){#logo{margin-left:auto;margin-right:auto;justify-self:center}#logo img{margin-left:auto;margin-right:auto}}', $css);
self::assertStringContainsString('@media (max-width:767px){#logo{margin-left:9px;margin-right:0;justify-self:auto}#logo img{margin-left:auto;margin-right:auto}}', $css);
}
public function testSectionAccordionAndSliderInnerWrappersUseBlockAlignment(): void
{
foreach (['section', 'accordionitem', 'slider'] as $type) {
$css = (new StyleCompiler())->compileDocument(['blocks' => [[
'id' => 'group', 'type' => $type, 'style' => ['desktop' => ['display' => 'flex']],
'children' => [['id' => 'logo', 'type' => 'image', 'style' => ['desktop' => ['alignSelf' => 'center']]]],
]]]);
self::assertStringContainsString('#logo{margin-left:auto;margin-right:auto;justify-self:center}#logo img{margin-left:auto;margin-right:auto}', $css);
}
}
}