Erscheinungsbild
End-to-End-Tests
Playwright prüft die Website so, wie sie ausgeliefert wird. Die Suite liegt unter tests/e2e/, die Konfiguration in playwright.config.ts.
sh
pnpm exec playwright install chromium # einmalig
pnpm run test:e2e # baut und prüftpnpm run test:e2e stößt den Build selbst an — den regulären und die Fixtures. Innerhalb von pnpm verify sind alle drei Ausgaben schon gebaut; die Kette ruft deshalb pnpm run test:e2e:built auf, das nur Playwright startet.
Gegen den Build, nicht gegen den Dev-Server
Genau das, was diese Suite prüft, entsteht erst beim Bauen: welches Skript vor der Consent-Entscheidung lädt (M5), welche Bildformate erzeugt wurden (M3b), was in der Sitemap steht (M4). Ein Dev-Server würde etwas anderes testen als das, was beim Kunden ankommt. Playwright startet deshalb astro preview auf den gebauten Ausgaben.
Vier Projekte, zwei Ausgaben
| Projekt | Ausgabe | Server | Port | Prüft |
|---|---|---|---|---|
starter | dist | astro preview | 4331 | alles außer Consent- und Formular-Szenarien |
consent | dist-fixture-full | astro preview | 4332 | consent.spec.ts |
forms | dist-fixture-full | astro preview | 4332 | forms.spec.ts |
forms-live | dist-fixture-full | php -S | 4333 | forms-live.spec.ts |
forms-live gibt es nur, solange server/forms/submit.php existiert. In einem Kundenprojekt ohne Standalone-PHP-Backend entfernt pnpm starter:init den Handler, und das Projekt verschwindet mit ihm — sonst wartete Playwright dort auf einen Server, den es nie geben wird.
Der Starter selbst hat weder Tracking noch ein Formular-Backend — er soll als Vorlage weder eine Container-ID noch einen Endpunkt mitbringen. Beide Szenarien brauchen aber eine Ausgabe, in der das tatsächlich konfiguriert ist. Das ist die Fixture „full", und sie läuft deshalb auf einem zweiten Vorschau-Server. Details unter Was geprüft ist.
Die Specs dürfen das Projekt nicht festschreiben
starter läuft gegen dist — also gegen die Ausgabe dieses Projekts. Genau darin liegt eine Falle, in die der Starter zunächst getappt ist: Specs, die „Kicktemp Astro Starter" als Titel, einen externen Navigationseintrag und „es gibt keine Formulare" behaupteten. In einem Kundenprojekt, das project.config.ts anpasst — also in jedem —, war pnpm verify damit rot, ohne dass an dem Projekt etwas nicht stimmte.
Deshalb gilt für smoke.spec.ts und navigation.spec.ts:
- Jede Erwartung kommt aus
project.config.tsoder gilt für jedes Projekt. Die Hilfen dafür stehen intests/e2e/support/projekt.ts. - Was ein Projekt abschalten kann, wird übersprungen statt behauptet. Kein Formular-Backend, keine optionale Consent-Kategorie, kein externer Navigationseintrag — dann entfällt der jeweilige Test mit einer Begründung, statt fehlzuschlagen.
- Kein Seitentitel, keine Beschriftung, kein Pfad wird ausgeschrieben. Impressum und Datenschutz sind die Ausnahme, weil das Schema sie zu Pflichtfeldern macht — aber auch sie kommen aus der Konfiguration, nicht aus dem Test.
Die Demo-Texte des Starters stehen in starter-demo.spec.ts — die einzige Datei, die konkrete Inhalte behauptet. pnpm starter:init entfernt sie zusammen mit den Demo-Inhalten.
Der Styleguide hat mit styleguide.spec.ts eine eigene Datei und bleibt: Er ist nicht Demo-Material, sondern die Prüffläche der a11y-Tests — die einzige Seite, auf der jedes Primitive in allen Varianten vorkommt, einschließlich Fehler- und Deaktiviert-Zustand.
Dasselbe Prinzip trägt an zwei anderen Stellen: Die Tests des PHP-Handlers benutzen eine eigene Allowlist (ADR 0010), und Feature-Flags werden an den Fixtures geprüft statt am Projekt (ADR 0002).
forms.spec.ts und forms-live.spec.ts schreiben keinen Feldnamen, keinen Optionswert und keine Schaltflächenbeschriftung aus: support/formular.ts liest das Formular aus src/content/forms.ts und die Seite, auf der es steht, aus der gebauten Fixture. Ein Projekt, das sein Kontaktformular umbaut, färbt diese Specs damit nicht rot.
In forms.spec.ts kommen die Antworten aus page.route() und nicht aus einem echten Backend. Das ist kein Notbehelf, sondern das Richtige: Geprüft wird die Zusage des Frontends, aus jeder vertragsgemäßen Antwort das Richtige zu machen. Eine abgeschnittene Antwort oder ein Netzwerkabbruch ließe sich gegen ein echtes Backend nur mit erheblichem Aufwand herstellen.
Der Durchstich
forms-live.spec.ts macht das Gegenstück: die gebaute Fixture hinter einem PHP-Server, der server/forms/submit.php unter genau der Adresse ausführt, die forms.endpoint nennt. Nichts wird abgefangen, nichts nachgebaut — und die erzeugte Benachrichtigung liest der Test aus der Ablage.
Den Grund liefert die Lücke zwischen den anderen beiden Ebenen: Die gemockte Suite prüft das Frontend gegen ein erfundenes Backend, PHPUnit den Handler ohne Browser. Ein Feldname, der nur auf einer Seite umbenannt wurde, oder ein Origin-Vergleich, der auf dem Papier stimmt, überlebt beide — und fällt erst dem ersten echten Absender auf.
Zwei Abweichungen von der Produktion sind unvermeidbar und bewusst gesetzt: Der Handler versendet nicht, sondern legt die Nachricht als .eml ab, und er prüft kein Captcha — die Fixture ist mit einem erfundenen Site Key gebaut, eine echte Prüfung bei Cloudflare könnte nie gelingen.
Der Lauf ist seriell, und das Ablageverzeichnis wird vor jedem Start geleert: Rate Limit und Doppelsubmissions sind Zustand auf dem Server. Parallele Tests würden einander die Zählerstände wegnehmen, und ein Lauf auf den Zählern des vorherigen prüfte etwas anderes, als er behauptet.
Voraussetzung
Dieses Projekt braucht PHP 8.3 im Pfad und die installierten Abhängigkeiten des Handlers. pnpm run test:e2e erledigt Letzteres selbst.
Kein fremder Server
reuseExistingServer steht auf false. Sonst liefe die Suite gegen das, was zufällig auf dem Port horcht — im schlimmsten Fall grün gegen eine völlig andere Anwendung. Belegt etwas einen der drei Ports, bricht der Lauf ab und sagt das.
Nur Chromium
Die Suite prüft Struktur, Barrierefreiheit und das Ladeverhalten von Skripten. In diesen Bereichen unterscheiden sich die Engines nicht. Ein zweiter Browser würde die Laufzeit jedes Pull Requests verdoppeln, ohne andere Fehler zu finden. Visuelle Abweichungen zwischen Engines gehören in die manuelle Abnahme. Begründung als ADR 0004.
Was aktuell geprüft wird
smoke.spec.ts
- Die Seiten laden und tragen genau eine
<h1> langstammt aus der Projektkonfiguration- Jede Seite hat eine nicht-leere Description
- Der Sprunglink ist das erste per Tab erreichbare Element, wird bei Fokus sichtbar und springt zum Inhalt
- Es geht keine Anfrage an einen fremden Host — Schriften sind selbst ausgeliefert
- Ohne Verbraucher erscheint der Consent-Dialog als Vorschau und bietet nur „Notwendig" an
- Ohne Formular-Backend wird kein Formularcode angefordert und nichts gespeichert
- Die 404-Seite ist erreichbar, trägt genau eine
<h1>undnoindex
starter-demo.spec.ts — nur im Starter, von pnpm starter:init entfernt
- Die Startseite des Starters nennt ihren Zweck
- Fehlerseite und Rechtsseiten tragen ihre Überschriften
styleguide.spec.ts — bleibt im Kundenprojekt
- Der Styleguide ist erreichbar und trägt
noindex - Die Button-Komponente erzeugt mit
hrefeinen Link, ohnehrefeine Schaltfläche
navigation.spec.ts
- Auf breiten Viewports sind die Einträge ohne Schaltfläche sichtbar
- Die aktuelle Seite trägt
aria-current="page" - Externe Ziele weisen für Screenreader auf den Seitenwechsel hin
- Die Aufklappnavigation öffnet und schließt über die Schaltfläche, über Escape (mit Fokusrückgabe) und über einen Klick daneben — und
aria-expandedstimmt in jedem Zustand - Impressum und Datenschutz sind von jeder Seite aus erreichbar und führen auf vorhandene Seiten
a11y.spec.ts
axe-core prüft jede gebaute Seite gegen WCAG 2.0/2.1 Level A und AA. Verstöße lassen den Lauf scheitern — es gibt keine Ausnahmeliste.
Welche Seiten das sind, steht nicht in der Spec, sondern in der Ausgabe: tests/e2e/support/ausgabe.ts liest die HTML-Dateien aus dist/ und leitet daraus die Adressen ab. Eine fest verdrahtete Liste wäre in einem Kundenprojekt entweder unvollständig — neue Seiten kämen ungeprüft dazu — oder falsch, sobald es eine der genannten Seiten nicht mehr gibt.
Ein zusätzlicher Lauf prüft den Styleguide mit geöffneter Aufklappnavigation. Dieser Zustand existiert nur zur Laufzeit und wäre in einer Prüfung des ausgelieferten HTML unsichtbar.
Der Styleguide ist hier der wichtigste Prüfpunkt: Er zeigt jedes Primitive in jeder Variante, sodass ein Aufruf die gesamte Komponentensammlung abdeckt.
consent.spec.ts (Projekt consent)
Die Szenarien der Einwilligung — keine Entscheidung, nur notwendige, Statistik, alles, Widerruf, zweiter Besuch, neue Fassungsnummer — samt axe-Prüfung von Banner und Einstellungsdialog. Die vollständige Liste steht unter Was geprüft ist.
forms.spec.ts (Projekt forms)
- Ein gültiges Absenden meldet den Text des Backends, leert das Formular und schickt genau das, was der Vertrag vorsieht
- Fehlende Pflichtfelder lösen keine Anfrage aus; der Fokus wandert in die Fehlerübersicht, und deren Einträge springen zum jeweiligen Feld
- Fehlertexte sind per
aria-describedbymit ihrem Feld verknüpft und verschwinden wieder - Feldfehler des Backends erscheinen genauso wie die aus dem Browser
- Rate Limit, Serverfehler, Antwort ohne JSON und Netzwerkabbruch werden unterschieden — bei einem Fehler bleiben die Eingaben erhalten
- Ein zweiter Versuch trägt dieselbe Übermittlungskennung
- Die Kampagne wird über Seitenwechsel hinweg gemerkt — aber nur mit Einwilligung
- Turnstile wird erst bei der ersten Berührung geladen, nicht beim Seitenaufruf
- axe prüft das Formular im Fehlerzustand, also mit Übersicht und Verknüpfungen, die erst zur Laufzeit entstehen
forms-live.spec.ts (Projekt forms-live)
- Eine Anfrage aus dem Browser geht durch den echten Handler und erzeugt die Benachrichtigung — mit Empfänger, Antwortadresse, Feldinhalten und dem Zeitpunkt der Einwilligung
- Ein gefüllter Honeypot führt zur neutralen Meldung des Handlers auf der Seite
- Ein fremder Origin wird abgewiesen, eine andere Methode als POST mit 405
- Feldfehler kommen in der Form des Vertrags zurück
- Dieselbe Übermittlungskennung ein zweites Mal erzeugt keine zweite Nachricht
- Das Rate Limit greift — mit eigener Absenderadresse über
X-Forwarded-For, damit der Test den übrigen nicht ihr Kontingent wegnimmt
Was axe nicht leistet
Automatisierte Prüfungen finden etwa ein Drittel der WCAG-Verstöße — Kontraste, fehlende Namen, kaputte Struktur. Ob eine Seite tatsächlich bedienbar und verständlich ist, entscheidet weiterhin die manuelle Abnahme.
In der CI
Der Workflow installiert vor pnpm verify nur Chromium (--with-deps für die Systembibliotheken des Runners) und richtet PHP 8.3 ein. Ein fehlgeschlagener Lauf lädt den Playwright-Bericht als Artefakt hoch — mit Trace des Wiederholungslaufs, sodass ein rotes Ergebnis ohne lokales Nachstellen nachvollziehbar ist.