Before doing anything, we need to install the required packages as a developer dependency:

npm install connect enzyme enzyme-adapter-react-16 enzyme-to-json finalhandler jest-image-snapshot puppeteer -D

And their type definitions:

Now it’s time to configure. First, we will create our Jest configuration file at the root directory, which usually one level up from the src directory:

module.exports = {

"roots": [

"<rootDir>/src"

],

"transform": {

"^.+\\.tsx?$": "ts-jest"

},

"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",

"moduleFileExtensions": [

"ts",

"tsx",

"js",

"jsx",

"json",

"node"

],

"snapshotSerializers": ["enzyme-to-json/serializer"],

}

This config sets your root directory to src directory, run .tsx files with ts-jest module and look for .ts, .tsx, .js, .jsx, .json files under any subdirectory of src directory.

We also use enzyme-to-json serializer for making snapshots of our components as JSON instead of the enzyme’s default way of doing it.

Now we have to create a file named setupTests.js under the src directory. This filename is special because of apps created by create-react-app look for this filename. This is a file which is imported globally in all of your test files. So we add the things we need to import again and again in every test here so we save time. Here is setupTests.js :

import { configure } from 'enzyme';

import Adapter from 'enzyme-adapter-react-16';

import { toMatchImageSnapshot } from "jest-image-snapshot"; expect.extend({ toMatchImageSnapshot }); configure({ adapter: new Adapter() });

Now we are ready for tests. Pick your favorite component and try the shallow rendering:

import React from "react";

import { shallow } from "enzyme";

import FavComponent from "./FavComponent"; test(`should render`, () => {

const wrapper = shallow(<FavComponent />);

expect(wrapper).toMatchSnapshot();

});

Save this test file to the same directory as the component. Name it FavComponent.test.tsx These extensions are important for Jest to find these test files.

Next we can move on to our Visual Regression Test now that we have seen the basics. To test our component’s screenshots we will write our own utility and I will explain it to you so that you can create your own solutions.

First, let's look at what we request in a test file:

import React from "react";

import ReactDOM from "react-dom";

import FavComponent from "./FavComponent";

import getComponentImage from "../utils/getComponentImage"; test(`should match screenshot`, async () => {

const div = document.createElement("div");

document.body.appendChild(div);

ReactDOM.render(<FavComponent />, div);



const screenshot = await getComponentImage();

expect(screenshot).toMatchImageSnapshot();



ReactDOM.unmountComponentAtNode();

document.body.removeChild(div);

});

First, we imported React to have JSX support. Then ReactDOM to mount the component in an element and render it within a container element that we’ve created as div constant. We can use document.createElement("div") like syntax because JSDOM is built-in with create-react-app. We requested screenshot from our utility. You will see its code in a minute, but do you will notice that we didn’t pass any parameters to getComponentImage() . This is because it gets the HTML content of the component from the global JSDOM object. After everything, we unmount the component and remove the container element to prevent clashes with other tests.

Now… What is getComponentImage anyway? Basically, it is a utility which creates an HTTP server which responds with our component’s HTML and connects to that HTTP server by puppeteer to take its screenshot. Then it returns this screenshot. Let’s start writing it.

First, we need a server to serve our component’s HTML:

import connect from "connect";

import finalhandler from "finalhandler";

import http from "http";

import puppeteer from "puppeteer";

import { AddressInfo } from "net"; const createServer = async (html: string) => { const app = connect(); app.use((request: any, response: any, next: connect.NextFunction) =>

request.url === "/" ? response.end(html) : next()

); app.use(finalhandler); const server = http.createServer(app); await new Promise((resolve, reject) => {

const startServer = () => {

server.once("error", (e: NodeJS.ErrnoException) => {

if (e.code === "EADDRINUSE") {

server.close(startServer);

}

});

server.listen(0, (err?) => (err ? reject(err) : resolve()));

};

startServer();

});

return server;

};

We start by importing the necessary modules. Let’s know the modules:

connect : Connect is an HTTP server module which supports middlewares. You can spot the similarities of usage with express.js at first sight.

http : This is a native module. It creates an HTTP server which you can listen on a specific port and IP.

puppeteer : Puppeteer provides a high-level API to control Chrome or Chromium. It is usually used in headless mode but can be used in non-headless mode too.

finalhandler : It is the final function invoked in the final step to respond to an HTTP request.

We create our connect object and have a framework with the support of middlewares. Then we install a middleware which responds back with our component’s HTML if it is on the root resource path “/” and if not responds back with a 404 by letting request next() to the finalhandler . Then we create an HTTP server and listen to its events in a promise we wait for it to complete.

On error, it throws EADDRINUSE — meaning the port is used by another service or program. If this happens, our utility will halt and won’t proceed. To overcome that we start the server again every time it encounters an error. The server listens on a random port by setting its port parameter to 0, but it can still clash with another program and we need to listen for errors. After everything, we return the server.

We served our component’s HTML, now we need to “see” it.

const takeScreenshot = async (url: string) => {

const browser = await puppeteer

.launch(

{

args: ["--disable-lcd-text"],

defaultViewport: { width: 1920, height: 1080 },

},

);



const page = await browser.newPage();



await page.goto(url, { waitUntil: "load" }); const image = await page.screenshot();

browser.close();



return image;

};

We call launch from our imported puppeteer object to create a chromium instance. We launch it with two important options properties. First is args: ["--disable-lcd-text"] . This is to disable anti-aliasing which makes chromium to give different screenshots in every different monitor. Also, we need to be consistent for component’s dimensions and there may be media queries for different resolutions so we set a fixed viewport dimension. These arrangements will make us sure about same results everywhere and every time the test runs.

We open a new page in the browser and redirect it to our server which will be passed as an argument for url later. We make puppeteer to take the screenshot of the page.

To prevent memory leaks, we close the browser then we return the image for later use.

These functions won’t fire unless we call them so last function does exactly that:

const generateImage = async () => {

const html = document.documentElement.outerHTML;



const server = await createServer(html);

const address = server.address()! as AddressInfo;

const port = address.port;



const url = `http://localhost:${port}`; const screenshot = await takeScreenshot(url);

await new Promise((resolve) => server.close(resolve));



return screenshot

}; export default generateImage;

We get our HTML from JSDOM’s global object, create our server with given HTML to respond back it to takeScreenshot() ‘s puppeteer browser instance. To redirect and get the image from this function we pass an URL parameter which is pointing to localhost.”!” in address variable means that server.address() is not null. We called it as AddressInfo because it can also be a string. We wait for the server to close by passing resolve to its event handler. At the end of the function, we return the screenshot and at the end of the file, we export this function for the use in the test file.

And you know how we used it in our favorite component’s test file. Now you have an idea to solve the problem by yourself.