Browser-Erweiterungen automatisiert testen mit Playwright
Wie Sie eine Chrome-Erweiterung in Playwright laden, Popup, Content Scripts und Service Worker in End-to-End-Tests prüfen und die Tests in einer CI-Pipeline laufen lassen.
Browser-Erweiterungen testen Sie mit Playwright, indem Sie Chromium mit einem persistenten Kontext und den Startparametern --disable-extensions-except und --load-extension starten. Danach ermitteln Sie die Erweiterungs-ID über den Service Worker, öffnen Popup oder Optionsseite unter chrome-extension://<id>/ und prüfen Content Scripts auf einer gewöhnlichen Testseite. Das Ganze lässt sich in einer CI-Pipeline automatisieren.
Gerade bei Erweiterungen lohnen sich automatisierte Tests besonders. Browser-Updates, geänderte Webseiten und neue Store-Regeln können eine funktionierende Erweiterung über Nacht beschädigen. Ein Test, der die wichtigsten Abläufe in einem echten Browser durchspielt, zeigt das, bevor Nutzer es tun.
Was Sie testen können und womit
| Teil der Erweiterung | Testart | Werkzeug |
|---|---|---|
| reine Logik (Parser, Regeln, Formatierung) | Unit-Test | Vitest oder Jest mit Attrappen für chrome.* |
| Popup, Optionsseite | End-to-End | Playwright, Seite über chrome-extension:// |
| Content Script auf Webseiten | End-to-End | Playwright mit lokaler Testseite |
| Service Worker, Nachrichten, Speicher | End-to-End | Playwright, Code im Service-Worker-Kontext ausführen |
| Firefox-spezifisches Verhalten | manuell oder Selenium | web-ext, geckodriver |
Wie Popup, Service Worker und Content Scripts zusammenspielen, beschreibt der Beitrag Content Scripts, Service Worker, Popup. Für die Tests ist diese Trennung wichtig, denn jeder Teil wird auf anderem Weg angesprochen.
Einrichtung
Installieren Sie Playwright Test im Projekt und laden Sie den mitgelieferten Chromium herunter:
npm init playwright@latest
npx playwright install chromium
Testen Sie immer die gebaute Erweiterung, also den Ausgabeordner Ihres Build-Werkzeugs, nicht den Quellcode. Ein Build-Setup mit TypeScript und Vite beschreibt der Artikel TypeScript und Vite für Erweiterungen einrichten. Bauen Sie vor jedem Testlauf neu, etwa über ein npm-Skript "pretest": "npm run build".
Eine Fixture, die die Erweiterung lädt
Playwright Test arbeitet mit Fixtures: wiederverwendbaren Bausteinen, die jeder Test anfordern kann. Die folgende Fixture startet Chromium mit der Erweiterung und stellt deren ID bereit:
// tests/fixtures.ts
import { test as base, chromium, type BrowserContext } from "@playwright/test";
import path from "node:path";
export const test = base.extend<{ context: BrowserContext; extensionId: string }>({
context: async ({}, use) => {
const pfad = path.join(__dirname, "..", "dist");
const context = await chromium.launchPersistentContext("", {
channel: "chromium",
args: [
`--disable-extensions-except=${pfad}`,
`--load-extension=${pfad}`,
],
});
await use(context);
await context.close();
},
extensionId: async ({ context }, use) => {
let [worker] = context.serviceWorkers();
if (!worker) worker = await context.waitForEvent("serviceworker");
const id = worker.url().split("/")[2];
await use(id);
},
});
export const expect = test.expect;
Drei Punkte sind entscheidend:
- Persistenter Kontext. Erweiterungen lassen sich nur in einem Kontext mit Benutzerprofil laden. Ein leerer String als Profilpfad erzeugt ein temporäres Profil, das nach dem Test verschwindet.
- Kanal
chromium. Damit nutzt Playwright den mitgelieferten Chromium im neuen Headless-Modus, der Erweiterungen unterstützt. Für die Fehlersuche setzen Sie zusätzlichheadless: falseund sehen den Browser. - ID aus dem Service Worker. Die Adresse des Service Workers hat die Form
chrome-extension://<id>/background.js. Der dritte Teil nach dem Aufteilen am Schrägstrich ist die ID.
Wer eine feste ID braucht, etwa weil eine Webseite mit der Erweiterung kommuniziert, kann einen öffentlichen Schlüssel im Feld key des Manifests hinterlegen. Für reine Tests ist das selten nötig.
Popup und Optionsseite testen
Das Popup ist eine normale HTML-Seite der Erweiterung. Playwright öffnet sie direkt als Tab:
// tests/popup.spec.ts
import { test, expect } from "./fixtures";
test("Popup speichert eine Notiz", async ({ page, extensionId }) => {
await page.goto(`chrome-extension://${extensionId}/popup.html`);
await page.getByLabel("Notiz").fill("Testnotiz");
await page.getByRole("button", { name: "Speichern" }).click();
await expect(page.getByText("Gespeichert")).toBeVisible();
});
Ein Unterschied bleibt: Als Tab geöffnet, kennt das Popup keinen „aktiven Tab“ im üblichen Sinn, und es schließt sich nicht automatisch. Funktionen, die mit chrome.tabs.query({ active: true }) arbeiten, liefern dann den Popup-Tab selbst. Testen Sie solche Abläufe besser über den Service Worker oder öffnen Sie vorher eine Zielseite in einem zweiten Tab und wechseln Sie gezielt.
Content Scripts testen
Content Scripts laufen auf Webseiten. Testen Sie sie gegen eine lokale, kontrollierte Seite statt gegen echte Websites, deren Aufbau sich jederzeit ändern kann:
test("Content Script markiert Preise", async ({ page }) => {
await page.goto("http://localhost:4173/testseite.html");
await expect(page.locator(".preis-markiert")).toHaveCount(3);
});
Eine kleine Testseite mit genau den Elementen, die Ihr Skript erwartet, liefert stabile Ergebnisse. Den lokalen Server starten Sie über die Option webServer in der Playwright-Konfiguration automatisch. Achten Sie darauf, dass die matches-Muster im Manifest auch http://localhost/* abdecken, sonst wird das Skript auf der Testseite gar nicht eingefügt. Am saubersten ist ein eigenes Test-Manifest, das nur beim Testbuild verwendet wird.
Service Worker und Speicher prüfen
Über das Service-Worker-Objekt führen Sie Code direkt im Hintergrundkontext aus. So prüfen Sie gespeicherte Daten oder setzen einen Ausgangszustand:
test("Einstellungen werden gespeichert", async ({ context }) => {
let [worker] = context.serviceWorkers();
if (!worker) worker = await context.waitForEvent("serviceworker");
await worker.evaluate(() => chrome.storage.local.set({ modus: "kompakt" }));
const wert = await worker.evaluate(() => chrome.storage.local.get("modus"));
expect(wert.modus).toBe("kompakt");
});
Beachten Sie, dass ein Service Worker unter Manifest V3 bei Inaktivität beendet wird. Tests, die Zustand in globalen Variablen erwarten, decken genau den Fehler auf, den Nutzer später erleben würden.
Unit-Tests für die Logik
End-to-End-Tests sind gründlich, aber langsam. Jeder Test startet einen Browser, lädt die Erweiterung und wartet auf Seiten. Deshalb lohnt es sich, die eigentliche Logik so zu schreiben, dass sie ohne Browser prüfbar ist. Trennen Sie dazu reine Funktionen, etwa das Erkennen von Preisen in einem Text oder das Zusammenführen von Einstellungen, vom Code, der Browser-APIs aufruft. Die reinen Funktionen testen Sie mit Vitest in Millisekunden.
Wo sich Browser-APIs nicht vermeiden lassen, ersetzen Sie sie im Test durch eine einfache Attrappe:
// tests/einstellungen.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { ladeEinstellungen } from "../src/einstellungen";
beforeEach(() => {
globalThis.chrome = {
storage: { local: { get: vi.fn().mockResolvedValue({ modus: "kompakt" }) } },
} as unknown as typeof chrome;
});
describe("ladeEinstellungen", () => {
it("ergänzt fehlende Werte mit Standards", async () => {
const e = await ladeEinstellungen();
expect(e.modus).toBe("kompakt");
expect(e.farbe).toBe("hell");
});
});
Die Attrappe bildet nur ab, was der Test braucht. Das ist bewusst so: Sie prüfen Ihre Logik, nicht die Browser-API. Ob das Zusammenspiel mit dem echten Browser stimmt, klären anschließend wenige, gezielte End-to-End-Tests.
Was sich zu testen lohnt
Nicht jede Zeile braucht einen Browser-Test. Eine sinnvolle Auswahl orientiert sich an dem, was Nutzer täglich tun und was bei Fehlern am meisten schadet:
- Der Hauptablauf. Die eine Funktion, für die Menschen die Erweiterung installieren, sollte mindestens einen End-to-End-Test haben.
- Erste Installation. Viele Erweiterungen öffnen eine Willkommensseite oder legen Standardwerte an. Ein Test mit frischem Profil zeigt, ob das klappt.
- Updates und Migrationen. Wenn sich das Format gespeicherter Daten ändert, prüfen Sie mit einem vorab befüllten Speicher, ob alte Daten korrekt übernommen werden.
- Berechtigungsgrenzen. Seiten, auf denen die Erweiterung nicht laufen soll, verdienen einen Test, der genau das bestätigt.
- Fehlerfälle. Was passiert, wenn ein Server nicht antwortet oder eine Seite anders aufgebaut ist als erwartet? Mit der Netzwerkumleitung von Playwright (
page.route) simulieren Sie solche Fälle.
Tests stabil halten
Wackelnde Tests sind schlimmer als keine, weil sie Vertrauen zerstören. Verzichten Sie auf feste Wartezeiten und warten Sie stattdessen auf Zustände, etwa auf ein sichtbares Element oder einen gespeicherten Wert. Nutzen Sie Locator nach Rolle und Beschriftung statt nach CSS-Klassen, die sich bei jeder Gestaltungsänderung verschieben. Und starten Sie für jeden Test einen frischen Kontext, damit gespeicherte Daten eines Tests den nächsten nicht beeinflussen. Die Fixture oben erledigt das bereits, solange sie im Standard-Geltungsbereich je Test läuft.
Typische Stolperfallen
| Problem | Ursache | Lösung |
|---|---|---|
| Erweiterung wird nicht geladen | Pfad zeigt auf Quellcode statt Build | Ausgabeordner angeben, vorher bauen |
| Kein Service Worker gefunden | Manifest-Fehler oder Startverzögerung | waitForEvent("serviceworker"), Manifest prüfen |
| Content Script greift nicht | matches deckt Testseite nicht ab |
Test-Manifest mit localhost |
| Tests beeinflussen sich gegenseitig | gemeinsames Profil | je Test ein frischer Kontext |
| Tests wackeln | feste Wartezeiten | auf Elemente und Zustände warten |
In der CI ausführen
In GitHub Actions oder einer anderen CI-Umgebung laufen die Tests wie lokal. Installieren Sie Chromium mit Abhängigkeiten über npx playwright install --with-deps chromium, bauen Sie die Erweiterung und starten Sie npx playwright test. Dank Headless-Modus brauchen Sie keine virtuelle Anzeige. Lassen Sie bei Fehlschlägen Spuren (Traces) und Screenshots als Artefakte speichern. Der Trace-Viewer von Playwright zeigt danach Schritt für Schritt, was im Browser passiert ist.
Eine sinnvolle Aufteilung: Unit-Tests bei jedem Commit, End-to-End-Tests bei jedem Pull Request und zusätzlich regelmäßig gegen die neueste Chromium-Version, damit Brüche durch Browser-Updates früh auffallen. Wer gerade erst die erste Erweiterung baut, findet die Grundlagen im Beitrag Chrome-Erweiterung programmieren.
Fragen und Antworten
- Kann ich Erweiterungen mit Playwright im Headless-Modus testen?
- Mit dem von Playwright mitgelieferten Chromium ja, wenn Sie den Kanal chromium verwenden, der den neuen Headless-Modus nutzt. Der ältere, abgespeckte Headless-Modus lädt keine Erweiterungen. Ob die Startparameter in der Markenversion Google Chrome noch greifen, hängt von deren Version ab, deshalb ist das mitgelieferte Chromium für Tests die verlässlichere Wahl.
- Wie teste ich meine Erweiterung in Firefox?
- Playwright lädt in Firefox keine WebExtensions über dieselben Parameter. Für Firefox bieten sich das Mozilla-Werkzeug web-ext für manuelle und halbautomatische Tests sowie Unit-Tests der Logik an. Wer echte End-to-End-Tests in Firefox braucht, greift auf Selenium mit geckodriver zurück, das temporäre Add-ons installieren kann.
- Brauche ich neben End-to-End-Tests noch Unit-Tests?
- Ja, in der Regel. End-to-End-Tests sind langsam und eignen sich für die wichtigsten Abläufe. Reine Logik wie das Parsen von Daten, Regeln oder Formatierungen testen Sie schneller mit einem Unit-Test-Werkzeug wie Vitest, wobei Sie die Browser-APIs durch einfache Attrappen ersetzen.
- Warum findet mein Test den Service Worker nicht?
- Der Service Worker startet erst, nachdem die Erweiterung geladen wurde, und kann bei Testbeginn noch fehlen. Prüfen Sie zuerst die bereits laufenden Service Worker des Kontexts und warten Sie andernfalls auf das Ereignis serviceworker. Fehlt er ganz, ist meist der Pfad zur Erweiterung falsch oder das Manifest fehlerhaft.
Weiterlesen
- Chrome-Erweiterung programmieren
Von der leeren manifest.json bis zur funktionierenden Erweiterung mit Popup, Service Worker und Kontextmenü: ein vollständiges Beispiel zum Nachbauen.
- TypeScript und Vite einrichten
Ein Build-Setup für Erweiterungen: TypeScript-Konfiguration, Typen für die Browser-APIs, mehrere Einstiegspunkte mit Vite, die Sonderrolle der Content Scripts und Alternativen wie WXT.
- Aufbau einer Erweiterung
Welche Teile eine Erweiterung hat, in welchem Kontext jeder läuft, was er darf und wie die Teile über Nachrichten und Speicher zusammenarbeiten.