From e5b2e1a7c0282aba496ffe2806201778b84a96fc Mon Sep 17 00:00:00 2001 From: mulekick Date: Tue, 22 Oct 2024 22:59:19 +0200 Subject: [PATCH] fix: fix types resolution when importing jest types from @jest/globals (#602) Also improve the guide for TypeScript in README. --- README.md | 82 +++++++++++------ packages/expect-puppeteer/README.md | 37 ++++---- packages/expect-puppeteer/src/index.test.ts | 1 - packages/expect-puppeteer/src/index.ts | 91 ++++++++++--------- .../expect-puppeteer/src/matchers/toClick.ts | 4 +- packages/jest-environment-puppeteer/README.md | 15 +++ .../tests/basic.test.ts | 1 - .../tests/browserContext-1.test.ts | 1 - .../tests/browserContext-2.test.ts | 1 - .../tests/config.test.ts | 1 - .../tests/resetBrowser.test.ts | 1 - .../tests/resetPage.test.ts | 1 - .../tests/runBeforeUnloadOnClose.test.ts | 1 - 13 files changed, 140 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index dac05957..10ad3cd2 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ 1. [Getting Started](#getting-started) - [Install the packages](#install-the-packages) - [Write a test](#write-a-test) + - [Use with Typescript](#use-with-typescript) - [Visual testing with Argos](#visual-testing-with-argos) 2. [Recipes](#recipes) - [Enhance testing with `expect-puppeteer` lib](#enhance-testing-with-expect-puppeteer-lib) @@ -64,6 +65,59 @@ describe("Google", () => { }); ``` +### Use with TypeScript + +TypeScript is natively supported from v8.0.0, for previous versions, you have to use [community-provided types](https://github.com/DefinitelyTyped/DefinitelyTyped). + +_Note : If you have upgraded to version v10.1.2 or above, we strongly recommend that you uninstall them :_ + +```bash +npm uninstall --save-dev @types/jest-environment-puppeteer @types/expect-puppeteer +``` + +Native types definitions are available whether you use `@types/jest` or `@jest/globals` for [jest types](https://jestjs.io/docs/getting-started#type-definitions). + +Once setup, import the jest-puppeteer modules in your test file, then write your test logic the same way you would in Javascript. + +- If using `@types/jest` : + +```ts +// import jest-puppeteer 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"); + }); +}); +``` + +- If using `@jest/globals` : + +```ts +// import jest types +import { expect, describe, beforeAll, it } from "@jest/globals"; + +// import jest-puppeteer 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"); + }); +}); +``` + ### Visual testing with Argos [Argos](https://argos-ci.com) is a powerful visual testing tool that allows to review visual changes introduced by each pull request. @@ -486,34 +540,6 @@ beforeEach(async () => { ## Troubleshooting -### TypeScript - -TypeScript is natively supported from v8.0.0, for previous versions, you have to use [community-provided types](https://github.com/DefinitelyTyped/DefinitelyTyped). - -Note though that it still requires installation of the [type definitions for jest](https://www.npmjs.com/package/@types/jest) : - -```bash -npm install --save-dev @types/jest -``` - -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"; - -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"); - }); -}); -``` - ### 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. 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/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..98159528 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,43 +103,50 @@ 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 {} } } +// 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; @@ -151,7 +158,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 +183,9 @@ const puppeteerExpect = (instance: T) => { ]; if (!isPage && !isFrame && !isHandle) - throw new Error(`${instance} is not supported`); + throw new Error( + `${String(instance?.constructor?.name ?? `current 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 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..80706b3f 100644 --- a/packages/jest-environment-puppeteer/README.md +++ b/packages/jest-environment-puppeteer/README.md @@ -37,6 +37,21 @@ 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 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 () => {