OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

PwaManifestTest.php

7 KB
<?php

declare(strict_types=1);

/**
 * Das Manifest, mit dem ein Browser OpenNIT als App führt.
 *
 * Es kommt aus der Anwendung statt als Datei im Webroot: So trägt es den Namen,
 * den der Betreiber vergeben hat, und es liegt unter `.webmanifest` statt unter
 * `.json`. Serverhärtungen sperren im Webroot häufig pauschal `*.json`, um
 * `composer.json` und dergleichen zu verbergen – ein Manifest unter dieser
 * Endung wäre davon mitbetroffen und der Browser bekäme es nie zu sehen.
 */

use OpenNIT\Controllers\PwaController;

return static function (): void {

    $wurzel = dirname(__DIR__);
    $quelle = static fn (string $pfad): string => (string)file_get_contents($wurzel . '/' . $pfad);

    /**
     * Ruft das Manifest ab und gibt Inhalt samt gesetzter Kopfzeilen zurück.
     *
     * @return array{0:array<string,mixed>, 1:array<string,string>}
     */
    $abrufen = static function (string $appName): array {
        $controller = new class ($appName) extends PwaController {
            /** @var array<string,string> */
            public array $kopf = [];

            public function __construct(private string $appName)
            {
            }

            protected function db()
            {
                return null;
            }
        };

        // Die Anwendung liefert nur den Namen; alles Weitere steht im Manifest.
        $appKlasse = new ReflectionClass(OpenNIT\Core\Application::class);
        $app       = $appKlasse->newInstanceWithoutConstructor();
        $cfg       = $appKlasse->getProperty('config');
        $cfg->setAccessible(true);
        $cfg->setValue($app, ['app' => ['name' => $appName]]);

        $feld = new ReflectionProperty(PwaController::class, 'app');
        $feld->setAccessible(true);
        $feld->setValue($controller, $app);

        $response = new class extends OpenNIT\Core\Response {
            /** @var array<string,string> */
            public array $gesetzt = [];

            public function setHeader(string $name, string $value): self
            {
                $this->gesetzt[$name] = $value;
                return $this;
            }
        };

        ob_start();
        $controller->manifest(new OpenNIT\Core\Request(), $response);
        $roh = (string)ob_get_clean();

        return [(array)json_decode($roh, true), $response->gesetzt];
    };

    // ----------------------------------------------------------------

    test('Das Manifest ist gültiges JSON mit den Pflichtangaben', function () use ($abrufen) {
        [$m, $kopf] = $abrufen('OpenNIT');

        foreach (['name', 'short_name', 'start_url', 'display', 'icons'] as $feld) {
            assertTrue(isset($m[$feld]), 'Dem Manifest fehlt ' . $feld);
        }
        assertSame('standalone', $m['display']);
        assertTrue(count($m['icons']) >= 2, 'Es fehlen Symbole');

        foreach ($m['icons'] as $icon) {
            assertTrue(isset($icon['src'], $icon['sizes'], $icon['type']), 'Ein Symbol ist unvollständig');
            assertTrue(str_starts_with((string)$icon['src'], '/'), 'Ein Symbol zeigt nicht auf diese Installation');
        }
    });

    test('Es trägt den Namen, den der Betreiber vergeben hat', function () use ($abrufen) {
        [$m] = $abrufen('IT-Dokumentation Musterfirma');

        assertSame('IT-Dokumentation Musterfirma', $m['name']);

        // Die Beschreibung sagt, worum es geht – den Namen trägt das Manifest
        // bereits; zweimal hintereinander liest es sich wie ein Versehen.
        assertFalse(
            str_contains((string)$m['description'], (string)$m['name']),
            'Die Beschreibung wiederholt den Namen: ' . $m['description']
        );
        assertTrue(trim((string)$m['description']) !== '', 'Die Beschreibung fehlt');
    });

    test('Ein leerer Name fällt auf den Produktnamen zurück', function () use ($abrufen) {
        [$m] = $abrufen('   ');

        assertSame('OpenNIT', $m['name']);
    });

    test('Der Kurzname bleibt kurz genug für den Startbildschirm', function () use ($abrufen) {
        [$m] = $abrufen('Eine ausgesprochen lange Bezeichnung der Installation');

        assertTrue(mb_strlen((string)$m['short_name']) <= 12, 'Kurzname: ' . $m['short_name']);
    });

    test('Der Kurzname endet an einer Wortgrenze', function () use ($abrufen) {
        // Mitten im Wort abgeschnitten sieht der Name auf dem Startbildschirm
        // nach einem Fehler aus.
        [$m] = $abrufen('IT-Dokumentation Musterfirma');
        assertSame('IT-Dokumentation', $m['short_name'], 'Der Kurzname endet mitten im Wort');

        [$kurz] = $abrufen('Doku Haus GmbH');
        assertSame('Doku Haus', $kurz['short_name'], 'Es wurde nicht an der Wortgrenze gekürzt');

        // Ein kurzer Name bleibt unangetastet.
        [$knapp] = $abrufen('Doku');
        assertSame('Doku', $knapp['short_name']);

        // Reicht schon das erste Wort über die Grenze, bleibt es stehen – das
        // Betriebssystem kürzt dann selbst, mit Auslassungszeichen.
        [$lang] = $abrufen('Unternehmensdokumentation');
        assertSame('Unternehmensdokumentation', $lang['short_name']);
    });

    test('Der Medientyp ist der des Standards', function () use ($abrufen) {
        // Mit application/json weisen Browser das Manifest zurück.
        [, $kopf] = $abrufen('OpenNIT');

        assertTrue(
            str_starts_with($kopf['Content-Type'] ?? '', 'application/manifest+json'),
            'Content-Type: ' . ($kopf['Content-Type'] ?? '—')
        );
    });

    // ----------------------------------------------------------------
    // Wie es erreichbar ist
    // ----------------------------------------------------------------

    test('Der Pfad endet nicht auf .json', function () use ($quelle) {
        // Eine Härtung, die `*.json` im Webroot sperrt, nähme dem Browser sonst
        // das Manifest – ohne dass an OpenNIT etwas falsch wäre.
        $layout = $quelle('templates/layouts/main.php');

        assertTrue(
            str_contains($layout, '<link rel="manifest" href="/manifest.webmanifest">'),
            'Das Layout verweist nicht auf das Manifest unter seiner eigenen Endung'
        );
        assertFalse(
            str_contains($layout, 'href="/manifest.json"'),
            'Das Layout verweist weiterhin auf die .json-Fassung'
        );
    });

    test('Das Manifest ist ohne Anmeldung erreichbar', function () use ($quelle) {
        // Ein Browser holt es ohne Sitzungsdaten; hinter einer Anmeldung bliebe
        // es unerreichbar.
        $app = $quelle('src/Core/Application.php');

        assertTrue(
            (bool)preg_match(
                "/router->get\('\/manifest\.webmanifest',\s*'PwaController@manifest'\);/",
                $app
            ),
            'Die Route fehlt oder verlangt eine Anmeldung'
        );
    });

    test('Der veraltete Hinweis steht nicht mehr allein', function () use ($quelle) {
        // Browser melden `apple-mobile-web-app-capable` als überholt; die
        // allgemeine Schreibweise gehört daneben, die eigene bleibt für ältere
        // iOS-Fassungen stehen.
        $layout = $quelle('templates/layouts/main.php');

        assertTrue(
            str_contains($layout, '<meta name="mobile-web-app-capable" content="yes">'),
            'Die allgemeine Schreibweise fehlt'
        );
        assertTrue(
            str_contains($layout, '<meta name="apple-mobile-web-app-capable" content="yes">'),
            'Die Schreibweise für ältere iOS-Fassungen wurde entfernt'
        );
    });
};