Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor async utils #537

Merged
merged 4 commits into from
Jan 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 18 additions & 24 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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