Skip to content

fix: Don't trigger "missing act" warnings when using waitFor+real timers #980

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
})
26 changes: 25 additions & 1 deletion src/__tests__/act.js
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -43,3 +43,27 @@ test('calls to hydrate will run useEffects', () => {
render(<MyUselessComponent />, {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)
})
2 changes: 0 additions & 2 deletions src/__tests__/end-to-end.js
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}
})
Expand Down
76 changes: 74 additions & 2 deletions src/act-compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,79 @@ function actPolyfill(cb) {
ReactDOM.render(<div />, 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
Expand Down Expand Up @@ -131,6 +203,6 @@ function asyncAct(cb) {
}

export default act
export {asyncAct}
export {asyncAct, setReactActEnvironment, getIsReactActEnvironment}

/* eslint no-console:0 */
19 changes: 17 additions & 2 deletions src/pure.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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)
}
},
})
}

Expand Down
8 changes: 8 additions & 0 deletions tests/setup-env.js
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
import '@testing-library/jest-dom/extend-expect'

beforeEach(() => {
global.IS_REACT_ACT_ENVIRONMENT = true
})

afterEach(() => {
global.IS_REACT_ACT_ENVIRONMENT = false
})