loading...
Cover image for Automate the UI Testing of your chrome extension

Automate the UI Testing of your chrome extension

gokatz profile image Gokul Kathirvel Originally published at gokatz.me ・8 min read

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

// 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. 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.
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:

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.


// 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) 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.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.

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 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 to structure our tests.

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

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

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.

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

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

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.

Posted on by:

gokatz profile

Gokul Kathirvel

@gokatz

A fellow Human and frontend dev @Zoho - loves EmberJs. Just Started with VueJs

Discussion

markdown guide