Running Puppeteer with Jest on Github actions for automated testing with coverage.

Liron Navon
Level Up Coding
Published in
8 min readOct 12, 2021

--

Puppeteer is a library that exposes an API for chrome and chromium. I will walk you through the process of creating Github actions (Github’s CI/CD pipeline solution) that can run integration tests using jest, running it inside a container, and producing coverage report.

You can access the full project on Github here: https://github.com/liron-navon/puppeteer-jest-github-actions-example. In this post I will go over it and explain how it works.

Writing a simple test with jest and puppeteer

A test needs a test case, in this case, I will use a simple HTML file, where we have a button (id=”btn”) and a counter (id=”counter”), and a JS file that simply counts the times the button was clicked and update the counter.

<body>
<p>button was clicked: <span id="counter">0</span> times</p>
<button id="btn">click me</button>
<script src="./index.js" type="text/javascript"></script>
</body>

And the index.js file:

const btn = document.querySelector('#btn');
const counter = document.querySelector('#counter');
btn.addEventListener('click', () => {
counter.innerHTML = +counter.textContent + 1;
});

To test this, I created a small test file where we are opening a browser with jest-puppeteer, going to our page before each test, and testing the file by selecting the button, clicking it, and checking the counter.

describe('my first test with jest-puppeteer', () => {  beforeEach(async () => {
await page.goto('http://localhost:9999');
});
it('can count', async () => {
// refer to the elements we need
const counter = await page.$('#counter');
const btn = await page.$('#btn');
// before any click we expect 0
let counterText = await page.evaluate(
el => el.textContent, counter
)
expect(+counterText).toEqual(0);
// cick the button
await btn.click();
// expect counter to increament
counterText = await page.evaluate(el => el.textContent, counter)
expect(+counterText).toEqual(1);
});
})

Configuring Jest and Puppeteer to run locally

Now we need to run jest-puppeteer, so we need a few dev dependencies, the full list is in the repository in package.json, but the main things are:
jest-puppeteer — used to run the tests in puppeteer environment.
jest-puppeteer-Istanbul — which allows us to create a coverage report.
babel-plugin-Istanbul — wraps our code in Istanbul so we can collect coverage.
parcel — will build the project for us with babel so we can use Istanbul and will also act as our development server, it’s similar to Webpack but with no tiring configurations.
jest — the test runner.

And to run our HTML file in a browser we can use parcel:

parcel src/index.html --port 9999

To run our tests and server I created the scripts in package.json “npm test” and “npm start”.

Now we need to add a jest.config.js file where we define how to run the tests and produce coverage. The main take here is using jest-puppeteer as the preset to run tests with puppeteer, and the setup/reporters we take from jest-puppeteer-Istanbul — we will also generate different report files that we will use later in the pipelines.

const config = {
"preset": "jest-puppeteer",
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*"
],
"coverageReporters": [
"text",
"lcov",
"cobertura"
],
"setupFilesAfterEnv": [
"jest-puppeteer-istanbul/lib/setup"
],
"reporters": [
"default",
"jest-puppeteer-istanbul/lib/reporter"
],
"coverageDirectory": "coverage"
}
module.exports = config;

And we need a small .babelrc file where we define the use of the Istanbul plugin in our compilation process. Of course, we do not want that part in production since it will generate a lot of extra code, so we look for the environment variables and only apply the Istanbul plugin in test/development/ci.

const plugins = [];// when run in the CI enviroonment
const isCI = Boolean(process.env.CI || false);
// when run locally directly (calling npm start)
const isDevelopment = process.env.NODE_ENV === "development";
// when run in test environment by jest (npm test)
const isTest = process.env.NODE_ENV === "test";
if (isTest || isDevelopment || isCI) {
plugins.push("istanbul");
}
module.exports = {
plugins: plugins
}

That’s pretty much all we need, you can run start in one terminal, and test in a different terminal, but that’s not very comfortable and not easy to do in a CI environment. Luckily jest-puppeteer allows us to set a configuration file named jest-puppeteer.config.js in the root of our project and it will be automatically picked up by the library.

So we can define how to run our server, and which port jest-puppeteer should look for. If the server is already running it will not run the start command, which is very convenient for development.

The ciPipelineOptions will be used in the CI environment since we need to disable some chrome optimizations to make it run inside a docker environment with Linux, and since our environment have a stable chrome version we can use that instead of chromium, and of course, we will run it in a headless mode since the CI doesn’t have a screen.

const ci = Boolean(process.env.CI || false);
const baseOptions = {
server: {
command: 'npm run start',
port: 9999
}
}
const ciPipelineOptions = {
launch: {
executablePath: '/usr/bin/google-chrome-stable',
headless: true,
args: [
'--ignore-certificate-errors',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-accelerated-2d-canvas',
'--disable-gpu'
]
},
server: baseOptions.server
}
module.exports = ci ? ciPipelineOptions : baseOptions;

Now we can just run “npm test” to run our server and test locally and we will see a message like this. It means all our files are tested and covered, if we miss any tests, jest will let us know which lines are not covered so we can increase our coverage.

Preparing to run the action

Now that we are ready and everything is tested, let’s talk CI pipelines, GitHub Actions require us to have the pipeline files in the directory “.github/workflows” and so I created a tests.yml file inside workflows where we are going to define the pipeline, the full file is in the git repository, so I will talk about it part by part.

We define a workflow called Tests, it will run when we push to master, and when we create a pull request from any branch to master.

name: Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

Then we define jobs, we need one job called CI, where we will run it on a Linux (Ubuntu-20.04) virtual machine (VM), but on the VM, we will run a container that includes all the system dependencies we need for puppeteer to run and a stable chrome browser. Then we will define the CI environment variable, which exists in GitHub actions all the time, but not inside the container.

jobs:
ci:
runs-on: ubuntu-20.04
container: lironavon/docker-puppeteer-container:14.16.0
env:
CI: true
steps:

The first 2 steps are not required, but I always include them in my builds, the first will set up git in the container — this will allow us to use git operations inside the container, the second one is the caching step which will cache all our node modules for faster pipelines.

# add git to the container and set it up with the token
- name: setup git in container
uses: actions/checkout@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
# cache the dependencies from any node_modules directory
- name: cache dependencies
uses: actions/cache@v2
with:
path: '**/node_modules'
key: node_modules-${{ hashFiles('**/package-lock.json')}}

The next 2 steps are pretty simple, we will install dependencies in CI mode, and ignore scripts, do you remember the “executablePath” we defined in jest-puppeteer’s configuration file? If we didn’t define it we would need to let puppeteer run a script that will download its own chromium browser, so here we prevent it from doing so, and the next step is simply to run our tests.

# install the dependencies
- name: install dependencies
run: npm ci --ignore-scripts
# run the tests
- name: test
run: npm test

Until here we can simply push to GitHub and the GitHub action should run and result in passing pipelines, if any test will fail, the pipelines will fail.

We can even produce a badge for our project, indicating the tests are passing, by adding to the README file

![Workflow badge](https://github.com/USER_NAME/REPOSITORY_NAME/actions/workflows/FILE_NAME.yml/badge.svg)

Branch protection

Of course, the main reason to test is to make sure that no one is pushing code that can hurt our project, in Github, we can enforce branch protection by going to settings > branches (1), setting a branch name pattern (2) setting status checks (3) and picking our pipeline job (4) — remember we set “jobs:ci…” in the tests.yml file? So it’s that.

And now when we create a pull request we will see a message telling us the CI status must pass before we can merge the branch.

This will protect our branch so all tests must pass before merging, but we also want to test for coverage.

The next step in the workflow will validate the coverage results for us, we use the coverage-check-action to do it. Pass the lcov file we got from Istanbul, and say that under 90% coverage we will fail the pipeline.

# check coverage
- name: validate coverage
uses: devmasx/coverage-check-action@v1.2.0
with:
type: lcov
result_path: coverage/lcov.info
min_coverage: 90
token: ${{ github.token }}

It will also generate a coverage message for us to check the coverage.

Adding a coverage badge

The last step is purely to add another badge, which is very nice for open source projects:

For this, we can use the coverage-comment-action, which requires us to enable the GitHub wiki feature, where it will store our badge information. Then we can decide what color to give it based on coverage, so under 90% it will be red, between 90% and 100% it will be orange, and it has to be 100% to be green, you can, of course, change it to your individual use case.

# create a badge and make a report of the coverage
- name: display coverage
uses: ewjoachim/coverage-comment-action@v1
with:
GITHUB_TOKEN: ${{ github.token }}
COVERAGE_FILE: cobertura-coverage.xml
BADGE_ENABLED: true
BADGE_FILENAME: coverage-comment-badge.json
MINIMUM_GREEN: 100
MINIMUM_ORANGE: 90

Conclusion

Jest is a great test runner, and puppeteer is amazing when we need to automate browser interactions. GitHub Actions are pretty straightforward once you understand how to work with them, and you should always check coverage when running tests if possible.

Please clap and follow as I will publish content every couple of weeks, I truly appreciate every follower, clapper, and commenter 🙂.

--

--