Skip to content

Commit

Permalink
fix: fix types resolution when importing jest types from @jest/globals (
Browse files Browse the repository at this point in the history
#602)

Also improve the guide for TypeScript in README.
  • Loading branch information
mulekick authored Oct 22, 2024
1 parent 6a91899 commit e5b2e1a
Show file tree
Hide file tree
Showing 13 changed files with 140 additions and 97 deletions.
82 changes: 54 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<void> => {
await page.goto("https://google.com");
});

it('should display "google" text on page', async (): Promise<void> => {
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<void> => {
await page.goto("https://google.com");
});

it('should display "google" text on page', async (): Promise<void> => {
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.
Expand Down Expand Up @@ -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<void> => {
await page.goto("https://google.com");
});

it('should display "google" text on page', async (): Promise<void> => {
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.
Expand Down
37 changes: 19 additions & 18 deletions packages/expect-puppeteer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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`.

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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";
Expand All @@ -208,7 +208,7 @@ await expect(page).toUploadFile(
);
```

### <a name="MatchSelector"></a>{type: [string], value: [string]}
### <a name="MatchSelector"></a>Match Selector

An object used as parameter in order to select an element.

Expand Down Expand Up @@ -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
1 change: 0 additions & 1 deletion packages/expect-puppeteer/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { getDefaultOptions, setDefaultOptions } from "expect-puppeteer";

// import globals
import "jest-puppeteer";
import "expect-puppeteer";

expect.addSnapshotSerializer({
print: () => "hello",
Expand Down
91 changes: 50 additions & 41 deletions packages/expect-puppeteer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ type Wrapper<T> = T extends (
? (...args: A) => R
: never;

// declare matchers list
type PuppeteerMatchers<T> = T extends PuppeteerInstance
// declare common matchers list
type InstanceMatchers<T> = T extends PuppeteerInstance
? {
// common
toClick: Wrapper<typeof toClick>;
Expand All @@ -64,24 +64,24 @@ type PuppeteerMatchers<T> = T extends PuppeteerInstance
: never;

// declare page matchers list
interface PageMatchers extends PuppeteerMatchers<Page> {
interface PageMatchers extends InstanceMatchers<Page> {
// instance specific
toDisplayDialog: Wrapper<typeof toDisplayDialog>;
// inverse matchers
not: PuppeteerMatchers<Page>[`not`] & {};
not: InstanceMatchers<Page>[`not`] & {};
}

// declare frame matchers list
interface FrameMatchers extends PuppeteerMatchers<Frame> {
interface FrameMatchers extends InstanceMatchers<Frame> {
// inverse matchers
not: PuppeteerMatchers<Frame>[`not`] & {};
not: InstanceMatchers<Frame>[`not`] & {};
}

// declare element matchers list
interface ElementHandleMatchers
extends PuppeteerMatchers<ElementHandle<Element>> {
extends InstanceMatchers<ElementHandle<Element>> {
// inverse matchers
not: PuppeteerMatchers<ElementHandle<Element>>[`not`] & {};
not: InstanceMatchers<ElementHandle<Element>>[`not`] & {};
}

// declare matchers per instance type
Expand All @@ -103,43 +103,50 @@ type GlobalWithExpect = typeof globalThis & { expect: PuppeteerExpect };

// ---------------------------

// extend global jest object
// not possible to use PMatchersPerType directly ...
interface PuppeteerMatchers<T> {
// common
toClick: T extends PuppeteerInstance ? Wrapper<typeof toClick> : never;
toFill: T extends PuppeteerInstance ? Wrapper<typeof toFill> : never;
toFillForm: T extends PuppeteerInstance ? Wrapper<typeof toFillForm> : never;
toMatchTextContent: T extends PuppeteerInstance
? Wrapper<typeof toMatchTextContent>
: never;
toMatchElement: T extends PuppeteerInstance
? Wrapper<typeof toMatchElement>
: never;
toSelect: T extends PuppeteerInstance ? Wrapper<typeof toSelect> : never;
toUploadFile: T extends PuppeteerInstance
? Wrapper<typeof toUploadFile>
: never;
// page
toDisplayDialog: T extends Page ? Wrapper<typeof toDisplayDialog> : never;
// inverse matchers
not: {
toMatchTextContent: T extends PuppeteerInstance
? Wrapper<typeof notToMatchTextContent>
: never;
toMatchElement: T extends PuppeteerInstance
? Wrapper<typeof notToMatchElement>
: 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<R, T> {
// common
toClick: T extends PuppeteerInstance ? Wrapper<typeof toClick> : never;
toFill: T extends PuppeteerInstance ? Wrapper<typeof toFill> : never;
toFillForm: T extends PuppeteerInstance
? Wrapper<typeof toFillForm>
: never;
toMatchTextContent: T extends PuppeteerInstance
? Wrapper<typeof toMatchTextContent>
: never;
toMatchElement: T extends PuppeteerInstance
? Wrapper<typeof toMatchElement>
: never;
toSelect: T extends PuppeteerInstance ? Wrapper<typeof toSelect> : never;
toUploadFile: T extends PuppeteerInstance
? Wrapper<typeof toUploadFile>
: never;
// page
toDisplayDialog: T extends Page ? Wrapper<typeof toDisplayDialog> : never;
// inverse matchers
not: {
toMatchTextContent: T extends PuppeteerInstance
? Wrapper<typeof notToMatchTextContent>
: never;
toMatchElement: T extends PuppeteerInstance
? Wrapper<typeof notToMatchElement>
: never;
};
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars
interface Matchers<R, T> extends PuppeteerMatchers<T> {}
}
}

// 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<R, T> extends PuppeteerMatchers<T> {}
}

// ---------------------------
// @ts-expect-error global node object w/ initial jest expect prop attached
const jestExpect = global.expect as JestExpect;
Expand All @@ -151,7 +158,7 @@ const wrapMatcher = <T extends PuppeteerInstance>(
instance: T,
) =>
async function throwingMatcher(...args: unknown[]): Promise<unknown> {
// ???
// update the assertions counter
jestExpect.getState().assertionCalls += 1;
try {
// run async matcher
Expand All @@ -176,7 +183,9 @@ const puppeteerExpect = <T extends PuppeteerInstance>(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 = {
Expand Down
4 changes: 2 additions & 2 deletions packages/expect-puppeteer/src/matchers/toClick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
15 changes: 15 additions & 0 deletions packages/jest-environment-puppeteer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Loading

0 comments on commit e5b2e1a

Please sign in to comment.