Use when writing or debugging Playwright tests for a Chromium browser extension: the persistent-context fixture that loads the extension and finds its ID, why an edit seems to have no effect (reload matrix), and popup, content-script, storage and MV3 service-worker tests.
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Assertion recipes for playwright-extension-fixtures: what to test on
each Chromium-extension surface (popup, content script, background
service worker, messaging, chrome.storage) once the fixture from the
main skill is in place. Every test below imports test / expect from
the fixture file, which supplies the persistent context and the
resolved extensionId.
import { test, expect } from './fixtures';
test('popup renders and increments counter', async ({ page, extensionId }) => {
await page.goto(`chrome-extension://${extensionId}/popup.html`);
await page.click('[data-testid="increment"]');
await expect(page.locator('[data-testid="count"]')).toHaveText('1');
});test('content script highlights matched terms', async ({ page }) => {
await page.goto('https://example.com/');
// Content script runs at document_idle by default
await page.waitForFunction(() =>
document.querySelector('[data-extension-marker]') !== null
);
await expect(page.locator('mark[data-extension-marker]')).toHaveCount(3);
});test('popup sends message; background responds', async ({ context, extensionId }) => {
let [sw] = context.serviceWorkers();
if (!sw) sw = await context.waitForEvent('serviceworker');
// Eval in service worker context
const swReady = await sw.evaluate(() => {
return new Promise<string>((resolve) => {
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
sendResponse({ echo: msg.text });
return true;
});
resolve('ready');
});
});
expect(swReady).toBe('ready');
const popup = await context.newPage();
await popup.goto(`chrome-extension://${extensionId}/popup.html`);
const reply = await popup.evaluate(async () => {
return chrome.runtime.sendMessage({ text: 'hello' });
});
expect(reply).toEqual({ echo: 'hello' });
});chrome.storage persistenceQuota, area-selection, and storage.onChanged tests are in
storage-tests.md; the baseline persistence check:
test('storage value persists across popup reload', async ({ context, extensionId }) => {
const popup = await context.newPage();
await popup.goto(`chrome-extension://${extensionId}/popup.html`);
await popup.evaluate(async () => {
await chrome.storage.local.set({ pref: 'dark' });
});
await popup.reload();
const value = await popup.evaluate(async () => {
const { pref } = await chrome.storage.local.get('pref');
return pref;
});
expect(value).toBe('dark');
});Per Playwright Chrome extensions docs: Chrome auto-suspends MV3
service workers after ~30s of inactivity. Playwright keeps the same
Worker object alive - evaluate() calls continue transparently
without requiring new event handlers.
test('alarm survives service worker restart', async ({ context }) => {
let [sw] = context.serviceWorkers();
if (!sw) sw = await context.waitForEvent('serviceworker');
await sw.evaluate(() => chrome.alarms.create('hourly', { periodInMinutes: 60 }));
// Simulate idle
await new Promise(r => setTimeout(r, 35_000));
// Same sw object; evaluate still works post-restart
const alarms = await sw.evaluate(() => chrome.alarms.getAll());
expect(alarms.find((a: any) => a.name === 'hourly')).toBeDefined();
});Scenario: a "Reader" extension whose popup increments a counter and toggles a
pref written to chrome.storage, with a content script that marks matched
terms on visited pages.
npm run build emits dist/manifest.json (manifest_version: 3) plus
popup.html and content.js.extensionId fixture reads the SW
URL, e.g. chrome-extension://abcdefghijklmnop/.chrome-extension://${extensionId}/popup.html,
clicks [data-testid="increment"], and asserts the count reads 1.https://example.com/, waits for the
injected marker, and asserts three mark[data-extension-marker] nodes.{ pref: 'dark' }, reloads the popup,
and asserts the value survives.npx playwright test runs headed locally; CI reruns it with
channel: 'chromium', headless: true per the main skill.Result: a green run confirms popup rendering, content-script injection, and storage persistence across reload - the extension's core surfaces are covered.
web-ext tooling (see manifest-v3-test-surface.md,
references/web-ext-firefox.md).chrome.declarativeNetRequest) cannot be
fully unit-tested without a browser; integration tests are required.