Fassung 1.7.0: verlässliche Installation, übersichtliche Verwaltung und bedienbare Bewegung
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 10:49:54 +02:00
1 parent b164c4edfe
commit 495b26896e
55 files changed
+1632 -423

No files matched your search

+63 -36
View File
@@ -85,6 +85,43 @@ final class CmsTest extends TestCase
}
}
public function testEditorPaginationAndChoicesKeepTypesFiltersAndParentLookup(): void
{
$ids = [];
for ($i = 0; $i < 43; ++$i) {
$document = DemoContent::pages()['home'];
$document['title'] = 'Eintrag '.sprintf('%02d', $i);
$ids[] = $this->pages->createPage('eintrag-'.sprintf('%02d', $i), null, 'post', $document);
}
$first = $this->pages->editorPage('post');
$second = $this->pages->editorPage('post', 2);
self::assertSame(43, $first['total']);
self::assertCount(20, $first['items']);
self::assertCount(20, $second['items']);
self::assertSame([], array_intersect(array_column($first['items'], 'id'), array_column($second['items'], 'id')));
self::assertCount(3, $this->pages->editorPage('post', 999)['items']);
self::assertSame(3, $this->pages->editorPage('post', 999)['page']);
self::assertCount(43, $this->pages->editorPage('post', -1, 50)['items']);
self::assertSame(2, $this->pages->editorPage('page')['total']);
self::assertSame(1, $this->pages->editorPage('post', query: 'Eintrag 42')['total']);
self::assertSame(0, $this->pages->editorPage('post', query: '%')['total'], 'LIKE wildcards are literal search text.');
self::assertSame(0, $this->pages->editorPage('post', query: '_')['total']);
$saved = $this->pages->findForEditor($ids[0]);
$this->pages->publish($ids[0], $saved['draft_revision_id'], $saved['version']);
self::assertSame(1, $this->pages->editorPage('post', status: 'published')['total']);
self::assertSame(42, $this->pages->editorPage('post', status: 'draft')['total']);
$this->pages->saveDraft($ids[0], DemoContent::pages()['home'], $saved['version'] + 1);
self::assertSame(1, $this->pages->editorPage('post', status: 'changed')['total']);
$this->db->update('page', ['deleted_at' => gmdate('Y-m-d H:i:s')], ['id' => $ids[42]]);
self::assertSame(42, $this->pages->editorPage('post')['total']);
self::assertSame(1, $this->pages->editorPage('post', status: 'trash')['total']);
self::assertCount(20, $this->pages->choices(type: 'post'));
self::assertSame([], $this->pages->choices(id: $ids[42]));
self::assertCount(2, $this->pages->choices(parentsOnly: true));
self::assertSame('page-2', $this->pages->choices(id: 'page-2', parentsOnly: true)[0]['id']);
self::assertSame([], $this->pages->choices(id: $ids[1], parentsOnly: true));
}
public function testDocumentRoundTripAndDraftPrivacy(): void
{
$document = DemoContent::pages()['home'];
@@ -690,11 +727,8 @@ final class CmsTest extends TestCase
}
}
public function testASliderAdvancesByItselfOnlyWhenItWasAskedTo(): void
public function testSliderControlsLoadOnlyForPagesWithSliders(): void
{
// Autorotation braucht ein Skript - das einzige auf einer öffentlichen Seite außer den
// Tabs. Es kommt nur auf Seiten, die einen solchen Slider tatsächlich enthalten, und die
// Richtlinie bleibt sonst bei script-src 'none'.
$document = DemoContent::starter('Mit Slider');
$document['schemaVersion'] = 17;
$document['blocks'][] = ['id' => 'slider', 'type' => 'slider', 'props' => [
@@ -703,22 +737,26 @@ final class CmsTest extends TestCase
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
$ohne = $this->request('/');
self::assertStringNotContainsString('slider.js', $ohne->getContent());
self::assertStringContainsString("script-src 'none'", (string) $ohne->headers->get('Content-Security-Policy'));
self::assertStringContainsString('slider.js?v=', $ohne->getContent());
self::assertStringNotContainsString('data-autoplay=', $ohne->getContent());
self::assertStringContainsString("script-src 'self'", (string) $ohne->headers->get('Content-Security-Policy'));
$document['blocks'][array_key_last($document['blocks'])]['props']['autoplay'] = 7;
$revision = $this->pages->saveDraft('page-1', $document, $this->pages->findForEditor('page-1')['version']);
$this->pages->publish('page-1', $revision, $this->pages->findForEditor('page-1')['version']);
$mit = $this->request('/');
self::assertStringContainsString('<script src="/assets/slider.js?v=', $mit->getContent());
self::assertStringContainsString('<script type="module" src="/assets/slider.js?v=', $mit->getContent());
self::assertStringContainsString('data-autoplay="7"', $mit->getContent());
self::assertStringContainsString("script-src 'self'", (string) $mit->headers->get('Content-Security-Policy'));
// Das Skript hält sich an die Bewegungsschranke und hört beim ersten Eingriff auf.
$skript = (string) file_get_contents(__DIR__.'/../public/assets/slider.js');
self::assertStringContainsString("matchMedia('(prefers-reduced-motion: reduce)')", $skript);
self::assertStringContainsString("'pointerdown', 'keydown', 'touchstart', 'wheel'", $skript);
// Removing the block removes the public dependency and closes the script policy again.
array_pop($document['blocks']);
$revision = $this->pages->saveDraft('page-1', $document, $this->pages->findForEditor('page-1')['version']);
$this->pages->publish('page-1', $revision, $this->pages->findForEditor('page-1')['version']);
$plain = $this->request('/');
self::assertStringNotContainsString('slider.js', $plain->getContent());
self::assertStringContainsString("script-src 'none'", (string) $plain->headers->get('Content-Security-Policy'));
}
public function testTheSamePostsCanBeShownInThreeWays(): void
@@ -770,30 +808,19 @@ final class CmsTest extends TestCase
(new DocumentValidator())->validate($document);
}
public function testEntranceEffectsRunOnScrollWhereTheBrowserCanAndOnLoadOtherwise(): void
public function testEntranceEffectsLoadOnlyWhenUsedAndKeepTheirSelectionInHtml(): void
{
// Umut: „Effekte gibt es keine“. Es gab sie - aber nur beim Zeigen, nicht beim Laden, und
// eine Einblendung beim Laden ist unterhalb der Falz wertlos: sie ist vorbei, bevor
// jemand hinsieht. Mit einer Scroll-Zeitachse läuft dieselbe Animation, wenn der
// Abschnitt in den Blick kommt - ohne eine Zeile JavaScript.
$css = (string) file_get_contents(__DIR__.'/../public/assets/enhancements.css');
self::assertStringContainsString('@supports (animation-timeline: view())', $css);
self::assertStringContainsString('animation-timeline: view()', $css);
// Beides steht hinter der Bewegungsschranke, und der Rückfall ist die Fassung von vorher:
// ein Browser ohne Scroll-Zeitachse zeigt alles, nur eben beim Laden animiert.
$geschuetzt = explode('@media (prefers-reduced-motion: no-preference) {', $css);
self::assertStringNotContainsString('animation-timeline', $geschuetzt[0], 'Keine Bewegung außerhalb der Schranke.');
self::assertSame(1, substr_count($css, '@supports (animation-timeline: view())'));
// Der wichtigste Teil, und er ist mit Messen bezahlt: auf dem Scrollweg wird **keine
// Deckkraft** animiert. Eine Scroll-Zeitachse ist umkehrbar, also stünde alles unterhalb
// der Falz dauerhaft am Anfang der Kurve - bei Deckkraft 0,85 wird aus #3d4d5c ein
// #78838d, und damit 3,86:1 statt 7,1:1. axe hat genau das gemeldet.
self::assertStringContainsString('@keyframes kant-scroll-rise { from { transform: translateY(20px) } to { transform: none } }', $css);
self::assertStringContainsString('@keyframes kant-scroll-fade { from { transform: translateY(10px) } to { transform: none } }', $css);
$scrollteil = explode('@supports (animation-timeline: view())', $css)[1];
self::assertStringNotContainsString('opacity', explode('.fx-delay-long', $scrollteil)[0], 'Auf dem Scrollweg bleibt der Text voll deckend.');
$document = DemoContent::starter('Mit Bewegung');
$document['schemaVersion'] = 19;
$document['blocks'][0]['layout']['effects']['entrance'] = 'left';
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
$response = $this->request('/');
self::assertStringContainsString('fx-entrance-left', $response->getContent());
self::assertStringContainsString('motion.js?v=', $response->getContent());
self::assertStringContainsString("script-src 'self'", (string) $response->headers->get('Content-Security-Policy'));
unset($document['blocks'][0]['layout']['effects']['entrance']);
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 2), 3);
self::assertStringNotContainsString('motion.js?v=', $this->request('/')->getContent());
// Und die drei Beitragsvorlagen bringen die Einblendung von sich aus mit.
foreach (array_keys(PostLayouts::all()) as $key) {
@@ -3307,8 +3334,8 @@ final class CmsTest extends TestCase
foreach ($expected as $needle) {
self::assertStringContainsString($needle, $html, $type);
}
// No script is needed for any of them: the public page still forbids scripting.
self::assertStringContainsString("script-src 'none'", (string) $this->request('/')->headers->get('Content-Security-Policy'));
// Sliders receive controls; the other blocks keep a script-free public page.
self::assertStringContainsString('slider' === $type ? "script-src 'self'" : "script-src 'none'", (string) $this->request('/')->headers->get('Content-Security-Policy'));
}
/** @return iterable<string, array{string, array<string, mixed>}> */