From 6b9a7207adee15f9f94cfbd23903f9f54fc178ec Mon Sep 17 00:00:00 2001
From: Abdelrahman Ashraf
Artem Malko
π»
Gerrit Alex
π»
+
Karthick Raja
π»
From 332a6d5972733975980d5e0b3e9583927aadf4d1 Mon Sep 17 00:00:00 2001
From: Lidor Avitan <35113398+lidoravitan@users.noreply.github.com>
Date: Mon, 26 Oct 2020 20:59:24 +0200
Subject: [PATCH 003/126] docs: update complex example (#806)
Co-authored-by: Lidor Avitan
Abdelrahman Ashraf
π»
Gerrit Alex
π»
Karthick Raja
π»
+
Abdelrahman Ashraf
π»
From c2806fcc88d488663e0202a45e9c5ff528fb39fc Mon Sep 17 00:00:00 2001
From: Sebastian Silbermann
Lidor Avitan
π
Karthick Raja
π»
Abdelrahman Ashraf
π»
+
Lidor Avitan
π
-
This project follows the [all-contributors][all-contributors] specification.
From e07e6416f25fd75df510c5d4211dc06f9f7398d7 Mon Sep 17 00:00:00 2001
From: Weyert de Boer
Jordan Harband
π π€ = {
| DocumentFragment
| Array
Lidor Avitan
π
+
Jordan Harband
π π€
+
From 3af1b4ba12dfc3a5c8f4968c1b92dba53823f9af Mon Sep 17 00:00:00 2001
From: Nick McCurdy
+
Marco Moretti
π»
= { - container: HTMLElement - baseElement: HTMLElement + container: Element + baseElement: Element debug: ( baseElement?: - | HTMLElement + | Element | DocumentFragment - | Array, + | Array , maxLength?: number, options?: prettyFormat.OptionsReceived, ) => void @@ -27,8 +27,8 @@ export type RenderResult = { } & {[P in keyof Q]: BoundFunction} export interface RenderOptions{ - container?: HTMLElement - baseElement?: HTMLElement + container?: Element + baseElement?: Element hydrate?: boolean queries?: Q wrapper?: React.ComponentType From 276673ff001858fc5844df35114bf32e48bcdc1a Mon Sep 17 00:00:00 2001 From: Nick McCurdyDate: Thu, 19 Nov 2020 09:45:06 -0500 Subject: [PATCH 017/126] chore(types): add test for rendering SVG elements (#834) --- types/test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/types/test.tsx b/types/test.tsx index 3971037d..07c4dbb7 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -44,6 +44,15 @@ export function testRenderOptions() { render(, options) } +export function testSVGRenderOptions() { + const container = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'svg', + ) + const options = {container} + render(, options) +} + export function testFireEvent() { const {container} = render() fireEvent.click(container) From 7fcb0f2e930a34bc1a0fa3dc964cd05a74fdfefd Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Fri, 20 Nov 2020 15:31:49 -0800 Subject: [PATCH 018/126] fix(dependencies): bump dom testing library (#836) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4792c6cc..79726397 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^7.27.1" + "@testing-library/dom": "^7.28.1" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", From 03bea9fd76349f458b6aff7b472acefe1879375f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Wed, 2 Dec 2020 20:08:37 +0100 Subject: [PATCH 019/126] chore: Add 'Cancel Previous Runs' step (#840) Co-authored-by: Kent C. Dodds --- .github/workflows/validate.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 02277648..e99fa965 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -21,6 +21,11 @@ jobs: react: [latest, next, experimental] runs-on: ubuntu-latest steps: + - name: π Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ secrets.GITHUB_TOKEN }} + - name: β¬οΈ Checkout repo uses: actions/checkout@v2 @@ -53,6 +58,11 @@ jobs: contains('refs/heads/master,refs/heads/beta,refs/heads/next,refs/heads/alpha', github.ref) && github.event_name == 'push' }} steps: + - name: π Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ secrets.GITHUB_TOKEN }} + - name: β¬οΈ Checkout repo uses: actions/checkout@v2 From 64a8b9c2f30922d362028d9c6a31543c1874d1c7 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Fri, 4 Dec 2020 11:34:48 -0700 Subject: [PATCH 020/126] chore: update all deps (#842) --- package.json | 9 ++++----- src/__tests__/rerender.js | 4 ++-- types/test.tsx | 4 ++-- types/tsconfig.json | 20 ++------------------ types/tslint.json | 9 --------- 5 files changed, 10 insertions(+), 36 deletions(-) delete mode 100644 types/tslint.json diff --git a/package.json b/package.json index 79726397..d1d6aa36 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "setup": "npm install && npm run validate -s", "test": "kcd-scripts test", "test:update": "npm test -- --updateSnapshot --coverage", - "typecheck": "dtslint ./types/", + "typecheck": "kcd-scripts typecheck --build types", "validate": "kcd-scripts validate" }, "files": [ @@ -48,15 +48,14 @@ }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", - "@types/react-dom": "^16.9.9", + "@types/react-dom": "^17.0.0", "dotenv-cli": "^4.0.0", - "dtslint": "4.0.5", - "kcd-scripts": "^7.0.3", + "kcd-scripts": "^7.5.1", "npm-run-all": "^4.1.5", "react": "^17.0.1", "react-dom": "^17.0.1", "rimraf": "^3.0.2", - "typescript": "^4.0.5" + "typescript": "^4.1.2" }, "peerDependencies": { "react": "*", diff --git a/src/__tests__/rerender.js b/src/__tests__/rerender.js index f8ea377e..be3c259c 100644 --- a/src/__tests__/rerender.js +++ b/src/__tests__/rerender.js @@ -23,9 +23,9 @@ test('hydrate will not update props until next render', () => { hydrate: true, }) - expect(initialInputElement.value).toBe(firstValue) + expect(initialInputElement).toHaveValue(firstValue) const secondValue = 'goodbye' rerender( null} />) - expect(initialInputElement.value).toBe(secondValue) + expect(initialInputElement).toHaveValue(secondValue) }) diff --git a/types/test.tsx b/types/test.tsx index 07c4dbb7..1366caac 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import {render, fireEvent, screen, waitFor} from '@testing-library/react' -import * as pure from '@testing-library/react/pure' +import {render, fireEvent, screen, waitFor} from '.' +import * as pure from './pure' export async function testRender() { const page = render() diff --git a/types/tsconfig.json b/types/tsconfig.json index aeacacb0..a7829065 100644 --- a/types/tsconfig.json +++ b/types/tsconfig.json @@ -1,20 +1,4 @@ -// this additional tsconfig is required by dtslint -// see: https://github.com/Microsoft/dtslint#typestsconfigjson { - "compilerOptions": { - "module": "commonjs", - "lib": ["es6", "dom"], - "jsx": "react", - "noImplicitAny": true, - "noImplicitThis": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "noEmit": true, - "baseUrl": ".", - "skipLibCheck": true, - "paths": { - "@testing-library/react": ["."], - "@testing-library/react/pure": ["."] - } - } + "extends": "../node_modules/kcd-scripts/shared-tsconfig.json", + "include": ["."] } diff --git a/types/tslint.json b/types/tslint.json deleted file mode 100644 index cb0fce9f..00000000 --- a/types/tslint.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": ["dtslint/dtslint.json"], - "rules": { - "no-useless-files": false, - "no-relative-import-in-test": false, - "semicolon": false, - "whitespace": false - } -} From 1389f094748991b3a900e2fcc9f4bf21df212da9 Mon Sep 17 00:00:00 2001 From: sanchit121 <30828115+sanchit121@users.noreply.github.com> Date: Thu, 7 Jan 2021 23:29:45 +0530 Subject: [PATCH 021/126] fix: Return type of unmount is `void` (#857) Co-authored-by: Sanchit Agarwal --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 3dcaf10c..8e0d1c02 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -22,7 +22,7 @@ export type RenderResult = { options?: prettyFormat.OptionsReceived, ) => void rerender: (ui: React.ReactElement) => void - unmount: () => boolean + unmount: () => void asFragment: () => DocumentFragment } & {[P in keyof Q]: BoundFunction} From deafd51bc20209612674dbdaba920750c1d6352e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Jan 2021 19:00:46 +0100 Subject: [PATCH 022/126] docs: add sanchit121 as a contributor (#858) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 10 ++ README.md | 257 ++++++++++++++++++++++---------------------- 2 files changed, 139 insertions(+), 128 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8c514263..ed301861 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1222,6 +1222,16 @@ "contributions": [ "code" ] + }, + { + "login": "sanchit121", + "name": "sanchit121", + "avatar_url": "https://avatars2.githubusercontent.com/u/30828115?v=4", + "profile": "https://github.com/sanchit121", + "contributions": [ + "bug", + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 73e421bb..06a1fbdd 100644 --- a/README.md +++ b/README.md @@ -441,173 +441,174 @@ Thanks goes to these people ([emoji key][emojis]):Date: Tue, 19 Jan 2021 19:51:19 -0500 Subject: [PATCH 023/126] docs: use vanity URL for Discord --- .github/ISSUE_TEMPLATE.md | 3 ++- .github/ISSUE_TEMPLATE/Question.md | 2 +- README.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 01d98992..496c8563 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -23,7 +23,7 @@ Thanks for your interest in the project. We appreciate bugs filed and PRs submit instead of filing an issue on GitHub. You can follow the instructions in this codesandbox to make a reproduction of your issue: https://kcd.im/rtl-help * Discord - https://discord.gg/c6JN9fM + https://discord.gg/testing-library * Stack Overflow https://stackoverflow.com/questions/tagged/react-testing-library @@ -61,6 +61,7 @@ for the supported version. Relevant code or config ```javascript + ``` What you did: diff --git a/.github/ISSUE_TEMPLATE/Question.md b/.github/ISSUE_TEMPLATE/Question.md index d9a372d9..e625486b 100644 --- a/.github/ISSUE_TEMPLATE/Question.md +++ b/.github/ISSUE_TEMPLATE/Question.md @@ -16,7 +16,7 @@ For questions related to using the library, please visit a support community instead of filing an issue on GitHub. You can follow the instructions in this codesandbox to make a reproduction of your issue: https://kcd.im/rtl-help -- Discord https://discord.gg/c6JN9fM +- Discord https://discord.gg/testing-library - Stack Overflow https://stackoverflow.com/questions/tagged/react-testing-library - Documentation: https://github.com/testing-library/testing-library-docs diff --git a/README.md b/README.md index 06a1fbdd..9d9efb5d 100644 --- a/README.md +++ b/README.md @@ -653,7 +653,7 @@ Contributions of any kind welcome! [requests]: https://github.com/testing-library/react-testing-library/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3Aenhancement+is%3Aopen [good-first-issue]: https://github.com/testing-library/react-testing-library/issues?utf8=β&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3A"good+first+issue"+ [discord-badge]: https://img.shields.io/discord/723559267868737556.svg?color=7389D8&labelColor=6A7EC2&logo=discord&logoColor=ffffff&style=flat-square -[discord]: https://discord.gg/c6JN9fM +[discord]: https://discord.gg/testing-library [stackoverflow]: https://stackoverflow.com/questions/tagged/react-testing-library [react-hooks-testing-library]: https://github.com/testing-library/react-hooks-testing-library From 81c2de90c621df4f0702db02adf0c1f31bc08cd6 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 2 Feb 2021 10:30:49 +0100 Subject: [PATCH 024/126] fix(render): Default to HTMLElement in returned container (#868) --- types/index.d.ts | 26 ++++++++++++++++++-------- types/test.tsx | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 8e0d1c02..4d599256 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -6,12 +6,16 @@ import { BoundFunction, prettyFormat, } from '@testing-library/dom' +import {Renderer} from 'react-dom' import {act as reactAct} from 'react-dom/test-utils' export * from '@testing-library/dom' -export type RenderResult = { - container: Element +export type RenderResult< + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement +> = { + container: Container baseElement: Element debug: ( baseElement?: @@ -26,8 +30,11 @@ export type RenderResult= { asFragment: () => DocumentFragment } & {[P in keyof Q]: BoundFunction} -export interface RenderOptions{ - container?: Element +export interface RenderOptions< + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement +> { + container?: Container baseElement?: Element hydrate?: boolean queries?: Q @@ -39,14 +46,17 @@ type Omit= Pick > /** * Render into a container which is appended to document.body. It should be used with cleanup. */ +export function render< + Q extends Queries, + Container extends Element | DocumentFragment = HTMLElement +>( + ui: React.ReactElement, + options: RenderOptions , +): RenderResultexport function render( ui: React.ReactElement, options?: Omit, ): RenderResult -export function render ( - ui: React.ReactElement, - options: RenderOptions, -): RenderResult/** * Unmounts React trees that were mounted with render. diff --git a/types/test.tsx b/types/test.tsx index 1366caac..7cc0e015 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -3,7 +3,7 @@ import {render, fireEvent, screen, waitFor} from '.' import * as pure from './pure' export async function testRender() { - const page = render() + const page = render() // single queries page.getByText('foo') @@ -17,11 +17,12 @@ export async function testRender() { // helpers const {container, rerender, debug} = page + expectType(container) return {container, rerender, debug} } export async function testPureRender() { - const page = pure.render() + const page = pure.render() // single queries page.getByText('foo') @@ -35,13 +36,15 @@ export async function testPureRender() { // helpers const {container, rerender, debug} = page + expectType (container) return {container, rerender, debug} } export function testRenderOptions() { const container = document.createElement('div') const options = {container} - render(, options) + const {container: returnedContainer} = render(, options) + expectType (returnedContainer) } export function testSVGRenderOptions() { @@ -50,7 +53,8 @@ export function testSVGRenderOptions() { 'svg', ) const options = {container} - render(, options) + const {container: returnedContainer} = render( , options) + expectType (returnedContainer) } export function testFireEvent() { @@ -87,3 +91,27 @@ eslint testing-library/no-debug: "off", testing-library/prefer-screen-queries: "off" */ + +// https://stackoverflow.com/questions/53807517/how-to-test-if-two-types-are-exactly-the-same +type IfEquals = ( () => G extends T + ? 1 + : 2) extends () => G extends U ? 1 : 2 + ? Yes + : No + +/** + * Issues a type error if `Expected` is not identical to `Actual`. + * + * `Expected` should be declared when invoking `expectType`. + * `Actual` should almost always we be a `typeof value` statement. + * + * Source: https://github.com/mui-org/material-ui/blob/6221876a4b468a3330ffaafa8472de7613933b87/packages/material-ui-types/index.d.ts#L73-L84 + * + * @example `expectType (value)` + * TypeScript issues a type error since `value is not assignable to never`. + * This means `typeof value` is not identical to `number | string` + * @param actual + */ +declare function expectType ( + actual: IfEquals , +): void From 5576e6f71445fd08cb10a58b571976eb410699ec Mon Sep 17 00:00:00 2001 From: Solufa Date: Wed, 3 Feb 2021 01:33:01 +0900 Subject: [PATCH 025/126] fix(render): add default type for Queries parameter (#871) --- types/index.d.ts | 2 +- types/test.tsx | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 4d599256..6d0c67ee 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -47,7 +47,7 @@ type Omit = Pick > * Render into a container which is appended to document.body. It should be used with cleanup. */ export function render< - Q extends Queries, + Q extends Queries = typeof queries, Container extends Element | DocumentFragment = HTMLElement >( ui: React.ReactElement, diff --git a/types/test.tsx b/types/test.tsx index 7cc0e015..01105c06 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -84,6 +84,22 @@ export async function testWaitFor() { await waitFor(() => {}) } +export function testQueries() { + const {getByLabelText} = render( + , + ) + expectType >( + getByLabelText('Username'), + ) + + const container = document.createElement('div') + const options = {container} + const {getByText} = render( Hello World, options) + expectType>( + getByText('Hello World'), + ) +} + /* eslint testing-library/prefer-explicit-assert: "off", From cbe020ada151a92aa1be46a695354a33045aafac Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 2 Feb 2021 17:36:20 +0100 Subject: [PATCH 026/126] docs: add solufa as a contributor (#872) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 10 ++++++++++ README.md | 1 + 2 files changed, 11 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index ed301861..3ef0ecdf 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1232,6 +1232,16 @@ "bug", "code" ] + }, + { + "login": "solufa", + "name": "Solufa", + "avatar_url": "https://avatars.githubusercontent.com/u/9402912?v=4", + "profile": "https://github.com/solufa", + "contributions": [ + "bug", + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 9d9efb5d..c46eac6d 100644 --- a/README.md +++ b/README.md @@ -605,6 +605,7 @@ Thanks goes to these people ([emoji key][emojis]): From 0d56ea8f4527e943fbc1ec747cea6f565f079748 Mon Sep 17 00:00:00 2001 From: Solufa
Marco Moretti
π»+
sanchit121
π π»
Solufa
π π»Date: Wed, 3 Feb 2021 18:42:51 +0900 Subject: [PATCH 027/126] chore(types): fix type parameter of Queries test (#873) --- types/test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/test.tsx b/types/test.tsx index 01105c06..5f9575ac 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -88,7 +88,7 @@ export function testQueries() { const {getByLabelText} = render( , ) - expectType >( + expectType >( getByLabelText('Username'), ) From 95c2fa58520d633da825a4792c532b43c7f62355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Fri, 19 Feb 2021 21:24:30 +0200 Subject: [PATCH 028/126] test: fix typo (#878) --- src/__tests__/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/events.js b/src/__tests__/events.js index 8a6899af..587bfdae 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -228,7 +228,7 @@ test('calling `fireEvent` directly works too', () => { ) }) -test('blur/foucs bubbles in react', () => { +test('blur/focus bubbles in react', () => { const handleBlur = jest.fn() const handleBubbledBlur = jest.fn() const handleFocus = jest.fn() From a241cb8a417135a529acb5504c83c0889080b8c2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 20 Feb 2021 01:53:24 -0500 Subject: [PATCH 029/126] docs: add AriPerkkio as a contributor (#879) * docs: update README.md * docs: update .all-contributorsrc Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3ef0ecdf..193b0711 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1242,6 +1242,15 @@ "bug", "code" ] + }, + { + "login": "AriPerkkio", + "name": "Ari PerkkiΓΆ", + "avatar_url": "https://avatars.githubusercontent.com/u/14806298?v=4", + "profile": "https://codepen.io/ariperkkio/", + "contributions": [ + "test" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index c46eac6d..50564c4f 100644 --- a/README.md +++ b/README.md @@ -606,6 +606,7 @@ Thanks goes to these people ([emoji key][emojis]):
Marco Moretti
π»
sanchit121
π π»+
Solufa
π π»From 58150b9efdd5515ad88e2d3373d926ee89093278 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout
Ari PerkkiΓΆ
β οΈDate: Tue, 30 Mar 2021 13:19:08 +0300 Subject: [PATCH 030/126] fix(types): exclude tsconfig from types folder in publish flow (#893) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d1d6aa36..fde78a35 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dont-cleanup-after-each.js", "pure.js", "pure.d.ts", - "types" + "types/*.d.ts" ], "keywords": [ "testing", From 57b3287c00eb0afe15baa9eb7c5a3808049d390c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Wed, 21 Apr 2021 17:31:06 +0200 Subject: [PATCH 031/126] chore: add tests for Node 16 (#900) --- .github/workflows/validate.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e99fa965..222d4f15 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,20 +17,18 @@ jobs: if: ${{ !contains(github.head_ref, 'all-contributors') }} strategy: matrix: - node: [10.13, 12, 14, 15] + node: [10.13, 12, 14, 16] react: [latest, next, experimental] runs-on: ubuntu-latest steps: - name: π Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.6.0 - with: - access_token: ${{ secrets.GITHUB_TOKEN }} + uses: styfle/cancel-workflow-action@0.9.0 - name: β¬οΈ Checkout repo uses: actions/checkout@v2 - name: β Setup node - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} @@ -59,15 +57,13 @@ jobs: github.ref) && github.event_name == 'push' }} steps: - name: π Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.6.0 - with: - access_token: ${{ secrets.GITHUB_TOKEN }} + uses: styfle/cancel-workflow-action@0.9.0 - name: β¬οΈ Checkout repo uses: actions/checkout@v2 - name: β Setup node - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: node-version: 14 From 657e106cca5b84763098185b4d4c0d4e0d92c1b4 Mon Sep 17 00:00:00 2001 From: Nick McCurdy Date: Fri, 23 Apr 2021 09:09:27 -0400 Subject: [PATCH 032/126] chore: continue testing on Node 15 (#902) --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 222d4f15..96254996 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,7 +17,7 @@ jobs: if: ${{ !contains(github.head_ref, 'all-contributors') }} strategy: matrix: - node: [10.13, 12, 14, 16] + node: [10.13, 12, 14, 15, 16] react: [latest, next, experimental] runs-on: ubuntu-latest steps: From 7c84fa39602120e0b2d23a8898af9f3585f39c31 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 23 Apr 2021 15:19:06 +0200 Subject: [PATCH 033/126] docs: add nickmccurdy as a contributor (#903) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 193b0711..ad6a5484 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -876,7 +876,8 @@ "profile": "https://nickmccurdy.com/", "contributions": [ "review", - "question" + "question", + "infra" ] }, { diff --git a/README.md b/README.md index 50564c4f..e0295221 100644 --- a/README.md +++ b/README.md @@ -554,7 +554,7 @@ Thanks goes to these people ([emoji key][emojis]):
James George
π
JoΓ£o Fernandes
π-
Alejandro Perea
π+
Nick McCurdy
π π¬
Nick McCurdy
π π¬ π
Sebastian Silbermann
πFrom fe163763bba28bbd8ec08f94e36db40efbdd2ae8 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout From 830da02407429b4a4a72f8eed7455adca33a39d8 Mon Sep 17 00:00:00 2001 From: "Angus J. Pope" <44686792+anpaopao@users.noreply.github.com> Date: Mon, 24 May 2021 19:40:27 +1000 Subject: [PATCH 037/126] docs: Fix link to egghead open source contribution how to (#916) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5876fb3..68e77e6f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,6 @@ Also, please watch the repo and respond to questions/bug reports/feature requests! Thanks! [egghead]: - https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github + https://app.egghead.io/series/how-to-contribute-to-an-open-source-project-on-github [all-contributors]: https://github.com/all-contributors/all-contributors [issues]: https://github.com/testing-library/react-testing-library/issues From 875ee5b5c255dcd8af42197ca5489a39b9334b3d Mon Sep 17 00:00:00 2001 From: Sebastian SilbermannDate: Sun, 25 Apr 2021 12:51:27 +0300 Subject: [PATCH 034/126] chore: rename master to main in all references (#901) Co-authored-by: Sebastian Silbermann --- .github/workflows/validate.yml | 6 +++--- CONTRIBUTING.md | 14 +++++++------- README.md | 14 +++++++------- other/MAINTAINING.md | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 96254996..dc793472 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -3,7 +3,7 @@ on: push: branches: - '+([0-9])?(.{+([0-9]),x}).x' - - 'master' + - 'main' - 'next' - 'next-major' - 'beta' @@ -53,7 +53,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.repository == 'testing-library/react-testing-library' && - contains('refs/heads/master,refs/heads/beta,refs/heads/next,refs/heads/alpha', + contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', github.ref) && github.event_name == 'push' }} steps: - name: π Cancel Previous Runs @@ -82,7 +82,7 @@ jobs: branches: | [ '+([0-9])?(.{+([0-9]),x}).x', - 'master', + 'main', 'next', 'next-major', {name: 'beta', prerelease: true}, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3867b62..c5876fb3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,20 +11,20 @@ series [How to Contribute to an Open Source Project on GitHub][egghead] 2. Run `npm run setup -s` to install dependencies and run validation 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` -> Tip: Keep your `master` branch pointing at the original repository and make -> pull requests from branches on your fork. To do this, run: +> Tip: Keep your `main` branch pointing at the original repository and make pull +> requests from branches on your fork. To do this, run: > > ``` > git remote add upstream https://github.com/testing-library/react-testing-library.git > git fetch upstream -> git branch --set-upstream-to=upstream/master master +> git branch --set-upstream-to=upstream/main main > ``` > > This will add the original repository as a "remote" called "upstream," Then -> fetch the git information from that remote, then set your local `master` -> branch to use the upstream master branch whenever you run `git pull`. Then you -> can make all of your pull request branches based on this `master` branch. -> Whenever you want to update your version of `master`, do a regular `git pull`. +> fetch the git information from that remote, then set your local `main` branch +> to use the upstream main branch whenever you run `git pull`. Then you can make +> all of your pull request branches based on this `main` branch. Whenever you +> want to update your version of `main`, do a regular `git pull`. ## Committing and Pushing changes diff --git a/README.md b/README.md index e0295221..f4b495c5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ height="80" width="80" alt="goat" - src="https://raw.githubusercontent.com/testing-library/react-testing-library/master/other/goat.png" + src="https://raw.githubusercontent.com/testing-library/react-testing-library/main/other/goat.png" /> @@ -44,7 +44,7 @@ practices. @@ -362,9 +362,9 @@ You'll find runnable examples of testing with different libraries in [the `react-testing-library-examples` codesandbox](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples). Some included are: -- [`react-redux`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/master/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-redux.js&previewwindow=tests) -- [`react-router`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/master/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-router.js&previewwindow=tests) -- [`react-context`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/master/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-context.js&previewwindow=tests) +- [`react-redux`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/main/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-redux.js&previewwindow=tests) +- [`react-router`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/main/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-router.js&previewwindow=tests) +- [`react-context`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/main/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-context.js&previewwindow=tests) You can also find React Testing Library examples at [react-testing-examples.com](https://react-testing-examples.com/jest-rtl/). @@ -636,11 +636,11 @@ Contributions of any kind welcome! [downloads-badge]: https://img.shields.io/npm/dm/@testing-library/react.svg?style=flat-square [npmtrends]: http://www.npmtrends.com/@testing-library/react [license-badge]: https://img.shields.io/npm/l/@testing-library/react.svg?style=flat-square -[license]: https://github.com/testing-library/react-testing-library/blob/master/LICENSE +[license]: https://github.com/testing-library/react-testing-library/blob/main/LICENSE [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square [prs]: http://makeapullrequest.com [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square -[coc]: https://github.com/testing-library/react-testing-library/blob/master/CODE_OF_CONDUCT.md +[coc]: https://github.com/testing-library/react-testing-library/blob/main/CODE_OF_CONDUCT.md [github-watch-badge]: https://img.shields.io/github/watchers/testing-library/react-testing-library.svg?style=social [github-watch]: https://github.com/testing-library/react-testing-library/watchers [github-star-badge]: https://img.shields.io/github/stars/testing-library/react-testing-library.svg?style=social diff --git a/other/MAINTAINING.md b/other/MAINTAINING.md index cade1c2a..da09ba7c 100644 --- a/other/MAINTAINING.md +++ b/other/MAINTAINING.md @@ -48,7 +48,7 @@ to release. See the next section on Releases for more about that. ## Release -Our releases are automatic. They happen whenever code lands into `master`. A +Our releases are automatic. They happen whenever code lands into `main`. A github action gets kicked off and if it's successful, a tool called [`semantic-release`](https://github.com/semantic-release/semantic-release) is used to automatically publish a new release to npm as well as a changelog to From 8a1c8e9396aa7f4843b54e31e2c95404ab8ad940 Mon Sep 17 00:00:00 2001 From: Johannes Ewald
Date: Fri, 14 May 2021 14:39:23 +0200 Subject: [PATCH 035/126] fix: Guard against `process` not being defined (#911) Webpack 5 doesn't shim Node's process object by default anymore. Only instances of process.env.NODE_ENV are replaced statically. This means that unguarded checks of process.env will crash in browser environments (such as Karma). --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 4f92e02b..c89dc1a2 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,7 @@ import {cleanup} from './pure' // this ensures that tests run in isolation from each other // if you don't like this then either import the `pure` module // or set the RTL_SKIP_AUTO_CLEANUP env variable to 'true'. -if (!process.env.RTL_SKIP_AUTO_CLEANUP) { +if (typeof process === "undefined" || !process.env?.RTL_SKIP_AUTO_CLEANUP) { // ignore teardown() in code coverage because Jest does not support it /* istanbul ignore else */ if (typeof afterEach === 'function') { From 3d47043432a10b8279fd88eb99cd82f512f0d851 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 14 May 2021 14:40:04 +0200 Subject: [PATCH 036/126] docs: add jhnns as a contributor (#912) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index ad6a5484..89091cb7 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1252,6 +1252,15 @@ "contributions": [ "test" ] + }, + { + "login": "jhnns", + "name": "Johannes Ewald", + "avatar_url": "https://avatars.githubusercontent.com/u/781746?v=4", + "profile": "https://github.com/jhnns", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index f4b495c5..80d7ad9e 100644 --- a/README.md +++ b/README.md @@ -607,6 +607,7 @@ Thanks goes to these people ([emoji key][emojis]):
sanchit121
π π»
Solufa
π π»+
Ari PerkkiΓΆ
β οΈ
Johannes Ewald
π»Date: Tue, 8 Jun 2021 10:55:05 +0200 Subject: [PATCH 038/126] chore: Don't test with node 15 (#922) --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index dc793472..d747c513 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,7 +17,7 @@ jobs: if: ${{ !contains(github.head_ref, 'all-contributors') }} strategy: matrix: - node: [10.13, 12, 14, 15, 16] + node: [10.13, 12, 14, 16] react: [latest, next, experimental] runs-on: ubuntu-latest steps: From 770246e5cf15593bee96de5ce8b43305826c0893 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 8 Jun 2021 11:30:21 +0200 Subject: [PATCH 039/126] fix: Bump testing-library/dom to v8 alpha (#923) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fde78a35..717cdf7a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^7.28.1" + "@testing-library/dom": "^8.0.0-alpha.3" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", From c1a931d769dfba1bef49442de34132c2dd3837ef Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 8 Jun 2021 20:05:19 +0200 Subject: [PATCH 040/126] chore: Bump kcd-scripts to 11.x (#921) --- package.json | 7 +++++-- src/__tests__/cleanup.js | 11 ++++++----- src/__tests__/debug.js | 4 ++-- src/__tests__/new-act.js | 4 ++-- src/__tests__/no-act.js | 6 +++--- src/__tests__/old-act.js | 34 +++++++++++++++++----------------- src/__tests__/render.js | 16 ++++++++-------- types/test.tsx | 32 ++++++++++++++++---------------- 8 files changed, 59 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 717cdf7a..74cbf66d 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@testing-library/jest-dom": "^5.11.6", "@types/react-dom": "^17.0.0", "dotenv-cli": "^4.0.0", - "kcd-scripts": "^7.5.1", + "kcd-scripts": "^11.1.0", "npm-run-all": "^4.1.5", "react": "^17.0.1", "react-dom": "^17.0.1", @@ -68,7 +68,10 @@ "react/no-adjacent-inline-elements": "off", "import/no-unassigned-import": "off", "import/named": "off", - "testing-library/no-dom-import": "off" + "testing-library/no-container": "off", + "testing-library/no-dom-import": "off", + "testing-library/no-unnecessary-act": "off", + "testing-library/prefer-user-event": "off" } }, "eslintIgnore": [ diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js index 6043ae06..0dcbac12 100644 --- a/src/__tests__/cleanup.js +++ b/src/__tests__/cleanup.js @@ -54,15 +54,16 @@ describe('fake timers and missing act warnings', () => { jest.useRealTimers() }) - test('cleanup does not flush immediates', () => { + test('cleanup does not flush microtasks', () => { const microTaskSpy = jest.fn() function Test() { const counter = 1 const [, setDeferredCounter] = React.useState(null) React.useEffect(() => { let cancelled = false - setImmediate(() => { + Promise.resolve().then(() => { microTaskSpy() + // eslint-disable-next-line jest/no-if -- false positive if (!cancelled) { setDeferredCounter(counter) } @@ -92,12 +93,12 @@ describe('fake timers and missing act warnings', () => { const [, setDeferredCounter] = React.useState(null) React.useEffect(() => { let cancelled = false - setImmediate(() => { + setTimeout(() => { deferredStateUpdateSpy() if (!cancelled) { setDeferredCounter(counter) } - }) + }, 0) return () => { cancelled = true @@ -108,7 +109,7 @@ describe('fake timers and missing act warnings', () => { } render( ) - jest.runAllImmediates() + jest.runAllTimers() cleanup() expect(deferredStateUpdateSpy).toHaveBeenCalledTimes(1) diff --git a/src/__tests__/debug.js b/src/__tests__/debug.js index e4e9faa0..f3aad595 100644 --- a/src/__tests__/debug.js +++ b/src/__tests__/debug.js @@ -43,8 +43,8 @@ test('allows same arguments as prettyDOM', () => { expect(console.log).toHaveBeenCalledTimes(1) expect(console.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ - " - ...", ++ ..., ] `) }) diff --git a/src/__tests__/new-act.js b/src/__tests__/new-act.js index 56ce4970..42552594 100644 --- a/src/__tests__/new-act.js +++ b/src/__tests__/new-act.js @@ -49,7 +49,7 @@ test('async act recovers from errors', async () => { expect(console.error.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "call console.error", + call console.error, ], ] `) @@ -67,7 +67,7 @@ test('async act recovers from sync errors', async () => { expect(console.error.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "call console.error", + call console.error, ], ] `) diff --git a/src/__tests__/no-act.js b/src/__tests__/no-act.js index 039a79ae..686d78bb 100644 --- a/src/__tests__/no-act.js +++ b/src/__tests__/no-act.js @@ -2,7 +2,7 @@ let act, asyncAct beforeEach(() => { jest.resetModules() - act = require('..').act + act = require('../pure').act asyncAct = require('../act-compat').asyncAct jest.spyOn(console, 'error').mockImplementation(() => {}) }) @@ -53,7 +53,7 @@ test('async act recovers from errors', async () => { expect(console.error.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "call console.error", + call console.error, ], ] `) @@ -71,7 +71,7 @@ test('async act recovers from sync errors', async () => { expect(console.error.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "call console.error", + call console.error, ], ] `) diff --git a/src/__tests__/old-act.js b/src/__tests__/old-act.js index b3de9377..0153fea3 100644 --- a/src/__tests__/old-act.js +++ b/src/__tests__/old-act.js @@ -32,18 +32,18 @@ test('async act works even when the act is an old one', async () => { console.error('sigil') }) expect(console.error.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "sigil", - ], - Array [ - "It looks like you're using a version of react-dom that supports the \\"act\\" function, but not an awaitable version of \\"act\\" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning.", - ], - Array [ - "sigil", - ], - ] - `) + Array [ + Array [ + sigil, + ], + Array [ + It looks like you're using a version of react-dom that supports the "act" function, but not an awaitable version of "act" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning., + ], + Array [ + sigil, + ], + ] + `) expect(callback).toHaveBeenCalledTimes(1) // and it doesn't warn you twice @@ -71,10 +71,10 @@ test('async act recovers from async errors', async () => { expect(console.error.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "It looks like you're using a version of react-dom that supports the \\"act\\" function, but not an awaitable version of \\"act\\" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning.", + It looks like you're using a version of react-dom that supports the "act" function, but not an awaitable version of "act" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning., ], Array [ - "call console.error", + call console.error, ], ] `) @@ -92,7 +92,7 @@ test('async act recovers from sync errors', async () => { expect(console.error.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "call console.error", + call console.error, ], ] `) @@ -109,11 +109,11 @@ test('async act can handle any sort of console.error', async () => { Array [ Array [ Object { - "error": "some error", + error: some error, }, ], Array [ - "It looks like you're using a version of react-dom that supports the \\"act\\" function, but not an awaitable version of \\"act\\" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning.", + It looks like you're using a version of react-dom that supports the "act" function, but not an awaitable version of "act" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning., ], ] `) diff --git a/src/__tests__/render.js b/src/__tests__/render.js index fdc1ff4c..fea1a649 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -78,14 +78,14 @@ test('renders options.wrapper around node', () => { expect(screen.getByTestId('wrapper')).toBeInTheDocument() expect(container.firstChild).toMatchInlineSnapshot(` -- --`) ++ ++ `) }) test('flushes useEffect cleanup functions sync on unmount()', () => { diff --git a/types/test.tsx b/types/test.tsx index 5f9575ac..239caed6 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -3,39 +3,39 @@ import {render, fireEvent, screen, waitFor} from '.' import * as pure from './pure' export async function testRender() { - const page = render() + const view = render() // single queries - page.getByText('foo') - page.queryByText('foo') - await page.findByText('foo') + view.getByText('foo') + view.queryByText('foo') + await view.findByText('foo') // multiple queries - page.getAllByText('bar') - page.queryAllByText('bar') - await page.findAllByText('bar') + view.getAllByText('bar') + view.queryAllByText('bar') + await view.findAllByText('bar') // helpers - const {container, rerender, debug} = page + const {container, rerender, debug} = view expectType(container) return {container, rerender, debug} } export async function testPureRender() { - const page = pure.render() + const view = pure.render() // single queries - page.getByText('foo') - page.queryByText('foo') - await page.findByText('foo') + view.getByText('foo') + view.queryByText('foo') + await view.findByText('foo') // multiple queries - page.getAllByText('bar') - page.queryAllByText('bar') - await page.findAllByText('bar') + view.getAllByText('bar') + view.queryAllByText('bar') + await view.findAllByText('bar') // helpers - const {container, rerender, debug} = page + const {container, rerender, debug} = view expectType (container) return {container, rerender, debug} } From 7957355f38242ff0ba736eb35e3cc8cf4c95df24 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 12 Jun 2021 10:10:30 +0200 Subject: [PATCH 041/126] docs: add anpaopao as a contributor (#917) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Sebastian Silbermann --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 89091cb7..518b9f23 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1261,6 +1261,15 @@ "contributions": [ "code" ] + }, + { + "login": "anpaopao", + "name": "Angus J. Pope", + "avatar_url": "https://avatars.githubusercontent.com/u/44686792?v=4", + "profile": "https://github.com/anpaopao", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 80d7ad9e..76fc0223 100644 --- a/README.md +++ b/README.md @@ -608,6 +608,7 @@ Thanks goes to these people ([emoji key][emojis]):
Solufa
π π»
Ari PerkkiΓΆ
β οΈ+
Johannes Ewald
π»From 487eb851d96406557859524eab3fabae82dd3d18 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann
Angus J. Pope
πDate: Sat, 12 Jun 2021 11:05:11 +0200 Subject: [PATCH 042/126] test: Ignore React 18 legacy root deprecation warnings (#928) --- src/__tests__/cleanup.js | 19 ++++++++++++++----- src/__tests__/no-act.js | 36 ++++++++++++++++++++++++------------ src/__tests__/stopwatch.js | 5 ++++- tests/setup-env.js | 20 ++++++++++++++++++++ 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js index 6043ae06..b034ca75 100644 --- a/src/__tests__/cleanup.js +++ b/src/__tests__/cleanup.js @@ -82,7 +82,10 @@ describe('fake timers and missing act warnings', () => { expect(microTaskSpy).toHaveBeenCalledTimes(0) // console.error is mocked // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalledTimes(0) + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 1 : 0, + ) }) test('cleanup does not swallow missing act warnings', () => { @@ -114,10 +117,16 @@ describe('fake timers and missing act warnings', () => { expect(deferredStateUpdateSpy).toHaveBeenCalledTimes(1) // console.error is mocked // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalledTimes(1) - // eslint-disable-next-line no-console - expect(console.error.mock.calls[0][0]).toMatch( - 'a test was not wrapped in act(...)', + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 2 : 1, ) + // eslint-disable-next-line no-console + expect( + console.error.mock.calls[ + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 1 : 0 + ][0], + ).toMatch('a test was not wrapped in act(...)') }) }) diff --git a/src/__tests__/no-act.js b/src/__tests__/no-act.js index 039a79ae..1e53e125 100644 --- a/src/__tests__/no-act.js +++ b/src/__tests__/no-act.js @@ -1,9 +1,10 @@ -let act, asyncAct +let act, asyncAct, React beforeEach(() => { jest.resetModules() act = require('..').act asyncAct = require('../act-compat').asyncAct + React = require('react') jest.spyOn(console, 'error').mockImplementation(() => {}) }) @@ -17,7 +18,10 @@ test('act works even when there is no act from test utils', () => { const callback = jest.fn() act(callback) expect(callback).toHaveBeenCalledTimes(1) - expect(console.error).toHaveBeenCalledTimes(0) + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 1 : 0, + ) }) test('async act works when it does not exist (older versions of react)', async () => { @@ -26,7 +30,10 @@ test('async act works when it does not exist (older versions of react)', async ( await Promise.resolve() await callback() }) - expect(console.error).toHaveBeenCalledTimes(0) + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 2 : 0, + ) expect(callback).toHaveBeenCalledTimes(1) callback.mockClear() @@ -36,7 +43,10 @@ test('async act works when it does not exist (older versions of react)', async ( await Promise.resolve() await callback() }) - expect(console.error).toHaveBeenCalledTimes(0) + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 2 : 0, + ) expect(callback).toHaveBeenCalledTimes(1) }) @@ -49,14 +59,16 @@ test('async act recovers from errors', async () => { } catch (err) { console.error('call console.error') } - expect(console.error).toHaveBeenCalledTimes(1) - expect(console.error.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "call console.error", - ], - ] - `) + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 2 : 1, + ) + expect( + console.error.mock.calls[ + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 1 : 0 + ][0], + ).toMatch('call console.error') }) test('async act recovers from sync errors', async () => { diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js index eeaf395c..400fce10 100644 --- a/src/__tests__/stopwatch.js +++ b/src/__tests__/stopwatch.js @@ -53,5 +53,8 @@ test('unmounts a component', async () => { // and get an error. await sleep(5) // eslint-disable-next-line no-console - expect(console.error).not.toHaveBeenCalled() + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 1 : 0, + ) }) diff --git a/tests/setup-env.js b/tests/setup-env.js index 264828a9..71eeac3d 100644 --- a/tests/setup-env.js +++ b/tests/setup-env.js @@ -1 +1,21 @@ import '@testing-library/jest-dom/extend-expect' + +beforeEach(() => { + const originalConsoleError = console.error + jest + .spyOn(console, 'error') + .mockImplementation((message, ...optionalParams) => { + // Ignore ReactDOM.render/ReactDOM.hydrate deprecation warning + if (message.indexOf('Use createRoot instead.') !== -1) { + return + } + originalConsoleError(message, ...optionalParams) + }) +}) + +afterEach(() => { + // maybe another test already restore console error mocks + if (typeof console.error.mockRestore === 'function') { + console.error.mockRestore() + } +}) From c1878a9ea68a3a13982233684b924a917e87a1f6 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sat, 12 Jun 2021 11:18:37 +0200 Subject: [PATCH 043/126] test: Ignore React 18 legacy root deprecation warnings (#929) --- src/__tests__/cleanup.js | 19 +++++++++++++----- src/__tests__/new-act.js | 6 +++--- src/__tests__/no-act.js | 40 +++++++++++++++++++++++++------------- src/__tests__/old-act.js | 6 +++--- src/__tests__/stopwatch.js | 5 ++++- tests/setup-env.js | 19 ++++++++++++++++++ 6 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js index 0dcbac12..9d3f52d4 100644 --- a/src/__tests__/cleanup.js +++ b/src/__tests__/cleanup.js @@ -83,7 +83,10 @@ describe('fake timers and missing act warnings', () => { expect(microTaskSpy).toHaveBeenCalledTimes(0) // console.error is mocked // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalledTimes(0) + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 1 : 0, + ) }) test('cleanup does not swallow missing act warnings', () => { @@ -115,10 +118,16 @@ describe('fake timers and missing act warnings', () => { expect(deferredStateUpdateSpy).toHaveBeenCalledTimes(1) // console.error is mocked // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalledTimes(1) - // eslint-disable-next-line no-console - expect(console.error.mock.calls[0][0]).toMatch( - 'a test was not wrapped in act(...)', + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 2 : 1, ) + // eslint-disable-next-line no-console + expect( + console.error.mock.calls[ + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 1 : 0 + ][0], + ).toMatch('a test was not wrapped in act(...)') }) }) diff --git a/src/__tests__/new-act.js b/src/__tests__/new-act.js index 42552594..af81e29c 100644 --- a/src/__tests__/new-act.js +++ b/src/__tests__/new-act.js @@ -1,4 +1,4 @@ -let asyncAct +let asyncAct, consoleErrorMock jest.mock('react-dom/test-utils', () => ({ act: cb => { @@ -9,11 +9,11 @@ jest.mock('react-dom/test-utils', () => ({ beforeEach(() => { jest.resetModules() asyncAct = require('../act-compat').asyncAct - jest.spyOn(console, 'error').mockImplementation(() => {}) + consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { - console.error.mockRestore() + consoleErrorMock.mockRestore() }) test('async act works when it does not exist (older versions of react)', async () => { diff --git a/src/__tests__/no-act.js b/src/__tests__/no-act.js index 686d78bb..d739e763 100644 --- a/src/__tests__/no-act.js +++ b/src/__tests__/no-act.js @@ -1,14 +1,15 @@ -let act, asyncAct +let act, asyncAct, React, consoleErrorMock beforeEach(() => { jest.resetModules() act = require('../pure').act asyncAct = require('../act-compat').asyncAct - jest.spyOn(console, 'error').mockImplementation(() => {}) + React = require('react') + consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { - console.error.mockRestore() + consoleErrorMock.mockRestore() }) jest.mock('react-dom/test-utils', () => ({})) @@ -17,7 +18,10 @@ test('act works even when there is no act from test utils', () => { const callback = jest.fn() act(callback) expect(callback).toHaveBeenCalledTimes(1) - expect(console.error).toHaveBeenCalledTimes(0) + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 1 : 0, + ) }) test('async act works when it does not exist (older versions of react)', async () => { @@ -26,7 +30,10 @@ test('async act works when it does not exist (older versions of react)', async ( await Promise.resolve() await callback() }) - expect(console.error).toHaveBeenCalledTimes(0) + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 2 : 0, + ) expect(callback).toHaveBeenCalledTimes(1) callback.mockClear() @@ -36,7 +43,10 @@ test('async act works when it does not exist (older versions of react)', async ( await Promise.resolve() await callback() }) - expect(console.error).toHaveBeenCalledTimes(0) + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 2 : 0, + ) expect(callback).toHaveBeenCalledTimes(1) }) @@ -49,14 +59,16 @@ test('async act recovers from errors', async () => { } catch (err) { console.error('call console.error') } - expect(console.error).toHaveBeenCalledTimes(1) - expect(console.error.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - call console.error, - ], - ] - `) + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 2 : 1, + ) + expect( + console.error.mock.calls[ + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 1 : 0 + ][0], + ).toMatch('call console.error') }) test('async act recovers from sync errors', async () => { diff --git a/src/__tests__/old-act.js b/src/__tests__/old-act.js index 0153fea3..6081fef8 100644 --- a/src/__tests__/old-act.js +++ b/src/__tests__/old-act.js @@ -1,13 +1,13 @@ -let asyncAct +let asyncAct, consoleErrorMock beforeEach(() => { jest.resetModules() asyncAct = require('../act-compat').asyncAct - jest.spyOn(console, 'error').mockImplementation(() => {}) + consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { - console.error.mockRestore() + consoleErrorMock.mockRestore() }) jest.mock('react-dom/test-utils', () => ({ diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js index eeaf395c..400fce10 100644 --- a/src/__tests__/stopwatch.js +++ b/src/__tests__/stopwatch.js @@ -53,5 +53,8 @@ test('unmounts a component', async () => { // and get an error. await sleep(5) // eslint-disable-next-line no-console - expect(console.error).not.toHaveBeenCalled() + expect(console.error).toHaveBeenCalledTimes( + // ReactDOM.render is deprecated in React 18 + React.version.startsWith('18') ? 1 : 0, + ) }) diff --git a/tests/setup-env.js b/tests/setup-env.js index 264828a9..6c0b953b 100644 --- a/tests/setup-env.js +++ b/tests/setup-env.js @@ -1 +1,20 @@ import '@testing-library/jest-dom/extend-expect' + +let consoleErrorMock + +beforeEach(() => { + const originalConsoleError = console.error + consoleErrorMock = jest + .spyOn(console, 'error') + .mockImplementation((message, ...optionalParams) => { + // Ignore ReactDOM.render/ReactDOM.hydrate deprecation warning + if (message.indexOf('Use createRoot instead.') !== -1) { + return + } + originalConsoleError(message, ...optionalParams) + }) +}) + +afterEach(() => { + consoleErrorMock.mockRestore() +}) From 42dad789af193cdb11701db3fdb970f8ec0ffc83 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sat, 12 Jun 2021 11:27:17 +0200 Subject: [PATCH 044/126] feat: Drop support for node 10 (#930) BREAKING CHANGE: node 10 is no longer supported. It reached its end-of-life on 30.04.2021. --- .codesandbox/ci.json | 3 ++- .github/workflows/validate.yml | 2 +- package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index f9286936..f866879a 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,3 +1,4 @@ { - "sandboxes": ["new", "github/kentcdodds/react-testing-library-examples"] + "sandboxes": ["new", "github/kentcdodds/react-testing-library-examples"], + "node": "12" } diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d747c513..191bbcc1 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,7 +17,7 @@ jobs: if: ${{ !contains(github.head_ref, 'all-contributors') }} strategy: matrix: - node: [10.13, 12, 14, 16] + node: [12, 14, 16] react: [latest, next, experimental] runs-on: ubuntu-latest steps: diff --git a/package.json b/package.json index 74cbf66d..9eadb089 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "types": "types/index.d.ts", "module": "dist/@testing-library/react.esm.js", "engines": { - "node": ">=10" + "node": ">=12" }, "scripts": { "prebuild": "rimraf dist", From 05c74219cc19a46b0aa81a81db93121387bd4f8c Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sun, 13 Jun 2021 18:02:19 +0200 Subject: [PATCH 045/126] fix: Update @testing-library/dom (#931) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9eadb089..68622939 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.0.0-alpha.3" + "@testing-library/dom": "^8.0.0-alpha.6" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", From 6e6bf85301feed5d6bfa498d6d7f58a0bdef4520 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Sun, 20 Jun 2021 10:28:18 +0200 Subject: [PATCH 046/126] feat: Bump @testing-library/dom BREAKING CHANGE: Bump `@testing-library/dom` to 8.0.0. Please check out the [`@testing-library/dom@8.0.0` release page](https://github.com/testing-library/dom-testing-library/releases/tag/v8.0.0) for a detailed list of breaking changes. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 68622939..42555be0 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.0.0-alpha.6" + "@testing-library/dom": "^8.0.0" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", From 801edaa0d607e8a5acb988a9ba7777262c5d2df4 Mon Sep 17 00:00:00 2001 From: Dominik Lesch <62334278+leschdom@users.noreply.github.com> Date: Thu, 15 Jul 2021 03:25:14 +0200 Subject: [PATCH 047/126] docs(CONTRIBUTING): update links (#932) * docs(CONTRIBUTING): update links ... the old egghead.io link redirects to https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github which displays "We've got a new version of egghead in BETA. Click here to try it!" remove obsolete all-contributors link (was removed in 29aad25c) * docs: use consistent spelling for TypeScript ... inspired by https://effectivetypescript.com/2021/02/03/pet-peeves/ Co-authored-by: Matan Borenkraout --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- CONTRIBUTING.md | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 371a86f6..f5000e21 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -37,7 +37,7 @@ merge of your pull request! - [ ] Documentation added to the [docs site](https://github.com/testing-library/testing-library-docs) - [ ] Tests -- [ ] Typescript definitions updated +- [ ] TypeScript definitions updated - [ ] Ready to be merged diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68e77e6f..e16e9d61 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,11 +35,11 @@ sure to include those changes (if they exist) in your commit. ### Update Typings If your PR introduced some changes in the API, you are more than welcome to -modify the Typescript type definition to reflect those changes. Just modify the -`/types/index.d.ts` file accordingly. If you have never seen Typescript +modify the TypeScript type definition to reflect those changes. Just modify the +`/types/index.d.ts` file accordingly. If you have never seen TypeScript definitions before, you can read more about it in its [documentation pages](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html). -Though this library itself is not written in Typescript we use +Though this library itself is not written in TypeScript we use [dtslint](https://github.com/microsoft/dtslint) to lint our typings. ## Help needed @@ -50,6 +50,5 @@ Also, please watch the repo and respond to questions/bug reports/feature requests! Thanks! [egghead]: - https://app.egghead.io/series/how-to-contribute-to-an-open-source-project-on-github -[all-contributors]: https://github.com/all-contributors/all-contributors + https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github [issues]: https://github.com/testing-library/react-testing-library/issues From 8f3bd3c34fa63edf33b2139f27544e41f7593dfd Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Jul 2021 21:45:28 -0400 Subject: [PATCH 048/126] docs: add leschdom as a contributor for doc (#941) * docs: update README.md * docs: update .all-contributorsrc Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 518b9f23..27ba8e24 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1270,6 +1270,15 @@ "contributions": [ "doc" ] + }, + { + "login": "leschdom", + "name": "Dominik Lesch", + "avatar_url": "https://avatars.githubusercontent.com/u/62334278?v=4", + "profile": "https://github.com/leschdom", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 76fc0223..f0fa3b2a 100644 --- a/README.md +++ b/README.md @@ -609,6 +609,7 @@ Thanks goes to these people ([emoji key][emojis]):
Ari PerkkiΓΆ
β οΈ
Johannes Ewald
π»+
Angus J. Pope
πFrom 68fd459149e04d2edea991f61ea04dfc8c3cba34 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann
Dominik Lesch
πDate: Sat, 11 Sep 2021 11:38:50 +0200 Subject: [PATCH 049/126] test: Ensure broken node 16.9.0 isn't used (#960) --- .github/workflows/validate.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 191bbcc1..b9cde88a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,7 +17,8 @@ jobs: if: ${{ !contains(github.head_ref, 'all-contributors') }} strategy: matrix: - node: [12, 14, 16] + # TODO: relax `'16.9.1'` to `16` once GitHub has 16.9.1 cached. 16.9.0 is broken due to https://github.com/nodejs/node/issues/40030 + node: [12, 14, '16.9.1'] react: [latest, next, experimental] runs-on: ubuntu-latest steps: From 071a6fdc1d8378bad0ad2d9f8fa58ec846ed7e2d Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sat, 11 Sep 2021 11:42:24 +0200 Subject: [PATCH 050/126] test: Format codebase with prettier (#959) --- .github/workflows/validate.yml | 4 ++++ package.json | 1 + src/index.js | 2 +- types/index.d.ts | 6 +++--- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b9cde88a..67b71c24 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -38,6 +38,10 @@ jobs: with: useLockFile: false + # TODO: Can be removed if https://github.com/kentcdodds/kcd-scripts/pull/146 is released + - name: Verify format (`npm run format` committed?) + run: npm run format -- --check --no-write + # as requested by the React team :) # https://reactjs.org/blog/2019/10/22/react-release-channels.html#using-the-next-channel-for-integration-testing - name: βοΈ Setup react diff --git a/package.json b/package.json index 42555be0..1cd283fb 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "build:bundle:main": "dotenv -e .bundle.main.env kcd-scripts build -- --bundle --no-clean", "build:bundle:pure": "dotenv -e .bundle.main.env -e .bundle.pure.env kcd-scripts build -- --bundle --no-clean", "build:main": "kcd-scripts build --no-clean", + "format": "kcd-scripts format", "lint": "kcd-scripts lint", "setup": "npm install && npm run validate -s", "test": "kcd-scripts test", diff --git a/src/index.js b/src/index.js index c89dc1a2..96fbe155 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,7 @@ import {cleanup} from './pure' // this ensures that tests run in isolation from each other // if you don't like this then either import the `pure` module // or set the RTL_SKIP_AUTO_CLEANUP env variable to 'true'. -if (typeof process === "undefined" || !process.env?.RTL_SKIP_AUTO_CLEANUP) { +if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) { // ignore teardown() in code coverage because Jest does not support it /* istanbul ignore else */ if (typeof afterEach === 'function') { diff --git a/types/index.d.ts b/types/index.d.ts index 6d0c67ee..46dd1575 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -13,7 +13,7 @@ export * from '@testing-library/dom' export type RenderResult< Q extends Queries = typeof queries, - Container extends Element | DocumentFragment = HTMLElement + Container extends Element | DocumentFragment = HTMLElement, > = { container: Container baseElement: Element @@ -32,7 +32,7 @@ export type RenderResult< export interface RenderOptions< Q extends Queries = typeof queries, - Container extends Element | DocumentFragment = HTMLElement + Container extends Element | DocumentFragment = HTMLElement, > { container?: Container baseElement?: Element @@ -48,7 +48,7 @@ type Omit = Pick > */ export function render< Q extends Queries = typeof queries, - Container extends Element | DocumentFragment = HTMLElement + Container extends Element | DocumentFragment = HTMLElement, >( ui: React.ReactElement, options: RenderOptions , From fbacb0da993518c2975efee17604e99a3180664f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20G=C3=B3mez?= <44379989+ImADrafter@users.noreply.github.com> Date: Sat, 11 Sep 2021 11:50:31 +0200 Subject: [PATCH 051/126] feat: improve JSDocs for RenderOptions (#909) Co-authored-by: Sebastian Silbermann--- types/index.d.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index 46dd1575..663e6280 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -34,10 +34,42 @@ export interface RenderOptions< Q extends Queries = typeof queries, Container extends Element | DocumentFragment = HTMLElement, > { + /** + * By default, React Testing Library will create a div and append that div to the document.body. Your React component will be rendered in the created div. If you provide your own HTMLElement container via this option, + * it will not be appended to the document.body automatically. + * + * For example: If you are unit testing a `` element, it cannot be a child of a div. In this case, you can + * specify a table as the render container. + * + * @see https://testing-library.com/docs/react-testing-library/api/#container + */ container?: Container + /** + * Defaults to the container if the container is specified. Otherwise `document.body` is used for the default. This is used as + * the base element for the queries as well as what is printed when you use `debug()`. + * + * @see https://testing-library.com/docs/react-testing-library/api/#baseelement + */ baseElement?: Element + /** + * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side + * rendering and use ReactDOM.hydrate to mount your components. + * + * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) + */ hydrate?: boolean + /** + * Queries to bind. Overrides the default set from DOM Testing Library unless merged. + * + * @see https://testing-library.com/docs/react-testing-library/api/#queries + */ queries?: Q + /** + * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating + * reusable custom render functions for common data providers. See setup for examples. + * + * @see https://testing-library.com/docs/react-testing-library/api/#wrapper + */ wrapper?: React.ComponentType } From 3325061cac7c6cfcefa7da7b80765c7d36a25b40 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 11 Sep 2021 11:54:16 +0200 Subject: [PATCH 052/126] docs: add ImADrafter as a contributor for doc (#961) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 +++ 2 files changed, 12 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 27ba8e24..b5bb36d0 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1279,6 +1279,15 @@ "contributions": [ "doc" ] + }, + { + "login": "ImADrafter", + "name": "Marcos GΓ³mez", + "avatar_url": "https://avatars.githubusercontent.com/u/44379989?v=4", + "profile": "https://github.com/ImADrafter", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index f0fa3b2a..60970526 100644 --- a/README.md +++ b/README.md @@ -611,6 +611,9 @@ Thanks goes to these people ([emoji key][emojis]):
Angus J. Pope
π+
Dominik Lesch
π+ From 84851dc660d49543707ba9eead42395c45ec06e2 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann+
Marcos GΓ³mez
πDate: Sun, 12 Sep 2021 11:12:06 +0200 Subject: [PATCH 053/126] test: Backport tests using the full timer matrix (#962) --- src/__tests__/end-to-end.js | 76 +++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 20 deletions(-) diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js index 87c70f1b..cf222aec 100644 --- a/src/__tests__/end-to-end.js +++ b/src/__tests__/end-to-end.js @@ -1,5 +1,5 @@ import * as React from 'react' -import {render, waitForElementToBeRemoved, screen} from '../' +import {render, waitForElementToBeRemoved, screen, waitFor} from '../' const fetchAMessage = () => new Promise(resolve => { @@ -11,27 +11,63 @@ const fetchAMessage = () => }, randomTimeout) }) -class ComponentWithLoader extends React.Component { - state = {loading: true} - async componentDidMount() { - const data = await fetchAMessage() - this.setState({data, loading: false}) // eslint-disable-line - } - render() { - if (this.state.loading) { - return From 55e79c290d3ec8a8eb3d39539e2c05bf35dff3d9 Mon Sep 17 00:00:00 2001 From: Jeff WayLoading...+function ComponentWithLoader() { + const [state, setState] = React.useState({data: undefined, loading: true}) + React.useEffect(() => { + let cancelled = false + fetchAMessage().then(data => { + if (!cancelled) { + setState({data, loading: false}) + } + }) + + return () => { + cancelled = true } - return ( -- Loaded this message: {this.state.data.returnedMessage}! -- ) + }, []) + + if (state.loading) { + returnLoading...} + + return ( ++ Loaded this message: {state.data.returnedMessage}! ++ ) } -test('it waits for the data to be loaded', async () => { - render() - const loading = () => screen.getByText('Loading...') - await waitForElementToBeRemoved(loading) - expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) +describe.each([ + ['real timers', () => jest.useRealTimers()], + ['fake legacy timers', () => jest.useFakeTimers('legacy')], + ['fake modern timers', () => jest.useFakeTimers('modern')], +])('it waits for the data to be loaded using %s', (label, useTimers) => { + beforeEach(() => { + useTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + test('waitForElementToBeRemoved', async () => { + render( ) + const loading = () => screen.getByText('Loading...') + await waitForElementToBeRemoved(loading) + expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) + }) + + test('waitFor', async () => { + render( ) + const message = () => screen.getByText(/Loaded this message:/) + await waitFor(message) + expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) + }) + + test('findBy', async () => { + render( ) + await expect(screen.findByTestId('message')).resolves.toHaveTextContent( + /Hello World/, + ) + }) }) From a218b63163e1cafffb7da08fa05f26b68a34f14d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 12:06:02 +0200 Subject: [PATCH 054/126] docs: add akashshyamdev as a contributor for bug (#967) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index b5bb36d0..6e91ce5d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1288,6 +1288,15 @@ "contributions": [ "doc" ] + }, + { + "login": "akashshyamdev", + "name": "Akash Shyam", + "avatar_url": "https://avatars.githubusercontent.com/u/56759828?v=4", + "profile": "https://www.akashshyam.online/", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 60970526..3adcede1 100644 --- a/README.md +++ b/README.md @@ -613,6 +613,7 @@ Thanks goes to these people ([emoji key][emojis]): From cde904cd161782090d7ab7256bcb9b13e771d0ea Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" +
Marcos GΓ³mez
π
Akash Shyam
πDate: Mon, 27 Sep 2021 06:39:57 -0600 Subject: [PATCH 055/126] fix(TS): make wrapper allow a simple function comp (#966) Co-authored-by: eps1lon --- types/index.d.ts | 2 +- types/test.tsx | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 663e6280..3baf7b5e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -70,7 +70,7 @@ export interface RenderOptions< * * @see https://testing-library.com/docs/react-testing-library/api/#wrapper */ - wrapper?: React.ComponentType + wrapper?: React.ComponentType<{children: React.ReactElement}> } type Omit = Pick > diff --git a/types/test.tsx b/types/test.tsx index 239caed6..7c45eaae 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -100,6 +100,28 @@ export function testQueries() { ) } +export function wrappedRender( + ui: React.ReactElement, + options?: pure.RenderOptions, +) { + const Wrapper = ({children}: {children: React.ReactElement}): JSX.Element => { + return {children}+ } + + return pure.render(ui, {wrapper: Wrapper, ...options}) +} + +export function wrappedRenderB( + ui: React.ReactElement, + options?: pure.RenderOptions, +) { + const Wrapper: React.FunctionComponent = ({children}) => { + return{children}+ } + + return pure.render(ui, {wrapper: Wrapper, ...options}) +} + /* eslint testing-library/prefer-explicit-assert: "off", From 7f53b5673f64ff72a54c19b0685f901cfec96b03 Mon Sep 17 00:00:00 2001 From: Sebastian SilbermannDate: Sun, 3 Oct 2021 15:14:51 +0200 Subject: [PATCH 056/126] fix(render): Don't reject wrapper types based on statics (#973) --- types/index.d.ts | 2 +- types/test.tsx | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 3baf7b5e..92eb2d7b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -70,7 +70,7 @@ export interface RenderOptions< * * @see https://testing-library.com/docs/react-testing-library/api/#wrapper */ - wrapper?: React.ComponentType<{children: React.ReactElement}> + wrapper?: React.JSXElementConstructor<{children: React.ReactElement}> } type Omit = Pick > diff --git a/types/test.tsx b/types/test.tsx index 7c45eaae..71ea30a9 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -115,13 +115,32 @@ export function wrappedRenderB( ui: React.ReactElement, options?: pure.RenderOptions, ) { - const Wrapper: React.FunctionComponent = ({children}) => { + const Wrapper: React.FunctionComponent<{children?: React.ReactNode}> = ({ + children, + }) => { return {children}} return pure.render(ui, {wrapper: Wrapper, ...options}) } +export function wrappedRenderC( + ui: React.ReactElement, + options?: pure.RenderOptions, +) { + interface AppWrapperProps { + userProviderProps?: {user: string} + } + const AppWrapperProps: React.FunctionComponent= ({ + children, + userProviderProps = {user: 'TypeScript'}, + }) => { + return {children}+ } + + return pure.render(ui, {wrapper: AppWrapperProps, ...options}) +} + /* eslint testing-library/prefer-explicit-assert: "off", From 8f17a2bc4fa9c2c17a623b38ded9ab18b086e457 Mon Sep 17 00:00:00 2001 From: Philipp FritscheDate: Thu, 7 Oct 2021 14:12:08 +0200 Subject: [PATCH 057/126] chore: set protocol for npm to `https` (#975) --- .npmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index d2722898..1df2a6d8 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,2 @@ -registry=http://registry.npmjs.org/ +registry=https://registry.npmjs.org/ package-lock=false From 149d9a9af3addeb6c49696867b05b87afe0d0b3c Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Tue, 15 Feb 2022 23:50:32 +0100 Subject: [PATCH 058/126] fix: Add `@types/react-dom` as a direct dependency (#1001) Co-authored-by: Sebastian Silbermann --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1cd283fb..73d39de8 100644 --- a/package.json +++ b/package.json @@ -45,11 +45,11 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.0.0" + "@testing-library/dom": "^8.0.0", + "@types/react-dom": "*" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", - "@types/react-dom": "^17.0.0", "dotenv-cli": "^4.0.0", "kcd-scripts": "^11.1.0", "npm-run-all": "^4.1.5", From b0f9d9741205c54836bf82b76b86ec001a8c0e1f Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 15 Feb 2022 23:51:13 +0100 Subject: [PATCH 059/126] docs: add fmeum as a contributor for code, bug (#1016) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 10 ++++++++++ README.md | 1 + 2 files changed, 11 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6e91ce5d..df9690ed 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1297,6 +1297,16 @@ "contributions": [ "bug" ] + }, + { + "login": "fmeum", + "name": "Fabian Meumertzheim", + "avatar_url": "https://avatars.githubusercontent.com/u/4312191?v=4", + "profile": "https://hen.ne.ke", + "contributions": [ + "code", + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 3adcede1..0bc06ceb 100644 --- a/README.md +++ b/README.md @@ -614,6 +614,7 @@ Thanks goes to these people ([emoji key][emojis]): From 96ed8dafa5d02add2168a3da65d1cc0ffe6d6d1f Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann
Marcos GΓ³mez
π+
Akash Shyam
π
Fabian Meumertzheim
π» πDate: Wed, 9 Mar 2022 15:32:49 +0100 Subject: [PATCH 060/126] fix: Match runtime type of baseElement in TypeScript types (#1023) --- types/index.d.ts | 11 +++++++---- types/test.tsx | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 92eb2d7b..604b3966 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -14,9 +14,10 @@ export * from '@testing-library/dom' export type RenderResult< Q extends Queries = typeof queries, Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, > = { container: Container - baseElement: Element + baseElement: BaseElement debug: ( baseElement?: | Element @@ -33,6 +34,7 @@ export type RenderResult< export interface RenderOptions< Q extends Queries = typeof queries, Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, > { /** * By default, React Testing Library will create a div and append that div to the document.body. Your React component will be rendered in the created div. If you provide your own HTMLElement container via this option, @@ -50,7 +52,7 @@ export interface RenderOptions< * * @see https://testing-library.com/docs/react-testing-library/api/#baseelement */ - baseElement?: Element + baseElement?: BaseElement /** * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side * rendering and use ReactDOM.hydrate to mount your components. @@ -81,10 +83,11 @@ type Omit = Pick > export function render< Q extends Queries = typeof queries, Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, >( ui: React.ReactElement, - options: RenderOptions , -): RenderResult+ options: RenderOptions, +): RenderResultexport function render( ui: React.ReactElement, options?: Omit, diff --git a/types/test.tsx b/types/test.tsx index 71ea30a9..eae6e81f 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -141,6 +141,25 @@ export function wrappedRenderC( return pure.render(ui, {wrapper: AppWrapperProps, ...options}) } +export function testBaseElement() { + const {baseElement: baseDefaultElement} = render() + expectType (baseDefaultElement) + + const container = document.createElement('input') + const {baseElement: baseElementFromContainer} = render(, {container}) + expectType ( + baseElementFromContainer, + ) + + const baseElementOption = document.createElement('input') + const {baseElement: baseElementFromOption} = render(, { + baseElement: baseElementOption, + }) + expectType ( + baseElementFromOption, + ) +} + /* eslint testing-library/prefer-explicit-assert: "off", From 0c4aabe0da1587754229f7614a2ddfdaddd0b1aa Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sat, 19 Mar 2022 08:43:44 +0100 Subject: [PATCH 061/126] chore: Fix failing codesandbox/ci (#1026) --- .codesandbox/ci.json | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index f866879a..bf3237bb 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,4 +1,5 @@ { + "installCommand": "install:csb", "sandboxes": ["new", "github/kentcdodds/react-testing-library-examples"], "node": "12" } diff --git a/package.json b/package.json index 73d39de8..f184a8bc 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "build:bundle:pure": "dotenv -e .bundle.main.env -e .bundle.pure.env kcd-scripts build -- --bundle --no-clean", "build:main": "kcd-scripts build --no-clean", "format": "kcd-scripts format", + "install:csb": "npm install", "lint": "kcd-scripts lint", "setup": "npm install && npm run validate -s", "test": "kcd-scripts test", From ccd8a0d97dd9da0a420f2cf012a24d414d1646ed Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Thu, 31 Mar 2022 06:55:38 +0200 Subject: [PATCH 062/126] feat: Add support for React 18 (#1031) BREAKING CHANGE: Drop support for React 17 and earlier. We'll use the new [`createRoot` API](reactwg/react-18#5) by default which comes with a set of [changes while also enabling support for concurrent features](reactwg/react-18#4). To can opt-out of this change by using `render(ui, { legacyRoot: true } )`. But be aware that the legacy root API is deprecated in React 18 and its usage will trigger console warnings. Co-authored-by: Philipp Fritsche --- .github/workflows/validate.yml | 3 + package.json | 10 +- src/__tests__/act.js | 26 ++++- src/__tests__/cleanup.js | 19 +--- src/__tests__/new-act.js | 8 +- src/__tests__/no-act.js | 92 ---------------- src/__tests__/old-act.js | 142 ------------------------- src/__tests__/render.js | 96 ++++++++++++++++- src/__tests__/stopwatch.js | 5 +- src/act-compat.js | 186 ++++++++++++-------------------- src/index.js | 16 +++ src/pure.js | 187 +++++++++++++++++++++++++-------- tests/setup-env.js | 19 ---- types/index.d.ts | 5 + 14 files changed, 370 insertions(+), 444 deletions(-) delete mode 100644 src/__tests__/no-act.js delete mode 100644 src/__tests__/old-act.js diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 67b71c24..45cc7d13 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -16,6 +16,7 @@ jobs: # ignore all-contributors PRs if: ${{ !contains(github.head_ref, 'all-contributors') }} strategy: + fail-fast: false matrix: # TODO: relax `'16.9.1'` to `16` once GitHub has 16.9.1 cached. 16.9.0 is broken due to https://github.com/nodejs/node/issues/40030 node: [12, 14, '16.9.1'] @@ -52,6 +53,8 @@ jobs: - name: β¬οΈ Upload coverage report uses: codecov/codecov-action@v1 + with: + flags: ${{ matrix.react }} release: needs: main diff --git a/package.json b/package.json index f184a8bc..4781e962 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.0.0", + "@testing-library/dom": "^8.5.0", "@types/react-dom": "*" }, "devDependencies": { @@ -54,14 +54,14 @@ "dotenv-cli": "^4.0.0", "kcd-scripts": "^11.1.0", "npm-run-all": "^4.1.5", - "react": "^17.0.1", - "react-dom": "^17.0.1", + "react": "^18.0.0", + "react-dom": "^18.0.0", "rimraf": "^3.0.2", "typescript": "^4.1.2" }, "peerDependencies": { - "react": "*", - "react-dom": "*" + "react": "^18.0.0", + "react-dom": "^18.0.0" }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", diff --git a/src/__tests__/act.js b/src/__tests__/act.js index b60aac37..5430f28b 100644 --- a/src/__tests__/act.js +++ b/src/__tests__/act.js @@ -1,5 +1,5 @@ import * as React from 'react' -import {render, fireEvent, screen} from '../' +import {act, render, fireEvent, screen} from '../' test('render calls useEffect immediately', () => { const effectCb = jest.fn() @@ -43,3 +43,27 @@ test('calls to hydrate will run useEffects', () => { render( , {hydrate: true}) expect(effectCb).toHaveBeenCalledTimes(1) }) + +test('cleans up IS_REACT_ACT_ENVIRONMENT if its callback throws', () => { + global.IS_REACT_ACT_ENVIRONMENT = false + + expect(() => + act(() => { + throw new Error('threw') + }), + ).toThrow('threw') + + expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false) +}) + +test('cleans up IS_REACT_ACT_ENVIRONMENT if its async callback throws', async () => { + global.IS_REACT_ACT_ENVIRONMENT = false + + await expect(() => + act(async () => { + throw new Error('thenable threw') + }), + ).rejects.toThrow('thenable threw') + + expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false) +}) diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js index 9d3f52d4..0dcbac12 100644 --- a/src/__tests__/cleanup.js +++ b/src/__tests__/cleanup.js @@ -83,10 +83,7 @@ describe('fake timers and missing act warnings', () => { expect(microTaskSpy).toHaveBeenCalledTimes(0) // console.error is mocked // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalledTimes( - // ReactDOM.render is deprecated in React 18 - React.version.startsWith('18') ? 1 : 0, - ) + expect(console.error).toHaveBeenCalledTimes(0) }) test('cleanup does not swallow missing act warnings', () => { @@ -118,16 +115,10 @@ describe('fake timers and missing act warnings', () => { expect(deferredStateUpdateSpy).toHaveBeenCalledTimes(1) // console.error is mocked // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalledTimes( - // ReactDOM.render is deprecated in React 18 - React.version.startsWith('18') ? 2 : 1, - ) + expect(console.error).toHaveBeenCalledTimes(1) // eslint-disable-next-line no-console - expect( - console.error.mock.calls[ - // ReactDOM.render is deprecated in React 18 - React.version.startsWith('18') ? 1 : 0 - ][0], - ).toMatch('a test was not wrapped in act(...)') + expect(console.error.mock.calls[0][0]).toMatch( + 'a test was not wrapped in act(...)', + ) }) }) diff --git a/src/__tests__/new-act.js b/src/__tests__/new-act.js index af81e29c..05f9d45a 100644 --- a/src/__tests__/new-act.js +++ b/src/__tests__/new-act.js @@ -1,4 +1,4 @@ -let asyncAct, consoleErrorMock +let asyncAct jest.mock('react-dom/test-utils', () => ({ act: cb => { @@ -8,12 +8,12 @@ jest.mock('react-dom/test-utils', () => ({ beforeEach(() => { jest.resetModules() - asyncAct = require('../act-compat').asyncAct - consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {}) + asyncAct = require('../act-compat').default + jest.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { - consoleErrorMock.mockRestore() + console.error.mockRestore() }) test('async act works when it does not exist (older versions of react)', async () => { diff --git a/src/__tests__/no-act.js b/src/__tests__/no-act.js deleted file mode 100644 index d739e763..00000000 --- a/src/__tests__/no-act.js +++ /dev/null @@ -1,92 +0,0 @@ -let act, asyncAct, React, consoleErrorMock - -beforeEach(() => { - jest.resetModules() - act = require('../pure').act - asyncAct = require('../act-compat').asyncAct - React = require('react') - consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {}) -}) - -afterEach(() => { - consoleErrorMock.mockRestore() -}) - -jest.mock('react-dom/test-utils', () => ({})) - -test('act works even when there is no act from test utils', () => { - const callback = jest.fn() - act(callback) - expect(callback).toHaveBeenCalledTimes(1) - expect(console.error).toHaveBeenCalledTimes( - // ReactDOM.render is deprecated in React 18 - React.version.startsWith('18') ? 1 : 0, - ) -}) - -test('async act works when it does not exist (older versions of react)', async () => { - const callback = jest.fn() - await asyncAct(async () => { - await Promise.resolve() - await callback() - }) - expect(console.error).toHaveBeenCalledTimes( - // ReactDOM.render is deprecated in React 18 - React.version.startsWith('18') ? 2 : 0, - ) - expect(callback).toHaveBeenCalledTimes(1) - - callback.mockClear() - console.error.mockClear() - - await asyncAct(async () => { - await Promise.resolve() - await callback() - }) - expect(console.error).toHaveBeenCalledTimes( - // ReactDOM.render is deprecated in React 18 - React.version.startsWith('18') ? 2 : 0, - ) - expect(callback).toHaveBeenCalledTimes(1) -}) - -test('async act recovers from errors', async () => { - try { - await asyncAct(async () => { - await null - throw new Error('test error') - }) - } catch (err) { - console.error('call console.error') - } - expect(console.error).toHaveBeenCalledTimes( - // ReactDOM.render is deprecated in React 18 - React.version.startsWith('18') ? 2 : 1, - ) - expect( - console.error.mock.calls[ - // ReactDOM.render is deprecated in React 18 - React.version.startsWith('18') ? 1 : 0 - ][0], - ).toMatch('call console.error') -}) - -test('async act recovers from sync errors', async () => { - try { - await asyncAct(() => { - throw new Error('test error') - }) - } catch (err) { - console.error('call console.error') - } - expect(console.error).toHaveBeenCalledTimes(1) - expect(console.error.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - call console.error, - ], - ] - `) -}) - -/* eslint no-console:0 */ diff --git a/src/__tests__/old-act.js b/src/__tests__/old-act.js deleted file mode 100644 index 6081fef8..00000000 --- a/src/__tests__/old-act.js +++ /dev/null @@ -1,142 +0,0 @@ -let asyncAct, consoleErrorMock - -beforeEach(() => { - jest.resetModules() - asyncAct = require('../act-compat').asyncAct - consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => {}) -}) - -afterEach(() => { - consoleErrorMock.mockRestore() -}) - -jest.mock('react-dom/test-utils', () => ({ - act: cb => { - cb() - return { - then() { - console.error( - 'Warning: Do not await the result of calling ReactTestUtils.act(...), it is not a Promise.', - ) - }, - } - }, -})) - -test('async act works even when the act is an old one', async () => { - const callback = jest.fn() - await asyncAct(async () => { - console.error('sigil') - await Promise.resolve() - await callback() - console.error('sigil') - }) - expect(console.error.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - sigil, - ], - Array [ - It looks like you're using a version of react-dom that supports the "act" function, but not an awaitable version of "act" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning., - ], - Array [ - sigil, - ], - ] - `) - expect(callback).toHaveBeenCalledTimes(1) - - // and it doesn't warn you twice - callback.mockClear() - console.error.mockClear() - - await asyncAct(async () => { - await Promise.resolve() - await callback() - }) - expect(console.error).toHaveBeenCalledTimes(0) - expect(callback).toHaveBeenCalledTimes(1) -}) - -test('async act recovers from async errors', async () => { - try { - await asyncAct(async () => { - await null - throw new Error('test error') - }) - } catch (err) { - console.error('call console.error') - } - expect(console.error).toHaveBeenCalledTimes(2) - expect(console.error.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - It looks like you're using a version of react-dom that supports the "act" function, but not an awaitable version of "act" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning., - ], - Array [ - call console.error, - ], - ] - `) -}) - -test('async act recovers from sync errors', async () => { - try { - await asyncAct(() => { - throw new Error('test error') - }) - } catch (err) { - console.error('call console.error') - } - expect(console.error).toHaveBeenCalledTimes(1) - expect(console.error.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - call console.error, - ], - ] - `) -}) - -test('async act can handle any sort of console.error', async () => { - await asyncAct(async () => { - console.error({error: 'some error'}) - await null - }) - - expect(console.error).toHaveBeenCalledTimes(2) - expect(console.error.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - error: some error, - }, - ], - Array [ - It looks like you're using a version of react-dom that supports the "act" function, but not an awaitable version of "act" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning., - ], - ] - `) -}) - -test('async act should not show an error when ReactTestUtils.act returns something', async () => { - jest.resetModules() - jest.mock('react-dom/test-utils', () => ({ - act: () => { - return new Promise(resolve => { - console.error( - 'Warning: The callback passed to ReactTestUtils.act(...) function must not return anything', - ) - resolve() - }) - }, - })) - asyncAct = require('../act-compat').asyncAct - await asyncAct(async () => { - await null - }) - - expect(console.error).toHaveBeenCalledTimes(0) -}) - -/* eslint no-console:0 */ diff --git a/src/__tests__/render.js b/src/__tests__/render.js index fea1a649..88e2b98d 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -1,6 +1,13 @@ import * as React from 'react' import ReactDOM from 'react-dom' -import {render, screen} from '../' +import ReactDOMServer from 'react-dom/server' +import {fireEvent, render, screen} from '../' + +afterEach(() => { + if (console.error.mockRestore !== undefined) { + console.error.mockRestore() + } +}) test('renders div into document', () => { const ref = React.createRef() @@ -101,3 +108,90 @@ test('flushes useEffect cleanup functions sync on unmount()', () => { expect(spy).toHaveBeenCalledTimes(1) }) + +test('can be called multiple times on the same container', () => { + const container = document.createElement('div') + + const {unmount} = render(, {container}) + + expect(container).toContainHTML('') + + render(, {container}) + + expect(container).toContainHTML('') + + unmount() + + expect(container).toBeEmptyDOMElement() +}) + +test('hydrate will make the UI interactive', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}) + function App() { + const [clicked, handleClick] = React.useReducer(n => n + 1, 0) + + return ( + + ) + } + const ui = + const container = document.createElement('div') + document.body.appendChild(container) + container.innerHTML = ReactDOMServer.renderToString(ui) + + expect(container).toHaveTextContent('clicked:0') + + render(ui, {container, hydrate: true}) + + expect(console.error).not.toHaveBeenCalled() + + fireEvent.click(container.querySelector('button')) + + expect(container).toHaveTextContent('clicked:1') +}) + +test('hydrate can have a wrapper', () => { + const wrapperComponentMountEffect = jest.fn() + function WrapperComponent({children}) { + React.useEffect(() => { + wrapperComponentMountEffect() + }) + + return children + } + const ui = + const container = document.createElement('div') + document.body.appendChild(container) + container.innerHTML = ReactDOMServer.renderToString(ui) + + render(ui, {container, hydrate: true, wrapper: WrapperComponent}) + + expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(1) +}) + +test('legacyRoot uses legacy ReactDOM.render', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}) + render(, {legacyRoot: true}) + + expect(console.error).toHaveBeenCalledTimes(1) + expect(console.error).toHaveBeenNthCalledWith( + 1, + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ) +}) + +test('legacyRoot uses legacy ReactDOM.hydrate', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}) + const ui = + const container = document.createElement('div') + container.innerHTML = ReactDOMServer.renderToString(ui) + render(ui, {container, hydrate: true, legacyRoot: true}) + + expect(console.error).toHaveBeenCalledTimes(1) + expect(console.error).toHaveBeenNthCalledWith( + 1, + "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ) +}) diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js index 400fce10..eeaf395c 100644 --- a/src/__tests__/stopwatch.js +++ b/src/__tests__/stopwatch.js @@ -53,8 +53,5 @@ test('unmounts a component', async () => { // and get an error. await sleep(5) // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalledTimes( - // ReactDOM.render is deprecated in React 18 - React.version.startsWith('18') ? 1 : 0, - ) + expect(console.error).not.toHaveBeenCalled() }) diff --git a/src/act-compat.js b/src/act-compat.js index 40ecdab9..d7a09d68 100644 --- a/src/act-compat.js +++ b/src/act-compat.js @@ -1,135 +1,85 @@ -import * as React from 'react' -import ReactDOM from 'react-dom' import * as testUtils from 'react-dom/test-utils' -const reactAct = testUtils.act -const actSupported = reactAct !== undefined +const domAct = testUtils.act -// act is supported react-dom@16.8.0 -// so for versions that don't have act from test utils -// we do this little polyfill. No warnings, but it's -// better than nothing. -function actPolyfill(cb) { - ReactDOM.unstable_batchedUpdates(cb) - ReactDOM.render(, document.createElement('div')) +function getGlobalThis() { + /* istanbul ignore else */ + if (typeof self !== 'undefined') { + return self + } + /* istanbul ignore next */ + if (typeof window !== 'undefined') { + return window + } + /* istanbul ignore next */ + if (typeof global !== 'undefined') { + return global + } + /* istanbul ignore next */ + throw new Error('unable to locate global object') } -const act = reactAct || actPolyfill +function setIsReactActEnvironment(isReactActEnvironment) { + getGlobalThis().IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment +} -let youHaveBeenWarned = false -let isAsyncActSupported = null +function getIsReactActEnvironment() { + return getGlobalThis().IS_REACT_ACT_ENVIRONMENT +} -function asyncAct(cb) { - if (actSupported === true) { - if (isAsyncActSupported === null) { - return new Promise((resolve, reject) => { - // patch console.error here - const originalConsoleError = console.error - console.error = function error(...args) { - /* if console.error fired *with that specific message* */ - /* istanbul ignore next */ - const firstArgIsString = typeof args[0] === 'string' - if ( - firstArgIsString && - args[0].indexOf( - 'Warning: Do not await the result of calling ReactTestUtils.act', - ) === 0 - ) { - // v16.8.6 - isAsyncActSupported = false - } else if ( - firstArgIsString && - args[0].indexOf( - 'Warning: The callback passed to ReactTestUtils.act(...) function must not return anything', - ) === 0 - ) { - // no-op - } else { - originalConsoleError.apply(console, args) - } +function withGlobalActEnvironment(actImplementation) { + return callback => { + const previousActEnvironment = getIsReactActEnvironment() + setIsReactActEnvironment(true) + try { + // The return value of `act` is always a thenable. + let callbackNeedsToBeAwaited = false + const actResult = actImplementation(() => { + const result = callback() + if ( + result !== null && + typeof result === 'object' && + typeof result.then === 'function' + ) { + callbackNeedsToBeAwaited = true } - let cbReturn, result - try { - result = reactAct(() => { - cbReturn = cb() - return cbReturn - }) - } catch (err) { - console.error = originalConsoleError - reject(err) - return - } - - result.then( - () => { - console.error = originalConsoleError - // if it got here, it means async act is supported - isAsyncActSupported = true - resolve() - }, - err => { - console.error = originalConsoleError - isAsyncActSupported = true - reject(err) - }, - ) - - // 16.8.6's act().then() doesn't call a resolve handler, so we need to manually flush here, sigh - - if (isAsyncActSupported === false) { - console.error = originalConsoleError - /* istanbul ignore next */ - if (!youHaveBeenWarned) { - // if act is supported and async act isn't and they're trying to use async - // act, then they need to upgrade from 16.8 to 16.9. - // This is a seamless upgrade, so we'll add a warning - console.error( - `It looks like you're using a version of react-dom that supports the "act" function, but not an awaitable version of "act" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning.`, + return result + }) + if (callbackNeedsToBeAwaited) { + const thenable = actResult + return { + then: (resolve, reject) => { + thenable.then( + returnValue => { + setIsReactActEnvironment(previousActEnvironment) + resolve(returnValue) + }, + error => { + setIsReactActEnvironment(previousActEnvironment) + reject(error) + }, ) - youHaveBeenWarned = true - } - - cbReturn.then(() => { - // a faux-version. - // todo - copy https://github.com/facebook/react/blob/master/packages/shared/enqueueTask.js - Promise.resolve().then(() => { - // use sync act to flush effects - act(() => {}) - resolve() - }) - }, reject) + }, } - }) - } else if (isAsyncActSupported === false) { - // use the polyfill directly - let result - act(() => { - result = cb() - }) - return result.then(() => { - return Promise.resolve().then(() => { - // use sync act to flush effects - act(() => {}) - }) - }) + } else { + setIsReactActEnvironment(previousActEnvironment) + return actResult + } + } catch (error) { + // Can't be a `finally {}` block since we don't know if we have to immediately restore IS_REACT_ACT_ENVIRONMENT + // or if we have to await the callback first. + setIsReactActEnvironment(previousActEnvironment) + throw error } - // all good! regular act - return act(cb) } - // use the polyfill - let result - act(() => { - result = cb() - }) - return result.then(() => { - return Promise.resolve().then(() => { - // use sync act to flush effects - act(() => {}) - }) - }) } +const act = withGlobalActEnvironment(domAct) + export default act -export {asyncAct} +export { + setIsReactActEnvironment as setReactActEnvironment, + getIsReactActEnvironment, +} /* eslint no-console:0 */ diff --git a/src/index.js b/src/index.js index 96fbe155..bb0d0270 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ +import {getIsReactActEnvironment, setReactActEnvironment} from './act-compat' import {cleanup} from './pure' // if we're running in a test runner that supports afterEach @@ -20,6 +21,21 @@ if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) { cleanup() }) } + + // No test setup with other test runners available + /* istanbul ignore else */ + if (typeof beforeAll === 'function' && typeof afterAll === 'function') { + // This matches the behavior of React < 18. + let previousIsReactActEnvironment = getIsReactActEnvironment() + beforeAll(() => { + previousIsReactActEnvironment = getIsReactActEnvironment() + setReactActEnvironment(true) + }) + + afterAll(() => { + setReactActEnvironment(previousIsReactActEnvironment) + }) + } } export * from './pure' diff --git a/src/pure.js b/src/pure.js index 75098f78..64b761b0 100644 --- a/src/pure.js +++ b/src/pure.js @@ -1,20 +1,32 @@ import * as React from 'react' import ReactDOM from 'react-dom' +import * as ReactDOMClient from 'react-dom/client' import { getQueriesForElement, prettyDOM, configure as configureDTL, } from '@testing-library/dom' -import act, {asyncAct} from './act-compat' +import act, { + getIsReactActEnvironment, + setReactActEnvironment, +} from './act-compat' import {fireEvent} from './fire-event' configureDTL({ + unstable_advanceTimersWrapper: cb => { + return act(cb) + }, + // We just want to run `waitFor` without IS_REACT_ACT_ENVIRONMENT + // But that's not necessarily how `asyncWrapper` is used since it's a public method. + // Let's just hope nobody else is using it. asyncWrapper: async cb => { - let result - await asyncAct(async () => { - result = await cb() - }) - return result + const previousActEnvironment = getIsReactActEnvironment() + setReactActEnvironment(false) + try { + return await cb() + } finally { + setReactActEnvironment(previousActEnvironment) + } }, eventWrapper: cb => { let result @@ -25,32 +37,70 @@ configureDTL({ }, }) +// Ideally we'd just use a WeakMap where containers are keys and roots are values. +// We use two variables so that we can bail out in constant time when we render with a new container (most common use case) +/** + * @type {Set } + */ const mountedContainers = new Set() +/** + * @type Array<{container: import('react-dom').Container, root: ReturnType }> + */ +const mountedRootEntries = [] -function render( - ui, - { - container, - baseElement = container, - queries, - hydrate = false, - wrapper: WrapperComponent, - } = {}, +function createConcurrentRoot( + container, + {hydrate, ui, wrapper: WrapperComponent}, ) { - if (!baseElement) { - // default to document.body instead of documentElement to avoid output of potentially-large - // head elements (such as JSS style blocks) in debug output - baseElement = document.body + let root + if (hydrate) { + act(() => { + root = ReactDOMClient.hydrateRoot( + container, + WrapperComponent ? React.createElement(WrapperComponent, null, ui) : ui, + ) + }) + } else { + root = ReactDOMClient.createRoot(container) } - if (!container) { - container = baseElement.appendChild(document.createElement('div')) + + return { + hydrate() { + /* istanbul ignore if */ + if (!hydrate) { + throw new Error( + 'Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.', + ) + } + // Nothing to do since hydration happens when creating the root object. + }, + render(element) { + root.render(element) + }, + unmount() { + root.unmount() + }, } +} - // we'll add it to the mounted containers regardless of whether it's actually - // added to document.body so the cleanup method works regardless of whether - // they're passing us a custom container or not. - mountedContainers.add(container) +function createLegacyRoot(container) { + return { + hydrate(element) { + ReactDOM.hydrate(element, container) + }, + render(element) { + ReactDOM.render(element, container) + }, + unmount() { + ReactDOM.unmountComponentAtNode(container) + }, + } +} +function renderRoot( + ui, + {baseElement, container, hydrate, queries, root, wrapper: WrapperComponent}, +) { const wrapUiIfNeeded = innerElement => WrapperComponent ? React.createElement(WrapperComponent, null, innerElement) @@ -58,9 +108,9 @@ function render( act(() => { if (hydrate) { - ReactDOM.hydrate(wrapUiIfNeeded(ui), container) + root.hydrate(wrapUiIfNeeded(ui), container) } else { - ReactDOM.render(wrapUiIfNeeded(ui), container) + root.render(wrapUiIfNeeded(ui), container) } }) @@ -75,11 +125,15 @@ function render( console.log(prettyDOM(el, maxLength, options)), unmount: () => { act(() => { - ReactDOM.unmountComponentAtNode(container) + root.unmount() }) }, rerender: rerenderUi => { - render(wrapUiIfNeeded(rerenderUi), {container, baseElement}) + renderRoot(wrapUiIfNeeded(rerenderUi), { + container, + baseElement, + root, + }) // Intentionally do not return anything to avoid unnecessarily complicating the API. // folks can use all the same utilities we return in the first place that are bound to the container }, @@ -99,28 +153,73 @@ function render( } } -function cleanup() { - mountedContainers.forEach(cleanupAtContainer) +function render( + ui, + { + container, + baseElement = container, + legacyRoot = false, + queries, + hydrate = false, + wrapper, + } = {}, +) { + if (!baseElement) { + // default to document.body instead of documentElement to avoid output of potentially-large + // head elements (such as JSS style blocks) in debug output + baseElement = document.body + } + if (!container) { + container = baseElement.appendChild(document.createElement('div')) + } + + let root + // eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first. + if (!mountedContainers.has(container)) { + const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot + root = createRootImpl(container, {hydrate, ui, wrapper}) + + mountedRootEntries.push({container, root}) + // we'll add it to the mounted containers regardless of whether it's actually + // added to document.body so the cleanup method works regardless of whether + // they're passing us a custom container or not. + mountedContainers.add(container) + } else { + mountedRootEntries.forEach(rootEntry => { + // Else is unreachable since `mountedContainers` has the `container`. + // Only reachable if one would accidentally add the container to `mountedContainers` but not the root to `mountedRootEntries` + /* istanbul ignore else */ + if (rootEntry.container === container) { + root = rootEntry.root + } + }) + } + + return renderRoot(ui, { + container, + baseElement, + queries, + hydrate, + wrapper, + root, + }) } -// maybe one day we'll expose this (perhaps even as a utility returned by render). -// but let's wait until someone asks for it. -function cleanupAtContainer(container) { - act(() => { - ReactDOM.unmountComponentAtNode(container) +function cleanup() { + mountedRootEntries.forEach(({root, container}) => { + act(() => { + root.unmount() + }) + if (container.parentNode === document.body) { + document.body.removeChild(container) + } }) - if (container.parentNode === document.body) { - document.body.removeChild(container) - } - mountedContainers.delete(container) + mountedRootEntries.length = 0 + mountedContainers.clear() } // just re-export everything from dom-testing-library export * from '@testing-library/dom' export {render, cleanup, act, fireEvent} -// NOTE: we're not going to export asyncAct because that's our own compatibility -// thing for people using react-dom@16.8.0. Anyone else doesn't need it and -// people should just upgrade anyway. - /* eslint func-name-matching:0 */ diff --git a/tests/setup-env.js b/tests/setup-env.js index 6c0b953b..264828a9 100644 --- a/tests/setup-env.js +++ b/tests/setup-env.js @@ -1,20 +1 @@ import '@testing-library/jest-dom/extend-expect' - -let consoleErrorMock - -beforeEach(() => { - const originalConsoleError = console.error - consoleErrorMock = jest - .spyOn(console, 'error') - .mockImplementation((message, ...optionalParams) => { - // Ignore ReactDOM.render/ReactDOM.hydrate deprecation warning - if (message.indexOf('Use createRoot instead.') !== -1) { - return - } - originalConsoleError(message, ...optionalParams) - }) -}) - -afterEach(() => { - consoleErrorMock.mockRestore() -}) diff --git a/types/index.d.ts b/types/index.d.ts index 604b3966..a9bfa279 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -60,6 +60,11 @@ export interface RenderOptions< * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) */ hydrate?: boolean + /** + * Set to `true` if you want to force synchronous `ReactDOM.render`. + * Otherwise `render` will default to concurrent React if available. + */ + legacyRoot?: boolean /** * Queries to bind. Overrides the default set from DOM Testing Library unless merged. * From 93bc2c8afc8a7988ef9b4f5cb7f4101a2400735d Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 11 Apr 2022 19:59:54 +0200 Subject: [PATCH 063/126] test(types): Don't assume implicit children (#1042) * test(types): Don't assume implicit children * format --- types/test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/types/test.tsx b/types/test.tsx index eae6e81f..a8a7c7ae 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -129,6 +129,7 @@ export function wrappedRenderC( options?: pure.RenderOptions, ) { interface AppWrapperProps { + children?: React.ReactNode userProviderProps?: {user: string} } const AppWrapperProps: React.FunctionComponent = ({ From 2a889e80658ce93882c5ba253ea65f5542ece2d0 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 11 Apr 2022 11:03:31 -0700 Subject: [PATCH 064/126] fix: Specify a non-* version for @types/react-dom (#1040) fixes https://github.com/testing-library/react-testing-library/issues/1039 Co-authored-by: Sebastian Silbermann --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4781e962..8d7c629b 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^8.5.0", - "@types/react-dom": "*" + "@types/react-dom": "^18.0.0" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", From c8c93f83228a68a270583c139972e79b1812b7d3 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 20:06:24 +0200 Subject: [PATCH 065/126] docs: add Nokel81 as a contributor for bug, code (#1043) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 10 ++++++++++ README.md | 1 + 2 files changed, 11 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index df9690ed..0eb9e2a5 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1307,6 +1307,16 @@ "code", "bug" ] + }, + { + "login": "Nokel81", + "name": "Sebastian Malton", + "avatar_url": "https://avatars.githubusercontent.com/u/8225332?v=4", + "profile": "https://github.com/Nokel81", + "contributions": [ + "bug", + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 0bc06ceb..ea31e3f8 100644 --- a/README.md +++ b/README.md @@ -615,6 +615,7 @@ Thanks goes to these people ([emoji key][emojis]):
Marcos GΓ³mez
π
Akash Shyam
π+
Fabian Meumertzheim
π» πFrom 2c451b346815b30dace8a5f7b2ed6a78d17f47cc Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann
Sebastian Malton
π π»Date: Wed, 13 Apr 2022 16:38:43 +0200 Subject: [PATCH 066/126] chore: Run release from 12.x branch (#1044) (#1045) --- .github/workflows/validate.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 45cc7d13..7e95b942 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -2,7 +2,9 @@ name: validate on: push: branches: - - '+([0-9])?(.{+([0-9]),x}).x' + # Match SemVer major release branches + # e.g. "12.x" or "8.x" + - '[0-9]+.x' - 'main' - 'next' - 'next-major' @@ -61,8 +63,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.repository == 'testing-library/react-testing-library' && - contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', - github.ref) && github.event_name == 'push' }} + github.event_name == 'push' }} steps: - name: π Cancel Previous Runs uses: styfle/cancel-workflow-action@0.9.0 From 9535eff82ada685c410b3b25ef3e2313ea3a86aa Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Fri, 15 Apr 2022 20:55:24 +0200 Subject: [PATCH 067/126] feat: Add `renderHook` (#991) Co-authored-by: Michael Peyper Co-authored-by: Kent C. Dodds --- src/__tests__/renderHook.js | 62 +++++++++++++++++++++++++++++++++++++ src/pure.js | 30 +++++++++++++++++- types/index.d.ts | 46 +++++++++++++++++++++++++++ types/test.tsx | 25 ++++++++++++++- 4 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/renderHook.js diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js new file mode 100644 index 00000000..fd6b95a4 --- /dev/null +++ b/src/__tests__/renderHook.js @@ -0,0 +1,62 @@ +import React from 'react' +import {renderHook} from '../pure' + +test('gives comitted result', () => { + const {result} = renderHook(() => { + const [state, setState] = React.useState(1) + + React.useEffect(() => { + setState(2) + }, []) + + return [state, setState] + }) + + expect(result.current).toEqual([2, expect.any(Function)]) +}) + +test('allows rerendering', () => { + const {result, rerender} = renderHook( + ({branch}) => { + const [left, setLeft] = React.useState('left') + const [right, setRight] = React.useState('right') + + // eslint-disable-next-line jest/no-if + switch (branch) { + case 'left': + return [left, setLeft] + case 'right': + return [right, setRight] + + default: + throw new Error( + 'No Props passed. This is a bug in the implementation', + ) + } + }, + {initialProps: {branch: 'left'}}, + ) + + expect(result.current).toEqual(['left', expect.any(Function)]) + + rerender({branch: 'right'}) + + expect(result.current).toEqual(['right', expect.any(Function)]) +}) + +test('allows wrapper components', async () => { + const Context = React.createContext('default') + function Wrapper({children}) { + return {children} + } + const {result} = renderHook( + () => { + return React.useContext(Context) + }, + { + wrapper: Wrapper, + }, + ) + + expect(result.current).toEqual('provided') +}) diff --git a/src/pure.js b/src/pure.js index 64b761b0..4c416d44 100644 --- a/src/pure.js +++ b/src/pure.js @@ -218,8 +218,36 @@ function cleanup() { mountedContainers.clear() } +function renderHook(renderCallback, options = {}) { + const {initialProps, wrapper} = options + const result = React.createRef() + + function TestComponent({renderCallbackProps}) { + const pendingResult = renderCallback(renderCallbackProps) + + React.useEffect(() => { + result.current = pendingResult + }) + + return null + } + + const {rerender: baseRerender, unmount} = render( +, + {wrapper}, + ) + + function rerender(rerenderCallbackProps) { + return baseRerender( + , + ) + } + + return {result, rerender, unmount} +} + // just re-export everything from dom-testing-library export * from '@testing-library/dom' -export {render, cleanup, act, fireEvent} +export {render, renderHook, cleanup, act, fireEvent} /* eslint func-name-matching:0 */ diff --git a/types/index.d.ts b/types/index.d.ts index a9bfa279..fda03e5b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -98,6 +98,52 @@ export function render( options?: Omit , ): RenderResult +interface RenderHookResult { + /** + * Triggers a re-render. The props will be passed to your renderHook callback. + */ + rerender: (props?: Props) => void + /** + * This is a stable reference to the latest value returned by your renderHook + * callback + */ + result: { + /** + * The value returned by your renderHook callback + */ + current: Result + } + /** + * Unmounts the test component. This is useful for when you need to test + * any cleanup your useEffects have. + */ + unmount: () => void +} + +interface RenderHookOptions { + /** + * The argument passed to the renderHook callback. Can be useful if you plan + * to use the rerender utility to change the values passed to your hook. + */ + initialProps?: Props + /** + * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating + * reusable custom render functions for common data providers. See setup for examples. + * + * @see https://testing-library.com/docs/react-testing-library/api/#wrapper + */ + wrapper?: React.JSXElementConstructor<{children: React.ReactElement}> +} + +/** + * Allows you to render a hook within a test React component without having to + * create that component yourself. + */ +export function renderHook ( + render: (initialProps: Props) => Result, + options?: RenderHookOptions , +): RenderHookResult + /** * Unmounts React trees that were mounted with render. */ diff --git a/types/test.tsx b/types/test.tsx index a8a7c7ae..17ba7012 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import {render, fireEvent, screen, waitFor} from '.' +import {render, fireEvent, screen, waitFor, renderHook} from '.' import * as pure from './pure' export async function testRender() { @@ -161,6 +161,29 @@ export function testBaseElement() { ) } +export function testRenderHook() { + const {result, rerender, unmount} = renderHook(() => React.useState(2)[0]) + + expectType (result.current) + + rerender() + + unmount() +} + +export function testRenderHookProps() { + const {result, rerender, unmount} = renderHook( + ({defaultValue}) => React.useState(defaultValue)[0], + {initialProps: {defaultValue: 2}}, + ) + + expectType (result.current) + + rerender() + + unmount() +} + /* eslint testing-library/prefer-explicit-assert: "off", From 9171163fccf0a7ea43763475ca2980898b4079a5 Mon Sep 17 00:00:00 2001 From: Andrew Hummel Date: Fri, 15 Apr 2022 17:07:31 -0500 Subject: [PATCH 068/126] fix(TS): export interface RenderHookResult (#1049) --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index fda03e5b..1948114f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -98,7 +98,7 @@ export function render( options?: Omit , ): RenderResult -interface RenderHookResult { +export interface RenderHookResult { /** * Triggers a re-render. The props will be passed to your renderHook callback. */ From 46b28ade730f97a49a253d630f5b97c17ff24f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20B=C3=B6ttcher?= Date: Tue, 3 May 2022 20:34:37 +0200 Subject: [PATCH 069/126] feat: Export RenderHookOptions type (#1062) --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 1948114f..e3f5bc60 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -120,7 +120,7 @@ export interface RenderHookResult { unmount: () => void } -interface RenderHookOptions { +export interface RenderHookOptions { /** * The argument passed to the renderHook callback. Can be useful if you plan * to use the rerender utility to change the values passed to your hook. From 00c89dce86585d6a163c383a05abaf5a7f646bf6 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 3 May 2022 20:36:41 +0200 Subject: [PATCH 070/126] docs: add mboettcher as a contributor for code (#1063) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0eb9e2a5..e267d285 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1317,6 +1317,15 @@ "bug", "code" ] + }, + { + "login": "mboettcher", + "name": "Martin BΓΆttcher", + "avatar_url": "https://avatars.githubusercontent.com/u/2325337?v=4", + "profile": "https://github.com/mboettcher", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ea31e3f8..9992250a 100644 --- a/README.md +++ b/README.md @@ -616,6 +616,7 @@ Thanks goes to these people ([emoji key][emojis]):
Akash Shyam
π
Fabian Meumertzheim
π» π+
Sebastian Malton
π π»From f176285e4e92754751b708e1b1adf1f38edea6a8 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann
Martin BΓΆttcher
π»Date: Tue, 17 May 2022 20:51:03 +0200 Subject: [PATCH 071/126] chore: Run with latest Node 16 again (#1071) --- .github/workflows/validate.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 7e95b942..9379216c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -20,8 +20,7 @@ jobs: strategy: fail-fast: false matrix: - # TODO: relax `'16.9.1'` to `16` once GitHub has 16.9.1 cached. 16.9.0 is broken due to https://github.com/nodejs/node/issues/40030 - node: [12, 14, '16.9.1'] + node: [12, 14, 16] react: [latest, next, experimental] runs-on: ubuntu-latest steps: From c80809a956b0b9f3289c4a6fa8b5e8cc72d6ef6d Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sat, 28 May 2022 10:18:06 +0200 Subject: [PATCH 072/126] feat: Use `globalThis` if available (#1070) --- package.json | 3 +++ src/act-compat.js | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/package.json b/package.json index 8d7c629b..4cba00fd 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,9 @@ }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", + "globals": { + "globalThis": "readonly" + }, "rules": { "react/prop-types": "off", "react/no-adjacent-inline-elements": "off", diff --git a/src/act-compat.js b/src/act-compat.js index d7a09d68..86518196 100644 --- a/src/act-compat.js +++ b/src/act-compat.js @@ -4,6 +4,10 @@ const domAct = testUtils.act function getGlobalThis() { /* istanbul ignore else */ + if (typeof globalThis !== 'undefined') { + return globalThis + } + /* istanbul ignore next */ if (typeof self !== 'undefined') { return self } From 73ee9ba13cb4b337f06e2ed61099d6af9a4968da Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Mon, 22 Aug 2022 12:40:43 +0300 Subject: [PATCH 073/126] test: Correct a typo in test name (#1112) --- src/__tests__/renderHook.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js index fd6b95a4..b65d67a2 100644 --- a/src/__tests__/renderHook.js +++ b/src/__tests__/renderHook.js @@ -1,7 +1,7 @@ import React from 'react' import {renderHook} from '../pure' -test('gives comitted result', () => { +test('gives committed result', () => { const {result} = renderHook(() => { const [state, setState] = React.useState(1) From 27a9584629e28339b9961edefbb2134d7c570678 Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Sun, 4 Sep 2022 18:47:54 +0200 Subject: [PATCH 074/126] feat(renderHook): allow passing of all render options to renderHook (#1118) --- src/__tests__/renderHook.js | 26 ++++++++++++++++++++++++++ src/pure.js | 4 ++-- types/index.d.ts | 24 ++++++++++++++---------- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js index b65d67a2..92bc47ed 100644 --- a/src/__tests__/renderHook.js +++ b/src/__tests__/renderHook.js @@ -60,3 +60,29 @@ test('allows wrapper components', async () => { expect(result.current).toEqual('provided') }) + +test('legacyRoot uses legacy ReactDOM.render', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}) + + const Context = React.createContext('default') + function Wrapper({children}) { + return {children} + } + const {result} = renderHook( + () => { + return React.useContext(Context) + }, + { + wrapper: Wrapper, + legacyRoot: true, + }, + ) + + expect(result.current).toEqual('provided') + + expect(console.error).toHaveBeenCalledTimes(1) + expect(console.error).toHaveBeenNthCalledWith( + 1, + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ) +}) diff --git a/src/pure.js b/src/pure.js index 4c416d44..94b3b2bd 100644 --- a/src/pure.js +++ b/src/pure.js @@ -219,7 +219,7 @@ function cleanup() { } function renderHook(renderCallback, options = {}) { - const {initialProps, wrapper} = options + const {initialProps, ...renderOptions} = options const result = React.createRef() function TestComponent({renderCallbackProps}) { @@ -234,7 +234,7 @@ function renderHook(renderCallback, options = {}) { const {rerender: baseRerender, unmount} = render(, - {wrapper}, + renderOptions, ) function rerender(rerenderCallbackProps) { diff --git a/types/index.d.ts b/types/index.d.ts index e3f5bc60..558edfad 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -120,28 +120,32 @@ export interface RenderHookResult { unmount: () => void } -export interface RenderHookOptions { +export interface RenderHookOptions< + Props, + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +> extends RenderOptions { /** * The argument passed to the renderHook callback. Can be useful if you plan * to use the rerender utility to change the values passed to your hook. */ initialProps?: Props - /** - * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating - * reusable custom render functions for common data providers. See setup for examples. - * - * @see https://testing-library.com/docs/react-testing-library/api/#wrapper - */ - wrapper?: React.JSXElementConstructor<{children: React.ReactElement}> } /** * Allows you to render a hook within a test React component without having to * create that component yourself. */ -export function renderHook( +export function renderHook< + Result, + Props, + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +>( render: (initialProps: Props) => Result, - options?: RenderHookOptions , + options?: RenderHookOptions , ): RenderHookResult /** From bef9e07c1743affa6fca459fda5ab5b488ccd9bf Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 4 Sep 2022 18:48:26 +0200 Subject: [PATCH 075/126] docs: add TkDodo as a contributor for code (#1119) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index e267d285..2d451b71 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1326,6 +1326,15 @@ "contributions": [ "code" ] + }, + { + "login": "TkDodo", + "name": "Dominik Dorfmeister", + "avatar_url": "https://avatars.githubusercontent.com/u/1021430?v=4", + "profile": "http://tkdodo.eu", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 9992250a..bbe8d94e 100644 --- a/README.md +++ b/README.md @@ -617,6 +617,7 @@ Thanks goes to these people ([emoji key][emojis]):
Fabian Meumertzheim
π» π
Sebastian Malton
π π»+
Martin BΓΆttcher
π»From 7c7dc785501f2e75cbcb5d49df78340914dfba8c Mon Sep 17 00:00:00 2001 From: Stephen Sauceda
Dominik Dorfmeister
π»Date: Sat, 1 Oct 2022 15:20:38 -0400 Subject: [PATCH 076/126] docs: acknowledge peer dependency requirements (#1131) --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index bbe8d94e..8704fa2b 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,16 @@ yarn add --dev @testing-library/react This library has `peerDependencies` listings for `react` and `react-dom`. +_React Testing Library versions 13+ require React v18. If your project uses an +older version of React, be sure to install version 12:_ + +``` +npm install --save-dev @testing-library/react@12 + + +yarn add --dev @testing-library/react@12 +``` + You may also be interested in installing `@testing-library/jest-dom` so you can use [the custom jest matchers](https://github.com/testing-library/jest-dom). From bca9bf8bca1dfb9655980801838fb851d0ef8763 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 1 Oct 2022 21:23:18 +0200 Subject: [PATCH 077/126] add stephensauceda as a contributor for doc (#1132) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 ++ README.md | 364 ++++++++++++++++++++++---------------------- 2 files changed, 194 insertions(+), 179 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 2d451b71..270dd6a0 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1335,6 +1335,15 @@ "contributions": [ "code" ] + }, + { + "login": "stephensauceda", + "name": "Stephen Sauceda", + "avatar_url": "https://avatars.githubusercontent.com/u/1017723?v=4", + "profile": "https://stephensauceda.com", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 8704fa2b..45324901 100644 --- a/README.md +++ b/README.md @@ -450,185 +450,191 @@ Thanks goes to these people ([emoji key][emojis]): Date: Wed, 5 Oct 2022 21:32:52 +0300 Subject: [PATCH 078/126] Update outdated LICENSE year (#1133) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 4c43675b..ca399d57 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ The MIT License (MIT) -Copyright (c) 2017 Kent C. Dodds +Copyright (c) 2017-Present Kent C. Dodds Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 185e3142a320908fc2a707c7aba815444abf675c Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sat, 8 Oct 2022 13:22:18 +0200 Subject: [PATCH 079/126] test: Add Node.js 18.x to test matrix (#1138) --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9379216c..ad4adccf 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 16] + node: [12, 14, 16, 18] react: [latest, next, experimental] runs-on: ubuntu-latest steps: From 801ad37ac79caced867aa05931b914035c6b527a Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 6 Dec 2022 21:25:35 +0100 Subject: [PATCH 080/126] test: Fail on unexpected console.warn and console.error (#1139) --- package.json | 2 + src/__tests__/cleanup.js | 1 + src/__tests__/new-act.js | 2 +- src/__tests__/render.js | 37 ++-- src/__tests__/renderHook.js | 33 ++- tests/failOnUnexpectedConsoleCalls.js | 129 +++++++++++ tests/setup-env.js | 1 + tests/shouldIgnoreConsoleError.js | 43 ++++ tests/toWarnDev.js | 303 ++++++++++++++++++++++++++ 9 files changed, 510 insertions(+), 41 deletions(-) create mode 100644 tests/failOnUnexpectedConsoleCalls.js create mode 100644 tests/shouldIgnoreConsoleError.js create mode 100644 tests/toWarnDev.js diff --git a/package.json b/package.json index 4cba00fd..d2dd6a97 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,9 @@ }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", + "chalk": "^4.1.2", "dotenv-cli": "^4.0.0", + "jest-diff": "^27.5.1", "kcd-scripts": "^11.1.0", "npm-run-all": "^4.1.5", "react": "^18.0.0", diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js index 0dcbac12..4517c098 100644 --- a/src/__tests__/cleanup.js +++ b/src/__tests__/cleanup.js @@ -51,6 +51,7 @@ describe('fake timers and missing act warnings', () => { }) afterEach(() => { + jest.restoreAllMocks() jest.useRealTimers() }) diff --git a/src/__tests__/new-act.js b/src/__tests__/new-act.js index 05f9d45a..4909d4a6 100644 --- a/src/__tests__/new-act.js +++ b/src/__tests__/new-act.js @@ -13,7 +13,7 @@ beforeEach(() => { }) afterEach(() => { - console.error.mockRestore() + jest.restoreAllMocks() }) test('async act works when it does not exist (older versions of react)', async () => { diff --git a/src/__tests__/render.js b/src/__tests__/render.js index 88e2b98d..46925f49 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -3,12 +3,6 @@ import ReactDOM from 'react-dom' import ReactDOMServer from 'react-dom/server' import {fireEvent, render, screen} from '../' -afterEach(() => { - if (console.error.mockRestore !== undefined) { - console.error.mockRestore() - } -}) - test('renders div into document', () => { const ref = React.createRef() const {container} = render() @@ -126,7 +120,6 @@ test('can be called multiple times on the same container', () => { }) test('hydrate will make the UI interactive', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}) function App() { const [clicked, handleClick] = React.useReducer(n => n + 1, 0) @@ -145,8 +138,6 @@ test('hydrate will make the UI interactive', () => { render(ui, {container, hydrate: true}) - expect(console.error).not.toHaveBeenCalled() - fireEvent.click(container.querySelector('button')) expect(container).toHaveTextContent('clicked:1') @@ -172,26 +163,26 @@ test('hydrate can have a wrapper', () => { }) test('legacyRoot uses legacy ReactDOM.render', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}) - render(, {legacyRoot: true}) - - expect(console.error).toHaveBeenCalledTimes(1) - expect(console.error).toHaveBeenNthCalledWith( - 1, - "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + expect(() => { + render(, {legacyRoot: true}) + }).toErrorDev( + [ + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], + {withoutStack: true}, ) }) test('legacyRoot uses legacy ReactDOM.hydrate', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}) const ui = const container = document.createElement('div') container.innerHTML = ReactDOMServer.renderToString(ui) - render(ui, {container, hydrate: true, legacyRoot: true}) - - expect(console.error).toHaveBeenCalledTimes(1) - expect(console.error).toHaveBeenNthCalledWith( - 1, - "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + expect(() => { + render(ui, {container, hydrate: true, legacyRoot: true}) + }).toErrorDev( + [ + "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], + {withoutStack: true}, ) }) diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js index 92bc47ed..f6b7a343 100644 --- a/src/__tests__/renderHook.js +++ b/src/__tests__/renderHook.js @@ -62,27 +62,26 @@ test('allows wrapper components', async () => { }) test('legacyRoot uses legacy ReactDOM.render', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}) - const Context = React.createContext('default') function Wrapper({children}) { return {children} } - const {result} = renderHook( - () => { - return React.useContext(Context) - }, - { - wrapper: Wrapper, - legacyRoot: true, - }, + let result + expect(() => { + result = renderHook( + () => { + return React.useContext(Context) + }, + { + wrapper: Wrapper, + legacyRoot: true, + }, + ).result + }).toErrorDev( + [ + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], + {withoutStack: true}, ) - expect(result.current).toEqual('provided') - - expect(console.error).toHaveBeenCalledTimes(1) - expect(console.error).toHaveBeenNthCalledWith( - 1, - "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", - ) }) diff --git a/tests/failOnUnexpectedConsoleCalls.js b/tests/failOnUnexpectedConsoleCalls.js new file mode 100644 index 00000000..83e0c641 --- /dev/null +++ b/tests/failOnUnexpectedConsoleCalls.js @@ -0,0 +1,129 @@ +// Fork of https://github.com/facebook/react/blob/513417d6951fa3ff5729302b7990b84604b11afa/scripts/jest/setupTests.js#L71-L161 +/** +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +/* eslint-disable prefer-template */ +/* eslint-disable func-names */ +const util = require('util') +const chalk = require('chalk') +const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError') + +const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => { + const newMethod = function (format, ...args) { + // Ignore uncaught errors reported by jsdom + // and React addendums because they're too noisy. + if (methodName === 'error' && shouldIgnoreConsoleError(format, args)) { + return + } + + // Capture the call stack now so we can warn about it later. + // The call stack has helpful information for the test author. + // Don't throw yet though b'c it might be accidentally caught and suppressed. + const stack = new Error().stack + unexpectedConsoleCallStacks.push([ + stack.substr(stack.indexOf('\n') + 1), + util.format(format, ...args), + ]) + } + + console[methodName] = newMethod + + return newMethod +} + +const isSpy = spy => + (spy.calls && typeof spy.calls.count === 'function') || + spy._isMockFunction === true + +const flushUnexpectedConsoleCalls = ( + mockMethod, + methodName, + expectedMatcher, + unexpectedConsoleCallStacks, +) => { + if (console[methodName] !== mockMethod && !isSpy(console[methodName])) { + throw new Error( + `Test did not tear down console.${methodName} mock properly.`, + ) + } + if (unexpectedConsoleCallStacks.length > 0) { + const messages = unexpectedConsoleCallStacks.map( + ([stack, message]) => + `${chalk.red(message)}\n` + + `${stack + .split('\n') + .map(line => chalk.gray(line)) + .join('\n')}`, + ) + + const message = + `Expected test not to call ${chalk.bold( + `console.${methodName}()`, + )}.\n\n` + + 'If the warning is expected, test for it explicitly by:\n' + + `1. Using the ${chalk.bold('.' + expectedMatcher + '()')} ` + + `matcher, or...\n` + + `2. Mock it out using ${chalk.bold( + 'spyOnDev', + )}(console, '${methodName}') or ${chalk.bold( + 'spyOnProd', + )}(console, '${methodName}'), and test that the warning occurs.` + + throw new Error(`${message}\n\n${messages.join('\n\n')}`) + } +} + +const unexpectedErrorCallStacks = [] +const unexpectedWarnCallStacks = [] + +const errorMethod = patchConsoleMethod('error', unexpectedErrorCallStacks) +const warnMethod = patchConsoleMethod('warn', unexpectedWarnCallStacks) + +const flushAllUnexpectedConsoleCalls = () => { + flushUnexpectedConsoleCalls( + errorMethod, + 'error', + 'toErrorDev', + unexpectedErrorCallStacks, + ) + flushUnexpectedConsoleCalls( + warnMethod, + 'warn', + 'toWarnDev', + unexpectedWarnCallStacks, + ) + unexpectedErrorCallStacks.length = 0 + unexpectedWarnCallStacks.length = 0 +} + +const resetAllUnexpectedConsoleCalls = () => { + unexpectedErrorCallStacks.length = 0 + unexpectedWarnCallStacks.length = 0 +} + +expect.extend({ + ...require('./toWarnDev'), +}) + +beforeEach(resetAllUnexpectedConsoleCalls) +afterEach(flushAllUnexpectedConsoleCalls) diff --git a/tests/setup-env.js b/tests/setup-env.js index 264828a9..a4ddfa17 100644 --- a/tests/setup-env.js +++ b/tests/setup-env.js @@ -1 +1,2 @@ import '@testing-library/jest-dom/extend-expect' +import './failOnUnexpectedConsoleCalls' diff --git a/tests/shouldIgnoreConsoleError.js b/tests/shouldIgnoreConsoleError.js new file mode 100644 index 00000000..75528267 --- /dev/null +++ b/tests/shouldIgnoreConsoleError.js @@ -0,0 +1,43 @@ +// Fork of https://github.com/facebook/react/blob/513417d6951fa3ff5729302b7990b84604b11afa/scripts/jest/shouldIgnoreConsoleError.js +/** +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +module.exports = function shouldIgnoreConsoleError(format) { + if (process.env.NODE_ENV !== 'production') { + if (typeof format === 'string') { + if (format.indexOf('Error: Uncaught [') === 0) { + // This looks like an uncaught error from invokeGuardedCallback() wrapper + // in development that is reported by jsdom. Ignore because it's noisy. + return true + } + if (format.indexOf('The above error occurred') === 0) { + // This looks like an error addendum from ReactFiberErrorLogger. + // Ignore it too. + return true + } + } + } + // Looks legit + return false +} diff --git a/tests/toWarnDev.js b/tests/toWarnDev.js new file mode 100644 index 00000000..ac5f1b19 --- /dev/null +++ b/tests/toWarnDev.js @@ -0,0 +1,303 @@ +// Fork of https://github.com/facebook/react/blob/513417d6951fa3ff5729302b7990b84604b11afa/scripts/jest/matchers/toWarnDev.js +/** +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ +/* eslint-disable no-unsafe-finally */ +/* eslint-disable no-negated-condition */ +/* eslint-disable @babel/no-invalid-this */ +/* eslint-disable prefer-template */ +/* eslint-disable func-names */ +/* eslint-disable complexity */ +const util = require('util') +const jestDiff = require('jest-diff').default +const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError') + +function normalizeCodeLocInfo(str) { + if (typeof str !== 'string') { + return str + } + // This special case exists only for the special source location in + // ReactElementValidator. That will go away if we remove source locations. + str = str.replace(/Check your code at .+?:\d+/g, 'Check your code at **') + // V8 format: + // at Component (/path/filename.js:123:45) + // React format: + // in Component (at filename.js:123) + // eslint-disable-next-line prefer-arrow-callback + return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { + return '\n in ' + name + ' (at **)' + }) +} + +const createMatcherFor = (consoleMethod, matcherName) => + function matcher(callback, expectedMessages, options = {}) { + if (process.env.NODE_ENV !== 'production') { + // Warn about incorrect usage of matcher. + if (typeof expectedMessages === 'string') { + expectedMessages = [expectedMessages] + } else if (!Array.isArray(expectedMessages)) { + throw Error( + `${matcherName}() requires a parameter of type string or an array of strings ` + + `but was given ${typeof expectedMessages}.`, + ) + } + if ( + options != null && + (typeof options !== 'object' || Array.isArray(options)) + ) { + throw new Error( + `${matcherName}() second argument, when present, should be an object. ` + + 'Did you forget to wrap the messages into an array?', + ) + } + if (arguments.length > 3) { + // `matcher` comes from Jest, so it's more than 2 in practice + throw new Error( + `${matcherName}() received more than two arguments. ` + + 'Did you forget to wrap the messages into an array?', + ) + } + + const withoutStack = options.withoutStack + const logAllErrors = options.logAllErrors + const warningsWithoutComponentStack = [] + const warningsWithComponentStack = [] + const unexpectedWarnings = [] + + let lastWarningWithMismatchingFormat = null + let lastWarningWithExtraComponentStack = null + + // Catch errors thrown by the callback, + // But only rethrow them if all test expectations have been satisfied. + // Otherwise an Error in the callback can mask a failed expectation, + // and result in a test that passes when it shouldn't. + let caughtError + + const isLikelyAComponentStack = message => + typeof message === 'string' && + (message.includes('\n in ') || message.includes('\n at ')) + + const consoleSpy = (format, ...args) => { + // Ignore uncaught errors reported by jsdom + // and React addendums because they're too noisy. + if ( + !logAllErrors && + consoleMethod === 'error' && + shouldIgnoreConsoleError(format, args) + ) { + return + } + + const message = util.format(format, ...args) + const normalizedMessage = normalizeCodeLocInfo(message) + + // Remember if the number of %s interpolations + // doesn't match the number of arguments. + // We'll fail the test if it happens. + let argIndex = 0 + format.replace(/%s/g, () => argIndex++) + if (argIndex !== args.length) { + lastWarningWithMismatchingFormat = { + format, + args, + expectedArgCount: argIndex, + } + } + + // Protect against accidentally passing a component stack + // to warning() which already injects the component stack. + if ( + args.length >= 2 && + isLikelyAComponentStack(args[args.length - 1]) && + isLikelyAComponentStack(args[args.length - 2]) + ) { + lastWarningWithExtraComponentStack = { + format, + } + } + + for (let index = 0; index < expectedMessages.length; index++) { + const expectedMessage = expectedMessages[index] + if ( + normalizedMessage === expectedMessage || + normalizedMessage.includes(expectedMessage) + ) { + if (isLikelyAComponentStack(normalizedMessage)) { + warningsWithComponentStack.push(normalizedMessage) + } else { + warningsWithoutComponentStack.push(normalizedMessage) + } + expectedMessages.splice(index, 1) + return + } + } + + let errorMessage + if (expectedMessages.length === 0) { + errorMessage = + 'Unexpected warning recorded: ' + + this.utils.printReceived(normalizedMessage) + } else if (expectedMessages.length === 1) { + errorMessage = + 'Unexpected warning recorded: ' + + jestDiff(expectedMessages[0], normalizedMessage) + } else { + errorMessage = + 'Unexpected warning recorded: ' + + jestDiff(expectedMessages, [normalizedMessage]) + } + + // Record the call stack for unexpected warnings. + // We don't throw an Error here though, + // Because it might be suppressed by ReactFiberScheduler. + unexpectedWarnings.push(new Error(errorMessage)) + } + + // TODO Decide whether we need to support nested toWarn* expectations. + // If we don't need it, add a check here to see if this is already our spy, + // And throw an error. + const originalMethod = console[consoleMethod] + + // Avoid using Jest's built-in spy since it can't be removed. + console[consoleMethod] = consoleSpy + + try { + callback() + } catch (error) { + caughtError = error + } finally { + // Restore the unspied method so that unexpected errors fail tests. + console[consoleMethod] = originalMethod + + // Any unexpected Errors thrown by the callback should fail the test. + // This should take precedence since unexpected errors could block warnings. + if (caughtError) { + throw caughtError + } + + // Any unexpected warnings should be treated as a failure. + if (unexpectedWarnings.length > 0) { + return { + message: () => unexpectedWarnings[0].stack, + pass: false, + } + } + + // Any remaining messages indicate a failed expectations. + if (expectedMessages.length > 0) { + return { + message: () => + `Expected warning was not recorded:\n ${this.utils.printReceived( + expectedMessages[0], + )}`, + pass: false, + } + } + + if (typeof withoutStack === 'number') { + // We're expecting a particular number of warnings without stacks. + if (withoutStack !== warningsWithoutComponentStack.length) { + return { + message: () => + `Expected ${withoutStack} warnings without a component stack but received ${warningsWithoutComponentStack.length}:\n` + + warningsWithoutComponentStack.map(warning => + this.utils.printReceived(warning), + ), + pass: false, + } + } + } else if (withoutStack === true) { + // We're expecting that all warnings won't have the stack. + // If some warnings have it, it's an error. + if (warningsWithComponentStack.length > 0) { + return { + message: () => + `Received warning unexpectedly includes a component stack:\n ${this.utils.printReceived( + warningsWithComponentStack[0], + )}\nIf this warning intentionally includes the component stack, remove ` + + `{withoutStack: true} from the ${matcherName}() call. If you have a mix of ` + + `warnings with and without stack in one ${matcherName}() call, pass ` + + `{withoutStack: N} where N is the number of warnings without stacks.`, + pass: false, + } + } + } else if (withoutStack === false || withoutStack === undefined) { + // We're expecting that all warnings *do* have the stack (default). + // If some warnings don't have it, it's an error. + if (warningsWithoutComponentStack.length > 0) { + return { + message: () => + `Received warning unexpectedly does not include a component stack:\n ${this.utils.printReceived( + warningsWithoutComponentStack[0], + )}\nIf this warning intentionally omits the component stack, add ` + + `{withoutStack: true} to the ${matcherName} call.`, + pass: false, + } + } + } else { + throw Error( + `The second argument for ${matcherName}(), when specified, must be an object. It may have a ` + + `property called "withoutStack" whose value may be undefined, boolean, or a number. ` + + `Instead received ${typeof withoutStack}.`, + ) + } + + if (lastWarningWithMismatchingFormat !== null) { + return { + message: () => + `Received ${ + lastWarningWithMismatchingFormat.args.length + } arguments for a message with ${ + lastWarningWithMismatchingFormat.expectedArgCount + } placeholders:\n ${this.utils.printReceived( + lastWarningWithMismatchingFormat.format, + )}`, + pass: false, + } + } + + if (lastWarningWithExtraComponentStack !== null) { + return { + message: () => + `Received more than one component stack for a warning:\n ${this.utils.printReceived( + lastWarningWithExtraComponentStack.format, + )}\nDid you accidentally pass a stack to warning() as the last argument? ` + + `Don't forget warning() already injects the component stack automatically.`, + pass: false, + } + } + + return {pass: true} + } + } else { + // Any uncaught errors or warnings should fail tests in production mode. + callback() + + return {pass: true} + } + } + +module.exports = { + toWarnDev: createMatcherFor('warn', 'toWarnDev'), + toErrorDev: createMatcherFor('error', 'toErrorDev'), +} From c43512a9271f5738496a3ed49aed7e3e9dad071c Mon Sep 17 00:00:00 2001 From: AlexDate: Mon, 12 Dec 2022 19:33:05 +0200 Subject: [PATCH 081/126] GitHub Workflows security hardening (#1162) --- .github/workflows/validate.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ad4adccf..0f99d084 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -12,6 +12,10 @@ on: - 'alpha' - '!all-contributors/**' pull_request: {} +permissions: + actions: write # to cancel/stop running workflows (styfle/cancel-workflow-action) + contents: read # to fetch code (actions/checkout) + jobs: main: continue-on-error: ${{ matrix.react != 'latest' }} @@ -58,6 +62,10 @@ jobs: flags: ${{ matrix.react }} release: + permissions: + actions: write # to cancel/stop running workflows (styfle/cancel-workflow-action) + contents: write # to create release tags (cycjimmy/semantic-release-action) + needs: main runs-on: ubuntu-latest if: From 9b7a1e2bea5bf20ba9728f98eb7c68cdb80b7fdd Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 31 Jan 2023 05:53:01 +0100 Subject: [PATCH 082/126] feat: Drop support for Node.js 12.x (#1169) BREAKING CHANGE: Minimum supported Node.js version is now 14.x --- .codesandbox/ci.json | 2 +- .github/workflows/validate.yml | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index bf3237bb..d5850328 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,5 +1,5 @@ { "installCommand": "install:csb", "sandboxes": ["new", "github/kentcdodds/react-testing-library-examples"], - "node": "12" + "node": "14" } diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0f99d084..53093e67 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - node: [12, 14, 16, 18] + node: [14, 16, 18] react: [latest, next, experimental] runs-on: ubuntu-latest steps: diff --git a/package.json b/package.json index d2dd6a97..9ee97f1d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "types": "types/index.d.ts", "module": "dist/@testing-library/react.esm.js", "engines": { - "node": ">=12" + "node": ">=14" }, "scripts": { "prebuild": "rimraf dist", From 1934bf224f9d45f3fc91cb722e31d3885aa9c7a0 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 15 Feb 2023 17:52:48 +0100 Subject: [PATCH 083/126] Bump kcd-scripts to 13.0.0 (#1170) * Bump kcd-scripts to 13.0.0 * Resolve lint issues --- package.json | 8 ++++++-- src/__tests__/cleanup.js | 3 ++- src/__tests__/debug.js | 3 +-- src/__tests__/new-act.js | 8 ++++---- src/__tests__/renderHook.js | 2 +- tests/setup-env.js | 3 +++ tests/toWarnDev.js | 2 +- types/test.tsx | 1 - 8 files changed, 18 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 9ee97f1d..b475390a 100644 --- a/package.json +++ b/package.json @@ -53,8 +53,8 @@ "@testing-library/jest-dom": "^5.11.6", "chalk": "^4.1.2", "dotenv-cli": "^4.0.0", - "jest-diff": "^27.5.1", - "kcd-scripts": "^11.1.0", + "jest-diff": "^29.4.1", + "kcd-scripts": "^13.0.0", "npm-run-all": "^4.1.5", "react": "^18.0.0", "react-dom": "^18.0.0", @@ -67,6 +67,9 @@ }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", + "parserOptions": { + "ecmaVersion": 2022 + }, "globals": { "globalThis": "readonly" }, @@ -76,6 +79,7 @@ "import/no-unassigned-import": "off", "import/named": "off", "testing-library/no-container": "off", + "testing-library/no-debugging-utils": "off", "testing-library/no-dom-import": "off", "testing-library/no-unnecessary-act": "off", "testing-library/prefer-user-event": "off" diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js index 4517c098..9f17c722 100644 --- a/src/__tests__/cleanup.js +++ b/src/__tests__/cleanup.js @@ -64,7 +64,7 @@ describe('fake timers and missing act warnings', () => { let cancelled = false Promise.resolve().then(() => { microTaskSpy() - // eslint-disable-next-line jest/no-if -- false positive + // eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false positive if (!cancelled) { setDeferredCounter(counter) } @@ -96,6 +96,7 @@ describe('fake timers and missing act warnings', () => { let cancelled = false setTimeout(() => { deferredStateUpdateSpy() + // eslint-disable-next-line jest/no-conditional-in-test -- false-positive if (!cancelled) { setDeferredCounter(counter) } diff --git a/src/__tests__/debug.js b/src/__tests__/debug.js index f3aad595..c6a1d1fe 100644 --- a/src/__tests__/debug.js +++ b/src/__tests__/debug.js @@ -42,7 +42,7 @@ test('allows same arguments as prettyDOM', () => { debug(container, 6, {highlight: false}) expect(console.log).toHaveBeenCalledTimes(1) expect(console.log.mock.calls[0]).toMatchInlineSnapshot(` - Array [ + [ ..., ] @@ -52,5 +52,4 @@ test('allows same arguments as prettyDOM', () => { /* eslint no-console: "off", - testing-library/no-debug: "off", */ diff --git a/src/__tests__/new-act.js b/src/__tests__/new-act.js index 4909d4a6..0412a8a3 100644 --- a/src/__tests__/new-act.js +++ b/src/__tests__/new-act.js @@ -47,8 +47,8 @@ test('async act recovers from errors', async () => { } expect(console.error).toHaveBeenCalledTimes(1) expect(console.error.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ + [ + [ call console.error, ], ] @@ -65,8 +65,8 @@ test('async act recovers from sync errors', async () => { } expect(console.error).toHaveBeenCalledTimes(1) expect(console.error.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ + [ + [ call console.error, ], ] diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js index f6b7a343..11b7009a 100644 --- a/src/__tests__/renderHook.js +++ b/src/__tests__/renderHook.js @@ -21,7 +21,7 @@ test('allows rerendering', () => { const [left, setLeft] = React.useState('left') const [right, setRight] = React.useState('right') - // eslint-disable-next-line jest/no-if + // eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false-positive switch (branch) { case 'left': return [left, setLeft] diff --git a/tests/setup-env.js b/tests/setup-env.js index a4ddfa17..c9b976f5 100644 --- a/tests/setup-env.js +++ b/tests/setup-env.js @@ -1,2 +1,5 @@ import '@testing-library/jest-dom/extend-expect' import './failOnUnexpectedConsoleCalls' +import {TextEncoder} from 'util' + +global.TextEncoder = TextEncoder diff --git a/tests/toWarnDev.js b/tests/toWarnDev.js index ac5f1b19..ca58346f 100644 --- a/tests/toWarnDev.js +++ b/tests/toWarnDev.js @@ -24,7 +24,7 @@ SOFTWARE. */ /* eslint-disable no-unsafe-finally */ /* eslint-disable no-negated-condition */ -/* eslint-disable @babel/no-invalid-this */ +/* eslint-disable no-invalid-this */ /* eslint-disable prefer-template */ /* eslint-disable func-names */ /* eslint-disable complexity */ diff --git a/types/test.tsx b/types/test.tsx index 17ba7012..c33f07b6 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -188,7 +188,6 @@ export function testRenderHookProps() { eslint testing-library/prefer-explicit-assert: "off", testing-library/no-wait-for-empty-callback: "off", - testing-library/no-debug: "off", testing-library/prefer-screen-queries: "off" */ From 153a095369cdbe3149a720df9435dc698024c678 Mon Sep 17 00:00:00 2001 From: Sebastian SilbermannDate: Thu, 16 Feb 2023 23:34:19 +0100 Subject: [PATCH 084/126] chore: Allow semantic-release to post updates in issues (#1176) --- .github/workflows/validate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 53093e67..5db8153c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -65,6 +65,7 @@ jobs: permissions: actions: write # to cancel/stop running workflows (styfle/cancel-workflow-action) contents: write # to create release tags (cycjimmy/semantic-release-action) + issues: write # to post release that resolves an issue (cycjimmy/semantic-release-action) needs: main runs-on: ubuntu-latest From 6653c239c0acbafd204326c8951cde8206d39898 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Thu, 16 Feb 2023 23:37:50 +0100 Subject: [PATCH 085/126] feat: Bump `@testing-library/dom` to 9.0.0 (#1177) BREAKING CHANGE: See https://github.com/testing-library/dom-testing-library/releases/tag/v9.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b475390a..f9061345 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", + "@testing-library/dom": "^9.0.0", "@types/react-dom": "^18.0.0" }, "devDependencies": { From f78839bf4147a777a823e33a429bcf5de9562f9e Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Thu, 16 Feb 2023 23:46:50 +0100 Subject: [PATCH 086/126] fix: Prevent "missing act" warning for queued microtasks (#1137) * Add intended behavior * fix: Prevent "missing act" warning for in-flight promises * Disable TL lint rules in tests * Implementation without macrotask * Now I member --- package.json | 2 + src/__tests__/end-to-end.js | 211 ++++++++++++++++++++++++++---------- src/pure.js | 30 ++++- 3 files changed, 182 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index f9061345..70aebdad 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,8 @@ "testing-library/no-debugging-utils": "off", "testing-library/no-dom-import": "off", "testing-library/no-unnecessary-act": "off", + "testing-library/prefer-explicit-assert": "off", + "testing-library/prefer-find-by": "off", "testing-library/prefer-user-event": "off" } }, diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js index cf222aec..005591d3 100644 --- a/src/__tests__/end-to-end.js +++ b/src/__tests__/end-to-end.js @@ -1,73 +1,164 @@ import * as React from 'react' import {render, waitForElementToBeRemoved, screen, waitFor} from '../' -const fetchAMessage = () => - new Promise(resolve => { - // we are using random timeout here to simulate a real-time example - // of an async operation calling a callback at a non-deterministic time - const randomTimeout = Math.floor(Math.random() * 100) - setTimeout(() => { - resolve({returnedMessage: 'Hello World'}) - }, randomTimeout) - }) - -function ComponentWithLoader() { - const [state, setState] = React.useState({data: undefined, loading: true}) - React.useEffect(() => { - let cancelled = false - fetchAMessage().then(data => { - if (!cancelled) { - setState({data, loading: false}) - } +describe.each([ + ['real timers', () => jest.useRealTimers()], + ['fake legacy timers', () => jest.useFakeTimers('legacy')], + ['fake modern timers', () => jest.useFakeTimers('modern')], +])( + 'it waits for the data to be loaded in a macrotask using %s', + (label, useTimers) => { + beforeEach(() => { + useTimers() + }) + + afterEach(() => { + jest.useRealTimers() }) - return () => { - cancelled = true + const fetchAMessageInAMacrotask = () => + new Promise(resolve => { + // we are using random timeout here to simulate a real-time example + // of an async operation calling a callback at a non-deterministic time + const randomTimeout = Math.floor(Math.random() * 100) + setTimeout(() => { + resolve({returnedMessage: 'Hello World'}) + }, randomTimeout) + }) + + function ComponentWithMacrotaskLoader() { + const [state, setState] = React.useState({data: undefined, loading: true}) + React.useEffect(() => { + let cancelled = false + fetchAMessageInAMacrotask().then(data => { + if (!cancelled) { + setState({data, loading: false}) + } + }) + + return () => { + cancelled = true + } + }, []) + + if (state.loading) { + return Loading...+ } + + return ( ++ Loaded this message: {state.data.returnedMessage}! ++ ) } - }, []) - if (state.loading) { - returnLoading...- } + test('waitForElementToBeRemoved', async () => { + render() + const loading = () => screen.getByText('Loading...') + await waitForElementToBeRemoved(loading) + expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) + }) + + test('waitFor', async () => { + render( ) + await waitFor(() => screen.getByText(/Loading../)) + await waitFor(() => screen.getByText(/Loaded this message:/)) + expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) + }) - return ( - - Loaded this message: {state.data.returnedMessage}! -- ) -} + test('findBy', async () => { + render() + await expect(screen.findByTestId('message')).resolves.toHaveTextContent( + /Hello World/, + ) + }) + }, +) describe.each([ ['real timers', () => jest.useRealTimers()], ['fake legacy timers', () => jest.useFakeTimers('legacy')], ['fake modern timers', () => jest.useFakeTimers('modern')], -])('it waits for the data to be loaded using %s', (label, useTimers) => { - beforeEach(() => { - useTimers() - }) - - afterEach(() => { - jest.useRealTimers() - }) - - test('waitForElementToBeRemoved', async () => { - render( ) - const loading = () => screen.getByText('Loading...') - await waitForElementToBeRemoved(loading) - expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) - }) - - test('waitFor', async () => { - render( ) - const message = () => screen.getByText(/Loaded this message:/) - await waitFor(message) - expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) - }) - - test('findBy', async () => { - render( ) - await expect(screen.findByTestId('message')).resolves.toHaveTextContent( - /Hello World/, - ) - }) -}) +])( + 'it waits for the data to be loaded in a microtask using %s', + (label, useTimers) => { + beforeEach(() => { + useTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + const fetchAMessageInAMicrotask = () => + Promise.resolve({ + status: 200, + json: () => Promise.resolve({title: 'Hello World'}), + }) + + function ComponentWithMicrotaskLoader() { + const [fetchState, setFetchState] = React.useState({fetching: true}) + + React.useEffect(() => { + if (fetchState.fetching) { + fetchAMessageInAMicrotask().then(res => { + return ( + res + .json() + // By spec, the runtime can only yield back to the event loop once + // the microtask queue is empty. + // So we ensure that we actually wait for that as well before yielding back from `waitFor`. + .then(data => data) + .then(data => data) + .then(data => data) + .then(data => data) + .then(data => data) + .then(data => data) + .then(data => data) + .then(data => data) + .then(data => data) + .then(data => data) + .then(data => data) + .then(data => { + setFetchState({todo: data.title, fetching: false}) + }) + ) + }) + } + }, [fetchState]) + + if (fetchState.fetching) { + return Loading..
+ } + + return ( +Loaded this message: {fetchState.todo}+ ) + } + + test('waitForElementToBeRemoved', async () => { + render() + const loading = () => screen.getByText('Loading..') + await waitForElementToBeRemoved(loading) + expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) + }) + + test('waitFor', async () => { + render( ) + await waitFor(() => { + screen.getByText('Loading..') + }) + await waitFor(() => { + screen.getByText(/Loaded this message:/) + }) + expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) + }) + + test('findBy', async () => { + render( ) + await expect(screen.findByTestId('message')).resolves.toHaveTextContent( + /Hello World/, + ) + }) + }, +) diff --git a/src/pure.js b/src/pure.js index 94b3b2bd..845aede1 100644 --- a/src/pure.js +++ b/src/pure.js @@ -12,6 +12,20 @@ import act, { } from './act-compat' import {fireEvent} from './fire-event' +function jestFakeTimersAreEnabled() { + /* istanbul ignore else */ + if (typeof jest !== 'undefined' && jest !== null) { + return ( + // legacy timers + setTimeout._isMockFunction === true || // modern timers + // eslint-disable-next-line prefer-object-has-own -- No Object.hasOwn in all target environments we support. + Object.prototype.hasOwnProperty.call(setTimeout, 'clock') + ) + } // istanbul ignore next + + return false +} + configureDTL({ unstable_advanceTimersWrapper: cb => { return act(cb) @@ -23,7 +37,21 @@ configureDTL({ const previousActEnvironment = getIsReactActEnvironment() setReactActEnvironment(false) try { - return await cb() + const result = await cb() + // Drain microtask queue. + // Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call. + // The caller would have no chance to wrap the in-flight Promises in `act()` + await new Promise(resolve => { + setTimeout(() => { + resolve() + }, 0) + + if (jestFakeTimersAreEnabled()) { + jest.advanceTimersByTime(0) + } + }) + + return result } finally { setReactActEnvironment(previousActEnvironment) } From f6c6d9610da4fe90ec64445391e0ea8bfe39e65d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Wed, 26 Apr 2023 12:56:53 +0200 Subject: [PATCH 087/126] chore: remove `styfle/cancel-workflow-action` usage (#1204) --- .github/workflows/validate.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 5db8153c..60e8fd44 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -11,7 +11,12 @@ on: - 'beta' - 'alpha' - '!all-contributors/**' - pull_request: {} + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: actions: write # to cancel/stop running workflows (styfle/cancel-workflow-action) contents: read # to fetch code (actions/checkout) @@ -28,9 +33,6 @@ jobs: react: [latest, next, experimental] runs-on: ubuntu-latest steps: - - name: π Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.0 - - name: β¬οΈ Checkout repo uses: actions/checkout@v2 @@ -73,9 +75,6 @@ jobs: ${{ github.repository == 'testing-library/react-testing-library' && github.event_name == 'push' }} steps: - - name: π Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.0 - - name: β¬οΈ Checkout repo uses: actions/checkout@v2 From 5dc81dc790b1831707e89cf52b3fecb3c3d294d2 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Thu, 4 May 2023 15:17:31 +0300 Subject: [PATCH 088/126] chore: rename `next` channel to `canary` (#1207) * chore: add canary to our test matrix * chore: rename next to canary --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 60e8fd44..f1359d76 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: node: [14, 16, 18] - react: [latest, next, experimental] + react: [latest, canary, experimental] runs-on: ubuntu-latest steps: - name: β¬οΈ Checkout repo From 6b4180e71286cef86a359435697965e59d408d91 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sun, 28 May 2023 11:03:05 +0200 Subject: [PATCH 089/126] test: Add test for flushing before exiting `waitFor` (#1215) --- src/__tests__/end-to-end.js | 72 ++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js index 005591d3..f93c23be 100644 --- a/src/__tests__/end-to-end.js +++ b/src/__tests__/end-to-end.js @@ -80,7 +80,7 @@ describe.each([ ['fake legacy timers', () => jest.useFakeTimers('legacy')], ['fake modern timers', () => jest.useFakeTimers('modern')], ])( - 'it waits for the data to be loaded in a microtask using %s', + 'it waits for the data to be loaded in many microtask using %s', (label, useTimers) => { beforeEach(() => { useTimers() @@ -162,3 +162,73 @@ describe.each([ }) }, ) + +describe.each([ + ['real timers', () => jest.useRealTimers()], + ['fake legacy timers', () => jest.useFakeTimers('legacy')], + ['fake modern timers', () => jest.useFakeTimers('modern')], +])( + 'it waits for the data to be loaded in a microtask using %s', + (label, useTimers) => { + beforeEach(() => { + useTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + const fetchAMessageInAMicrotask = () => + Promise.resolve({ + status: 200, + json: () => Promise.resolve({title: 'Hello World'}), + }) + + function ComponentWithMicrotaskLoader() { + const [fetchState, setFetchState] = React.useState({fetching: true}) + + React.useEffect(() => { + if (fetchState.fetching) { + fetchAMessageInAMicrotask().then(res => { + return res.json().then(data => { + setFetchState({todo: data.title, fetching: false}) + }) + }) + } + }, [fetchState]) + + if (fetchState.fetching) { + return Loading..
+ } + + return ( +Loaded this message: {fetchState.todo}+ ) + } + + test('waitForElementToBeRemoved', async () => { + render() + const loading = () => screen.getByText('Loading..') + await waitForElementToBeRemoved(loading) + expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) + }) + + test('waitFor', async () => { + render( ) + await waitFor(() => { + screen.getByText('Loading..') + }) + await waitFor(() => { + screen.getByText(/Loaded this message:/) + }) + expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) + }) + + test('findBy', async () => { + render( ) + await expect(screen.findByTestId('message')).resolves.toHaveTextContent( + /Hello World/, + ) + }) + }, +) From 6de5f4c29f73e740152de31bbe3ccc6e711aa210 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Wed, 26 Jul 2023 21:57:48 +0300 Subject: [PATCH 090/126] docs(readme): remove deprecated link (#1229) Resolves #1228 --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 45324901..6752c3d2 100644 --- a/README.md +++ b/README.md @@ -376,9 +376,6 @@ Some included are: - [`react-router`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/main/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-router.js&previewwindow=tests) - [`react-context`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/main/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-context.js&previewwindow=tests) -You can also find React Testing Library examples at -[react-testing-examples.com](https://react-testing-examples.com/jest-rtl/). - ## Hooks If you are interested in testing a custom hook, check out [React Hooks Testing From 5b489166e50d5d53608d98b283e8e936e1cce91d Mon Sep 17 00:00:00 2001 From: Colin Diesh Date: Sun, 10 Sep 2023 15:58:41 -0400 Subject: [PATCH 091/126] docs: fix readme CI badge (#1237) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6752c3d2..b129c789 100644 --- a/README.md +++ b/README.md @@ -651,7 +651,7 @@ Contributions of any kind welcome! [npm]: https://www.npmjs.com/ [yarn]: https://classic.yarnpkg.com [node]: https://nodejs.org -[build-badge]: https://img.shields.io/github/workflow/status/testing-library/react-testing-library/validate?logo=github&style=flat-square +[build-badge]: https://img.shields.io/github/actions/workflow/status/testing-library/react-testing-library/validate.yml?branch=main&logo=github [build]: https://github.com/testing-library/react-testing-library/actions?query=workflow%3Avalidate [coverage-badge]: https://img.shields.io/codecov/c/github/testing-library/react-testing-library.svg?style=flat-square [coverage]: https://codecov.io/github/testing-library/react-testing-library From c04b8f006c5a683d05c460c8ee1e2248d6f74350 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 10 Sep 2023 23:00:37 +0300 Subject: [PATCH 092/126] docs: add cmdcolin as a contributor for doc (#1238) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 13 +- README.md | 286 ++++++++++++++++++++++---------------------- 2 files changed, 155 insertions(+), 144 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 270dd6a0..16957ca9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1344,8 +1344,19 @@ "contributions": [ "doc" ] + }, + { + "login": "cmdcolin", + "name": "Colin Diesh", + "avatar_url": "https://avatars.githubusercontent.com/u/6511937?v=4", + "profile": "http://cmdcolin.github.io", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, - "repoHost": "https://github.com" + "repoHost": "https://github.com", + "commitType": "docs", + "commitConvention": "angular" } diff --git a/README.md b/README.md index b129c789..a3731749 100644 --- a/README.md +++ b/README.md @@ -449,189 +449,189 @@ Thanks goes to these people ([emoji key][emojis]): Date: Wed, 8 Nov 2023 08:51:17 +0200 Subject: [PATCH 093/126] feat: add warnings when globals are missing (#1244) * feat: add warnings when globals are missing * revert the istanbul ignore removal * improve error message * Apply suggestions from code review Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> --------- Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> --- src/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/index.js b/src/index.js index bb0d0270..26028a9a 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,10 @@ if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) { teardown(() => { cleanup() }) + } else { + console.warn( + `The current test runner does not support afterEach/teardown hooks. This means we won't be able to run automatic cleanup and you should be calling cleanup() manually.`, + ) } // No test setup with other test runners available @@ -35,6 +39,10 @@ if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) { afterAll(() => { setReactActEnvironment(previousIsReactActEnvironment) }) + } else { + console.warn( + 'The current test runner does not support beforeAll/afterAll hooks. This means you should be setting IS_REACT_ACT_ENVIRONMENT manually.', + ) } } From fd52a593a7987a14d3cf5c94f112795a1630725d Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Fri, 17 Nov 2023 16:46:53 +0200 Subject: [PATCH 094/126] fix: log globals warning only once (#1252) Resolves #1249 --- src/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 26028a9a..42cfe59e 100644 --- a/src/index.js +++ b/src/index.js @@ -20,7 +20,8 @@ if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) { teardown(() => { cleanup() }) - } else { + } else if (!process.env.RTL_AFTEREACH_WARNING_LOGGED) { + process.env.RTL_AFTEREACH_WARNING_LOGGED = true console.warn( `The current test runner does not support afterEach/teardown hooks. This means we won't be able to run automatic cleanup and you should be calling cleanup() manually.`, ) @@ -39,7 +40,8 @@ if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) { afterAll(() => { setReactActEnvironment(previousIsReactActEnvironment) }) - } else { + } else if (!process.env.RTL_AFTERALL_WARNING_LOGGED) { + process.env.RTL_AFTERALL_WARNING_LOGGED = true console.warn( 'The current test runner does not support beforeAll/afterAll hooks. This means you should be setting IS_REACT_ACT_ENVIRONMENT manually.', ) From 1c67477443244e52c3ae57db49e1a6e8226e0c0d Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Fri, 17 Nov 2023 17:46:40 +0200 Subject: [PATCH 095/126] fix: revert missing hooks warnings (#1255) --- src/index.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/index.js b/src/index.js index 42cfe59e..bb0d0270 100644 --- a/src/index.js +++ b/src/index.js @@ -20,11 +20,6 @@ if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) { teardown(() => { cleanup() }) - } else if (!process.env.RTL_AFTEREACH_WARNING_LOGGED) { - process.env.RTL_AFTEREACH_WARNING_LOGGED = true - console.warn( - `The current test runner does not support afterEach/teardown hooks. This means we won't be able to run automatic cleanup and you should be calling cleanup() manually.`, - ) } // No test setup with other test runners available @@ -40,11 +35,6 @@ if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) { afterAll(() => { setReactActEnvironment(previousIsReactActEnvironment) }) - } else if (!process.env.RTL_AFTERALL_WARNING_LOGGED) { - process.env.RTL_AFTERALL_WARNING_LOGGED = true - console.warn( - 'The current test runner does not support beforeAll/afterAll hooks. This means you should be setting IS_REACT_ACT_ENVIRONMENT manually.', - ) } } From 03a301f2488b32c94d6f6f139191f6ff71221944 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Fri, 8 Dec 2023 18:32:16 +0200 Subject: [PATCH 096/126] chore: update stackblitz url in issue template (#1258) --- .github/ISSUE_TEMPLATE/Bug_Report.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Bug_Report.md b/.github/ISSUE_TEMPLATE/Bug_Report.md index daefe8c6..c04bef38 100644 --- a/.github/ISSUE_TEMPLATE/Bug_Report.md +++ b/.github/ISSUE_TEMPLATE/Bug_Report.md @@ -60,13 +60,8 @@ https://github.com/testing-library/testing-library-docs ### Reproduction: ### Problem description: From 0880eba4a01c030f942ad93600081bbb86eac959 Mon Sep 17 00:00:00 2001 From: Yusuke Iinuma Date: Wed, 31 Jan 2024 06:20:01 +0900 Subject: [PATCH 097/126] feat: add `reactStrictMode` option to enable strict mode render (#1241) * feat: add `reactStrictMode` option and override `getConfig` and `configure` functions from DTL * feat: update types for overridden `getConfig` and `configure` functions * test: add tests for checking configure APIs support RTL option and do not degrade * refactor: use a wrapper option for simplicity * refactor: use same function for wrapping UI if needed * feat: enable strict mode render if `reactStrictMode` option is true * test: add tests for checking strict mode works and can be combine with wrapper --------- Co-authored-by: Sebastian Silbermann --- src/__tests__/__snapshots__/render.js.snap | 2 +- src/__tests__/config.js | 66 ++++ src/__tests__/render.js | 332 ++++++++++++--------- src/__tests__/rerender.js | 113 +++++-- src/config.js | 34 +++ src/pure.js | 35 ++- types/index.d.ts | 13 + types/test.tsx | 22 ++ 8 files changed, 443 insertions(+), 174 deletions(-) create mode 100644 src/__tests__/config.js create mode 100644 src/config.js diff --git a/src/__tests__/__snapshots__/render.js.snap b/src/__tests__/__snapshots__/render.js.snap index eaf41443..345cd937 100644 --- a/src/__tests__/__snapshots__/render.js.snap +++ b/src/__tests__/__snapshots__/render.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`supports fragments 1`] = ` +exports[`render API supports fragments 1`] = ` diff --git a/src/__tests__/config.js b/src/__tests__/config.js new file mode 100644 index 00000000..7fdb1e00 --- /dev/null +++ b/src/__tests__/config.js @@ -0,0 +1,66 @@ +import {configure, getConfig} from '../' + +describe('configuration API', () => { + let originalConfig + beforeEach(() => { + // Grab the existing configuration so we can restore + // it at the end of the test + configure(existingConfig => { + originalConfig = existingConfig + // Don't change the existing config + return {} + }) + }) + + afterEach(() => { + configure(originalConfig) + }) + + describe('DTL options', () => { + test('configure can set by a plain JS object', () => { + const testIdAttribute = 'not-data-testid' + configure({testIdAttribute}) + + expect(getConfig().testIdAttribute).toBe(testIdAttribute) + }) + + test('configure can set by a function', () => { + // setup base option + const baseTestIdAttribute = 'data-testid' + configure({testIdAttribute: baseTestIdAttribute}) + + const modifiedPrefix = 'modified-' + configure(existingConfig => ({ + testIdAttribute: `${modifiedPrefix}${existingConfig.testIdAttribute}`, + })) + + expect(getConfig().testIdAttribute).toBe( + `${modifiedPrefix}${baseTestIdAttribute}`, + ) + }) + }) + + describe('RTL options', () => { + test('configure can set by a plain JS object', () => { + configure({reactStrictMode: true}) + + expect(getConfig().reactStrictMode).toBe(true) + }) + + test('configure can set by a function', () => { + configure(existingConfig => ({ + reactStrictMode: !existingConfig.reactStrictMode, + })) + + expect(getConfig().reactStrictMode).toBe(true) + }) + }) + + test('configure can set DTL and RTL options at once', () => { + const testIdAttribute = 'not-data-testid' + configure({testIdAttribute, reactStrictMode: true}) + + expect(getConfig().testIdAttribute).toBe(testIdAttribute) + expect(getConfig().reactStrictMode).toBe(true) + }) +}) diff --git a/src/__tests__/render.js b/src/__tests__/render.js index 46925f49..39f4bc92 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -1,84 +1,100 @@ import * as React from 'react' import ReactDOM from 'react-dom' import ReactDOMServer from 'react-dom/server' -import {fireEvent, render, screen} from '../' +import {fireEvent, render, screen, configure} from '../' + +describe('render API', () => { + let originalConfig + beforeEach(() => { + // Grab the existing configuration so we can restore + // it at the end of the test + configure(existingConfig => { + originalConfig = existingConfig + // Don't change the existing config + return {} + }) + }) -test('renders div into document', () => { - const ref = React.createRef() - const {container} = render() - expect(container.firstChild).toBe(ref.current) -}) + afterEach(() => { + configure(originalConfig) + }) -test('works great with react portals', () => { - class MyPortal extends React.Component { - constructor(...args) { - super(...args) - this.portalNode = document.createElement('div') - this.portalNode.dataset.testid = 'my-portal' - } - componentDidMount() { - document.body.appendChild(this.portalNode) - } - componentWillUnmount() { - this.portalNode.parentNode.removeChild(this.portalNode) - } - render() { - return ReactDOM.createPortal( -
, - this.portalNode, - ) - } - } - - function Greet({greeting, subject}) { - return ( - - - {greeting} {subject} - -- ) - } - - const {unmount} = render() - expect(screen.getByText('Hello World')).toBeInTheDocument() - const portalNode = screen.getByTestId('my-portal') - expect(portalNode).toBeInTheDocument() - unmount() - expect(portalNode).not.toBeInTheDocument() -}) + test('renders div into document', () => { + const ref = React.createRef() + const {container} = render() + expect(container.firstChild).toBe(ref.current) + }) -test('returns baseElement which defaults to document.body', () => { - const {baseElement} = render() - expect(baseElement).toBe(document.body) -}) + test('works great with react portals', () => { + class MyPortal extends React.Component { + constructor(...args) { + super(...args) + this.portalNode = document.createElement('div') + this.portalNode.dataset.testid = 'my-portal' + } + componentDidMount() { + document.body.appendChild(this.portalNode) + } + componentWillUnmount() { + this.portalNode.parentNode.removeChild(this.portalNode) + } + render() { + return ReactDOM.createPortal( + , + this.portalNode, + ) + } + } -test('supports fragments', () => { - class Test extends React.Component { - render() { + function Greet({greeting, subject}) { return ( -) } - } - const {asFragment} = render(DocumentFragment
is pretty cool! + + {greeting} {subject} +) - expect(asFragment()).toMatchSnapshot() -}) + const {unmount} = render( ) + expect(screen.getByText('Hello World')).toBeInTheDocument() + const portalNode = screen.getByTestId('my-portal') + expect(portalNode).toBeInTheDocument() + unmount() + expect(portalNode).not.toBeInTheDocument() + }) -test('renders options.wrapper around node', () => { - const WrapperComponent = ({children}) => ( - {children}- ) + test('returns baseElement which defaults to document.body', () => { + const {baseElement} = render() + expect(baseElement).toBe(document.body) + }) + + test('supports fragments', () => { + class Test extends React.Component { + render() { + return ( +++ ) + } + } - const {container} = render(, { - wrapper: WrapperComponent, + const {asFragment} = render(DocumentFragment
is pretty cool! +) + expect(asFragment()).toMatchSnapshot() }) - expect(screen.getByTestId('wrapper')).toBeInTheDocument() - expect(container.firstChild).toMatchInlineSnapshot(` + test('renders options.wrapper around node', () => { + const WrapperComponent = ({children}) => ( + {children}+ ) + + const {container} = render(, { + wrapper: WrapperComponent, + }) + + expect(screen.getByTestId('wrapper')).toBeInTheDocument() + expect(container.firstChild).toMatchInlineSnapshot(`@@ -87,102 +103,138 @@ test('renders options.wrapper around node', () => { />`) -}) + }) -test('flushes useEffect cleanup functions sync on unmount()', () => { - const spy = jest.fn() - function Component() { - React.useEffect(() => spy, []) - return null - } - const {unmount} = render() - expect(spy).toHaveBeenCalledTimes(0) + test('renders options.wrapper around node when reactStrictMode is true', () => { + configure({reactStrictMode: true}) - unmount() + const WrapperComponent = ({children}) => ( + {children}+ ) + const {container} = render(, { + wrapper: WrapperComponent, + }) - expect(spy).toHaveBeenCalledTimes(1) -}) + expect(screen.getByTestId('wrapper')).toBeInTheDocument() + expect(container.firstChild).toMatchInlineSnapshot(` ++ ++ `) + }) + + test('renders twice when reactStrictMode is true', () => { + configure({reactStrictMode: true}) -test('can be called multiple times on the same container', () => { - const container = document.createElement('div') + const spy = jest.fn() + function Component() { + spy() + return null + } - const {unmount} = render(, {container}) + render() + expect(spy).toHaveBeenCalledTimes(2) + }) - expect(container).toContainHTML('') + test('flushes useEffect cleanup functions sync on unmount()', () => { + const spy = jest.fn() + function Component() { + React.useEffect(() => spy, []) + return null + } + const {unmount} = render( ) + expect(spy).toHaveBeenCalledTimes(0) - render(, {container}) + unmount() - expect(container).toContainHTML('') + expect(spy).toHaveBeenCalledTimes(1) + }) - unmount() + test('can be called multiple times on the same container', () => { + const container = document.createElement('div') - expect(container).toBeEmptyDOMElement() -}) + const {unmount} = render(, {container}) -test('hydrate will make the UI interactive', () => { - function App() { - const [clicked, handleClick] = React.useReducer(n => n + 1, 0) + expect(container).toContainHTML('') - return ( - - ) - } - const ui = - const container = document.createElement('div') - document.body.appendChild(container) - container.innerHTML = ReactDOMServer.renderToString(ui) + render(, {container}) - expect(container).toHaveTextContent('clicked:0') + expect(container).toContainHTML('') - render(ui, {container, hydrate: true}) + unmount() - fireEvent.click(container.querySelector('button')) + expect(container).toBeEmptyDOMElement() + }) - expect(container).toHaveTextContent('clicked:1') -}) + test('hydrate will make the UI interactive', () => { + function App() { + const [clicked, handleClick] = React.useReducer(n => n + 1, 0) -test('hydrate can have a wrapper', () => { - const wrapperComponentMountEffect = jest.fn() - function WrapperComponent({children}) { - React.useEffect(() => { - wrapperComponentMountEffect() - }) + return ( + + ) + } + const ui = + const container = document.createElement('div') + document.body.appendChild(container) + container.innerHTML = ReactDOMServer.renderToString(ui) - return children - } - const ui = - const container = document.createElement('div') - document.body.appendChild(container) - container.innerHTML = ReactDOMServer.renderToString(ui) + expect(container).toHaveTextContent('clicked:0') - render(ui, {container, hydrate: true, wrapper: WrapperComponent}) + render(ui, {container, hydrate: true}) - expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(1) -}) + fireEvent.click(container.querySelector('button')) -test('legacyRoot uses legacy ReactDOM.render', () => { - expect(() => { - render(, {legacyRoot: true}) - }).toErrorDev( - [ - "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", - ], - {withoutStack: true}, - ) -}) + expect(container).toHaveTextContent('clicked:1') + }) + + test('hydrate can have a wrapper', () => { + const wrapperComponentMountEffect = jest.fn() + function WrapperComponent({children}) { + React.useEffect(() => { + wrapperComponentMountEffect() + }) + + return children + } + const ui = + const container = document.createElement('div') + document.body.appendChild(container) + container.innerHTML = ReactDOMServer.renderToString(ui) -test('legacyRoot uses legacy ReactDOM.hydrate', () => { - const ui = - const container = document.createElement('div') - container.innerHTML = ReactDOMServer.renderToString(ui) - expect(() => { - render(ui, {container, hydrate: true, legacyRoot: true}) - }).toErrorDev( - [ - "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", - ], - {withoutStack: true}, - ) + render(ui, {container, hydrate: true, wrapper: WrapperComponent}) + + expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(1) + }) + + test('legacyRoot uses legacy ReactDOM.render', () => { + expect(() => { + render(, {legacyRoot: true}) + }).toErrorDev( + [ + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], + {withoutStack: true}, + ) + }) + + test('legacyRoot uses legacy ReactDOM.hydrate', () => { + const ui = + const container = document.createElement('div') + container.innerHTML = ReactDOMServer.renderToString(ui) + expect(() => { + render(ui, {container, hydrate: true, legacyRoot: true}) + }).toErrorDev( + [ + "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], + {withoutStack: true}, + ) + }) }) diff --git a/src/__tests__/rerender.js b/src/__tests__/rerender.js index be3c259c..6c48c4dd 100644 --- a/src/__tests__/rerender.js +++ b/src/__tests__/rerender.js @@ -1,31 +1,98 @@ import * as React from 'react' -import {render} from '../' - -test('rerender will re-render the element', () => { - const Greeting = props => {props.message}- const {container, rerender} = render() - expect(container.firstChild).toHaveTextContent('hi') - rerender( ) - expect(container.firstChild).toHaveTextContent('hey') -}) +import {render, configure} from '../' + +describe('rerender API', () => { + let originalConfig + beforeEach(() => { + // Grab the existing configuration so we can restore + // it at the end of the test + configure(existingConfig => { + originalConfig = existingConfig + // Don't change the existing config + return {} + }) + }) + + afterEach(() => { + configure(originalConfig) + }) + + test('rerender will re-render the element', () => { + const Greeting = props => {props.message}+ const {container, rerender} = render() + expect(container.firstChild).toHaveTextContent('hi') + rerender( ) + expect(container.firstChild).toHaveTextContent('hey') + }) + + test('hydrate will not update props until next render', () => { + const initialInputElement = document.createElement('input') + const container = document.createElement('div') + container.appendChild(initialInputElement) + document.body.appendChild(container) + + const firstValue = 'hello' + initialInputElement.value = firstValue -test('hydrate will not update props until next render', () => { - const initialInputElement = document.createElement('input') - const container = document.createElement('div') - container.appendChild(initialInputElement) - document.body.appendChild(container) + const {rerender} = render( null} />, { + container, + hydrate: true, + }) - const firstValue = 'hello' - initialInputElement.value = firstValue + expect(initialInputElement).toHaveValue(firstValue) - const {rerender} = render( null} />, { - container, - hydrate: true, + const secondValue = 'goodbye' + rerender( null} />) + expect(initialInputElement).toHaveValue(secondValue) }) - expect(initialInputElement).toHaveValue(firstValue) + test('re-renders options.wrapper around node when reactStrictMode is true', () => { + configure({reactStrictMode: true}) - const secondValue = 'goodbye' - rerender( null} />) - expect(initialInputElement).toHaveValue(secondValue) + const WrapperComponent = ({children}) => ( + {children}+ ) + const Greeting = props =>{props.message}+ const {container, rerender} = render(, { + wrapper: WrapperComponent, + }) + + expect(container.firstChild).toMatchInlineSnapshot(` + ++ `) + + rerender(+ hi ++) + expect(container.firstChild).toMatchInlineSnapshot(` + ++ `) + }) + + test('re-renders twice when reactStrictMode is true', () => { + configure({reactStrictMode: true}) + + const spy = jest.fn() + function Component() { + spy() + return null + } + + const {rerender} = render(+ hey ++) + expect(spy).toHaveBeenCalledTimes(2) + + spy.mockClear() + rerender( ) + expect(spy).toHaveBeenCalledTimes(2) + }) }) diff --git a/src/config.js b/src/config.js new file mode 100644 index 00000000..dc8a5035 --- /dev/null +++ b/src/config.js @@ -0,0 +1,34 @@ +import { + getConfig as getConfigDTL, + configure as configureDTL, +} from '@testing-library/dom' + +let configForRTL = { + reactStrictMode: false, +} + +function getConfig() { + return { + ...getConfigDTL(), + ...configForRTL, + } +} + +function configure(newConfig) { + if (typeof newConfig === 'function') { + // Pass the existing config out to the provided function + // and accept a delta in return + newConfig = newConfig(getConfig()) + } + + const {reactStrictMode, ...configForDTL} = newConfig + + configureDTL(configForDTL) + + configForRTL = { + ...configForRTL, + reactStrictMode, + } +} + +export {getConfig, configure} diff --git a/src/pure.js b/src/pure.js index 845aede1..3939a11a 100644 --- a/src/pure.js +++ b/src/pure.js @@ -11,6 +11,7 @@ import act, { setReactActEnvironment, } from './act-compat' import {fireEvent} from './fire-event' +import {getConfig, configure} from './config' function jestFakeTimersAreEnabled() { /* istanbul ignore else */ @@ -76,6 +77,18 @@ const mountedContainers = new Set() */ const mountedRootEntries = [] +function strictModeIfNeeded(innerElement) { + return getConfig().reactStrictMode + ? React.createElement(React.StrictMode, null, innerElement) + : innerElement +} + +function wrapUiIfNeeded(innerElement, wrapperComponent) { + return wrapperComponent + ? React.createElement(wrapperComponent, null, innerElement) + : innerElement +} + function createConcurrentRoot( container, {hydrate, ui, wrapper: WrapperComponent}, @@ -85,7 +98,7 @@ function createConcurrentRoot( act(() => { root = ReactDOMClient.hydrateRoot( container, - WrapperComponent ? React.createElement(WrapperComponent, null, ui) : ui, + strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), ) }) } else { @@ -129,16 +142,17 @@ function renderRoot( ui, {baseElement, container, hydrate, queries, root, wrapper: WrapperComponent}, ) { - const wrapUiIfNeeded = innerElement => - WrapperComponent - ? React.createElement(WrapperComponent, null, innerElement) - : innerElement - act(() => { if (hydrate) { - root.hydrate(wrapUiIfNeeded(ui), container) + root.hydrate( + strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), + container, + ) } else { - root.render(wrapUiIfNeeded(ui), container) + root.render( + strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), + container, + ) } }) @@ -157,10 +171,11 @@ function renderRoot( }) }, rerender: rerenderUi => { - renderRoot(wrapUiIfNeeded(rerenderUi), { + renderRoot(rerenderUi, { container, baseElement, root, + wrapper: WrapperComponent, }) // Intentionally do not return anything to avoid unnecessarily complicating the API. // folks can use all the same utilities we return in the first place that are bound to the container @@ -276,6 +291,6 @@ function renderHook(renderCallback, options = {}) { // just re-export everything from dom-testing-library export * from '@testing-library/dom' -export {render, renderHook, cleanup, act, fireEvent} +export {render, renderHook, cleanup, act, fireEvent, getConfig, configure} /* eslint func-name-matching:0 */ diff --git a/types/index.d.ts b/types/index.d.ts index 558edfad..1f1135c5 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -5,12 +5,25 @@ import { Queries, BoundFunction, prettyFormat, + Config as ConfigDTL, } from '@testing-library/dom' import {Renderer} from 'react-dom' import {act as reactAct} from 'react-dom/test-utils' export * from '@testing-library/dom' +export interface Config extends ConfigDTL { + reactStrictMode: boolean +} + +export interface ConfigFn { + (existingConfig: Config): Partial +} + +export function configure(configDelta: ConfigFn | Partial ): void + +export function getConfig(): Config + export type RenderResult< Q extends Queries = typeof queries, Container extends Element | DocumentFragment = HTMLElement, diff --git a/types/test.tsx b/types/test.tsx index c33f07b6..3486a9a8 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -62,6 +62,28 @@ export function testFireEvent() { fireEvent.click(container) } +export function testConfigure() { + // test for DTL's config + pure.configure({testIdAttribute: 'foobar'}) + pure.configure(existingConfig => ({ + testIdAttribute: `modified-${existingConfig.testIdAttribute}`, + })) + + // test for RTL's config + pure.configure({reactStrictMode: true}) + pure.configure(existingConfig => ({ + reactStrictMode: !existingConfig.reactStrictMode, + })) +} + +export function testGetConfig() { + // test for DTL's config + pure.getConfig().testIdAttribute + + // test for RTL's config + pure.getConfig().reactStrictMode +} + export function testDebug() { const {debug, getAllByTestId} = render( <> From 4509fb68aaf42f3b750e57a3e2d073a498fc59db Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 22:21:05 +0100 Subject: [PATCH 098/126] docs: add yinm as a contributor for code (#1269) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 16957ca9..de2ba851 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1353,6 +1353,15 @@ "contributions": [ "doc" ] + }, + { + "login": "yinm", + "name": "Yusuke Iinuma", + "avatar_url": "https://avatars.githubusercontent.com/u/13295106?v=4", + "profile": "http://yinm.info", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index a3731749..1ffc881d 100644 --- a/README.md +++ b/README.md @@ -630,6 +630,7 @@ Thanks goes to these people ([emoji key][emojis]): +
Colin Diesh
π
Yusuke Iinuma
π»Date: Thu, 1 Feb 2024 11:49:10 -0800 Subject: [PATCH 099/126] fix: Update types to support all possible react component return values (#1272) * Update types to support all possible react component return values * Update type test types --- types/index.d.ts | 8 ++++---- types/test.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 1f1135c5..5db1d201 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -39,7 +39,7 @@ export type RenderResult< maxLength?: number, options?: prettyFormat.OptionsReceived, ) => void - rerender: (ui: React.ReactElement) => void + rerender: (ui: React.ReactNode) => void unmount: () => void asFragment: () => DocumentFragment } & {[P in keyof Q]: BoundFunction } @@ -90,7 +90,7 @@ export interface RenderOptions< * * @see https://testing-library.com/docs/react-testing-library/api/#wrapper */ - wrapper?: React.JSXElementConstructor<{children: React.ReactElement}> + wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> } type Omit= Pick > @@ -103,11 +103,11 @@ export function render< Container extends Element | DocumentFragment = HTMLElement, BaseElement extends Element | DocumentFragment = Container, >( - ui: React.ReactElement, + ui: React.ReactNode, options: RenderOptions , ): RenderResultexport function render( - ui: React.ReactElement, + ui: React.ReactNode, options?: Omit, ): RenderResult diff --git a/types/test.tsx b/types/test.tsx index 3486a9a8..6ff899de 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -123,10 +123,10 @@ export function testQueries() { } export function wrappedRender( - ui: React.ReactElement, + ui: React.ReactNode, options?: pure.RenderOptions, ) { - const Wrapper = ({children}: {children: React.ReactElement}): JSX.Element => { + const Wrapper = ({children}: {children: React.ReactNode}): JSX.Element => { return {children}} @@ -134,7 +134,7 @@ export function wrappedRender( } export function wrappedRenderB( - ui: React.ReactElement, + ui: React.ReactNode, options?: pure.RenderOptions, ) { const Wrapper: React.FunctionComponent<{children?: React.ReactNode}> = ({ @@ -147,7 +147,7 @@ export function wrappedRenderB( } export function wrappedRenderC( - ui: React.ReactElement, + ui: React.ReactNode, options?: pure.RenderOptions, ) { interface AppWrapperProps { From edb6344d578a8c224daf0cd6e2984f36cc6e8d86 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 20:49:43 +0100 Subject: [PATCH 100/126] docs: add trappar as a contributor for code (#1273) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index de2ba851..c3b86064 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1362,6 +1362,15 @@ "contributions": [ "code" ] + }, + { + "login": "trappar", + "name": "Jeff Way", + "avatar_url": "https://avatars.githubusercontent.com/u/525726?v=4", + "profile": "https://github.com/trappar", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 1ffc881d..85613475 100644 --- a/README.md +++ b/README.md @@ -631,6 +631,7 @@ Thanks goes to these people ([emoji key][emojis]):From 7e42f4e84115510f560be36b5febb3d9f20e8899 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann
Colin Diesh
π+
Yusuke Iinuma
π»
Jeff Way
π»Date: Tue, 19 Mar 2024 23:24:53 +0100 Subject: [PATCH 101/126] chore: Fix tests (#1288) --- jest.config.js | 16 ++++++++++++++++ package.json | 2 +- src/__tests__/render.js | 31 +++++++++++++++++++++++-------- src/__tests__/renderHook.js | 19 +++++++++++++++---- tests/toWarnDev.js | 2 +- 5 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 jest.config.js diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..30654cdb --- /dev/null +++ b/jest.config.js @@ -0,0 +1,16 @@ +const {jest: jestConfig} = require('kcd-scripts/config') + +module.exports = Object.assign(jestConfig, { + coverageThreshold: { + ...jestConfig.coverageThreshold, + // Full coverage across the build matrix (React versions) but not in a single job + // Ful coverage is checked via codecov + './src/pure.js': { + // minimum coverage of jobs using different React versions + branches: 97, + functions: 88, + lines: 94, + statements: 94, + }, + }, +}) diff --git a/package.json b/package.json index 70aebdad..0b7f83d8 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@testing-library/jest-dom": "^5.11.6", "chalk": "^4.1.2", "dotenv-cli": "^4.0.0", - "jest-diff": "^29.4.1", + "jest-diff": "^29.7.0", "kcd-scripts": "^13.0.0", "npm-run-all": "^4.1.5", "react": "^18.0.0", diff --git a/src/__tests__/render.js b/src/__tests__/render.js index 39f4bc92..b5222d81 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -3,6 +3,13 @@ import ReactDOM from 'react-dom' import ReactDOMServer from 'react-dom/server' import {fireEvent, render, screen, configure} from '../' +// Needs to be changed to 19.0.0 once alpha started. +const isReactExperimental = React.version.startsWith('18.3.0-experimental') +const isReactCanary = React.version.startsWith('18.3.0') + +// Needs to be changed to isReactExperimental || isReactCanary once alpha started. +const testGateReact18 = isReactExperimental ? test.skip : test + describe('render API', () => { let originalConfig beforeEach(() => { @@ -213,27 +220,35 @@ describe('render API', () => { expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(1) }) - test('legacyRoot uses legacy ReactDOM.render', () => { + testGateReact18('legacyRoot uses legacy ReactDOM.render', () => { expect(() => { render(, {legacyRoot: true}) }).toErrorDev( - [ - "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", - ], + isReactCanary + ? [ + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://react.dev/link/switch-to-createroot", + ] + : [ + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], {withoutStack: true}, ) }) - test('legacyRoot uses legacy ReactDOM.hydrate', () => { + testGateReact18('legacyRoot uses legacy ReactDOM.hydrate', () => { const ui = const container = document.createElement('div') container.innerHTML = ReactDOMServer.renderToString(ui) expect(() => { render(ui, {container, hydrate: true, legacyRoot: true}) }).toErrorDev( - [ - "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", - ], + isReactCanary + ? [ + "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://react.dev/link/switch-to-createroot", + ] + : [ + "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], {withoutStack: true}, ) }) diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js index 11b7009a..34259b44 100644 --- a/src/__tests__/renderHook.js +++ b/src/__tests__/renderHook.js @@ -1,6 +1,13 @@ import React from 'react' import {renderHook} from '../pure' +// Needs to be changed to 19.0.0 once alpha started. +const isReactExperimental = React.version.startsWith('18.3.0-experimental') +const isReactCanary = React.version.startsWith('18.3.0') + +// Needs to be changed to isReactExperimental || isReactCanary once alpha started. +const testGateReact18 = isReactExperimental ? test.skip : test + test('gives committed result', () => { const {result} = renderHook(() => { const [state, setState] = React.useState(1) @@ -61,7 +68,7 @@ test('allows wrapper components', async () => { expect(result.current).toEqual('provided') }) -test('legacyRoot uses legacy ReactDOM.render', () => { +testGateReact18('legacyRoot uses legacy ReactDOM.render', () => { const Context = React.createContext('default') function Wrapper({children}) { return {children} @@ -78,9 +85,13 @@ test('legacyRoot uses legacy ReactDOM.render', () => { }, ).result }).toErrorDev( - [ - "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", - ], + isReactCanary + ? [ + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://react.dev/link/switch-to-createroot", + ] + : [ + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], {withoutStack: true}, ) expect(result.current).toEqual('provided') diff --git a/tests/toWarnDev.js b/tests/toWarnDev.js index ca58346f..2aae39f0 100644 --- a/tests/toWarnDev.js +++ b/tests/toWarnDev.js @@ -29,7 +29,7 @@ SOFTWARE. /* eslint-disable func-names */ /* eslint-disable complexity */ const util = require('util') -const jestDiff = require('jest-diff').default +const jestDiff = require('jest-diff').diff const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError') function normalizeCodeLocInfo(str) { From 3da62fd9741ca74bcd0d2bc668ba76a2d8f3751f Mon Sep 17 00:00:00 2001 From: Sebastian SilbermannDate: Tue, 19 Mar 2024 23:54:12 +0100 Subject: [PATCH 102/126] fix: Remove unused types (#1287) --- types/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 5db1d201..49a1b7ff 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,7 +7,6 @@ import { prettyFormat, Config as ConfigDTL, } from '@testing-library/dom' -import {Renderer} from 'react-dom' import {act as reactAct} from 'react-dom/test-utils' export * from '@testing-library/dom' From cf045b4743afeb651b14bd7bb0d04b955768c010 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 20 Mar 2024 00:08:40 +0100 Subject: [PATCH 103/126] chore: Update Codecov configuration to latest (#1289) --- .github/workflows/validate.yml | 12 +++++++----- codecov.yml | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 codecov.yml diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f1359d76..c2e20a61 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -34,10 +34,10 @@ jobs: runs-on: ubuntu-latest steps: - name: β¬οΈ Checkout repo - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: β Setup node - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -59,9 +59,11 @@ jobs: run: npm run validate - name: β¬οΈ Upload coverage report - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 with: + fail_ci_if_error: true flags: ${{ matrix.react }} + token: ${{ secrets.CODECOV_TOKEN }} release: permissions: @@ -76,10 +78,10 @@ jobs: github.event_name == 'push' }} steps: - name: β¬οΈ Checkout repo - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: β Setup node - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: 14 diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..472fcd83 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,20 @@ +coverage: + status: + project: + default: + # basic + target: 100% + threshold: 0% + flags: + - canary + - experimental + - latest + branches: + - main + - 12.x + if_ci_failed: success + if_not_found: failure + informational: false + only_pulls: false +github_checks: + annotations: true From 4e10ba3a788f6f66287dab5bb4a09f658664ec50 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Thu, 21 Mar 2024 11:24:52 +0200 Subject: [PATCH 104/126] chore: change canary version to specific prefix (#1290) --- src/__tests__/render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/render.js b/src/__tests__/render.js index b5222d81..175174ca 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -5,7 +5,7 @@ import {fireEvent, render, screen, configure} from '../' // Needs to be changed to 19.0.0 once alpha started. const isReactExperimental = React.version.startsWith('18.3.0-experimental') -const isReactCanary = React.version.startsWith('18.3.0') +const isReactCanary = React.version.startsWith('18.3.0-canary') // Needs to be changed to isReactExperimental || isReactCanary once alpha started. const testGateReact18 = isReactExperimental ? test.skip : test From 9c4a46d5b9923c21c936d206614a8febcc939fc2 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 8 Apr 2024 16:07:24 +0200 Subject: [PATCH 105/126] feat: Add support for React 19 Canary (#1294) --- .github/workflows/validate.yml | 2 +- jest.config.js | 15 ++++++----- src/__tests__/new-act.js | 13 +++++---- src/__tests__/render.js | 48 ++++++++++++++++++++-------------- src/__tests__/renderHook.js | 39 ++++++++++++++++++--------- src/act-compat.js | 7 ++--- src/pure.js | 17 ++++++++++++ types/index.d.ts | 1 + 8 files changed, 96 insertions(+), 46 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c2e20a61..4a20e2ab 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: node: [14, 16, 18] - react: [latest, canary, experimental] + react: ['18.x', latest, canary, experimental] runs-on: ubuntu-latest steps: - name: β¬οΈ Checkout repo diff --git a/jest.config.js b/jest.config.js index 30654cdb..860358cd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,14 +3,17 @@ const {jest: jestConfig} = require('kcd-scripts/config') module.exports = Object.assign(jestConfig, { coverageThreshold: { ...jestConfig.coverageThreshold, - // Full coverage across the build matrix (React versions) but not in a single job + // Full coverage across the build matrix (React 18, 19) but not in a single job // Ful coverage is checked via codecov - './src/pure.js': { - // minimum coverage of jobs using different React versions - branches: 97, + './src/act-compat': { + branches: 90, + }, + './src/pure': { + // minimum coverage of jobs using React 18 and 19 + branches: 95, functions: 88, - lines: 94, - statements: 94, + lines: 92, + statements: 92, }, }, }) diff --git a/src/__tests__/new-act.js b/src/__tests__/new-act.js index 0412a8a3..0464ad24 100644 --- a/src/__tests__/new-act.js +++ b/src/__tests__/new-act.js @@ -1,10 +1,13 @@ let asyncAct -jest.mock('react-dom/test-utils', () => ({ - act: cb => { - return cb() - }, -})) +jest.mock('react', () => { + return { + ...jest.requireActual('react'), + act: cb => { + return cb() + }, + } +}) beforeEach(() => { jest.resetModules() diff --git a/src/__tests__/render.js b/src/__tests__/render.js index 175174ca..16f7dbe2 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -3,12 +3,11 @@ import ReactDOM from 'react-dom' import ReactDOMServer from 'react-dom/server' import {fireEvent, render, screen, configure} from '../' -// Needs to be changed to 19.0.0 once alpha started. -const isReactExperimental = React.version.startsWith('18.3.0-experimental') -const isReactCanary = React.version.startsWith('18.3.0-canary') +const isReact18 = React.version.startsWith('18.') +const isReact19 = React.version.startsWith('19.') -// Needs to be changed to isReactExperimental || isReactCanary once alpha started. -const testGateReact18 = isReactExperimental ? test.skip : test +const testGateReact18 = isReact18 ? test : test.skip +const testGateReact19 = isReact19 ? test : test.skip describe('render API', () => { let originalConfig @@ -224,17 +223,21 @@ describe('render API', () => { expect(() => { render(, {legacyRoot: true}) }).toErrorDev( - isReactCanary - ? [ - "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://react.dev/link/switch-to-createroot", - ] - : [ - "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", - ], + [ + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], {withoutStack: true}, ) }) + testGateReact19('legacyRoot throws', () => { + expect(() => { + render(, {legacyRoot: true}) + }).toThrowErrorMatchingInlineSnapshot( + `\`legacyRoot: true\` is not supported in this version of React. Please use React 18 instead.`, + ) + }) + testGateReact18('legacyRoot uses legacy ReactDOM.hydrate', () => { const ui = const container = document.createElement('div') @@ -242,14 +245,21 @@ describe('render API', () => { expect(() => { render(ui, {container, hydrate: true, legacyRoot: true}) }).toErrorDev( - isReactCanary - ? [ - "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://react.dev/link/switch-to-createroot", - ] - : [ - "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", - ], + [ + "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], {withoutStack: true}, ) }) + + testGateReact19('legacyRoot throws even with hydrate', () => { + const ui = + const container = document.createElement('div') + container.innerHTML = ReactDOMServer.renderToString(ui) + expect(() => { + render(ui, {container, hydrate: true, legacyRoot: true}) + }).toThrowErrorMatchingInlineSnapshot( + `\`legacyRoot: true\` is not supported in this version of React. Please use React 18 instead.`, + ) + }) }) diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js index 34259b44..c7c8b066 100644 --- a/src/__tests__/renderHook.js +++ b/src/__tests__/renderHook.js @@ -1,12 +1,11 @@ import React from 'react' import {renderHook} from '../pure' -// Needs to be changed to 19.0.0 once alpha started. -const isReactExperimental = React.version.startsWith('18.3.0-experimental') -const isReactCanary = React.version.startsWith('18.3.0') +const isReact18 = React.version.startsWith('18.') +const isReact19 = React.version.startsWith('19.') -// Needs to be changed to isReactExperimental || isReactCanary once alpha started. -const testGateReact18 = isReactExperimental ? test.skip : test +const testGateReact18 = isReact18 ? test : test.skip +const testGateReact19 = isReact19 ? test : test.skip test('gives committed result', () => { const {result} = renderHook(() => { @@ -85,14 +84,30 @@ testGateReact18('legacyRoot uses legacy ReactDOM.render', () => { }, ).result }).toErrorDev( - isReactCanary - ? [ - "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://react.dev/link/switch-to-createroot", - ] - : [ - "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", - ], + [ + "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", + ], {withoutStack: true}, ) expect(result.current).toEqual('provided') }) + +testGateReact19('legacyRoot throws', () => { + const Context = React.createContext('default') + function Wrapper({children}) { + return {children} + } + expect(() => { + renderHook( + () => { + return React.useContext(Context) + }, + { + wrapper: Wrapper, + legacyRoot: true, + }, + ).result + }).toThrowErrorMatchingInlineSnapshot( + `\`legacyRoot: true\` is not supported in this version of React. Please use React 18 instead.`, + ) +}) diff --git a/src/act-compat.js b/src/act-compat.js index 86518196..5877755c 100644 --- a/src/act-compat.js +++ b/src/act-compat.js @@ -1,6 +1,7 @@ -import * as testUtils from 'react-dom/test-utils' +import * as React from 'react' +import * as DeprecatedReactTestUtils from 'react-dom/test-utils' -const domAct = testUtils.act +const reactAct = React.act ?? DeprecatedReactTestUtils.act function getGlobalThis() { /* istanbul ignore else */ @@ -78,7 +79,7 @@ function withGlobalActEnvironment(actImplementation) { } } -const act = withGlobalActEnvironment(domAct) +const act = withGlobalActEnvironment(reactAct) export default act export { diff --git a/src/pure.js b/src/pure.js index 3939a11a..38ec519f 100644 --- a/src/pure.js +++ b/src/pure.js @@ -207,6 +207,14 @@ function render( wrapper, } = {}, ) { + if (legacyRoot && typeof ReactDOM.render !== 'function') { + const error = new Error( + '`legacyRoot: true` is not supported in this version of React. Please use React 18 instead.', + ) + Error.captureStackTrace(error, render) + throw error + } + if (!baseElement) { // default to document.body instead of documentElement to avoid output of potentially-large // head elements (such as JSS style blocks) in debug output @@ -263,6 +271,15 @@ function cleanup() { function renderHook(renderCallback, options = {}) { const {initialProps, ...renderOptions} = options + + if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') { + const error = new Error( + '`legacyRoot: true` is not supported in this version of React. Please use React 18 instead.', + ) + Error.captureStackTrace(error, renderHook) + throw error + } + const result = React.createRef() function TestComponent({renderCallbackProps}) { diff --git a/types/index.d.ts b/types/index.d.ts index 49a1b7ff..e7cf02bc 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -73,6 +73,7 @@ export interface RenderOptions< */ hydrate?: boolean /** + * Only works if used with React 18. * Set to `true` if you want to force synchronous `ReactDOM.render`. * Otherwise `render` will default to concurrent React if available. */ From 787cb85f8baa3d2e2a9916b7dad12c0a76d787a4 Mon Sep 17 00:00:00 2001 From: Matan BorenkraoutDate: Wed, 10 Apr 2024 18:18:28 +0300 Subject: [PATCH 106/126] Release: 15.0.0 (#1295) BREAKING CHANGE: Minimum supported Node.js version is 18.0 BREAKING CHANGE: New version of `@testing-library/dom` changes various roles. Check out the changed tests in https://github.com/testing-library/dom-testing-library/commit/2c570553d8f31b008451398152a9bd30bce362b3 to get an overview about what changed. --- .codesandbox/ci.json | 2 +- .github/workflows/validate.yml | 2 +- package.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index d5850328..002bafb4 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,5 +1,5 @@ { "installCommand": "install:csb", "sandboxes": ["new", "github/kentcdodds/react-testing-library-examples"], - "node": "14" + "node": "18" } diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4a20e2ab..aa4eeed7 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - node: [14, 16, 18] + node: [18, 20] react: ['18.x', latest, canary, experimental] runs-on: ubuntu-latest steps: diff --git a/package.json b/package.json index 0b7f83d8..9483256a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "types": "types/index.d.ts", "module": "dist/@testing-library/react.esm.js", "engines": { - "node": ">=14" + "node": ">=18" }, "scripts": { "prebuild": "rimraf dist", @@ -46,7 +46,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^9.0.0", + "@testing-library/dom": "^10.0.0", "@types/react-dom": "^18.0.0" }, "devDependencies": { From 1645d21950ab8e3c6740b7e51b8a179a4c975c24 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Thu, 11 Apr 2024 19:03:17 +0200 Subject: [PATCH 107/126] fix: Stop using nullish coalescing (#1299) --- src/act-compat.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/act-compat.js b/src/act-compat.js index 5877755c..6eaec0fb 100644 --- a/src/act-compat.js +++ b/src/act-compat.js @@ -1,7 +1,8 @@ import * as React from 'react' import * as DeprecatedReactTestUtils from 'react-dom/test-utils' -const reactAct = React.act ?? DeprecatedReactTestUtils.act +const reactAct = + typeof React.act === 'function' ? React.act : DeprecatedReactTestUtils.act function getGlobalThis() { /* istanbul ignore else */ From c63b873072d62c858959c2a19e68f8e2cc0b11be Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sat, 13 Apr 2024 11:21:10 +0200 Subject: [PATCH 108/126] fix: Improve `legacyRoot` error message (#1301) --- src/__tests__/render.js | 4 ++-- src/__tests__/renderHook.js | 2 +- src/pure.js | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/__tests__/render.js b/src/__tests__/render.js index 16f7dbe2..f00410b4 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -234,7 +234,7 @@ describe('render API', () => { expect(() => { render(, {legacyRoot: true}) }).toThrowErrorMatchingInlineSnapshot( - `\`legacyRoot: true\` is not supported in this version of React. Please use React 18 instead.`, + `\`legacyRoot: true\` is not supported in this version of React. If your app runs React 19 or later, you should remove this flag. If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.`, ) }) @@ -259,7 +259,7 @@ describe('render API', () => { expect(() => { render(ui, {container, hydrate: true, legacyRoot: true}) }).toThrowErrorMatchingInlineSnapshot( - `\`legacyRoot: true\` is not supported in this version of React. Please use React 18 instead.`, + `\`legacyRoot: true\` is not supported in this version of React. If your app runs React 19 or later, you should remove this flag. If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.`, ) }) }) diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js index c7c8b066..fe7551a2 100644 --- a/src/__tests__/renderHook.js +++ b/src/__tests__/renderHook.js @@ -108,6 +108,6 @@ testGateReact19('legacyRoot throws', () => { }, ).result }).toThrowErrorMatchingInlineSnapshot( - `\`legacyRoot: true\` is not supported in this version of React. Please use React 18 instead.`, + `\`legacyRoot: true\` is not supported in this version of React. If your app runs React 19 or later, you should remove this flag. If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.`, ) }) diff --git a/src/pure.js b/src/pure.js index 38ec519f..f546af98 100644 --- a/src/pure.js +++ b/src/pure.js @@ -209,7 +209,9 @@ function render( ) { if (legacyRoot && typeof ReactDOM.render !== 'function') { const error = new Error( - '`legacyRoot: true` is not supported in this version of React. Please use React 18 instead.', + '`legacyRoot: true` is not supported in this version of React. ' + + 'If your app runs React 19 or later, you should remove this flag. ' + + 'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.', ) Error.captureStackTrace(error, render) throw error @@ -274,7 +276,9 @@ function renderHook(renderCallback, options = {}) { if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') { const error = new Error( - '`legacyRoot: true` is not supported in this version of React. Please use React 18 instead.', + '`legacyRoot: true` is not supported in this version of React. ' + + 'If your app runs React 19 or later, you should remove this flag. ' + + 'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.', ) Error.captureStackTrace(error, renderHook) throw error From 067d0c6d2e87092f6ecaa8c9fcf505e4576055cf Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 23 Apr 2024 12:21:18 +0200 Subject: [PATCH 109/126] fix: Don't raise TypeScript errors when hydating `document` (#1304) --- types/index.d.ts | 60 ++++++++++++++++++++++++++++++++++++++++++------ types/test.tsx | 11 +++++++++ 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index e7cf02bc..566e3d05 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,5 +1,5 @@ // TypeScript Version: 3.8 - +import * as ReactDOMClient from 'react-dom/client' import { queries, Queries, @@ -43,10 +43,10 @@ export type RenderResult< asFragment: () => DocumentFragment } & {[P in keyof Q]: BoundFunction } -export interface RenderOptions< - Q extends Queries = typeof queries, - Container extends Element | DocumentFragment = HTMLElement, - BaseElement extends Element | DocumentFragment = Container, +export interface BaseRenderOptions< + Q extends Queries, + Container extends RendererableContainer | HydrateableContainer, + BaseElement extends Element | DocumentFragment, > { /** * By default, React Testing Library will create a div and append that div to the document.body. Your React component will be rendered in the created div. If you provide your own HTMLElement container via this option, @@ -93,6 +93,44 @@ export interface RenderOptions< wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> } +type RendererableContainer = ReactDOMClient.Container +type HydrateableContainer = Parameters[0] +export interface ClientRenderOptions< + Q extends Queries, + Container extends Element | DocumentFragment, + BaseElement extends Element | DocumentFragment = Container, +> extends BaseRenderOptions { + /** + * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side + * rendering and use ReactDOM.hydrate to mount your components. + * + * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) + */ + hydrate?: false | undefined +} + +export interface HydrateOptions< + Q extends Queries, + Container extends Element | DocumentFragment, + BaseElement extends Element | DocumentFragment = Container, +> extends BaseRenderOptions{ + /** + * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side + * rendering and use ReactDOM.hydrate to mount your components. + * + * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) + */ + hydrate: true +} + +export type RenderOptions< + Q extends Queries = typeof queries, + Container extends RendererableContainer | HydrateableContainer = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +> = + | ClientRenderOptions+ | HydrateOptions+ type Omit= Pick > /** @@ -100,11 +138,19 @@ type Omit = Pick > */ export function render< Q extends Queries = typeof queries, - Container extends Element | DocumentFragment = HTMLElement, + Container extends RendererableContainer = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +>( + ui: React.ReactNode, + options: ClientRenderOptions , +): RenderResult+export function render< + Q extends Queries = typeof queries, + Container extends HydrateableContainer = HTMLElement, BaseElement extends Element | DocumentFragment = Container, >( ui: React.ReactNode, - options: RenderOptions, + options: HydrateOptions, ): RenderResultexport function render( ui: React.ReactNode, diff --git a/types/test.tsx b/types/test.tsx index 6ff899de..da0bda06 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -206,6 +206,17 @@ export function testRenderHookProps() { unmount() } +export function testContainer() { + render('a', {container: document.createElement('div')}) + render('a', {container: document.createDocumentFragment()}) + // @ts-expect-error Only allowed in React 19 + render('a', {container: document}) + render('a', {container: document.createElement('div'), hydrate: true}) + // @ts-expect-error Only allowed for createRoot + render('a', {container: document.createDocumentFragment(), hydrate: true}) + render('a', {container: document, hydrate: true}) +} + /* eslint testing-library/prefer-explicit-assert: "off", From 48282c2f35fb7338834b40983c12b889af35f5d1 Mon Sep 17 00:00:00 2001 From: Sebastian SilbermannDate: Tue, 23 Apr 2024 17:32:08 +0200 Subject: [PATCH 110/126] fix: Ensure `renderHook` options extend options for `render` (#1308) --- types/index.d.ts | 63 ++++++++++++++++++++++++++++++++++++++++++------ types/test.tsx | 19 +++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 566e3d05..78302693 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -179,12 +179,12 @@ export interface RenderHookResult { unmount: () => void } -export interface RenderHookOptions< +export interface BaseRenderHookOptions< Props, - Q extends Queries = typeof queries, - Container extends Element | DocumentFragment = HTMLElement, - BaseElement extends Element | DocumentFragment = Container, -> extends RenderOptions { + Q extends Queries, + Container extends RendererableContainer | HydrateableContainer, + BaseElement extends Element | DocumentFragment, +> extends BaseRenderOptions{ /** * The argument passed to the renderHook callback. Can be useful if you plan * to use the rerender utility to change the values passed to your hook. @@ -192,6 +192,45 @@ export interface RenderHookOptions< initialProps?: Props } +export interface ClientRenderHookOptions< + Props, + Q extends Queries, + Container extends Element | DocumentFragment, + BaseElement extends Element | DocumentFragment = Container, +> extends BaseRenderHookOptions{ + /** + * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side + * rendering and use ReactDOM.hydrate to mount your components. + * + * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) + */ + hydrate?: false | undefined +} + +export interface HydrateHookOptions< + Props, + Q extends Queries, + Container extends Element | DocumentFragment, + BaseElement extends Element | DocumentFragment = Container, +> extends BaseRenderHookOptions { + /** + * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side + * rendering and use ReactDOM.hydrate to mount your components. + * + * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) + */ + hydrate: true +} + +export type RenderHookOptions< + Props, + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +> = + | ClientRenderHookOptions + | HydrateHookOptions + /** * Allows you to render a hook within a test React component without having to * create that component yourself. @@ -200,11 +239,21 @@ export function renderHook< Result, Props, Q extends Queries = typeof queries, - Container extends Element | DocumentFragment = HTMLElement, + Container extends RendererableContainer = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +>( + render: (initialProps: Props) => Result, + options?: ClientRenderHookOptions , +): RenderHookResult +export function renderHook< + Result, + Props, + Q extends Queries = typeof queries, + Container extends HydrateableContainer = HTMLElement, BaseElement extends Element | DocumentFragment = Container, >( render: (initialProps: Props) => Result, - options?: RenderHookOptions , + options?: HydrateHookOptions , ): RenderHookResult /** diff --git a/types/test.tsx b/types/test.tsx index da0bda06..734d70e7 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -45,6 +45,8 @@ export function testRenderOptions() { const options = {container} const {container: returnedContainer} = render(, options) expectType (returnedContainer) + + render(, {wrapper: () => null}) } export function testSVGRenderOptions() { @@ -191,6 +193,8 @@ export function testRenderHook() { rerender() unmount() + + renderHook(() => null, {wrapper: () => null}) } export function testRenderHookProps() { @@ -215,6 +219,21 @@ export function testContainer() { // @ts-expect-error Only allowed for createRoot render('a', {container: document.createDocumentFragment(), hydrate: true}) render('a', {container: document, hydrate: true}) + + renderHook(() => null, {container: document.createElement('div')}) + renderHook(() => null, {container: document.createDocumentFragment()}) + // @ts-expect-error Only allowed in React 19 + renderHook(() => null, {container: document}) + renderHook(() => null, { + container: document.createElement('div'), + hydrate: true, + }) + // @ts-expect-error Only allowed for createRoot + renderHook(() => null, { + container: document.createDocumentFragment(), + hydrate: true, + }) + renderHook(() => null, {container: document, hydrate: true}) } /* From d143f46b39cbc750630a5cf2863182b4036d35ac Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Fri, 26 Apr 2024 12:50:09 +0200 Subject: [PATCH 111/126] fix: Stop restricting `container` option based on `hydrate` (#1313) --- types/index.d.ts | 149 ++++++++++++++++++++------------------------ types/test.tsx | 26 ++++++-- types/tsconfig.json | 3 + 3 files changed, 91 insertions(+), 87 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 78302693..099bbe84 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -25,16 +25,16 @@ export function getConfig(): Config export type RenderResult< Q extends Queries = typeof queries, - Container extends Element | DocumentFragment = HTMLElement, - BaseElement extends Element | DocumentFragment = Container, + Container extends RendererableContainer | HydrateableContainer = HTMLElement, + BaseElement extends RendererableContainer | HydrateableContainer = Container, > = { container: Container baseElement: BaseElement debug: ( baseElement?: - | Element - | DocumentFragment - | Array , + | RendererableContainer + | HydrateableContainer + | Array , maxLength?: number, options?: prettyFormat.OptionsReceived, ) => void @@ -43,10 +43,48 @@ export type RenderResult< asFragment: () => DocumentFragment } & {[P in keyof Q]: BoundFunction } -export interface BaseRenderOptions< +/** @deprecated */ +export type BaseRenderOptions< Q extends Queries, Container extends RendererableContainer | HydrateableContainer, - BaseElement extends Element | DocumentFragment, + BaseElement extends RendererableContainer | HydrateableContainer, +> = RenderOptions+ +type RendererableContainer = ReactDOMClient.Container +type HydrateableContainer = Parameters[0] +/** @deprecated */ +export interface ClientRenderOptions< + Q extends Queries, + Container extends RendererableContainer, + BaseElement extends RendererableContainer = Container, +> extends BaseRenderOptions { + /** + * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side + * rendering and use ReactDOM.hydrate to mount your components. + * + * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) + */ + hydrate?: false | undefined +} +/** @deprecated */ +export interface HydrateOptions< + Q extends Queries, + Container extends HydrateableContainer, + BaseElement extends HydrateableContainer = Container, +> extends BaseRenderOptions{ + /** + * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side + * rendering and use ReactDOM.hydrate to mount your components. + * + * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) + */ + hydrate: true +} + +export interface RenderOptions< + Q extends Queries = typeof queries, + Container extends RendererableContainer | HydrateableContainer = HTMLElement, + BaseElement extends RendererableContainer | HydrateableContainer = Container, > { /** * By default, React Testing Library will create a div and append that div to the document.body. Your React component will be rendered in the created div. If you provide your own HTMLElement container via this option, @@ -93,44 +131,6 @@ export interface BaseRenderOptions< wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> } -type RendererableContainer = ReactDOMClient.Container -type HydrateableContainer = Parameters[0] -export interface ClientRenderOptions< - Q extends Queries, - Container extends Element | DocumentFragment, - BaseElement extends Element | DocumentFragment = Container, -> extends BaseRenderOptions { - /** - * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side - * rendering and use ReactDOM.hydrate to mount your components. - * - * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) - */ - hydrate?: false | undefined -} - -export interface HydrateOptions< - Q extends Queries, - Container extends Element | DocumentFragment, - BaseElement extends Element | DocumentFragment = Container, -> extends BaseRenderOptions{ - /** - * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side - * rendering and use ReactDOM.hydrate to mount your components. - * - * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) - */ - hydrate: true -} - -export type RenderOptions< - Q extends Queries = typeof queries, - Container extends RendererableContainer | HydrateableContainer = HTMLElement, - BaseElement extends Element | DocumentFragment = Container, -> = - | ClientRenderOptions- | HydrateOptions- type Omit= Pick > /** @@ -138,19 +138,11 @@ type Omit = Pick > */ export function render< Q extends Queries = typeof queries, - Container extends RendererableContainer = HTMLElement, - BaseElement extends Element | DocumentFragment = Container, ->( - ui: React.ReactNode, - options: ClientRenderOptions , -): RenderResult-export function render< - Q extends Queries = typeof queries, - Container extends HydrateableContainer = HTMLElement, - BaseElement extends Element | DocumentFragment = Container, + Container extends RendererableContainer | HydrateableContainer = HTMLElement, + BaseElement extends RendererableContainer | HydrateableContainer = Container, >( ui: React.ReactNode, - options: HydrateOptions, + options: RenderOptions, ): RenderResultexport function render( ui: React.ReactNode, @@ -179,19 +171,15 @@ export interface RenderHookResult{ unmount: () => void } -export interface BaseRenderHookOptions< +/** @deprecated */ +export type BaseRenderHookOptions< Props, Q extends Queries, Container extends RendererableContainer | HydrateableContainer, BaseElement extends Element | DocumentFragment, -> extends BaseRenderOptions { - /** - * The argument passed to the renderHook callback. Can be useful if you plan - * to use the rerender utility to change the values passed to your hook. - */ - initialProps?: Props -} +> = RenderHookOptions+/** @deprecated */ export interface ClientRenderHookOptions< Props, Q extends Queries, @@ -207,6 +195,7 @@ export interface ClientRenderHookOptions< hydrate?: false | undefined } +/** @deprecated */ export interface HydrateHookOptions< Props, Q extends Queries, @@ -222,14 +211,18 @@ export interface HydrateHookOptions< hydrate: true } -export type RenderHookOptions< +export interface RenderHookOptions< Props, Q extends Queries = typeof queries, - Container extends Element | DocumentFragment = HTMLElement, - BaseElement extends Element | DocumentFragment = Container, -> = - | ClientRenderHookOptions - | HydrateHookOptions + Container extends RendererableContainer | HydrateableContainer = HTMLElement, + BaseElement extends RendererableContainer | HydrateableContainer = Container, +> extends BaseRenderOptions { + /** + * The argument passed to the renderHook callback. Can be useful if you plan + * to use the rerender utility to change the values passed to your hook. + */ + initialProps?: Props +} /** * Allows you to render a hook within a test React component without having to @@ -239,21 +232,11 @@ export function renderHook< Result, Props, Q extends Queries = typeof queries, - Container extends RendererableContainer = HTMLElement, - BaseElement extends Element | DocumentFragment = Container, ->( - render: (initialProps: Props) => Result, - options?: ClientRenderHookOptions, -): RenderHookResult -export function renderHook< - Result, - Props, - Q extends Queries = typeof queries, - Container extends HydrateableContainer = HTMLElement, - BaseElement extends Element | DocumentFragment = Container, + Container extends RendererableContainer | HydrateableContainer = HTMLElement, + BaseElement extends RendererableContainer | HydrateableContainer = Container, >( render: (initialProps: Props) => Result, - options?: HydrateHookOptions , + options?: RenderHookOptions , ): RenderHookResult /** diff --git a/types/test.tsx b/types/test.tsx index 734d70e7..f8cf4aad 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -166,6 +166,24 @@ export function wrappedRenderC( return pure.render(ui, {wrapper: AppWrapperProps, ...options}) } +export function wrappedRenderHook ( + hook: () => unknown, + options?: pure.RenderHookOptions , +) { + interface AppWrapperProps { + children?: React.ReactNode + userProviderProps?: {user: string} + } + const AppWrapperProps: React.FunctionComponent = ({ + children, + userProviderProps = {user: 'TypeScript'}, + }) => { + return {children}+ } + + return pure.renderHook(hook, {...options}) +} + export function testBaseElement() { const {baseElement: baseDefaultElement} = render() expectType(baseDefaultElement) @@ -213,22 +231,22 @@ export function testRenderHookProps() { export function testContainer() { render('a', {container: document.createElement('div')}) render('a', {container: document.createDocumentFragment()}) - // @ts-expect-error Only allowed in React 19 + // Only allowed in React 19 render('a', {container: document}) render('a', {container: document.createElement('div'), hydrate: true}) - // @ts-expect-error Only allowed for createRoot + // Only allowed for createRoot but typing `render` appropriately makes it harder to compose. render('a', {container: document.createDocumentFragment(), hydrate: true}) render('a', {container: document, hydrate: true}) renderHook(() => null, {container: document.createElement('div')}) renderHook(() => null, {container: document.createDocumentFragment()}) - // @ts-expect-error Only allowed in React 19 + // Only allowed in React 19 renderHook(() => null, {container: document}) renderHook(() => null, { container: document.createElement('div'), hydrate: true, }) - // @ts-expect-error Only allowed for createRoot + // Only allowed for createRoot but typing `render` appropriately makes it harder to compose. renderHook(() => null, { container: document.createDocumentFragment(), hydrate: true, diff --git a/types/tsconfig.json b/types/tsconfig.json index a7829065..4e7d649c 100644 --- a/types/tsconfig.json +++ b/types/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../node_modules/kcd-scripts/shared-tsconfig.json", + "compilerOptions": { + "skipLibCheck": false + }, "include": ["."] } From f6a1677501b53471f6a989078726aeb0dea114be Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Fri, 26 Apr 2024 13:01:06 +0200 Subject: [PATCH 112/126] chore: Adjust tests to workaround 18.3.0 bug (#1315) --- package.json | 4 ++-- src/__tests__/stopwatch.js | 3 --- tests/shouldIgnoreConsoleError.js | 9 +++++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9483256a..3c38a74e 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,8 @@ "jest-diff": "^29.7.0", "kcd-scripts": "^13.0.0", "npm-run-all": "^4.1.5", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", "rimraf": "^3.0.2", "typescript": "^4.1.2" }, diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js index eeaf395c..e3eaebbe 100644 --- a/src/__tests__/stopwatch.js +++ b/src/__tests__/stopwatch.js @@ -40,7 +40,6 @@ class StopWatch extends React.Component { const sleep = t => new Promise(resolve => setTimeout(resolve, t)) test('unmounts a component', async () => { - jest.spyOn(console, 'error').mockImplementation(() => {}) const {unmount, container} = render( ) fireEvent.click(screen.getByText('Start')) unmount() @@ -52,6 +51,4 @@ test('unmounts a component', async () => { // if it's not, then we'll call setState on an unmounted component // and get an error. await sleep(5) - // eslint-disable-next-line no-console - expect(console.error).not.toHaveBeenCalled() }) diff --git a/tests/shouldIgnoreConsoleError.js b/tests/shouldIgnoreConsoleError.js index 75528267..1c722ba1 100644 --- a/tests/shouldIgnoreConsoleError.js +++ b/tests/shouldIgnoreConsoleError.js @@ -36,6 +36,15 @@ module.exports = function shouldIgnoreConsoleError(format) { // Ignore it too. return true } + if ( + format.startsWith( + 'Warning: `ReactDOMTestUtils.act` is deprecated in favor of `React.act`.', + ) + ) { + // This is a React bug in 18.3.0. + // Versions with `ReactDOMTestUtils.ac` being deprecated, should have `React.act` + return true + } } } // Looks legit From b6e59f7d58c69f617cc9854d8c3e6f86c2dfb0e9 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Wed, 1 May 2024 21:29:24 +0300 Subject: [PATCH 113/126] fix: export new act when available (#1319) --- package.json | 9 ++++++++- types/index.d.ts | 12 +++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 3c38a74e..0b7876a8 100644 --- a/package.json +++ b/package.json @@ -51,20 +51,27 @@ }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", + "@types/react": "^18.3.1", "chalk": "^4.1.2", "dotenv-cli": "^4.0.0", "jest-diff": "^29.7.0", "kcd-scripts": "^13.0.0", "npm-run-all": "^4.1.5", - "react": "^18.3.0", + "react": "^18.3.1", "react-dom": "^18.3.0", "rimraf": "^3.0.2", "typescript": "^4.1.2" }, "peerDependencies": { + "@types/react": "^18.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", "parserOptions": { diff --git a/types/index.d.ts b/types/index.d.ts index 099bbe84..c1cb345f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,7 +7,9 @@ import { prettyFormat, Config as ConfigDTL, } from '@testing-library/dom' -import {act as reactAct} from 'react-dom/test-utils' +import {act as reactDeprecatedAct} from 'react-dom/test-utils' +//@ts-ignore +import {act as reactAct} from 'react' export * from '@testing-library/dom' @@ -245,10 +247,10 @@ export function renderHook< export function cleanup(): void /** - * Simply calls ReactDOMTestUtils.act(cb) + * Simply calls React.act(cb) * If that's not available (older version of react) then it - * simply calls the given callback immediately + * simply calls the deprecated version which is ReactTestUtils.act(cb) */ -export const act: typeof reactAct extends undefined - ? (callback: () => void) => void +export const act: typeof reactAct extends never + ? typeof reactDeprecatedAct : typeof reactAct From c1f2957a219ec7e6a6159a57c5d0717655c515cd Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 7 May 2024 13:07:04 +0200 Subject: [PATCH 114/126] fix: Ensure `act` is not `any` when `React.act` is not declared (#1323) --- types/index.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index c1cb345f..37c8392a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -251,6 +251,7 @@ export function cleanup(): void * If that's not available (older version of react) then it * simply calls the deprecated version which is ReactTestUtils.act(cb) */ -export const act: typeof reactAct extends never +// IfAny from https://stackoverflow.com/a/61626123/3406963 +export const act: 0 extends 1 & typeof reactAct ? typeof reactDeprecatedAct : typeof reactAct From a4744fa904bf11812c92093226c3805450472636 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 3 Jun 2024 15:52:18 +0200 Subject: [PATCH 115/126] feat: Move `@testing-library/dom` and `@types/react-dom` to peer dependencies (#1305) BREAKING CHANGE: `@testing-library/dom` was moved to a peer dependency and needs to be explicitly installed. This reduces the chance of having conflicting versions of `@testing-library/dom` installed that frequently caused bugs when used with `@testing-library/user-event`. We will also be able to allow new versions of `@testing-library/dom` being used without a SemVer major release of `@testing-library/react` by just widening the peer dependency. `@types/react-dom` needs to be installed if you're typechecking files using `@testing-library/react`. Co-authored-by: Matan Borenkraout --- README.md | 11 +++++++---- package.json | 11 ++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 85613475..bffa75a5 100644 --- a/README.md +++ b/README.md @@ -97,10 +97,12 @@ primary guiding principle is: ## Installation This module is distributed via [npm][npm] which is bundled with [node][node] and -should be installed as one of your project's `devDependencies`: +should be installed as one of your project's `devDependencies`. +Starting from RTL version 16, you'll also need to install +`@testing-library/dom`: ``` -npm install --save-dev @testing-library/react +npm install --save-dev @testing-library/react @testing-library/dom ``` or @@ -108,10 +110,11 @@ or for installation via [yarn][yarn] ``` -yarn add --dev @testing-library/react +yarn add --dev @testing-library/react @testing-library/dom ``` -This library has `peerDependencies` listings for `react` and `react-dom`. +This library has `peerDependencies` listings for `react`, `react-dom` and +starting from RTL version 16 also `@testing-library/dom`. _React Testing Library versions 13+ require React v18. If your project uses an older version of React, be sure to install version 12:_ diff --git a/package.json b/package.json index 0b7876a8..8bfbeecc 100644 --- a/package.json +++ b/package.json @@ -45,13 +45,13 @@ "author": "Kent C. Dodds (https://kentcdodds.com)", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^10.0.0", - "@types/react-dom": "^18.0.0" + "@babel/runtime": "^7.12.5" }, "devDependencies": { + "@testing-library/dom": "^10.0.0", "@testing-library/jest-dom": "^5.11.6", "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", "chalk": "^4.1.2", "dotenv-cli": "^4.0.0", "jest-diff": "^29.7.0", @@ -63,13 +63,18 @@ "typescript": "^4.1.2" }, "peerDependencies": { + "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } }, "eslintConfig": { From 7a28fa916891c544746879d5013b3e07c035a4b7 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Wed, 28 Aug 2024 17:43:24 +0200 Subject: [PATCH 116/126] Fix React Canary and Experimental tests (#1353) --- tests/setup-env.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/setup-env.js b/tests/setup-env.js index c9b976f5..1a4401de 100644 --- a/tests/setup-env.js +++ b/tests/setup-env.js @@ -1,5 +1,9 @@ import '@testing-library/jest-dom/extend-expect' import './failOnUnexpectedConsoleCalls' import {TextEncoder} from 'util' +import {MessageChannel} from 'worker_threads' global.TextEncoder = TextEncoder +// TODO: Revisit once https://github.com/jsdom/jsdom/issues/2448 is resolved +// This isn't perfect but good enough. +global.MessageChannel = MessageChannel From 3dcd8a9649e25054c0e650d95fca2317b7008576 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Thu, 29 Aug 2024 11:28:03 +0200 Subject: [PATCH 117/126] fix: Add support for exactOptionalPropertyTypes in TypeScript (#1351) --- types/index.d.ts | 25 +++++++++++++------------ types/test.tsx | 7 ++++++- types/tsconfig.json | 1 + 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 37c8392a..3ad8cf46 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -36,9 +36,10 @@ export type RenderResult< baseElement?: | RendererableContainer | HydrateableContainer - | Array , - maxLength?: number, - options?: prettyFormat.OptionsReceived, + | Array + | undefined, + maxLength?: number | undefined, + options?: prettyFormat.OptionsReceived | undefined, ) => void rerender: (ui: React.ReactNode) => void unmount: () => void @@ -97,40 +98,40 @@ export interface RenderOptions< * * @see https://testing-library.com/docs/react-testing-library/api/#container */ - container?: Container + container?: Container | undefined /** * Defaults to the container if the container is specified. Otherwise `document.body` is used for the default. This is used as * the base element for the queries as well as what is printed when you use `debug()`. * * @see https://testing-library.com/docs/react-testing-library/api/#baseelement */ - baseElement?: BaseElement + baseElement?: BaseElement | undefined /** * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side * rendering and use ReactDOM.hydrate to mount your components. * * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) */ - hydrate?: boolean + hydrate?: boolean | undefined /** * Only works if used with React 18. * Set to `true` if you want to force synchronous `ReactDOM.render`. * Otherwise `render` will default to concurrent React if available. */ - legacyRoot?: boolean + legacyRoot?: boolean | undefined /** * Queries to bind. Overrides the default set from DOM Testing Library unless merged. * * @see https://testing-library.com/docs/react-testing-library/api/#queries */ - queries?: Q + queries?: Q | undefined /** * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating * reusable custom render functions for common data providers. See setup for examples. * * @see https://testing-library.com/docs/react-testing-library/api/#wrapper */ - wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> + wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> | undefined } type Omit = Pick > @@ -148,7 +149,7 @@ export function render< ): RenderResult export function render( ui: React.ReactNode, - options?: Omit, + options?: Omit | undefined, ): RenderResult export interface RenderHookResult { @@ -223,7 +224,7 @@ export interface RenderHookOptions< * The argument passed to the renderHook callback. Can be useful if you plan * to use the rerender utility to change the values passed to your hook. */ - initialProps?: Props + initialProps?: Props | undefined } /** @@ -238,7 +239,7 @@ export function renderHook< BaseElement extends RendererableContainer | HydrateableContainer = Container, >( render: (initialProps: Props) => Result, - options?: RenderHookOptions , + options?: RenderHookOptions | undefined, ): RenderHookResult /** diff --git a/types/test.tsx b/types/test.tsx index f8cf4aad..2b3dd7ca 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -132,7 +132,12 @@ export function wrappedRender( return {children}} - return pure.render(ui, {wrapper: Wrapper, ...options}) + return pure.render(ui, { + wrapper: Wrapper, + // testing exactOptionalPropertyTypes comaptibility + hydrate: options?.hydrate, + ...options, + }) } export function wrappedRenderB( diff --git a/types/tsconfig.json b/types/tsconfig.json index 4e7d649c..bad26af7 100644 --- a/types/tsconfig.json +++ b/types/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../node_modules/kcd-scripts/shared-tsconfig.json", "compilerOptions": { + "exactOptionalPropertyTypes": true, "skipLibCheck": false }, "include": ["."] From 85ac2534a59abd38880011e77da4bb8c716eba84 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann"Date: Thu, 5 Dec 2024 20:59:45 +0100 Subject: [PATCH 118/126] feat: Add support for React 19 (#1367) --- package.json | 12 ++++++------ types/test.tsx | 6 +++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 8bfbeecc..711140d1 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ "devDependencies": { "@testing-library/dom": "^10.0.0", "@testing-library/jest-dom": "^5.11.6", - "@types/react": "^18.3.1", - "@types/react-dom": "^18.3.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "chalk": "^4.1.2", "dotenv-cli": "^4.0.0", "jest-diff": "^29.7.0", @@ -64,10 +64,10 @@ }, "peerDependencies": { "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { diff --git a/types/test.tsx b/types/test.tsx index 2b3dd7ca..67832b23 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -128,7 +128,11 @@ export function wrappedRender( ui: React.ReactNode, options?: pure.RenderOptions, ) { - const Wrapper = ({children}: {children: React.ReactNode}): JSX.Element => { + const Wrapper = ({ + children, + }: { + children: React.ReactNode + }): React.JSX.Element => { return {children}} From 7134e5d7adc9f10b6ddaf6efdca376fac2f0b521 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann"Date: Thu, 16 Jan 2025 00:09:31 +0100 Subject: [PATCH 119/126] [ci] Fix codecov action (#1376) --- .github/workflows/validate.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index aa4eeed7..fe69085a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -59,11 +59,10 @@ jobs: run: npm run validate - name: β¬οΈ Upload coverage report - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: fail_ci_if_error: true flags: ${{ matrix.react }} - token: ${{ secrets.CODECOV_TOKEN }} release: permissions: From eab6e679a30eb87c1019717bb1fe1b57e5207c39 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 16 Jan 2025 00:11:07 +0100 Subject: [PATCH 120/126] [ci] Codecov fix follow-up Accidentally removed the token which is still required for protected branches. --- .github/workflows/validate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index fe69085a..488c633b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -63,6 +63,7 @@ jobs: with: fail_ci_if_error: true flags: ${{ matrix.react }} + token: ${{ secrets.CODECOV_TOKEN }} release: permissions: From 9618c5133706ec964f649e60a777cc384db58a3f Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Thu, 16 Jan 2025 00:16:35 +0100 Subject: [PATCH 121/126] feat: Add support for React error handlers (#1354) --- package.json | 2 +- src/__tests__/error-handlers.js | 183 ++++++++++++++++++++++++++++++++ src/pure.js | 24 ++++- tests/toWarnDev.js | 2 +- types/index.d.ts | 24 +++++ types/test.tsx | 22 ++++ 6 files changed, 252 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/error-handlers.js diff --git a/package.json b/package.json index 711140d1..81ee82c9 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "kcd-scripts": "^13.0.0", "npm-run-all": "^4.1.5", "react": "^18.3.1", - "react-dom": "^18.3.0", + "react-dom": "^18.3.1", "rimraf": "^3.0.2", "typescript": "^4.1.2" }, diff --git a/src/__tests__/error-handlers.js b/src/__tests__/error-handlers.js new file mode 100644 index 00000000..60db1410 --- /dev/null +++ b/src/__tests__/error-handlers.js @@ -0,0 +1,183 @@ +/* eslint-disable jest/no-if */ +/* eslint-disable jest/no-conditional-in-test */ +/* eslint-disable jest/no-conditional-expect */ +import * as React from 'react' +import {render, renderHook} from '../' + +const isReact19 = React.version.startsWith('19.') + +const testGateReact19 = isReact19 ? test : test.skip + +test('render errors', () => { + function Thrower() { + throw new Error('Boom!') + } + + if (isReact19) { + expect(() => { + render( ) + }).toThrow('Boom!') + } else { + expect(() => { + expect(() => { + render( ) + }).toThrow('Boom!') + }).toErrorDev([ + 'Error: Uncaught [Error: Boom!]', + // React retries on error + 'Error: Uncaught [Error: Boom!]', + ]) + } +}) + +test('onUncaughtError is not supported in render', () => { + function Thrower() { + throw new Error('Boom!') + } + const onUncaughtError = jest.fn(() => {}) + + expect(() => { + render( , { + onUncaughtError(error, errorInfo) { + console.log({error, errorInfo}) + }, + }) + }).toThrow( + 'onUncaughtError is not supported. The `render` call will already throw on uncaught errors.', + ) + + expect(onUncaughtError).toHaveBeenCalledTimes(0) +}) + +testGateReact19('onCaughtError is supported in render', () => { + const thrownError = new Error('Boom!') + const handleComponentDidCatch = jest.fn() + const onCaughtError = jest.fn() + class ErrorBoundary extends React.Component { + state = {error: null} + static getDerivedStateFromError(error) { + return {error} + } + componentDidCatch(error, errorInfo) { + handleComponentDidCatch(error, errorInfo) + } + render() { + if (this.state.error) { + return null + } + return this.props.children + } + } + function Thrower() { + throw thrownError + } + + render( + + , + { + onCaughtError, + }, + ) + + expect(onCaughtError).toHaveBeenCalledWith(thrownError, { + componentStack: expect.any(String), + errorBoundary: expect.any(Object), + }) +}) + +test('onRecoverableError is supported in render', () => { + const onRecoverableError = jest.fn() + + const container = document.createElement('div') + container.innerHTML = '+ server' + // We just hope we forwarded the callback correctly (which is guaranteed since we just pass it along) + // Frankly, I'm too lazy to assert on React 18 hydration errors since they're a mess. + // eslint-disable-next-line jest/no-conditional-in-test + if (isReact19) { + render(client, { + container, + hydrate: true, + onRecoverableError, + }) + expect(onRecoverableError).toHaveBeenCalledTimes(1) + } else { + expect(() => { + render(client, { + container, + hydrate: true, + onRecoverableError, + }) + }).toErrorDev(['', ''], {withoutStack: 1}) + expect(onRecoverableError).toHaveBeenCalledTimes(2) + } +}) + +test('onUncaughtError is not supported in renderHook', () => { + function useThrower() { + throw new Error('Boom!') + } + const onUncaughtError = jest.fn(() => {}) + + expect(() => { + renderHook(useThrower, { + onUncaughtError(error, errorInfo) { + console.log({error, errorInfo}) + }, + }) + }).toThrow( + 'onUncaughtError is not supported. The `render` call will already throw on uncaught errors.', + ) + + expect(onUncaughtError).toHaveBeenCalledTimes(0) +}) + +testGateReact19('onCaughtError is supported in renderHook', () => { + const thrownError = new Error('Boom!') + const handleComponentDidCatch = jest.fn() + const onCaughtError = jest.fn() + class ErrorBoundary extends React.Component { + state = {error: null} + static getDerivedStateFromError(error) { + return {error} + } + componentDidCatch(error, errorInfo) { + handleComponentDidCatch(error, errorInfo) + } + render() { + if (this.state.error) { + return null + } + return this.props.children + } + } + function useThrower() { + throw thrownError + } + + renderHook(useThrower, { + onCaughtError, + wrapper: ErrorBoundary, + }) + + expect(onCaughtError).toHaveBeenCalledWith(thrownError, { + componentStack: expect.any(String), + errorBoundary: expect.any(Object), + }) +}) + +// Currently, there's no recoverable error without hydration. +// The option is still supported though. +test('onRecoverableError is supported in renderHook', () => { + const onRecoverableError = jest.fn() + + renderHook( + () => { + // TODO: trigger recoverable error + }, + { + onRecoverableError, + }, + ) +}) diff --git a/src/pure.js b/src/pure.js index f546af98..fe95024a 100644 --- a/src/pure.js +++ b/src/pure.js @@ -91,7 +91,7 @@ function wrapUiIfNeeded(innerElement, wrapperComponent) { function createConcurrentRoot( container, - {hydrate, ui, wrapper: WrapperComponent}, + {hydrate, onCaughtError, onRecoverableError, ui, wrapper: WrapperComponent}, ) { let root if (hydrate) { @@ -99,10 +99,14 @@ function createConcurrentRoot( root = ReactDOMClient.hydrateRoot( container, strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), + {onCaughtError, onRecoverableError}, ) }) } else { - root = ReactDOMClient.createRoot(container) + root = ReactDOMClient.createRoot(container, { + onCaughtError, + onRecoverableError, + }) } return { @@ -202,11 +206,19 @@ function render( container, baseElement = container, legacyRoot = false, + onCaughtError, + onUncaughtError, + onRecoverableError, queries, hydrate = false, wrapper, } = {}, ) { + if (onUncaughtError !== undefined) { + throw new Error( + 'onUncaughtError is not supported. The `render` call will already throw on uncaught errors.', + ) + } if (legacyRoot && typeof ReactDOM.render !== 'function') { const error = new Error( '`legacyRoot: true` is not supported in this version of React. ' + @@ -230,7 +242,13 @@ function render( // eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first. if (!mountedContainers.has(container)) { const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot - root = createRootImpl(container, {hydrate, ui, wrapper}) + root = createRootImpl(container, { + hydrate, + onCaughtError, + onRecoverableError, + ui, + wrapper, + }) mountedRootEntries.push({container, root}) // we'll add it to the mounted containers regardless of whether it's actually diff --git a/tests/toWarnDev.js b/tests/toWarnDev.js index 2aae39f0..3005125e 100644 --- a/tests/toWarnDev.js +++ b/tests/toWarnDev.js @@ -115,7 +115,7 @@ const createMatcherFor = (consoleMethod, matcherName) => // doesn't match the number of arguments. // We'll fail the test if it happens. let argIndex = 0 - format.replace(/%s/g, () => argIndex++) + String(format).replace(/%s/g, () => argIndex++) if (argIndex !== args.length) { lastWarningWithMismatchingFormat = { format, diff --git a/types/index.d.ts b/types/index.d.ts index 3ad8cf46..2f814a6d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -119,6 +119,30 @@ export interface RenderOptions< * Otherwise `render` will default to concurrent React if available. */ legacyRoot?: boolean | undefined + /** + * Only supported in React 19. + * Callback called when React catches an error in an Error Boundary. + * Called with the error caught by the Error Boundary, and an `errorInfo` object containing the `componentStack`. + * + * @see {@link https://react.dev/reference/react-dom/client/createRoot#parameters createRoot#options} + */ + onCaughtError?: ReactDOMClient.RootOptions extends { + onCaughtError: infer OnCaughtError + } + ? OnCaughtError + : never + /** + * Callback called when React automatically recovers from errors. + * Called with an error React throws, and an `errorInfo` object containing the `componentStack`. + * Some recoverable errors may include the original error cause as `error.cause`. + * + * @see {@link https://react.dev/reference/react-dom/client/createRoot#parameters createRoot#options} + */ + onRecoverableError?: ReactDOMClient.RootOptions['onRecoverableError'] + /** + * Not supported at the moment + */ + onUncaughtError?: never /** * Queries to bind. Overrides the default set from DOM Testing Library unless merged. * diff --git a/types/test.tsx b/types/test.tsx index 67832b23..825d5699 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -263,6 +263,28 @@ export function testContainer() { renderHook(() => null, {container: document, hydrate: true}) } +export function testErrorHandlers() { + // React 19 types are not used in tests. Verify manually if this works with `"@types/react": "npm:types-react@rc"` + render(null, { + // Should work with React 19 types + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + onCaughtError: () => {}, + }) + render(null, { + // Should never work as it's not supported yet. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + onUncaughtError: () => {}, + }) + render(null, { + onRecoverableError: (error, errorInfo) => { + console.error(error) + console.log(errorInfo.componentStack) + }, + }) +} + /* eslint testing-library/prefer-explicit-assert: "off", From c3e3d9027c325ef169f139d449dcd65ffe444ac4 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann"Date: Thu, 16 Jan 2025 00:51:40 +0100 Subject: [PATCH 122/126] test: Use React 19 by default (#1377) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 81ee82c9..146c7d02 100644 --- a/package.json +++ b/package.json @@ -57,8 +57,8 @@ "jest-diff": "^29.7.0", "kcd-scripts": "^13.0.0", "npm-run-all": "^4.1.5", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "rimraf": "^3.0.2", "typescript": "^4.1.2" }, From 65bc994e7d4c1c388c51826f5352cf0320abb008 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Mon, 17 Mar 2025 22:24:31 +0100 Subject: [PATCH 123/126] test: Run with relevant React stable types (#1352) --- .github/workflows/validate.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 488c633b..f239c717 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -55,6 +55,12 @@ jobs: - name: βοΈ Setup react run: npm install react@${{ matrix.react }} react-dom@${{ matrix.react }} + - name: βοΈ Setup react types + if: ${{ matrix.react != 'canary' && matrix.react != 'experimental' }} + run: + npm install @types/react@${{ matrix.react }} @types/react-dom@${{ + matrix.react }} + - name: βΆοΈ Run validate script run: npm run validate From 8782f3be71eb2384df6c546dddab515867de3d7a Mon Sep 17 00:00:00 2001 From: Bernardo Belchior Date: Wed, 2 Apr 2025 17:57:28 +0100 Subject: [PATCH 124/126] Add `reactStrictMode` as an option to `render` (#1390) --- src/__tests__/render.js | 34 ++++++++++++++++++++++++++++++ src/__tests__/renderHook.js | 32 ++++++++++++++++++++++++++-- src/pure.js | 42 ++++++++++++++++++++++++++++++------- types/index.d.ts | 5 +++++ 4 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/__tests__/render.js b/src/__tests__/render.js index f00410b4..6f5b5b39 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -262,4 +262,38 @@ describe('render API', () => { `\`legacyRoot: true\` is not supported in this version of React. If your app runs React 19 or later, you should remove this flag. If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.`, ) }) + + test('reactStrictMode in renderOptions has precedence over config when rendering', () => { + const wrapperComponentMountEffect = jest.fn() + function WrapperComponent({children}) { + React.useEffect(() => { + wrapperComponentMountEffect() + }) + + return children + } + const ui = + configure({reactStrictMode: false}) + + render(ui, {wrapper: WrapperComponent, reactStrictMode: true}) + + expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2) + }) + + test('reactStrictMode in config is used when renderOptions does not specify reactStrictMode', () => { + const wrapperComponentMountEffect = jest.fn() + function WrapperComponent({children}) { + React.useEffect(() => { + wrapperComponentMountEffect() + }) + + return children + } + const ui = + configure({reactStrictMode: true}) + + render(ui, {wrapper: WrapperComponent}) + + expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2) + }) }) diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js index fe7551a2..f331e90e 100644 --- a/src/__tests__/renderHook.js +++ b/src/__tests__/renderHook.js @@ -1,5 +1,5 @@ -import React from 'react' -import {renderHook} from '../pure' +import React, {useEffect} from 'react' +import {configure, renderHook} from '../pure' const isReact18 = React.version.startsWith('18.') const isReact19 = React.version.startsWith('19.') @@ -111,3 +111,31 @@ testGateReact19('legacyRoot throws', () => { `\`legacyRoot: true\` is not supported in this version of React. If your app runs React 19 or later, you should remove this flag. If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.`, ) }) + +describe('reactStrictMode', () => { + let originalConfig + beforeEach(() => { + // Grab the existing configuration so we can restore + // it at the end of the test + configure(existingConfig => { + originalConfig = existingConfig + // Don't change the existing config + return {} + }) + }) + + afterEach(() => { + configure(originalConfig) + }) + + test('reactStrictMode in renderOptions has precedence over config when rendering', () => { + const hookMountEffect = jest.fn() + configure({reactStrictMode: false}) + + renderHook(() => useEffect(() => hookMountEffect()), { + reactStrictMode: true, + }) + + expect(hookMountEffect).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/pure.js b/src/pure.js index fe95024a..0f9c487d 100644 --- a/src/pure.js +++ b/src/pure.js @@ -77,8 +77,8 @@ const mountedContainers = new Set() */ const mountedRootEntries = [] -function strictModeIfNeeded(innerElement) { - return getConfig().reactStrictMode +function strictModeIfNeeded(innerElement, reactStrictMode) { + return reactStrictMode ?? getConfig().reactStrictMode ? React.createElement(React.StrictMode, null, innerElement) : innerElement } @@ -91,14 +91,24 @@ function wrapUiIfNeeded(innerElement, wrapperComponent) { function createConcurrentRoot( container, - {hydrate, onCaughtError, onRecoverableError, ui, wrapper: WrapperComponent}, + { + hydrate, + onCaughtError, + onRecoverableError, + ui, + wrapper: WrapperComponent, + reactStrictMode, + }, ) { let root if (hydrate) { act(() => { root = ReactDOMClient.hydrateRoot( container, - strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), + strictModeIfNeeded( + wrapUiIfNeeded(ui, WrapperComponent), + reactStrictMode, + ), {onCaughtError, onRecoverableError}, ) }) @@ -144,17 +154,31 @@ function createLegacyRoot(container) { function renderRoot( ui, - {baseElement, container, hydrate, queries, root, wrapper: WrapperComponent}, + { + baseElement, + container, + hydrate, + queries, + root, + wrapper: WrapperComponent, + reactStrictMode, + }, ) { act(() => { if (hydrate) { root.hydrate( - strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), + strictModeIfNeeded( + wrapUiIfNeeded(ui, WrapperComponent), + reactStrictMode, + ), container, ) } else { root.render( - strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), + strictModeIfNeeded( + wrapUiIfNeeded(ui, WrapperComponent), + reactStrictMode, + ), container, ) } @@ -180,6 +204,7 @@ function renderRoot( baseElement, root, wrapper: WrapperComponent, + reactStrictMode, }) // Intentionally do not return anything to avoid unnecessarily complicating the API. // folks can use all the same utilities we return in the first place that are bound to the container @@ -212,6 +237,7 @@ function render( queries, hydrate = false, wrapper, + reactStrictMode, } = {}, ) { if (onUncaughtError !== undefined) { @@ -248,6 +274,7 @@ function render( onRecoverableError, ui, wrapper, + reactStrictMode, }) mountedRootEntries.push({container, root}) @@ -273,6 +300,7 @@ function render( hydrate, wrapper, root, + reactStrictMode, }) } diff --git a/types/index.d.ts b/types/index.d.ts index 2f814a6d..bdd60567 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -156,6 +156,11 @@ export interface RenderOptions< * @see https://testing-library.com/docs/react-testing-library/api/#wrapper */ wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> | undefined + /** + * When enabled, is rendered around the inner element. + * If defined, overrides the value of `reactStrictMode` set in `configure`. + */ + reactStrictMode?: boolean } type Omit = Pick > From 9fc6a75d74bb8e03a48d3339efde4dd83cd5328b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 19:01:06 +0200 Subject: [PATCH 125/126] feat: add bernardobelchior as a contributor for code, and doc (#1391) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 10 ++++++++++ README.md | 1 + 2 files changed, 11 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index c3b86064..b22c9414 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1371,6 +1371,16 @@ "contributions": [ "code" ] + }, + { + "login": "bernardobelchior", + "name": "Bernardo Belchior", + "avatar_url": "https://avatars.githubusercontent.com/u/12778398?v=4", + "profile": "http://belchior.me", + "contributions": [ + "code", + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index bffa75a5..7e18d5dd 100644 --- a/README.md +++ b/README.md @@ -635,6 +635,7 @@ Thanks goes to these people ([emoji key][emojis]):
Colin Diesh
π
Yusuke Iinuma
π»+
Jeff Way
π»From 1c931a6c03091d725eccee7767d9ec696d5d33c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?=
Bernardo Belchior
π» πDate: Thu, 7 Aug 2025 20:13:48 +0200 Subject: [PATCH 126/126] chore(deps): use `npm-run-all2` (#1411) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 146c7d02..b1bff976 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "dotenv-cli": "^4.0.0", "jest-diff": "^29.7.0", "kcd-scripts": "^13.0.0", - "npm-run-all": "^4.1.5", + "npm-run-all2": "^6.2.6", "react": "^19.0.0", "react-dom": "^19.0.0", "rimraf": "^3.0.2",