diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e6b270ac..6a727c41 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -16,7 +16,7 @@ jobs: if: ${{ !contains(github.head_ref, 'all-contributors') }} strategy: matrix: - node: [10.14, 12, 14, 15, 16] + node: [14, 16, 18, 20] runs-on: ubuntu-latest steps: - name: ⬇️ Checkout repo diff --git a/README.md b/README.md index 7c6d3c65..4f0961d9 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,10 @@ clear to read and to maintain. - [Installation](#installation) - [Usage](#usage) + - [With `@jest/globals`](#with-jestglobals) + - [With Vitest](#with-vitest) - [With TypeScript](#with-typescript) + - [With another Jest-compatible `expect`](#with-another-jest-compatible-expect) - [Custom matchers](#custom-matchers) - [`toBeDisabled`](#tobedisabled) - [`toBeEnabled`](#tobeenabled) @@ -128,6 +131,39 @@ import '@testing-library/jest-dom' setupFilesAfterEnv: ['/jest-setup.js'] ``` +### With `@jest/globals` + +If you are using [`@jest/globals`][jest-globals announcement] with +[`injectGlobals: false`][inject-globals docs], you will need to use a different +import in your tests setup file: + +```javascript +// In your own jest-setup.js (or any other name) +import '@testing-library/jest-dom/jest-globals' +``` + +[jest-globals announcement]: + https://jestjs.io/blog/2020/05/05/jest-26#a-new-way-to-consume-jest---jestglobals +[inject-globals docs]: + https://jestjs.io/docs/configuration#injectglobals-boolean + +### With Vitest + +If you are using [vitest][], this module will work as-is, but you will need to +use a different import in your tests setup file. This file should be added to +the [`setupFiles`][vitest setupfiles] property in your vitest config: + +```javascript +// In your own vitest-setup.js (or any other name) +import '@testing-library/jest-dom/vitest' + +// In vitest.config.js add (if you haven't already) +setupFiles: ['./vitest-setup.js'] +``` + +[vitest]: https://vitest.dev/ +[vitest setupfiles]: https://vitest.dev/config/#setupfiles + ### With TypeScript If you're using TypeScript, make sure your setup file is a `.ts` and not a `.js` @@ -144,6 +180,18 @@ haven't already: ], ``` +### With another Jest-compatible `expect` + +If you are using a different test runner that is compatible with Jest's `expect` +interface, it might be possible to use it with this library: + +```javascript +import * as matchers from '@testing-library/jest-dom/matchers' +import {expect} from 'my-test-runner/expect' + +expect.extend(matchers) +``` + ## Custom matchers `@testing-library/jest-dom` can work with any library or framework that returns diff --git a/extend-expect.js b/extend-expect.js deleted file mode 100644 index e7d19c10..00000000 --- a/extend-expect.js +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line -require('./dist/extend-expect') diff --git a/jest-globals.d.ts b/jest-globals.d.ts new file mode 100644 index 00000000..6c8e0b60 --- /dev/null +++ b/jest-globals.d.ts @@ -0,0 +1 @@ +/// diff --git a/jest-globals.js b/jest-globals.js new file mode 100644 index 00000000..eeaf6ac9 --- /dev/null +++ b/jest-globals.js @@ -0,0 +1,4 @@ +const globals = require('@jest/globals') +const extensions = require('./dist/matchers') + +globals.expect.extend(extensions) diff --git a/matchers.d.ts b/matchers.d.ts new file mode 100644 index 00000000..c1ec8ce5 --- /dev/null +++ b/matchers.d.ts @@ -0,0 +1,3 @@ +import * as matchers from './types/matchers' + +export = matchers diff --git a/package.json b/package.json index 0304d2c1..f8346ee5 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "version": "0.0.0-semantically-released", "description": "Custom jest matchers to test the state of the DOM", "main": "dist/index.js", + "types": "types/index.d.ts", "engines": { - "node": ">=8", + "node": ">=14", "npm": ">=6", "yarn": ">=1" }, @@ -19,8 +20,11 @@ }, "files": [ "dist", - "extend-expect.js", - "matchers.js" + "types", + "*.d.ts", + "jest-globals.js", + "matchers.js", + "vitest.js" ], "keywords": [ "testing", @@ -32,7 +36,6 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", "aria-query": "^5.0.0", "chalk": "^3.0.0", "@adobe/css-tools": "^4.0.1", @@ -42,16 +45,44 @@ "redent": "^3.0.0" }, "devDependencies": { + "@jest/globals": "^29.6.2", + "expect": "^29.6.2", "jest-environment-jsdom-sixteen": "^1.0.3", "jest-watch-select-projects": "^2.0.0", "jsdom": "^16.2.1", - "kcd-scripts": "^11.1.0", - "pretty-format": "^25.1.0" + "kcd-scripts": "^14.0.0", + "pretty-format": "^25.1.0", + "vitest": "^0.34.1", + "typescript": "^5.1.6" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2020 + }, "rules": { - "@babel/no-invalid-this": "off" + "no-invalid-this": "off" }, "overrides": [ { @@ -61,6 +92,18 @@ "rules": { "max-lines-per-function": "off" } + }, + { + "files": [ + "**/*.d.ts" + ], + "rules": { + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-invalid-void-type": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/triple-slash-reference": "off" + } } ] }, diff --git a/src/extend-expect.js b/src/extend-expect.js deleted file mode 100644 index 3801a1d5..00000000 --- a/src/extend-expect.js +++ /dev/null @@ -1,3 +0,0 @@ -import * as extensions from './matchers' - -expect.extend(extensions) diff --git a/src/index.js b/src/index.js index 8cecbe35..3801a1d5 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,3 @@ -import './extend-expect' +import * as extensions from './matchers' + +expect.extend(extensions) diff --git a/src/to-be-in-the-document.js b/src/to-be-in-the-document.js index 8ccc451a..a7eda78e 100644 --- a/src/to-be-in-the-document.js +++ b/src/to-be-in-the-document.js @@ -29,7 +29,7 @@ export function toBeInTheDocument(element) { '', ), '', - // eslint-disable-next-line @babel/new-cap + // eslint-disable-next-line new-cap this.utils.RECEIVED_COLOR(this.isNot ? errorFound() : errorNotFound()), ].join('\n') }, diff --git a/src/to-contain-element.js b/src/to-contain-element.js index c94ddbf9..445a6120 100644 --- a/src/to-contain-element.js +++ b/src/to-contain-element.js @@ -17,7 +17,7 @@ export function toContainElement(container, element) { 'element', ), '', - // eslint-disable-next-line @babel/new-cap + // eslint-disable-next-line new-cap this.utils.RECEIVED_COLOR(`${this.utils.stringify( container.cloneNode(false), )} ${ diff --git a/src/to-contain-html.js b/src/to-contain-html.js index ccbff5f5..30158ee1 100644 --- a/src/to-contain-html.js +++ b/src/to-contain-html.js @@ -23,7 +23,7 @@ export function toContainHTML(container, htmlText) { '', ), 'Expected:', - // eslint-disable-next-line @babel/new-cap + // eslint-disable-next-line new-cap ` ${this.utils.EXPECTED_COLOR(htmlText)}`, 'Received:', ` ${this.utils.printReceived(container.cloneNode(true))}`, diff --git a/src/utils.js b/src/utils.js index cdbb1088..2b45be02 100644 --- a/src/utils.js +++ b/src/utils.js @@ -28,7 +28,7 @@ class GenericTypeError extends Error { '', ), '', - // eslint-disable-next-line @babel/new-cap + // eslint-disable-next-line new-cap `${context.utils.RECEIVED_COLOR( 'received', )} value must ${expectedString}.`, @@ -91,9 +91,9 @@ class InvalidCSSError extends Error { this.message = [ received.message, '', - // eslint-disable-next-line @babel/new-cap + // eslint-disable-next-line new-cap context.utils.RECEIVED_COLOR(`Failing css:`), - // eslint-disable-next-line @babel/new-cap + // eslint-disable-next-line new-cap context.utils.RECEIVED_COLOR(`${received.css}`), ].join('\n') } @@ -137,11 +137,11 @@ function getMessage( ) { return [ `${matcher}\n`, - // eslint-disable-next-line @babel/new-cap + // eslint-disable-next-line new-cap `${expectedLabel}:\n${context.utils.EXPECTED_COLOR( redent(display(context, expectedValue), 2), )}`, - // eslint-disable-next-line @babel/new-cap + // eslint-disable-next-line new-cap `${receivedLabel}:\n${context.utils.RECEIVED_COLOR( redent(display(context, receivedValue), 2), )}`, diff --git a/tests/setup-env.js b/tests/setup-env.js index a9325d25..151f6e7b 100644 --- a/tests/setup-env.js +++ b/tests/setup-env.js @@ -1,4 +1,4 @@ import {plugins} from 'pretty-format' -import '../src/extend-expect' +import '../src/index' expect.addSnapshotSerializer(plugins.ConvertAnsi) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..9a426aba --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "skipLibCheck": true + }, + "include": ["*.d.ts", "types"] +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 00000000..cebdd1af --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1 @@ +/// diff --git a/types/jest-globals.d.ts b/types/jest-globals.d.ts new file mode 100644 index 00000000..f7de8014 --- /dev/null +++ b/types/jest-globals.d.ts @@ -0,0 +1,8 @@ +import {type expect} from '@jest/globals' +import {type TestingLibraryMatchers} from './matchers' + +export {} +declare module '@jest/expect' { + export interface Matchers> + extends TestingLibraryMatchers {} +} diff --git a/types/jest.d.ts b/types/jest.d.ts new file mode 100644 index 00000000..bca4339c --- /dev/null +++ b/types/jest.d.ts @@ -0,0 +1,10 @@ +/// + +import {type TestingLibraryMatchers} from './matchers' + +declare global { + namespace jest { + interface Matchers + extends TestingLibraryMatchers {} + } +} diff --git a/types/matchers.d.ts b/types/matchers.d.ts new file mode 100755 index 00000000..af43570d --- /dev/null +++ b/types/matchers.d.ts @@ -0,0 +1,666 @@ +declare namespace matchers { + interface TestingLibraryMatchers extends Record { + /** + * @deprecated + * since v1.9.0 + * @description + * Assert whether a value is a DOM element, or not. Contrary to what its name implies, this matcher only checks + * that you passed to it a valid DOM element. + * + * It does not have a clear definition of what "the DOM" is. Therefore, it does not check whether that element + * is contained anywhere. + * @see + * [testing-library/jest-dom#toBeInTheDom](https://github.com/testing-library/jest-dom#toBeInTheDom) + */ + toBeInTheDOM(container?: HTMLElement | SVGElement): R + /** + * @description + * Assert whether an element is present in the document or not. + * @example + * + * + * expect(queryByTestId('svg-element')).toBeInTheDocument() + * expect(queryByTestId('does-not-exist')).not.toBeInTheDocument() + * @see + * [testing-library/jest-dom#tobeinthedocument](https://github.com/testing-library/jest-dom#tobeinthedocument) + */ + toBeInTheDocument(): R + /** + * @description + * This allows you to check if an element is currently visible to the user. + * + * An element is visible if **all** the following conditions are met: + * * it does not have its css property display set to none + * * it does not have its css property visibility set to either hidden or collapse + * * it does not have its css property opacity set to 0 + * * its parent element is also visible (and so on up to the top of the DOM tree) + * * it does not have the hidden attribute + * * if `
` it has the open attribute + * @example + *
+ * Zero Opacity + *
+ * + *
Visible Example
+ * + * expect(getByTestId('zero-opacity')).not.toBeVisible() + * expect(getByTestId('visible')).toBeVisible() + * @see + * [testing-library/jest-dom#tobevisible](https://github.com/testing-library/jest-dom#tobevisible) + */ + toBeVisible(): R + /** + * @deprecated + * since v5.9.0 + * @description + * Assert whether an element has content or not. + * @example + * + * + * + * + * expect(getByTestId('empty')).toBeEmpty() + * expect(getByTestId('not-empty')).not.toBeEmpty() + * @see + * [testing-library/jest-dom#tobeempty](https://github.com/testing-library/jest-dom#tobeempty) + */ + toBeEmpty(): R + /** + * @description + * Assert whether an element has content or not. + * @example + * + * + * + * + * expect(getByTestId('empty')).toBeEmptyDOMElement() + * expect(getByTestId('not-empty')).not.toBeEmptyDOMElement() + * @see + * [testing-library/jest-dom#tobeemptydomelement](https://github.com/testing-library/jest-dom#tobeemptydomelement) + */ + toBeEmptyDOMElement(): R + /** + * @description + * Allows you to check whether an element is disabled from the user's perspective. + * + * Matches if the element is a form control and the `disabled` attribute is specified on this element or the + * element is a descendant of a form element with a `disabled` attribute. + * @example + * + * + * expect(getByTestId('button')).toBeDisabled() + * @see + * [testing-library/jest-dom#tobedisabled](https://github.com/testing-library/jest-dom#tobedisabled) + */ + toBeDisabled(): R + /** + * @description + * Allows you to check whether an element is not disabled from the user's perspective. + * + * Works like `not.toBeDisabled()`. + * + * Use this matcher to avoid double negation in your tests. + * @example + * + * + * expect(getByTestId('button')).toBeEnabled() + * @see + * [testing-library/jest-dom#tobeenabled](https://github.com/testing-library/jest-dom#tobeenabled) + */ + toBeEnabled(): R + /** + * @description + * Check if a form element, or the entire `form`, is currently invalid. + * + * An `input`, `select`, `textarea`, or `form` element is invalid if it has an `aria-invalid` attribute with no + * value or a value of "true", or if the result of `checkValidity()` is false. + * @example + * + * + *
+ * + *
+ * + * expect(getByTestId('no-aria-invalid')).not.toBeInvalid() + * expect(getByTestId('invalid-form')).toBeInvalid() + * @see + * [testing-library/jest-dom#tobeinvalid](https://github.com/testing-library/jest-dom#tobeinvalid) + */ + toBeInvalid(): R + /** + * @description + * This allows you to check if a form element is currently required. + * + * An element is required if it is having a `required` or `aria-required="true"` attribute. + * @example + * + *
+ * + * expect(getByTestId('required-input')).toBeRequired() + * expect(getByTestId('supported-role')).not.toBeRequired() + * @see + * [testing-library/jest-dom#toberequired](https://github.com/testing-library/jest-dom#toberequired) + */ + toBeRequired(): R + /** + * @description + * Allows you to check if a form element is currently required. + * + * An `input`, `select`, `textarea`, or `form` element is invalid if it has an `aria-invalid` attribute with no + * value or a value of "false", or if the result of `checkValidity()` is true. + * @example + * + * + *
+ * + *
+ * + * expect(getByTestId('no-aria-invalid')).not.toBeValid() + * expect(getByTestId('invalid-form')).toBeInvalid() + * @see + * [testing-library/jest-dom#tobevalid](https://github.com/testing-library/jest-dom#tobevalid) + */ + toBeValid(): R + /** + * @description + * Allows you to assert whether an element contains another element as a descendant or not. + * @example + * + * + * + * + * const ancestor = getByTestId('ancestor') + * const descendant = getByTestId('descendant') + * const nonExistantElement = getByTestId('does-not-exist') + * expect(ancestor).toContainElement(descendant) + * expect(descendant).not.toContainElement(ancestor) + * expect(ancestor).not.toContainElement(nonExistantElement) + * @see + * [testing-library/jest-dom#tocontainelement](https://github.com/testing-library/jest-dom#tocontainelement) + */ + toContainElement(element: HTMLElement | SVGElement | null): R + /** + * @description + * Assert whether a string representing a HTML element is contained in another element. + * @example + * + * + * expect(getByTestId('parent')).toContainHTML('') + * @see + * [testing-library/jest-dom#tocontainhtml](https://github.com/testing-library/jest-dom#tocontainhtml) + */ + toContainHTML(htmlText: string): R + /** + * @description + * Allows you to check if a given element has an attribute or not. + * + * You can also optionally check that the attribute has a specific expected value or partial match using + * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) or + * [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp). + * @example + * + * + * expect(button).toHaveAttribute('disabled') + * expect(button).toHaveAttribute('type', 'submit') + * expect(button).not.toHaveAttribute('type', 'button') + * @see + * [testing-library/jest-dom#tohaveattribute](https://github.com/testing-library/jest-dom#tohaveattribute) + */ + toHaveAttribute(attr: string, value?: unknown): R + /** + * @description + * Check whether the given element has certain classes within its `class` attribute. + * + * You must provide at least one class, unless you are asserting that an element does not have any classes. + * @example + * + * + *
no classes
+ * + * const deleteButton = getByTestId('delete-button') + * const noClasses = getByTestId('no-classes') + * expect(deleteButton).toHaveClass('btn') + * expect(deleteButton).toHaveClass('btn-danger xs') + * expect(deleteButton).toHaveClass('btn xs btn-danger', {exact: true}) + * expect(deleteButton).not.toHaveClass('btn xs btn-danger', {exact: true}) + * expect(noClasses).not.toHaveClass() + * @see + * [testing-library/jest-dom#tohaveclass](https://github.com/testing-library/jest-dom#tohaveclass) + */ + toHaveClass(...classNames: string[]): R + toHaveClass(classNames: string, options?: {exact: boolean}): R + /** + * @description + * This allows you to check whether the given form element has the specified displayed value (the one the + * end user will see). It accepts , + * + * + * + * + * + * + * + * const input = screen.getByLabelText('First name') + * const textarea = screen.getByLabelText('Description') + * const selectSingle = screen.getByLabelText('Fruit') + * const selectMultiple = screen.getByLabelText('Fruits') + * + * expect(input).toHaveDisplayValue('Luca') + * expect(textarea).toHaveDisplayValue('An example description here.') + * expect(selectSingle).toHaveDisplayValue('Select a fruit...') + * expect(selectMultiple).toHaveDisplayValue(['Banana', 'Avocado']) + * + * @see + * [testing-library/jest-dom#tohavedisplayvalue](https://github.com/testing-library/jest-dom#tohavedisplayvalue) + */ + toHaveDisplayValue(value: string | RegExp | Array): R + /** + * @description + * Assert whether an element has focus or not. + * @example + *
+ * + *
+ * + * const input = getByTestId('element-to-focus') + * input.focus() + * expect(input).toHaveFocus() + * input.blur() + * expect(input).not.toHaveFocus() + * @see + * [testing-library/jest-dom#tohavefocus](https://github.com/testing-library/jest-dom#tohavefocus) + */ + toHaveFocus(): R + /** + * @description + * Check if a form or fieldset contains form controls for each given name, and having the specified value. + * + * Can only be invoked on a form or fieldset element. + * @example + *
+ * + * + * + * + *
+ * + * expect(getByTestId('login-form')).toHaveFormValues({ + * username: 'jane.doe', + * rememberMe: true, + * }) + * @see + * [testing-library/jest-dom#tohaveformvalues](https://github.com/testing-library/jest-dom#tohaveformvalues) + */ + toHaveFormValues(expectedValues: Record): R + /** + * @description + * Check if an element has specific css properties with specific values applied. + * + * Only matches if the element has *all* the expected properties applied, not just some of them. + * @example + * + * + * const button = getByTestId('submit-button') + * expect(button).toHaveStyle('background-color: green') + * expect(button).toHaveStyle({ + * 'background-color': 'green', + * display: 'none' + * }) + * @see + * [testing-library/jest-dom#tohavestyle](https://github.com/testing-library/jest-dom#tohavestyle) + */ + toHaveStyle(css: string | Record): R + /** + * @description + * Check whether the given element has a text content or not. + * + * When a string argument is passed through, it will perform a partial case-sensitive match to the element + * content. + * + * To perform a case-insensitive match, you can use a RegExp with the `/i` modifier. + * + * If you want to match the whole content, you can use a RegExp to do it. + * @example + * Text Content + * + * const element = getByTestId('text-content') + * expect(element).toHaveTextContent('Content') + * // to match the whole content + * expect(element).toHaveTextContent(/^Text Content$/) + * // to use case-insentive match + * expect(element).toHaveTextContent(/content$/i) + * expect(element).not.toHaveTextContent('content') + * @see + * [testing-library/jest-dom#tohavetextcontent](https://github.com/testing-library/jest-dom#tohavetextcontent) + */ + toHaveTextContent( + text: string | RegExp, + options?: {normalizeWhitespace: boolean}, + ): R + /** + * @description + * Check whether the given form element has the specified value. + * + * Accepts ``, `