Automate the UI Testing of your chrome extension
Tags: testing, chromeextension, automation, javascript
Published: 2019-04-22 16:04:02

---
title: Automate the UI Testing of your chrome extension
published: true
description: UI testing the chrome extension (or any extension for that matter) is tricky as it has its own workflow and environment to run. This post will walk you through the process of automating those test using Puppeteer, an API library to control Chrome.
canonical_url: https://gokatz.me/blog/automate-chrome-extension-testing/
tags: #testing, #chromeextension, #automation, #js
series: Chrome Extension Toolkit
cover_image: https://thepracticaldev.s3.amazonaws.com/i/xvuekr26799h269bdp9z.png
---

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,

```bash
yarn add puppeteer mocha -D
```
or

```bash
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.

```js
// test.js

const puppeteer = require('puppeteer');

let browser = await puppeteer.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.

```js
// test.js

const extensionPath = <path-to-your-extension>; // For instance, 'dist'

const browser = await puppeteer.launch({
    headless: false, // extension are allowed only in the head-full mode
    args: [
        `--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](https://stackoverflow.com/questions/23873623/obtaining-chrome-extension-id-for-development/23877974#23877974). 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.

```js
// test.js

// This wait time is for background script to boot.
// This is completely an arbitrary one.
const dummyPage = await browser.newPage();
await dummyPage.waitFor(2000); // arbitrary wait time.

const extensionName = <name-of-your-extension> // For instance, 'GreetMe'

const targets = await browser.targets();
const extensionTarget = 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:

```js
const extensionUrl = extensionTarget._targetInfo.url || '';
const [,, extensionID] = extensionUrl.split('/');
``` 

We successfully got our extension ID (by either way) 💪


### 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.

```js

// test.js

// This is the page mentioned in `default_popup` key of `manifest.json`
const extensionPopupHtml = 'index.html'

const extensionPage = await browser.newPage();
await extensionPage.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`)](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pageselector)  and [`$$` (`querySelectorAll`)](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pageselector) APIs provided by Puppeteer. You can use your preferred assertion library. In this example, I'm using the node's native [`assert`](https://nodejs.org/api/assert.html) package.

```js
// test.js

const assert = require('assert');

const inputElement = await extensionPage.$('[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. 

```js
await extensionPage.type('[data-test-input]', 'Gokul Kathirvel');
await extensionPage.click('[data-test-greet-button]');
const greetMessage  = await extensionPage.$eval('#greetMsg', element => element.textContent)
assert.equal(greetMessage, 'Hello, Gokul Kathirvel!', 'Greeting message is not shown');
```

**NOTE:** Puppeteer got a lot of useful [APIs](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md) to control and extract useful information from Chrome.

### Use test runners

In order to patch tests in a meaningful manner and to get good visual feedback and, we can use a test runner. In this example, I'm going to demonstrate how to use [`mocha`](https://mochajs.org/) to structure our tests. 

```js
// test.js

describe('Home Page', async function() {
  it('Greet Message', async function() {
    const inputElement = await extensionPage.$('[data-test-input]');
    assert.ok(inputElement, 'Input is not rendered');
  
    await extensionPage.type('[data-test-input]', 'Gokul Kathirvel');
    await extensionPage.click('[data-test-greet-button]');
  
    const greetMessage  = await extensionPage.$eval('#greetMsg', element => element.textContent)
    assert.equal(greetMessage, 'Hello, Gokul Kathirvel!', 'Greeting message is not shown');
  })
});
```


## Joining all the pieces

Let's join all pieces to create a completely automated test suite for your extension.

```js
// test.js

const puppeteer = require('puppeteer');
const assert = require('assert');

const extensionPath = 'src';
let extensionPage = null;
let browser = 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(async function() {
    await boot();
  });

  describe('Home Page', async function() {
    it('Greet Message', async function() {
      const inputElement = await extensionPage.$('[data-test-input]');
      assert.ok(inputElement, 'Input is not rendered');
    
      await extensionPage.type('[data-test-input]', 'Gokul Kathirvel');
      await extensionPage.click('[data-test-greet-button]');
    
      const greetMessage  = await extensionPage.$eval('#greetMsg', element => element.textContent)
      assert.equal(greetMessage, 'Hello, Gokul Kathirvel!', 'Greeting message is not shown');
    })
  });

  after(async function() {
    await browser.close();
  });
});

async function boot() {
  browser = await puppeteer.launch({
    headless: false, // extension are allowed only in head-full mode
    args: [
      `--disable-extensions-except=${extensionPath}`,
      `--load-extension=${extensionPath}`
    ]
  });

  const dummyPage = await browser.newPage();
  await dummyPage.waitFor(2000); // arbitrary wait time.

  const targets = await browser.targets();
  const extensionTarget = targets.find(({ _targetInfo }) => {
    return _targetInfo.title === 'GreetMe';
  });

  const extensionUrl = extensionTarget._targetInfo.url || '';
  const [,, extensionID] = extensionUrl.split('/');
  const extensionPopupHtml = 'index.html'

  extensionPage = await browser.newPage();
  await extensionPage.goto(`chrome-extension://${extensionID}/${extensionPopupHtml}`);
}
```

we can run this script by invoking the `mocha` command.

```bash
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.

```bash
$ yarn test
$ mocha test.js


  Extension UI Testing
    Home Page
      ✓ Greet Message (142ms)


  1 passing (5s)
```

Congrats, You made far to the end 🤝

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.

```yml
version: 2
jobs:
  build:
    docker:
      - image: circleci/node:8.12.0-browsers

    working_directory: ~/repo

    steps:
      - checkout
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          
          # fall back to using the latest cache if no exact match is found
          - v1-dependencies-

      # Install your dependencies
      - run: yarn install

      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}

      # build the extension if required
      # Run our test suite 
      - run: yarn test
```

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](https://github.com/gokatz/greet-me-extension). 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](https://dev.to/gokatz/automate-your-chrome-extension-deployment-in-minutes-48gb/). Check out if you are manually deploying your extension. This may be the time to automate that too 😉

{% link https://dev.to/gokatz/automate-your-chrome-extension-deployment-in-minutes-48gb %}

That's all for today. Let's meet at another time with some other exciting stuff. Bye for Now. If you got any feedback or suggestion, please post in the comment. I would love to work on that.