OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

PageEditorAssetsTest.php

7 KB
<?php

declare(strict_types=1);

/**
 * Hält die Editor-Vorlage und ihre ausgelagerten Dateien zusammen.
 *
 * Bedienung und Gestaltung des Seiten-Editors liegen in
 * `public/assets/js/page-editor.js` und `public/assets/css/page-editor.css`.
 * Diese Trennung fällt nicht auf, wenn sie bricht: Eine fehlende Einbindung
 * erzeugt keinen Fehler beim Ausliefern, sondern einen Editor ohne Funktion.
 * Ebenso bleibt zurückgerutschtes Inline-JS unbemerkt – es macht die Vorlage
 * nur wieder unübersichtlich.
 */
return static function (): void {

    $wurzel   = dirname(__DIR__);
    $vorlage  = (string)file_get_contents($wurzel . '/templates/pages/edit.php');
    $skript   = (string)file_get_contents($wurzel . '/public/assets/js/page-editor.js');
    $stil     = (string)file_get_contents($wurzel . '/public/assets/css/page-editor.css');

    // ----------------------------------------------------------------
    // Die Trennung hält
    // ----------------------------------------------------------------

    test('Die Vorlage bindet Skript und Stil ein, statt sie mitzuführen', function () use ($vorlage) {
        assertTrue(str_contains($vorlage, '/assets/js/page-editor.js'), 'Das Skript wird nicht geladen');
        assertTrue(str_contains($vorlage, '/assets/css/page-editor.css'), 'Der Stil wird nicht geladen');

        // Ein `<script>` ohne `src` wäre wieder Inline-Code. Erlaubt bleibt
        // der JSON-Block mit den Angaben für das Skript.
        preg_match_all('/<script(?![^>]*\bsrc=)(?![^>]*application\/json)[^>]*>/i', $vorlage, $inline);
        assertSame(0, count($inline[0]), 'Die Vorlage führt wieder eigenes JavaScript');

        assertFalse(str_contains($vorlage, '<style>'), 'Die Vorlage führt wieder eigenes CSS');
    });

    test('Das Skript kommt ohne serverseitigen Code aus', function () use ($skript, $stil) {
        // Eine statisch ausgelieferte Datei wird nie durch PHP gereicht.
        assertFalse(str_contains($skript, '<?'), 'Im Skript steht noch PHP');
        assertFalse(str_contains($stil, '<?'), 'Im Stil steht noch PHP');
    });

    // ----------------------------------------------------------------
    // Markup ruft nur auf, was es auch gibt
    // ----------------------------------------------------------------

    test('Jede aus dem Markup gerufene Funktion hängt am window', function () use ($vorlage, $skript) {
        preg_match_all('/\bon(?:click|change|submit|input)="([a-zA-Z_][A-Za-z0-9_]*)\s*\(/', $vorlage, $treffer);
        $gerufen = array_unique($treffer[1]);
        assertTrue($gerufen !== [], 'Keine Aufrufe gefunden – Suchmuster passt nicht mehr');

        foreach ($gerufen as $name) {
            assertTrue(
                str_contains($skript, 'window.' . $name . ' ='),
                "Aus dem Markup gerufen, aber nicht am window: {$name}()"
            );
        }
    });

    // ----------------------------------------------------------------
    // Die Angaben des Servers erreichen das Skript
    // ----------------------------------------------------------------

    test('Was das Skript erwartet, liefert die Vorlage auch', function () use ($vorlage, $skript) {
        assertTrue(
            str_contains($vorlage, 'id="page-editor-data"'),
            'Der Datenblock fehlt in der Vorlage'
        );
        assertTrue(
            str_contains($skript, "getElementById('page-editor-data')"),
            'Das Skript liest den Datenblock nicht'
        );

        // Jeder Schlüssel, den das Skript liest, wird auch geschrieben.
        foreach (['isEdit', 'tinymce', 'aiEnabled', 'canDelete', 'pageId', 'draftContent'] as $feld) {
            assertTrue(str_contains($skript, 'cfg.' . $feld), "Das Skript nutzt {$feld} nicht mehr");
            assertTrue(str_contains($vorlage, "'" . $feld . "'"), "Die Vorlage liefert {$feld} nicht");
        }
    });

    test('Der Datenblock verlässt seinen eigenen Block nicht', function () use ($vorlage) {
        // Ohne Maskierung könnte ein Seiteninhalt den JSON-Block schließen.
        assertTrue(str_contains($vorlage, 'JSON_HEX_TAG'), 'Spitze Klammern werden nicht maskiert');
        assertTrue(str_contains($vorlage, 'JSON_HEX_QUOT'), 'Anführungszeichen werden nicht maskiert');
        assertTrue(str_contains($vorlage, 'JSON_HEX_AMP'), 'Kaufmanns-Und wird nicht maskiert');
    });

    // ----------------------------------------------------------------
    // TinyMCE steht vor dem Skript, das es aufruft
    // ----------------------------------------------------------------

    test('TinyMCE wird geladen, bevor das Editor-Skript es benutzt', function () use ($wurzel, $vorlage, $skript) {
        assertTrue(str_contains($skript, 'tinymce.init('), 'Das Skript richtet TinyMCE nicht mehr ein');

        // Die Vorlage lädt TinyMCE im Rumpf, `extra_js` folgt erst danach.
        $layout = (string)file_get_contents($wurzel . '/templates/layouts/main.php');
        $inhalt = strpos($layout, '<?= $content ?>');
        $extra  = strpos($layout, 'extra_js');
        assertTrue($inhalt !== false && $extra !== false, 'Das Layout hat sich geändert');
        assertTrue($inhalt < $extra, 'Das Editor-Skript liefe vor TinyMCE');
        assertTrue(str_contains($vorlage, 'tinymce.min.js'), 'TinyMCE wird nicht mehr geladen');
    });

    test('Ein Fehler in einem Abschnitt reisst die uebrigen nicht mit', function () use ($skript) {
        // Vorher waren es vier getrennte Skriptbloecke; ein Fehler im einen
        // liess die anderen weiterlaufen. Das muss die eine Datei nachbilden.
        assertSame(
            4,
            preg_match_all("/abschnitt\('[^']+', function \(\) \{/", $skript),
            'Nicht jeder der vier Abschnitte ist abgeschirmt'
        );
        assertTrue(str_contains($skript, 'console.error'), 'Ein Fehler bliebe unsichtbar');

        // Die einzige Verbindung zwischen zwei Abschnitten liegt im
        // gemeinsamen Geltungsbereich, nicht in einem von beiden.
        assertTrue(
            (bool)preg_match('/^    var openAiEditorModal = /m', $skript),
            'Der KI-Dialog ist fuer den Editor-Abschnitt nicht erreichbar'
        );
    });

    // ----------------------------------------------------------------
    // Icons folgen dem Umschalter
    // ----------------------------------------------------------------

    test('Die Editor-Icons sind umschaltbar statt fest verdrahtet', function () use ($wurzel, $vorlage) {
        // Eine Emoji-Entity im Markup bleibt Emoji, gleich welche Einstellung gilt.
        preg_match_all('/&#\d{4,};/', $vorlage, $entities);
        assertSame(0, count($entities[0]), 'Im Markup stehen wieder fest verdrahtete Emoji');

        // Jedes verwendete Icon braucht seine Zuordnung, sonst bleibt es im
        // Emoji-Modus monochrom.
        $map = (string)file_get_contents($wurzel . '/src/Core/IconMap.php');
        preg_match_all('/fas (fa-[a-z0-9-]+)/', $vorlage, $treffer);
        $icons = array_unique($treffer[1]);
        assertTrue($icons !== [], 'Keine Icons gefunden – Suchmuster passt nicht mehr');

        foreach ($icons as $icon) {
            assertTrue(
                (bool)preg_match("/'" . preg_quote($icon, '/') . "'\s*=>/", $map),
                "Icon ohne Zuordnung in IconMap.php: {$icon}"
            );
        }
    });
};