AI News Hub Logo

AI News Hub

Testing Firefox Extensions with Playwright: End-to-End Testing Guide

DEV Community
Weather Clock Dash

Testing Firefox Extensions with Playwright: End-to-End Testing Guide Extension testing is one of those things everyone knows they should do but few actually do. I've been using Playwright for end-to-end tests on the Weather & Clock Dashboard extension and it's changed how I think about extension quality. Unit tests don't cover the biggest failure modes: Does the extension actually load in Firefox? Does the new tab override work? Does dark mode actually change the theme? Does the weather display when location is set? E2E tests catch all of these. npm install --save-dev @playwright/test npx playwright install firefox playwright.config.ts: import { defineConfig, devices } from '@playwright/test'; import path from 'path'; const EXTENSION_PATH = path.resolve(__dirname, '.'); export default defineConfig({ testDir: './tests', use: { browserName: 'firefox', }, projects: [ { name: 'firefox-extension', use: { ...devices['Desktop Firefox'], launchOptions: { args: [ `-load-extension=${EXTENSION_PATH}`, '-extension-arg', ] } } } ] }); Note: Firefox extension loading in Playwright uses a different API than Chrome. Here's the Firefox-specific approach: import { chromium, firefox } from 'playwright'; async function launchFirefoxWithExtension(extensionPath: string) { const browser = await firefox.launch({ headless: false, // Firefox requires headful for extensions in dev mode firefoxUserPrefs: { 'extensions.autoDisableScopes': 0, 'extensions.enabledScopes': 15, } }); const context = await browser.newContext(); // Load extension await context.addInitScript(() => { // Extension-specific initialization }); return { browser, context }; } For new tab extensions, the most reliable approach is to load the HTML file directly in tests: import { test, expect } from '@playwright/test'; import path from 'path'; const NEWTAB_URL = `file://${path.resolve(__dirname, '../newtab.html')}`; test('renders weather widget', async ({ page }) => { // Mock the weather API await page.route('**/api.openweathermap.org/**', route => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ name: 'San Francisco', sys: { country: 'US' }, weather: [{ main: 'Clear', description: 'clear sky', icon: '01d' }], main: { temp: 72, feels_like: 70, humidity: 45 }, }) }); }); // Set city in localStorage await page.addInitScript(() => { localStorage.setItem('city', 'San Francisco'); }); await page.goto(NEWTAB_URL); await page.waitForTimeout(1500); // Assertions await expect(page.locator('#weather-temp')).toContainText('72'); await expect(page.locator('#weather-city')).toContainText('San Francisco'); await expect(page.locator('#weather-description')).toContainText('clear sky'); }); test('dark mode toggle persists', async ({ page }) => { await page.goto(NEWTAB_URL); // Initially light mode await expect(page.locator('body')).not.toHaveClass(/dark/); // Toggle dark mode await page.click('#theme-toggle'); await page.waitForTimeout(300); // Should be dark await expect(page.locator('body')).toHaveClass(/dark/); // Reload — should persist await page.reload(); await page.waitForTimeout(500); await expect(page.locator('body')).toHaveClass(/dark/); }); test('world clock shows correct city', async ({ page }) => { await page.addInitScript(() => { localStorage.setItem('worldClocks', JSON.stringify([ { label: 'Tokyo', timezone: 'Asia/Tokyo' }, { label: 'London', timezone: 'Europe/London' } ])); }); await page.goto(NEWTAB_URL); await page.waitForTimeout(1000); const clocks = await page.locator('.world-clock').all(); expect(clocks).toHaveLength(2); await expect(clocks[0]).toContainText('Tokyo'); await expect(clocks[1]).toContainText('London'); // Both should show a valid time (HH:MM format) const tokyoTime = await clocks[0].locator('.clock-time').textContent(); expect(tokyoTime).toMatch(/\d{1,2}:\d{2}/); }); test('shows cached data when offline', async ({ page }) => { // First, cache some data await page.addInitScript(() => { const cachedWeather = { data: { name: 'Cached City', main: { temp: 65 } }, timestamp: Date.now() - 1000 // 1 second ago }; localStorage.setItem('cache_weather', JSON.stringify(cachedWeather)); }); // Simulate offline by blocking API requests await page.route('**/api.openweathermap.org/**', route => { route.abort('failed'); }); await page.goto(NEWTAB_URL); await page.waitForTimeout(2000); // Should show cached data await expect(page.locator('#weather-city')).toContainText('Cached City'); await expect(page.locator('#weather-temp')).toContainText('65'); // Should show offline indicator const status = await page.locator('#weather-status').textContent(); expect(status?.toLowerCase()).toContain('offline'); }); // package.json { "scripts": { "test": "playwright test", "test:ui": "playwright test --ui", "test:headed": "playwright test --headed" } } npm test # Run all tests headlessly npm run test:headed # Run with visible browser npm run test:ui # Interactive UI mode npx playwright test --debug # Step through tests # .github/workflows/test.yml - name: Run E2E tests run: | npx playwright install firefox npm test For the Weather & Clock Dashboard, these tests run on every PR before merging. Install the extension: Weather & Clock Dashboard on AMO Part of a series on building Firefox browser extensions. firefox #testing #playwright #webdev #browserextension