Here's what we cover in this article:
- Motivation to perform e2e testing on browser extensions.
- Why Puppeteer and known limitations.
- A complete example of a functional chrome extension and a sample React application.
- A complete example of a puppeteer + jest set up to run your automation.
All the code referenced in this article can be found in our repo tweak-extension/puppeteer-test-browser-ext.
If you develop browser extensions, you've probably realized that extensions are complex to test. Unlike the conventional web application where everything happens within the same app, with extensions, you'll constantly interact across different applications - at least the website in the active browser tab and the extension itself. Unit tests are fantastic, they'll help us validate specific aspects of the implementation, but they feel like a tiny grain of sand, when attempting to validate real-world use cases, between two components that run in isolation.
Manual testing and the development lifecycle of extensions, takes a lot of time, much more than traditional applications. Although you might have a nice setup in place, with hot reloads and so on, you'll always have to:
- Make a set of code changes.
- Reload the extension.
- Make sure the latest extension is loaded in the browser - sometimes, a manual extension reload is required.
- Make sure no previous stored data is lingering around in the extension storage.
- Finally, load the extension to interact with the target website and the extension to validate the desired behavior.
It would be amazing to just spin up a browser instance and start interacting with the chrome extension and the target application, just like you do for e2e testing the typical single page application. But how do you even interact with a chrome extension programmatically? Or first, how to load a chrome extension? And how can I keep interacting with both the extension and the website? Seems like a lot of work.
If you have the above questions, this article is for you. The tweak team makes extensive use of Puppeteer to automate our extension tests and simulate real-world use-cases.
"Puppeteer is a Node library that provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default but can be configured to run full (non-headless) Chrome or Chromium.", from pptr.dev
The goal is to run our e2e tests where we interact with both an extension and a target application, just like our users would. Our high-level automation flow is the following:
- Open a browser tab with the target application.
- Load the browser extension and open the popup in a new tab
- Do interactions with the extensions.
- Go back to the target app, check if the extension produced the desired changes on the page.
Why do we open the popup in a new tab? At this time is not possible to test popups (or content scripts).
We've set up a small open-source repo puppeteer-test-browser-ext containing the case study we'll reference in this article to lay clear expectations and make sure you have something tangible moving forward.
Our case study repo contains:
- Replace text extension - a minimalist chrome extension whose only feature is to replace text on a web page. Receives a "from" and a "to" words to replace one with the other.
- Target react app - A React app with a single component. The app has a button that, once clicked, shows a paragraph of text. That's all. This text is the one that our chrome extension is going to modify.
- Puppeteer tests - our end-to-end test and other auxiliary code to make our tests run.
By the end, we'll detail all the steps to achieve the automated test run you see in the below GIF.
Above we saw how the extension looks like. It's composed by two input fields and a button. Below is the most crucial bit of the extension.
We execute a function that replaces text in a target web page using script injection. This is the function we'll call when clicking on the
This extension is a Manifest V3 extension. We're using the API
chrome.scripting.executeScript, which is relatively recent. All the browser extension code can be found in puppeteer-test-browser-ext/replacer-chrome-extension.
We've seen the React app in the previous screenshot. Here's the
App component. Similarly, all the application code can be found in puppeteer-test-browser-ext/src.
Here we'll use the
class attribute to select elements in our test. This is not a good practice, use data attributes test identifiers to make your tests less prone to change due to modifications in the component.
It's a good practice to reflect on what exactly we want to test before getting hands-on. Here's our test plan.
- When a user opens the React application.
- The user should see a button on the web page.
- Then the user clicks the button to display the text.
- When the user goes to the chrome extension.
- When the user writes the word "music" and its replacement "**TEST**" in the extension and the user clicks on the "replace" button.
- When the user goes back to the website.
- Then the user should see the string "**TEST**" on the page.
- Then the user should no longer see the string "music" on the page.
Now that we have the test case, let's implement it. We'll describe the most critical aspects of how we set up our tests within our sample codebase, and last we'll deep dive into the specifics of the test implementation.
Here we provide a bit more insights into our test case setup. We have:
package.jsonwhere you'll find
"puppeteer"amongst all the pre-packed create-react-app dependencies.
jest.config.jsfile that contains a minimalist setup.
- An ejected
create-react-app, and its code under
e2ewhere we'll write our test,
e2e/sampleTest.spec.jscontains the test implementation and
e2e/bootstrap.jscontains auxiliary code to run our tests.
Jest has the role of test runner. Puppeteer is the glue between your test and an actual browser session. You'll be able to perform assertions and use the whole Jest API as per usual.
You want to increase the default global value for a test to timeout. E2E tests might take up several seconds to run. If you're using Jest, you can configure the timeout value with the property
e2e/bootstrap.js you'll find all the code necessary to load the target application and the browser extension into a puppeteer-controlled session.
Let's break down this script into smaller steps.
First we must run
There are a few things to notice here:
- Puppeteer does not load extensions in the headless mode, thus the
- We parameterize
slowMobecause they are incredibly relevant for debugging.
- We use the options
--disable-extensions-exceptto make sure we bootstrap a clean environment where our extension is the only one loaded in the session.
Following open a page with the target app.
Next, we find the extension id, which will allow us to load the extension in a new tab by referencing the
chrome-extension:// namespace to access the extension resources.
🥚 How to retrieve the extension id for a Manifest V2 extension?
Once we figure out the id, we load the extension in a tab, and similarly to the application, we grab a page reference and return it.
Toggle to see the full implementation
The most complex work is done. We can reference our test plan and now focus our efforts on implementing the test itself.
The test is split into 3 phases. Let's look into them.
In the first two steps we check that the target app is loaded, and we make sure the text we want to replace is expanded by clicking on the
"show text" button.
Following steps 3 and 4, we switch to the extension, set up a replacement word, and click the
Last, in step 5, we verify the text is replaced in the target app.
Toggle to see the full implementation
At last, let's run our test. We just need to make sure we're serving our react app and trigger the test run.
If you look closely at the extension implementation, at the top of the popup.js file, we write some additional logic to ensure the extension can figure which browser to interact with. This is not an ideal solution but the only workaround that we know. Because we can not interact with the popup, we need to load the extension in a new tab, which adds the overhead on the application to be flexible enough to discover the proper tab, depending on the execution context. When running automation tests vs. when running in a production environment. We assume that during the puppeteer session, the target application will always be loaded in the tab with index
We hope this guide gets you started on your journey to test your chrome extension. You can find all this code and more in tweak-extension/puppeteer-test-browser-ext.
If you liked this article, consider sharing (tweeting) it to your followers.