Gideon Pyzer - Web Developer

Go back

Muppeteer

Muppeteer came about after a need to replace PhantomCSS from our tech stack. PhantomJS is no longer being supported, and with Chrome's Remote Debugging Protocol, it's now possible to interact with the page and the browser instance itself from Node.

PhantomCSS is a library for supporting visual regression testing. It works by taking a screenshot of a component or page during the test, and comparing it to a baseline screenshot (versioned in GIT). If the difference is significant enough (based on a configured threshold), the test fails. But why is this important?

Visual Regression Testing

Unit tests will cover functional code, and so we can test whether a value is as expected, and we can also test whether an element exists on the page. However, how do we verify that it looks right? For example, someone accidentally sets a global style that adds padding to every element. The tests would still pass fine under a typical testing model.

You can check out a presentation I gave on PhantomFlow, which introduces the topic.

How does Muppeteer work?

Muppeteer is a test framework for running UI tests in Chrome. It is composed of:

  • Mocha - a test runner
  • Chai - an assertion library
  • Puppeteer - a Node library for interacting with Chrome via RDP
  • Pixelmatch - a pixel-level image comparison library

Muppeteer provides a convenient test API which abstracts away boilerplate setup code. It's loosely based on PhantomCSS, which runs visual comparisons of images in a (deprecated) PhantomJS world.

const container = '.todoapp';
const input = 'header input';
const listItem = '.todo-list li';
const firstItem = listItem + ':nth-of-type(1)';
const firstItemToggle = firstItem + ' .toggle';
const firstItemRemoveButton = firstItem + ' button';
const secondItem = listItem + ':nth-of-type(2)';
const todoCount = '.todo-count';

describeComponent({name: 'todomvc', url: 'http://todomvc.com/examples/react/#/'}, function() {
    describe('Add a todo item', async function() {
        it('typing text and hitting enter key adds new item', async function() {
            await Muppeteer.page.waitForSelector(input);
            await Muppeteer.page.type(input, 'My first item');
            await Muppeteer.page.keyboard.press('Enter');
            await Muppeteer.page.waitForSelector(firstItem);
            Muppeteer.assert.equal(await Muppeteer.page.getText(firstItem), 'My first item');
            await Muppeteer.assert.visual(container);
        });
        it('clicking checkbox marks item as complete', async function() {
            await Muppeteer.page.waitForSelector(firstItemToggle);
            await Muppeteer.page.click(firstItemToggle);
            await Muppeteer.page.waitForNthSelectorAttributeValue(listItem, 1, 'class', 'completed');
            await Muppeteer.assert.visual(container);
        });
        it('typing more text and hitting enter adds a second item', async function() {
            await Muppeteer.page.type(input, 'My second item');
            await Muppeteer.page.keyboard.press('Enter');
            await Muppeteer.page.waitForSelector(secondItem);
            Muppeteer.assert.equal(await Muppeteer.page.getText(secondItem), 'My second item');
            await Muppeteer.assert.visual(container);
        });
        it('hovering over first item shows x button', async function() {
            await Muppeteer.page.hover(firstItem);
            await Muppeteer.assert.visual(container);
        });
        it('clicking on first item x button removes it from the list', async function() {
            await Muppeteer.page.click(firstItemRemoveButton);
            await Muppeteer.page.waitForElementCount(listItem, 1);
            Muppeteer.assert.equal(await Muppeteer.page.getText(todoCount), '1 item left');
            await Muppeteer.assert.visual(container);
        });
    });
});