playwright-webext: a library for testing browser extensions
Playwright's official guide for testing browser extensions gives a good starting point. It shows how to launch a browser with an extension, wait for its service worker, and find its id.
But you still have to write extra code to interact with extension storage, emulate browser events, open extension pages, and handle other extension-specific tasks.
I'd like to share my library playwright-webext, a set of Playwright fixtures that simplify these tasks and save some setup code in your tests.
To get started, set extensionPath in playwright.config.ts:
import { defineConfig } from '@playwright/test';
import type { WebextOptions } from 'playwright-webext';
export default defineConfig<WebextOptions>({
use: {
extensionPath: './dist',
},
});
This installs the extension before each test. The built-in extension fixture provides helper methods for interacting with it.
For example, to test the popup:
import { expect } from '@playwright/test';
import { test } from 'playwright-webext';
test('shows the popup', async ({ extension }) => {
// open the popup
const popup = await extension.openPopup();
// check the heading
await expect(popup.getByRole('heading')).toHaveText('My popup');
});
Or test a setting change on the options page:
test('change mySetting', async ({ extension }) => {
// populate the storage
await extension.storage.local.set({ mySetting: false });
// open the options page and change the setting
const options = await extension.openOptionsPage();
await options.getByRole('checkbox', { name: 'My setting' }).check();
// verify the new value is saved
await extension.storage.local.expect('mySetting').toBe(true);
});
You can also test side panels, data migration, i18n, uninstall flows, and more.
You are welcome to try it and share your feedback.
GitHub: vitalets/playwright-webext
1 point · 1 comment
Testing extensions with Playwright-webext
Playwright’s official guide for testing browser extensions gives a good starting point. It shows how to launch a browser with an extension, wait for its service worker, and find its id.
But for comprehensive testing, you still have to write extra code to interact with extension storage, emulate browser events, open extension pages, and handle other specific actions.
I'd like to share my library playwright-webext, a set of Playwright fixtures that simplify these tasks and let you focus on the actual scenarios.
Here is an example: a test that prepares the extension storage, opens the popup, and verifies the displayed value:
import { expect } from '@playwright/test';
import { test } from 'playwright-webext';
test('popup', async ({ extension }) => {
await extension.storage.local.set({ mySetting: true });
const popup = await extension.openPopup();
await expect(popup.getByRole('checkbox', { name: 'My setting' })).toBeChecked();
});
The extension fixture provides APIs for interacting with the default pages (options, side-panel), checking i18n, data migration, uninstall flow and other tasks.
You are welcome to try it and share the feedback on how do you test your extensions end-to-end and what are the main challenges.
2 points · 6 comments
Testing browser extensions with playwright-webext
Testing a browser extension is more than checking a web page. You may need to prepare storage, open an extension popup, options page, or side panel, trigger browser events, and check data migration between extension versions.
Playwright's official extensions guide shows how to launch a browser with an extension, wait for its service worker, and find its ID. That gives a good starting point, but the extension-specific parts of the tests still need fixtures and helpers.
In this post, I want to introduce my project, playwright-webext, which makes browser extension testing easier. I'll take a simple extension and showcase 3 tests covering the extension install, interacting with the popup, and checking the upgrade to a new version.
Example Extension
The example extension is a simple todo list. Users can open its popup to view their tasks, add new items, and remove completed ones:
<p>
<img src="vitalets.github.io" alt="Opening the extension popup, adding a task, and deleting another with the × button" width="748" height="494">
</p>
<center><small>Todo list in the popup</small></center><br>
The extension keeps its tasks in chrome.storage.local. When the popup opens, it reads the saved list and displays it.
Set up the tests
Install the dependencies:
npm install -D @playwright/test playwright-webext
Then install Chromium for Playwright:
npx playwright install chromium
Build the extension into the dist directory.
Configure Playwright to point to the built extension:
playwright.config.ts
import { defineConfig } from '@playwright/test';
import type { WebextOptions } from 'playwright-webext';
export default defineConfig<WebextOptions>({
testDir: './test',
use: {
extensionPath: './dist',
},
});
Test the installation
I'll start with the simplest test. On installation, the extension saves the current date in chrome.storage.local for analytics purposes:
background.js
chrome.runtime.onInstalled.addListener(({ reason }) => {
if (reason === 'install') {
chrome.storage.local.set({ installationDate: new Date().toISOString() });
}
});
The test to check this behavior is a single line:
test/install.spec.ts
import { test } from 'playwright-webext';
test('saves the installation date', async ({ extension }) => {
await extension.storage.local.expect('installationDate').toMatch(/^\d{4}-\d{2}-\d{2}/);
});
Playwright-webext provides an extension fixture that automatically installs an extension before each test. The storage assertion waits for the installation handler to save a value starting with YYYY-MM-DD.
Run the tests with:
npx playwright test
By default, Playwright runs Chromium in headless mode, without a visible browser window. This keeps the run fast and lightweight. The result appears in the terminal:
<p>
<img src="vitalets.github.io" alt="Installation date test passing: 1 passed in 1.4 seconds" width="734" height="107">
</p>
To watch the test run in a visible browser, use headed mode:
npx playwright test --headed
If for some reason the extension does not write the installation date to storage, I'll get an error:
<p>
<img src="vitalets.github.io" alt="Installation test failing because installationDate is undefined in storage after a 5000 ms timeout" width="870" height="404">
</p>
Test the popup
The next goal is to check the extension popup. I'll add two tasks to the storage, open the popup and check that the tasks are displayed. Then I'll delete the first task and check that only the second remains in the storage:
test/popup.spec.ts
import { expect } from '@playwright/test';
import { test } from 'playwright-webext';
test('shows saved tasks in the popup', async ({ extension }) => {
// prepare 2 tasks in storage
await extension.storage.local.set({
todos: ['Buy groceries', 'Read a book'],
});
// open the popup and check the tasks are visible
const popup = await extension.openPopup();
await expect(popup.getByRole('listitem')).toContainText([
'Buy groceries',
'Read a book',
]);
// delete the task
await popup.getByRole('button', { name: 'Delete Buy groceries' }).click();
// check the storage for remaining task
await extension.storage.local.expect('todos').toEqual(['Read a book']);
});
The same extension fixture provides a method for opening the extension popup: extension.openPopup(). It returns a regular Playwright Page instance that I can use for testing the popup with Playwright locators and assertions.
Playwright automatically waits for the tasks to appear in the popup. The list can contain extra items, but the two expected tasks must appear in the given order.
The popup test passes too. I can open the HTML report to inspect the result:
npx playwright show-report
The report provides access to traces showing each test step, along with screenshots of the page at that point:
<p>
<img src="vitalets.github.io" alt="Playwright Trace Viewer showing the popup with Buy groceries and Read a book at the toContainText assertion" width="1180">
</p>
<center><small>Popup screenshot in the trace viewer</small></center><br>
You may notice that the popup opened in a regular tab rather than as a native browser popup. This is a workaround for a Playwright limitation: its supported Page API does not expose the native popup for interaction. In most cases, using a regular tab is acceptable and gives many opportunities to test the popup content using Playwright locators.
Test the migration
The third test is a bit more advanced. Suppose I release v2 of the extension with a new format for storing saved tasks.
In v1, tasks were stored as an array of strings:
{
"todos": [
"Buy groceries",
"Read a book"
]
}
In v2, tasks are stored as an array of objects:
{
"todos": [
{ "name": "Buy groceries", "done": false },
{ "name": "Read a book", "done": false }
]
}
Playwright-webext can test this migration too.
For this test, place the v1 build in a separate directory, test/dist-v1. Version 2 stays in dist, as configured in extensionPath. Setting extensionAutoInstall: false allows the test to install the old version first:
test/migration.spec.ts
import { test } from 'playwright-webext';
test.use({
extensionAutoInstall: false,
});
test('migrates todos from v1 to v2', async ({ extension }) => {
// install the old version
await extension.install('./test/dist-v1');
// populate the storage
await extension.storage.local.set({
todos: ['Buy groceries', 'Read a book'],
});
// upgrade to the new version
await extension.upgrade();
// check the migrated data
await extension.storage.local.expect('todos').toEqual([
{ name: 'Buy groceries', done: false },
{ name: 'Read a book', done: false },
]);
});
extension.upgrade() keeps the extension's stored data and triggers the update handler. The version 2 extension converts the strings to objects in its update handler, and the storage assertion waits for the result. This ensures that existing users keep their tasks when they update.
Putting it together
I run all three tests and get the standard HTML report. It includes the results, screenshots, and traces for each test:
<p>
<img src="vitalets.github.io" alt="Playwright HTML report showing three passed tests for installation, popup, and migration" width="740" height="470">
</p>
<center><small>All three tests pass</small></center><br>
Recap
Playwright-webext takes care of extension testing setup, letting you focus on the actual scenarios. You're welcome to check the documentation and try it with your extension. Thanks for reading!
Mentions
playwright-bddを教えてもらった! 使ってみたい vitalets/playwright-bdd
playwright-webext: a library for testing browser extensions Playwright's official guide for testing browser extensions gives a good starting point. It shows how to launch a browser with an extension, wait for its service worker, and find its id. But you still have to write extra code to interact with extension storage, emulate browser events, open extension pages, and handle other extension-specific tasks. I'd like to share my library playwright-webext, a set of Playwright fixtures that simplify these tasks and save some setup code in your tests. To get started, set extensionPath in playwright.config.ts: import { defineConfig } from '@playwright/test'; import type { WebextOptions } from 'playwright-webext'; export default defineConfig<WebextOptions>({ use: { extensionPath: './dist', }, }); This installs the extension before each test. The built-in extension fixture provides helper methods for interacting with it. For example, to test the popup: import { expect } from '@playwright/test'; import { test } from 'playwright-webext'; test('shows the popup', async ({ extension }) => { // open the popup const popup = await extension.openPopup(); // check the heading await expect(popup.getByRole('heading')).toHaveText('My popup'); }); Or test a setting change on the options page: test('change mySetting', async ({ extension }) => { // populate the storage await extension.storage.local.set({ mySetting: false }); // open the options page and change the setting const options = await extension.openOptionsPage(); await options.getByRole('checkbox', { name: 'My setting' }).check(); // verify the new value is saved await extension.storage.local.expect('mySetting').toBe(true); }); You can also test side panels, data migration, i18n, uninstall flows, and more. You are welcome to try it and share your feedback. GitHub: vitalets/playwright-webext
Testing extensions with Playwright-webext Playwright’s official guide for testing browser extensions gives a good starting point. It shows how to launch a browser with an extension, wait for its service worker, and find its id. But for comprehensive testing, you still have to write extra code to interact with extension storage, emulate browser events, open extension pages, and handle other specific actions. I'd like to share my library playwright-webext, a set of Playwright fixtures that simplify these tasks and let you focus on the actual scenarios. Here is an example: a test that prepares the extension storage, opens the popup, and verifies the displayed value: import { expect } from '@playwright/test'; import { test } from 'playwright-webext'; test('popup', async ({ extension }) => { await extension.storage.local.set({ mySetting: true }); const popup = await extension.openPopup(); await expect(popup.getByRole('checkbox', { name: 'My setting' })).toBeChecked(); }); The extension fixture provides APIs for interacting with the default pages (options, side-panel), checking i18n, data migration, uninstall flow and other tasks. You are welcome to try it and share the feedback on how do you test your extensions end-to-end and what are the main challenges.
Testing browser extensions with playwright-webext Testing a browser extension is more than checking a web page. You may need to prepare storage, open an extension popup, options page, or side panel, trigger browser events, and check data migration between extension versions. Playwright's official extensions guide shows how to launch a browser with an extension, wait for its service worker, and find its ID. That gives a good starting point, but the extension-specific parts of the tests still need fixtures and helpers. In this post, I want to introduce my project, playwright-webext, which makes browser extension testing easier. I'll take a simple extension and showcase 3 tests covering the extension install, interacting with the popup, and checking the upgrade to a new version. Example Extension The example extension is a simple todo list. Users can open its popup to view their tasks, add new items, and remove completed ones: <p> <img src="vitalets.github.io" alt="Opening the extension popup, adding a task, and deleting another with the × button" width="748" height="494"> </p> <center><small>Todo list in the popup</small></center><br> The extension keeps its tasks in chrome.storage.local. When the popup opens, it reads the saved list and displays it. Set up the tests Install the dependencies: npm install -D @playwright/test playwright-webext Then install Chromium for Playwright: npx playwright install chromium Build the extension into the dist directory. Configure Playwright to point to the built extension: playwright.config.ts import { defineConfig } from '@playwright/test'; import type { WebextOptions } from 'playwright-webext'; export default defineConfig<WebextOptions>({ testDir: './test', use: { extensionPath: './dist', }, }); Test the installation I'll start with the simplest test. On installation, the extension saves the current date in chrome.storage.local for analytics purposes: background.js chrome.runtime.onInstalled.addListener(({ reason }) => { if (reason === 'install') { chrome.storage.local.set({ installationDate: new Date().toISOString() }); } }); The test to check this behavior is a single line: test/install.spec.ts import { test } from 'playwright-webext'; test('saves the installation date', async ({ extension }) => { await extension.storage.local.expect('installationDate').toMatch(/^\d{4}-\d{2}-\d{2}/); }); Playwright-webext provides an extension fixture that automatically installs an extension before each test. The storage assertion waits for the installation handler to save a value starting with YYYY-MM-DD. Run the tests with: npx playwright test By default, Playwright runs Chromium in headless mode, without a visible browser window. This keeps the run fast and lightweight. The result appears in the terminal: <p> <img src="vitalets.github.io" alt="Installation date test passing: 1 passed in 1.4 seconds" width="734" height="107"> </p> To watch the test run in a visible browser, use headed mode: npx playwright test --headed If for some reason the extension does not write the installation date to storage, I'll get an error: <p> <img src="vitalets.github.io" alt="Installation test failing because installationDate is undefined in storage after a 5000 ms timeout" width="870" height="404"> </p> Test the popup The next goal is to check the extension popup. I'll add two tasks to the storage, open the popup and check that the tasks are displayed. Then I'll delete the first task and check that only the second remains in the storage: test/popup.spec.ts import { expect } from '@playwright/test'; import { test } from 'playwright-webext'; test('shows saved tasks in the popup', async ({ extension }) => { // prepare 2 tasks in storage await extension.storage.local.set({ todos: ['Buy groceries', 'Read a book'], }); // open the popup and check the tasks are visible const popup = await extension.openPopup(); await expect(popup.getByRole('listitem')).toContainText([ 'Buy groceries', 'Read a book', ]); // delete the task await popup.getByRole('button', { name: 'Delete Buy groceries' }).click(); // check the storage for remaining task await extension.storage.local.expect('todos').toEqual(['Read a book']); }); The same extension fixture provides a method for opening the extension popup: extension.openPopup(). It returns a regular Playwright Page instance that I can use for testing the popup with Playwright locators and assertions. Playwright automatically waits for the tasks to appear in the popup. The list can contain extra items, but the two expected tasks must appear in the given order. The popup test passes too. I can open the HTML report to inspect the result: npx playwright show-report The report provides access to traces showing each test step, along with screenshots of the page at that point: <p> <img src="vitalets.github.io" alt="Playwright Trace Viewer showing the popup with Buy groceries and Read a book at the toContainText assertion" width="1180"> </p> <center><small>Popup screenshot in the trace viewer</small></center><br> You may notice that the popup opened in a regular tab rather than as a native browser popup. This is a workaround for a Playwright limitation: its supported Page API does not expose the native popup for interaction. In most cases, using a regular tab is acceptable and gives many opportunities to test the popup content using Playwright locators. Test the migration The third test is a bit more advanced. Suppose I release v2 of the extension with a new format for storing saved tasks. In v1, tasks were stored as an array of strings: { "todos": [ "Buy groceries", "Read a book" ] } In v2, tasks are stored as an array of objects: { "todos": [ { "name": "Buy groceries", "done": false }, { "name": "Read a book", "done": false } ] } Playwright-webext can test this migration too. For this test, place the v1 build in a separate directory, test/dist-v1. Version 2 stays in dist, as configured in extensionPath. Setting extensionAutoInstall: false allows the test to install the old version first: test/migration.spec.ts import { test } from 'playwright-webext'; test.use({ extensionAutoInstall: false, }); test('migrates todos from v1 to v2', async ({ extension }) => { // install the old version await extension.install('./test/dist-v1'); // populate the storage await extension.storage.local.set({ todos: ['Buy groceries', 'Read a book'], }); // upgrade to the new version await extension.upgrade(); // check the migrated data await extension.storage.local.expect('todos').toEqual([ { name: 'Buy groceries', done: false }, { name: 'Read a book', done: false }, ]); }); extension.upgrade() keeps the extension's stored data and triggers the update handler. The version 2 extension converts the strings to objects in its update handler, and the storage assertion waits for the result. This ensures that existing users keep their tasks when they update. Putting it together I run all three tests and get the standard HTML report. It includes the results, screenshots, and traces for each test: <p> <img src="vitalets.github.io" alt="Playwright HTML report showing three passed tests for installation, popup, and migration" width="740" height="470"> </p> <center><small>All three tests pass</small></center><br> Recap Playwright-webext takes care of extension testing setup, letting you focus on the actual scenarios. You're welcome to check the documentation and try it with your extension. Thanks for reading!