diff --git a/.all-contributorsrc b/.all-contributorsrc
index 5ea4dc36..51bccf3d 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -582,6 +582,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/10645051?v=4",
"profile": "https://github.com/chris110408",
"contributions": [
+ "code",
"test"
]
},
diff --git a/README.md b/README.md
index 88f0944b..7eaab8b3 100644
--- a/README.md
+++ b/README.md
@@ -247,7 +247,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
andyrooger 💻 |
Bryan Wain 🐛 👀 |
Robert Snow ⚠️ |
- Chris Chen ⚠️ |
+ Chris Chen 💻 ⚠️ |
Masious 📖 |
diff --git a/src/__tests__/asyncHook.fakeTimers.test.ts b/src/__tests__/asyncHook.fakeTimers.test.ts
deleted file mode 100644
index 98d6b2c9..00000000
--- a/src/__tests__/asyncHook.fakeTimers.test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-describe('async hook (fake timers) tests', () => {
- beforeEach(() => {
- jest.useFakeTimers()
- })
-
- afterEach(() => {
- jest.useRealTimers()
- })
-
- runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => {
- test('should wait for arbitrary expectation to pass when using advanceTimersByTime()', async () => {
- const { waitFor } = renderHook(() => null)
-
- let actual = 0
- const expected = 1
-
- setTimeout(() => {
- actual = expected
- }, 200)
-
- let complete = false
-
- jest.advanceTimersByTime(200)
-
- await waitFor(() => {
- expect(actual).toBe(expected)
- complete = true
- })
-
- expect(complete).toBe(true)
- })
-
- test('should wait for arbitrary expectation to pass when using runOnlyPendingTimers()', async () => {
- const { waitFor } = renderHook(() => null)
-
- let actual = 0
- const expected = 1
-
- setTimeout(() => {
- actual = expected
- }, 200)
-
- let complete = false
-
- jest.runOnlyPendingTimers()
-
- await waitFor(() => {
- expect(actual).toBe(expected)
- complete = true
- })
-
- expect(complete).toBe(true)
- })
- })
-})
-
-// eslint-disable-next-line jest/no-export
-export {}
diff --git a/src/__tests__/asyncHook.test.ts b/src/__tests__/asyncHook.test.ts
index 17979ae2..29869c08 100644
--- a/src/__tests__/asyncHook.test.ts
+++ b/src/__tests__/asyncHook.test.ts
@@ -21,238 +21,253 @@ describe('async hook tests', () => {
return value
}
+ describe.each([
+ { timerType: 'real timer', setTimer: () => jest.useRealTimers() },
+ { timerType: 'fake timer (legacy)', setTimer: () => jest.useFakeTimers('legacy') },
+ { timerType: 'fake timer (modern)', setTimer: () => jest.useFakeTimers('modern') }
+ ])('$timerType', ({ setTimer }) => {
+ beforeEach(() => {
+ setTimer()
+ })
- runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => {
- test('should wait for next update', async () => {
- const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second']))
+ afterEach(() => {
+ jest.useRealTimers()
+ })
- expect(result.current).toBe('first')
+ runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => {
+ test('should wait for next update', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second']))
- await waitForNextUpdate()
+ expect(result.current).toBe('first')
- expect(result.current).toBe('second')
- })
+ await waitForNextUpdate()
- test('should wait for multiple updates', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useSequence(['first', 'second', 'third'])
- )
+ expect(result.current).toBe('second')
+ })
- expect(result.current).toBe('first')
+ test('should wait for multiple updates', async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useSequence(['first', 'second', 'third'])
+ )
- await waitForNextUpdate()
+ expect(result.current).toBe('first')
- expect(result.current).toBe('second')
+ await waitForNextUpdate()
- await waitForNextUpdate()
+ expect(result.current).toBe('second')
- expect(result.current).toBe('third')
- })
+ await waitForNextUpdate()
- test('should reject if timeout exceeded when waiting for next update', async () => {
- const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second']))
+ expect(result.current).toBe('third')
+ })
- expect(result.current).toBe('first')
+ test('should reject if timeout exceeded when waiting for next update', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second']))
- await expect(waitForNextUpdate({ timeout: 10 })).rejects.toThrow(
- Error('Timed out in waitForNextUpdate after 10ms.')
- )
- })
+ expect(result.current).toBe('first')
- test('should not reject when waiting for next update if timeout has been disabled', async () => {
- const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second'], 1100))
+ await expect(waitForNextUpdate({ timeout: 10 })).rejects.toThrow(
+ Error('Timed out in waitForNextUpdate after 10ms.')
+ )
+ })
- expect(result.current).toBe('first')
+ test('should not reject when waiting for next update if timeout has been disabled', async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useSequence(['first', 'second'], 1100)
+ )
- await waitForNextUpdate({ timeout: false })
+ expect(result.current).toBe('first')
- expect(result.current).toBe('second')
- })
+ await waitForNextUpdate({ timeout: false })
- test('should wait for expectation to pass', async () => {
- const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))
+ expect(result.current).toBe('second')
+ })
- expect(result.current).toBe('first')
+ test('should wait for expectation to pass', async () => {
+ const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))
- let complete = false
- await waitFor(() => {
- expect(result.current).toBe('third')
- complete = true
+ expect(result.current).toBe('first')
+
+ let complete = false
+ await waitFor(() => {
+ expect(result.current).toBe('third')
+ complete = true
+ })
+ expect(complete).toBe(true)
})
- expect(complete).toBe(true)
- })
- test('should wait for arbitrary expectation to pass', async () => {
- const { waitFor } = renderHook(() => null)
+ test('should wait for arbitrary expectation to pass', async () => {
+ const { waitFor } = renderHook(() => null)
- let actual = 0
- const expected = 1
+ let actual = 0
+ const expected = 1
- setTimeout(() => {
- actual = expected
- }, 200)
+ setTimeout(() => {
+ actual = expected
+ }, 200)
- let complete = false
- await waitFor(() => {
- expect(actual).toBe(expected)
- complete = true
+ let complete = false
+ await waitFor(() => {
+ expect(actual).toBe(expected)
+ complete = true
+ })
+
+ expect(complete).toBe(true)
})
- expect(complete).toBe(true)
- })
+ test('should not hang if expectation is already passing', async () => {
+ const { result, waitFor } = renderHook(() => useSequence(['first', 'second']))
- test('should not hang if expectation is already passing', async () => {
- const { result, waitFor } = renderHook(() => useSequence(['first', 'second']))
+ expect(result.current).toBe('first')
- expect(result.current).toBe('first')
+ let complete = false
+ await waitFor(() => {
+ expect(result.current).toBe('first')
+ complete = true
+ })
+ expect(complete).toBe(true)
+ })
+
+ test('should wait for truthy value', async () => {
+ const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))
- let complete = false
- await waitFor(() => {
expect(result.current).toBe('first')
- complete = true
- })
- expect(complete).toBe(true)
- })
- test('should wait for truthy value', async () => {
- const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))
+ await waitFor(() => result.current === 'third')
- expect(result.current).toBe('first')
+ expect(result.current).toBe('third')
+ })
- await waitFor(() => result.current === 'third')
+ test('should wait for arbitrary truthy value', async () => {
+ const { waitFor } = renderHook(() => null)
- expect(result.current).toBe('third')
- })
+ let actual = 0
+ const expected = 1
+
+ setTimeout(() => {
+ actual = expected
+ }, 200)
- test('should wait for arbitrary truthy value', async () => {
- const { waitFor } = renderHook(() => null)
+ await waitFor(() => actual === 1)
- let actual = 0
- const expected = 1
+ expect(actual).toBe(expected)
+ })
- setTimeout(() => {
- actual = expected
- }, 200)
+ test('should reject if timeout exceeded when waiting for expectation to pass', async () => {
+ const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))
- await waitFor(() => actual === 1)
+ expect(result.current).toBe('first')
- expect(actual).toBe(expected)
- })
+ await expect(
+ waitFor(
+ () => {
+ expect(result.current).toBe('third')
+ },
+ { timeout: 75 }
+ )
+ ).rejects.toThrow(Error('Timed out in waitFor after 75ms.'))
+ })
- test('should reject if timeout exceeded when waiting for expectation to pass', async () => {
- const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))
+ test('should not reject when waiting for expectation to pass if timeout has been disabled', async () => {
+ const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'], 550))
- expect(result.current).toBe('first')
+ expect(result.current).toBe('first')
- await expect(
- waitFor(
+ await waitFor(
() => {
expect(result.current).toBe('third')
},
- { timeout: 75 }
+ { timeout: false }
)
- ).rejects.toThrow(Error('Timed out in waitFor after 75ms.'))
- })
- test('should not reject when waiting for expectation to pass if timeout has been disabled', async () => {
- const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'], 550))
+ expect(result.current).toBe('third')
+ })
- expect(result.current).toBe('first')
+ test('should check on interval when waiting for expectation to pass', async () => {
+ const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))
- await waitFor(
- () => {
- expect(result.current).toBe('third')
- },
- { timeout: false }
- )
+ let checks = 0
- expect(result.current).toBe('third')
- })
+ await waitFor(
+ () => {
+ checks++
+ return result.current === 'third'
+ },
+ { interval: 100 }
+ )
- test('should check on interval when waiting for expectation to pass', async () => {
- const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third']))
+ expect(checks).toBe(3)
+ })
- let checks = 0
+ test('should wait for value to change', async () => {
+ const { result, waitForValueToChange } = renderHook(() =>
+ useSequence(['first', 'second', 'third'])
+ )
- await waitFor(
- () => {
- checks++
- return result.current === 'third'
- },
- { interval: 100 }
- )
+ expect(result.current).toBe('first')
- expect(checks).toBe(3)
- })
+ await waitForValueToChange(() => result.current === 'third')
- test('should wait for value to change', async () => {
- const { result, waitForValueToChange } = renderHook(() =>
- useSequence(['first', 'second', 'third'])
- )
+ expect(result.current).toBe('third')
+ })
- expect(result.current).toBe('first')
+ test('should wait for arbitrary value to change', async () => {
+ const { waitForValueToChange } = renderHook(() => null)
- await waitForValueToChange(() => result.current === 'third')
+ let actual = 0
+ const expected = 1
- expect(result.current).toBe('third')
- })
+ setTimeout(() => {
+ actual = expected
+ }, 200)
- test('should wait for arbitrary value to change', async () => {
- const { waitForValueToChange } = renderHook(() => null)
+ await waitForValueToChange(() => actual)
- let actual = 0
- const expected = 1
+ expect(actual).toBe(expected)
+ })
- setTimeout(() => {
- actual = expected
- }, 200)
+ test('should reject if timeout exceeded when waiting for value to change', async () => {
+ const { result, waitForValueToChange } = renderHook(() =>
+ useSequence(['first', 'second', 'third'])
+ )
- await waitForValueToChange(() => actual)
+ expect(result.current).toBe('first')
- expect(actual).toBe(expected)
- })
+ await expect(
+ waitForValueToChange(() => result.current === 'third', {
+ timeout: 75
+ })
+ ).rejects.toThrow(Error('Timed out in waitForValueToChange after 75ms.'))
+ })
- test('should reject if timeout exceeded when waiting for value to change', async () => {
- const { result, waitForValueToChange } = renderHook(() =>
- useSequence(['first', 'second', 'third'])
- )
+ test('should not reject when waiting for value to change if timeout is disabled', async () => {
+ const { result, waitForValueToChange } = renderHook(() =>
+ useSequence(['first', 'second', 'third'], 550)
+ )
- expect(result.current).toBe('first')
+ expect(result.current).toBe('first')
- await expect(
- waitForValueToChange(() => result.current === 'third', {
- timeout: 75
+ await waitForValueToChange(() => result.current === 'third', {
+ timeout: false
})
- ).rejects.toThrow(Error('Timed out in waitForValueToChange after 75ms.'))
- })
-
- test('should not reject when waiting for value to change if timeout is disabled', async () => {
- const { result, waitForValueToChange } = renderHook(() =>
- useSequence(['first', 'second', 'third'], 550)
- )
-
- expect(result.current).toBe('first')
- await waitForValueToChange(() => result.current === 'third', {
- timeout: false
+ expect(result.current).toBe('third')
})
- expect(result.current).toBe('third')
- })
+ test('should reject if selector throws error', async () => {
+ const { result, waitForValueToChange } = renderHook(() => useSequence(['first', 'second']))
- test('should reject if selector throws error', async () => {
- const { result, waitForValueToChange } = renderHook(() => useSequence(['first', 'second']))
-
- expect(result.current).toBe('first')
+ expect(result.current).toBe('first')
- await expect(
- waitForValueToChange(() => {
- if (result.current === 'second') {
- throw new Error('Something Unexpected')
- }
- return result.current
- })
- ).rejects.toThrow(Error('Something Unexpected'))
+ await expect(
+ waitForValueToChange(() => {
+ if (result.current === 'second') {
+ throw new Error('Something Unexpected')
+ }
+ return result.current
+ })
+ ).rejects.toThrow(Error('Something Unexpected'))
+ })
})
})
})
diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts
index a7424036..1aa2854f 100644
--- a/src/core/asyncUtils.ts
+++ b/src/core/asyncUtils.ts
@@ -14,32 +14,35 @@ const DEFAULT_INTERVAL = 50
const DEFAULT_TIMEOUT = 1000
function asyncUtils(act: Act, addResolver: (callback: () => void) => void): AsyncUtils {
- const wait = async (callback: () => boolean | void, { interval, timeout }: WaitOptions) => {
+ const wait = async (
+ callback: () => boolean | void,
+ { interval, timeout }: Required
+ ) => {
const checkResult = () => {
const callbackResult = callback()
return callbackResult ?? callbackResult === undefined
}
- const timeoutSignal = createTimeoutController(timeout)
+ const timeoutController = createTimeoutController(timeout, { allowFakeTimers: true })
const waitForResult = async () => {
while (true) {
- const intervalSignal = createTimeoutController(interval)
- timeoutSignal.onTimeout(() => intervalSignal.cancel())
+ const intervalController = createTimeoutController(interval)
+ timeoutController.onTimeout(() => intervalController.cancel())
- await intervalSignal.wrap(new Promise(addResolver))
+ await intervalController.wrap(new Promise(addResolver))
- if (checkResult() || timeoutSignal.timedOut) {
+ if (checkResult() || timeoutController.timedOut) {
return
}
}
}
if (!checkResult()) {
- await act(() => timeoutSignal.wrap(waitForResult()))
+ await act(() => timeoutController.wrap(waitForResult()))
}
- return !timeoutSignal.timedOut
+ return !timeoutController.timedOut
}
const waitFor = async (
diff --git a/src/helpers/createTimeoutController.ts b/src/helpers/createTimeoutController.ts
index 643d3768..6a5bda2a 100644
--- a/src/helpers/createTimeoutController.ts
+++ b/src/helpers/createTimeoutController.ts
@@ -1,8 +1,9 @@
-import { WaitOptions } from '../types'
+import { fakeTimersAreEnabled, advanceTimers } from './fakeTimers'
-function createTimeoutController(timeout: WaitOptions['timeout']) {
+function createTimeoutController(timeout: number | false, { allowFakeTimers = false } = {}) {
let timeoutId: NodeJS.Timeout
const timeoutCallbacks: Array<() => void> = []
+ let finished = false
const timeoutController = {
onTimeout(callback: () => void) {
@@ -12,22 +13,30 @@ function createTimeoutController(timeout: WaitOptions['timeout']) {
return new Promise((resolve, reject) => {
timeoutController.timedOut = false
timeoutController.onTimeout(resolve)
-
if (timeout) {
timeoutId = setTimeout(() => {
+ finished = true
timeoutController.timedOut = true
timeoutCallbacks.forEach((callback) => callback())
resolve()
}, timeout)
}
+ if (fakeTimersAreEnabled() && allowFakeTimers) {
+ advanceTimers(() => finished)
+ }
+
promise
.then(resolve)
.catch(reject)
- .finally(() => timeoutController.cancel())
+ .finally(() => {
+ finished = true
+ timeoutController.cancel()
+ })
})
},
cancel() {
+ finished = true
clearTimeout(timeoutId)
},
timedOut: false
diff --git a/src/helpers/fakeTimers.ts b/src/helpers/fakeTimers.ts
new file mode 100644
index 00000000..60e60dd9
--- /dev/null
+++ b/src/helpers/fakeTimers.ts
@@ -0,0 +1,24 @@
+export const fakeTimersAreEnabled = () => {
+ /* istanbul ignore else */
+ if (typeof jest !== 'undefined' && jest !== null) {
+ return (
+ // legacy timers
+ jest.isMockFunction(setTimeout) ||
+ // modern timers
+ Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
+ )
+ }
+ // istanbul ignore next
+ return false
+}
+
+export function advanceTimers(checkComplete: () => boolean) {
+ const advanceTime = async () => {
+ if (!checkComplete()) {
+ jest.advanceTimersByTime(1)
+ await Promise.resolve()
+ await advanceTime()
+ }
+ }
+ return advanceTime()
+}