Skip to content

argos-ci/jest-puppeteer

Repository files navigation

🎪 jest-puppeteer

npm version npm dm npm dt

jest-puppeteer is a Jest preset that enables end-to-end testing with Puppeteer. It offers a straightforward API for launching new browser instances and interacting with web pages through them.

Table of Contents

  1. Getting Started
  2. Recipes
  3. Configuring Jest-Puppeteer
  4. API
  5. Troubleshooting
  6. Acknowledgements

Getting Started

Install the packages

npm install --save-dev jest-puppeteer puppeteer jest

Update your Jest configuration

Add jest-puppeteer as a preset in your Jest configuration file "jest.config.js":

{
  "preset": "jest-puppeteer"
}

Note Ensure you remove any existing testEnvironment options from your Jest configuration

Write a test

To write a test, create a new file with a .test.js extension, and include your test logic using the page exposed by jest-puppeteer. Here's a basic example:

import "expect-puppeteer";

describe("Google", () => {
  beforeAll(async () => {
    await page.goto("https://google.com");
  });

  it('should display "google" text on page', async () => {
    await expect(page).toMatchTextContent("google");
  });
});

Visual testing with Argos

Argos is a powerful visual testing tool that allows to review visual changes introduced by each pull request. By integrating Argos with jest-puppeteer, you can easily capture and compare screenshots to ensure the visual consistency of your application.

To get started with Argos, follow these steps:

  1. Install Argos GitHub App
  2. Install the packages
npm install --save-dev @argos-ci/cli @argos-ci/puppeteer
  1. Take screenshots during E2E tests with: await argosScreenshot(page, "/screenshots/myScreenshot.png")
  2. Include the following command in your CI workflow to upload screenshots to Argos: npx @argos-ci/cli upload ./screenshots

After installing Argos, learn how to review visual changes in your development workflow.

Synchronous configuration

// jest-puppeteer.config.cjs

/** @type {import('jest-environment-puppeteer').JestPuppeteerConfig} */
module.exports = {
  launch: {
    dumpio: true,
    headless: process.env.HEADLESS !== "false",
  },
  server: {
    command: "node server.js",
    port: 4444,
    launchTimeout: 10000,
    debug: true,
  },
};

Asynchronous configuration

In this example, an already-running instance of Chrome is used by passing the active WebSocket endpoint to the connect option. This can be particularly helpful when connecting to a Chrome instance running in the cloud.

// jest-puppeteer.config.cjs
const dockerHost = "http://localhost:9222";

async function getConfig() {
  const data = await fetch(`${dockerHost}/json/version`).json();
  const browserWSEndpoint = data.webSocketDebuggerUrl;
  /** @type {import('jest-environment-puppeteer').JestPuppeteerConfig} */
  return {
    connect: {
      browserWSEndpoint,
    },
    server: {
      command: "node server.js",
      port: 3000,
      launchTimeout: 10000,
      debug: true,
    },
  };
}

module.exports = getConfig();

Recipes

Enhance testing with expect-puppeteer lib

It can be challenging to write integration tests with the Puppeteer API, as it is not specifically designed for testing purposes. To simplify the writing tests process, the expect-puppeteer API offers specific matchers when making expectations on a Puppeteer Page.

Here are some examples:

Find a text in the page

// Assert that the current page contains 'Text in the page'
await expect(page).toMatchTextContent("Text in the page");

Click a button

// Assert that a button containing text "Home" will be clicked
await expect(page).toClick("button", { text: "Home" });

Fill a form

// Assert that a form will be filled
await expect(page).toFillForm('form[name="myForm"]', {
  firstName: "James",
  lastName: "Bond",
});

Debug mode

Debugging tests can sometimes be challenging. Jest Puppeteer provides a debug mode that allows you to pause test execution and inspect the browser. To activate debug mode, call jestPuppeteer.debug() in your test:

await jestPuppeteer.debug();

Remember that using jestPuppeteer.debug() will pause the test indefinitely. To resume, remove or comment out the line and rerun the test. To prevent timeouts during debugging, consider increasing Jest's default timeout:

jest.setTimeout(300000); // Set the timeout to 5 minutes (300000 ms)

Automatic server starting

Jest Puppeteer allows to start a server before running your tests suite and will close it after the tests end. To automatically start a server, you have to add a server section to your jest-puppeteer.config.cjs file and specify the command to start server and a port number:

// jest-puppeteer.config.cjs

/** @type {import('jest-environment-puppeteer').JestPuppeteerConfig} */
module.exports = {
  server: {
    command: "node server.js",
    port: 4444,
  },
};

Other options are documented in jest-dev-server.

Customizing Puppeteer instance

To customize Puppeteer instance, you can update the jest-puppeteer.config.cjs file.

For example, to launch Firefox browser instead of default chrome, you can set the launch.product property to "firefox".

You can also update the browser context to use the incognito mode to have isolation between instances. Read jest-puppeteer-environment readme to learn more about the possible options.

Default config values:

// jest-puppeteer.config.cjs

/** @type {import('jest-environment-puppeteer').JestPuppeteerConfig} */
module.exports = {
  launch: {
    dumpio: true,
    headless: process.env.HEADLESS !== "false",
    product: "chrome",
  },
  browserContext: "default",
};

Customizing setupTestFrameworkScriptFile or setupFilesAfterEnv

If you are using custom setup files, you must include expect-puppeteer in your setup to access the matchers it offers. Add the following to your custom setup file:

// setup.js
require("expect-puppeteer");

// Your custom setup
// ...
// jest.config.js
module.exports = {
  // ...
  setupTestFrameworkScriptFile: "./setup.js",
  // or
  setupFilesAfterEnv: ["./setup.js"],
};

Be cautious when setting your custom setupFilesAfterEnv and globalSetup, as it may result in undefined globals. Using multiple projects in Jest is one way to mitigate this issue.

module.exports = {
  projects: [
    {
      displayName: "integration",
      preset: "jest-puppeteer",
      transform: {
        "\\.tsx?$": "babel-jest",
        ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$":
          "jest-transform-stub",
      },
      moduleNameMapper: {
        "^.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$":
          "jest-transform-stub",
      },
      modulePathIgnorePatterns: [".next"],
      testMatch: [
        "<rootDir>/src/**/__integration__/**/*.test.ts",
        "<rootDir>/src/**/__integration__/**/*.test.tsx",
      ],
    },
    {
      displayName: "unit",
      transform: {
        "\\.tsx?$": "babel-jest",
        ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$":
          "jest-transform-stub",
      },
      moduleNameMapper: {
        "^.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$":
          "jest-transform-stub",
      },
      globalSetup: "<rootDir>/setupEnv.ts",
      setupFilesAfterEnv: ["<rootDir>/setupTests.ts"],
      modulePathIgnorePatterns: [".next"],
      testMatch: [
        "<rootDir>/src/**/__tests_/**/*.test.ts",
        "<rootDir>/src/**/__tests__/**/*.test.tsx",
      ],
    },
  ],
};

Extend PuppeteerEnvironment

If you need to use your custom environment, you can extend the PuppeteerEnvironment.

First, create a JavaScript file for your custom environment:

// custom-environment.js
const PuppeteerEnvironment = require("jest-environment-puppeteer");

class CustomEnvironment extends PuppeteerEnvironment {
  async setup() {
    await super.setup();
    // Your setup
  }

  async teardown() {
    // Your teardown
    await super.teardown();
  }
}

module.exports = CustomEnvironment;

Next, assign your JavaScript file's path to the testEnvironment property in your Jest configuration:

{
  // ...
  "testEnvironment": "./custom-environment.js"
}

Your custom setup and teardown will now be executed before and after each test suite, respectively.

Implementing custom globalSetup and globalTeardown

You can create custom globalSetup and globalTeardown methods. For this purpose, jest-environment-puppeteer exposes the setup and teardown methods, allowing you to integrate them with your custom global setup and teardown methods, as shown in the example below:

// global-setup.js
const setupPuppeteer = require("jest-environment-puppeteer/setup");

module.exports = async function globalSetup(globalConfig) {
  await setupPuppeteer(globalConfig);
  // Your global setup
};
// global-teardown.js
const teardownPuppeteer = require("jest-environment-puppeteer/teardown");

module.exports = async function globalTeardown(globalConfig) {
  // Your global teardown
  await teardownPuppeteer(globalConfig);
};

Then assigning your js file paths to the globalSetup and globalTeardown property in your Jest configuration.

{
  // ...
  "globalSetup": "./global-setup.js",
  "globalTeardown": "./global-teardown.js"
}

Now, your custom globalSetup and globalTeardown will be executed once before and after all test suites, respectively.

Configuring Jest-Puppeteer

Jest Puppeteer employs cosmiconfig for configuration file support, allowing you to configure Jest Puppeteer in various ways (listed in order of precedence):

  • A "jest-puppeteer" key in your package.json file.
  • A .jest-puppeteerrc file in either JSON or YAML format.
  • A .jest-puppeteerrc.json, .jest-puppeteerrc.yml, .jest-puppeteerrc.yaml, or .jest-puppeteerrc.json5 file.
  • A .jest-puppeteerrc.js, .jest-puppeteerrc.cjs, jest-puppeteer.config.js, or jest-puppeteer.config.cjs file that exports an object using module.exports.
  • A .jest-puppeteerrc.toml file.

By default, the configuration is searched for at the root of the project. To define a custom path, use the JEST_PUPPETEER_CONFIG environment variable.

Ensure that the exported configuration is either a config object or a Promise that returns a config object.

interface JestPuppeteerConfig {
  /**
   * Puppeteer connect options.
   * @see https://pptr.dev/api/puppeteer.connectoptions
   */
  connect?: ConnectOptions;
  /**
   * Puppeteer launch options.
   * @see https://pptr.dev/api/puppeteer.launchoptions
   */
  launch?: PuppeteerLaunchOptions;
  /**
   * Server config for `jest-dev-server`.
   * @see https://www.npmjs.com/package/jest-dev-server
   */
  server?: JestDevServerConfig | JestDevServerConfig[];
  /**
   * Allow to run one browser per worker.
   * @default false
   */
  browserPerWorker?: boolean;
  /**
   * Browser context to use.
   * @default "default"
   */
  browserContext?: "default" | "incognito";
  /**
   * Exit on page error.
   * @default true
   */
  exitOnPageError?: boolean;
  /**
   * Use `runBeforeUnload` in `page.close`.
   * @see https://pptr.dev/api/puppeteer.page.close
   * @default false
   */
  runBeforeUnloadOnClose?: boolean;
}

API

global.browser

Provides access to the Puppeteer Browser.

it("should open a new page", async () => {
  const page = await browser.newPage();
  await page.goto("https://google.com");
});

global.page

Provides access to a Puppeteer Page that is opened at the start (most commonly used).

it("should fill an input", async () => {
  await page.type("#myinput", "Hello");
});

global.context

Provides access to a browser context that is instantiated when the browser is launched. You can control whether each test has its own isolated browser context using the browserContext option in your configuration file.

global.expect(page)

A helper for making Puppeteer assertions. For more information, refer to the documentation.

await expect(page).toMatchTextContent("A text in the page");
// ...

global.jestPuppeteer.debug()

Put test in debug mode.

  • Jest is suspended (no timeout)
  • A debugger instruction to Chromium, if Puppeteer has been launched with { devtools: true } it will pause
it("should put test in debug mode", async () => {
  await jestPuppeteer.debug();
});

global.jestPuppeteer.resetPage()

To reset global.page before each test, use the following code:

beforeEach(async () => {
  await jestPuppeteer.resetPage();
});

global.jestPuppeteer.resetBrowser()

To reset global.browser, global.context, and global.page before each test, use the following code:

beforeEach(async () => {
  await jestPuppeteer.resetBrowser();
});

Troubleshooting

TypeScript

TypeScript is natively supported from v8.0.0, for previous versions, you have to use community-provided types.

CI Timeout

Most Continuous Integration (CI) platforms restrict the number of threads you can use. If you run multiple test suites, the tests may timeout due to Jest attempting to run Puppeteer in parallel, and the CI platform being unable to process all parallel jobs in time.

A solution to this issue is to run your tests serially in a CI environment. Users have found that running tests serially in such environments can result in up to 50% performance improvements.

You can achieve this through the CLI by running:

jest --runInBand

Alternatively, you can set Jest to use a maximum number of workers that your CI environment supports:

jest --maxWorkers=2

Prevent ESLint errors on global variables

Jest Puppeteer provides five global variables: browser, page, context, puppeteerConfig, and jestPuppeteer. To prevent errors related to these globals, include them in your ESLint configuration:

// .eslintrc.js
module.exports = {
  env: {
    jest: true,
  },
  globals: {
    page: true,
    browser: true,
    context: true,
    puppeteerConfig: true,
    jestPuppeteer: true,
  },
};

Acknowledgements

Special thanks to Fumihiro Xue for providing an excellent Jest example.