dferber90 / jsdom-screenshot

📸 Take screenshots of jsdom with puppeteer
https://github.com/dferber90/visual-regression-testing-example
MIT License
65 stars 18 forks source link
jsdom puppeteer screenshot snapshot visual-regression-testing

jsdom-screenshot

Generate screenshots of JSDOM.

⚠️ This package is useful for visual regression testing, but highly experimental.

If you just want visual regression testing that works, I'd recommend using a CI service for it. Otherwise you'll run differences due to different operating systems, font-rendering, animations and even GPUs.

This package will only give you the image, you'll have to diff it with something else (like jest-image-snapshot). If you are using Jest, you might be interested in jest-transform-css, which allows you to load styles into your Jest test setup.

This package can be paired with jest-transform-css and jest-image-snapshot to enable Visual Regression Testing in Jest. See jest-transform-css for more information.

Table of Contents

Install

npm install jsdom-screenshot --save-dev

Usage

You must be in a jsdom environment.

import { generateImage } from "jsdom-screenshot";

// add some content to jsdom (this could also be React or any other library!)
const div = document.createElement("div");
div.innerHTML = "Hello World";
document.body.appendChild(div);

// take screenshot
generateImage();

Usage in Jest, React & react-testing-library

It is recommended to use this package with jest-image-snapshot and react-testing-library. Use it as together like this:

import React from "react";
import { generateImage, setDefaultOptions } from "jsdom-screenshot";
import { render } from "react-testing-library";
import { SomeComponent } from "<your-code>";

it("should have no visual regressions", async () => {
  render(<SomeComponent />);
  expect(await generateImage()).toMatchImageSnapshot();
});

You probably want to use a setupTestFrameworkScriptFile like this:

// react-testing-library setup
import "jest-dom/extend-expect";
import "react-testing-library/cleanup-after-each";
// set up visual regression testing
import { toMatchImageSnapshot } from "jest-image-snapshot";
import { setDefaultOptions } from "jsdom-screenshot";

// TravisCI and Linux OS require --no-sandbox to be able to run the tests
// https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-on-travis-ci
setDefaultOptions({
  launch: { args: process.env.CI === "true" ? ["--no-sandbox"] : [] }
});

// give tests more time as taking screenshots takes a while
jest.setTimeout(10000);

expect.extend({ toMatchImageSnapshot });

API

generateImage(options)

generateImage is the main function you're going to use to take a screenshot of the JSDOM. It supports these options.

Tip: You can use react-testing-library's fireEvent to get the component into any state before taking the screenshot.

Options

options = {
  // Options used to launch Puppeteer (puppeteer.launch(options))
  launch: {},
  // Options used to take a screenshot (puppeteer.screenshot(options))
  screenshot: {},
  // An array of folders containing static files to be served
  serve: ["public", "assets"],
  // Prints the jsdom markup to the console before taking the screenshot
  debug: true,
  // Wait for resources to be loaded before taking the screenshot
  waitUntilNetworkIdle: false,
  // Shortcut to set launch.defaultViewport
  viewport: {},
  // Enables request interception
  intercept: () => {}
};
options.launch

launch options are passed to puppeteer.launch([options]), see docs.

options.screenshot

screenshot options are passed to page.screenshot([options]), see docs.

options.serve

serve is an array of strings. You can provide a list of folders to serve statically. This is useful when your component uses assets through relative links like <img src="https://github.com/dferber90/jsdom-screenshot/raw/master/party-parrot.gif" />.

In this case, you could provide serve: ["images"] when the images folder at the root of your project (where you launch the tests from) contains party-parrot.gif.

options.debug

Prints the jsdom markup to the console before taking the screenshot.

See the Debugging JSDOM section below for more information.

options.viewport

This is a shortcut to set options.launch.defaultViewport. options.launch.defaultViewport will take precedence in case both are passed.

options.targetSelector

A CSS selector can be provided to take a screenshot only of an element found by given selector. This will set puppeteers options.screenshot.clip to match the given element's offset properties (offsetLeft, offsetTop, offsetWidth and offsetHeight).

Example:

import React from "react";
import { generateImage, setDefaultOptions } from "jsdom-screenshot";
import { render } from "react-testing-library";
import { SomeComponent } from "<your-code>";

it("should have no visual regressions", async () => {
  // display: "table" prevents div from using full width,
  // so the screenshot would not cover the full width here
  render(
    <div data-testid="root" style={{ display: "table" }}>
      <SomeComponent />
    </div>
  );

  const image = await generateImage({
    targetSelector: "[data-testid=root]"
  });
  expect(image).toMatchImageSnapshot();
});
options.waitUntilNetworkIdle

When set to true, jsdom-screenshot will wait until the network becomes idle (all resources are loaded) before taking a screenshot. You can use this to ensure that all resources are loaded before the screenshot is taken.

It is disabled by default as it adds roughly one second to each screenshot. Use it wisely to avoid slowing down tests unnecessarily. You can mock requests using options.intercept.

options.intercept

When provided, puppeteer's request interception will be enabled. The provided function will be called with the intercepted request.

Activating request interception enables request.abort, request.continue and request.respond methods. This provides the capability to modify network requests that are made by a page.

This can be used to speed up tests by stubbing requests.

generateImage({
  intercept: request => {
    if (request.url().endsWith(".png") || request.url().endsWith(".jpg")) {
      // Blocks some images.
      request.abort();
    } else if (request.url().endsWith("/some-big-library.css")) {
      // Fake a response
      request.respond({
        status: 200,
        contentType: "text/css",
        body: "html, body { background: red }"
      });
    } else {
      // Call request.continue() for requests which should not be intercepted
      request.continue();
    }
  }
});

See page.setintercept of puppeteer.

Changing viewport

Puppeteer will use an 800x600 viewport by default. You can change the viewport by passing launch.defaultViewport:

generateImage({
  launch: {
    defaultViewport: { width: 1024, height: 768 }
  }
});

As this is a lot of typing, there is a shortcut for it:

generateImage({ viewport: { width: 1024, height: 768 } });

launch.defaultViewport / viewport also supports deviceScaleFactor, isMobile, hasTouch and isLandscape.

See launch.defaultViewport.

setDefaultOptions(options)

Having to reapply the same options for every test is pretty inconvenient. The setDefaultOptions function can be used to set default options for every generateImage call. Any options passed to the generateImage call will get merged with the specified defaultOptions.

This function can be used to provide global defaults. Note that these defaults are global for all generateImage calls. You should typically only call setDefaultOptions once in your test-setup file.

For example with Jest, you could do the following in your setupTestFrameworkScriptFile file:

import { setDefaultOptions } from "jsdom-screenshot";

/*
  TravisCI requires --no-sandbox to be able to run the tests.
  We the launch options globally here so that they don't need to be
  repeated for every `generateImage` call.
*/
setDefaultOptions({
  launch: { args: process.env.CI === "true" ? ["--no-sandbox"] : [] }
});

restoreDefaultOptions()

The restoreDefaultOptions function restores the default options provided by jsdom-screenshot. See setDefaultOptions for a usage example.

debug(element)

Logs the JSDOM contents to the console. See Debugging for more information.

How it works

High level

jsdom is an emulator of a subset of browser features. jsdom does not have the capability to render visual content, and will act like a headless browser by default. jsdom does not do any layout or rendering ref. We use jsdom to obtain the state of the HTML which we want to take a screenshot of. Consumers can use jsdom to easily get components into the state they want to take a screenshot of. jsdom-screenshot then uses the markup ("the HTML") at that moment (of that state). jsdom-screenshot launches a local webserver and serves the obtained markup as index.html. It further serves assets provided through serve so that local assets are loaded. Then jsdom-screenshot uses puppeteer to take a screenshot take screenshots of that page using headless Google Chrome.

Technically

The generateImage function reads the whole markup of jsdom using document.documentElement.outerHTML.

It then starts a local webserver on a random open port to serve the obtained markup as as index.html.

Once the server is read, it launches a puppeteer instance and opens that index.html page. It waits until all resources are loaded (the network becomes idle) before taking a screenshot.

It then returns that screenshot.

Performance

Launching puppeteer to take a screenshot takes around 750ms. The rest depends on your application. You should try to mock/stub network requests to keep tests fast (see options.intercept).

You should not go overboard with Visual Regression Tests, but a few errors caught with good Visual Regression Tests will make up for the lost time in tests. Find a good balance that works for you.

Debugging

Debugging JSDOM

You can print the markup of jsdom which gets passed to puppeteer to take the screenshot by passing debug: true:

generateImage({ debug: true });

You can also import the debug function and call it manually at any point. It will log the markup of jsdom to the console:

import { generateImage, debug } from "jsdom-screenshot";

it("should have no visual regressions", async () => {
  const div = document.createElement("div");
  div.innerText = "Hello World";
  document.body.appendChild(div);

  debug(); // <---- prints the jsdom markup to the console

  expect(await generateImage()).toMatchImageSnapshot();
});

Debugging puppeteer

You can set the following launch in case you need to debug what the page looks like before taking a screenshot:

generateImage({
  launch: {
    // Whether to auto-open a DevTools panel for each tab.
    // If this option is true, the headless option will be set false.
    devtools: true,
    // Whether to run browser in headless mode.
    // Defaults to true unless the devtools option is true.
    headless: false,
    // Slows down Puppeteer operations by the specified amount of milliseconds.
    // Useful so that you can see what is going on.
    slowMo: 500
  }
});

Attribution

This package was built by massively rewriting component-image. Huge thanks to @corygibbons for laying the foundation of this package.