NightwatchJS on Dockerized Chrome

I spent some time this week setting up a webapp repo with NightwatchJS for UI automation testing. It’s not a framework I have experience with (I’m more used to Pytest) but we had some existing Nightwatch tests in the organization so I decided to roll with it.

I like the idea of writing UI automation testing in JS instead of Python. Typically it’s the frontend engineers who write the UI automation anyway, and inevitably pieces of the frontend implementation (e.g. CSS selectors) end up in the automation tests. Having those tests in the same language and repo as the frontend app seems intuitive.

Here was my success criteria: - have the Nightwatch tests in the same repo as the application - be able to run the tests on a local machine with minimal tooling - be able to run them in a docker container for CI/CD pipelines - have the tests run against the actual application in staging, not a local version

It was pretty straightforward, with the usual obnoxious hiccups around getting Chrome to cooperate. There’s lots of outdated information out there about how you need to install Selenium, Xvfb, Chromedriver binaries, etc. That might have been true with older versions of Nightwatch but it’s pretty clean now. You can use Chromedriver as an NPM module and keep the tooling dependencies to a minimum.

My package.json:

{
  "devDependencies": {
    "chromedriver": "^89.0.0",
    "nightwatch": "^1.6.2"
  }
}

That’s it. The critical pieces (and the gotchas) come in your nightwatch.conf.js file.

module.exports = {
  webdriver: {
    start_process: true,
    server_path: "node_modules/.bin/chromedriver",
    port: 9515
  },
  test_settings: {
    desiredCapabilities: {
      browserName: "chrome",
      chromeOptions: {
        args: ["--headless", "--no-sandbox", "--disable-gpu"]
      },
      alwaysMatch: {
        acceptInsecureCerts: true
      }
    }
  }
}

You may not need acceptInsecureCerts but in my case I needed to workaround not being able to resolve SSL certs for an internal application.

One of the test cases I wrote needed to validate that a CSV file could be downloaded from our React app. Normally in Chrome you get a download confirmation window if you click a download link, which is a pain to deal with in a test. Here’s how to update your nightwatch.conf.js file to get downloads to be automatic and land in the directory of your choosing:

{
  chromeOptions: {
    prefs: {
      download: {
        default_directory: require("path").resolve(__dirname + "/tmp"),
        prompt_for_download: false,
      },
      profile: {
        default_content_setting_values: {
          automatic_downloads: 1,
        }
      },
    }
  }
}

As for the Dockerized approach, my Dockerfile is FROM an internal Centos7 image.

# install chrome
RUN yum install -y wget && \
  wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm

# set up the project
RUN mkdir /automation
COPY . /automation
WORKDIR /automation
RUN npm install

# start the nightwatch tests
CMD npx nightwatch

Since we’re using the CMD directive, all we have to do is build the image and then run it and the tests will begin immediately.

docker build . -t automation:local
docker run automation:local

As for actually working with Nightwatch…I like it. The API is pretty well documented and it has all the features you need for basic browser automation. And as you can see, setup was a breeze. There’s two aspects of the API that I think could be improved, though.

First is the assertion APIs. Nightwatch offers assert, verify, and expect, each of which work differently. But all three variants are tied to the context of a WebElement: assert/verify take a selector as the first argument, and expect is supposed to be chained with the element finder API.

What if I just want to fail the test if some variable does not equal a value? Do I really have to add another dependency like Chai or be a barbarian and use an if with a throw new Error?

The other thing is that the docs encourage you to use PageObject, but that API is not well-integrated with Nightwatch’s general browser API. I really like the PageObject abstraction layer. It keeps your test logic concise, and gathers potentially brittle element selectors in one module.

The problem is that there are critical Nightwatch features (like window management, pausing, grabbing all elements that match a selector) that are only available from the browser API. Nightwatch wants you to chain together calls but there’s no graceful way to swap back and forth between browser APIs like pause and your PageObject custom commands.

It’s possible there are idiomatic solutions to these initial problems that I’ll discover as I spend more time with Nightwatch. But so far, I’m impressed.