From 0405f56ffef0bcf719fc8bbed856e33417401acf Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 4 Aug 2020 21:13:40 +0200 Subject: [PATCH 001/142] test(fireEvent): Add expected behavior for blur/focus in React (#757) --- src/__tests__/events.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/__tests__/events.js b/src/__tests__/events.js index dc529344..bac063de 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -211,3 +211,30 @@ test('calling `fireEvent` directly works too', () => { }), ) }) + +test('blur/foucs bubbles in react', () => { + const handleBlur = jest.fn() + const handleBubbledBlur = jest.fn() + const handleFocus = jest.fn() + const handleBubbledFocus = jest.fn() + const {container} = render( +
+
, + ) + const button = container.firstChild.firstChild + + fireEvent.focus(button) + + expect(handleBlur).toHaveBeenCalledTimes(0) + expect(handleBubbledBlur).toHaveBeenCalledTimes(0) + expect(handleFocus).toHaveBeenCalledTimes(1) + expect(handleBubbledFocus).toHaveBeenCalledTimes(1) + + fireEvent.blur(button) + + expect(handleBlur).toHaveBeenCalledTimes(1) + expect(handleBubbledBlur).toHaveBeenCalledTimes(1) + expect(handleFocus).toHaveBeenCalledTimes(1) + expect(handleBubbledFocus).toHaveBeenCalledTimes(1) +}) From 9aac1570d8bccfdf4584b22196cc23a479b47aff Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 5 Aug 2020 04:02:17 +0200 Subject: [PATCH 002/142] fix(fireEvent): Make sure react dispatches focus/blur events (#758) * test: Run CI with experimental React * stable -> latest * fix(fireEvent): Make sure react dispatches focus/blur events * Remove todo --- .travis.yml | 11 ++++++----- src/fire-event.js | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index c2f47b7a..eae48ee7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,14 +9,14 @@ node_js: - 14 - node env: - - REACT_NEXT=false - - REACT_NEXT=true + - REACT_DIST=latest + - REACT_DIST=next + - REACT_DIST=experimental install: - npm install # as requested by the React team :) # https://reactjs.org/blog/2019/10/22/react-release-channels.html#using-the-next-channel-for-integration-testing - - if [ "$REACT_NEXT" = true ]; then npm install react@next - react-dom@next; fi + - npm install react@$REACT_DIST react-dom@$REACT_DIST script: - npm run validate - npx codecov@3 @@ -27,7 +27,8 @@ branches: jobs: allow_failures: - - env: REACT_NEXT=true + - REACT_DIST=next + - REACT_DIST=experimental include: - stage: release node_js: 14 diff --git a/src/fire-event.js b/src/fire-event.js index 071aff8a..b4e60928 100644 --- a/src/fire-event.js +++ b/src/fire-event.js @@ -41,4 +41,18 @@ fireEvent.select = (node, init) => { fireEvent.keyUp(node, init) } +// React event system tracks native focusout/focusin events for +// running blur/focus handlers +// @link https://github.com/facebook/react/pull/19186 +const blur = fireEvent.blur +const focus = fireEvent.focus +fireEvent.blur = (...args) => { + fireEvent.focusOut(...args) + return blur(...args) +} +fireEvent.focus = (...args) => { + fireEvent.focusIn(...args) + return focus(...args) +} + export {fireEvent} From 276eb656feffed6e3da14b34965df889ed31b0cd Mon Sep 17 00:00:00 2001 From: Ryan Bigg Date: Fri, 21 Aug 2020 16:45:14 +1000 Subject: [PATCH 003/142] fix: Bump @testing-library/dom to 7.22.3 (#766) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 652c7d04..acb3f0ac 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.3", - "@testing-library/dom": "^7.17.1" + "@testing-library/dom": "^7.22.3" }, "devDependencies": { "@testing-library/jest-dom": "^5.10.1", From 5a10621fd081f44074a3a77a2ebc9e246c1c4db2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2020 09:08:56 +0200 Subject: [PATCH 004/142] docs: add radar as a contributor (#767) 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 0a5aea46..2e621d62 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1140,6 +1140,15 @@ "contributions": [ "code" ] + }, + { + "login": "radar", + "name": "Ryan Bigg", + "avatar_url": "https://avatars3.githubusercontent.com/u/2687?v=4", + "profile": "http://ryanbigg.com", + "contributions": [ + "maintenance" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index f33fde6c..30f00cd2 100644 --- a/README.md +++ b/README.md @@ -590,6 +590,7 @@ Thanks goes to these people ([emoji key][emojis]):
Braden Lee

📖
Kamran Ayub

💻 ⚠️
Matan Borenkraout

💻 +
Ryan Bigg

🚧 From 693228ce10f23e2b695730ea88dbdaa35506e00e Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sun, 30 Aug 2020 05:23:18 +0200 Subject: [PATCH 005/142] feat: use act to flush instead of custom implementation (#768) BREAKING CHANGE: cleanup is now synchronous and wraps the unmounting process in `act`. --- src/__tests__/cleanup.js | 93 +++++++++++++++++++++++++++++++++++++--- src/flush-microtasks.js | 67 ----------------------------- src/pure.js | 10 ++--- 3 files changed, 91 insertions(+), 79 deletions(-) delete mode 100644 src/flush-microtasks.js diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js index c0f1676d..4b67814a 100644 --- a/src/__tests__/cleanup.js +++ b/src/__tests__/cleanup.js @@ -1,7 +1,7 @@ import React from 'react' import {render, cleanup} from '../' -test('cleans up the document', async () => { +test('cleans up the document', () => { const spy = jest.fn() const divId = 'my-div' @@ -17,17 +17,17 @@ test('cleans up the document', async () => { } render() - await cleanup() + cleanup() expect(document.body).toBeEmptyDOMElement() expect(spy).toHaveBeenCalledTimes(1) }) -test('cleanup does not error when an element is not a child', async () => { +test('cleanup does not error when an element is not a child', () => { render(
, {container: document.createElement('div')}) - await cleanup() + cleanup() }) -test('cleanup runs effect cleanup functions', async () => { +test('cleanup runs effect cleanup functions', () => { const spy = jest.fn() const Test = () => { @@ -37,6 +37,87 @@ test('cleanup runs effect cleanup functions', async () => { } render() - await cleanup() + cleanup() expect(spy).toHaveBeenCalledTimes(1) }) + +describe('fake timers and missing act warnings', () => { + beforeEach(() => { + jest.resetAllMocks() + jest.spyOn(console, 'error').mockImplementation(() => { + // assert messages explicitly + }) + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + test('cleanup does not flush immediates', () => { + const microTaskSpy = jest.fn() + function Test() { + const counter = 1 + const [, setDeferredCounter] = React.useState(null) + React.useEffect(() => { + let cancelled = false + setImmediate(() => { + microTaskSpy() + if (!cancelled) { + setDeferredCounter(counter) + } + }) + + return () => { + cancelled = true + } + }, [counter]) + + return null + } + render() + + cleanup() + + expect(microTaskSpy).toHaveBeenCalledTimes(0) + // console.error is mocked + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledTimes(0) + }) + + test('cleanup does not swallow missing act warnings', () => { + const deferredStateUpdateSpy = jest.fn() + function Test() { + const counter = 1 + const [, setDeferredCounter] = React.useState(null) + React.useEffect(() => { + let cancelled = false + setImmediate(() => { + deferredStateUpdateSpy() + if (!cancelled) { + setDeferredCounter(counter) + } + }) + + return () => { + cancelled = true + } + }, [counter]) + + return null + } + render() + + jest.runAllImmediates() + cleanup() + + 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(...)', + ) + }) +}) diff --git a/src/flush-microtasks.js b/src/flush-microtasks.js deleted file mode 100644 index e1d8fe6f..00000000 --- a/src/flush-microtasks.js +++ /dev/null @@ -1,67 +0,0 @@ -/* istanbul ignore file */ -// the part of this file that we need tested is definitely being run -// and the part that is not cannot easily have useful tests written -// anyway. So we're just going to ignore coverage for this file -/** - * copied and modified from React's enqueueTask.js - */ - -function getIsUsingFakeTimers() { - return ( - typeof jest !== 'undefined' && - typeof setTimeout !== 'undefined' && - (setTimeout.hasOwnProperty('_isMockFunction') || - setTimeout.hasOwnProperty('clock')) - ) -} - -let didWarnAboutMessageChannel = false -let enqueueTask - -try { - // read require off the module object to get around the bundlers. - // we don't want them to detect a require and bundle a Node polyfill. - const requireString = `require${Math.random()}`.slice(0, 7) - const nodeRequire = module && module[requireString] - // assuming we're in node, let's try to get node's - // version of setImmediate, bypassing fake timers if any. - enqueueTask = nodeRequire.call(module, 'timers').setImmediate -} catch (_err) { - // we're in a browser - // we can't use regular timers because they may still be faked - // so we try MessageChannel+postMessage instead - enqueueTask = callback => { - const supportsMessageChannel = typeof MessageChannel === 'function' - if (supportsMessageChannel) { - const channel = new MessageChannel() - channel.port1.onmessage = callback - channel.port2.postMessage(undefined) - } else if (didWarnAboutMessageChannel === false) { - didWarnAboutMessageChannel = true - - // eslint-disable-next-line no-console - console.error( - 'This browser does not have a MessageChannel implementation, ' + - 'so enqueuing tasks via await act(async () => ...) will fail. ' + - 'Please file an issue at https://github.com/facebook/react/issues ' + - 'if you encounter this warning.', - ) - } - } -} - -export default function flushMicroTasks() { - return { - then(resolve) { - if (getIsUsingFakeTimers()) { - // without this, a test using fake timers would never get microtasks - // actually flushed. I spent several days on this... Really hard to - // reproduce the problem, so there's no test for it. But it works! - jest.advanceTimersByTime(0) - resolve() - } else { - enqueueTask(resolve) - } - }, - } -} diff --git a/src/pure.js b/src/pure.js index f2f3438f..8a062038 100644 --- a/src/pure.js +++ b/src/pure.js @@ -7,7 +7,6 @@ import { } from '@testing-library/dom' import act, {asyncAct} from './act-compat' import {fireEvent} from './fire-event' -import flush from './flush-microtasks' configureDTL({ asyncWrapper: async cb => { @@ -100,17 +99,16 @@ function render( } } -async function cleanup() { +function cleanup() { mountedContainers.forEach(cleanupAtContainer) - // flush microtask queue after unmounting in case - // unmount sequence generates new microtasks - await flush() } // 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) { - ReactDOM.unmountComponentAtNode(container) + act(() => { + ReactDOM.unmountComponentAtNode(container) + }) if (container.parentNode === document.body) { document.body.removeChild(container) } From 534ea33d514297ed368530f39e210675e1553979 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Wed, 2 Sep 2020 11:41:32 -0600 Subject: [PATCH 006/142] chore: update all deps --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index acb3f0ac..61bfefef 100644 --- a/package.json +++ b/package.json @@ -43,20 +43,20 @@ "author": "Kent C. Dodds (https://kentcdodds.com)", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.10.3", - "@testing-library/dom": "^7.22.3" + "@babel/runtime": "^7.11.2", + "@testing-library/dom": "^7.23.0" }, "devDependencies": { - "@testing-library/jest-dom": "^5.10.1", + "@testing-library/jest-dom": "^5.11.4", "@types/react-dom": "^16.9.8", - "dotenv-cli": "^3.1.0", - "dtslint": "3.6.12", - "kcd-scripts": "^6.2.3", + "dotenv-cli": "^3.2.0", + "dtslint": "4.0.0", + "kcd-scripts": "^6.3.0", "npm-run-all": "^4.1.5", "react": "^16.13.1", "react-dom": "^16.13.1", "rimraf": "^3.0.2", - "typescript": "^3.9.5" + "typescript": "^4.0.2" }, "peerDependencies": { "react": "*", From 88d42769f1d13a43e2e073677bf9fcff844b2d49 Mon Sep 17 00:00:00 2001 From: Anton Halim Date: Wed, 2 Sep 2020 15:02:24 -0700 Subject: [PATCH 007/142] Improve some documentation (#770) --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 30f00cd2..388ec957 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ practices.

- [The problem](#the-problem) -- [This solution](#this-solution) +- [The solution](#the-solution) - [Installation](#installation) - [Suppressing unnecessary warnings on React DOM 16.8](#suppressing-unnecessary-warnings-on-react-dom-168) - [Examples](#examples) @@ -83,7 +83,7 @@ maintainable in the long run so refactors of your components (changes to implementation but not functionality) don't break your tests and slow you and your team down. -## This solution +## The solution The `React Testing Library` is a very lightweight solution for testing React components. It provides light utility functions on top of `react-dom` and @@ -104,7 +104,7 @@ npm install --save-dev @testing-library/react or -for installation via [yarn](https://classic.yarnpkg.com/en/) +for installation via [yarn][yarn] ``` yarn add --dev @testing-library/react @@ -132,7 +132,7 @@ the following snippet to your test configuration ```js // this is just a little hack to silence a warning that we'll get until we -// upgrade to 16.9: https://github.com/facebook/react/pull/14853 +// upgrade to 16.9. See also: https://github.com/facebook/react/pull/14853 const originalError = console.error beforeAll(() => { console.error = (...args) => { @@ -156,8 +156,8 @@ afterAll(() => { // hidden-message.js import React from 'react' -// NOTE: React Testing Library works with React Hooks _and_ classes just as well -// and your tests will be the same however you write your components. +// NOTE: React Testing Library works well with React Hooks and classes. +// Your tests will be the same regardless of how you write your components. function HiddenMessage({children}) { const [showMessage, setShowMessage] = React.useState(false) return ( @@ -372,7 +372,7 @@ You can also find React Testing Library examples at If you are interested in testing a custom hook, check out [React Hooks Testing Library][react-hooks-testing-library]. -> NOTE it is not recommended to test single-use custom hooks in isolation from +> NOTE: it is not recommended to test single-use custom hooks in isolation from > the components where it's being used. It's better to test the component that's > using the hook rather than the hook itself. The `React Hooks Testing Library` > is intended to be used for reusable hooks/libraries. @@ -383,7 +383,7 @@ Library][react-hooks-testing-library]. > confidence they can give you.][guiding-principle] We try to only expose methods and utilities that encourage you to write tests -that closely resemble how your react components are used. +that closely resemble how your React components are used. Utilities are included in this project based on the following guiding principles: @@ -397,8 +397,8 @@ principles: `react-dom`. 3. Utility implementations and APIs should be simple and flexible. -At the end of the day, what we want is for this library to be pretty -light-weight, simple, and understandable. +Most importantly, we want React Testing Library to be pretty +light-weight, simple, and easy to understand. ## Docs @@ -407,8 +407,8 @@ light-weight, simple, and understandable. ## Issues -_Looking to contribute? Look for the [Good First Issue][good-first-issue] -label._ +Looking to contribute? Look for the [Good First Issue][good-first-issue] +label. ### 🐛 Bugs @@ -608,6 +608,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/travis/testing-library/react-testing-library.svg?style=flat-square [build]: https://travis-ci.org/testing-library/react-testing-library From 491bedc294f7bfc56127ecfc396430aa0d3debb1 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2020 18:28:18 -0400 Subject: [PATCH 008/142] docs: add antonhalim as a contributor (#775) * 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 | 3 +++ 2 files changed, 12 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 2e621d62..f826d705 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1149,6 +1149,15 @@ "contributions": [ "maintenance" ] + }, + { + "login": "antonhalim", + "name": "Anton Halim", + "avatar_url": "https://avatars1.githubusercontent.com/u/10498035?v=4", + "profile": "https://antonhalim.com", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 388ec957..e53be16d 100644 --- a/README.md +++ b/README.md @@ -592,6 +592,9 @@ Thanks goes to these people ([emoji key][emojis]):
Matan Borenkraout

💻
Ryan Bigg

🚧 + +
Anton Halim

📖 + From 9191890c882314c0048206e23a1fff40561b7ee4 Mon Sep 17 00:00:00 2001 From: Artem Malko Date: Thu, 3 Sep 2020 13:29:06 +0700 Subject: [PATCH 009/142] fix: Update typings for cleanup (#776) --- 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 09824704..50702ecc 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -47,7 +47,7 @@ export function render( /** * Unmounts React trees that were mounted with render. */ -export function cleanup(): Promise +export function cleanup(): void /** * Simply calls ReactDOMTestUtils.act(cb) From 20acad35b1a9a3f614fa183b82a5bd606336889f Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2020 08:51:36 +0200 Subject: [PATCH 010/142] docs: add artem-malko as a contributor (#777) 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 f826d705..2eceb2e6 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1158,6 +1158,15 @@ "contributions": [ "doc" ] + }, + { + "login": "artem-malko", + "name": "Artem Malko", + "avatar_url": "https://avatars0.githubusercontent.com/u/1823689?v=4", + "profile": "http://artmalko.ru", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index e53be16d..b3a2f588 100644 --- a/README.md +++ b/README.md @@ -594,6 +594,7 @@ Thanks goes to these people ([emoji key][emojis]):
Anton Halim

📖 +
Artem Malko

💻 From 220d8d4fd1f29c64e5094a6efa46fdee7b8105de Mon Sep 17 00:00:00 2001 From: Gerrit Alex Date: Thu, 3 Sep 2020 19:43:27 +0200 Subject: [PATCH 011/142] fix(cleanup): remove unnecessary async/await (#778) --- src/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index b69f6555..4f92e02b 100644 --- a/src/index.js +++ b/src/index.js @@ -9,15 +9,15 @@ if (!process.env.RTL_SKIP_AUTO_CLEANUP) { // ignore teardown() in code coverage because Jest does not support it /* istanbul ignore else */ if (typeof afterEach === 'function') { - afterEach(async () => { - await cleanup() + afterEach(() => { + cleanup() }) } else if (typeof teardown === 'function') { // Block is guarded by `typeof` check. // eslint does not support `typeof` guards. // eslint-disable-next-line no-undef - teardown(async () => { - await cleanup() + teardown(() => { + cleanup() }) } } From b32fea015c87c018277d52a88ed9f161455f145c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2020 20:03:59 +0200 Subject: [PATCH 012/142] docs: add ljosberinn as a contributor (#779) 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 2eceb2e6..de7f33aa 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1167,6 +1167,15 @@ "contributions": [ "code" ] + }, + { + "login": "ljosberinn", + "name": "Gerrit Alex", + "avatar_url": "https://avatars1.githubusercontent.com/u/29307652?v=4", + "profile": "http://gerritalex.de", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b3a2f588..ff6fe55f 100644 --- a/README.md +++ b/README.md @@ -595,6 +595,7 @@ Thanks goes to these people ([emoji key][emojis]):
Anton Halim

📖
Artem Malko

💻 +
Gerrit Alex

💻 From 865c4fd6f96145f717a4b517d247cd68f034cfce Mon Sep 17 00:00:00 2001 From: tapico-weyert <70971917+tapico-weyert@users.noreply.github.com> Date: Wed, 9 Sep 2020 00:24:17 +0100 Subject: [PATCH 013/142] docs: update the cheat sheet pdf + source file to fix typo (#782) fixes #759 --- other/cheat-sheet.pdf | Bin 46907 -> 48836 bytes other/design files/cheat-sheet.afpub | Bin 158235 -> 137815 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/other/cheat-sheet.pdf b/other/cheat-sheet.pdf index 8f1d1e60f63aeadc0fef01c599cb974ba3bfc0e4..d8288e0316d570ce25261e7f1fb019239a543dfa 100644 GIT binary patch delta 14384 zcmb7r1z1&E^EV+~3P>XdX#`FO-KCV2q!J!NDQN`Rbe9|u>5@=FIz(wi1*8;E5D5Xn z0;LS#e-4V*xc7O#k4Jd+o;_>U#BXNJ+I!jZo}_z+oDBK=9A29}}z`z4Qp&$rIUrvtN!_ysW?M&_a z+$d?;CPaZYEY!Dx+K@R#^q4~ZoRMu0X~5pNC{-1eTtyQ!Hl(QN>cBr>Y%a3y zT}GAXbw_FaLijXO!Q}9l5H>a=CtA$w%rOe67>(_NLTrd*VpO$qHGHuHaF1%M8dD(F7Nwff6yfk&E9y6KHz?DXS ztg%~W`LL=-E!HL$QcyAI*`D zGhQEdgespgpIwcON?8py_PU4)CgFI>F@5CfloM^5Z>Pb=A->WXHsoS|hzCQdsr9PNy<|3~OV*f@g)Q{mMSaKBhLv1J$c3=sV01sC@ zC@#=nU`GFVj)osRh7oXbg3>_S+T$V!&%4LYzYi4<@E20BBldhsmzaCka+J$ zZ=Ge&FL(07j@|P{Vf*^2u~_U;u;CWVi)|T9+Xos9<>&1$&~M9Zfvs|symks!dTx}K zZhBTkOyM=1oqW6047TBFHH8=piO4Pi@RmBWbVe_$Z)|jYJQL;iUBEa0!6~KV`~tBm zo!co-5O>yTgS3{4i_c@1UM=dCKJ1GOQChOIKYBE6DCL@>9~pa(>f9sI#1#&zC&oA< zk;<~PlUP`K%E%C<_JHBj@%jhmlh384q|7xf8DGE8KRI2XXJc}k`pXw6Ehk%8zI9xI zVJQ-$6Gs%6Fgwo#kqie~9G+YdS3Eolhe)gUgWKmAXA6wLZ`Lp03&0*78$(-pocd5~ zK8+I2_Vy;;G^_&hJmF3}9LO7mwnP}or{5sQa?623dcYW;s^a3sjvg2l6B~67!VV+&%X4~y@GjyG%&ZK>9$?rjY^WD^cq$m zz1Q7s3-T0|DW#LSzEkVe46DDY{t#iBF$VXWsef^U8)JXLYVJF<7v8NKOD%YMC5dRbPUi=8L>#jAwsZveKdXmz3W zHtCg@atP>&{MT!5`gS5_2JZ#%zC5~O zT^wgbmCL_&*T1H={4`8#YN`zx&LdL63y5^zFu!RpD>}yB3G*w}r zwferYyj`X45XS^!7?-=xJJobsb}F0X5076r#C5z>d?;=mihbVr+{5fKW7R6rwZku? zXx7LiMNUaXQ@PoEyiU?aV)v|Hwuy{}&4S!o)rvVM}XsKqY6%igRz#9shphUAnle^Qd9~ zNzenP^_jy=$zJ(6d6AX9p0$bU#2mH1gj30wqLA8P~|YIeK0mzufXLCFdG3k%rF6CBLdZ;lLZO-CMR4 zFdCDY&6!JE!8>YeU+bjhutkO!q1@T|g9edEg*a)=r8~T<6sbEGh^317 z`l>htICZyqSJVoOy~h^@kJ)BD`t1bpx^;4(S2y_R5p#6sl%g-I#P!iIMY&P>I(d_8;6wIjkMa0FGn?fJldsltoBPap0tpaR zrSyb#2-Gl5Ka-v``DzV2E@QFzndgz9U_){YWl0)0;OMzxUlDYr^H7tad)ViZdILhcdVYCec#b)nanBV zL26PN69CER%D%c((oaVumaS+$`0_Ns65jt=z>}TNgEY@v)mDZ~G2dJ{+op2QMKqAX zV;9L00R3v{mK6cSUXjo;QfegPn@`l&x0-u&O}8-$m!#i=B3T^u1H3uUbfo~XE}X^J z=OeCs9f_8(w7V?O*RA&g{7GvXM`uVzgJjEOoD2=SH0oQminDd++80 zpcuwm!CBTY`R4kXtsT|gktI3ax65s)P?za0r*F-gow@Vg64EVO_l>Bic4b=I5>j_! z$sHOWoj7h?CdGN>q=LM7f(@e=ke3p^*GD@P8;hGO19{7@3_q!MGIihCZg%r0E)&1Y z8YZ7O_E3d=#@$-c_C>Uy_wBXd*`^*A51Q4@3qoXZ4suC~w5#ED$IF89tBy2_gNktJqe*9$qEQ)x>sBpxyC?bq0nMlfjsP329LpP< zmCLYqG9>e6pKn8fkxL4SsmQ?r%x8tRX-Z666MtTDazc7TAIkeX!zv+|eN@^0<8y{W^|HN2(?ss(eguO(&Jx$2e0sRk;=p>{*&*CcDe!-d7q%%5B^ zt4WY5655r;m7gpVDOz-!xD429dag2Ul|Nn+ z5GZqkT)edYWbci9zb~07spsD_DDT!gAh^bjPg$cYM-$nr`Sq8AKABwH9RH|4QsB3F z1w2{l(~|Nr7~!v8Nm0iVeEZcm=arG>thVg!Zt{7DwPBvUj&tjb=Wy=8#G=~O*<6+z z^B)}4xkEy=7Y83Cv#x4#r#^e-&9uBlu`GBy0!cIrd8^2qo2d#9l-SKt^D(1=r}u}} z7VJeYjw{QP7BuR$2B_A8Q#bsCNQmD^Jxe&diyv4ua3oG+#+Q`GHUUdRxG~ zJ+(X6G(KFJ%RIa&>}gsE18}?_T(6`0uDRDPhbH>61c*zP<-@)*zRZ~5pX?LAu@r0H z5n80!eMMCJY%W#z8T&p&8i+VhL#Y7EdEBMs$kZb+sA`te&JzE`W zJ8LfIj$LWYU;OxWW36XAr7ADf!}<(;s*fl8uK2Mr|bp9YfZV6Ou|=pcy^ zBz-$XL_{PcB*)oVBt`g)WT1{(@|R6c23zQyM2gc=Q3`Xjn#m)>&gmuD8z*25DagrA z@NoDTsoSbaAEBjo)>h;?dKi41E6Q3QA;=@h$)YGBq>F*D9-%)(N^(h0Sq&;mM@s_} z6_7zAv1d&Z!vY#_<-h3fxRsq^tpZ4MeLQ? z!H(8K9N^_reKq7+XMI7SYnC_mK~=u8oOEwRZsYan`^}ZqRFvU%rdFnghZ*SI9BjsW zTiT1RP50evsk}i>PA1IHH`H|N*~3;oo?|COgeE#_W(K>p)Rp?~+#cy{!oZ=x*uBb> z=wKdh&RksVOn-a0e_%pr*n>MY8p;Zdw~9L2d!S-6{%)S$PIh6J-E}pM(&OXP<0E8{ zs#a&LJ8Q~ni?f-T7(4544Yk+va#^>phSKcG`%MBMZUjU;&fm?=+GL>Rc1dEe zv8GZ(9`4L(t+W`x=#&aKJL_P5$*t7br!8eR#yV1PiR{?G(L1;Jj&U7f02n%MX28Tm z?pNm4WFQgNVO&+i*NZAB@Aw9s?G*pSe%nAGSDoqscJ3PDQ^)b24e( zM7@7@DsukrjpU|ebnmT4%2`4^>P1dPZw&=f+jO^RgL?Yut4QCvYo575ak#dfOZJMc z&-}_Ku;a$Nwnh2JBKb;=Jtt!JGC5U6E{8>n?+9h~ZC-?;|4a6~8#Q*|l(XeMK=W@Er4i@sao&-R#z=nY(*8eQ4S)Gtv36No*C~ zUrYyX2gV52u^B^x60pNnVdPxNyCd?!6!{+tDMfpSw>Ml}x8fxbiZ#OL*1Jl-wlufQ zGm{K#%3cV3Eg)>oHLRgGntzG$-F?TVte)#Bd{|N99xrODG5NFcfr*8|E2{F#!Huu7 zFgNEmo^pkHOWX@>g$I4|nr|7F$Wp(tuJiIPfDn4F75Y_&qUX8ggefPosJ7j}ROEsj z@Mf;347ib9r$R^j{OIhK;-S8{u&jD6lv47Tn*Sx{QFX9=QpC{t_l0gn zW(;R;wfdPRNoyqrEstI!4PYa71NW)~LKOgLcos%!^);o5_4B!PQ}Fz+A(1Mxs}dr%T*(xH%<-nI}W$sFJi`Q!gd_O zy6OfC6VhWJjC>2ok~n{R=Zz`in>Av|Gz_wotxk?oIQ7KQCC(n$kZDurE|%@SdsHBz z=%R`9d#qaDM&(iKM=2qclB+dq$7v}frR@@0BtJh5b&zH9A$QP>o0yDc>BvElTk>lG zH8!2RMQWjOH)d|1tt;ezjffq(`*{sg^mZ|_HcM(x?0D<-G4C@pMA*~v9E}ntdiPzQ zFHc^YYl`|J!EB-#X%#Pg{6yh!y^yF&6k@7Ar(I6hG%<2DnUP{5;Yvsmy~Q)Cg7C_w zFq*O};i0=|B@vHX-fmL%H*ZO#4|Nmyi30Y~qw$HgWZ}Fg`&RVX7i9BKRjMvp#%7f< z`w*>>q!oqvKm@0z{VqL>=Oxz^tb6-(d}lRwqyI%C*ON2MJ-dlxU&mWdGo71aS}k1d zC4D>MEv1mqdE{A!CReZglQjResYInWlfy-U>(<{KUPzs?AJO`JT6RHOV)&eHXL|`4 zDCiwDyWT)5MqILNeXWkCRYQGqV1qg{n!ADThJnRf&z+N`Zz8>Psb143`_(a7dZs8j zgM;6Xl(ob|x+W7!?sz?+sWbrZXgn{!w?%^Z=ve7LW7fwSDz>bqJZ)e| zi)6aW1=PNsc1_z%(= zS5a7)RiXSw6RTTQrQZ0HoJyI?r;l9o8)aJ6kT^{Cq(Eq z!?K4fXB#0aIdfy;c>Ja;-CUCV)kAU#_m5#&+TEY+KHVkf%F(zve3;aaST7!PHz`F} zU`8Zdu)*@0_g!_cMtxo1h;1HSznOjzdlVplktNWQw~$B0UakJij=N_4!!H6ko?=sV zu_hjpiJmK>AIIC$TDo<wQJq?9~2%82mREIb4IKFGSI_x84BcjOW z%wUfhs;SxNOcC!*pDj%7el4BY><};2_9*?uiHe9N*DE95Z616N6cjsy4ey>Wy*R?? zgz0}+{B?L2oh-NrVkodraABCEjC0VtOHRVH5M|)5@VtR%;el{*?sdt9#5UV7Y2Xy? zt#17{KC;t@IiyWAoK}CBEXwDEFYTyRCUSJFhRcZMaiPB{@~x>3B&3A_fPni<96H0ffr^_l z{VT2v4K3td(bad)hK*!iliTu$^xb%2M6XWoJ{c$<>4QT~l_is{AAh^L*nT@>F5<~! zr3iIKU^n1#+1?DCB-b${jClR^uHqgaJOYxWBY#D9uVz!n-BPnkzG(m>e2N=45$V;M zxYnxaYkK-Hc+o5X2d+)b=GVH}0|bwfm-1#ow+Hc*juh+pl`OQand z(4Ow>ZLSK~X+_stiptzT$pSa)lmVI6L8S<4ofwkt}*I9 zX$tOfPIO}MP+D~8>!#N4%w^KoScuQocB4o zxp~}YY<-4&YjBcTZR}!_K&#%>g#;guoYL(ftM&T~=~+1YtFQB6tk@}@T_0KRH;2C_ zb*mFyx~b^8aB*%z+=ff(%TQ1iB(Q9?azRkpf4$PmbH_pya4kI1lXeVd z5aL!9c)=sXQOjpjk)_!!m-hrmzlB+E+UsF8Pqr~CCGB*oE6H71-L=(e3KG#DlGayJYN?ErqYj@Rm3Zxax_@05>9zo0LSx>lC;*f&dcpqph%#BJPC zwE|4B?QpI}Gc@!h)4bn-+3eVh!8!~f62q4C@MuV{q|cXfv5lg$6j8c;rxLox=T|DE%!4H2 zUw+Spr6MIZljB=Ku>Isat~2k>(W?91S$*P3Lv-<8&k&9acG1EK&#Kd=Sl< zsxprQGhcD+=o3hJHPs>?8}*r&;&RCu{2lGKyG>8XXp%$< z=wihcXfvzblgWf(R6Va^~A6dXHG>Ct&jS28UuMyS7?Mop`^O3YY3t)24J(xZ}{ z({|6}Omih9$+Xot+SqPkZM(Nk+1nllVh2iz`&gn+n<$?jmK?XcP<*LVz(DSCKMM5n z>;NLNh$VP@?wRv&!Rq^zW?>eMv96G)62P`K3Vmi?GtEjBSiQNdpI?l+q}21k}BXCFM;`t&|m zQ4@?DW?8@a-nYv{_Gqn~<%}`ILtR$YS$&Sd2~p}M;CBaT;@**at}D< znOj-zJdQ1VXKO<36ff8v-L6_!q9~zB!;c26!j1p zMBV2MkK9>XmK-DzGF&Yhj!8*cW3eh5m5bTb(6!S&70uN1u|0Cnbm8;8MHS3ba(BI? zHrlcqoI^Zf0}n$M-qMKW-4G4$Wn?)!r;yo`tN=Zn1CnHZy(FUo%!^ZzvVZ8j?IF}w zBI|d`{~>v5RCo?;*{i4ilDEvHa$D03bnP#7Ye&K}PKwA}cwiqk#CG_@5~=? zpjg&>wHaXNa578hbooULNnRWA4!pajWM;|p0q+PaGKG1Y^Q_*d5&Mf`_le>?9D@t0 zSOPY>xI$IgZM8`y^a0z6+n)f@Q+n%dIbS*7b>&pgF{&CKG9l8o=_HX$*rH`$;4Y>VlW#T7Vik8ykcder%xM^^Z z>2X(WY0U{P3fCb1k7ZUIJTupam(EbR6Mxv@FoR$U!bfQoh`+5)3W|fIw~(ci*Dk}w z9`V&qh?yKRUj5)Cq#+ub8SEQPX+>E)@Y!G7&yzv<3+qY*pg{LhT|DQk9A)>6deo9X{O>RTS zIO_~o1EY6IqsnpeQR@4dkD_*Ut!_N>6Xy%-|F~*>-R)j??A}W3h1M@0RdGYnMV;5n z`Se6CR~b1yu!#WDt-UBk^Wtph%T0O49$oNYL0DYu*2uhOJq+hn%T`UmuzoEt6()Pn zOm7qHE^?7*SkOGemrRgYgS@Zd(#?Q(flk$OiAPeL*D;XX>M&aNmr!PNw&5&i(b}tD zsX8mARa3NML7E^Kr#@-0{Lw>?dh6ph`J9)sHK0wn*yGy(=#D=)BA|ZR>PvooDDxp+ z>`Hy++L5`IX1#)5$LuKnwXLcW*ybaq_u@)wkZ%sub~*X9kBWrWuoQOO4a4_n7tY_y zW~QE4ybm9eZK_N=vrZNAQtQ^`7j$0~k>fnKV~@!{P9=)qOX^mB7MdXdC%tQD zBcP{uY()ZU2∋3~-MXAr>}^%S(Fnc-=X@3RfIWtoGW= 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 delta 12486 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 diff --git a/other/design files/cheat-sheet.afpub b/other/design files/cheat-sheet.afpub index 73c75ca0106fc3162b7916de85923b8e87f0a9f2..bd0b0329cb30b517a7f3a19ecbb963227be5eeaa 100644 GIT binary patch delta 59020 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%0DGM;QM_8#jCwK ziVNN<8a_4)QVRQ@`xxA~M~|^Ga_l zW>RGigM`*2eb+xYUo&f7LaJ4$^KBFV*$w6NYjez4?s}7=v5|_sxy_I2)v?`wMLEbJETAH}(XDH!HNV>`|J3#6H(H%FjTIq+$fK^WuO~^9 z@PzFiP6!kHx$3R`;U7bn#9tNzyUNr7mnYz|!rC z-EnQnz12_-?5c6o5V2P5qOM^1r=M21SZl1=mHfGPY)S4aF}u}@6wNLsVvt`yMfV_o z1iI2wVyGcV+coTSV<&<5v63{cg?P`B<)G0s=|YSQ<7Y!-aYgkA>9f_MlrL|sARKOZ zKGn_=Qt4t@s16y-4C(UF9lQ5V4<)ti$MVk=i&@p>eSEd&EC*?P!o%8_oxf64e(Hfv ziM^J>!on`y*S%eaqz)7E@`k)yPamVpN0Xpqt87iq3`T9cv$Emu-@72)c83s4_kaL= z^m99?Tjt-Q4DzJP5Cb6SVo{ojJP8(55})$^?Q_CvFswAg#DLP?W++z*gaW9`eK)GQ zF+^SFFhE%Y1#TCWcNY4fRKLn&RTmUaSec7;t>-r&^8@>jMrZ(&)QXaz4xYjQiGCLf z#Jp_~Bhv@0(c;Wcr)4OO5fDpoQN10_zI$lV4fu{%#l@(Ul+JcynJ<@#C4+&3hYkpf zh`2!+>a{V^)_QT;y4TM5FCl`#ZzZONkP2mXtEXf@cJ!^1T25rl+alavEVWq72DK0k4y4;i5~pa8O*6Kb@>~88AC|0D z`oH|%vWMKM`6C$+bkp}Vc}jX8(C$(>V#r$bebP0|)4lm<^3qOAttCH3_(tSJJ9cs6f@QsGi>d)AxlZtmYYQ}TD2GXU<4- zA{|SN`96*t)!hGa`~FPER~w3Klej{{;B4PJxz|!3cpp66Yb5)+egKmOMGhLt78gf! zbt!>(iMB+bHR{UBKSKT;@1QzCV(IR$Q(U}>O@9!S>r`~u}Qd$b9XjDEO=Let~*1b5N`Scw#JrSsX z0~?byP$w-2%7I1&;2k-NK+Iqs(Gt)4AtlZ;y{;4RrBVG|PjV16T|XZ|^+_ zZBRM3L);wyxQ&8fr}N-}qLvo%f6M{nnY)|&aaKH&13R-Tv#@Y(E2fK1GDc35DU<)! zRX{_i$J>!1vXW$G05tA9QU06P+|LMOD>N^VBkn3AYYuUQ32@o+V}K01uwPwniJ0{1 zRWq@uUo@LiqxmZB;>TR5Q;Z zWfr#RWivM<)laAeljTW+8)V>A^MmvakR$Eh1~Ep&7h-WS?>t<3eu%-gr~`44>o#`d zzugOfMLglh5fTEw^EQa#k2gL!${8EdJuZ9J$$m9HYH}(aJ06TI zG*^XgyCl#59*BA093o$clMyp#D6_~p=+3hrO{D2U>yB?48c6oN4dAeC{u8$yv9-pK!otE$YB#TpC>O$kW0*@c|r0{ zv+68lug*@k=bVBF?pl-Dg^il5{xH3Gh^UGEZhqJiVnShbS+LXQBRGxqz!C$pR8)u&9f(K$ZCFO1)G83}qfUcb+} z!Z&IAC4>69mBM8d08Cr8T>Jat5DX~nufLw=d4CPZt%&w?qZX#-bFMiB&y7fx1f=I1 zF1A13hmm93(G7SQ?&Ary-{yU96yDrUiV4@Da~!3+)>24yGKQ{VR8+E4qN}lP?I{4s z1L@DzkzS68G@i+zM5U{cOK+5JGO1V8t+SW&xg`snK?H@!Zj|znCcEhOc#V98H{V%t zRlh0G_4-gW=t{Ljo04dkcp|v+ZjI907Y9Ti%m>S*x8uZWo!C&^;RD@PzgG02kE@i? z{Gt{8De6^NE(~=?l%1TzjA@NAS_M}HGx$2kcvaLf$kM2&6miUc^-N9lRmi^)5`Yw% zQKOGBs*VA`@}!J~L;TQ!H%Cj7jER)UY%r_Sa)=q?WBsF+aRo?~L#vis81@1B#w|P6saW*TY)czG*Eg7IG@vsK)y1yNio84e)_O|s(EqBiT zZkUD_BZcf| z9ZMZoZel_j*gm`|KQ4#h3U2@*H9iNw!Y{dH;yn- zek0SM^%7aG@xh6krxTY0VBFSnGtxZL_63w7-g}Q0YGLvIdpsmCHhby0tl2A^+P0{{ z)aV~(>@6ES*oWtd3c?DO_E=g&^P{QMZ2YFy0Y!*uR2o_vUQQtp!C?6)5c5|TfbTE- z2T_KxUCDBXsiuo`l5rvfgGW1$HLlWIK|zKen+t&@@=9@K%8i$cLo!tCHnDMnk}daO z?X5U8^iXhe@?2t-((t&hLb|Ei@fpBApqR(bQgVU}0G>5u&a;G8W{>&o-LV%p5&s14 z2k=|myC0xruu%K}hR*+)3E+frPbRPmm7@n1Zf&$ z>>}qN-1S(xL6Boc^>RNvzlw#mgKO${EDt46Lhmma$XAr#B7J|TEnD2k$YK!{h9HRb z`jE*`{dd$7vj(!9p|2gG!O?9}hn-<8=e6i5N(<-ey;e8opqB~C-rx5CGu&jeuh9DU za)}#*UvPj9ar2^I+7vYMJi;04T6ARo*EqZ`l=Lp0s%2f#NB z(b$04AV<x_(U^2#{{5=Vv`Q z``25~NTQM0t2bIDmNqs*0s?J7mUaR0 z!p)0hT?-HoIJyPa4mL78PzhCF9APLRNmKpjH-x=2GB&0Nq$u1|j^wL-E%0yVHIqKa zOKHRF2Mf6|K9Ym^cO%gDH?$uUj8B}vT`qRV7ix1&d~l>QJ^D-qL@yA+Ku7{w_3-pG zuQh_|15FqeAO$QiRgp@CgoK0%Sy}49>VfG892sD8)PAGKoplV*@>Yfhnz!nM|Ch6% z;2_r*wi`Pt##<^|!R+sRKo5ftxMXI0m#<^Vy@dMZ-8+6DW&w}3uX>P0cfL?6beOCT z4E(16PY(+k?qbmxvm&1Cu=X9jS$IK_XePDs1wAn}Od~ z%-2p(VRI4B0cj9yS)eYn&=W;p_R!bA=KS%P1J(r)zzYX{Fr;AExkar&f(2;buO$~2 zHZba-sJW`;efjdOXUPrFyb1?9aVnVs2b$;Vtg-AR1B|tE9FUF-SbJ^H+=E-fC@Qd! zf^`j6i7|4109Xv|F#<^ke5jxtz%}2zeft^gr*%_k90@DNr^N&aY8YRj7r}M87rD)o z*1v@QW$-Ez=M6;++p>}1g4+zP|9av#%pddwfFy8+0>Z*lJo5D0X@*WL_VRS#HIHvkk-(Sl;HhYc4p?ChB?&Vmq!l|CV-5uI#M76aYfDDT?i%yRJc5$ zT-`U4i^*$Xs^aG2BJmbXOG}G3cXV`w|1mX$UZ2I-JBD>mKiM-dc}>blvqhs)NW{L zJXF3^>YNRWgyy=UW35|rVF>6+z?7rcu&}W4z88|K!$ra9`|~laEkE(AAh0Ce0YJ77 z1aWt;l9CeFjT@BSw9v`X>FF5#(xnWW-1&9W3vu8S!1AYGY6U_`W+p<5-nU>#Mc0+J zwY7YPl^9|2a)KGJ8X9Ooxj+lP5dDGCrkCD>0A&ihlktxx@0+m5iqz9aejq>8 zU_+s0my45%i$1UxYKqeT{&%`v3@TL!4l&wv0r8+6#2qk=qTPKUO;dtJhB&Ia08luF zsR0#!d1_UQzkq>083T=6R!W$Bh0V&rc<<=~mDs2E9(?g%RW zKa*VqNXxQ!?_PdX=R`SlY{Yl%-}uFeDV#kA{)YNUPTJU!kr8=USAn{pUUZX_lL)L= z(5Fh?zLhT>6ZGCPj$@P=e13(%=lo#2VLqG=f*ZY#x(3W zh%}%gL8JjKK8aoZ>#7G39o3%e`XHo$7AX5h&kaN#a3ZL3phDZtAv|G+V1~)JRRkUI zZh`Z{sTtW(f~4DXFm4(TGFNkRHq_$~n(rzqUI9(hsz23r{INHrth47}hk#Vb#m8p} zJ_^tf!4rvIk?rAEVL6~3nBd~<3M#`5eIwDms&Jn2potid9%uagJgpbt!t23xR`&L?DS!DLl4lyXRuF&f_VgNBeF0hpL=><+!7Px?uu0^gfCNEdKUZ# zeCD|1{Pmrjh07CL-^b0KJKt4S0Um}B;5US0^MtJoq9{dy+3%T@p zVj>+}>tP0_fe%#J$k1O(>V0Jx2mggA7V0C(4*(0l%20!vo}QxFq~h!3B=`FdAMl55 z^{A<-NmvqJKz0rTw z&zZD!b)TyhJ?W0SF1g#oih(|g@k-U(w`#`7bA0R&cpUVuu&`i7zv@wCRDL$qdQJy2 zExboi1KS!h8`=#H8t>_LYaZA;{sCfghd~Aos0&^)z&I*KPB@+F=ds}!dY7i3Xod%P zW7muGLzD(bWakrLgCfW|HMJi;>>bRJI6fI~9fZLeC{T<>bVZ(T!A~DKx1<0m1g}Vl zZGAj->fNm9pzK{XXj711xNjB^KwmdC05W|f*uF3RorCQ`*Tf{wm|qz*SlT0as`m8* zc)I}@CkRM~qwoGj6pXLGDJl81xN}QT5LO5lNy;I}-!Q*V&I*;|1%-t}$JdKMG(~{= z25^mjXM~&yL`pC|L#hBX{V+{p|B^gJUvR`P#!0|a0fL3}2RRD%c+?UMAz&r9hHTKK z;Q|EL_{_j$1a1Z}Ex^kjg|L+Y@4!~>g$kv3G_u&!8nkQJs^AJnzZ_6H7IaC9ml@fa zysLXD`AEaHDh1=FNTh;%fSoWc3^)j`85$h+ef>*rt`=bISL9EFu>@SfhUy)vb(@2s z2Zh`kOz?TO4d_kdyOJNlFPaCYJy3|d^>?#0Sggt0Z30hu0@giHSCp z3U}UD4(-hmyNo?R3<7!9o)Js_`J~vZ%%7%yxh}NhG~go=3(BXoj`WRHGgUG}`Gp16 za|2b7QFpTK*!OE1b?(bAJaN?_4E|($Q>~mdmiuyE>Gk_wS1@MUM7rV}yJsqi?phD~ zF5em1B?8_Z=xc$SvvaOwJGdOmViufU=6NmGf_)~m%w8clpaPI?b15mW4F+hL3IY8V z(h>@!2!1R>(cfR+%8>fK_rf+78%LUgZ&Pz?iT78Jdwy{iM|A)2GUIk)yS598PPBQi zVQAIgpO#}~neQxy%=tQFmnS8_jIF>T$3-E&eY9M~{#z(QZ&|%PqZw08#aPq09+L?- zhG9wx8IF?JX=-|L%-PB0?sWTc7NV;Eb1deABd4T*f=gLm3GzJq_>RDj4><*t-XnFJ z-j-vJV{0w_()Lx>uRXUoFNx8g9&r9YOuctJR{#G8Y?T>8$lfd2dy_pQ4YIOZMz(Nd zX0ObQ?8-`!O*UB}NmfQ?w(N1g&gc8P@5lXLaXHs@o%0^A*K@tIHF{W(!HXfU#ZmgG zOx5f3!WL15v=#E=s-Zrdp0xSl| zTMLj2nk8|bYzpW7cHQO&SA^ad##}!%rQdzx8-7%O9@Bh^L+e?fFfsTND8t z3~t&`-5+kU!B}ey#DoP$QGKkE50!Td_FdS(RBvNV`FM?#%ozZS%a51qhi3u9BVzOm z1UT+(lo-IN+Iw^?c2iMYBHlwqNx$6~bTks6hbN7B@FCfPJLQtF9?>&NAaq|kM*i(s z1)`j+Tp$qFOx3DC?{8WYtB`1b=HndeK5<}R+5!r_V@;QdO+Q%|a`moOO%YB_|j) z&{cBN=QdCP(G1V-$6wK|SY|fzwpPE7izYF@?CZXjDWa^m(k!bpsLveVT(WYPyo^j! z1HoOO{Q{e!q&fkNOp;YJC8DpbSwbWof9ys!J8|M+>7V0_xs*~3&tE?x=tw63s3TkK zUf4xhAdY|B2aMzAS4t*4jh3m)Lj)^aX1?O0v<%J0QY3eWX@Py}%G|#Y+x^XEK2Tfr z&f24ZIbB1kPxtx(F$R$K9sy#D-n(w+p;n7KeLR4f82mEW3O?Sd`Ne(jOcY=o01Igv z&o;W|31C#?gwz?#+V!SUlE+^&)OP*L>Nt28FLD>d;%f;D!}ewB)z&+HBf)-`$g)_# zqM^W~AVSl=xD%#D+;V)zE5k5QXwv!+VtZqF2m%y%Dai|GTag(7R#sL!9a=mbjLkzI zZZP5Dz%FWc-7W+=Q5gpL>ubTi{k^*`LaCtQ%0D0hO<-Z72M;WmjCCgXVLicsTk)z~ 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 delta 79625 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 From c7b5622bf7fa4b5435a0b12d8deaf4f57923cf8d Mon Sep 17 00:00:00 2001 From: Karthick Raja <47154512+karthick3018@users.noreply.github.com> Date: Sun, 13 Sep 2020 21:20:55 +0530 Subject: [PATCH 014/142] fix: Trigger onPointerEnter/Leave when calling pointerEnter/Leave (#784) --- src/__tests__/events.js | 20 ++++++++++++++++++++ src/fire-event.js | 11 +++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/__tests__/events.js b/src/__tests__/events.js index bac063de..bc36dab3 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -197,6 +197,26 @@ test('onChange works', () => { expect(handleChange).toHaveBeenCalledTimes(1) }) +test('calling `onPointerEnter` directly works too', () => { + const handlePointerEnter = jest.fn() + const handlePointerLeave = jest.fn() + const {container} = render( +

, + ) + const button = container.firstChild.firstChild + + fireEvent.pointerEnter(button) + expect(handlePointerEnter).toHaveBeenCalledTimes(1) + + fireEvent.pointerLeave(button) + expect(handlePointerLeave).toHaveBeenCalledTimes(1) +}) + test('calling `fireEvent` directly works too', () => { const handleEvent = jest.fn() const { diff --git a/src/fire-event.js b/src/fire-event.js index b4e60928..cb790c7f 100644 --- a/src/fire-event.js +++ b/src/fire-event.js @@ -24,6 +24,17 @@ fireEvent.mouseLeave = (...args) => { return fireEvent.mouseOut(...args) } +const pointerEnter = fireEvent.pointerEnter +const pointerLeave = fireEvent.pointerLeave +fireEvent.pointerEnter = (...args) => { + pointerEnter(...args) + return fireEvent.pointerOver(...args) +} +fireEvent.pointerLeave = (...args) => { + pointerLeave(...args) + return fireEvent.pointerOut(...args) +} + const select = fireEvent.select fireEvent.select = (node, init) => { select(node, init) From 732f3a19d46306d298dda88e4bfe0ea127c2973d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2020 18:16:14 +0200 Subject: [PATCH 015/142] docs: add karthick3018 as a contributor (#785) 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 de7f33aa..5e1d0fd4 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1176,6 +1176,15 @@ "contributions": [ "code" ] + }, + { + "login": "karthick3018", + "name": "Karthick Raja", + "avatar_url": "https://avatars1.githubusercontent.com/u/47154512?v=4", + "profile": "https://github.com/karthick3018", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ff6fe55f..12749724 100644 --- a/README.md +++ b/README.md @@ -596,6 +596,7 @@ Thanks goes to these people ([emoji key][emojis]):
Anton Halim

📖
Artem Malko

💻
Gerrit Alex

💻 +
Karthick Raja

💻 From c546a6f4927f925bf1187e631ca0444001f067f5 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sun, 13 Sep 2020 20:33:30 +0200 Subject: [PATCH 016/142] fix: Trigger ongot-/onlostpointercapture when calling got-/lostpointercapture (#786) --- package.json | 2 +- src/__tests__/events.js | 36 ++++++++++++++++-------------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 61bfefef..291427b3 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.2", - "@testing-library/dom": "^7.23.0" + "@testing-library/dom": "^7.24.2" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.4", diff --git a/src/__tests__/events.js b/src/__tests__/events.js index bc36dab3..0e28848f 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -62,6 +62,22 @@ const eventTypes = [ ], elementType: 'button', }, + { + type: 'Pointer', + events: [ + 'pointerOver', + 'pointerEnter', + 'pointerDown', + 'pointerMove', + 'pointerUp', + 'pointerCancel', + 'pointerOut', + 'pointerLeave', + 'gotPointerCapture', + 'lostPointerCapture', + ], + elementType: 'button', + }, { type: 'Selection', events: ['select'], @@ -197,26 +213,6 @@ test('onChange works', () => { expect(handleChange).toHaveBeenCalledTimes(1) }) -test('calling `onPointerEnter` directly works too', () => { - const handlePointerEnter = jest.fn() - const handlePointerLeave = jest.fn() - const {container} = render( -
-
, - ) - const button = container.firstChild.firstChild - - fireEvent.pointerEnter(button) - expect(handlePointerEnter).toHaveBeenCalledTimes(1) - - fireEvent.pointerLeave(button) - expect(handlePointerLeave).toHaveBeenCalledTimes(1) -}) - test('calling `fireEvent` directly works too', () => { const handleEvent = jest.fn() const { From 6b9a7207adee15f9f94cfbd23903f9f54fc178ec Mon Sep 17 00:00:00 2001 From: Abdelrahman Ashraf Date: Wed, 14 Oct 2020 21:11:26 +0800 Subject: [PATCH 017/142] feat: bump @testing-library/dom to v7.26.0 (#799) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 291427b3..9b314d91 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.2", - "@testing-library/dom": "^7.24.2" + "@testing-library/dom": "^7.26.0" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.4", From d9a36fa88c0d9e52e9f19639c72e32d626d88abd Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2020 07:11:56 -0600 Subject: [PATCH 018/142] docs: add theashraf as a contributor (#800) 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 5e1d0fd4..58552c8c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1185,6 +1185,15 @@ "contributions": [ "code" ] + }, + { + "login": "theashraf", + "name": "Abdelrahman Ashraf", + "avatar_url": "https://avatars1.githubusercontent.com/u/39750790?v=4", + "profile": "https://github.com/theashraf", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 12749724..6bccc9ac 100644 --- a/README.md +++ b/README.md @@ -597,6 +597,7 @@ Thanks goes to these people ([emoji key][emojis]):
Artem Malko

💻
Gerrit Alex

💻
Karthick Raja

💻 +
Abdelrahman Ashraf

💻 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 019/142] docs: update complex example (#806) Co-authored-by: Lidor Avitan --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6bccc9ac..529d5c17 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,9 @@ function Login() { password: passwordInput.value, }), }) - .then(r => r.json()) + .then((r) => + r.json().then((data) => (r.ok ? data : Promise.reject(data))) + ) .then( user => { setState({loading: false, resolved: true, error: null}) @@ -282,9 +284,10 @@ import {setupServer} from 'msw/node' import {render, fireEvent, screen} from '@testing-library/react' import Login from '../login' +const fakeUserResponse = { token: 'fake_user_token' } const server = setupServer( rest.post('/api/login', (req, res, ctx) => { - return res(ctx.json({token: 'fake_user_token'})) + return res(ctx.json(fakeUserResponse)) }), ) @@ -322,7 +325,7 @@ test('allows the user to login successfully', async () => { test('handles server exceptions', async () => { // mock the server error response for this test suite only. server.use( - rest.post('/', (req, res, ctx) => { + rest.post('/api/login', (req, res, ctx) => { return res(ctx.status(500), ctx.json({message: 'Internal server error'})) }), ) From 50f54b59c621b93463abfe743b2aa68218ad3f63 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 13:00:20 -0600 Subject: [PATCH 020/142] docs: add lidoravitan as a contributor (#807) 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 58552c8c..0fd4f4ab 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1194,6 +1194,15 @@ "contributions": [ "code" ] + }, + { + "login": "lidoravitan", + "name": "Lidor Avitan", + "avatar_url": "https://avatars0.githubusercontent.com/u/35113398?v=4", + "profile": "https://github.com/lidoravitan", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 529d5c17..ba9848ea 100644 --- a/README.md +++ b/README.md @@ -601,6 +601,7 @@ Thanks goes to these people ([emoji key][emojis]):
Gerrit Alex

💻
Karthick Raja

💻
Abdelrahman Ashraf

💻 +
Lidor Avitan

📖 From c2806fcc88d488663e0202a45e9c5ff528fb39fc Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Thu, 29 Oct 2020 15:06:17 +0100 Subject: [PATCH 021/142] test: Fix node 15 (#809) --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eae48ee7..fbe104b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,15 @@ node_js: - 10.14.2 - 12 - 14 - - node + - 15 env: - REACT_DIST=latest - REACT_DIST=next - REACT_DIST=experimental install: + # 7.0.6 fixed CI environments prompting user input. + # Can be removed once node15 bumps the shipped npm version. + - npm install --global npm@>=7.0.6 - npm install # as requested by the React team :) # https://reactjs.org/blog/2019/10/22/react-release-channels.html#using-the-next-channel-for-integration-testing From 0db811283819fdc9774e36155ff806f44500533c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Thu, 29 Oct 2020 22:08:52 +0100 Subject: [PATCH 022/142] chore: update React imports (#811) --- README.md | 22 ++++++++++------------ src/__tests__/act.js | 2 +- src/__tests__/auto-cleanup-skip.js | 2 +- src/__tests__/auto-cleanup.js | 2 +- src/__tests__/cleanup.js | 2 +- src/__tests__/debug.js | 2 +- src/__tests__/end-to-end.js | 2 +- src/__tests__/events.js | 2 +- src/__tests__/multi-base.js | 2 +- src/__tests__/render.js | 2 +- src/__tests__/rerender.js | 2 +- src/__tests__/stopwatch.js | 2 +- src/act-compat.js | 2 +- src/pure.js | 2 +- 14 files changed, 23 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ba9848ea..a43cb5a6 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ afterAll(() => { ```jsx // hidden-message.js -import React from 'react' +import * as React from 'react' // NOTE: React Testing Library works well with React Hooks and classes. // Your tests will be the same regardless of how you write your components. @@ -184,7 +184,7 @@ export default HiddenMessage import '@testing-library/jest-dom' // NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required -import React from 'react' +import * as React from 'react' import {render, fireEvent, screen} from '@testing-library/react' import HiddenMessage from '../hidden-message' @@ -209,7 +209,7 @@ test('shows the children when the checkbox is checked', () => { ```jsx // login.js -import React from 'react' +import * as React from 'react' function Login() { const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), { @@ -233,9 +233,7 @@ function Login() { password: passwordInput.value, }), }) - .then((r) => - r.json().then((data) => (r.ok ? data : Promise.reject(data))) - ) + .then(r => r.json().then(data => (r.ok ? data : Promise.reject(data)))) .then( user => { setState({loading: false, resolved: true, error: null}) @@ -276,7 +274,7 @@ export default Login // again, these first two imports are something you'd normally handle in // your testing framework configuration rather than importing them in every file. import '@testing-library/jest-dom' -import React from 'react' +import * as React from 'react' // import API mocking utilities from Mock Service Worker. import {rest} from 'msw' import {setupServer} from 'msw/node' @@ -284,7 +282,7 @@ import {setupServer} from 'msw/node' import {render, fireEvent, screen} from '@testing-library/react' import Login from '../login' -const fakeUserResponse = { token: 'fake_user_token' } +const fakeUserResponse = {token: 'fake_user_token'} const server = setupServer( rest.post('/api/login', (req, res, ctx) => { return res(ctx.json(fakeUserResponse)) @@ -400,8 +398,8 @@ principles: `react-dom`. 3. Utility implementations and APIs should be simple and flexible. -Most importantly, we want React Testing Library to be pretty -light-weight, simple, and easy to understand. +Most importantly, we want React Testing Library to be pretty light-weight, +simple, and easy to understand. ## Docs @@ -410,8 +408,7 @@ light-weight, simple, and easy to understand. ## Issues -Looking to contribute? Look for the [Good First Issue][good-first-issue] -label. +Looking to contribute? Look for the [Good First Issue][good-first-issue] label. ### 🐛 Bugs @@ -607,6 +604,7 @@ Thanks goes to these people ([emoji key][emojis]): + This project follows the [all-contributors][all-contributors] specification. diff --git a/src/__tests__/act.js b/src/__tests__/act.js index 97438d77..b60aac37 100644 --- a/src/__tests__/act.js +++ b/src/__tests__/act.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import {render, fireEvent, screen} from '../' test('render calls useEffect immediately', () => { diff --git a/src/__tests__/auto-cleanup-skip.js b/src/__tests__/auto-cleanup-skip.js index e5ef35ae..5696d4e3 100644 --- a/src/__tests__/auto-cleanup-skip.js +++ b/src/__tests__/auto-cleanup-skip.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' let render beforeAll(() => { diff --git a/src/__tests__/auto-cleanup.js b/src/__tests__/auto-cleanup.js index 27debe45..450a6136 100644 --- a/src/__tests__/auto-cleanup.js +++ b/src/__tests__/auto-cleanup.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import {render} from '../' // This just verifies that by importing RTL in an diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js index 4b67814a..6043ae06 100644 --- a/src/__tests__/cleanup.js +++ b/src/__tests__/cleanup.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import {render, cleanup} from '../' test('cleans up the document', () => { diff --git a/src/__tests__/debug.js b/src/__tests__/debug.js index 14c0779a..e4e9faa0 100644 --- a/src/__tests__/debug.js +++ b/src/__tests__/debug.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import {render, screen} from '../' beforeEach(() => { diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js index cbbf0973..87c70f1b 100644 --- a/src/__tests__/end-to-end.js +++ b/src/__tests__/end-to-end.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import {render, waitForElementToBeRemoved, screen} from '../' const fetchAMessage = () => diff --git a/src/__tests__/events.js b/src/__tests__/events.js index 0e28848f..8a6899af 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import {render, fireEvent} from '../' const eventTypes = [ diff --git a/src/__tests__/multi-base.js b/src/__tests__/multi-base.js index 06ad0902..ef5a7e11 100644 --- a/src/__tests__/multi-base.js +++ b/src/__tests__/multi-base.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import {render} from '../' // these are created once per test suite and reused for each case diff --git a/src/__tests__/render.js b/src/__tests__/render.js index 4e78afa6..fdc1ff4c 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import ReactDOM from 'react-dom' import {render, screen} from '../' diff --git a/src/__tests__/rerender.js b/src/__tests__/rerender.js index 7cb9156d..f8ea377e 100644 --- a/src/__tests__/rerender.js +++ b/src/__tests__/rerender.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import {render} from '../' test('rerender will re-render the element', () => { diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js index 3fe423b1..eeaf395c 100644 --- a/src/__tests__/stopwatch.js +++ b/src/__tests__/stopwatch.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import {render, fireEvent, screen} from '../' class StopWatch extends React.Component { diff --git a/src/act-compat.js b/src/act-compat.js index e999ecfe..40ecdab9 100644 --- a/src/act-compat.js +++ b/src/act-compat.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import ReactDOM from 'react-dom' import * as testUtils from 'react-dom/test-utils' diff --git a/src/pure.js b/src/pure.js index 8a062038..75098f78 100644 --- a/src/pure.js +++ b/src/pure.js @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import ReactDOM from 'react-dom' import { getQueriesForElement, From 1fc17466722be4dcc3224b2997b9c1f3bda4f740 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Fri, 30 Oct 2020 09:17:11 -0600 Subject: [PATCH 023/142] chore: adjust .travis to install latest npm (#812) As recommended by @ljharb https://github.com/testing-library/react-testing-library/pull/809/files#r514441103 --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index fbe104b8..026c7f4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,10 +12,9 @@ env: - REACT_DIST=latest - REACT_DIST=next - REACT_DIST=experimental +before_install: + - nvm install-latest-npm install: - # 7.0.6 fixed CI environments prompting user input. - # Can be removed once node15 bumps the shipped npm version. - - npm install --global npm@>=7.0.6 - npm install # as requested by the React team :) # https://reactjs.org/blog/2019/10/22/react-release-channels.html#using-the-next-channel-for-integration-testing From 48d2e73d8e7c77d03c63ec0876faa4196fdcfde9 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2020 09:18:54 -0600 Subject: [PATCH 024/142] docs: add ljharb as a contributor (#813) * docs: update README.md * docs: update .all-contributorsrc * docs: update README.md * docs: update .all-contributorsrc Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 10 ++++++++++ README.md | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0fd4f4ab..bf9b7cac 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1203,6 +1203,16 @@ "contributions": [ "doc" ] + }, + { + "login": "ljharb", + "name": "Jordan Harband", + "avatar_url": "https://avatars1.githubusercontent.com/u/45469?v=4", + "profile": "https://github.com/ljharb", + "contributions": [ + "review", + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index a43cb5a6..32eba9dc 100644 --- a/README.md +++ b/README.md @@ -599,12 +599,12 @@ Thanks goes to these people ([emoji key][emojis]):
Karthick Raja

💻
Abdelrahman Ashraf

💻
Lidor Avitan

📖 +
Jordan Harband

👀 🤔 - This project follows the [all-contributors][all-contributors] specification. From e07e6416f25fd75df510c5d4211dc06f9f7398d7 Mon Sep 17 00:00:00 2001 From: Weyert de Boer Date: Tue, 3 Nov 2020 14:25:18 +0000 Subject: [PATCH 025/142] fix: upgrade dependencies, typescript, @testing-library/dom etc (#816) Co-authored-by: Weyert de Boer --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 9b314d91..e4b15e4a 100644 --- a/package.json +++ b/package.json @@ -43,20 +43,20 @@ "author": "Kent C. Dodds (https://kentcdodds.com)", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.11.2", - "@testing-library/dom": "^7.26.0" + "@babel/runtime": "^7.12.1", + "@testing-library/dom": "^7.26.4" }, "devDependencies": { - "@testing-library/jest-dom": "^5.11.4", + "@testing-library/jest-dom": "^5.11.5", "@types/react-dom": "^16.9.8", - "dotenv-cli": "^3.2.0", - "dtslint": "4.0.0", - "kcd-scripts": "^6.3.0", + "dotenv-cli": "^4.0.0", + "dtslint": "4.0.4", + "kcd-scripts": "^6.6.0", "npm-run-all": "^4.1.5", "react": "^16.13.1", "react-dom": "^16.13.1", "rimraf": "^3.0.2", - "typescript": "^4.0.2" + "typescript": "^4.0.5" }, "peerDependencies": { "react": "*", From 2712dc2da46bc8af456b4759d5e4b2c971c54092 Mon Sep 17 00:00:00 2001 From: Marco Moretti Date: Tue, 10 Nov 2020 20:04:53 +0100 Subject: [PATCH 026/142] fix: import pretty-format from @testing-library/dom (#821) --- package.json | 2 +- types/index.d.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e4b15e4a..96c7df0f 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.1", - "@testing-library/dom": "^7.26.4" + "@testing-library/dom": "^7.26.6" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.5", diff --git a/types/index.d.ts b/types/index.d.ts index 50702ecc..953f05d6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,7 +1,11 @@ // TypeScript Version: 3.8 -import {OptionsReceived as PrettyFormatOptions} from 'pretty-format' -import {queries, Queries, BoundFunction} from '@testing-library/dom' +import { + queries, + Queries, + BoundFunction, + prettyFormat, +} from '@testing-library/dom' import {act as reactAct} from 'react-dom/test-utils' export * from '@testing-library/dom' @@ -15,7 +19,7 @@ export type RenderResult = { | DocumentFragment | Array, maxLength?: number, - options?: PrettyFormatOptions, + options?: prettyFormat.OptionsReceived, ) => void rerender: (ui: React.ReactElement) => void unmount: () => boolean From 007b0b72d60fe018c950de1c07cb9889d7682f68 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2020 12:09:13 -0700 Subject: [PATCH 027/142] docs: add marcosvega91 as a contributor (#822) 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 bf9b7cac..8c514263 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1213,6 +1213,15 @@ "review", "ideas" ] + }, + { + "login": "marcosvega91", + "name": "Marco Moretti", + "avatar_url": "https://avatars2.githubusercontent.com/u/5365582?v=4", + "profile": "https://github.com/marcosvega91", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 32eba9dc..c7033e52 100644 --- a/README.md +++ b/README.md @@ -601,6 +601,9 @@ Thanks goes to these people ([emoji key][emojis]):
Lidor Avitan

📖
Jordan Harband

👀 🤔 + +
Marco Moretti

💻 + From 3af1b4ba12dfc3a5c8f4968c1b92dba53823f9af Mon Sep 17 00:00:00 2001 From: Nick McCurdy Date: Sat, 14 Nov 2020 11:19:21 -0500 Subject: [PATCH 028/142] chore: switch to github actions (#826) Co-authored-by: Kent C. Dodds --- .github/workflows/validate.yml | 88 ++++++++++++++++++++++++++++++++++ .travis.yml | 38 --------------- README.md | 15 +++--- other/MAINTAINING.md | 8 ++-- package.json | 15 +++--- types/test.tsx | 26 ++++++---- 6 files changed, 127 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/validate.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 00000000..dcb53ad2 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,88 @@ +name: validate +on: + push: + branches: + [ + '+([0-9])?(.{+([0-9]),x}).x', + 'master', + 'next', + 'next-major', + 'beta', + 'alpha', + '!all-contributors/**', + ] + pull_request: + branches-ignore: ['all-contributors/**'] +jobs: + main: + continue-on-error: ${{ matrix.react != 'latest' }} + strategy: + matrix: + node: [10.13, 12, 14, 15] + react: [latest, next, experimental] + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v2 + + - name: ⎔ Setup node + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + + - name: 📥 Download deps + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + # 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 + run: npm install react@${{matrix.react}} react-dom@${{matrix.react}} + + - name: ▶️ Run validate script + run: npm run validate + + - name: ⬆️ Upload coverage report + uses: codecov/codecov-action@v1 + + release: + needs: main + 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', + github.ref) && github.event_name == 'push' }} + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v2 + + - name: ⎔ Setup node + uses: actions/setup-node@v1 + with: + node-version: 14 + + - name: 📥 Download deps + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: 🏗 Run build script + run: npm run build + + - name: 🚀 Release + uses: cycjimmy/semantic-release-action@v2 + with: + semantic_version: 17 + branches: | + [ + '+([0-9])?(.{+([0-9]),x}).x', + 'master', + 'next', + 'next-major', + {name: 'beta', prerelease: true}, + {name: 'alpha', prerelease: true} + ] + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 026c7f4b..00000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -language: node_js -cache: npm -notifications: - email: false -node_js: - # technically we support 10.0.0, but some of our tooling doesn't - - 10.14.2 - - 12 - - 14 - - 15 -env: - - REACT_DIST=latest - - REACT_DIST=next - - REACT_DIST=experimental -before_install: - - nvm install-latest-npm -install: - - npm install - # as requested by the React team :) - # https://reactjs.org/blog/2019/10/22/react-release-channels.html#using-the-next-channel-for-integration-testing - - npm install react@$REACT_DIST react-dom@$REACT_DIST -script: - - npm run validate - - npx codecov@3 -branches: - only: - - master - - beta - -jobs: - allow_failures: - - REACT_DIST=next - - REACT_DIST=experimental - include: - - stage: release - node_js: 14 - script: kcd-scripts travis-release - if: fork = false diff --git a/README.md b/README.md index c7033e52..73e421bb 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,12 @@ practices.

[![Build Status][build-badge]][build] [![Code Coverage][coverage-badge]][coverage] -[![version][version-badge]][package] [![downloads][downloads-badge]][npmtrends] +[![version][version-badge]][package] +[![downloads][downloads-badge]][npmtrends] [![MIT License][license-badge]][license] - -[![All Contributors](https://img.shields.io/badge/all_contributors-102-orange.svg?style=flat-square)](#contributors) -[![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] +[![All Contributors][all-contributors-badge]](#contributors) +[![PRs Welcome][prs-badge]][prs] +[![Code of Conduct][coc-badge]][coc] [![Discord][discord-badge]][discord] [![Watch on GitHub][github-watch-badge]][github-watch] @@ -608,6 +609,7 @@ Thanks goes to these people ([emoji key][emojis]): + This project follows the [all-contributors][all-contributors] specification. @@ -622,8 +624,8 @@ 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/travis/testing-library/react-testing-library.svg?style=flat-square -[build]: https://travis-ci.org/testing-library/react-testing-library +[build-badge]: https://img.shields.io/github/workflow/status/testing-library/react-testing-library/validate?logo=github&style=flat-square +[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 [version-badge]: https://img.shields.io/npm/v/@testing-library/react.svg?style=flat-square @@ -644,6 +646,7 @@ Contributions of any kind welcome! [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/testing-library/react-testing-library.svg?style=social [emojis]: https://github.com/all-contributors/all-contributors#emoji-key [all-contributors]: https://github.com/all-contributors/all-contributors +[all-contributors-badge]: https://img.shields.io/github/all-contributors/testing-library/react-testing-library?color=orange&style=flat-square [guiding-principle]: https://twitter.com/kentcdodds/status/977018512689455106 [bugs]: https://github.com/testing-library/react-testing-library/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Acreated-desc [requests]: https://github.com/testing-library/react-testing-library/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3Aenhancement+is%3Aopen diff --git a/other/MAINTAINING.md b/other/MAINTAINING.md index 703126da..cade1c2a 100644 --- a/other/MAINTAINING.md +++ b/other/MAINTAINING.md @@ -32,9 +32,9 @@ any more of you than that. As a maintainer, you're fine to make your branches on the main repo or on your own fork. Either way is fine. -When we receive a pull request, a travis build is kicked off automatically (see -the `.travis.yml` for what runs in the travis build). We avoid merging anything -that breaks the travis build. +When we receive a pull request, a github action is kicked off automatically (see +the `.github/workflows/validate.yml` for what runs in the action). We avoid +merging anything that breaks the validate action. Please review PRs and focus on the code rather than the individual. You never know when this is someone's first ever PR and we want their experience to be as @@ -49,7 +49,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 -travis build gets kicked off and if it's successful, a tool called +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 GitHub. It is only able to determine the version and whether a release is diff --git a/package.json b/package.json index 96c7df0f..086e82bc 100644 --- a/package.json +++ b/package.json @@ -43,18 +43,19 @@ "author": "Kent C. Dodds (https://kentcdodds.com)", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.1", + "@babel/runtime": "^7.12.5", "@testing-library/dom": "^7.26.6" }, "devDependencies": { - "@testing-library/jest-dom": "^5.11.5", - "@types/react-dom": "^16.9.8", + "@testing-library/jest-dom": "^5.11.6", + "@types/estree": "0.0.45", + "@types/react-dom": "^16.9.9", "dotenv-cli": "^4.0.0", - "dtslint": "4.0.4", - "kcd-scripts": "^6.6.0", + "dtslint": "4.0.5", + "kcd-scripts": "^7.0.3", "npm-run-all": "^4.1.5", - "react": "^16.13.1", - "react-dom": "^16.13.1", + "react": "^17.0.1", + "react-dom": "^17.0.1", "rimraf": "^3.0.2", "typescript": "^4.0.5" }, diff --git a/types/test.tsx b/types/test.tsx index c273feb0..3971037d 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import {render, fireEvent, screen, waitFor} from '@testing-library/react' import * as pure from '@testing-library/react/pure' -async function testRender() { +export async function testRender() { const page = render(
) // single queries @@ -17,9 +17,10 @@ async function testRender() { // helpers const {container, rerender, debug} = page + return {container, rerender, debug} } -async function testPureRender() { +export async function testPureRender() { const page = pure.render(
) // single queries @@ -34,20 +35,21 @@ async function testPureRender() { // helpers const {container, rerender, debug} = page + return {container, rerender, debug} } -async function testRenderOptions() { +export function testRenderOptions() { const container = document.createElement('div') const options = {container} render(
, options) } -async function testFireEvent() { +export function testFireEvent() { const {container} = render(
@@ -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 051/142] 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 052/142] 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

💻 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 053/142] 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 Silbermann Date: Tue, 8 Jun 2021 10:55:05 +0200 Subject: [PATCH 054/142] 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 055/142] 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 056/142] 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 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 079/142] 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 080/142] 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 081/142] 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

💻 🐛 +
Sebastian Malton

🐛 💻 From 2c451b346815b30dace8a5f7b2ed6a78d17f47cc Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 13 Apr 2022 16:38:43 +0200 Subject: [PATCH 082/142] 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 083/142] 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 084/142] 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 085/142] 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 086/142] 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

🐛 💻 +
Martin Böttcher

💻 From f176285e4e92754751b708e1b1adf1f38edea6a8 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 17 May 2022 20:51:03 +0200 Subject: [PATCH 087/142] 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 088/142] 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 089/142] 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 090/142] 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 091/142] 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

💻 +
Dominik Dorfmeister

💻 From 7c7dc785501f2e75cbcb5d49df78340914dfba8c Mon Sep 17 00:00:00 2001 From: Stephen Sauceda Date: Sat, 1 Oct 2022 15:20:38 -0400 Subject: [PATCH 092/142] 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 093/142] 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]): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Kent C. Dodds

💻 📖 🚇 ⚠️

Ryan Castner

📖

Daniel Sandiego

💻

Paweł Mikołajczyk

💻

Alejandro Ñáñez Ortiz

📖

Matt Parrish

🐛 💻 📖 ⚠️

Justin Hall

📦

Anto Aravinth

💻 ⚠️ 📖

Jonah Moses

📖

Łukasz Gandecki

💻 ⚠️ 📖

Ivan Babak

🐛 🤔

Jesse Day

💻

Ernesto García

💬 💻 📖

Josef Maxx Blake

💻 📖 ⚠️

Michal Baranowski

📝

Arthur Puthin

📖

Thomas Chia

💻 📖

Thiago Galvani

📖

Christian

⚠️

Alex Krolick

💬 📖 💡 🤔

Johann Hubert Sonntagbauer

💻 📖 ⚠️

Maddi Joyce

💻

Ryan Vice

📖

Ian Wilson

📝

Daniel

🐛 💻

Giorgio Polvara

🐛 🤔

John Gozde

💻

Sam Horton

📖 💡 🤔

Richard Kotze (mobile)

📖

Brahian E. Soto Mercedes

📖

Benoit de La Forest

📖

Salah

💻 ⚠️

Adam Gordon

🐛 💻

Matija Marohnić

📖

Justice Mba

📖

Mark Pollmann

📖

Ehtesham Kafeel

💻 📖

Julio Pavón

💻

Duncan L

📖 💡

Tiago Almeida

📖

Robert Smith

🐛

Zach Green

📖

dadamssg

📖

Yazan Aabed

📝

Tim

🐛 💻 📖 ⚠️

Divyanshu Maithani

📹

Deepak Grover

📹

Eyal Cohen

📖

Peter Makowski

📖

Michiel Nuyts

📖

Joe Ng'ethe

💻 📖

Kate

📖

Sean

📖

James Long

🤔 📦

Herb Hagely

💡

Alex Wendte

💡

Monica Powell

📖

Vitaly Sivkov

💻

Weyert de Boer

🤔 👀 🎨

EstebanMarin

📖

Victor Martins

📖

Royston Shufflebotham

🐛 📖 💡

chrbala

💻

Donavon West

💻 📖 🤔 ⚠️

Richard Maisano

💻

Marco Biedermann

💻 🚧 ⚠️

Alex Zherdev

🐛 💻

André Matulionis dos Santos

💻 💡 ⚠️

Daniel K.

🐛 💻 🤔 ⚠️ 👀

mohamedmagdy17593

💻

Loren ☺️

📖

MarkFalconbridge

🐛 💻

Vinicius

📖 💡

Peter Schyma

💻

Ian Schmitz

📖

Joel Marcotte

🐛 ⚠️ 💻

Alejandro Dustet

🐛

Brandon Carroll

📖

Lucas Machado

📖

Pascal Duez

📦

Minh Nguyen

💻

LiaoJimmy

📖

Sunil Pai

💻 ⚠️

Dan Abramov

👀

Christian Murphy

🚇

Ivakhnenko Dmitry

💻

James George

📖

João Fernandes

📖

Alejandro Perea

👀

Nick McCurdy

👀 💬 🚇

Sebastian Silbermann

👀

Adrià Fontcuberta

👀 📖

John Reilly

👀

Michaël De Boey

👀 💻

Tim Yates

👀

Brian Donovan

💻

Noam Gabriel Jacobson

📖

Ronald van der Kooij

⚠️ 💻

Aayush Rajvanshi

📖

Ely Alamillo

💻 ⚠️

Daniel Afonso

💻 ⚠️

Laurens Bosscher

💻

Sakito Mukai

📖

Türker Teke

📖

Zach Brogan

💻 ⚠️

Ryota Murakami

📖

Michael Hottman

🤔

Steven Fitzpatrick

🐛

Juan Je García

📖

Championrunner

📖

Sam Tsai

💻 ⚠️ 📖

Christian Rackerseder

💻

Andrei Picus

🐛 👀

Artem Zakharchenko

📖

Michael

📖

Braden Lee

📖

Kamran Ayub

💻 ⚠️

Matan Borenkraout

💻

Ryan Bigg

🚧

Anton Halim

📖

Artem Malko

💻

Gerrit Alex

💻

Karthick Raja

💻

Abdelrahman Ashraf

💻

Lidor Avitan

📖

Jordan Harband

👀 🤔

Marco Moretti

💻

sanchit121

🐛 💻

Solufa

🐛 💻

Ari Perkkiö

⚠️

Johannes Ewald

💻

Angus J. Pope

📖

Dominik Lesch

📖

Marcos Gómez

📖

Akash Shyam

🐛

Fabian Meumertzheim

💻 🐛

Sebastian Malton

🐛 💻

Martin Böttcher

💻

Dominik Dorfmeister

💻
Kent C. Dodds
Kent C. Dodds

💻 📖 🚇 ⚠️
Ryan Castner
Ryan Castner

📖
Daniel Sandiego
Daniel Sandiego

💻
Paweł Mikołajczyk
Paweł Mikołajczyk

💻
Alejandro Ñáñez Ortiz
Alejandro Ñáñez Ortiz

📖
Matt Parrish
Matt Parrish

🐛 💻 📖 ⚠️
Justin Hall
Justin Hall

📦
Anto Aravinth
Anto Aravinth

💻 ⚠️ 📖
Jonah Moses
Jonah Moses

📖
Łukasz Gandecki
Łukasz Gandecki

💻 ⚠️ 📖
Ivan Babak
Ivan Babak

🐛 🤔
Jesse Day
Jesse Day

💻
Ernesto García
Ernesto García

💬 💻 📖
Josef Maxx Blake
Josef Maxx Blake

💻 📖 ⚠️
Michal Baranowski
Michal Baranowski

📝
Arthur Puthin
Arthur Puthin

📖
Thomas Chia
Thomas Chia

💻 📖
Thiago Galvani
Thiago Galvani

📖
Christian
Christian

⚠️
Alex Krolick
Alex Krolick

💬 📖 💡 🤔
Johann Hubert Sonntagbauer
Johann Hubert Sonntagbauer

💻 📖 ⚠️
Maddi Joyce
Maddi Joyce

💻
Ryan Vice
Ryan Vice

📖
Ian Wilson
Ian Wilson

📝
Daniel
Daniel

🐛 💻
Giorgio Polvara
Giorgio Polvara

🐛 🤔
John Gozde
John Gozde

💻
Sam Horton
Sam Horton

📖 💡 🤔
Richard Kotze (mobile)
Richard Kotze (mobile)

📖
Brahian E. Soto Mercedes
Brahian E. Soto Mercedes

📖
Benoit de La Forest
Benoit de La Forest

📖
Salah
Salah

💻 ⚠️
Adam Gordon
Adam Gordon

🐛 💻
Matija Marohnić
Matija Marohnić

📖
Justice Mba
Justice Mba

📖
Mark Pollmann
Mark Pollmann

📖
Ehtesham Kafeel
Ehtesham Kafeel

💻 📖
Julio Pavón
Julio Pavón

💻
Duncan L
Duncan L

📖 💡
Tiago Almeida
Tiago Almeida

📖
Robert Smith
Robert Smith

🐛
Zach Green
Zach Green

📖
dadamssg
dadamssg

📖
Yazan Aabed
Yazan Aabed

📝
Tim
Tim

🐛 💻 📖 ⚠️
Divyanshu Maithani
Divyanshu Maithani

📹
Deepak Grover
Deepak Grover

📹
Eyal Cohen
Eyal Cohen

📖
Peter Makowski
Peter Makowski

📖
Michiel Nuyts
Michiel Nuyts

📖
Joe Ng'ethe
Joe Ng'ethe

💻 📖
Kate
Kate

📖
Sean
Sean

📖
James Long
James Long

🤔 📦
Herb Hagely
Herb Hagely

💡
Alex Wendte
Alex Wendte

💡
Monica Powell
Monica Powell

📖
Vitaly Sivkov
Vitaly Sivkov

💻
Weyert de Boer
Weyert de Boer

🤔 👀 🎨
EstebanMarin
EstebanMarin

📖
Victor Martins
Victor Martins

📖
Royston Shufflebotham
Royston Shufflebotham

🐛 📖 💡
chrbala
chrbala

💻
Donavon West
Donavon West

💻 📖 🤔 ⚠️
Richard Maisano
Richard Maisano

💻
Marco Biedermann
Marco Biedermann

💻 🚧 ⚠️
Alex Zherdev
Alex Zherdev

🐛 💻
André Matulionis dos Santos
André Matulionis dos Santos

💻 💡 ⚠️
Daniel K.
Daniel K.

🐛 💻 🤔 ⚠️ 👀
mohamedmagdy17593
mohamedmagdy17593

💻
Loren ☺️
Loren ☺️

📖
MarkFalconbridge
MarkFalconbridge

🐛 💻
Vinicius
Vinicius

📖 💡
Peter Schyma
Peter Schyma

💻
Ian Schmitz
Ian Schmitz

📖
Joel Marcotte
Joel Marcotte

🐛 ⚠️ 💻
Alejandro Dustet
Alejandro Dustet

🐛
Brandon Carroll
Brandon Carroll

📖
Lucas Machado
Lucas Machado

📖
Pascal Duez
Pascal Duez

📦
Minh Nguyen
Minh Nguyen

💻
LiaoJimmy
LiaoJimmy

📖
Sunil Pai
Sunil Pai

💻 ⚠️
Dan Abramov
Dan Abramov

👀
Christian Murphy
Christian Murphy

🚇
Ivakhnenko Dmitry
Ivakhnenko Dmitry

💻
James George
James George

📖
João Fernandes
João Fernandes

📖
Alejandro Perea
Alejandro Perea

👀
Nick McCurdy
Nick McCurdy

👀 💬 🚇
Sebastian Silbermann
Sebastian Silbermann

👀
Adrià Fontcuberta
Adrià Fontcuberta

👀 📖
John Reilly
John Reilly

👀
Michaël De Boey
Michaël De Boey

👀 💻
Tim Yates
Tim Yates

👀
Brian Donovan
Brian Donovan

💻
Noam Gabriel Jacobson
Noam Gabriel Jacobson

📖
Ronald van der Kooij
Ronald van der Kooij

⚠️ 💻
Aayush Rajvanshi
Aayush Rajvanshi

📖
Ely Alamillo
Ely Alamillo

💻 ⚠️
Daniel Afonso
Daniel Afonso

💻 ⚠️
Laurens Bosscher
Laurens Bosscher

💻
Sakito Mukai
Sakito Mukai

📖
Türker Teke
Türker Teke

📖
Zach Brogan
Zach Brogan

💻 ⚠️
Ryota Murakami
Ryota Murakami

📖
Michael Hottman
Michael Hottman

🤔
Steven Fitzpatrick
Steven Fitzpatrick

🐛
Juan Je García
Juan Je García

📖
Championrunner
Championrunner

📖
Sam Tsai
Sam Tsai

💻 ⚠️ 📖
Christian Rackerseder
Christian Rackerseder

💻
Andrei Picus
Andrei Picus

🐛 👀
Artem Zakharchenko
Artem Zakharchenko

📖
Michael
Michael

📖
Braden Lee
Braden Lee

📖
Kamran Ayub
Kamran Ayub

💻 ⚠️
Matan Borenkraout
Matan Borenkraout

💻
Ryan Bigg
Ryan Bigg

🚧
Anton Halim
Anton Halim

📖
Artem Malko
Artem Malko

💻
Gerrit Alex
Gerrit Alex

💻
Karthick Raja
Karthick Raja

💻
Abdelrahman Ashraf
Abdelrahman Ashraf

💻
Lidor Avitan
Lidor Avitan

📖
Jordan Harband
Jordan Harband

👀 🤔
Marco Moretti
Marco Moretti

💻
sanchit121
sanchit121

🐛 💻
Solufa
Solufa

🐛 💻
Ari Perkkiö
Ari Perkkiö

⚠️
Johannes Ewald
Johannes Ewald

💻
Angus J. Pope
Angus J. Pope

📖
Dominik Lesch
Dominik Lesch

📖
Marcos Gómez
Marcos Gómez

📖
Akash Shyam
Akash Shyam

🐛
Fabian Meumertzheim
Fabian Meumertzheim

💻 🐛
Sebastian Malton
Sebastian Malton

🐛 💻
Martin Böttcher
Martin Böttcher

💻
Dominik Dorfmeister
Dominik Dorfmeister

💻
Stephen Sauceda
Stephen Sauceda

📖
From 4d76a4a75541ceccbc23a452ac6b291e6bfde927 Mon Sep 17 00:00:00 2001 From: Sergey Bunas Date: Wed, 5 Oct 2022 21:32:52 +0300 Subject: [PATCH 094/142] 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 095/142] 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 096/142] 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: Alex Date: Mon, 12 Dec 2022 19:33:05 +0200 Subject: [PATCH 097/142] 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 098/142] 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 099/142] 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 Silbermann Date: Thu, 16 Feb 2023 23:34:19 +0100 Subject: [PATCH 100/142] 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 101/142] 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 102/142] 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) { - return
Loading...
- } + 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 103/142] 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 104/142] 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 105/142] 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 106/142] 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 107/142] 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 108/142] 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]): - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + + + + - - -
Kent C. Dodds
Kent C. Dodds

💻 📖 🚇 ⚠️
Ryan Castner
Ryan Castner

📖
Daniel Sandiego
Daniel Sandiego

💻
Paweł Mikołajczyk
Paweł Mikołajczyk

💻
Alejandro Ñáñez Ortiz
Alejandro Ñáñez Ortiz

📖
Matt Parrish
Matt Parrish

🐛 💻 📖 ⚠️
Justin Hall
Justin Hall

📦
Kent C. Dodds
Kent C. Dodds

💻 📖 🚇 ⚠️
Ryan Castner
Ryan Castner

📖
Daniel Sandiego
Daniel Sandiego

💻
Paweł Mikołajczyk
Paweł Mikołajczyk

💻
Alejandro Ñáñez Ortiz
Alejandro Ñáñez Ortiz

📖
Matt Parrish
Matt Parrish

🐛 💻 📖 ⚠️
Justin Hall
Justin Hall

📦
Anto Aravinth
Anto Aravinth

💻 ⚠️ 📖
Jonah Moses
Jonah Moses

📖
Łukasz Gandecki
Łukasz Gandecki

💻 ⚠️ 📖
Ivan Babak
Ivan Babak

🐛 🤔
Jesse Day
Jesse Day

💻
Ernesto García
Ernesto García

💬 💻 📖
Josef Maxx Blake
Josef Maxx Blake

💻 📖 ⚠️
Anto Aravinth
Anto Aravinth

💻 ⚠️ 📖
Jonah Moses
Jonah Moses

📖
Łukasz Gandecki
Łukasz Gandecki

💻 ⚠️ 📖
Ivan Babak
Ivan Babak

🐛 🤔
Jesse Day
Jesse Day

💻
Ernesto García
Ernesto García

💬 💻 📖
Josef Maxx Blake
Josef Maxx Blake

💻 📖 ⚠️
Michal Baranowski
Michal Baranowski

📝
Arthur Puthin
Arthur Puthin

📖
Thomas Chia
Thomas Chia

💻 📖
Thiago Galvani
Thiago Galvani

📖
Christian
Christian

⚠️
Alex Krolick
Alex Krolick

💬 📖 💡 🤔
Johann Hubert Sonntagbauer
Johann Hubert Sonntagbauer

💻 📖 ⚠️
Michal Baranowski
Michal Baranowski

📝
Arthur Puthin
Arthur Puthin

📖
Thomas Chia
Thomas Chia

💻 📖
Thiago Galvani
Thiago Galvani

📖
Christian
Christian

⚠️
Alex Krolick
Alex Krolick

💬 📖 💡 🤔
Johann Hubert Sonntagbauer
Johann Hubert Sonntagbauer

💻 📖 ⚠️
Maddi Joyce
Maddi Joyce

💻
Ryan Vice
Ryan Vice

📖
Ian Wilson
Ian Wilson

📝
Daniel
Daniel

🐛 💻
Giorgio Polvara
Giorgio Polvara

🐛 🤔
John Gozde
John Gozde

💻
Sam Horton
Sam Horton

📖 💡 🤔
Maddi Joyce
Maddi Joyce

💻
Ryan Vice
Ryan Vice

📖
Ian Wilson
Ian Wilson

📝
Daniel
Daniel

🐛 💻
Giorgio Polvara
Giorgio Polvara

🐛 🤔
John Gozde
John Gozde

💻
Sam Horton
Sam Horton

📖 💡 🤔
Richard Kotze (mobile)
Richard Kotze (mobile)

📖
Brahian E. Soto Mercedes
Brahian E. Soto Mercedes

📖
Benoit de La Forest
Benoit de La Forest

📖
Salah
Salah

💻 ⚠️
Adam Gordon
Adam Gordon

🐛 💻
Matija Marohnić
Matija Marohnić

📖
Justice Mba
Justice Mba

📖
Richard Kotze (mobile)
Richard Kotze (mobile)

📖
Brahian E. Soto Mercedes
Brahian E. Soto Mercedes

📖
Benoit de La Forest
Benoit de La Forest

📖
Salah
Salah

💻 ⚠️
Adam Gordon
Adam Gordon

🐛 💻
Matija Marohnić
Matija Marohnić

📖
Justice Mba
Justice Mba

📖
Mark Pollmann
Mark Pollmann

📖
Ehtesham Kafeel
Ehtesham Kafeel

💻 📖
Julio Pavón
Julio Pavón

💻
Duncan L
Duncan L

📖 💡
Tiago Almeida
Tiago Almeida

📖
Robert Smith
Robert Smith

🐛
Zach Green
Zach Green

📖
Mark Pollmann
Mark Pollmann

📖
Ehtesham Kafeel
Ehtesham Kafeel

💻 📖
Julio Pavón
Julio Pavón

💻
Duncan L
Duncan L

📖 💡
Tiago Almeida
Tiago Almeida

📖
Robert Smith
Robert Smith

🐛
Zach Green
Zach Green

📖
dadamssg
dadamssg

📖
Yazan Aabed
Yazan Aabed

📝
Tim
Tim

🐛 💻 📖 ⚠️
Divyanshu Maithani
Divyanshu Maithani

📹
Deepak Grover
Deepak Grover

📹
Eyal Cohen
Eyal Cohen

📖
Peter Makowski
Peter Makowski

📖
dadamssg
dadamssg

📖
Yazan Aabed
Yazan Aabed

📝
Tim
Tim

🐛 💻 📖 ⚠️
Divyanshu Maithani
Divyanshu Maithani

📹
Deepak Grover
Deepak Grover

📹
Eyal Cohen
Eyal Cohen

📖
Peter Makowski
Peter Makowski

📖
Michiel Nuyts
Michiel Nuyts

📖
Joe Ng'ethe
Joe Ng'ethe

💻 📖
Kate
Kate

📖
Sean
Sean

📖
James Long
James Long

🤔 📦
Herb Hagely
Herb Hagely

💡
Alex Wendte
Alex Wendte

💡
Michiel Nuyts
Michiel Nuyts

📖
Joe Ng'ethe
Joe Ng'ethe

💻 📖
Kate
Kate

📖
Sean
Sean

📖
James Long
James Long

🤔 📦
Herb Hagely
Herb Hagely

💡
Alex Wendte
Alex Wendte

💡
Monica Powell
Monica Powell

📖
Vitaly Sivkov
Vitaly Sivkov

💻
Weyert de Boer
Weyert de Boer

🤔 👀 🎨
EstebanMarin
EstebanMarin

📖
Victor Martins
Victor Martins

📖
Royston Shufflebotham
Royston Shufflebotham

🐛 📖 💡
chrbala
chrbala

💻
Monica Powell
Monica Powell

📖
Vitaly Sivkov
Vitaly Sivkov

💻
Weyert de Boer
Weyert de Boer

🤔 👀 🎨
EstebanMarin
EstebanMarin

📖
Victor Martins
Victor Martins

📖
Royston Shufflebotham
Royston Shufflebotham

🐛 📖 💡
chrbala
chrbala

💻
Donavon West
Donavon West

💻 📖 🤔 ⚠️
Richard Maisano
Richard Maisano

💻
Marco Biedermann
Marco Biedermann

💻 🚧 ⚠️
Alex Zherdev
Alex Zherdev

🐛 💻
André Matulionis dos Santos
André Matulionis dos Santos

💻 💡 ⚠️
Daniel K.
Daniel K.

🐛 💻 🤔 ⚠️ 👀
mohamedmagdy17593
mohamedmagdy17593

💻
Donavon West
Donavon West

💻 📖 🤔 ⚠️
Richard Maisano
Richard Maisano

💻
Marco Biedermann
Marco Biedermann

💻 🚧 ⚠️
Alex Zherdev
Alex Zherdev

🐛 💻
André Matulionis dos Santos
André Matulionis dos Santos

💻 💡 ⚠️
Daniel K.
Daniel K.

🐛 💻 🤔 ⚠️ 👀
mohamedmagdy17593
mohamedmagdy17593

💻
Loren ☺️
Loren ☺️

📖
MarkFalconbridge
MarkFalconbridge

🐛 💻
Vinicius
Vinicius

📖 💡
Peter Schyma
Peter Schyma

💻
Ian Schmitz
Ian Schmitz

📖
Joel Marcotte
Joel Marcotte

🐛 ⚠️ 💻
Alejandro Dustet
Alejandro Dustet

🐛
Loren ☺️
Loren ☺️

📖
MarkFalconbridge
MarkFalconbridge

🐛 💻
Vinicius
Vinicius

📖 💡
Peter Schyma
Peter Schyma

💻
Ian Schmitz
Ian Schmitz

📖
Joel Marcotte
Joel Marcotte

🐛 ⚠️ 💻
Alejandro Dustet
Alejandro Dustet

🐛
Brandon Carroll
Brandon Carroll

📖
Lucas Machado
Lucas Machado

📖
Pascal Duez
Pascal Duez

📦
Minh Nguyen
Minh Nguyen

💻
LiaoJimmy
LiaoJimmy

📖
Sunil Pai
Sunil Pai

💻 ⚠️
Dan Abramov
Dan Abramov

👀
Brandon Carroll
Brandon Carroll

📖
Lucas Machado
Lucas Machado

📖
Pascal Duez
Pascal Duez

📦
Minh Nguyen
Minh Nguyen

💻
LiaoJimmy
LiaoJimmy

📖
Sunil Pai
Sunil Pai

💻 ⚠️
Dan Abramov
Dan Abramov

👀
Christian Murphy
Christian Murphy

🚇
Ivakhnenko Dmitry
Ivakhnenko Dmitry

💻
James George
James George

📖
João Fernandes
João Fernandes

📖
Alejandro Perea
Alejandro Perea

👀
Nick McCurdy
Nick McCurdy

👀 💬 🚇
Sebastian Silbermann
Sebastian Silbermann

👀
Christian Murphy
Christian Murphy

🚇
Ivakhnenko Dmitry
Ivakhnenko Dmitry

💻
James George
James George

📖
João Fernandes
João Fernandes

📖
Alejandro Perea
Alejandro Perea

👀
Nick McCurdy
Nick McCurdy

👀 💬 🚇
Sebastian Silbermann
Sebastian Silbermann

👀
Adrià Fontcuberta
Adrià Fontcuberta

👀 📖
John Reilly
John Reilly

👀
Michaël De Boey
Michaël De Boey

👀 💻
Tim Yates
Tim Yates

👀
Brian Donovan
Brian Donovan

💻
Noam Gabriel Jacobson
Noam Gabriel Jacobson

📖
Ronald van der Kooij
Ronald van der Kooij

⚠️ 💻
Adrià Fontcuberta
Adrià Fontcuberta

👀 📖
John Reilly
John Reilly

👀
Michaël De Boey
Michaël De Boey

👀 💻
Tim Yates
Tim Yates

👀
Brian Donovan
Brian Donovan

💻
Noam Gabriel Jacobson
Noam Gabriel Jacobson

📖
Ronald van der Kooij
Ronald van der Kooij

⚠️ 💻
Aayush Rajvanshi
Aayush Rajvanshi

📖
Ely Alamillo
Ely Alamillo

💻 ⚠️
Daniel Afonso
Daniel Afonso

💻 ⚠️
Laurens Bosscher
Laurens Bosscher

💻
Sakito Mukai
Sakito Mukai

📖
Türker Teke
Türker Teke

📖
Zach Brogan
Zach Brogan

💻 ⚠️
Aayush Rajvanshi
Aayush Rajvanshi

📖
Ely Alamillo
Ely Alamillo

💻 ⚠️
Daniel Afonso
Daniel Afonso

💻 ⚠️
Laurens Bosscher
Laurens Bosscher

💻
Sakito Mukai
Sakito Mukai

📖
Türker Teke
Türker Teke

📖
Zach Brogan
Zach Brogan

💻 ⚠️
Ryota Murakami
Ryota Murakami

📖
Michael Hottman
Michael Hottman

🤔
Steven Fitzpatrick
Steven Fitzpatrick

🐛
Juan Je García
Juan Je García

📖
Championrunner
Championrunner

📖
Sam Tsai
Sam Tsai

💻 ⚠️ 📖
Christian Rackerseder
Christian Rackerseder

💻
Ryota Murakami
Ryota Murakami

📖
Michael Hottman
Michael Hottman

🤔
Steven Fitzpatrick
Steven Fitzpatrick

🐛
Juan Je García
Juan Je García

📖
Championrunner
Championrunner

📖
Sam Tsai
Sam Tsai

💻 ⚠️ 📖
Christian Rackerseder
Christian Rackerseder

💻
Andrei Picus
Andrei Picus

🐛 👀
Artem Zakharchenko
Artem Zakharchenko

📖
Michael
Michael

📖
Braden Lee
Braden Lee

📖
Kamran Ayub
Kamran Ayub

💻 ⚠️
Matan Borenkraout
Matan Borenkraout

💻
Ryan Bigg
Ryan Bigg

🚧
Andrei Picus
Andrei Picus

🐛 👀
Artem Zakharchenko
Artem Zakharchenko

📖
Michael
Michael

📖
Braden Lee
Braden Lee

📖
Kamran Ayub
Kamran Ayub

💻 ⚠️
Matan Borenkraout
Matan Borenkraout

💻
Ryan Bigg
Ryan Bigg

🚧
Anton Halim
Anton Halim

📖
Artem Malko
Artem Malko

💻
Gerrit Alex
Gerrit Alex

💻
Karthick Raja
Karthick Raja

💻
Abdelrahman Ashraf
Abdelrahman Ashraf

💻
Lidor Avitan
Lidor Avitan

📖
Jordan Harband
Jordan Harband

👀 🤔
Anton Halim
Anton Halim

📖
Artem Malko
Artem Malko

💻
Gerrit Alex
Gerrit Alex

💻
Karthick Raja
Karthick Raja

💻
Abdelrahman Ashraf
Abdelrahman Ashraf

💻
Lidor Avitan
Lidor Avitan

📖
Jordan Harband
Jordan Harband

👀 🤔
Marco Moretti
Marco Moretti

💻
sanchit121
sanchit121

🐛 💻
Solufa
Solufa

🐛 💻
Ari Perkkiö
Ari Perkkiö

⚠️
Johannes Ewald
Johannes Ewald

💻
Angus J. Pope
Angus J. Pope

📖
Dominik Lesch
Dominik Lesch

📖
Marco Moretti
Marco Moretti

💻
sanchit121
sanchit121

🐛 💻
Solufa
Solufa

🐛 💻
Ari Perkkiö
Ari Perkkiö

⚠️
Johannes Ewald
Johannes Ewald

💻
Angus J. Pope
Angus J. Pope

📖
Dominik Lesch
Dominik Lesch

📖
Marcos Gómez
Marcos Gómez

📖
Akash Shyam
Akash Shyam

🐛
Fabian Meumertzheim
Fabian Meumertzheim

💻 🐛
Sebastian Malton
Sebastian Malton

🐛 💻
Martin Böttcher
Martin Böttcher

💻
Dominik Dorfmeister
Dominik Dorfmeister

💻
Stephen Sauceda
Stephen Sauceda

📖
Marcos Gómez
Marcos Gómez

📖
Akash Shyam
Akash Shyam

🐛
Fabian Meumertzheim
Fabian Meumertzheim

💻 🐛
Sebastian Malton
Sebastian Malton

🐛 💻
Martin Böttcher
Martin Böttcher

💻
Dominik Dorfmeister
Dominik Dorfmeister

💻
Stephen Sauceda
Stephen Sauceda

📖
Colin Diesh
Colin Diesh

📖
From d80319f5695d0ddbd93f7d63ca1cb71450663ba6 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Wed, 8 Nov 2023 08:51:17 +0200 Subject: [PATCH 109/142] 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 110/142] 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 111/142] 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 112/142] 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 113/142] 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 (
- DocumentFragment is pretty cool! + + {greeting} {subject} +
) } - } - const {asFragment} = render() - 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 ( +
+ DocumentFragment is pretty cool! +
+ ) + } + } - const {container} = render(
, { - wrapper: WrapperComponent, + const {asFragment} = render() + 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(` +
+
+ hi +
+
+ `) + + rerender() + expect(container.firstChild).toMatchInlineSnapshot(` +
+
+ hey +
+
+ `) + }) + + test('re-renders twice when reactStrictMode is true', () => { + configure({reactStrictMode: true}) + + const spy = jest.fn() + function Component() { + spy() + return null + } + + const {rerender} = render() + 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 114/142] 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
Colin Diesh

📖 + Yusuke Iinuma
Yusuke Iinuma

💻 From 55e79c290d3ec8a8eb3d39539e2c05bf35dff3d9 Mon Sep 17 00:00:00 2001 From: Jeff Way Date: Thu, 1 Feb 2024 11:49:10 -0800 Subject: [PATCH 115/142] 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, ): RenderResult export 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 116/142] 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]): Colin Diesh
Colin Diesh

📖 Yusuke Iinuma
Yusuke Iinuma

💻 + Jeff Way
Jeff Way

💻 From 7e42f4e84115510f560be36b5febb3d9f20e8899 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 19 Mar 2024 23:24:53 +0100 Subject: [PATCH 117/142] 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 Silbermann Date: Tue, 19 Mar 2024 23:54:12 +0100 Subject: [PATCH 118/142] 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 119/142] 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 120/142] 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 121/142] 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 Borenkraout Date: Wed, 10 Apr 2024 18:18:28 +0300 Subject: [PATCH 122/142] 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 123/142] 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 124/142] 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 125/142] 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, ): RenderResult export 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 Silbermann Date: Tue, 23 Apr 2024 17:32:08 +0200 Subject: [PATCH 126/142] 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(