DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

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

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.

Why E2E Testing for Extensions?

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.

Setup: Playwright with Firefox Extensions

npm install --save-dev @playwright/test
npx playwright install firefox
Enter fullscreen mode Exit fullscreen mode

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',
          ]
        }
      }
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

Simpler Approach: Test the HTML Directly

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');
});
Enter fullscreen mode Exit fullscreen mode

Testing Dark Mode

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/);
});
Enter fullscreen mode Exit fullscreen mode

Testing the World Clock

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}/);
});
Enter fullscreen mode Exit fullscreen mode

Testing Offline Behavior

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');
});
Enter fullscreen mode Exit fullscreen mode

Running Tests

// package.json
{
  "scripts": {
    "test": "playwright test",
    "test:ui": "playwright test --ui",
    "test:headed": "playwright test --headed"
  }
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

CI Integration

# .github/workflows/test.yml
- name: Run E2E tests
  run: |
    npx playwright install firefox
    npm test
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)