diff --git a/README.md b/README.md index dac05957..e97fb4be 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,50 @@ +Let's find a balance between detailed explanations and clarity. Here’s a more comprehensive version that retains structure but elaborates more where needed: + +--- + # 🎪 jest-puppeteer [![npm version](https://img.shields.io/npm/v/jest-puppeteer.svg)](https://www.npmjs.com/package/jest-puppeteer) -[![npm dm](https://img.shields.io/npm/dm/jest-puppeteer.svg)](https://www.npmjs.com/package/jest-puppeteer) -[![npm dt](https://img.shields.io/npm/dt/jest-puppeteer.svg)](https://www.npmjs.com/package/jest-puppeteer) +[![npm downloads](https://img.shields.io/npm/dm/jest-puppeteer.svg)](https://www.npmjs.com/package/jest-puppeteer) -`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. +`jest-puppeteer` is a Jest preset designed for seamless integration with Puppeteer, enabling end-to-end testing in a browser environment. With a simple API, it allows you to launch browsers and interact with web pages, making it perfect for testing UI interactions in web applications. ## Table of Contents 1. [Getting Started](#getting-started) - - [Install the packages](#install-the-packages) - - [Write a test](#write-a-test) - - [Visual testing with Argos](#visual-testing-with-argos) + - [Installation](#installation) + - [Basic Setup](#basic-setup) + - [Writing Your First Test](#writing-your-first-test) + - [TypeScript Setup](#typescript-setup) + - [Visual Testing with Argos](#visual-testing-with-argos) 2. [Recipes](#recipes) - - [Enhance testing with `expect-puppeteer` lib](#enhance-testing-with-expect-puppeteer-lib) - - [Debug mode](#debug-mode) - - [Automatic server starting](#automatic-server-starting) - - [Customizing Puppeteer instance](#customizing-puppeteer-instance) - - [Customizing `setupTestFrameworkScriptFile` or `setupFilesAfterEnv`](#customizing-setupTestFrameworkScriptFile-or-setupFilesAfterEnv) - - [Extend `PuppeteerEnvironment`](#extend-puppeteerenvironment) - - [Implementing custom `globalSetup` and `globalTeardown`](#implementing-custom-globalsetup-and-globalteardown) -3. [Configuring Jest-Puppeteer](#configuring-jest-puppeteer) -4. [API](#api) + - [Using `expect-puppeteer`](#using-expect-puppeteer) + - [Debugging Tests](#debugging-tests) + - [Automatic Server Management](#automatic-server-management) + - [Customizing the Puppeteer Instance](#customizing-the-puppeteer-instance) + - [Custom Test Setup](#custom-test-setup) + - [Extending `PuppeteerEnvironment`](#extending-puppeteerenvironment) + - [Global Setup and Teardown](#global-setup-and-teardown) +3. [Jest-Puppeteer Configuration](#jest-puppeteer-configuration) +4. [API Reference](#api-reference) 5. [Troubleshooting](#troubleshooting) 6. [Acknowledgements](#acknowledgements) ## Getting Started -### Install the packages +### Installation + +To start using `jest-puppeteer`, you’ll need to install the following packages: ```bash npm install --save-dev jest-puppeteer puppeteer jest ``` -### Update your Jest configuration +This will install Jest (the testing framework), Puppeteer (the headless browser tool), and `jest-puppeteer` (the integration between the two). + +### Basic Setup -Add jest-puppeteer as a preset in your Jest configuration file "jest.config.js": +In your Jest configuration file (`jest.config.js`), add `jest-puppeteer` as the preset: ```json { @@ -43,149 +52,117 @@ Add jest-puppeteer as a preset in your Jest configuration file "jest.config.js": } ``` -> **Note** -> Ensure you remove any existing `testEnvironment` options from your Jest configuration +This will configure Jest to use Puppeteer for running your tests. Make sure to remove any conflicting `testEnvironment` settings that might be present in your existing Jest configuration, as `jest-puppeteer` manages the environment for you. -### Write a test +### Writing Your First 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: +Once you’ve configured Jest, you can start writing tests using Puppeteer’s `page` object, which is automatically provided by `jest-puppeteer`. + +Create a test file (e.g., `google.test.js`): ```js import "expect-puppeteer"; -describe("Google", () => { +describe("Google Homepage", () => { beforeAll(async () => { await page.goto("https://google.com"); }); it('should display "google" text on page', async () => { - await expect(page).toMatchTextContent("google"); + await expect(page).toMatchTextContent(/Google/); }); }); ``` -### Visual testing with Argos +This example test navigates to Google’s homepage and checks if the page contains the word "Google". `jest-puppeteer` simplifies working with Puppeteer by exposing the `page` object, allowing you to write tests using a familiar syntax. -[Argos](https://argos-ci.com) 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. +### TypeScript Setup -To get started with Argos, follow these steps: +If you’re using TypeScript, `jest-puppeteer` natively supports it from version `8.0.0`. To get started with TypeScript, follow these steps: -1. [Install Argos GitHub App](https://github.com/apps/argos-ci) -2. Install the packages +1. Make sure your project is using the correct type definitions. If you’ve upgraded to version `10.1.2` or above, uninstall old types: -```sh -npm install --save-dev @argos-ci/cli @argos-ci/puppeteer +```bash +npm uninstall --save-dev @types/jest-environment-puppeteer @types/expect-puppeteer ``` -3. Take screenshots during E2E tests with: `await argosScreenshot(page, "/screenshots/myScreenshot.png")` -4. 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](https://argos-ci.com/docs/review-changes) in your development workflow. - -#### Synchronous configuration +2. Install `@types/jest` (`jest-puppeteer` does not support `@jest/globals`) : -```js -// 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, - }, -}; +```bash +npm install --save-dev @types/jest ``` -#### Asynchronous configuration +3. Jest will automatically pick up type definitions from `@types/jest`. Once you’ve set up the environment, you can start writing tests in TypeScript just like in JavaScript: -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. +```ts +import "jest-puppeteer"; +import "expect-puppeteer"; -```js -// 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, - }, - }; -} +describe("Google Homepage", (): void => { + beforeAll(async (): Promise => { + await page.goto("https://google.com"); + }); -module.exports = getConfig(); + it('should display "google" text on page', async (): Promise => { + await expect(page).toMatchTextContent(/Google/); + }); +}); ``` -## Recipes +### Visual Testing with Argos + +[Argos](https://argos-ci.com) is a powerful tool for visual testing, allowing you to track visual changes introduced by each pull request. By integrating Argos with `jest-puppeteer`, you can easily capture and compare screenshots to maintain the visual consistency of your application. + +To get started, check out the [Puppeteer Quickstart Guide](https://argos-ci.com/docs/quickstart/puppeteer). -### Enhance testing with `expect-puppeteer` lib +## Recipes -It can be challenging to write integration tests with the [Puppeteer API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md), as it is not specifically designed for testing purposes. -To simplify the writing tests process, the [expect-puppeteer API](https://github.com/smooth-code/jest-puppeteer/tree/master/packages/expect-puppeteer/README.md#api) offers specific matchers when making expectations on a [Puppeteer Page](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page). +### Using `expect-puppeteer` -Here are some examples: +Writing tests with Puppeteer’s core API can be verbose. The `expect-puppeteer` library simplifies this by adding custom matchers, such as checking for text content or interacting with elements. Some examples: -#### Find a text in the page +- Assert that a page contains certain text: ```js -// Assert that the current page contains 'Text in the page' -await expect(page).toMatchTextContent("Text in the page"); +await expect(page).toMatchTextContent("Expected text"); ``` -#### Click a button +- Simulate a button click: ```js -// Assert that a button containing text "Home" will be clicked -await expect(page).toClick("button", { text: "Home" }); +await expect(page).toClick("button", { text: "Submit" }); ``` -#### Fill a form +- Fill out a form: ```js -// Assert that a form will be filled -await expect(page).toFillForm('form[name="myForm"]', { - firstName: "James", - lastName: "Bond", +await expect(page).toFillForm('form[name="login"]', { + username: "testuser", + password: "password", }); ``` -### Debug mode +### Debugging Tests -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: +Debugging can sometimes be tricky in headless browser environments. `jest-puppeteer` provides a helpful `debug()` function, which pauses test execution and opens the browser for manual inspection: ```js 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: +To prevent the test from timing out, increase Jest’s timeout: ```js -jest.setTimeout(300000); // Set the timeout to 5 minutes (300000 ms) +jest.setTimeout(300000); // 5 minutes ``` -### Automatic server starting +This can be particularly useful when you need to step through interactions or inspect the state of the page during test execution. -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: +### Automatic Server Management -```js -// jest-puppeteer.config.cjs +If your tests depend on a running server (e.g., an Express app), you can configure `jest-puppeteer` to automatically start and stop the server before and after tests: -/** @type {import('jest-environment-puppeteer').JestPuppeteerConfig} */ +```js module.exports = { server: { command: "node server.js", @@ -194,118 +171,55 @@ module.exports = { }; ``` -Other options are documented in [jest-dev-server](https://github.com/smooth-code/jest-puppeteer/tree/master/packages/jest-dev-server). - -### Customizing Puppeteer instance - -To customize Puppeteer instance, you can update the `jest-puppeteer.config.cjs` file. +This eliminates the need to manually manage your server during testing. -For example, to launch Firefox browser instead of default chrome, you can set the `launch.product` property to "firefox". +### Customizing the Puppeteer Instance -You can also update the browser context to use the incognito mode to have isolation between instances. Read [jest-puppeteer-environment readme](https://github.com/smooth-code/jest-puppeteer/blob/master/packages/jest-environment-puppeteer/README.md) to learn more about the possible options. - -Default config values: +You can easily customize the Puppeteer instance used in your tests by modifying the `jest-puppeteer.config.js` file. For example, if you want to launch Firefox instead of Chrome: ```js -// jest-puppeteer.config.cjs - -/** @type {import('jest-environment-puppeteer').JestPuppeteerConfig} */ module.exports = { launch: { - dumpio: true, + product: "firefox", headless: process.env.HEADLESS !== "false", - product: "chrome", }, - browserContext: "default", }; ``` -### Customizing `setupTestFrameworkScriptFile` or `setupFilesAfterEnv` +This file allows you to configure browser options, set up browser contexts, and more. -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: +### Custom Test Setup + +If you have custom setup requirements, you can define setup files to initialize your environment before each test. For instance, you may want to import `expect-puppeteer` globally: ```js // setup.js require("expect-puppeteer"); - -// Your custom setup -// ... ``` -```js -// 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. +Then, in your Jest config: ```js 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: [ - "/src/**/__integration__/**/*.test.ts", - "/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: "/setupEnv.ts", - setupFilesAfterEnv: ["/setupTests.ts"], - modulePathIgnorePatterns: [".next"], - testMatch: [ - "/src/**/__tests_/**/*.test.ts", - "/src/**/__tests__/**/*.test.tsx", - ], - }, - ], + setupFilesAfterEnv: ["./setup.js"], }; ``` -### Extend `PuppeteerEnvironment` - -If you need to use your custom environment, you can extend the `PuppeteerEnvironment`. +### Extending `PuppeteerEnvironment` -First, create a JavaScript file for your custom environment: +For advanced use cases, you can extend the default `PuppeteerEnvironment` class to add custom functionality: ```js -// custom-environment.js const PuppeteerEnvironment = require("jest-environment-puppeteer"); class CustomEnvironment extends PuppeteerEnvironment { async setup() { await super.setup(); - // Your setup + // Custom setup logic } async teardown() { - // Your teardown + // Custom teardown logic await super.teardown(); } } @@ -313,20 +227,9 @@ class CustomEnvironment extends PuppeteerEnvironment { module.exports = CustomEnvironment; ``` -Next, assign your JavaScript file's path to the [`testEnvironment`](https://facebook.github.io/jest/docs/en/configuration.html#testenvironment-string) property in your Jest configuration: +### Global Setup and Teardown -```js -{ - // ... - "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`](https://facebook.github.io/jest/docs/en/configuration.html#globalsetup-string) and [`globalTeardown`](https://facebook.github.io/jest/docs/en/configuration.html#globalteardown-string) 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: +Sometimes, tests may require a global setup or teardown step that only runs once per test suite. You can define custom `globalSetup` and `globalTeardown` scripts: ```js // global-setup.js @@ -334,208 +237,147 @@ const setupPuppeteer = require("jest-environment-puppeteer/setup"); module.exports = async function globalSetup(globalConfig) { await setupPuppeteer(globalConfig); - // Your global setup -}; -``` - -```js -// global-teardown.js -const teardownPuppeteer = require("jest-environment-puppeteer/teardown"); - -module.exports = async function globalTeardown(globalConfig) { - // Your global teardown - await teardownPuppeteer(globalConfig); + // Additional setup logic }; ``` -Then assigning your js file paths to the [`globalSetup`](https://facebook.github.io/jest/docs/en/configuration.html#globalsetup-string) and [`globalTeardown`](https://facebook.github.io/jest/docs/en/configuration.html#globalteardown-string) property in your Jest configuration. +In your Jest configuration, reference these files: -```js +```json { - // ... "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 Configuration -Jest Puppeteer employs cosmiconfig for configuration file support, allowing you to configure Jest Puppeteer in various ways (listed in order of precedence): +Jest-Puppeteer supports various configuration formats through [cosmiconfig](https://github.com/davidtheclark/cosmiconfig), allowing flexible ways to define your setup. By default, the configuration is looked for at the root of your project, but you can also define a custom path using the `JEST_PUPPETEER_CONFIG` environment variable. -- 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. +Possible configuration formats: -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. +- A `"jest-puppeteer"` key in your `package.json`. +- A `.jest-puppeteerrc` file (JSON, YAML, or JavaScript). +- A `.jest-puppeteer.config.js` or `.jest-puppeteer.config.cjs` file that exports a configuration object. -Ensure that the exported configuration is either a config object or a Promise that returns a config object. +Example of a basic configuration file (`jest-puppeteer.config.js`): -```ts -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; -} +```js +module.exports = { + launch: { + headless: process.env.HEADLESS !== "false", + dumpio: true, // Show browser console logs + }, + browserContext: "default", // Use "incognito" if you want isolated sessions per test + server: { + command: "node server.js", + port: 4444, + launchTimeout: 10000, + debug: true, + }, +}; ``` -## API +You can further extend this configuration to connect to a remote instance of Chrome or customize the environment for your test runs. -### `global.browser` +## API Reference -Provides access to the [Puppeteer Browser](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-browser). +Jest-Puppeteer exposes several global objects and methods to facilitate test writing: -```js -it("should open a new page", async () => { - const page = await browser.newPage(); - await page.goto("https://google.com"); -}); -``` +- **`global.browser`**: Provides access to the Puppeteer [Browser](https://pptr.dev/#?product=Puppeteer&version=v13.0.0&show=api-class-browser) instance. -### `global.page` + Example: -Provides access to a [Puppeteer Page](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page) that is opened at the start (most commonly used). + ```js + const page = await browser.newPage(); + await page.goto("https://example.com"); + ``` -```js -it("should fill an input", async () => { - await page.type("#myinput", "Hello"); -}); -``` +- **`global.page`**: The default Puppeteer [Page](https://pptr.dev/#?product=Puppeteer&version=v13.0.0&show=api-class-page) object, automatically created and available in tests. -### `global.context` + Example: -Provides access to a [browser context](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-browsercontext) 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. + ```js + await page.type("#input", "Hello World"); + ``` -### `global.expect(page)` +- **`global.context`**: Gives access to the [browser context](https://pptr.dev/#?product=Puppeteer&version=v13.0.0&show=api-class-browsercontext), useful for isolating tests in separate contexts. -A helper for making Puppeteer assertions. For more information, refer to [the documentation](https://github.com/smooth-code/jest-puppeteer/tree/master/packages/expect-puppeteer/README.md#api). +- **`global.expect(page)`**: The enhanced `expect` API provided by `expect-puppeteer`. You can use this to make assertions on the Puppeteer `page`. -```js -await expect(page).toMatchTextContent("A text in the page"); -// ... -``` + Example: -### `global.jestPuppeteer.debug()` + ```js + await expect(page).toMatchTextContent("Expected text on page"); + ``` -Put test in debug mode. +- **`global.jestPuppeteer.debug()`**: Suspends test execution, allowing you to inspect the browser and debug. -- Jest is suspended (no timeout) -- A `debugger` instruction to Chromium, if Puppeteer has been launched with `{ devtools: true }` it will pause + Example: -```js -it("should put test in debug mode", async () => { + ```js await jestPuppeteer.debug(); -}); -``` + ``` -### `global.jestPuppeteer.resetPage()` +- **`global.jestPuppeteer.resetPage()`**: Resets the `page` object before each test. -To reset `global.page` before each test, use the following code: + Example: -```js -beforeEach(async () => { - await jestPuppeteer.resetPage(); -}); -``` + ```js + beforeEach(async () => { + await jestPuppeteer.resetPage(); + }); + ``` -### `global.jestPuppeteer.resetBrowser()` +- **`global.jestPuppeteer.resetBrowser()`**: Resets the `browser`, `context`, and `page` objects, ensuring a clean slate for each test. -To reset `global.browser`, `global.context`, and `global.page` before each test, use the following code: + Example: -```js -beforeEach(async () => { - await jestPuppeteer.resetBrowser(); -}); -``` + ```js + beforeEach(async () => { + await jestPuppeteer.resetBrowser(); + }); + ``` + +These methods simplify the setup and teardown process for tests, making it easier to work with Puppeteer in a Jest environment. ## Troubleshooting -### TypeScript +### CI Timeout Issues -TypeScript is natively supported from v8.0.0, for previous versions, you have to use [community-provided types](https://github.com/DefinitelyTyped/DefinitelyTyped). +In CI environments, tests may occasionally time out due to limited resources. Jest-Puppeteer allows you to control the number of workers used to run tests. Running tests serially can help avoid these timeouts: -Note though that it still requires installation of the [type definitions for jest](https://www.npmjs.com/package/@types/jest) : +Run tests in a single process: ```bash -npm install --save-dev @types/jest +jest --runInBand ``` -Once setup, import the modules to enable types resolution for the exposed globals, then write your test logic [the same way you would in Javascript](#recipes). - -```ts -// import globals -import "jest-puppeteer"; -import "expect-puppeteer"; +Alternatively, you can limit the number of parallel workers: -describe("Google", (): void => { - beforeAll(async (): Promise => { - await page.goto("https://google.com"); - }); - - it('should display "google" text on page', async (): Promise => { - await expect(page).toMatchTextContent("google"); - }); -}); +```bash +jest --maxWorkers=2 ``` -### 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. +This ensures that your CI environment doesn’t get overloaded by too many concurrent processes, which can improve the reliability of your tests. -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](https://jestjs.io/docs/en/troubleshooting#tests-are-extremely-slow-on-docker-and-or-continuous-integration-ci-server). +### Debugging CI Failures -You can achieve this through the CLI by running: +Sometimes, failures happen only in CI environments and not locally. In such cases, use the `debug()` method to open a browser during CI runs and inspect the page manually: -```sh -jest --runInBand +```js +await jestPuppeteer.debug(); ``` -Alternatively, you can set Jest to use a maximum number of workers that your CI environment supports: +To avoid test timeouts in CI, set a larger timeout during the debugging process: -``` -jest --maxWorkers=2 +```js +jest.setTimeout(600000); // 10 minutes ``` -### Prevent ESLint errors on global variables +### Preventing ESLint Errors with 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: +Jest-Puppeteer introduces global variables like `page`, `browser`, `context`, etc., which ESLint may flag as undefined. You can prevent this by adding these globals to your ESLint configuration: ```js // .eslintrc.js @@ -553,6 +395,8 @@ module.exports = { }; ``` +This configuration will prevent ESLint from throwing errors about undefined globals. + ## Acknowledgements -Special thanks to Fumihiro Xue for providing an excellent [Jest example](https://github.com/xfumihiro/jest-puppeteer-example). +Special thanks to [Fumihiro Xue](https://github.com/xfumihiro) for providing an excellent [Jest Puppeteer example](https://github.com/xfumihiro/jest-puppeteer-example), which served as an inspiration for this package. diff --git a/packages/expect-puppeteer/README.md b/packages/expect-puppeteer/README.md index c29e6712..4b7669ce 100644 --- a/packages/expect-puppeteer/README.md +++ b/packages/expect-puppeteer/README.md @@ -24,7 +24,7 @@ Modify your Jest configuration: Writing integration test is very hard, especially when you are testing a Single Page Applications. Data are loaded asynchronously and it is difficult to know exactly when an element will be displayed in the page. -[Puppeteer API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md) is great, but it is low level and not designed for integration testing. +[Puppeteer API](https://pptr.dev/api) is great, but it is low level and not designed for integration testing. This API is designed for integration testing: @@ -81,11 +81,11 @@ await expect(page).toMatchElement("div.inner", { text: "some text" }); Expect an element to be in the page or element, then click on it. -- `instance` <[Page]|[ElementHandle]> Context +- `instance` <[Page]|[Frame]|[ElementHandle]> Context - `selector` <[string]|[MatchSelector](#MatchSelector)> A [selector] or a [MatchSelector](#MatchSelector) to click on. - `options` <[Object]> Optional parameters - `button` <"left"|"right"|"middle"> Defaults to `left`. - - `clickCount` <[number]> defaults to 1. See [UIEvent.detail]. + - `count` <[number]> defaults to 1. See [UIEvent.detail]. - `delay` <[number]> Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0. - `text` <[string]|[RegExp]> A text or a RegExp to match in element `textContent`. @@ -111,8 +111,8 @@ const dialog = await expect(page).toDisplayDialog(async () => { Expect a control to be in the page or element, then fill it with text. -- `instance` <[Page]|[ElementHandle]> Context -- `selector` <[string]> A [selector] to match field +- `instance` <[Page]|[Frame]|[ElementHandle]> Context +- `selector` <[string]|[MatchSelector](#MatchSelector)> A [selector] or a [MatchSelector](#MatchSelector) to match field - `value` <[string]> Value to fill - `options` <[Object]> Optional parameters - `delay` <[number]> delay to pass to [the puppeteer `element.type` API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#elementhandletypetext-options) @@ -125,8 +125,8 @@ await expect(page).toFill('input[name="firstName"]', "James"); Expect a form to be in the page or element, then fill its controls. -- `instance` <[Page]|[ElementHandle]> Context -- `selector` <[string]> A [selector] to match form +- `instance` <[Page]|[Frame]|[ElementHandle]> Context +- `selector` <[string]|[MatchSelector](#MatchSelector)> A [selector] or a [MatchSelector](#MatchSelector) to match form - `values` <[Object]> Values to fill - `options` <[Object]> Optional parameters - `delay` <[number]> delay to pass to [the puppeteer `element.type` API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#elementhandletypetext-options) @@ -142,7 +142,7 @@ await expect(page).toFillForm('form[name="myForm"]', { Expect a text or a string RegExp to be present in the page or element. -- `instance` <[Page]|[ElementHandle]> Context +- `instance` <[Page]|[Frame]|[ElementHandle]> Context - `matcher` <[string]|[RegExp]> A text or a RegExp to match in page - `options` <[Object]> Optional parameters - `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: @@ -162,8 +162,8 @@ await expect(page).toMatchTextContent(/lo.*/); Expect an element be present in the page or element. -- `instance` <[Page]|[ElementHandle]> Context -- `selector` <[string]> A [selector] to match element +- `instance` <[Page]|[Frame]|[ElementHandle]> Context +- `selector` <[string]|[MatchSelector](#MatchSelector)> A [selector] or a [MatchSelector](#MatchSelector) to match element - `options` <[Object]> Optional parameters - `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: - `raf` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes. @@ -183,8 +183,8 @@ await expect(row).toClick("td:nth-child(3) a"); Expect a select control to be present in the page or element, then select the specified option. -- `instance` <[Page]|[ElementHandle]> Context -- `selector` <[string]> A [selector] to match select [element] +- `instance` <[Page]|[Frame]|[ElementHandle]> Context +- `selector` <[string]|[MatchSelector](#MatchSelector)> A [selector] or a [MatchSelector](#MatchSelector) to match select [element] - `valueOrText` <[string]> Value or text matching option ```js @@ -195,9 +195,9 @@ await expect(page).toSelect('select[name="choices"]', "Choice 1"); Expect a input file control to be present in the page or element, then fill it with a local file. -- `instance` <[Page]|[ElementHandle]> Context -- `selector` <[string]> A [selector] to match input [element] -- `filePath` <[string]> A file path +- `instance` <[Page]|[Frame]|[ElementHandle]> Context +- `selector` <[string]|[MatchSelector](#MatchSelector)> A [selector] or a [MatchSelector](#MatchSelector) to match input [element] +- `filePath` <[string]|[Array]<[string]>> A file path or array of file paths ```js import { join } from "node:path"; @@ -208,7 +208,7 @@ await expect(page).toUploadFile( ); ``` -### {type: [string], value: [string]} +### Match Selector An object used as parameter in order to select an element. @@ -242,6 +242,7 @@ setDefaultOptions({ timeout: 1000 }); [element]: https://developer.mozilla.org/en-US/docs/Web/API/element "Element" [map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map" [selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors "selector" -[page]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page "Page" -[elementhandle]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-elementhandle "ElementHandle" +[page]: https://pptr.dev/api/puppeteer.page "Page" +[frame]: https://pptr.dev/api/puppeteer.frame "Frame" +[elementhandle]: https://pptr.dev/api/puppeteer.elementhandle/ "ElementHandle" [uievent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail diff --git a/packages/expect-puppeteer/src/globals.test.ts b/packages/expect-puppeteer/src/globals.test.ts new file mode 100644 index 00000000..ec6add77 --- /dev/null +++ b/packages/expect-puppeteer/src/globals.test.ts @@ -0,0 +1,17 @@ +// import jest globals +import { xdescribe, beforeAll, it, expect } from "@jest/globals"; + +// import jest-puppeteer globals +import "jest-puppeteer"; +import "expect-puppeteer"; + +// test explicit imports from @jest/globals (incompatible with matchers implementation) +xdescribe("Google", (): void => { + beforeAll(async (): Promise => { + await page.goto("https://google.com"); + }); + + it('should display "google" text on page', async (): Promise => { + await expect(page).not.toMatchTextContent("google", {}); + }); +}); diff --git a/packages/expect-puppeteer/src/index.test.ts b/packages/expect-puppeteer/src/index.test.ts index 0cec8408..4ddab0dd 100644 --- a/packages/expect-puppeteer/src/index.test.ts +++ b/packages/expect-puppeteer/src/index.test.ts @@ -2,7 +2,6 @@ import { getDefaultOptions, setDefaultOptions } from "expect-puppeteer"; // import globals import "jest-puppeteer"; -import "expect-puppeteer"; expect.addSnapshotSerializer({ print: () => "hello", diff --git a/packages/expect-puppeteer/src/index.ts b/packages/expect-puppeteer/src/index.ts index a039e236..9601edd3 100644 --- a/packages/expect-puppeteer/src/index.ts +++ b/packages/expect-puppeteer/src/index.ts @@ -44,8 +44,8 @@ type Wrapper = T extends ( ? (...args: A) => R : never; -// declare matchers list -type PuppeteerMatchers = T extends PuppeteerInstance +// declare common matchers list +type InstanceMatchers = T extends PuppeteerInstance ? { // common toClick: Wrapper; @@ -64,24 +64,24 @@ type PuppeteerMatchers = T extends PuppeteerInstance : never; // declare page matchers list -interface PageMatchers extends PuppeteerMatchers { +interface PageMatchers extends InstanceMatchers { // instance specific toDisplayDialog: Wrapper; // inverse matchers - not: PuppeteerMatchers[`not`] & {}; + not: InstanceMatchers[`not`] & {}; } // declare frame matchers list -interface FrameMatchers extends PuppeteerMatchers { +interface FrameMatchers extends InstanceMatchers { // inverse matchers - not: PuppeteerMatchers[`not`] & {}; + not: InstanceMatchers[`not`] & {}; } // declare element matchers list interface ElementHandleMatchers - extends PuppeteerMatchers> { + extends InstanceMatchers> { // inverse matchers - not: PuppeteerMatchers>[`not`] & {}; + not: InstanceMatchers>[`not`] & {}; } // declare matchers per instance type @@ -103,40 +103,41 @@ type GlobalWithExpect = typeof globalThis & { expect: PuppeteerExpect }; // --------------------------- -// extend global jest object +// not possible to use PMatchersPerType directly ... +interface PuppeteerMatchers { + // common + toClick: T extends PuppeteerInstance ? Wrapper : never; + toFill: T extends PuppeteerInstance ? Wrapper : never; + toFillForm: T extends PuppeteerInstance ? Wrapper : never; + toMatchTextContent: T extends PuppeteerInstance + ? Wrapper + : never; + toMatchElement: T extends PuppeteerInstance + ? Wrapper + : never; + toSelect: T extends PuppeteerInstance ? Wrapper : never; + toUploadFile: T extends PuppeteerInstance + ? Wrapper + : never; + // page + toDisplayDialog: T extends Page ? Wrapper : never; + // inverse matchers + not: { + toMatchTextContent: T extends PuppeteerInstance + ? Wrapper + : never; + toMatchElement: T extends PuppeteerInstance + ? Wrapper + : never; + }; +} + +// support for @types/jest declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace jest { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Matchers { - // common - toClick: T extends PuppeteerInstance ? Wrapper : never; - toFill: T extends PuppeteerInstance ? Wrapper : never; - toFillForm: T extends PuppeteerInstance - ? Wrapper - : never; - toMatchTextContent: T extends PuppeteerInstance - ? Wrapper - : never; - toMatchElement: T extends PuppeteerInstance - ? Wrapper - : never; - toSelect: T extends PuppeteerInstance ? Wrapper : never; - toUploadFile: T extends PuppeteerInstance - ? Wrapper - : never; - // page - toDisplayDialog: T extends Page ? Wrapper : never; - // inverse matchers - not: { - toMatchTextContent: T extends PuppeteerInstance - ? Wrapper - : never; - toMatchElement: T extends PuppeteerInstance - ? Wrapper - : never; - }; - } + // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars + interface Matchers extends PuppeteerMatchers {} } } @@ -151,7 +152,7 @@ const wrapMatcher = ( instance: T, ) => async function throwingMatcher(...args: unknown[]): Promise { - // ??? + // update the assertions counter jestExpect.getState().assertionCalls += 1; try { // run async matcher @@ -176,7 +177,7 @@ const puppeteerExpect = (instance: T) => { ]; if (!isPage && !isFrame && !isHandle) - throw new Error(`${instance} is not supported`); + throw new Error(`${instance.constructor.name} is not supported`); // retrieve matchers const expectation = { @@ -237,7 +238,7 @@ const expectPuppeteer = ((actual: T) => { Object.keys(jestExpect).forEach((prop) => { // @ts-expect-error add jest expect properties to expect-puppeteer implementation - expectPuppeteer[prop] = jestExpect[prop]; + expectPuppeteer[prop] = jestExpect[prop] as unknown; }); export { expectPuppeteer as expect }; diff --git a/packages/expect-puppeteer/src/matchers/toClick.ts b/packages/expect-puppeteer/src/matchers/toClick.ts index 3256ddcd..49139c8b 100644 --- a/packages/expect-puppeteer/src/matchers/toClick.ts +++ b/packages/expect-puppeteer/src/matchers/toClick.ts @@ -9,7 +9,7 @@ export async function toClick( selector: Selector | string, options: ToClickOptions = {}, ) { - const { delay, button, clickCount, offset, ...otherOptions } = options; + const { delay, button, count, offset, ...otherOptions } = options; const element = await toMatchElement(instance, selector, otherOptions); - await element.click({ delay, button, clickCount, offset }); + await element.click({ delay, button, count, offset }); } diff --git a/packages/jest-environment-puppeteer/README.md b/packages/jest-environment-puppeteer/README.md index 9872c504..8bf67df9 100644 --- a/packages/jest-environment-puppeteer/README.md +++ b/packages/jest-environment-puppeteer/README.md @@ -37,11 +37,33 @@ describe("Google", () => { }); ``` +## TypeScript Setup + +If you’re using TypeScript, `jest-puppeteer` natively supports it from version `8.0.0`. To get started with TypeScript, follow these steps: + +1. Make sure your project is using the correct type definitions. If you’ve upgraded to version `10.1.2` or above, uninstall old types: + +```bash +npm uninstall --save-dev @types/jest-environment-puppeteer @types/expect-puppeteer +``` + +2. Install `@types/jest` (`jest-puppeteer` does not support `@jest/globals`) : + +```bash +npm install --save-dev @types/jest +``` + +3. Import the `jest-puppeteer` module to expose the global API : + +```ts +import "jest-puppeteer"; +``` + ## API ### `global.browser` -Give access to the [Puppeteer Browser](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-browser). +Give access to the [Puppeteer Browser](https://pptr.dev/api/puppeteer.browser). ```js it("should open a new page", async () => { @@ -52,7 +74,7 @@ it("should open a new page", async () => { ### `global.page` -Give access to a [Puppeteer Page](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page) opened at start (you will use it most of time). +Give access to a [Puppeteer Page](https://pptr.dev/api/puppeteer.page) opened at start (you will use it most of time). ```js it("should fill an input", async () => { @@ -62,7 +84,7 @@ it("should fill an input", async () => { ### `global.context` -Give access to a [browser context](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-browsercontext) 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 config. +Give access to a [browser context](https://pptr.dev/api/puppeteer.browsercontext) 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 config. ### `global.jestPuppeteer.debug()` diff --git a/packages/jest-environment-puppeteer/tests/basic.test.ts b/packages/jest-environment-puppeteer/tests/basic.test.ts index afc38d01..15149825 100644 --- a/packages/jest-environment-puppeteer/tests/basic.test.ts +++ b/packages/jest-environment-puppeteer/tests/basic.test.ts @@ -1,6 +1,5 @@ // import globals import "jest-puppeteer"; -import "expect-puppeteer"; describe("Basic", () => { beforeAll(async () => { diff --git a/packages/jest-environment-puppeteer/tests/browserContext-1.test.ts b/packages/jest-environment-puppeteer/tests/browserContext-1.test.ts index 5ef3920e..aa94c3f0 100644 --- a/packages/jest-environment-puppeteer/tests/browserContext-1.test.ts +++ b/packages/jest-environment-puppeteer/tests/browserContext-1.test.ts @@ -1,6 +1,5 @@ // import globals import "jest-puppeteer"; -import "expect-puppeteer"; describe("browserContext", () => { const test = process.env.INCOGNITO ? it : it.skip; diff --git a/packages/jest-environment-puppeteer/tests/browserContext-2.test.ts b/packages/jest-environment-puppeteer/tests/browserContext-2.test.ts index 426f3f3e..aa6aaf4e 100644 --- a/packages/jest-environment-puppeteer/tests/browserContext-2.test.ts +++ b/packages/jest-environment-puppeteer/tests/browserContext-2.test.ts @@ -1,6 +1,5 @@ // import globals import "jest-puppeteer"; -import "expect-puppeteer"; describe("browserContext", () => { const test = process.env.INCOGNITO ? it : it.skip; diff --git a/packages/jest-environment-puppeteer/tests/config.test.ts b/packages/jest-environment-puppeteer/tests/config.test.ts index 5bb279f0..2d1cef7d 100644 --- a/packages/jest-environment-puppeteer/tests/config.test.ts +++ b/packages/jest-environment-puppeteer/tests/config.test.ts @@ -3,7 +3,6 @@ import { readConfig } from "../src/config"; // import globals import "jest-puppeteer"; -import "expect-puppeteer"; // This test does not run on Node.js < v20 (segfault) xdescribe("readConfig", () => { diff --git a/packages/jest-environment-puppeteer/tests/resetBrowser.test.ts b/packages/jest-environment-puppeteer/tests/resetBrowser.test.ts index a252e356..8714c39e 100644 --- a/packages/jest-environment-puppeteer/tests/resetBrowser.test.ts +++ b/packages/jest-environment-puppeteer/tests/resetBrowser.test.ts @@ -1,6 +1,5 @@ // import globals import "jest-puppeteer"; -import "expect-puppeteer"; describe("resetBrowser", () => { test("should reset browser", async () => { diff --git a/packages/jest-environment-puppeteer/tests/resetPage.test.ts b/packages/jest-environment-puppeteer/tests/resetPage.test.ts index 74ebbc75..24dd54f1 100644 --- a/packages/jest-environment-puppeteer/tests/resetPage.test.ts +++ b/packages/jest-environment-puppeteer/tests/resetPage.test.ts @@ -1,6 +1,5 @@ // import globals import "jest-puppeteer"; -import "expect-puppeteer"; describe("resetPage", () => { test("should reset page", async () => { diff --git a/packages/jest-environment-puppeteer/tests/runBeforeUnloadOnClose.test.ts b/packages/jest-environment-puppeteer/tests/runBeforeUnloadOnClose.test.ts index 2def0aad..9c7ff5bd 100644 --- a/packages/jest-environment-puppeteer/tests/runBeforeUnloadOnClose.test.ts +++ b/packages/jest-environment-puppeteer/tests/runBeforeUnloadOnClose.test.ts @@ -1,6 +1,5 @@ // import globals import "jest-puppeteer"; -import "expect-puppeteer"; describe("runBeforeUnloadOnClose", () => { it("shouldn’t call page.close with runBeforeUnload by default", async () => {