skills/addfox/skills/addfox-testing

addfox-testing

SKILL.md

When to use

Use this skill when the user or codebase needs automated tests for an Addfox extension: rstest, *.test.ts / *.spec.ts, playwright.config.ts, e2e/ folder, mocking chrome.storage / runtime.sendMessage, or loading the unpacked build from .addfox/extension in a real browser.

Trigger examples:

  • "给扩展加单元测试 / E2E / Playwright"
  • "mock chrome API", "rstest.config", "测试 background 消息"
  • "CI 里跑扩展测试", "coverage", "extension fixture"
  • Choosing unit vs E2E for popup, content script, or content UI

Use addfox-best-practices for product architecture; use addfox-debugging if tests fail due to build or load errors.

How to use

Follow sections below for Rstest setup, chrome mocks, and Playwright extension loading. Supplementary notes: reference.md.


Addfox Testing

Use Rstest for unit tests and Playwright for E2E extension loading.


1. Test Types Overview

Test Type Tool Use Case Speed
Unit Rstest Logic, utilities, storage, messaging Fast (<1s)
Component Rstest + jsdom React/Vue/Svelte components Medium (~1s)
E2E Playwright Full extension load, user flows Slow (~10s)

Decision Matrix

Feature to Test Recommended Type Notes
Background message handlers Unit Mock chrome.runtime
Storage operations Unit Mock chrome.storage
Content script selectors Unit Use jsdom
React component rendering Component Use testing-library
Popup interactions E2E Full browser context
Cross-extension messaging E2E Real extension instances
Content UI appearance E2E Visual verification
Permission flows E2E Real browser prompts

2. Unit Testing with Rstest

2.1 Installation

Rstest is pre-configured in Addfox. Install additional dependencies:

# Rstest is included with Addfox
pnpm add -D @testing-library/react @testing-library/jest-dom jsdom

# For Vue
pnpm add -D @vue/test-utils

# For utilities
pnpm add -D happy-dom  # Alternative to jsdom, faster

2.2 Configuration

Add to project root (if not present):

// rstest.config.ts
import { defineConfig } from 'rstest';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom', // or 'happy-dom', 'node'
    setupFiles: ['./test/setup.ts'],
    include: ['**/*.test.ts', '**/*.spec.ts'],
    exclude: ['**/node_modules/**', '**/e2e/**']
  }
});

2.3 Mocking Extension APIs

Create mock for chrome.* APIs:

// test/mocks/chrome.ts
export const mockChrome = {
  runtime: {
    sendMessage: vi.fn(),
    onMessage: {
      addListener: vi.fn(),
      removeListener: vi.fn()
    },
    getManifest: vi.fn(() => ({
      manifest_version: 3,
      version: '1.0.0'
    }))
  },
  storage: {
    local: {
      get: vi.fn(),
      set: vi.fn()
    },
    sync: {
      get: vi.fn(),
      set: vi.fn()
    }
  },
  tabs: {
    query: vi.fn(),
    sendMessage: vi.fn(),
    create: vi.fn()
  },
  scripting: {
    executeScript: vi.fn()
  }
};

// test/setup.ts
import { mockChrome } from './mocks/chrome';

global.chrome = mockChrome as any;

2.4 Unit Test Examples

Background logic:

// src/utils/storage.test.ts
import { describe, it, expect, vi, beforeEach } from 'rstest';
import { saveSettings, getSettings } from './storage';

describe('storage', () => {
  beforeEach(() => {
    vi.resetAllMocks();
    chrome.storage.sync.get = vi.fn().mockResolvedValue({});
    chrome.storage.sync.set = vi.fn().mockResolvedValue(undefined);
  });

  it('saves settings to sync storage', async () => {
    const settings = { theme: 'dark', autoSave: true };
    await saveSettings(settings);
    
    expect(chrome.storage.sync.set).toHaveBeenCalledWith(settings);
  });

  it('retrieves settings with defaults', async () => {
    chrome.storage.sync.get = vi.fn().mockResolvedValue({ theme: 'light' });
    
    const result = await getSettings();
    
    expect(chrome.storage.sync.get).toHaveBeenCalled();
    expect(result.theme).toBe('light');
  });
});

Message handlers:

// src/background/messages.test.ts
import { describe, it, expect, vi } from 'rstest';
import { handleMessage } from './messages';

describe('message handlers', () => {
  it('handles GET_SETTINGS from popup', async () => {
    const message = { from: 'popup', action: 'GET_SETTINGS' };
    const sender = { tab: { id: 123 } };
    
    const response = await handleMessage(message, sender);
    
    expect(response).toHaveProperty('settings');
  });

  it('rejects unknown actions', async () => {
    const message = { from: 'content', action: 'UNKNOWN' };
    
    await expect(handleMessage(message, {}))
      .rejects.toThrow('Unknown action');
  });
});

Content script utilities:

// src/content/utils.test.ts
import { describe, it, expect } from 'rstest';
import { extractVideoInfo } from './utils';

// Mock document for content script tests
const mockDocument = `
  <video src="https://example.com/video.mp4" data-title="Test Video"></video>
`;

describe('extractVideoInfo', () => {
  it('extracts video URL from page', () => {
    document.body.innerHTML = mockDocument;
    
    const info = extractVideoInfo();
    
    expect(info.url).toBe('https://example.com/video.mp4');
    expect(info.title).toBe('Test Video');
  });

  it('returns null when no video found', () => {
    document.body.innerHTML = '<div>No video here</div>';
    
    const info = extractVideoInfo();
    
    expect(info).toBeNull();
  });
});

2.5 Component Testing

React component:

// src/components/Button.test.tsx
import { describe, it, expect, vi } from 'rstest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('handles click events', () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click</Button>);
    
    fireEvent.click(screen.getByText('Click'));
    
    expect(handleClick).toHaveBeenCalled();
  });
});

3. E2E Testing with Playwright

3.1 Installation

pnpm add -D @playwright/test
npx playwright install chromium firefox

3.2 E2E Configuration

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
import path from 'path';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: false, // Extensions should run sequentially
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: 1, // Single worker for extension tests
  reporter: 'list',
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    },
    // Firefox requires signed extensions for E2E
    // {
    //   name: 'firefox',
    //   use: { ...devices['Desktop Firefox'] }
    // }
  ]
});

3.3 Extension Fixture

// e2e/fixtures.ts
import { test as base, chromium, type BrowserContext } from '@playwright/test';
import path from 'path';

export const test = base.extend<{
  context: BrowserContext;
  extensionId: string;
}>({
  context: async ({}, use) => {
    // Build extension first
    const extensionPath = path.join(__dirname, '../.addfox/extension');
    
    const context = await chromium.launchPersistentContext('', {
      headless: false,
      args: [
        `--disable-extensions-except=${extensionPath}`,
        `--load-extension=${extensionPath}`
      ]
    });
    
    await use(context);
    await context.close();
  },
  
  extensionId: async ({ context }, use) => {
    // Get extension ID from background page
    let [background] = context.backgroundPages();
    if (!background) {
      background = await context.waitForEvent('backgroundpage');
    }
    
    const extensionId = background.url().split('/')[2];
    await use(extensionId);
  }
});

export const expect = test.expect;

3.4 E2E Test Examples

Extension load:

// e2e/extension-load.test.ts
import { test, expect } from './fixtures';

test('extension loads without errors', async ({ context }) => {
  const page = await context.newPage();
  
  // Check console for errors
  const errors: string[] = [];
  page.on('console', msg => {
    if (msg.type() === 'error') errors.push(msg.text());
  });
  
  await page.goto('https://example.com');
  await page.waitForTimeout(1000); // Allow extension to initialize
  
  expect(errors).toHaveLength(0);
});

Popup interaction:

// e2e/popup.test.ts
import { test, expect } from './fixtures';

test('popup opens and shows content', async ({ context, extensionId }) => {
  const page = await context.newPage();
  await page.goto(`chrome-extension://${extensionId}/popup/index.html`);
  
  // Verify popup rendered
  await expect(page.locator('body')).toContainText('My Extension');
  
  // Test interaction
  await page.click('button[data-testid="settings"]');
  await expect(page.locator('.settings-panel')).toBeVisible();
});

Content script injection:

// e2e/content-script.test.ts
import { test, expect } from './fixtures';

test('content script injects on matching page', async ({ context }) => {
  const page = await context.newPage();
  
  // Navigate to page matching content_scripts.matches
  await page.goto('https://example.com');
  
  // Wait for content script to inject
  await page.waitForSelector('[data-extension-root]', { timeout: 5000 });
  
  // Verify content UI exists
  const root = page.locator('[data-extension-root]');
  await expect(root).toBeVisible();
});

Background script:

// e2e/background.test.ts
import { test, expect } from './fixtures';

test('background script responds to messages', async ({ context }) => {
  const page = await context.newPage();
  await page.goto('https://example.com');
  
  // Send message from page to background via content script
  const response = await page.evaluate(async () => {
    return await chrome.runtime.sendMessage({
      from: 'test',
      action: 'PING'
    });
  });
  
  expect(response).toEqual({ status: 'pong' });
});

3.5 E2E Best Practices

Practice Why
Build before E2E addfox build must run before Playwright tests
Use test.extend Create reusable fixtures for extension context
Single worker Extension tests conflict with parallel runs
Headless off Some extension APIs require headed browser
Cleanup context Always close browser context after tests

4. Test Scripts

Add to package.json:

{
  "scripts": {
    "test": "rstest",
    "test:unit": "rstest --run",
    "test:e2e": "addfox build && playwright test",
    "test:e2e:ui": "addfox build && playwright test --ui",
    "test:coverage": "rstest --coverage"
  }
}

5. Testing Checklist

Unit Tests

  • Mock chrome.* APIs in setup file
  • Test background message handlers
  • Test storage operations with mocked storage
  • Test content script utilities in jsdom
  • Test components with testing-library

E2E Tests

  • Build extension before running Playwright
  • Create fixture for extension context
  • Test extension loads without console errors
  • Test popup/options page rendering
  • Test content script injection on matching pages
  • Test background script message handling

Configuration

  • rstest.config.ts with appropriate environment
  • playwright.config.ts with extension args
  • Test mocks for all used chrome.* APIs
  • CI workflow for automated testing

Additional resources

Weekly Installs
10
Repository
addfox/skills
GitHub Stars
1
First Seen
3 days ago
Installed on
amp10
cline10
opencode10
cursor10
kimi-cli10
warp10