Learn-skills.dev addfox-testing

install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/addfox/skills/addfox-testing" ~/.claude/skills/neversight-learn-skills-dev-addfox-testing && rm -rf "$T"
manifest: data/skills-md/addfox/skills/addfox-testing/SKILL.md
source content

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 TypeToolUse CaseSpeed
UnitRstestLogic, utilities, storage, messagingFast (<1s)
ComponentRstest + jsdomReact/Vue/Svelte componentsMedium (~1s)
E2EPlaywrightFull extension load, user flowsSlow (~10s)

Decision Matrix

Feature to TestRecommended TypeNotes
Background message handlersUnitMock
chrome.runtime
Storage operationsUnitMock
chrome.storage
Content script selectorsUnitUse jsdom
React component renderingComponentUse testing-library
Popup interactionsE2EFull browser context
Cross-extension messagingE2EReal extension instances
Content UI appearanceE2EVisual verification
Permission flowsE2EReal 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

PracticeWhy
Build before E2E
addfox build
must run before Playwright tests
Use
test.extend
Create reusable fixtures for extension context
Single workerExtension tests conflict with parallel runs
Headless offSome extension APIs require headed browser
Cleanup contextAlways 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