diff --git a/README.md b/README.md index 57e6b253..dac05957 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,41 @@ -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 downloads](https://img.shields.io/npm/dm/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) -`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. +`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](#getting-started) - - [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) + - [Install the packages](#install-the-packages) + - [Write a test](#write-a-test) + - [Visual testing with Argos](#visual-testing-with-argos) 2. [Recipes](#recipes) - - [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) + - [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) 5. [Troubleshooting](#troubleshooting) 6. [Acknowledgements](#acknowledgements) ## Getting Started -### Installation - -To start using `jest-puppeteer`, you’ll need to install the following packages: +### Install the packages ```bash npm install --save-dev jest-puppeteer puppeteer jest ``` -This will install Jest (the testing framework), Puppeteer (the headless browser tool), and `jest-puppeteer` (the integration between the two). - -### Basic Setup +### Update your Jest configuration -In your Jest configuration file (`jest.config.js`), add `jest-puppeteer` as the preset: +Add jest-puppeteer as a preset in your Jest configuration file "jest.config.js": ```json { @@ -52,131 +43,149 @@ In your Jest configuration file (`jest.config.js`), add `jest-puppeteer` as the } ``` -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. +> **Note** +> Ensure you remove any existing `testEnvironment` options from your Jest configuration -### Writing Your First Test +### Write a test -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`): +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: ```js import "expect-puppeteer"; -describe("Google Homepage", () => { +describe("Google", () => { beforeAll(async () => { await page.goto("https://google.com"); }); - it('should display "Google" in the page title', async () => { - await expect(page).toMatchTitle(/Google/); + it('should display "google" text on page', async () => { + await expect(page).toMatchTextContent("google"); }); }); ``` -This example test navigates to Google’s homepage and checks if the title contains the word "Google". `jest-puppeteer` simplifies working with Puppeteer by exposing the `page` object, allowing you to write tests using a familiar syntax. +### Visual testing with Argos -### TypeScript Setup +[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. -If you’re using TypeScript, `jest-puppeteer` natively supports it from version `8.0.0`. To get started with TypeScript, follow these steps: +To get started with Argos, 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: +1. [Install Argos GitHub App](https://github.com/apps/argos-ci) +2. Install the packages -```bash -npm uninstall --save-dev @types/jest-environment-puppeteer @types/expect-puppeteer +```sh +npm install --save-dev @argos-ci/cli @argos-ci/puppeteer ``` -2. Jest will automatically pick up type definitions from either `@types/jest` or `@jest/globals`. Once you’ve set up the environment, you can start writing tests in TypeScript just like in JavaScript: +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` -- Example using `@types/jest`: +After installing Argos, learn how to [review visual changes](https://argos-ci.com/docs/review-changes) in your development workflow. -```ts -import "jest-puppeteer"; -import "expect-puppeteer"; +#### Synchronous configuration -describe("Google Homepage", (): void => { - beforeAll(async (): Promise => { - await page.goto("https://google.com"); - }); +```js +// jest-puppeteer.config.cjs - it('should display "Google" in the title', async (): Promise => { - await expect(page).toMatchTitle(/Google/); - }); -}); +/** @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, + }, +}; ``` -- Example using `@jest/globals`: +#### Asynchronous configuration -```ts -import { expect, describe, beforeAll, it } from "@jest/globals"; -import "jest-puppeteer"; -import "expect-puppeteer"; +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. -describe("Google Homepage", (): void => { - beforeAll(async (): Promise => { - await page.goto("https://google.com"); - }); +```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, + }, + }; +} - it('should display "Google" in the title', async (): Promise => { - await expect(page).toMatchTitle(/Google/); - }); -}); +module.exports = getConfig(); ``` -### Visual Testing with Argos +## Recipes -[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. +### Enhance testing with `expect-puppeteer` lib -To get started, check out the [Puppeteer Quickstart Guide](https://argos-ci.com/docs/quickstart/puppeteer). +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). -## Recipes +Here are some examples: -### Using `expect-puppeteer` - -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: - -- Assert that a page contains certain text: +#### Find a text in the page ```js -await expect(page).toMatchTextContent("Expected text"); +// Assert that the current page contains 'Text in the page' +await expect(page).toMatchTextContent("Text in the page"); ``` -- Simulate a button click: +#### Click a button ```js -await expect(page).toClick("button", { text: "Submit" }); +// Assert that a button containing text "Home" will be clicked +await expect(page).toClick("button", { text: "Home" }); ``` -- Fill out a form: +#### Fill a form ```js -await expect(page).toFillForm('form[name="login"]', { - username: "testuser", - password: "password", +// Assert that a form will be filled +await expect(page).toFillForm('form[name="myForm"]', { + firstName: "James", + lastName: "Bond", }); ``` -### Debugging Tests +### Debug mode -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: +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: ```js await jestPuppeteer.debug(); ``` -To prevent the test from timing out, increase Jest’s timeout: +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: ```js -jest.setTimeout(300000); // 5 minutes +jest.setTimeout(300000); // Set the timeout to 5 minutes (300000 ms) ``` -This can be particularly useful when you need to step through interactions or inspect the state of the page during test execution. +### Automatic server starting -### Automatic Server Management - -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: +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: ```js +// jest-puppeteer.config.cjs + +/** @type {import('jest-environment-puppeteer').JestPuppeteerConfig} */ module.exports = { server: { command: "node server.js", @@ -185,55 +194,118 @@ module.exports = { }; ``` -This eliminates the need to manually manage your server during testing. +Other options are documented in [jest-dev-server](https://github.com/smooth-code/jest-puppeteer/tree/master/packages/jest-dev-server). -### Customizing the Puppeteer Instance +### Customizing Puppeteer instance -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: +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](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: ```js +// jest-puppeteer.config.cjs + +/** @type {import('jest-environment-puppeteer').JestPuppeteerConfig} */ module.exports = { launch: { - product: "firefox", + dumpio: true, headless: process.env.HEADLESS !== "false", + product: "chrome", }, + browserContext: "default", }; ``` -This file allows you to configure browser options, set up browser contexts, and more. +### Customizing `setupTestFrameworkScriptFile` or `setupFilesAfterEnv` -### 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: +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: ```js // setup.js require("expect-puppeteer"); -``` -Then, in your Jest config: +// Your custom setup +// ... +``` ```js +// jest.config.js module.exports = { + // ... + setupTestFrameworkScriptFile: "./setup.js", + // or setupFilesAfterEnv: ["./setup.js"], }; ``` -### Extending `PuppeteerEnvironment` +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. -For advanced use cases, you can extend the default `PuppeteerEnvironment` class to add custom functionality: +```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", + ], + }, + ], +}; +``` + +### Extend `PuppeteerEnvironment` + +If you need to use your custom environment, you can extend the `PuppeteerEnvironment`. + +First, create a JavaScript file for your custom environment: ```js +// custom-environment.js const PuppeteerEnvironment = require("jest-environment-puppeteer"); class CustomEnvironment extends PuppeteerEnvironment { async setup() { await super.setup(); - // Custom setup logic + // Your setup } async teardown() { - // Custom teardown logic + // Your teardown await super.teardown(); } } @@ -241,9 +313,20 @@ class CustomEnvironment extends PuppeteerEnvironment { module.exports = CustomEnvironment; ``` -### Global Setup and Teardown +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: -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 +{ + // ... + "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: ```js // global-setup.js @@ -251,147 +334,208 @@ const setupPuppeteer = require("jest-environment-puppeteer/setup"); module.exports = async function globalSetup(globalConfig) { await setupPuppeteer(globalConfig); - // Additional setup logic + // Your global setup }; ``` -In your Jest configuration, reference these files: +```js +// global-teardown.js +const teardownPuppeteer = require("jest-environment-puppeteer/teardown"); -```json +module.exports = async function globalTeardown(globalConfig) { + // Your global teardown + await teardownPuppeteer(globalConfig); +}; +``` + +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. + +```js { + // ... "globalSetup": "./global-setup.js", "globalTeardown": "./global-teardown.js" } ``` -### Jest-Puppeteer Configuration +Now, your custom `globalSetup` and `globalTeardown` will be executed once before and after all test suites, respectively. -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. +## Configuring Jest-Puppeteer -Possible configuration formats: +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`. -- A `.jest-puppeteerrc` file (JSON, YAML, or JavaScript). -- A `.jest-puppeteer.config.js` or `.jest-puppeteer.config.cjs` file that exports a configuration object. +- 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. -Example of a basic configuration file (`jest-puppeteer.config.js`): +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. -```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, - }, -}; +Ensure that the exported configuration is either a config object or a Promise that returns a config object. + +```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; +} ``` -You can further extend this configuration to connect to a remote instance of Chrome or customize the environment for your test runs. +## API -## API Reference +### `global.browser` -Jest-Puppeteer exposes several global objects and methods to facilitate test writing: +Provides access to the [Puppeteer Browser](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-browser). -- **`global.browser`**: Provides access to the Puppeteer [Browser](https://pptr.dev/#?product=Puppeteer&version=v13.0.0&show=api-class-browser) instance. +```js +it("should open a new page", async () => { + const page = await browser.newPage(); + await page.goto("https://google.com"); +}); +``` - Example: +### `global.page` - ```js - const page = await browser.newPage(); - await page.goto("https://example.com"); - ``` +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). -- **`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. +```js +it("should fill an input", async () => { + await page.type("#myinput", "Hello"); +}); +``` - Example: +### `global.context` - ```js - await page.type("#input", "Hello World"); - ``` +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. -- **`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. +### `global.expect(page)` -- **`global.expect(page)`**: The enhanced `expect` API provided by `expect-puppeteer`. You can use this to make assertions on the Puppeteer `page`. +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). - Example: +```js +await expect(page).toMatchTextContent("A text in the page"); +// ... +``` - ```js - await expect(page).toMatchTextContent("Expected text on page"); - ``` +### `global.jestPuppeteer.debug()` -- **`global.jestPuppeteer.debug()`**: Suspends test execution, allowing you to inspect the browser and debug. +Put test in debug mode. - Example: +- Jest is suspended (no timeout) +- A `debugger` instruction to Chromium, if Puppeteer has been launched with `{ devtools: true }` it will pause - ```js +```js +it("should put test in debug mode", async () => { await jestPuppeteer.debug(); - ``` - -- **`global.jestPuppeteer.resetPage()`**: Resets the `page` object before each test. +}); +``` - Example: +### `global.jestPuppeteer.resetPage()` - ```js - beforeEach(async () => { - await jestPuppeteer.resetPage(); - }); - ``` +To reset `global.page` before each test, use the following code: -- **`global.jestPuppeteer.resetBrowser()`**: Resets the `browser`, `context`, and `page` objects, ensuring a clean slate for each test. +```js +beforeEach(async () => { + await jestPuppeteer.resetPage(); +}); +``` - Example: +### `global.jestPuppeteer.resetBrowser()` - ```js - beforeEach(async () => { - await jestPuppeteer.resetBrowser(); - }); - ``` +To reset `global.browser`, `global.context`, and `global.page` before each test, use the following code: -These methods simplify the setup and teardown process for tests, making it easier to work with Puppeteer in a Jest environment. +```js +beforeEach(async () => { + await jestPuppeteer.resetBrowser(); +}); +``` ## Troubleshooting -### CI Timeout Issues +### TypeScript -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: +TypeScript is natively supported from v8.0.0, for previous versions, you have to use [community-provided types](https://github.com/DefinitelyTyped/DefinitelyTyped). -Run tests in a single process: +Note though that it still requires installation of the [type definitions for jest](https://www.npmjs.com/package/@types/jest) : ```bash -jest --runInBand +npm install --save-dev @types/jest ``` -Alternatively, you can limit the number of parallel workers: +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). -```bash -jest --maxWorkers=2 +```ts +// import globals +import "jest-puppeteer"; +import "expect-puppeteer"; + +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"); + }); +}); ``` -This ensures that your CI environment doesn’t get overloaded by too many concurrent processes, which can improve the reliability of your tests. +### CI Timeout -### Debugging CI Failures +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. -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: +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). -```js -await jestPuppeteer.debug(); +You can achieve this through the CLI by running: + +```sh +jest --runInBand ``` -To avoid test timeouts in CI, set a larger timeout during the debugging process: +Alternatively, you can set Jest to use a maximum number of workers that your CI environment supports: -```js -jest.setTimeout(600000); // 10 minutes +``` +jest --maxWorkers=2 ``` -### Preventing ESLint Errors with Global Variables +### Prevent ESLint errors on global variables -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: +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: ```js // .eslintrc.js @@ -409,8 +553,6 @@ module.exports = { }; ``` -This configuration will prevent ESLint from throwing errors about undefined globals. - ## Acknowledgements -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. +Special thanks to Fumihiro Xue for providing an excellent [Jest example](https://github.com/xfumihiro/jest-puppeteer-example). diff --git a/packages/expect-puppeteer/README.md b/packages/expect-puppeteer/README.md index 4b7669ce..c29e6712 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://pptr.dev/api) is great, but it is low level and not designed for integration testing. +[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. 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]|[Frame]|[ElementHandle]> Context +- `instance` <[Page]|[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`. - - `count` <[number]> defaults to 1. See [UIEvent.detail]. + - `clickCount` <[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]|[Frame]|[ElementHandle]> Context -- `selector` <[string]|[MatchSelector](#MatchSelector)> A [selector] or a [MatchSelector](#MatchSelector) to match field +- `instance` <[Page]|[ElementHandle]> Context +- `selector` <[string]> A [selector] 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]|[Frame]|[ElementHandle]> Context -- `selector` <[string]|[MatchSelector](#MatchSelector)> A [selector] or a [MatchSelector](#MatchSelector) to match form +- `instance` <[Page]|[ElementHandle]> Context +- `selector` <[string]> A [selector] 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]|[Frame]|[ElementHandle]> Context +- `instance` <[Page]|[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]|[Frame]|[ElementHandle]> Context -- `selector` <[string]|[MatchSelector](#MatchSelector)> A [selector] or a [MatchSelector](#MatchSelector) to match element +- `instance` <[Page]|[ElementHandle]> Context +- `selector` <[string]> A [selector] 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]|[Frame]|[ElementHandle]> Context -- `selector` <[string]|[MatchSelector](#MatchSelector)> A [selector] or a [MatchSelector](#MatchSelector) to match select [element] +- `instance` <[Page]|[ElementHandle]> Context +- `selector` <[string]> A [selector] 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]|[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 +- `instance` <[Page]|[ElementHandle]> Context +- `selector` <[string]> A [selector] to match input [element] +- `filePath` <[string]> A file path ```js import { join } from "node:path"; @@ -208,7 +208,7 @@ await expect(page).toUploadFile( ); ``` -### Match Selector +### {type: [string], value: [string]} An object used as parameter in order to select an element. @@ -242,7 +242,6 @@ 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://pptr.dev/api/puppeteer.page "Page" -[frame]: https://pptr.dev/api/puppeteer.frame "Frame" -[elementhandle]: https://pptr.dev/api/puppeteer.elementhandle/ "ElementHandle" +[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" [uievent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail diff --git a/packages/expect-puppeteer/src/index.test.ts b/packages/expect-puppeteer/src/index.test.ts index 4ddab0dd..0cec8408 100644 --- a/packages/expect-puppeteer/src/index.test.ts +++ b/packages/expect-puppeteer/src/index.test.ts @@ -2,6 +2,7 @@ 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 98159528..a039e236 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 common matchers list -type InstanceMatchers = T extends PuppeteerInstance +// declare matchers list +type PuppeteerMatchers = T extends PuppeteerInstance ? { // common toClick: Wrapper; @@ -64,24 +64,24 @@ type InstanceMatchers = T extends PuppeteerInstance : never; // declare page matchers list -interface PageMatchers extends InstanceMatchers { +interface PageMatchers extends PuppeteerMatchers { // instance specific toDisplayDialog: Wrapper; // inverse matchers - not: InstanceMatchers[`not`] & {}; + not: PuppeteerMatchers[`not`] & {}; } // declare frame matchers list -interface FrameMatchers extends InstanceMatchers { +interface FrameMatchers extends PuppeteerMatchers { // inverse matchers - not: InstanceMatchers[`not`] & {}; + not: PuppeteerMatchers[`not`] & {}; } // declare element matchers list interface ElementHandleMatchers - extends InstanceMatchers> { + extends PuppeteerMatchers> { // inverse matchers - not: InstanceMatchers>[`not`] & {}; + not: PuppeteerMatchers>[`not`] & {}; } // declare matchers per instance type @@ -103,50 +103,43 @@ type GlobalWithExpect = typeof globalThis & { expect: PuppeteerExpect }; // --------------------------- -// 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 +// extend global jest object declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace jest { - // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars - interface Matchers extends PuppeteerMatchers {} + // 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; + }; + } } } -// support for @jest/types -declare module "@jest/expect" { - // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars - interface Matchers extends PuppeteerMatchers {} -} - // --------------------------- // @ts-expect-error global node object w/ initial jest expect prop attached const jestExpect = global.expect as JestExpect; @@ -158,7 +151,7 @@ const wrapMatcher = ( instance: T, ) => async function throwingMatcher(...args: unknown[]): Promise { - // update the assertions counter + // ??? jestExpect.getState().assertionCalls += 1; try { // run async matcher @@ -183,9 +176,7 @@ const puppeteerExpect = (instance: T) => { ]; if (!isPage && !isFrame && !isHandle) - throw new Error( - `${String(instance?.constructor?.name ?? `current instance`)} is not supported`, - ); + throw new Error(`${instance} is not supported`); // retrieve matchers const expectation = { diff --git a/packages/expect-puppeteer/src/matchers/toClick.ts b/packages/expect-puppeteer/src/matchers/toClick.ts index 49139c8b..3256ddcd 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, count, offset, ...otherOptions } = options; + const { delay, button, clickCount, offset, ...otherOptions } = options; const element = await toMatchElement(instance, selector, otherOptions); - await element.click({ delay, button, count, offset }); + await element.click({ delay, button, clickCount, offset }); } diff --git a/packages/jest-environment-puppeteer/CHANGELOG.md b/packages/jest-environment-puppeteer/CHANGELOG.md index 542b8f8a..2fbd9247 100644 --- a/packages/jest-environment-puppeteer/CHANGELOG.md +++ b/packages/jest-environment-puppeteer/CHANGELOG.md @@ -3,17 +3,6 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [10.1.3](https://github.com/argos-ci/jest-puppeteer/compare/v10.1.2...v10.1.3) (2024-10-22) - - -### Bug Fixes - -* fix types resolution when importing jest types from @jest/globals ([#602](https://github.com/argos-ci/jest-puppeteer/issues/602)) ([e5b2e1a](https://github.com/argos-ci/jest-puppeteer/commit/e5b2e1a7c0282aba496ffe2806201778b84a96fc)) - - - - - ## [10.1.2](https://github.com/argos-ci/jest-puppeteer/compare/v10.1.1...v10.1.2) (2024-10-10) diff --git a/packages/jest-environment-puppeteer/README.md b/packages/jest-environment-puppeteer/README.md index 80706b3f..9872c504 100644 --- a/packages/jest-environment-puppeteer/README.md +++ b/packages/jest-environment-puppeteer/README.md @@ -37,21 +37,6 @@ describe("Google", () => { }); ``` -## Use with TypeScript - -_Note : If you have upgraded to version v10.1.2 or above, we strongly recommend that you uninstall the community provided types :_ - -```bash -npm uninstall --save-dev @types/jest-environment-puppeteer @types/expect-puppeteer -``` - -If using TypeScript, jest-puppeteer has to be explicitly imported in order to expose the global API : - -```ts -// import jest-puppeteer globals -import "jest-puppeteer"; -``` - ## API ### `global.browser` diff --git a/packages/jest-environment-puppeteer/tests/basic.test.ts b/packages/jest-environment-puppeteer/tests/basic.test.ts index 15149825..afc38d01 100644 --- a/packages/jest-environment-puppeteer/tests/basic.test.ts +++ b/packages/jest-environment-puppeteer/tests/basic.test.ts @@ -1,5 +1,6 @@ // 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 aa94c3f0..5ef3920e 100644 --- a/packages/jest-environment-puppeteer/tests/browserContext-1.test.ts +++ b/packages/jest-environment-puppeteer/tests/browserContext-1.test.ts @@ -1,5 +1,6 @@ // 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 aa6aaf4e..426f3f3e 100644 --- a/packages/jest-environment-puppeteer/tests/browserContext-2.test.ts +++ b/packages/jest-environment-puppeteer/tests/browserContext-2.test.ts @@ -1,5 +1,6 @@ // 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 2d1cef7d..5bb279f0 100644 --- a/packages/jest-environment-puppeteer/tests/config.test.ts +++ b/packages/jest-environment-puppeteer/tests/config.test.ts @@ -3,6 +3,7 @@ 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 8714c39e..a252e356 100644 --- a/packages/jest-environment-puppeteer/tests/resetBrowser.test.ts +++ b/packages/jest-environment-puppeteer/tests/resetBrowser.test.ts @@ -1,5 +1,6 @@ // 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 24dd54f1..74ebbc75 100644 --- a/packages/jest-environment-puppeteer/tests/resetPage.test.ts +++ b/packages/jest-environment-puppeteer/tests/resetPage.test.ts @@ -1,5 +1,6 @@ // 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 9c7ff5bd..2def0aad 100644 --- a/packages/jest-environment-puppeteer/tests/runBeforeUnloadOnClose.test.ts +++ b/packages/jest-environment-puppeteer/tests/runBeforeUnloadOnClose.test.ts @@ -1,5 +1,6 @@ // import globals import "jest-puppeteer"; +import "expect-puppeteer"; describe("runBeforeUnloadOnClose", () => { it("shouldn’t call page.close with runBeforeUnload by default", async () => {