惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
罗磊的独立博客
宝玉的分享
宝玉的分享
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
博客园_首页
量子位
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
S
SegmentFault 最新的问题
雷峰网
雷峰网
小众软件
小众软件
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Testing Firefox Extensions with Playwright: End-to-End Te...
Weather Cloc · 2026-05-04 · via 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.

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