diff --git a/jest.config.js b/jest.config.js
index 5c840226..0ed33704 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -8,8 +8,8 @@ module.exports = Object.assign(jestConfig, {
// minimum coverage of jobs using React 17 and 18
branches: 80,
functions: 78,
- lines: 84,
- statements: 84,
+ lines: 79,
+ statements: 79,
},
},
})
diff --git a/src/__tests__/act.js b/src/__tests__/act.js
index b60aac37..5430f28b 100644
--- a/src/__tests__/act.js
+++ b/src/__tests__/act.js
@@ -1,5 +1,5 @@
import * as React from 'react'
-import {render, fireEvent, screen} from '../'
+import {act, render, fireEvent, screen} from '../'
test('render calls useEffect immediately', () => {
const effectCb = jest.fn()
@@ -43,3 +43,27 @@ test('calls to hydrate will run useEffects', () => {
render(, {hydrate: true})
expect(effectCb).toHaveBeenCalledTimes(1)
})
+
+test('cleans up IS_REACT_ACT_ENVIRONMENT if its callback throws', () => {
+ global.IS_REACT_ACT_ENVIRONMENT = false
+
+ expect(() =>
+ act(() => {
+ throw new Error('threw')
+ }),
+ ).toThrow('threw')
+
+ expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
+})
+
+test('cleans up IS_REACT_ACT_ENVIRONMENT if its async callback throws', async () => {
+ global.IS_REACT_ACT_ENVIRONMENT = false
+
+ await expect(() =>
+ act(async () => {
+ throw new Error('thenable threw')
+ }),
+ ).rejects.toThrow('thenable threw')
+
+ expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
+})
diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js
index 787a944d..cf222aec 100644
--- a/src/__tests__/end-to-end.js
+++ b/src/__tests__/end-to-end.js
@@ -17,8 +17,6 @@ function ComponentWithLoader() {
let cancelled = false
fetchAMessage().then(data => {
if (!cancelled) {
- // Will trigger "missing act" warnings in React 18 with real timers
- // Need to wait for an action on https://github.com/reactwg/react-18/discussions/23#discussioncomment-1087897
setState({data, loading: false})
}
})
diff --git a/src/act-compat.js b/src/act-compat.js
index 16124afc..c8889e65 100644
--- a/src/act-compat.js
+++ b/src/act-compat.js
@@ -15,7 +15,79 @@ function actPolyfill(cb) {
ReactDOM.render(
, document.createElement('div'))
}
-const act = isomorphicAct || domAct || actPolyfill
+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')
+}
+
+function setReactActEnvironment(isReactActEnvironment) {
+ getGlobalThis().IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment
+}
+
+function getIsReactActEnvironment() {
+ return getGlobalThis().IS_REACT_ACT_ENVIRONMENT
+}
+
+function withGlobalActEnvironment(actImplementation) {
+ return callback => {
+ const previousActEnvironment = getIsReactActEnvironment()
+ setReactActEnvironment(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
+ }
+ return result
+ })
+ if (callbackNeedsToBeAwaited) {
+ const thenable = actResult
+ return {
+ then: (resolve, reject) => {
+ thenable.then(
+ returnValue => {
+ setReactActEnvironment(previousActEnvironment)
+ resolve(returnValue)
+ },
+ error => {
+ setReactActEnvironment(previousActEnvironment)
+ reject(error)
+ },
+ )
+ },
+ }
+ } else {
+ setReactActEnvironment(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.
+ setReactActEnvironment(previousActEnvironment)
+ throw error
+ }
+ }
+}
+
+const act = withGlobalActEnvironment(isomorphicAct || domAct || actPolyfill)
let youHaveBeenWarned = false
let isAsyncActSupported = null
@@ -131,6 +203,6 @@ function asyncAct(cb) {
}
export default act
-export {asyncAct}
+export {asyncAct, setReactActEnvironment, getIsReactActEnvironment}
/* eslint no-console:0 */
diff --git a/src/pure.js b/src/pure.js
index 0ac63feb..dc5fa3fa 100644
--- a/src/pure.js
+++ b/src/pure.js
@@ -5,7 +5,11 @@ import {
prettyDOM,
configure as configureDTL,
} from '@testing-library/dom'
-import act, {asyncAct} from './act-compat'
+import act, {
+ asyncAct,
+ getIsReactActEnvironment,
+ setReactActEnvironment,
+} from './act-compat'
import {fireEvent} from './fire-event'
configureDTL({
@@ -30,7 +34,18 @@ if (React.startTransition !== undefined) {
unstable_advanceTimersWrapper: cb => {
return act(cb)
},
- asyncWrapper: cb => 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 => {
+ const previousActEnvironment = getIsReactActEnvironment()
+ setReactActEnvironment(false)
+ try {
+ return await cb()
+ } finally {
+ setReactActEnvironment(previousActEnvironment)
+ }
+ },
})
}
diff --git a/tests/setup-env.js b/tests/setup-env.js
index 264828a9..6a5fcbee 100644
--- a/tests/setup-env.js
+++ b/tests/setup-env.js
@@ -1 +1,9 @@
import '@testing-library/jest-dom/extend-expect'
+
+beforeEach(() => {
+ global.IS_REACT_ACT_ENVIRONMENT = true
+})
+
+afterEach(() => {
+ global.IS_REACT_ACT_ENVIRONMENT = false
+})