From 732ec46da3b0bc5c83e618b06cf19ff7016f0982 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Sun, 19 Sep 2021 06:49:50 -0700 Subject: [PATCH] feat(async-utils): async utils advance fake timer automatically when waiting Fixes #631 * add jestFakeTimersAreEnabled and use it to detect faketimer in createTimeoutController (#688) * fix fakeTimer problem * add new fakeTimer test and revise the function * add advanceTime * revise the advanceTime * use jest.advanceTimersByTime * change timeout type * fix converage and revise type * test(fake-timers): add more tests to test suite for fake timers * fix the code after code review and clean up * fix the test timeout is false * clean up * fix coverage * add skip for pass checkers * add comment * test(async-utils): enable test to test CI fix * test(async-utils): combine fake timer tests with async tests * refactor(async-utils): Move DEFAULT_TIMEOUT out of timeout controller * refactor(async-utils): move fake timer advancement into seperate utility * refactor(async-utils): simplify fake timer advancement logic * docs: add chris110408 as a contributor for code * refactor(async-utils): only advance timers on a single timeoutController BREAKING CHANGE: tests that used to manually advance fake timers and use async utilities may now fail as timer would advance further Co-authored-by: Lei Chen Co-authored-by: Michael Peyper --- .all-contributorsrc | 1 + README.md | 2 +- src/__tests__/asyncHook.fakeTimers.test.ts | 58 ---- src/__tests__/asyncHook.test.ts | 333 +++++++++++---------- src/core/asyncUtils.ts | 19 +- src/helpers/createTimeoutController.ts | 17 +- src/helpers/fakeTimers.ts | 24 ++ 7 files changed, 224 insertions(+), 230 deletions(-) delete mode 100644 src/__tests__/asyncHook.fakeTimers.test.ts create mode 100644 src/helpers/fakeTimers.ts 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() +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy