Playwright fixture
The fixture shape
Section titled “The fixture shape”extforge init scaffolds tests/e2e/fixture.ts. Import from it instead of from @playwright/test:
import { test, expect } from '../e2e/fixture';The fixture provides two additional properties on top of standard Playwright fixtures:
| Fixture | Type | Description |
|---|---|---|
context | BrowserContext | A persistent browser context with the extension loaded. |
extensionId | string | The extension’s ID extracted from the service worker URL. |
Why launchPersistentContext
Section titled “Why launchPersistentContext”Playwright’s browser.launch() starts a browser with an ephemeral profile that does not support extensions. The --load-extension flag requires a real user-data directory, which only launchPersistentContext creates.
Passing '' as the first argument (the profile path) tells Playwright to create a temporary directory automatically. The directory is cleaned up when ctx.close() is called.
const ctx = await chromium.launchPersistentContext('', { headless: false, args: [ `--disable-extensions-except=${EXT_PATH}`, `--load-extension=${EXT_PATH}`, ],});--disable-extensions-except prevents other installed extensions from loading, keeping tests isolated.
Getting extensionId
Section titled “Getting extensionId”The fixture waits for the extension’s service worker to register, then extracts the ID from its URL:
let [sw] = context.serviceWorkers();if (!sw) sw = await context.waitForEvent('serviceworker');const id = sw.url().split('/')[2]!;// Service worker URL: chrome-extension://<id>/background.jsextensionId is stable for a given build. Use it to construct chrome-extension:// URLs:
await page.goto(`chrome-extension://${extensionId}/popup.html`);Opening the popup
Section titled “Opening the popup”The popup is a regular HTML page served from the extension. Open it with page.goto:
test('popup renders', async ({ page, extensionId }) => { await page.goto(`chrome-extension://${extensionId}/popup.html`); await expect(page.getByRole('heading', { name: 'My Extension' })).toBeVisible();});Playwright interacts with extension pages identically to normal web pages. The full Playwright API (click, fill, evaluate, waitForSelector, etc.) works.
Opening the side panel
Section titled “Opening the side panel”The side panel is also a regular HTML page:
test('sidepanel renders', async ({ page, extensionId }) => { await page.goto(`chrome-extension://${extensionId}/sidepanel.html`); await expect(page.locator('h1')).toContainText('Page Annotator');});Headed vs. headless
Section titled “Headed vs. headless”Extensions cannot load in headless Chromium. The fixture sets headless: false. Attempting headless: true will result in waitForEvent('serviceworker') timing out because Chrome refuses to run extensions without a display.
In CI, use a virtual display:
Linux (GitHub Actions):
- name: Install virtual display run: sudo apt-get install -y xvfb
- name: Run E2E tests run: Xvfb :99 -screen 0 1280x800x24 & DISPLAY=:99 pnpm test:e2eThe playwright/chromium base Docker image also ships with a virtual framebuffer.
macOS CI: macOS GitHub Actions runners have a display attached; headless: false works without Xvfb.
Sample test patterns
Section titled “Sample test patterns”Assert badge text set by background
Section titled “Assert badge text set by background”test('background sets badge on install', async ({ context, extensionId, page }) => { // Navigate to a page so the popup is accessible await page.goto(`chrome-extension://${extensionId}/popup.html`); // If the background sets a badge on install, read it via evaluate in the extension context const badge = await context.serviceWorkers()[0]!.evaluate( () => new Promise<string>(resolve => chrome.action.getBadgeText({}, resolve)) ); expect(badge).toBe('ON');});Inject content script and read DOM
Section titled “Inject content script and read DOM”test('content script annotates page', async ({ context, extensionId }) => { const page = await context.newPage(); await page.goto('https://example.com'); // Wait for the content script to run await page.waitForSelector('[data-extforge-annotated]'); const attr = await page.getAttribute('body', 'data-extforge-annotated'); expect(attr).toBe('true');});