How I Built E2E Tests for Chrome Extensions Using Playwright and CDP
Publish Date: Jul 13 '25
1 0
End-to-End Testing for Chrome Extensions with Playwright
The rainy days continue, but it's the perfect season for indoor coding, isn't it?
I'm K@zuki..
How do you test your Chrome extensions?
To be honest, when it comes to E2E testing for Chrome extensions, you might think: "Is it even possible?", "Is it necessary?", "Sounds complicated..."
I thought the same at first.
But when I actually tried it, it turned out to be surprisingly doable.
Moreover, by combining Playwright with Chrome DevTools Protocol (CDP), I found that you can write quite practical tests.
Today, I'd like to share the E2E testing approach I implemented for my Chrome extension called Snack Time.
This extension is developed using CRXJS.
So, if you run pnpm dev and load the output directory as an extension, the file will be updated in real time.
OUTPUT_DIR is the directory where the output will be placed.
When modifying Content.tsx (Timer component), hot reload may not work properly. In this case, you need to restart the extension:
Go to chrome://extensions
Find "Snack Time" extension
Click the reload button (↻) or toggle the extension off and on
This is a known limitation of Chrome Extension's content scripts.
You can write E2E tests for Chrome extensions using Playwright
Everything, including popups, can be accessed as web pages
Integration tests between popups and content scripts are achievable with CDP
Page Object Pattern makes test code maintainable
bringToFront() enables smooth switching between multiple windows
Why E2E Testing for Chrome Extensions is Challenging
First, let's organize the unique challenges of Chrome extensions.
Compared to regular web applications, Chrome extensions have several special circumstances.
Multiple Execution Contexts
Chrome extensions actually run in multiple "worlds":
Popup - The screen that appears when you click the extension icon
Content Scripts - Scripts injected into each web page
Options Page - Settings screen
Background - Scripts running in the background (Service Worker)
Other custom pages
Since these work together, it seems difficult to test with simple E2E tests.
The Peculiarity of Popups
One of the challenges when wanting to perform E2E testing for Chrome extensions is "not knowing how to interact with popups."
Many Chrome extensions require users to click the extension button in the toolbar and then click elements within the popup to trigger events.
However, Playwright and similar tools cannot normally access this toolbar, which makes it seem difficult.
Extension-Specific URLs
Chrome extension pages have special URLs like chrome-extension://[extension-id]/popup.html.
This extension ID changes depending on the environment, so you can't hardcode it.
Solving with Playwright
Now, let's get to the main topic.
Actually, Playwright can solve these problems quite elegantly.
💡 Any tool using the same driver should be capable of this.
Basic Setup
First, let's look at how to load a Chrome extension with Playwright.
// e2e/fixtures/extension.tsexportconsttest=base.extend<{context:BrowserContext;extensionId:string;}>({context:async ({},use)=>{constpathToExtension=path.join(__dirname,"../../dist");constcontext=awaitchromium.launchPersistentContext("",{headless:false,// Extensions don't work in headless modeargs:[`--disable-extensions-except=${pathToExtension}`,`--load-extension=${pathToExtension}`],});awaituse(context);awaitcontext.close();},extensionId:async ({context},use)=>{// Dynamically get the extension IDconstpage=awaitcontext.newPage();awaitpage.goto("chrome://extensions/");awaitpage.click("cr-toggle#devMode");constextensionCard=awaitpage.locator("extensions-item").first();constextensionId=awaitextensionCard.getAttribute("id");awaitpage.close();awaituse(extensionId);},});
By preparing this fixture and reusing it in each test, we can handle the issue of dynamically changing IDs.
Key points:
Use launchPersistentContext to load the extension
Dynamically retrieve the extension ID from chrome://extensions/
Define as a fixture so it can be reused across all tests
Opening Popups as Separate Pages
Here's the crucial point: by opening the popup as a regular page, we can avoid the freeze issue.
// Open the popup as a new pageconstpopupPage=awaitcontext.newPage();awaitpopupPage.goto(`chrome-extension://${extensionId}/popup/index.html`);// Now you can test it like a normal page!awaitpopupPage.click('button:has-text("5:00")');
Well, it's different from actual user interaction, but it's sufficient for functional testing.
Controlling Multi-Window with CDP
Next, let's talk about switching between popups and content pages.
In Snack Time, the timer specified in the popup is displayed within the content page.
Even if you try to test such a mechanism, you can't test it by operating in the tab where the popup is open because the active tab is not the content page.
This is where Chrome DevTools Protocol (CDP) comes in handy for finer browser control. Page.bringToFront is particularly useful as it brings a specific page to the front, allowing you to switch the active tab.
With this mechanism, you can write tests like this:
test("Set timer from popup",async ({extensionId,context,page})=>{// 1. Open the test target pageawaitpage.goto("https://example.com");constcontentPage=newContentTimerPage(page);// 2. Open the popupconstpopupPageHandle=awaitcontext.newPage();constpopupPage=newPopupPage(popupPageHandle,extensionId);awaitpopupPage.open();// 3. Bring content page to front (using CDP)awaitcontentPage.bringToFront();// 4. Set timer in popupawaitpopupPage.clickPresetButton("5");// 5. Verify timer is displayed on content pageawaitcontentPage.waitForTimer();awaitcontentPage.verifyTimerVisible();});
The great thing is that we can test in a way that's close to actual user operations.
sequenceDiagram
participant Test as Test Code
participant Popup as Popup Page<br/>(New Tab)
participant CDP as Chrome DevTools Protocol
participant Content as Content Page<br/>(example.com)
participant Timer as Timer Element<br/>(#snack-time-root)
Test->>Content: 1. Open example.com
Test->>Popup: 2. Open popup in new tab<br/>chrome-extension://id/popup.html
Test->>CDP: 3. bringToFront()<br/>(Content Page)
CDP->>Content: 4. Make content page active
Test->>Popup: 5. Click preset button (5 min)
Popup->>Timer: 6. Inject timer
Test->>Timer: 7. waitForSelector("#snack-time-root")
Test->>Timer: 8. Verify timer display
Organizing with Page Object Pattern
As test code grows, maintenance becomes challenging.
This is where the Page Object Pattern comes in.
Chrome extensions need to work independently for each tab. We can test this too:
test("Independent timers work in different tabs",async ({extensionId,context})=>{// Set timer in tab 1constpage1=awaitcontext.newPage();awaitpage1.goto("https://example.com");// ... set timer ...// Set different timer in tab 2constpage2=awaitcontext.newPage();awaitpage2.goto("https://www.google.com");// ... set timer ...// Verify both timers are working independentlyawaitpage1.bringToFront();awaitcontentPage1.verifyTimerVisible();awaitpage2.bringToFront();awaitcontentPage2.verifyTimerVisible();});
During implementation, I encountered several pitfalls.
Honestly, these are unavoidable challenges.
Waiting for Asynchronous Operations
Chrome extensions have many asynchronous operations, so you need to wait appropriately.
This is fundamental in E2E testing, not just for Chrome extensions, but it's especially important here.
// Wait for timer to appearasyncwaitForTimer():Promise<void>{awaitthis.page.waitForSelector("#snack-time-root");}
Can't Use Headless Mode
Chrome extensions don't work in headless mode. You need to set headless: false even in CI.
In CI services like GitHub Actions, you can handle this by using Xvfb.
Pros and Cons of Implementation
Here's what I've learned from actual maintenance:
Item
Content
Rating
Test Feasibility
Popup and content script integration
◎ Achievable
User Operation Reproduction
Some differences from actual operations
△ Compromises needed
Maintainability
Organized with Page Object Pattern
◎ Good
CI/CD Integration
No headless, Xvfb required
△ Additional setup needed
Learning Curve
Need to understand CDP
△ Moderate
Overall, while not perfect, it's sufficiently practical.
Especially with Snack Time using Closed Shadow DOM, operations are difficult.
I'm exploring alternative approaches for this, and I'll write about it if successful.
Conclusion
E2E testing for Chrome extensions is more practical than you might think, isn't it?
Sure, it's not perfect, and there are differences from actual popup behavior.
But being able to write tests that cover major functionality is significant.
Especially being able to test complex behaviors involving multiple contexts and user operation flows provides peace of mind.
When creating Chrome extensions, I encourage you to try E2E testing.
It might feel tedious at first, but once you set it up, it's not that different from regular web apps.
The Snack Time code is available on GitHub, so feel free to reference it if you're interested.