Skip to content

Commit

Permalink
refactor: async utils (#537)
Browse files Browse the repository at this point in the history
* feat: use default values for interval and timeout in async utils

BREAKING CHANGE: `interval` will now default to 50ms in async utils

BREAKING CHANGE: `timeout` will now default to 1000ms in async utils

BREAKING CHANGE: `suppressErrors` has been removed from async utils

* refactor: move timeoutPromise into callAfter helper

* fix: react is not an optional peerDependency

* refactor: return boolean instead of throwing error to handle timeout
  • Loading branch information
mpeyper committed Jan 12, 2021
1 parent bab38d9 commit 394f65a
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 369 deletions.
42 changes: 18 additions & 24 deletions docs/api-reference.md
Expand Up @@ -201,25 +201,26 @@ removed, the provided callback will no longer execute as part of running
### `waitForNextUpdate`

```ts
function waitForNextUpdate(options?: { timeout?: number }): Promise<void>
function waitForNextUpdate(options?: { timeout?: number | false }): Promise<void>
```

Returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as
the result of an asynchronous update.

#### `timeout`

The maximum amount of time in milliseconds (ms) to wait. By default, no timeout is applied.
_Default: 1000_

The maximum amount of time in milliseconds (ms) to wait.

### `waitFor`

```ts
function waitFor(
callback: () => boolean | void,
options?: {
interval?: number
timeout?: number
suppressErrors?: boolean
interval?: number | false
timeout?: number | false
}
): Promise<void>
```
Expand All @@ -230,29 +231,25 @@ in the callback to perform assertion or to test values.

#### `interval`

_Default: 50_

The amount of time in milliseconds (ms) to wait between checks of the callback if no renders occur.
Interval checking is disabled if `interval` is not provided in the options or provided as a `falsy`
value. By default, it is disabled.
Interval checking is disabled if `interval` is not provided as a `falsy`.

#### `timeout`

The maximum amount of time in milliseconds (ms) to wait. By default, no timeout is applied.

#### `suppressErrors`
_Default: 1000_

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. By default, errors are suppressed for this utility.
The maximum amount of time in milliseconds (ms) to wait.

### `waitForValueToChange`

```ts
function waitForValueToChange(
selector: () => any,
options?: {
interval?: number
timeout?: number
suppressErrors?: boolean
interval?: number | false
timeout?: number | false
}
): Promise<void>
```
Expand All @@ -263,16 +260,13 @@ for comparison.

#### `interval`

_Default: 50_

The amount of time in milliseconds (ms) to wait between checks of the callback if no renders occur.
Interval checking is disabled if `interval` is not provided in the options or provided as a `falsy`
value. By default, it is disabled.
Interval checking is disabled if `interval` is not provided as a `falsy`.

#### `timeout`

The maximum amount of time in milliseconds (ms) to wait. By default, no timeout is applied.

#### `suppressErrors`
_Default: 1000_

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. By default, errors are not suppressed for this utility.
The maximum amount of time in milliseconds (ms) to wait.
3 changes: 0 additions & 3 deletions package.json
Expand Up @@ -70,9 +70,6 @@
"react-test-renderer": ">=16.9.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
Expand Down
145 changes: 77 additions & 68 deletions src/core/asyncUtils.ts
@@ -1,97 +1,106 @@
import { Act, WaitOptions, AsyncUtils } from '../types'
import {
Act,
WaitOptions,
WaitForOptions,
WaitForValueToChangeOptions,
WaitForNextUpdateOptions,
AsyncUtils
} from '../types'

import { resolveAfter } from '../helpers/promises'
import { resolveAfter, callAfter } from '../helpers/promises'
import { TimeoutError } from '../helpers/error'

const DEFAULT_INTERVAL = 50
const DEFAULT_TIMEOUT = 1000

function asyncUtils(act: Act, addResolver: (callback: () => void) => void): AsyncUtils {
let nextUpdatePromise: Promise<void> | null = null

const waitForNextUpdate = async ({ timeout }: Pick<WaitOptions, 'timeout'> = {}) => {
if (nextUpdatePromise) {
await nextUpdatePromise
} else {
nextUpdatePromise = new Promise((resolve, reject) => {
let timeoutId: ReturnType<typeof setTimeout>
if (timeout && timeout > 0) {
timeoutId = setTimeout(
() => reject(new TimeoutError(waitForNextUpdate, timeout)),
timeout
)
const wait = async (callback: () => boolean | void, { interval, timeout }: WaitOptions) => {
const checkResult = () => {
const callbackResult = callback()
return callbackResult ?? callbackResult === undefined
}

const waitForResult = async () => {
while (true) {
await Promise.race(
[
new Promise<void>((resolve) => addResolver(resolve)),
interval && resolveAfter(interval)
].filter(Boolean)
)

if (checkResult()) {
return
}
addResolver(() => {
clearTimeout(timeoutId)
nextUpdatePromise = null
resolve()
})
})
await act(() => nextUpdatePromise as Promise<void>)
}
}

let timedOut = false

if (!checkResult()) {
if (timeout) {
const timeoutPromise = () =>
callAfter(() => {
timedOut = true
}, timeout)

await act(() => Promise.race([waitForResult(), timeoutPromise()]))
} else {
await act(waitForResult)
}
}

return !timedOut
}

const waitFor = async (
callback: () => boolean | void,
{ interval, timeout, suppressErrors = true }: WaitOptions = {}
{ interval = DEFAULT_INTERVAL, timeout = DEFAULT_TIMEOUT }: WaitForOptions = {}
) => {
const checkResult = () => {
const safeCallback = () => {
try {
const callbackResult = callback()
return callbackResult ?? callbackResult === undefined
return callback()
} catch (error: unknown) {
if (!suppressErrors) {
throw error
}
return undefined
return false
}
}

const waitForResult = async () => {
const initialTimeout = timeout
while (true) {
const startTime = Date.now()
try {
const nextCheck = interval
? Promise.race([waitForNextUpdate({ timeout }), resolveAfter(interval)])
: waitForNextUpdate({ timeout })

await nextCheck

if (checkResult()) {
return
}
} catch (error: unknown) {
if (error instanceof TimeoutError && initialTimeout) {
throw new TimeoutError(waitFor, initialTimeout)
}
throw error
}
if (timeout) timeout -= Date.now() - startTime
}
const result = await wait(safeCallback, { interval, timeout })
if (!result && timeout) {
throw new TimeoutError(waitFor, timeout)
}
}

if (!checkResult()) {
await waitForResult()
const waitForValueToChange = async (
selector: () => unknown,
{ interval = DEFAULT_INTERVAL, timeout = DEFAULT_TIMEOUT }: WaitForValueToChangeOptions = {}
) => {
const initialValue = selector()

const result = await wait(() => selector() !== initialValue, { interval, timeout })
if (!result && timeout) {
throw new TimeoutError(waitForValueToChange, timeout)
}
}

const waitForValueToChange = async (selector: () => unknown, options: WaitOptions = {}) => {
const initialValue = selector()
try {
await waitFor(() => selector() !== initialValue, {
suppressErrors: false,
...options
})
} catch (error: unknown) {
if (error instanceof TimeoutError && options.timeout) {
throw new TimeoutError(waitForValueToChange, options.timeout)
}
throw error
const waitForNextUpdate = async ({
timeout = DEFAULT_TIMEOUT
}: WaitForNextUpdateOptions = {}) => {
let updated = false
addResolver(() => {
updated = true
})

const result = await wait(() => updated, { interval: false, timeout })
if (!result && timeout) {
throw new TimeoutError(waitForNextUpdate, timeout)
}
}

return {
waitFor,
waitForNextUpdate,
waitForValueToChange
waitForValueToChange,
waitForNextUpdate
}
}

Expand Down

0 comments on commit 394f65a

Please sign in to comment.