How to take screenshots with Puppeteer and Jest
Puppeteer is a headless browser that lets you load your website, program user interactions, and capture screenshots and videos. This can be highly useful for documentation, marketing for the App Store, blog posts, comparison testing etc.
Note: For this article I'm going to use an example project with a simple input form: https://github.com/omichelsen/puppeteer-example.
Taking screenshots with Puppeteer
Install Puppeteer in your project: yarn add -D puppeteer
and create a js file with the following:
import puppeteer from 'puppeteer'
// create a new browser and open a page
const browser = await puppeteer.launch()
const page = await browser.newPage()
// set the size of the browser window
await page.setViewport({ width: 500, height: 300, deviceScaleFactor: 2 });
// open the URL you want to capture and wait for loading to complete
await page.goto(`http://localhost:4000`, {
waitUntil: 'networkidle0',
})
// take a screenshot of the window
await page.screenshot({ path: 'page.png' })
// close the browser (or the script will hang)
await browser.close()
Running this will give us a nice screenshot of the whole page:
If you want to capture just a single component (could be a button or a whole table), replace the line await page.screenshot(...)
with this:
// take a screenshot of the input component
const element = await page.$('input')
await element.screenshot({ path: 'component.png' })
This gives us a screenshot of only the input element:
Screenshot testing with Puppeteer and Jest
With screenshot testing you can compare snapshots of a whole page or a single component to catch unintentional changes. This is particularly useful when refactoring CSS or higher level components where changes can cascade to other parts of the app.
Here we are using the following packages: yarn add -D puppeteer jest jest-image-snapshot
import puppeteer from 'puppeteer'
// extend Jest with expect - usually put this in a Jest setup file (setupFilesAfterEnv)
import { toMatchImageSnapshot } from 'jest-image-snapshot'
expect.extend({ toMatchImageSnapshot })
describe('puppeteer-example', () => {
let browser
let page
beforeAll(async () => {
browser = await puppeteer.launch()
page = await browser.newPage()
})
afterAll(() => browser.close())
it('page renders correctly', async () => {
await page.goto('http://localhost:4000/')
const image = await page.screenshot()
expect(image).toMatchImageSnapshot()
})
it('component renders correctly', async () => {
await page.goto('http://localhost:4000/')
const element = await page.$('input')
const image = await element.screenshot()
expect(image).toMatchImageSnapshot()
})
})
The snapshots will be written on the first run and the output will look like this:
PASS ./screenshot.test.js
puppeteer-example
✓ page renders correctly (476 ms)
✓ component renders correctly (112 ms)
› 2 snapshots written.
Now for the testing part: let's say you go and change the placeholder
text of the input field. Our tests will now fail with a visual diff:
FAIL ./screenshot.test.js
puppeteer-example
✕ page renders correctly (326 ms)
✕ component renders correctly (128 ms)
● puppeteer-example › component renders correctly
Expected image to match or be a close match to snapshot but was 2.485963356973995% different from snapshot (673 differing pixels).
See diff for details: /Users/olem/projects/puppeteer-example/__image_snapshots__/__diff_output__/screenshot-test-js-puppeteer-example-component-renders-correctly-1-snap-diff.png
The diff output shows a before and after (left and right) and an overlay in the middle marking differences with yellow (before) and red (after).
We are now protected against unexpected changes to our app! In case the change was intentional, update your snapshots by running jest -u
.
Snapshots are saved to the folder __image_snapshots__
which should be commited to git for comparison on future runs.
E2E testing and video recording with Puppeteer and Jest
I thought I'd show how to write end-to-end (E2E) tests using Puppeteer since the code is very similar. This also is a nice use case for recording videos. Since Puppeteer is headless (no UI), videos can help debug complex user interaction flows of an E2E test if a test unexpectedly fails.
Recording videos can also be a powerful tool to produce marketing videos (e.g. to show in the App Store), examples for documentation etc.
Here we are using the following packages: yarn add -D puppeteer jest puppeteer-screen-recorder
. For this test we're going to fill out the input field and click the submit button:
import puppeteer from 'puppeteer'
import { PuppeteerScreenRecorder } from 'puppeteer-screen-recorder'
describe('puppeteer-example e2e', () => {
let browser
let page
let recorder
beforeAll(async () => {
browser = await puppeteer.launch()
page = await browser.newPage()
await page.setViewport({ width: 500, height: 300, deviceScaleFactor: 2 })
// record the e2e test
recorder = new PuppeteerScreenRecorder(page)
})
afterAll(() => browser.close())
it('should greet me', async () => {
// start recording
await recorder.start('./e2e.mp4')
await page.goto(`http://localhost:4000`, {
waitUntil: 'networkidle0',
})
// type in the name and submit
await page.type('input', 'Mr McDuck')
await page.click('button')
// find the text element and assert that the name is shown
const text = await page.$eval('h2', (e) => e.textContent)
expect(text).toContain('Hi, Mr McDuck!')
// stop recording
await recorder.stop()
})
})
We now have a passing test of our example form as you can see here:
You do not have to use Jest to record videos, you can also use puppeteer-screen-recorder
in a normal script like in our first example.
These are just some of the use cases I have found very helpful when developing, testing and marketing web apps. If you have other ideas how to use Puppeteer, please let me know in the comments below!