Automate the UI Testing of your chrome extension

Building a chrome extension is definitely a fun process! Chrome extensions open a whole new set of doors to the web developers and users. However, testing those awesome extensions is not as straight forward as testing any conventional web application in some aspects. In this post, Let's walk together with the path of adding our first test case that ensures the best for our extensions.

Why automate on the first place

The manual testing process is one of the boring stuff in Software Engineering 😆 With the various aspects such as new install, extension update, permission update, extension downgrade/delete of the Chrome extension, the process got a lot trickier and bored. It's really easier to miss testing few aspects on every release. Thus, automating these boring stuff can ensure the proper working of our extension during every single release.

How testing can be done

We will be testing a chrome extension using Puppeteer and structure our tests with the mocha test runner. Also, we'll see how to automate this testing process in your CI/CD process using CircleCI. You can use any of your favorite test runner and CI/CD tool.

Let's install our dependencies first,

yarn add puppeteer mocha -D

or

npm i puppeteer mocha --dev

We can test our chrome extensions with the help of Puppeteer by mimicking the steps we would follow in our manual testing process.

Open Chrome Browser

Load the unpacked version of the extension (via chrome://extensions page - dev mode)

Open our extension popup/index page

Test the targeted features

Let's automate those steps one by one. For better understanding, Kindly test the script we are building at each step by running them (node test.js) then and there.

Step 1: Open Chrome Programmatically

As a first step, we need to control Chrome programmatically. That exactly where Puppeteer helps us. As per the docs, Puppeteer is a Node library which provides a high-level API to control headless (and full non-headless) Chrome. In our case, we need to boot Chrome in full form as extensions can load only in full form.

// test.jsconstpuppeteer=require('puppeteer');letbrowser=awaitpuppeteer.launch({headless:false,// extension are allowed only in head-full mode});

On running the script (node test.js), The chromium build will be boot up with an empty page. Kill the node process to close the Chromium browser.

Step 2: Load Extensions

Next up, need to load our extension into chrome. Extensions can be load into the browser instance using --load-extension flag given by Puppeteer. Additionally, we need to disable all other extensions to prevent any unnecessary noise using --disable-extensions-except flag.

// test.jsconstextensionPath=<path-to-your-extension>;// For instance, 'dist'constbrowser=awaitpuppeteer.launch({headless:false,// extension are allowed only in the head-full modeargs:[`--disable-extensions-except=${extensionPath}`,`--load-extension=${extensionPath}`]});

On running this script, Chrome instance will be booted along with your extension. You can find your extension logo on the toolbar menu.

Step 3: Go to the extension popup page

Extension popup/index page will open when we click on the extension icon in the toolbar menu. The same page can be opened directly using the chrome-extension URL for the easier testing process. A normal extension page URL will be like chrome-extension://qwertyasdfgzxcvbniuqwiugiqwdv/index.html. This URL can be dissected into,

Extension Protocol (chrome-extension)

Extension ID (qwertyasdfgzxcvbniuqwiugiqwdv)

Popup/Index page path (index.html)

We need to construct this kind of URL for our extension in order to visit the page. Here the unknown part is the Extension ID. Thus, we need to know the arbitrary ID of our the extension generated by Chrome.

Know your extension ID: The Proper way

Chrome will assign a unique extension ID to every extension when loaded. This will be random every time we boot the extension on a new Chrome instance. However, a stable extension ID specific for our extension can be set by following the steps mentioned in this SO answer. This will be a bit long process but fool-proof. We can safely rely on the stable ID to test our extensions as the ID will not change when booted in various Chrome instance using Puppeteer.

Know your extension ID: The Background Script way

However, if our extension got background scripts, then the process would be a bit straight forward. We can detect the Extension ID programmatically.

When using background scripts, Chrome will create a target for the background script as soon as the extension gets loaded (Step 2). All the page targets managed by Chrome can be accessed by the targets method of the booted browser instance. using these targets, we can pull out our specific extension target with the help of title property (which will be our extension title given in the manifest.json). This target will contain the random extension ID assigned by Chrome during the current boot up.

// test.js// This wait time is for background script to boot.// This is completely an arbitrary one.constdummyPage=awaitbrowser.newPage();awaitdummyPage.waitFor(2000);// arbitrary wait time.constextensionName=<name-of-your-extension>// For instance, 'GreetMe'consttargets=awaitbrowser.targets();constextensionTarget=targets.find(({_targetInfo})=>{return_targetInfo.title===extensionName&&_targetInfo.type==='background_page';});

Once you fetch your extension target, we can extract the ID from the target URL. A sample background target url will be like, chrome-extension://qwertyasdfgzxcvbniuqwiugiqwdv/background.html. So, the extraction will be like:

En Route to the Extension page 🚌

Now, let's go to our extension page. For this, we need to create a new browser page and load the appropriate extension popup URL.

// test.js// This is the page mentioned in `default_popup` key of `manifest.json`constextensionPopupHtml='index.html'constextensionPage=awaitbrowser.newPage();awaitextensionPage.goto(`chrome-extension://${extensionID}/${extensionPopupHtml}`);

At this point, running the test script will boot up a new Chrome instance and open a new Page with your extension popup HTML page content as a usual web page.

Step 4: Test the targeted features

We have successfully booted up our extension page. It's time for a 🖐

Now, let's pour our web app testing knowledge here. As every web application, end-to-end testing can be done using DOM querying and asserting for the proper value. The same can be applied here. DOM of our extension page can be queried using the $ (querySelector) and $$ (querySelectorAll) APIs provided by Puppeteer. You can use your preferred assertion library. In this example, I'm using the node's native assert package.

// test.jsconstassert=require('assert');constinputElement=awaitextensionPage.$('[data-test-input]');assert.ok(inputElement,'Input is not rendered');

Events can be triggered on the extension page using various event APIs provided by the Puppeteer.

Joining all the pieces

// test.jsconstpuppeteer=require('puppeteer');constassert=require('assert');constextensionPath='src';letextensionPage=null;letbrowser=null;describe('Extension UI Testing',function(){this.timeout(20000);// default is 2 seconds and that may not be enough to boot browsers and pages.before(asyncfunction(){awaitboot();});describe('Home Page',asyncfunction(){it('Greet Message',asyncfunction(){constinputElement=awaitextensionPage.$('[data-test-input]');assert.ok(inputElement,'Input is not rendered');awaitextensionPage.type('[data-test-input]','Gokul Kathirvel');awaitextensionPage.click('[data-test-greet-button]');constgreetMessage=awaitextensionPage.$eval('#greetMsg',element=>element.textContent)assert.equal(greetMessage,'Hello, Gokul Kathirvel!','Greeting message is not shown');})});after(asyncfunction(){awaitbrowser.close();});});asyncfunctionboot(){browser=awaitpuppeteer.launch({headless:false,// extension are allowed only in head-full modeargs:[`--disable-extensions-except=${extensionPath}`,`--load-extension=${extensionPath}`]});constdummyPage=awaitbrowser.newPage();awaitdummyPage.waitFor(2000);// arbitrary wait time.consttargets=awaitbrowser.targets();constextensionTarget=targets.find(({_targetInfo})=>{return_targetInfo.title==='GreetMe';});constextensionUrl=extensionTarget._targetInfo.url||'';const[,,extensionID]=extensionUrl.split('/');constextensionPopupHtml='index.html'extensionPage=awaitbrowser.newPage();awaitextensionPage.goto(`chrome-extension://${extensionID}/${extensionPopupHtml}`);}

we can run this script by invoking the mocha command.

mocha test.js

let's create an npm script in package.json to map the mocha command,

"scripts": {
"test": "mocha test.js"
}

It would invoke the test and output the test case status in the terminal.

We have done creating our first test suites that test our extension page. It's time to wire this up with a CI overflow. I'm using CircleCI for this demo. We can use any such services like TravisCI, AppVeyor, etc.,

Wiring up with CI

create a config file for CircleCI, .circleci/config.yml and load up a few boilerplate steps. We will be using an image called circleci/node:8.12.0-browsers as this image has chrome pre-installed and we need not install any further dependencies. If you are using any other services, find an appropriate image with browsers pre-built.

OoOHoO... Congrats again. We just automated our test process successfully 🔥🔥 Try to automate your existing and future extension's testing process and be calm on your future releases. The sample extension along with their (working) tests has been hosted in GitHub. If you need any help, you can refer to the source code.

Hope you find this piece of writing useful. If so, I wrote about automating the chrome extension deployment in your CI/CD process in this blog post. Check out if you are manually deploying your extension. This may be the time to automate that too 😉