Dokumentschema v17: Projektraster, verlinktes Bild, klebender Kopfbereich
Alles aus dem Nachbau von kant-it.de, den Umut durchgesehen hat. Eine Beitragsliste kann Datum, Autor, Auszug und Kategorie ausblenden und sieht damit aus wie ein Projektraster statt wie ein Blogarchiv. Beinahe lautlos danebengegangen: |default(true) macht in Twig aus einem gespeicherten false ein true - die Einstellung haette sich speichern lassen und nichts getan. Der Test prueft beide Richtungen. Ein Bild darf verlinken, damit ein Logo auf die Startseite zeigt; die Adresse geht durch dieselbe Pruefung wie jeder andere Link. Und der Kopfbereich kann beim Scrollen oben bleiben - als Einstellung der Website, weil nur das umschliessende Element kleben kann. Der groesste Fund steckt im Editor: die Liste bekannter Schemafassungen endete bei 12, waehrend Bausteine v13 bis v15 verlangen. In einem aktuellen Dokument liess sich damit kein Baustein mehr einfuegen, und Symbol, Fortschritt, Slider und externer Inhalt in gar keinem. Jetzt eine Zahl an einer Stelle, mit einem Test ueber jeden Typ und jede Fassung. Geprueft: 389 PHP-Tests unter 8.2 und 8.4, 18 JS-Tests, Editortest, Browserpruefung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
efa285c0d0
commit
70283db20a
16 files changed
+185
-12
No files matched your search
@@ -599,6 +599,93 @@ final class CmsTest extends TestCase
|
||||
self::assertSame(['count' => 5, 'category' => 'projekte', 'tag' => ''], $stored['blocks'][2]['props']);
|
||||
}
|
||||
|
||||
public function testALogoCanLinkToTheStartPageAndTheHeaderCanStick(): void
|
||||
{
|
||||
// Zwei Selbstverständlichkeiten, die fehlten: ein Bild ließ sich nicht verlinken - also
|
||||
// auch kein Logo auf die Startseite - und der Kopfbereich konnte beim Scrollen nicht oben
|
||||
// bleiben. Beides ist beim Nachbau von kant-it.de aufgefallen.
|
||||
$document = DemoContent::starter('Mit Logo');
|
||||
$document['schemaVersion'] = 17;
|
||||
$document['blocks'][] = ['id' => 'logo', 'type' => 'image', 'props' => [
|
||||
'src' => '/media/'.str_repeat('a', 32).'/480', 'alt' => 'Firmenlogo', 'decorative' => false,
|
||||
'width' => 480, 'height' => 120, 'href' => '/',
|
||||
]];
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||||
|
||||
$html = $this->request('/')->getContent();
|
||||
self::assertStringContainsString('<a class="image-link" href="/">', $html);
|
||||
self::assertStringContainsString('alt="Firmenlogo"', $html);
|
||||
|
||||
// Der Kopfbereich klebt erst, wenn die Einstellung es sagt.
|
||||
self::assertStringContainsString('class="site-header"', $html);
|
||||
$settings = $this->site->load();
|
||||
$this->site->save([...$settings, 'stickyHeader' => true, 'sameAs' => '', 'consentGroups' => $settings['consentGroups']]);
|
||||
self::assertStringContainsString('class="site-header is-sticky"', $this->request('/')->getContent());
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{string, bool}> */
|
||||
public static function imageLinks(): iterable
|
||||
{
|
||||
yield 'lokal' => ['/kontakt', true];
|
||||
yield 'Sprungmarke' => ['#kontakt', true];
|
||||
yield 'HTTPS' => ['https://beispiel.de/', true];
|
||||
yield 'Skript' => ['javascript:alert(1)', false];
|
||||
yield 'Datenadresse' => ['data:text/html,hi', false];
|
||||
yield 'schemalos' => ['//fremd.test', false];
|
||||
}
|
||||
|
||||
#[DataProvider('imageLinks')]
|
||||
public function testAnImageLinkFollowsTheSameRulesAsEveryOtherLink(string $href, bool $allowed): void
|
||||
{
|
||||
$document = DemoContent::starter('Bildlink');
|
||||
$document['schemaVersion'] = 17;
|
||||
$document['blocks'][] = ['id' => 'logo', 'type' => 'image', 'props' => [
|
||||
'src' => '/bild.webp', 'alt' => 'Logo', 'decorative' => false, 'width' => 400, 'height' => 100, 'href' => $href,
|
||||
]];
|
||||
if (!$allowed) {
|
||||
$this->expectException(InvalidDocument::class);
|
||||
}
|
||||
(new DocumentValidator())->validate($document);
|
||||
if ($allowed) {
|
||||
self::assertTrue(true, 'Erlaubte Adresse wird angenommen.');
|
||||
}
|
||||
}
|
||||
|
||||
public function testAPostListCanLookLikeAProjectGridInsteadOfABlogArchive(): void
|
||||
{
|
||||
// Beim Nachbau von kant-it.de fehlte genau das: das Projektraster dort zeigt Bild und
|
||||
// Titel, die Beitragsliste zeigte zusätzlich Datum, Autor, Auszug und Kategorie und sah
|
||||
// damit aus wie ein Blog. Was eine Karte zeigt, ist jetzt einstellbar.
|
||||
$this->publishedPost('projekt-eins', 'Projekt eins', '2026-02-01', ['Projekte'], []);
|
||||
$id = $this->db->fetchOne('SELECT id FROM page WHERE path = ?', ['projekt-eins']);
|
||||
$this->posts->saveMetadata($id, ['excerpt' => 'Ein Auszug, der im Raster stört.', 'author' => 'Umut', 'publishedAt' => '2026-02-01'], ['Projekte'], [], (int) $this->db->fetchOne('SELECT version FROM page WHERE id = ?', [$id]));
|
||||
|
||||
$document = DemoContent::starter('Startseite');
|
||||
$document['schemaVersion'] = 17;
|
||||
$document['blocks'][] = ['id' => 'raster', 'type' => 'postlist', 'props' => [
|
||||
'count' => 5, 'category' => 'projekte', 'tag' => '',
|
||||
'showMeta' => false, 'showExcerpt' => false, 'showTerms' => false,
|
||||
], 'children' => []];
|
||||
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
|
||||
|
||||
$html = $this->request('/')->getContent();
|
||||
self::assertStringContainsString('Projekt eins', $html, 'Der Titel bleibt.');
|
||||
self::assertStringNotContainsString('Ein Auszug, der im Raster stört.', $html);
|
||||
self::assertStringNotContainsString('von Umut', $html);
|
||||
self::assertStringNotContainsString('post-terms', $html);
|
||||
|
||||
// Und die Gegenprobe: ohne die Schalter bleibt alles wie vorher. Das ist der Fall, der
|
||||
// fast danebengegangen wäre - „false|default(true)" ist in Twig true, weil der Filter auch
|
||||
// auf leere Werte greift. Die Einstellung hätte sich speichern lassen und nichts getan.
|
||||
$document['blocks'][array_key_last($document['blocks'])]['props'] = ['count' => 5, 'category' => 'projekte', 'tag' => ''];
|
||||
$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']);
|
||||
$html = $this->request('/')->getContent();
|
||||
self::assertStringContainsString('Ein Auszug, der im Raster stört.', $html);
|
||||
self::assertStringContainsString('von Umut', $html);
|
||||
self::assertStringContainsString('post-terms', $html);
|
||||
}
|
||||
|
||||
public function testFeedListsPublishedPostsWithAbsoluteAddresses(): void
|
||||
{
|
||||
$this->publishedPost('erster', 'Erster Beitrag', '2026-02-01', ['Projekte'], []);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { EditorHistory, createBlock, locate, insertBlock, duplicateBlock, moveBlock, upgradeDocument, effectiveDesign, setDesign, insertPreset, insertRow, blockPath, insertMedia, setEffect, isPluginBlock, blockLabel, blockIcon, canContain, isContainer, setStyle, effectiveStyle, hoverStyle, resetStyleDevice, clearStyle, copyStyle } from '../public/assets/editor-model.js';
|
||||
import { EditorHistory, createBlock, locate, insertBlock, duplicateBlock, moveBlock, upgradeDocument, effectiveDesign, setDesign, insertPreset, insertRow, blockPath, insertMedia, setEffect, isPluginBlock, blockLabel, blockIcon, canContain, isContainer, setStyle, effectiveStyle, hoverStyle, resetStyleDevice, clearStyle, copyStyle, SCHEMA_VERSION } from '../public/assets/editor-model.js';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { designCss } from '../scripts/build-design.mjs';
|
||||
|
||||
@@ -332,3 +332,28 @@ test('the hero preset assembles a full screen section that centres its content',
|
||||
history.undo();
|
||||
assert.deepEqual(history.document, fixture());
|
||||
});
|
||||
|
||||
test('a current document still accepts new blocks, and every block type can be inserted', () => {
|
||||
// Der Fehler, den der Nachbau von kant-it.de gefunden hat: die Liste bekannter Schemafassungen
|
||||
// im Editor endete bei 12, während Bausteine schon v13 bis v15 verlangten. In einem aktuellen
|
||||
// Dokument ließ sich damit gar nichts mehr einfügen - und ein Symbol, ein Slider oder ein
|
||||
// externer Inhalt nirgends.
|
||||
for (const version of [1, 5, 12, 13, 15, 16, SCHEMA_VERSION]) {
|
||||
const history = new EditorHistory({ ...fixture(), schemaVersion: version });
|
||||
const id = history.change(doc => insertBlock(doc, 'iconcard', 's2'));
|
||||
assert.ok(id, `v${version} nimmt einen Baustein an`);
|
||||
assert.ok(history.document.schemaVersion >= Math.max(version, 6));
|
||||
}
|
||||
|
||||
// Jeder Typ, der eine Fassung anhebt, landet tatsächlich im Dokument.
|
||||
for (const [type, needed] of [['gallery', 3], ['postlist', 5], ['quote', 6], ['tabs', 7], ['form', 8], ['breadcrumb', 9], ['menu', 11], ['embed', 14], ['icon', 15], ['progress', 15], ['slider', 15]]) {
|
||||
const history = new EditorHistory(fixture());
|
||||
const id = history.change(doc => insertBlock(doc, type, 's2'));
|
||||
assert.ok(locate(history.document.blocks, id), `${type} wird eingefügt`);
|
||||
assert.ok(history.document.schemaVersion >= needed, `${type} hebt auf mindestens v${needed}`);
|
||||
}
|
||||
|
||||
// Eine erfundene Fassung bleibt abgewiesen.
|
||||
assert.throws(() => upgradeDocument({ schemaVersion: 99, blocks: [] }, 6), /Unbekanntes Dokumentschema/);
|
||||
assert.throws(() => upgradeDocument({ schemaVersion: 5, blocks: [] }, SCHEMA_VERSION + 1), /Unbekanntes Dokumentschema/);
|
||||
});
|
||||
Reference in new issue
Block a user