diff --git a/README.md b/README.md index dd16810..f6f2283 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,12 @@ Lokaler CMS-Prototyp für PHP-Webhosting mit erstem visuellen Editor. Symfony 7. Die übernommenen Originaldokumente sind `AGENTS.md` und `ENTWICKLUNGSPLAN.md`. Den tatsächlichen Stand enthält `docs/STATUS.md`, Entscheidungen und Datenmodell stehen in `docs/ARCHITECTURE.md`. +## Formularbutton und Bewegungseffekte (ab 1.10.1) + +„Absenden“ übernimmt den Website-Standardbutton aus Design/Typografie: Form, Darstellung, Größe, Schrift und normale/Hover-Farben. Bestehende Formulare aktualisieren sich ohne erneutes Speichern; der Button hat seine natürliche Breite statt automatisch die gesamte Formularbreite. Formularfelder, Versand und Validierung bleiben unverändert. + +Einblendungen starten nach dem Wechsel von reduzierter zu erlaubter Bewegung wieder ohne Neuladen. Bereits gezeigte Elemente werden nicht erneut animiert. Im Editor zeigt eine Meldung, wenn der Browser Bewegung reduziert. Eine weiterhin aktive Browservorgabe wird beachtet; dieses Update überschreibt keine Besucherpräferenzen. + ## Sichtbarkeit und Hamburger-Menüs (ab 1.10.0) Element auswählen → **Sichtbarkeit**: auf allen Geräten, nur Desktop/Tablet/Mobil, auf einem Gerät ausblenden oder überall ausblenden. Die Auswahl steht in Inhalt, Design und Erweitert. Ausgeblendete Elemente bleiben links in der Struktur auswählbar; dort steht auch ihre Sichtbarkeit. „Auf allen Geräten“ setzt die Gerätebeschränkung zurück. Eigene Layoutwerte bleiben erhalten. Ausgeblendete Elternbereiche verbergen auch ihre Inhalte. diff --git a/VERSION b/VERSION index 81c871d..4dae298 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.10.0 +1.10.1 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3108552..7de10e3 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,6 +2,10 @@ Stand: 14.09.2026. Arbeitstitel KantSites; Produktname offen. +## ADR 089 – Formularaktionen nutzen den Website-Standardbutton (14.09.2026) + +Der öffentliche Absenden-Button erhält dieselbe Klasse `cms-button` wie normale Buttonblöcke. Die bisherigen formularspezifischen Farb- und Hover-Regeln entfallen; die bereits vorhandenen Theme-Variablen bestimmen Form, Darstellung, Größe, Schrift und Farben. `justify-self:start` verhindert das implizite Strecken durch das Formular-Grid. Es werden keine Designwerte in Formulardefinitionen kopiert: spätere Änderungen am Website-Standard wirken auch auf vorhandene Formulare. Nativer Submit-Typ, Beschriftung, Fokus, Validierung und Versand bleiben erhalten. Keine Daten-/Schemamigration; ein Code-Rückweg stellt das frühere Sonderdesign wieder her. + ## ADR 088 – Gerätesichtbarkeit und automatische Hamburger-Navigation (14.09.2026) `layout.visibility` enthält optional eine feste Auswahl: `all`, `desktop`, `tablet`, `mobile`, `hide-desktop`, `hide-tablet`, `hide-mobile`, `hidden`. Fehlend bedeutet überall sichtbar. Die Bedienidee wurde anhand der lokalen Flatsome-Dateien `inc/builder/shortcodes/commons/visibility.php` und `template-parts/header/header-main.php` geprüft; kein Fremdcode oder Asset wurde übernommen. Die Sichtbarkeit steht in allen Eigenschaftsansichten direkt am Element und wird im Strukturbaum benannt. Ausgeblendete Elemente bleiben dort auswählbar. Änderungen verwenden dieselbe History, Entwurfsspeicherung und Veröffentlichung wie andere Blockwerte. @@ -106,6 +110,8 @@ Die Mediathek filtert Titel und Alternativtext vor COUNT und LIMIT in der Datenb ## ADR 078 – Kontrollierbare Slider und Einblendungen beim Erscheinen (14.09.2026) +Ergänzung zur Laufzeit: Eine Änderung von reduzierter zu erlaubter Bewegung nimmt die Beobachtung noch nicht gezeigter Elemente wieder auf, auch wenn das Dokument schon mit reduzierter Bewegung gestartet ist. Die Aufräumfunktion entfernt denselben Präferenzlistener. Im Editor zeigt eine laufend aktualisierte Statusmeldung die tatsächlich gemeldete Browserpräferenz. Das CMS setzt diese weder aus einer Windows-Einstellung noch aus erfolgreichen Testprofilen voraus; es überschreibt sie nicht. Die Browsermeldung und ihre Ursache sind getrennt zu diagnostizieren. + Die bisherige Scroll-Zeitachse ersetzte unterschiedliche Effekte durch dieselbe Aufwärtsbewegung. `motion.js` beobachtet stattdessen sichtbare Blöcke und spielt die gewählte Richtung/Überblendung/Skalierung/Unschärfe einmal mit der Web Animations API. Verzögerungen bleiben erhalten. Ohne JavaScript sind alle Inhalte sichtbar; reduzierte Bewegung unterbindet die Wiedergabe und bricht laufende Animationen ab. Nur Dokumente mit einem Effekt laden das Modul, einschließlich Kopf/Fuß und verknüpften Sektionen. Inhalt und Dokumentschema bleiben unverändert. Slider behalten Scroll-Snap als Grundlage. Ein kleines Modul ergänzt sichtbare Pfeile, Zähler, optionale Punkte sowie Pause/Wiedergabe bei Autoplay. Manuelle Bedienung stoppt auch einen bereits geplanten Timer. Automatik wartet bei Hover, außerhalb des Bildschirms oder in unsichtbaren Tabs; Bewegungsreduktion deaktiviert sie. Ohne Skript bleibt der Inhalt scrollbar. Im Editor startet der Benutzer diese Module ausdrücklich aus dem Elternfenster; die iframe-Sandbox bleibt `allow-same-origin` ohne `allow-scripts`. Neue Vorschauen räumen Observer, Timer und Ereignisse der alten Instanz auf. diff --git a/docs/STATUS.md b/docs/STATUS.md index 2120466..f5a3949 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,5 +1,37 @@ # Projektstatus +## 1.10.1 – Standardbutton im Formular und zuverlässiges Wiederaufnehmen von Einblendungen (14.09.2026) + +**Umgesetzt:** „Absenden“ verwendet den konfigurierten Website-Standardbutton, einschließlich Form, Darstellung, Größe, Schrift sowie normalen und Hover-Farben. Harte Formularfarben entfernt und natürliche Buttonbreite hergestellt. Bestehende Formulare übernehmen Theme-Änderungen ohne Inhaltsmigration (ADR 089). Enthalten sind außerdem die unten dokumentierte Observer-Korrektur und die dynamische Bewegungsmeldung im Editor. + +**Prüfungen:** Formularabweichung vor der Korrektur mit realen berechneten CSS-Werten reproduziert (`var/form-button-before.log`). 10 gezielte PHP-Tests / 76 Assertions für Formulare, Validierung, Versand, Spam-Schutz und Menüformular erfolgreich; eine alte Menüerwartung auf die seit 1.10.0 getrennten responsiven Darstellungen angepasst. 33 JavaScript-Tests, alle 49 Twig-Dateien, Produktionscontainer und PHP-Stilprüfung erfolgreich. Bewegungstest auf isolierter Pilotkopie bei 1600/390 px einschließlich Präferenzwechsel und Editorhinweis erneut erfolgreich. Kein neuer Lighthouse-Gesamtlauf oder Test des normalen Chrome-Benutzerprofils. + +**Browser:** Drei Button-Prüfgruppen erfolgreich auf isolierter Kant-IT-Kopie. Globale Vererbung des Formularbuttons bei 1600/900/390 px ohne JavaScript mit berechneten CSS-Werten gegen einen gewöhnlichen Standardbutton verglichen; Hover, Tastaturfokus, natürliche Breite sowie spätere Umstellung von Rahmen/Pille auf gefüllt/abgerundet/groß mit eigenen Farben geprüft. Bestehende Button-Tests für Undo/Redo, Gerätevarianten und Veröffentlichung ebenfalls erfolgreich. Screenshots unter `reports/buttons/` visuell geprüft. + +**Release:** 1.10.1 beauftragt; Paketprüfung und Veröffentlichung folgen. Die Browservorgabe `prefers-reduced-motion` bleibt wirksam; ein dauerhaftes `true` wird durch dieses Release nicht überschrieben. Keine Kundeninhalte oder Systemeinstellungen verändert. + +## Einblendungen nach Freigabe wieder aufnehmen – lokal korrigiert (14.09.2026) + +**Neue Evidenz:** Umuts Screenshot zeigt eingeschaltete Windows-Animationseffekte. Auch die lokale Win32-Abfrage `SPI_GETCLIENTAREAANIMATION` ergibt `true`; kein Chrome-Prozess mit `--force-prefers-reduced-motion` gefunden. Die frühere Empfehlung, Windows-Animationen einzuschalten, ist damit nicht als Lösung belegt. Warum Umuts Chrome-Profil dennoch `prefers-reduced-motion: reduce` meldet, bleibt offen; eine Chrome-DevTools-Medienemulation ist eine mögliche, nicht verifizierte Ursache. + +**CMS-Fehler reproduziert und behoben:** Der Observer in `motion.js` startete nach dem Wechsel von reduzierter zu erlaubter Bewegung nicht erneut. Der neue Browserfall scheiterte vor der Änderung (`var/motion-resume-before.log`). Jetzt werden noch nicht gezeigte Elemente wieder beobachtet; bereits gezeigte Elemente laufen weiterhin nur einmal, laufende Animationen werden bei Reduktion abgebrochen. Keine Änderung an der grundsätzlichen Beachtung der Browserpräferenz. Im Editor erscheint eine dynamische Statusmeldung, wenn der Browser Einblendungen, Hover-Bewegungen und Slider-Automatik unterdrückt. + +**Geprüft:** Erweiterte `scripts/motion-check.mjs` auf isolierter Pilotkopie bei 1600/390 px erfolgreich: Start unter Reduktion und anschließende Freigabe ohne Reload, nachträgliches Sperren/Freigeben, keine Wiederholung bereits gezeigter Elemente, Hover aus/an, alle bisherigen Scroll-/Richtungsfälle, lesbare Ausgabe ohne JavaScript. Editorhinweis erscheint/verschwindet bei Präferenzänderung, Screenshot `reports/motion/editor-motion-status.png` visuell geprüft. 33 JavaScript-Tests, Twig-Lint für den Editor und Git-Diff-Prüfung erfolgreich; Originaldatenbank unverändert. + +**Direkte Produktionsgegenprobe:** Mit installiertem Chrome in zwei getrennten Testkontexten auf `web.kant-it.de`: bei emuliertem `reduce` keine Einblendung und Hover-Transform `none`; bei `no-preference` Kontaktsektion mit `translateY(28px)` und App-Karte beim Hover um −4 px verschoben. Keine JavaScript-Seitenfehler; Bericht `reports/motion/production-proof.json`. Während der Prüfung war an `leistung-app` inzwischen `fx-entrance-none` gespeichert; keine Produktionsinhalte verändert. Dieser Vergleich zeigt die Wirkung der Präferenz, ersetzt jedoch keine Prüfung des normalen Benutzerprofils. + +**Offen / nächster Schritt:** Die lokale Observer-Korrektur erklärt keinen dauerhaft wahren Browserwert und behebt bei weiter aktivem `reduce` nicht Umuts gesamten Produktionsbefund. Chrome-Rendering-Emulation prüfen; Windows-Einstellungen nicht erneut als Ursache behaupten. Lokale Änderungen noch nicht committed oder veröffentlicht, keine neue Datenmigration, kein neuer Lighthouse-Gesamtlauf. + +## Produktionsdiagnose: Bewegung in Chrome unterdrückt (14.09.2026) + +**Bestätigte Ursache:** Umut meldet für `https://web.kant-it.de/` in seinem Chrome `matchMedia('(prefers-reduced-motion: reduce)').matches === true`. Damit deaktivieren die vorhandenen CSS-/JavaScript-Regeln Einblendungen, Hover-Bewegungen und Slider-Automatik. Die öffentliche Karte `leistung-app` trägt tatsächlich `fx-hover-lift fx-entrance-left fx-delay-short`; die frühere Diagnose allein anhand der lokalen Pilotdaten war für Produktion unzureichend. + +**Geprüft:** Produktions-HTML und Effektdateien HTTP 200; `motion.js?v=1.10.0` entspricht dem lokalen Modul, ausgeliefertes CSS enthält die Hover-Regel samt Bewegungsschranke. Im Codex-Browser ebenfalls reduzierte Bewegung und deaktivierte Slider-Automatik gemessen, keine JavaScript-Warnungen/-Fehler. Direkter Zugriff auf Umuts Chrome wurde von der Browsersteuerung wegen nicht sicher ermittelbarer URL gestoppt; den Wert aus seinem Browser hat Umut selbst bestätigt. + +**Testverbesserung:** `scripts/motion-check.mjs` prüft zusätzlich den gespeicherten Hover-Effekt anhand der berechneten Verschiebung von −4 px und protokolliert die Bewegungsvorgabe separat ohne Medienemulation (`reducedMotion: null`). Optional installiertes Chrome über `MOTION_BROWSER_CHANNEL=chrome`; dieser Lauf auf einer isolierten Pilotkopie war bei 1600/390 px erfolgreich. Ein frisches Testprofil ersetzt weiterhin keine Messung im Benutzerprofil. + +**Einordnung:** Der später bereitgestellte Screenshot und die Systemabfrage zeigen, dass Windows-Animationen bereits eingeschaltet sind (siehe neuerer Eintrag oben). Die Herkunft der Browsermeldung bleibt ungeklärt; keine erfolgreiche Wiedergabe im normalen Chrome-Profil bestätigt. Keine Systemoption, Produktionsdatei oder gespeicherter Inhalt geändert; kein neues Release. + ## Prüfung der Scroll-Einblendungen mit Kant-IT-Daten (14.09.2026) **Befund:** Die Startseite in `var/kantit.db` enthält in Entwurf und Veröffentlichung 14 Einblendungen, ausschließlich `rise` und `fade`. Keine seitlichen Effekte an den Startseitenelementen gespeichert. `rise` bewegt den Block 28 Pixel bei voller Deckkraft; überwiegend sind ganze Sektionen animiert. Der bestehende Renderer lädt `motion.js`, und der Observer löst beim Scrollen aus. Im Editor erfordert die Wiedergabe „Bewegung testen“; reduzierte Systembewegung unterdrückt sie. diff --git a/public/assets/editor.js b/public/assets/editor.js index 1b04323..c0d906f 100644 --- a/public/assets/editor.js +++ b/public/assets/editor.js @@ -32,6 +32,10 @@ const collapsed = new Set(); let renderedProperties = null; let revealAfterPreview = null; let previewPlayback = false, stopPlayback = () => {}; +const motionPreference = window.matchMedia('(prefers-reduced-motion: reduce)'); +const showMotionStatus = () => { $('#motion-status').hidden = !motionPreference.matches; }; +motionPreference.addEventListener('change', showMotionStatus); +showMotionStatus(); function playPreview() { stopPlayback(); stopPlayback = () => {}; diff --git a/public/assets/motion.js b/public/assets/motion.js index bc7a84a..3fc7e11 100644 --- a/public/assets/motion.js +++ b/public/assets/motion.js @@ -26,10 +26,19 @@ export function initMotion(doc = document) { const observer = new view.IntersectionObserver(entries => { for (const entry of entries) if (entry.isIntersecting) { show(entry.target); observer.unobserve(entry.target); } }, { threshold: 0, rootMargin: '0px 0px -24px 0px' }); - if (!preference.matches) nodes.forEach(node => observer.observe(node)); - const reduce = () => { if (preference.matches) { observer.disconnect(); animations.forEach(animation => animation.cancel()); } }; - preference.addEventListener('change', reduce); - return () => { observer.disconnect(); animations.forEach(animation => animation.cancel()); preference.removeEventListener('change', reduce); }; + const syncPreference = () => { + if (preference.matches) { + observer.disconnect(); + animations.forEach(animation => animation.cancel()); + } else { + // Reduction may have been active at startup or enabled during this visit. + // Resume observing unseen content when movement is allowed again. + nodes.forEach(node => { if (!seen.has(node)) observer.observe(node); }); + } + }; + preference.addEventListener('change', syncPreference); + syncPreference(); + return () => { observer.disconnect(); animations.forEach(animation => animation.cancel()); preference.removeEventListener('change', syncPreference); }; } if (!document.querySelector('#editor')) initMotion(); diff --git a/public/assets/site.css b/public/assets/site.css index af63754..072bc93 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -56,7 +56,7 @@ p{hyphens:auto} .quote-rating{margin:0;letter-spacing:3px} .block-logos{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));align-items:center;gap:24px} .block-logos img{max-height:52px;width:auto;margin-inline:auto;display:block} -.kant .block-form button[type=submit]{background:var(--accent);color:#fff;border-color:var(--accent)}.kant .block-form button[type=submit]:hover{background:color-mix(in srgb,var(--accent) 86%,#000)}.kant .block-cta .button{background:var(--accent);color:#fff;border-color:var(--accent)}.kant .block-cta .button:hover{background:color-mix(in srgb,var(--accent) 86%,#000)}.block-cta{display:flex;flex-wrap:wrap;gap:20px 34px;align-items:center;justify-content:space-between;padding:32px;border:1px solid currentColor;border-radius:14px} +.kant .block-cta .button{background:var(--accent);color:#fff;border-color:var(--accent)}.kant .block-cta .button:hover{background:color-mix(in srgb,var(--accent) 86%,#000)}.block-cta{display:flex;flex-wrap:wrap;gap:20px 34px;align-items:center;justify-content:space-between;padding:32px;border:1px solid currentColor;border-radius:14px} .kant .block-cta h2{margin:0 0 6px;font-size:26px;letter-spacing:-.6px} .block-cta p{margin:0;line-height:1.6} .block-contact{display:grid;gap:14px;font-style:normal} @@ -87,6 +87,7 @@ p{hyphens:auto} /* Forms. Server rendered, no JavaScript; every message sits at its field. */ .block-form{display:grid;gap:20px;max-width:640px} +.block-form>.cms-button{justify-self:start} .form-field{display:grid;gap:7px} .form-field label{font-weight:600;font-size:14px} .form-field input,.form-field textarea,.form-field select{width:100%;padding:12px 13px;border:1px solid currentColor;border-radius:8px;background:#fff;color:inherit;font:inherit;min-height:46px} diff --git a/scripts/button-check.mjs b/scripts/button-check.mjs index a4bb64d..807df2a 100644 --- a/scripts/button-check.mjs +++ b/scripts/button-check.mjs @@ -16,6 +16,10 @@ const server = spawn(php, ['-d', 'memory_limit=256M', '-S', `127.0.0.1:${port}`, const report = 'reports/buttons'; await mkdir(report, { recursive: true }); let browser, page; const checks = [], errors = []; +const buttonStyle = node => { + const style = getComputedStyle(node); + return Object.fromEntries(['backgroundColor', 'color', 'borderRadius', 'borderColor', 'borderWidth', 'borderStyle', 'paddingTop', 'paddingRight', 'fontFamily', 'fontSize', 'fontWeight', 'letterSpacing', 'textTransform', 'lineHeight', 'textAlign'].map(key => [key, style[key]])); +}; try { for (let attempt = 0; attempt < 60; attempt++) { @@ -83,12 +87,49 @@ try { const plain = await browser.newContext({ javaScriptEnabled: false }); const publicPage = await plain.newPage(); for (const [width, radius, transparent] of [[1600, '8px', false], [900, '0px', true], [390, '999px', false]]) { await publicPage.setViewportSize({ width, height: 900 }); await publicPage.goto(origin); + const necessaryCookies = publicPage.getByRole('button', { name: 'Nur notwendige', exact: true }); + if (await necessaryCookies.count()) await necessaryCookies.click(); const values = await publicPage.locator('#qa-button').evaluate(el => { const s=getComputedStyle(el);return { radius:s.borderRadius, bg:s.backgroundColor, font:s.fontSize }; }); assert.equal(values.radius, radius); assert.equal(values.bg === 'rgba(0, 0, 0, 0)', transparent); assert.equal(values.font, '17px'); assert.equal(await publicPage.locator('#qa-button-default').evaluate(el => getComputedStyle(el).fontSize), '18px'); + const submit = publicPage.locator('.block-form button[type=submit]').first(); + const standard = publicPage.locator('#qa-button-default'); + assert.deepEqual(await submit.evaluate(buttonStyle), await standard.evaluate(buttonStyle), 'Form submit uses global shape, appearance, size, typography and colors'); + assert.ok(await submit.evaluate(node => node.getBoundingClientRect().width < node.form.getBoundingClientRect().width), 'Submit keeps natural button width'); + await submit.hover(); await publicPage.waitForTimeout(350); + const hoverStyle = await submit.evaluate(buttonStyle); + await standard.hover(); await publicPage.waitForTimeout(350); + assert.deepEqual(hoverStyle, await standard.evaluate(buttonStyle), 'Submit inherits the global hover colors'); + await submit.focus(); + assert.equal(await submit.evaluate(node => getComputedStyle(node).outlineStyle), 'solid', 'Submit has a keyboard focus indicator'); + await submit.screenshot({ path: `${report}/submit-${width}.png` }); await publicPage.locator('#qa-buttons-section').screenshot({ path: `${report}/public-${width}.png` }); } - await plain.close(); checks.push('published global defaults and local responsive variants without JavaScript'); + await page.goto(origin + '/admin/theme'); + await page.locator('#buttons-shape').selectOption('rounded'); + await page.locator('#buttons-appearance').selectOption('solid'); + await page.locator('#buttons-size').selectOption('large'); + await page.locator('#buttons-fontSize').fill('20'); + for (const [key, hex] of [['backgroundColor', '#124b36'], ['color', '#ffffff'], ['hoverBackgroundColor', '#092c20'], ['hoverColor', '#fff4cc']]) { + await page.locator(`#buttons-${key}`).click(); + await page.locator(`#buttons-${key}-hex`).fill(hex); + await page.locator(`#buttons-${key}-hex`).press('Enter'); + await page.keyboard.press('Escape'); + } + await page.getByRole('button', { name: 'Design speichern', exact: true }).click(); + await publicPage.goto(origin); + const updatedSubmit = publicPage.locator('.block-form button[type=submit]').first(); + const updatedDefault = publicPage.locator('#qa-button-default'); + assert.deepEqual(await updatedSubmit.evaluate(buttonStyle), await updatedDefault.evaluate(buttonStyle), 'A later change to solid global defaults updates existing forms'); + assert.equal(await updatedSubmit.evaluate(el => getComputedStyle(el).backgroundColor), 'rgb(18, 75, 54)'); + await updatedSubmit.hover(); await publicPage.waitForTimeout(350); + const updatedHover = await updatedSubmit.evaluate(buttonStyle); + await updatedDefault.hover(); await publicPage.waitForTimeout(350); + assert.deepEqual(updatedHover, await updatedDefault.evaluate(buttonStyle), 'Custom global hover colors update existing forms'); + await publicPage.setViewportSize({ width: 390, height: 1600 }); + await updatedSubmit.scrollIntoViewIfNeeded(); + await publicPage.locator('.block-form').first().screenshot({ path: `${report}/form-standard-button.png` }); + await plain.close(); checks.push('published global defaults, form submit including hover/focus/natural width, solid and outline theme changes, and local responsive variants without JavaScript'); assert.deepEqual(errors, []); await writeFile(`${report}/result.json`, JSON.stringify({ checks, passed: true }, null, 2)); console.log('Button checks passed:', checks.length, 'groups.'); diff --git a/scripts/motion-check.mjs b/scripts/motion-check.mjs index fa0125a..a33562d 100644 --- a/scripts/motion-check.mjs +++ b/scripts/motion-check.mjs @@ -10,6 +10,7 @@ const hash = async () => createHash('sha256').update(await readFile('var/kantit. const before = await hash(); 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'], { env, windowsHide: true, stdio: 'ignore' }); 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}`; @@ -23,7 +24,13 @@ try { if (attempt === 59) throw new Error('Pilot server did not start'); await new Promise(resolve => setTimeout(resolve, 200)); } - browser = await chromium.launch(); + browser = await chromium.launch(process.env.MOTION_BROWSER_CHANNEL ? { channel: process.env.MOTION_BROWSER_CHANNEL } : {}); + const systemContext = await browser.newContext({ reducedMotion: null }); + const systemPage = await systemContext.newPage(); + await systemPage.goto(origin); + const systemMotion = await systemPage.evaluate(() => ({ reduced: matchMedia('(prefers-reduced-motion: reduce)').matches, userAgent: navigator.userAgent })); + console.log('Default browser motion preference:', JSON.stringify(systemMotion)); + await systemContext.close(); for (const width of [1600, 390]) { const context = await browser.newContext({ viewport: { width, height: 900 }, reducedMotion: 'no-preference' }); await context.addInitScript(() => { @@ -40,6 +47,12 @@ try { const inventory = await page.locator('[class*="fx-entrance-"]').evaluateAll(nodes => nodes.map(node => ({ id: node.id, classes: node.className }))); assert.ok(inventory.length > 0, 'Pilot has entrance effects'); assert.equal(await page.locator('script[src*="motion.js"]').count(), 1); + const hoverTarget = page.locator('#leistung-app'); + await hoverTarget.scrollIntoViewIfNeeded(); await page.waitForTimeout(800); + await hoverTarget.hover(); await page.waitForTimeout(350); + const hovered = await hoverTarget.evaluate(node => ({ transform: getComputedStyle(node).transform, matches: node.matches(':hover'), classes: node.className })); + console.log('Pilot hover:', width, JSON.stringify(hovered)); + assert.equal(hovered.transform, 'matrix(1, 0, 0, 1, 0, -4)', 'Saved lift effect changes the rendered transform'); const target = page.locator('#kontakt'); assert.equal(await page.evaluate(() => motionCalls.some(call => call.id === 'kontakt')), false, 'Below-fold section must wait for scrolling'); await target.scrollIntoViewIfNeeded(); @@ -76,7 +89,26 @@ try { await page.reload(); await target.scrollIntoViewIfNeeded(); await page.waitForTimeout(800); assert.equal(await page.evaluate(() => motionCalls.length), 0, 'Reduced motion suppresses animations'); assert.equal(await target.evaluate(node => getComputedStyle(node).opacity), '1'); - results.push({ width, inventory, calls, checks: ['scroll trigger', 'once only', 'left/right computed movement', 'reduced motion'] }); + await hoverTarget.hover(); await page.waitForTimeout(350); + assert.equal(await hoverTarget.evaluate(node => getComputedStyle(node).transform), 'none', 'Reduced motion suppresses hover movement'); + await target.scrollIntoViewIfNeeded(); + await page.emulateMedia({ reducedMotion: 'no-preference' }); + await page.waitForFunction(() => motionCalls.some(call => call.id === 'kontakt'), null, { timeout: 3000 }); + await page.waitForTimeout(800); + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.emulateMedia({ reducedMotion: 'no-preference' }); + await page.waitForTimeout(100); + assert.equal(await page.evaluate(() => motionCalls.filter(call => call.id === 'kontakt').length), 1, 'Preference changes do not replay an already seen element'); + await hoverTarget.hover(); await page.waitForTimeout(350); + assert.equal(await hoverTarget.evaluate(node => getComputedStyle(node).transform), 'matrix(1, 0, 0, 1, 0, -4)', 'Hover resumes after allowing motion'); + // Also cover a page that starts normally, then suppresses motion before reaching its content. + await page.reload(); await page.waitForTimeout(800); + await page.emulateMedia({ reducedMotion: 'reduce' }); + await target.scrollIntoViewIfNeeded(); await page.waitForTimeout(100); + assert.equal(await page.evaluate(() => motionCalls.filter(call => call.id === 'kontakt').length), 0); + await page.emulateMedia({ reducedMotion: 'no-preference' }); + await page.waitForFunction(() => motionCalls.some(call => call.id === 'kontakt'), null, { timeout: 3000 }); + results.push({ width, inventory, calls, checks: ['scroll trigger', 'once only', 'left/right computed movement', 'saved hover lift', 'reduced motion', 'resume without reloading after initial and subsequent reduced motion', 'hover resumes after preference change'] }); await context.close(); } const noJs = await browser.newContext({ javaScriptEnabled: false }); @@ -84,9 +116,25 @@ try { assert.ok(await page.locator('main').innerText()); assert.equal(await page.locator('#kontakt').evaluate(node => getComputedStyle(node).opacity), '1'); await noJs.close(); + const editorContext = await browser.newContext({ reducedMotion: 'reduce', viewport: { width: 1600, height: 1000 } }); + const editor = await editorContext.newPage(); editor.on('pageerror', error => errors.push(error.message)); + await editor.goto(origin + '/admin/login'); + await editor.getByLabel('Benutzername').fill(fixture.username); + await editor.getByLabel('Passwort', { exact: true }).fill(fixture.password); + await editor.getByRole('button', { name: 'Anmelden', exact: true }).click(); + await editor.waitForURL(origin + '/admin'); + await editor.goto(`${origin}/admin/pages/${fixture.homeId}`); + await editor.locator('#motion-status').waitFor({ state: 'visible' }); + await editor.locator('#preview-status').filter({ hasText: 'Entwurf ·' }).waitFor(); + await editor.screenshot({ path: `${report}/editor-motion-status.png` }); + await editor.emulateMedia({ reducedMotion: 'no-preference' }); + await editor.locator('#motion-status').waitFor({ state: 'hidden' }); + await editor.emulateMedia({ reducedMotion: 'reduce' }); + await editor.locator('#motion-status').waitFor({ state: 'visible' }); + await editorContext.close(); assert.deepEqual(errors, []); assert.equal(await hash(), before, 'Original database unchanged'); - await writeFile(`${report}/result.json`, JSON.stringify({ results, errors, noJavaScript: true, originalUnchanged: true }, null, 2)); + await writeFile(`${report}/result.json`, JSON.stringify({ systemMotion, results, errors, editorMotionStatus: true, noJavaScript: true, originalUnchanged: true }, null, 2)); console.log(JSON.stringify({ passed: true, devices: results.map(result => result.width), effects: results[0].inventory, report })); } finally { await browser?.close(); server.kill(); diff --git a/templates/admin/editor.html.twig b/templates/admin/editor.html.twig index e892d21..92216b1 100644 --- a/templates/admin/editor.html.twig +++ b/templates/admin/editor.html.twig @@ -38,6 +38,7 @@
Dein Browser meldet „Bewegung reduzieren“. Einblendungen, Hover-Bewegungen und Slider-Automatik sind deshalb deaktiviert.
Vorschau wird geladen …