From 8f315999c3e48670b9447188a7459f557c6ebc83 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 14 Oct 2019 10:21:11 +1100 Subject: [PATCH 1/4] Added timeout to waitForNextUpdate --- docs/api-reference.md | 41 +++++++++++++++++++++------- src/asyncUtils.js | 40 +++++++++++++++++++++++++++ src/index.js | 16 ++--------- test/asyncHook.test.js | 62 ++++++++++++++++++++++++------------------ 4 files changed, 109 insertions(+), 50 deletions(-) create mode 100644 src/asyncUtils.js diff --git a/docs/api-reference.md b/docs/api-reference.md index 337871e3..1b9a522f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -34,7 +34,7 @@ more hooks for testing. The `props` passed into the callback will be the `initialProps` provided in the `options` to `renderHook`, unless new props are provided by a subsequent `rerender` call. -### `options` +### `options` (Optional) An options object to modify the execution of the `callback` function. See the [`renderHook` Options](/reference/api#renderhook-options) section for more details. @@ -68,15 +68,6 @@ The `renderHook` method returns an object that has a following properties: The `current` value or the `result` will reflect whatever is returned from the `callback` passed to `renderHook`. Any thrown values will be reflected in the `error` value of the `result`. -### `waitForNextUpdate` - -```js -function waitForNextUpdate(): Promise -``` - -- `waitForNextUpdate` (`function`) - returns a `Promise` that resolves the next time the hook - renders, commonly when state is updated as the result of a asynchronous action - ### `rerender` ```js @@ -96,9 +87,39 @@ function unmount(): void A function to unmount the test component. This is commonly used to trigger cleanup effects for `useEffect` hooks. +### `...asyncUtils` + +Utilities to assist with testing asynchronous behaviour. See the +[Async Utils](/reference/api#async-utils) section for more details. + --- ## `act` This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act) that is exported by `react-test-renderer`. + +--- + +## Async Utilities + +### `waitForNextUpdate` + +```js +function waitForNextUpdate(options?: WaitOptions): Promise +``` + +`waitForNextUpdate` returns a `Promise` that resolves the next time the hook renders, commonly when +state is updated as the result of an asynchronous update. + +An options object is accepted as the first parameter to modify it's execution. See the +[`wait` Options](/reference/api#wait-options) section for more details. + +### `wait` Options + +The async utilities accepts the following options: + +#### `timeout` + +The amount of time in milliseconds (ms) to wait. By default, the `wait` utilities will wait +indefinitely. diff --git a/src/asyncUtils.js b/src/asyncUtils.js new file mode 100644 index 00000000..e341c956 --- /dev/null +++ b/src/asyncUtils.js @@ -0,0 +1,40 @@ +import { act } from 'react-test-renderer' + +function createTimeoutError(utilName, timeout) { + const timeoutError = new Error(`Timed out in ${utilName} after ${timeout}ms.`) + timeoutError.timeout = true + return timeoutError +} + +function asyncUtils(addResolver) { + let nextUpdatePromise = null + + const resolveOnNextUpdate = ({ timeout }) => (resolve, reject) => { + let timeoutId + if (timeout > 0) { + timeoutId = setTimeout( + () => reject(createTimeoutError('waitForNextUpdate', timeout)), + timeout + ) + } + addResolver(() => { + clearTimeout(timeoutId) + nextUpdatePromise = null + resolve() + }) + } + + const waitForNextUpdate = async (options = {}) => { + if (!nextUpdatePromise) { + nextUpdatePromise = new Promise(resolveOnNextUpdate(options)) + await act(() => nextUpdatePromise) + } + return await nextUpdatePromise + } + + return { + waitForNextUpdate + } +} + +export default asyncUtils diff --git a/src/index.js b/src/index.js index f4411f5b..7e9765b5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import React, { Suspense } from 'react' import { act, create } from 'react-test-renderer' +import asyncUtils from './asyncUtils' function TestHook({ callback, hookProps, onError, children }) { try { @@ -73,20 +74,8 @@ function renderHook(callback, { initialProps, wrapper } = {}) { }) const { unmount, update } = testRenderer - let waitingForNextUpdate = null - const resolveOnNextUpdate = (resolve) => { - addResolver((...args) => { - waitingForNextUpdate = null - resolve(...args) - }) - } - return { result, - waitForNextUpdate: () => { - waitingForNextUpdate = waitingForNextUpdate || act(() => new Promise(resolveOnNextUpdate)) - return waitingForNextUpdate - }, rerender: (newProps = hookProps.current) => { hookProps.current = newProps act(() => { @@ -97,7 +86,8 @@ function renderHook(callback, { initialProps, wrapper } = {}) { act(() => { unmount() }) - } + }, + ...asyncUtils(addResolver) } } diff --git a/test/asyncHook.test.js b/test/asyncHook.test.js index 0049e343..046f454a 100644 --- a/test/asyncHook.test.js +++ b/test/asyncHook.test.js @@ -1,58 +1,66 @@ -import { useState, useEffect } from 'react' +import { useState, useRef, useEffect } from 'react' import { renderHook } from 'src' describe('async hook tests', () => { - const getSomeName = () => Promise.resolve('Betty') - - const useName = (prefix) => { - const [name, setName] = useState('nobody') + const useSequence = (...values) => { + const [first, ...otherValues] = values + const [value, setValue] = useState(first) + const index = useRef(0) useEffect(() => { - getSomeName().then((theName) => { - setName(prefix ? `${prefix} ${theName}` : theName) - }) - }, [prefix]) - - return name + const interval = setInterval(() => { + setValue(otherValues[index.current]) + index.current++ + }, 50) + return () => { + clearInterval(interval) + } + }, [...values]) + + return value } test('should wait for next update', async () => { - const { result, waitForNextUpdate } = renderHook(() => useName()) + const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second')) - expect(result.current).toBe('nobody') + expect(result.current).toBe('first') await waitForNextUpdate() - expect(result.current).toBe('Betty') + expect(result.current).toBe('second') }) test('should wait for multiple updates', async () => { - const { result, waitForNextUpdate, rerender } = renderHook(({ prefix }) => useName(prefix), { - initialProps: { prefix: 'Mrs.' } - }) + const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second', 'third')) - expect(result.current).toBe('nobody') + expect(result.current).toBe('first') await waitForNextUpdate() - expect(result.current).toBe('Mrs. Betty') - - rerender({ prefix: 'Ms.' }) + expect(result.current).toBe('second') await waitForNextUpdate() - expect(result.current).toBe('Ms. Betty') + expect(result.current).toBe('third') }) test('should resolve all when updating', async () => { - const { result, waitForNextUpdate } = renderHook(({ prefix }) => useName(prefix), { - initialProps: { prefix: 'Mrs.' } - }) + const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second')) - expect(result.current).toBe('nobody') + expect(result.current).toBe('first') await Promise.all([waitForNextUpdate(), waitForNextUpdate(), waitForNextUpdate()]) - expect(result.current).toBe('Mrs. Betty') + expect(result.current).toBe('second') + }) + + 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.') + ) }) }) From 9aca6675b352f7ccf4aa7a4b79ac72827f339f74 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 14 Oct 2019 11:10:06 +1100 Subject: [PATCH 2/4] Added wait async util with documentation --- docs/api-reference.md | 26 ++++++++++++++++++++------ src/asyncUtils.js | 28 ++++++++++++++++++++++++++++ test/asyncHook.test.js | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 1b9a522f..1a9cb746 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -103,17 +103,32 @@ This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act) ## Async Utilities +### `wait` + +```js +function wait(callback: function(): boolean|void, options?: WaitOptions): Promise +``` + +Returns a `Promise` that resolves if the provided callback executes without exception and returns a +truthy or `undefined` value. The callback is tested after each render of the hook. + +It is safe to use the [`result` of `renderHook`](/reference/api#result) in the callback to perform +assertion or to test values. + +See the [`wait` Options](/reference/api#wait-options) section for more details on the optional +`options` parameter. + ### `waitForNextUpdate` ```js function waitForNextUpdate(options?: WaitOptions): Promise ``` -`waitForNextUpdate` returns a `Promise` that resolves the next time the hook renders, commonly when -state is updated as the result of an asynchronous update. +Returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as +the result of an asynchronous update. -An options object is accepted as the first parameter to modify it's execution. See the -[`wait` Options](/reference/api#wait-options) section for more details. +See the [`wait` Options](/reference/api#wait-options) section for more details on the optional +`options` parameter. ### `wait` Options @@ -121,5 +136,4 @@ The async utilities accepts the following options: #### `timeout` -The amount of time in milliseconds (ms) to wait. By default, the `wait` utilities will wait -indefinitely. +The maximum amount of time in milliseconds (ms) to wait. By default, no timeout is applied. diff --git a/src/asyncUtils.js b/src/asyncUtils.js index e341c956..e64bb275 100644 --- a/src/asyncUtils.js +++ b/src/asyncUtils.js @@ -1,5 +1,13 @@ import { act } from 'react-test-renderer' +function actForResult(callback) { + let value + act(() => { + value = callback() + }) + return value +} + function createTimeoutError(utilName, timeout) { const timeoutError = new Error(`Timed out in ${utilName} after ${timeout}ms.`) timeoutError.timeout = true @@ -32,7 +40,27 @@ function asyncUtils(addResolver) { return await nextUpdatePromise } + const wait = async (callback, options = {}) => { + const initialTimeout = options.timeout + while (true) { + const startTime = Date.now() + try { + await waitForNextUpdate(options) + const callbackResult = actForResult(callback) + if (callbackResult || callbackResult === undefined) { + break + } + } catch (e) { + if (e.timeout) { + throw createTimeoutError('wait', initialTimeout) + } + } + options.timeout -= Date.now() - startTime + } + } + return { + wait, waitForNextUpdate } } diff --git a/test/asyncHook.test.js b/test/asyncHook.test.js index 046f454a..88dbfb8b 100644 --- a/test/asyncHook.test.js +++ b/test/asyncHook.test.js @@ -63,4 +63,42 @@ describe('async hook tests', () => { Error('Timed out in waitForNextUpdate after 10ms.') ) }) + + test('should wait for expectation to pass', async () => { + const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + let complete = false + await wait(() => { + expect(result.current).toBe('third') + complete = true + }) + expect(complete).toBe(true) + }) + + test('should wait for truthy value', async () => { + const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + await wait(() => result.current === 'third') + + expect(result.current).toBe('third') + }) + + test('should reject if timeout exceeded when waiting for expectation to pass', async () => { + const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + await expect( + wait( + () => { + expect(result.current).toBe('third') + }, + { timeout: 75 } + ) + ).rejects.toThrow(Error('Timed out in wait after 75ms.')) + }) }) From 72880f87fd37639027c2f4eabe1ec74ef6762141 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 14 Oct 2019 11:16:41 +1100 Subject: [PATCH 3/4] Added waitForValueToChange async util with documentation --- docs/api-reference.md | 21 ++++++++++++++++++--- src/asyncUtils.js | 23 ++++++++++++++++++++++- test/asyncHook.test.js | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 1a9cb746..4e9e8612 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -110,10 +110,10 @@ function wait(callback: function(): boolean|void, options?: WaitOptions): Promis ``` Returns a `Promise` that resolves if the provided callback executes without exception and returns a -truthy or `undefined` value. The callback is tested after each render of the hook. +truthy or `undefined` value. It is safe to use the [`result` of `renderHook`](/reference/api#result) +in the callback to perform assertion or to test values. -It is safe to use the [`result` of `renderHook`](/reference/api#result) in the callback to perform -assertion or to test values. +The callback is tested after each render of the hook. See the [`wait` Options](/reference/api#wait-options) section for more details on the optional `options` parameter. @@ -130,6 +130,21 @@ the result of an asynchronous update. See the [`wait` Options](/reference/api#wait-options) section for more details on the optional `options` parameter. +### `waitForValueToChange` + +```js +function waitForValueToChange(selector: function(): any, options?: WaitOptions): Promise +``` + +Returns a `Promise` that resolves if the value returned from the provided selector changes. It +expected that the [`result` of `renderHook`](/reference/api#result) to select the value for +comparison. + +The value is selected for comparison after each render of the hook. + +See the [`wait` Options](/reference/api#wait-options) section for more details on the optional +`options` parameter. + ### `wait` Options The async utilities accepts the following options: diff --git a/src/asyncUtils.js b/src/asyncUtils.js index e64bb275..fb4ed2da 100644 --- a/src/asyncUtils.js +++ b/src/asyncUtils.js @@ -59,9 +59,30 @@ function asyncUtils(addResolver) { } } + const waitForValueToChange = async (selector, options = {}) => { + const initialTimeout = options.timeout + const initialValue = actForResult(selector) + while (true) { + const startTime = Date.now() + try { + await waitForNextUpdate(options) + if (actForResult(selector) !== initialValue) { + break + } + } catch (e) { + if (e.timeout) { + throw createTimeoutError('waitForValueToChange', initialTimeout) + } + throw e + } + options.timeout -= Date.now() - startTime + } + } + return { wait, - waitForNextUpdate + waitForNextUpdate, + waitForValueToChange } } diff --git a/test/asyncHook.test.js b/test/asyncHook.test.js index 88dbfb8b..7ece20f9 100644 --- a/test/asyncHook.test.js +++ b/test/asyncHook.test.js @@ -101,4 +101,45 @@ describe('async hook tests', () => { ) ).rejects.toThrow(Error('Timed out in wait after 75ms.')) }) + + 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 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 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')) + }) }) From c5099d9468207c11a6b1ec4e1bc5e64c988b8a53 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 15 Oct 2019 22:59:52 +1100 Subject: [PATCH 4/4] Refactored waitForValueToChange to use wait under the hook and updated docs --- docs/api-reference.md | 45 +++++++++------ docs/usage/advanced-hooks.md | 15 +++-- src/asyncUtils.js | 108 ++++++++++++++++++----------------- test/asyncHook.test.js | 77 ++++++++++++++++++++++++- 4 files changed, 167 insertions(+), 78 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 4e9e8612..88683d15 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -103,32 +103,33 @@ This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act) ## Async Utilities -### `wait` +### `waitForNextUpdate` ```js -function wait(callback: function(): boolean|void, options?: WaitOptions): Promise +function waitForNextUpdate(options?: WaitOptions): Promise ``` -Returns a `Promise` that resolves if the provided callback executes without exception and returns a -truthy or `undefined` value. It is safe to use the [`result` of `renderHook`](/reference/api#result) -in the callback to perform assertion or to test values. - -The callback is tested after each render of the hook. +Returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as +the result of an asynchronous update. -See the [`wait` Options](/reference/api#wait-options) section for more details on the optional -`options` parameter. +See the [`wait` Options](/reference/api#wait-options) section for more details on the available +`options`. -### `waitForNextUpdate` +### `wait` ```js -function waitForNextUpdate(options?: WaitOptions): Promise +function wait(callback: function(): boolean|void, options?: WaitOptions): Promise ``` -Returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as -the result of an asynchronous update. +Returns a `Promise` that resolves if the provided callback executes without exception and returns a +truthy or `undefined` value. It is safe to use the [`result` of `renderHook`](/reference/api#result) +in the callback to perform assertion or to test values. + +The callback is tested after each render of the hook. By default, errors raised from the callback +will be suppressed (`suppressErrors = true`). -See the [`wait` Options](/reference/api#wait-options) section for more details on the optional -`options` parameter. +See the [`wait` Options](/reference/api#wait-options) section for more details on the available +`options`. ### `waitForValueToChange` @@ -140,10 +141,11 @@ Returns a `Promise` that resolves if the value returned from the provided select expected that the [`result` of `renderHook`](/reference/api#result) to select the value for comparison. -The value is selected for comparison after each render of the hook. +The value is selected for comparison after each render of the hook. By default, errors raised from +selecting the value will not be suppressed (`suppressErrors = false`). -See the [`wait` Options](/reference/api#wait-options) section for more details on the optional -`options` parameter. +See the [`wait` Options](/reference/api#wait-options) section for more details on the available +`options`. ### `wait` Options @@ -152,3 +154,10 @@ The async utilities accepts the following options: #### `timeout` The maximum amount of time in milliseconds (ms) to wait. By default, no timeout is applied. + +#### `suppressErrors` + +If this option is set to `true`, any errors that occur while waiting are treated as a failed check. +If this option is set to `false`, any errors that occur while waiting cause the promise to be +rejected. Please refer to the [utility descriptions](/reference/api#async-utilities) for the default +values of this option (if applicable). diff --git a/docs/usage/advanced-hooks.md b/docs/usage/advanced-hooks.md index fd7abf1e..cb5bdceb 100644 --- a/docs/usage/advanced-hooks.md +++ b/docs/usage/advanced-hooks.md @@ -95,9 +95,9 @@ you, your team, and your project. ## Async Sometimes, a hook can trigger asynchronous updates that will not be immediately reflected in the -`result.current` value. Luckily, `renderHook` returns a utility that allows the test to wait for the -hook to update using `async/await` (or just promise callbacks if you prefer) called -`waitForNextUpdate`. +`result.current` value. Luckily, `renderHook` returns some utilities that allows the test to wait +for the hook to update using `async/await` (or just promise callbacks if you prefer). The most basic +async utility is called `waitForNextUpdate`. Let's further extend `useCounter` to have an `incrementAsync` callback that will update the `count` after `100ms`: @@ -132,11 +132,14 @@ test('should increment counter after delay', async () => { }) ``` +For more details on the the other async utilities, please refer to the +[API Reference](/reference/api#async-utilities). + ### Suspense -`waitForNextUpdate` will also wait for hooks that suspends using -[React's `Suspense`](https://reactjs.org/docs/code-splitting.html#suspense) functionality finish -rendering. +All the [async utilities](/reference/api#async-utilities) will also wait for hooks that suspends +using [React's `Suspense`](https://reactjs.org/docs/code-splitting.html#suspense) functionality to +complete rendering. ## Errors diff --git a/src/asyncUtils.js b/src/asyncUtils.js index fb4ed2da..7d7d6ed6 100644 --- a/src/asyncUtils.js +++ b/src/asyncUtils.js @@ -1,14 +1,6 @@ import { act } from 'react-test-renderer' -function actForResult(callback) { - let value - act(() => { - value = callback() - }) - return value -} - -function createTimeoutError(utilName, timeout) { +function createTimeoutError(utilName, { timeout }) { const timeoutError = new Error(`Timed out in ${utilName} after ${timeout}ms.`) timeoutError.timeout = true return timeoutError @@ -17,65 +9,77 @@ function createTimeoutError(utilName, timeout) { function asyncUtils(addResolver) { let nextUpdatePromise = null - const resolveOnNextUpdate = ({ timeout }) => (resolve, reject) => { - let timeoutId - if (timeout > 0) { - timeoutId = setTimeout( - () => reject(createTimeoutError('waitForNextUpdate', timeout)), - timeout - ) - } - addResolver(() => { - clearTimeout(timeoutId) - nextUpdatePromise = null - resolve() - }) - } - const waitForNextUpdate = async (options = {}) => { if (!nextUpdatePromise) { - nextUpdatePromise = new Promise(resolveOnNextUpdate(options)) + const resolveOnNextUpdate = (resolve, reject) => { + let timeoutId + if (options.timeout > 0) { + timeoutId = setTimeout( + () => reject(createTimeoutError('waitForNextUpdate', options)), + options.timeout + ) + } + addResolver(() => { + clearTimeout(timeoutId) + nextUpdatePromise = null + resolve() + }) + } + + nextUpdatePromise = new Promise(resolveOnNextUpdate) await act(() => nextUpdatePromise) } return await nextUpdatePromise } - const wait = async (callback, options = {}) => { - const initialTimeout = options.timeout - while (true) { - const startTime = Date.now() + const wait = async (callback, { timeout, suppressErrors = true } = {}) => { + const checkResult = () => { try { - await waitForNextUpdate(options) - const callbackResult = actForResult(callback) - if (callbackResult || callbackResult === undefined) { - break - } + const callbackResult = callback() + return callbackResult || callbackResult === undefined } catch (e) { - if (e.timeout) { - throw createTimeoutError('wait', initialTimeout) + if (!suppressErrors) { + throw e } } - options.timeout -= Date.now() - startTime + } + + const waitForResult = async () => { + const initialTimeout = timeout + while (true) { + const startTime = Date.now() + try { + await waitForNextUpdate({ timeout }) + if (checkResult()) { + return + } + } catch (e) { + if (e.timeout) { + throw createTimeoutError('wait', { timeout: initialTimeout }) + } + throw e + } + timeout -= Date.now() - startTime + } + } + + if (!checkResult()) { + await waitForResult() } } const waitForValueToChange = async (selector, options = {}) => { - const initialTimeout = options.timeout - const initialValue = actForResult(selector) - while (true) { - const startTime = Date.now() - try { - await waitForNextUpdate(options) - if (actForResult(selector) !== initialValue) { - break - } - } catch (e) { - if (e.timeout) { - throw createTimeoutError('waitForValueToChange', initialTimeout) - } - throw e + const initialValue = selector() + try { + await wait(() => selector() !== initialValue, { + suppressErrors: false, + ...options + }) + } catch (e) { + if (e.timeout) { + throw createTimeoutError('waitForValueToChange', options) } - options.timeout -= Date.now() - startTime + throw e } } diff --git a/test/asyncHook.test.js b/test/asyncHook.test.js index 7ece20f9..c4f67eba 100644 --- a/test/asyncHook.test.js +++ b/test/asyncHook.test.js @@ -9,8 +9,10 @@ describe('async hook tests', () => { useEffect(() => { const interval = setInterval(() => { - setValue(otherValues[index.current]) - index.current++ + setValue(otherValues[index.current++]) + if (index.current === otherValues.length) { + clearInterval(interval) + } }, 50) return () => { clearInterval(interval) @@ -77,6 +79,57 @@ describe('async hook tests', () => { expect(complete).toBe(true) }) + test('should not hang if expectation is already passing', async () => { + const { result, wait } = renderHook(() => useSequence('first', 'second')) + + expect(result.current).toBe('first') + + let complete = false + await wait(() => { + expect(result.current).toBe('first') + complete = true + }) + expect(complete).toBe(true) + }) + + test('should reject if callback throws error', async () => { + const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + await expect( + wait( + () => { + console.log(result.current) + if (result.current === 'second') { + throw new Error('Something Unexpected') + } + return result.current === 'third' + }, + { + suppressErrors: false + } + ) + ).rejects.toThrow(Error('Something Unexpected')) + }) + + test('should reject if callback immediately throws error', async () => { + const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + await expect( + wait( + () => { + throw new Error('Something Unexpected') + }, + { + suppressErrors: false + } + ) + ).rejects.toThrow(Error('Something Unexpected')) + }) + test('should wait for truthy value', async () => { const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) @@ -142,4 +195,24 @@ describe('async hook tests', () => { }) ).rejects.toThrow(Error('Something Unexpected')) }) + + test('should not reject if selector throws error and suppress errors option is enabled', async () => { + const { result, waitForValueToChange } = renderHook(() => + useSequence('first', 'second', 'third') + ) + + expect(result.current).toBe('first') + + await waitForValueToChange( + () => { + if (result.current === 'second') { + throw new Error('Something Unexpected') + } + return result.current === 'third' + }, + { suppressErrors: true } + ) + + expect(result.current).toBe('third') + }) })