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() +}