From 79b2245c51c9231261f487b4a3bc2cb25ea4a62b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Sep 2021 10:28:54 +0100 Subject: [PATCH 1/4] chore(deps-dev): bump @typescript-eslint/parser from 4.30.0 to 4.31.0 (#689) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.30.0 to 4.31.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.31.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a854f40..7c26c013 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ }, "devDependencies": { "@typescript-eslint/eslint-plugin": "4.30.0", - "@typescript-eslint/parser": "4.30.0", + "@typescript-eslint/parser": "4.31.0", "all-contributors-cli": "6.20.0", "codecov": "3.8.3", "cross-env": "^7.0.3", From 0df0c4ca3d175f2abe93eafed552b0ca29a6f618 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Sep 2021 11:02:49 +0100 Subject: [PATCH 2/4] chore(deps-dev): bump @typescript-eslint/eslint-plugin (#690) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.30.0 to 4.31.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.31.0/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7c26c013..eeef84a7 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "react-error-boundary": "^3.1.0" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "4.30.0", + "@typescript-eslint/eslint-plugin": "4.31.0", "@typescript-eslint/parser": "4.31.0", "all-contributors-cli": "6.20.0", "codecov": "3.8.3", From 437f684ce74fadedde14325418eb52809def32ab Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Thu, 9 Sep 2021 20:28:22 +1000 Subject: [PATCH 3/4] test(fake-timers): add more tests to test suite for fake timers --- src/__tests__/asyncHook.fakeTimers.test.ts | 269 ++++++++++++++++++++- 1 file changed, 261 insertions(+), 8 deletions(-) diff --git a/src/__tests__/asyncHook.fakeTimers.test.ts b/src/__tests__/asyncHook.fakeTimers.test.ts index 69bff39d..bf79ed27 100644 --- a/src/__tests__/asyncHook.fakeTimers.test.ts +++ b/src/__tests__/asyncHook.fakeTimers.test.ts @@ -1,4 +1,27 @@ +import { useState, useRef, useEffect } from 'react' + describe('async hook (fake timers) tests', () => { + const useSequence = (values: string[], intervalMs = 50) => { + const [first, ...otherValues] = values + const [value, setValue] = useState(() => first) + const index = useRef(0) + + useEffect(() => { + const interval = setInterval(() => { + setValue(otherValues[index.current++]) + if (index.current >= otherValues.length) { + clearInterval(interval) + } + }, intervalMs) + return () => { + clearInterval(interval) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, otherValues) + + return value + } + beforeEach(() => { jest.useFakeTimers() }) @@ -54,9 +77,9 @@ describe('async hook (fake timers) tests', () => { test('should waitFor arbitrary expectation to pass when fake timers are not advanced explicitly', async () => { const fn = jest.fn().mockReturnValueOnce(false).mockReturnValueOnce(true) - + const { waitFor } = renderHook(() => null) - + await waitFor(() => { expect(fn()).toBe(true) }) @@ -70,7 +93,7 @@ describe('async hook (fake timers) tests', () => { setTimeout(() => { actual = expected - }, 101) + }, 30) let complete = false @@ -80,14 +103,244 @@ describe('async hook (fake timers) tests', () => { expect(actual).toBe(expected) complete = true }, - { timeout: 100, interval: 50 } + { timeout: 29, interval: 10 } ) - ).rejects.toThrow(Error('Timed out in waitFor after 100ms.')) + ).rejects.toThrow(Error('Timed out in waitFor after 29ms.')) expect(complete).toBe(false) }) + + test('should wait for next update', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second'])) + + expect(result.current).toBe('first') + + await waitForNextUpdate() + + expect(result.current).toBe('second') + }) + + test('should wait for multiple updates', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSequence(['first', 'second', 'third']) + ) + + expect(result.current).toBe('first') + + await waitForNextUpdate() + + expect(result.current).toBe('second') + + await waitForNextUpdate() + + expect(result.current).toBe('third') + }) + + test('should reject if timeout exceeded when waiting for next update', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second'])) + + expect(result.current).toBe('first') + + await expect(waitForNextUpdate({ timeout: 10 })).rejects.toThrow( + Error('Timed out in waitForNextUpdate after 10ms.') + ) + }) + + // eslint-disable-next-line jest/no-disabled-tests + test.skip('should not reject when waiting for next update if timeout has been disabled', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second'], 1100)) + + expect(result.current).toBe('first') + + await waitForNextUpdate({ timeout: false }) + + expect(result.current).toBe('second') + }) + + test('should wait for expectation to pass', async () => { + const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'])) + + expect(result.current).toBe('first') + + let complete = false + await waitFor(() => { + expect(result.current).toBe('third') + complete = true + }) + expect(complete).toBe(true) + }) + + test('should wait for arbitrary expectation to pass', async () => { + const { waitFor } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + let complete = false + await waitFor(() => { + expect(actual).toBe(expected) + complete = true + }) + + expect(complete).toBe(true) + }) + + test('should not hang if expectation is already passing', async () => { + const { result, waitFor } = renderHook(() => useSequence(['first', 'second'])) + + 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'])) + + expect(result.current).toBe('first') + + await waitFor(() => result.current === 'third') + + expect(result.current).toBe('third') + }) + + test('should wait for arbitrary truthy value', async () => { + const { waitFor } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + await waitFor(() => actual === 1) + + expect(actual).toBe(expected) + }) + + test('should reject if timeout exceeded when waiting for expectation to pass', async () => { + const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'])) + + expect(result.current).toBe('first') + + await expect( + waitFor( + () => { + expect(result.current).toBe('third') + }, + { timeout: 75 } + ) + ).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('first') + + await waitFor( + () => { + expect(result.current).toBe('third') + }, + { timeout: false } + ) + + expect(result.current).toBe('third') + }) + + test('should check on interval when waiting for expectation to pass', async () => { + const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'])) + + let checks = 0 + + await waitFor( + () => { + checks++ + return result.current === 'third' + }, + { interval: 100 } + ) + + expect(checks).toBe(3) + }) + + test('should wait for value to change', async () => { + const { result, waitForValueToChange } = renderHook(() => + useSequence(['first', 'second', 'third']) + ) + + expect(result.current).toBe('first') + + await waitForValueToChange(() => result.current === 'third') + + expect(result.current).toBe('third') + }) + + test('should wait for arbitrary value to change', async () => { + const { waitForValueToChange } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + await waitForValueToChange(() => actual) + + expect(actual).toBe(expected) + }) + + test('should reject if timeout exceeded when waiting for value to change', async () => { + const { result, waitForValueToChange } = renderHook(() => + useSequence(['first', 'second', 'third']) + ) + + expect(result.current).toBe('first') + + await expect( + waitForValueToChange(() => result.current === 'third', { + timeout: 75 + }) + ).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') + }) + + test('should reject if selector throws error', async () => { + const { result, waitForValueToChange } = renderHook(() => useSequence(['first', 'second'])) + + 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')) + }) }) }) - -// eslint-disable-next-line jest/no-export -export {} From f69525bf5391b210017c1c21bd5a3a40372f7175 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Thu, 9 Sep 2021 21:41:50 +1000 Subject: [PATCH 4/4] fix(fake-timers): advance fake timers in ms steps Additional refactoring to remove casting and improve readability --- src/__tests__/asyncHook.fakeTimers.test.ts | 7 +++-- src/core/asyncUtils.ts | 21 ++++++++------- src/helpers/createTimeoutController.ts | 24 +++++++++++------ src/helpers/fakeTimers.ts | 31 ++++++++++++++++++++++ src/helpers/jestFakeTimersAreEnabled.ts | 13 --------- 5 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 src/helpers/fakeTimers.ts delete mode 100644 src/helpers/jestFakeTimersAreEnabled.ts diff --git a/src/__tests__/asyncHook.fakeTimers.test.ts b/src/__tests__/asyncHook.fakeTimers.test.ts index bf79ed27..9c1c4559 100644 --- a/src/__tests__/asyncHook.fakeTimers.test.ts +++ b/src/__tests__/asyncHook.fakeTimers.test.ts @@ -77,9 +77,9 @@ describe('async hook (fake timers) tests', () => { test('should waitFor arbitrary expectation to pass when fake timers are not advanced explicitly', async () => { const fn = jest.fn().mockReturnValueOnce(false).mockReturnValueOnce(true) - + const { waitFor } = renderHook(() => null) - + await waitFor(() => { expect(fn()).toBe(true) }) @@ -146,8 +146,7 @@ describe('async hook (fake timers) tests', () => { ) }) - // eslint-disable-next-line jest/no-disabled-tests - test.skip('should not reject when waiting for next update if timeout has been disabled', async () => { + test('should not reject when waiting for next update if timeout has been disabled', async () => { const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second'], 1100)) expect(result.current).toBe('first') diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index a62e5940..d0d4b12a 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -10,36 +10,39 @@ import { import { createTimeoutController } from '../helpers/createTimeoutController' import { TimeoutError } from '../helpers/error' -const DEFAULT_TIMEOUT = 1000 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 as number | boolean, false) + const timeoutController = createTimeoutController(timeout, { allowFakeTimers: !interval }) const waitForResult = async () => { while (true) { - const intervalSignal = createTimeoutController(interval as number | boolean, true) - timeoutSignal.onTimeout(() => intervalSignal.cancel()) + const intervalController = createTimeoutController(interval, { allowFakeTimers: true }) + 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 033d052f..3c965a58 100644 --- a/src/helpers/createTimeoutController.ts +++ b/src/helpers/createTimeoutController.ts @@ -1,6 +1,9 @@ -import { jestFakeTimersAreEnabled } from './jestFakeTimersAreEnabled' +import { fakeTimersAreEnabled, advanceTimers } from './fakeTimers' -function createTimeoutController(timeout: number | boolean, allowFakeTimers: boolean) { +function createTimeoutController( + timeout: number | false, + { allowFakeTimers }: { allowFakeTimers: boolean } +) { let timeoutId: NodeJS.Timeout const timeoutCallbacks: Array<() => void> = [] @@ -18,17 +21,22 @@ function createTimeoutController(timeout: number | boolean, allowFakeTimers: boo timeoutController.timedOut = true timeoutCallbacks.forEach((callback) => callback()) resolve() - }, timeout as number) - - if (jestFakeTimersAreEnabled() && allowFakeTimers) { - jest.advanceTimersByTime(timeout as number) - } + }, timeout) } + let finished = false + promise .then(resolve) .catch(reject) - .finally(() => timeoutController.cancel()) + .finally(() => { + finished = true + timeoutController.cancel() + }) + + if (allowFakeTimers && fakeTimersAreEnabled()) { + advanceTimers(timeout, () => finished) + } }) }, cancel() { diff --git a/src/helpers/fakeTimers.ts b/src/helpers/fakeTimers.ts new file mode 100644 index 00000000..ee292155 --- /dev/null +++ b/src/helpers/fakeTimers.ts @@ -0,0 +1,31 @@ +export function 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(timeout: number | false, checkContinue: () => boolean) { + const advanceTime = async (currentMs: number) => { + if (!timeout || currentMs < timeout) { + jest.advanceTimersByTime(1) + + await Promise.resolve() + + if (checkContinue()) { + return + } + + await advanceTime(currentMs + 1) + } + } + + advanceTime(0) +} diff --git a/src/helpers/jestFakeTimersAreEnabled.ts b/src/helpers/jestFakeTimersAreEnabled.ts deleted file mode 100644 index 5a0e2a88..00000000 --- a/src/helpers/jestFakeTimersAreEnabled.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const jestFakeTimersAreEnabled = () => { - /* 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 -}