diff --git a/async.d.ts b/async.d.ts
new file mode 100644
index 00000000..1c8f6ead
--- /dev/null
+++ b/async.d.ts
@@ -0,0 +1 @@
+export * from './types/pure-async'
diff --git a/async.js b/async.js
new file mode 100644
index 00000000..e791260d
--- /dev/null
+++ b/async.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react/async'
+module.exports = require('./dist/async')
diff --git a/package.json b/package.json
index 8bfbeecc..3a30a6f4 100644
--- a/package.json
+++ b/package.json
@@ -25,9 +25,13 @@
},
"files": [
"dist",
+ "async.js",
+ "async.d.ts",
"dont-cleanup-after-each.js",
"pure.js",
"pure.d.ts",
+ "pure-async.js",
+ "pure-async.d.ts",
"types/*.d.ts"
],
"keywords": [
diff --git a/pure-async.d.ts b/pure-async.d.ts
new file mode 100644
index 00000000..1c8f6ead
--- /dev/null
+++ b/pure-async.d.ts
@@ -0,0 +1 @@
+export * from './types/pure-async'
diff --git a/pure-async.js b/pure-async.js
new file mode 100644
index 00000000..856726a1
--- /dev/null
+++ b/pure-async.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react/pure-async'
+module.exports = require('./dist/pure-async')
diff --git a/src/__tests__/async.js b/src/__tests__/async.js
new file mode 100644
index 00000000..f6c13426
--- /dev/null
+++ b/src/__tests__/async.js
@@ -0,0 +1,73 @@
+// TODO: Upstream that the rule should check import source
+/* eslint-disable testing-library/no-await-sync-events */
+import * as React from 'react'
+import {act, render, fireEvent} from '../async'
+
+const isReact19 = React.version.startsWith('19.')
+
+const testGateReact19 = isReact19 ? test : test.skip
+
+testGateReact19('async data requires async APIs', async () => {
+ let resolve
+ const promise = new Promise(_resolve => {
+ resolve = _resolve
+ })
+
+ function Component() {
+ const value = React.use(promise)
+ return
{value}
+ }
+
+ const {container} = await render(
+
+
+ ,
+ )
+
+ expect(container).toHaveTextContent('loading...')
+
+ await act(async () => {
+ resolve('Hello, Dave!')
+ })
+
+ expect(container).toHaveTextContent('Hello, Dave!')
+})
+
+testGateReact19('async fireEvent', async () => {
+ let resolve
+ function Component() {
+ const [promise, setPromise] = React.useState('initial')
+ const value = typeof promise === 'string' ? promise : React.use(promise)
+ return (
+
+ )
+ }
+
+ const {container} = await render(
+
+
+ ,
+ )
+
+ expect(container).toHaveTextContent('Value: initial')
+
+ await fireEvent.click(container.querySelector('button'))
+
+ expect(container).toHaveTextContent('loading...')
+
+ await act(() => {
+ resolve('Hello, Dave!')
+ })
+
+ expect(container).toHaveTextContent('Hello, Dave!')
+})
diff --git a/src/async.js b/src/async.js
new file mode 100644
index 00000000..cffcbfea
--- /dev/null
+++ b/src/async.js
@@ -0,0 +1,42 @@
+/* istanbul ignore file */
+import {getIsReactActEnvironment, setReactActEnvironment} from './act-compat'
+import {cleanup} from './pure-async'
+
+// if we're running in a test runner that supports afterEach
+// or teardown then we'll automatically run cleanup afterEach test
+// this ensures that tests run in isolation from each other
+// if you don't like this then either import the `pure` module
+// or set the RTL_SKIP_AUTO_CLEANUP env variable to 'true'.
+if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) {
+ // ignore teardown() in code coverage because Jest does not support it
+ /* istanbul ignore else */
+ if (typeof afterEach === 'function') {
+ afterEach(async () => {
+ await cleanup()
+ })
+ } else if (typeof teardown === 'function') {
+ // Block is guarded by `typeof` check.
+ // eslint does not support `typeof` guards.
+ // eslint-disable-next-line no-undef
+ teardown(async () => {
+ await cleanup()
+ })
+ }
+
+ // No test setup with other test runners available
+ /* istanbul ignore else */
+ if (typeof beforeAll === 'function' && typeof afterAll === 'function') {
+ // This matches the behavior of React < 18.
+ let previousIsReactActEnvironment = getIsReactActEnvironment()
+ beforeAll(() => {
+ previousIsReactActEnvironment = getIsReactActEnvironment()
+ setReactActEnvironment(true)
+ })
+
+ afterAll(() => {
+ setReactActEnvironment(previousIsReactActEnvironment)
+ })
+ }
+}
+
+export * from './pure-async'
diff --git a/src/fire-event-async.js b/src/fire-event-async.js
new file mode 100644
index 00000000..09c7719d
--- /dev/null
+++ b/src/fire-event-async.js
@@ -0,0 +1,70 @@
+/* istanbul ignore file */
+import {fireEvent as dtlFireEvent} from '@testing-library/dom'
+
+// react-testing-library's version of fireEvent will call
+// dom-testing-library's version of fireEvent. The reason
+// we make this distinction however is because we have
+// a few extra events that work a bit differently
+const fireEvent = (...args) => dtlFireEvent(...args)
+
+Object.keys(dtlFireEvent).forEach(key => {
+ fireEvent[key] = (...args) => dtlFireEvent[key](...args)
+})
+
+// React event system tracks native mouseOver/mouseOut events for
+// running onMouseEnter/onMouseLeave handlers
+// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31
+const mouseEnter = fireEvent.mouseEnter
+const mouseLeave = fireEvent.mouseLeave
+fireEvent.mouseEnter = async (...args) => {
+ await mouseEnter(...args)
+ return fireEvent.mouseOver(...args)
+}
+fireEvent.mouseLeave = async (...args) => {
+ await mouseLeave(...args)
+ return fireEvent.mouseOut(...args)
+}
+
+const pointerEnter = fireEvent.pointerEnter
+const pointerLeave = fireEvent.pointerLeave
+fireEvent.pointerEnter = async (...args) => {
+ await pointerEnter(...args)
+ return fireEvent.pointerOver(...args)
+}
+fireEvent.pointerLeave = async (...args) => {
+ await pointerLeave(...args)
+ return fireEvent.pointerOut(...args)
+}
+
+const select = fireEvent.select
+fireEvent.select = async (node, init) => {
+ await select(node, init)
+ // React tracks this event only on focused inputs
+ node.focus()
+
+ // React creates this event when one of the following native events happens
+ // - contextMenu
+ // - mouseUp
+ // - dragEnd
+ // - keyUp
+ // - keyDown
+ // so we can use any here
+ // @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224
+ await fireEvent.keyUp(node, init)
+}
+
+// React event system tracks native focusout/focusin events for
+// running blur/focus handlers
+// @link https://github.com/facebook/react/pull/19186
+const blur = fireEvent.blur
+const focus = fireEvent.focus
+fireEvent.blur = async (...args) => {
+ await fireEvent.focusOut(...args)
+ return blur(...args)
+}
+fireEvent.focus = async (...args) => {
+ await fireEvent.focusIn(...args)
+ return focus(...args)
+}
+
+export {fireEvent}
diff --git a/src/pure-async.js b/src/pure-async.js
new file mode 100644
index 00000000..21ffd97f
--- /dev/null
+++ b/src/pure-async.js
@@ -0,0 +1,330 @@
+/* istanbul ignore file */
+import * as React from 'react'
+import ReactDOM from 'react-dom'
+import * as ReactDOMClient from 'react-dom/client'
+import {
+ getQueriesForElement,
+ prettyDOM,
+ configure as configureDTL,
+} from '@testing-library/dom'
+import {getIsReactActEnvironment, setReactActEnvironment} from './act-compat'
+import {fireEvent} from './fire-event'
+import {getConfig, configure} from './config'
+
+async function act(scope) {
+ const previousActEnvironment = getIsReactActEnvironment()
+ setReactActEnvironment(true)
+ try {
+ // React.act isn't async yet so we need to force it.
+ return await React.act(async () => {
+ scope()
+ })
+ } finally {
+ setReactActEnvironment(previousActEnvironment)
+ }
+}
+
+function jestFakeTimersAreEnabled() {
+ /* istanbul ignore else */
+ if (typeof jest !== 'undefined' && jest !== null) {
+ return (
+ // legacy timers
+ setTimeout._isMockFunction === true || // modern timers
+ // eslint-disable-next-line prefer-object-has-own -- No Object.hasOwn in all target environments we support.
+ Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
+ )
+ } // istanbul ignore next
+
+ return false
+}
+
+configureDTL({
+ unstable_advanceTimersWrapper: cb => {
+ return act(cb)
+ },
+ // We just want to run `waitFor` without IS_REACT_ACT_ENVIRONMENT
+ // But that's not necessarily how `asyncWrapper` is used since it's a public method.
+ // Let's just hope nobody else is using it.
+ asyncWrapper: async cb => {
+ const previousActEnvironment = getIsReactActEnvironment()
+ setReactActEnvironment(false)
+ try {
+ const result = await cb()
+ // Drain microtask queue.
+ // Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call.
+ // The caller would have no chance to wrap the in-flight Promises in `act()`
+ await new Promise(resolve => {
+ setTimeout(() => {
+ resolve()
+ }, 0)
+
+ if (jestFakeTimersAreEnabled()) {
+ jest.advanceTimersByTime(0)
+ }
+ })
+
+ return result
+ } finally {
+ setReactActEnvironment(previousActEnvironment)
+ }
+ },
+ eventWrapper: async cb => {
+ let result
+ await act(() => {
+ result = cb()
+ })
+ return result
+ },
+})
+
+// Ideally we'd just use a WeakMap where containers are keys and roots are values.
+// We use two variables so that we can bail out in constant time when we render with a new container (most common use case)
+/**
+ * @type {Set}
+ */
+const mountedContainers = new Set()
+/**
+ * @type Array<{container: import('react-dom').Container, root: ReturnType}>
+ */
+const mountedRootEntries = []
+
+function strictModeIfNeeded(innerElement) {
+ return getConfig().reactStrictMode
+ ? React.createElement(React.StrictMode, null, innerElement)
+ : innerElement
+}
+
+function wrapUiIfNeeded(innerElement, wrapperComponent) {
+ return wrapperComponent
+ ? React.createElement(wrapperComponent, null, innerElement)
+ : innerElement
+}
+
+async function createConcurrentRoot(
+ container,
+ {hydrate, ui, wrapper: WrapperComponent},
+) {
+ let root
+ if (hydrate) {
+ await act(() => {
+ root = ReactDOMClient.hydrateRoot(
+ container,
+ strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
+ )
+ })
+ } else {
+ root = ReactDOMClient.createRoot(container)
+ }
+
+ return {
+ hydrate() {
+ /* istanbul ignore if */
+ if (!hydrate) {
+ throw new Error(
+ 'Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.',
+ )
+ }
+ // Nothing to do since hydration happens when creating the root object.
+ },
+ render(element) {
+ root.render(element)
+ },
+ unmount() {
+ root.unmount()
+ },
+ }
+}
+
+function createLegacyRoot(container) {
+ return {
+ hydrate(element) {
+ ReactDOM.hydrate(element, container)
+ },
+ render(element) {
+ ReactDOM.render(element, container)
+ },
+ unmount() {
+ ReactDOM.unmountComponentAtNode(container)
+ },
+ }
+}
+
+async function renderRootAsync(
+ ui,
+ {baseElement, container, hydrate, queries, root, wrapper: WrapperComponent},
+) {
+ await act(() => {
+ if (hydrate) {
+ root.hydrate(
+ strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
+ container,
+ )
+ } else {
+ root.render(
+ strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
+ container,
+ )
+ }
+ })
+
+ return {
+ container,
+ baseElement,
+ debug: (el = baseElement, maxLength, options) =>
+ Array.isArray(el)
+ ? // eslint-disable-next-line no-console
+ el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
+ : // eslint-disable-next-line no-console,
+ console.log(prettyDOM(el, maxLength, options)),
+ unmount: async () => {
+ await act(() => {
+ root.unmount()
+ })
+ },
+ rerender: async rerenderUi => {
+ await renderRootAsync(rerenderUi, {
+ container,
+ baseElement,
+ root,
+ wrapper: WrapperComponent,
+ })
+ // Intentionally do not return anything to avoid unnecessarily complicating the API.
+ // folks can use all the same utilities we return in the first place that are bound to the container
+ },
+ asFragment: () => {
+ /* istanbul ignore else (old jsdom limitation) */
+ if (typeof document.createRange === 'function') {
+ return document
+ .createRange()
+ .createContextualFragment(container.innerHTML)
+ } else {
+ const template = document.createElement('template')
+ template.innerHTML = container.innerHTML
+ return template.content
+ }
+ },
+ ...getQueriesForElement(baseElement, queries),
+ }
+}
+
+async function render(
+ ui,
+ {
+ container,
+ baseElement = container,
+ legacyRoot = false,
+ queries,
+ hydrate = false,
+ wrapper,
+ } = {},
+) {
+ if (legacyRoot && typeof ReactDOM.render !== 'function') {
+ const error = new Error(
+ '`legacyRoot: true` is not supported in this version of React. ' +
+ 'If your app runs React 19 or later, you should remove this flag. ' +
+ 'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.',
+ )
+ Error.captureStackTrace(error, render)
+ throw error
+ }
+
+ if (!baseElement) {
+ // default to document.body instead of documentElement to avoid output of potentially-large
+ // head elements (such as JSS style blocks) in debug output
+ baseElement = document.body
+ }
+ if (!container) {
+ container = baseElement.appendChild(document.createElement('div'))
+ }
+
+ let root
+ // eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first.
+ if (!mountedContainers.has(container)) {
+ const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
+ root = await createRootImpl(container, {hydrate, ui, wrapper})
+
+ mountedRootEntries.push({container, root})
+ // we'll add it to the mounted containers regardless of whether it's actually
+ // added to document.body so the cleanup method works regardless of whether
+ // they're passing us a custom container or not.
+ mountedContainers.add(container)
+ } else {
+ mountedRootEntries.forEach(rootEntry => {
+ // Else is unreachable since `mountedContainers` has the `container`.
+ // Only reachable if one would accidentally add the container to `mountedContainers` but not the root to `mountedRootEntries`
+ /* istanbul ignore else */
+ if (rootEntry.container === container) {
+ root = rootEntry.root
+ }
+ })
+ }
+
+ return renderRootAsync(ui, {
+ container,
+ baseElement,
+ queries,
+ hydrate,
+ wrapper,
+ root,
+ })
+}
+
+async function cleanup() {
+ for (const {root, container} of mountedRootEntries) {
+ // eslint-disable-next-line no-await-in-loop -- act calls can't overlap
+ await act(() => {
+ root.unmount()
+ })
+ if (container.parentNode === document.body) {
+ document.body.removeChild(container)
+ }
+ }
+
+ mountedRootEntries.length = 0
+ mountedContainers.clear()
+}
+
+async function renderHook(renderCallback, options = {}) {
+ const {initialProps, ...renderOptions} = options
+
+ if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') {
+ const error = new Error(
+ '`legacyRoot: true` is not supported in this version of React. ' +
+ 'If your app runs React 19 or later, you should remove this flag. ' +
+ 'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.',
+ )
+ Error.captureStackTrace(error, renderHook)
+ throw error
+ }
+
+ const result = React.createRef()
+
+ function TestComponent({renderCallbackProps}) {
+ const pendingResult = renderCallback(renderCallbackProps)
+
+ React.useEffect(() => {
+ result.current = pendingResult
+ })
+
+ return null
+ }
+
+ const {rerender: baseRerender, unmount} = await render(
+ ,
+ renderOptions,
+ )
+
+ function rerender(rerenderCallbackProps) {
+ return baseRerender(
+ ,
+ )
+ }
+
+ return {result, rerender, unmount}
+}
+
+// just re-export everything from dom-testing-library
+export * from '@testing-library/dom'
+export {render, renderHook, cleanup, act, fireEvent, getConfig, configure}
+
+/* eslint func-name-matching:0 */
diff --git a/types/pure-async.d.ts b/types/pure-async.d.ts
new file mode 100644
index 00000000..7257c396
--- /dev/null
+++ b/types/pure-async.d.ts
@@ -0,0 +1,264 @@
+// TypeScript Version: 3.8
+// copy of ./index.d.ts but async
+import * as ReactDOMClient from 'react-dom/client'
+import {
+ queries,
+ Queries,
+ BoundFunction,
+ prettyFormat,
+ Config as ConfigDTL,
+ EventType,
+ FireFunction,
+ FireObject,
+} from '@testing-library/dom'
+
+export * from '@testing-library/dom'
+
+export interface Config extends ConfigDTL {
+ reactStrictMode: boolean
+}
+
+export interface ConfigFn {
+ (existingConfig: Config): Partial
+}
+
+export function configure(configDelta: ConfigFn | Partial): void
+
+export function getConfig(): Config
+
+export type RenderResult<
+ Q extends Queries = typeof queries,
+ Container extends RendererableContainer | HydrateableContainer = HTMLElement,
+ BaseElement extends RendererableContainer | HydrateableContainer = Container,
+> = {
+ container: Container
+ baseElement: BaseElement
+ debug: (
+ baseElement?:
+ | RendererableContainer
+ | HydrateableContainer
+ | Array
+ | undefined,
+ maxLength?: number | undefined,
+ options?: prettyFormat.OptionsReceived | undefined,
+ ) => void
+ rerender: (ui: React.ReactNode) => Promise
+ unmount: () => Promise
+ asFragment: () => DocumentFragment
+} & {[P in keyof Q]: BoundFunction}
+
+/** @deprecated */
+export type BaseRenderOptions<
+ Q extends Queries,
+ Container extends RendererableContainer | HydrateableContainer,
+ BaseElement extends RendererableContainer | HydrateableContainer,
+> = RenderOptions
+
+type RendererableContainer = ReactDOMClient.Container
+type HydrateableContainer = Parameters[0]
+/** @deprecated */
+export interface ClientRenderOptions<
+ Q extends Queries,
+ Container extends RendererableContainer,
+ BaseElement extends RendererableContainer = Container,
+> extends BaseRenderOptions {
+ /**
+ * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
+ * rendering and use ReactDOM.hydrate to mount your components.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
+ */
+ hydrate?: false | undefined
+}
+/** @deprecated */
+export interface HydrateOptions<
+ Q extends Queries,
+ Container extends HydrateableContainer,
+ BaseElement extends HydrateableContainer = Container,
+> extends BaseRenderOptions {
+ /**
+ * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
+ * rendering and use ReactDOM.hydrate to mount your components.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
+ */
+ hydrate: true
+}
+
+export interface RenderOptions<
+ Q extends Queries = typeof queries,
+ Container extends RendererableContainer | HydrateableContainer = HTMLElement,
+ BaseElement extends RendererableContainer | HydrateableContainer = Container,
+> {
+ /**
+ * By default, React Testing Library will create a div and append that div to the document.body. Your React component will be rendered in the created div. If you provide your own HTMLElement container via this option,
+ * it will not be appended to the document.body automatically.
+ *
+ * For example: If you are unit testing a `` element, it cannot be a child of a div. In this case, you can
+ * specify a table as the render container.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#container
+ */
+ container?: Container | undefined
+ /**
+ * Defaults to the container if the container is specified. Otherwise `document.body` is used for the default. This is used as
+ * the base element for the queries as well as what is printed when you use `debug()`.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#baseelement
+ */
+ baseElement?: BaseElement | undefined
+ /**
+ * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
+ * rendering and use ReactDOM.hydrate to mount your components.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
+ */
+ hydrate?: boolean | undefined
+ /**
+ * Only works if used with React 18.
+ * Set to `true` if you want to force synchronous `ReactDOM.render`.
+ * Otherwise `render` will default to concurrent React if available.
+ */
+ legacyRoot?: boolean | undefined
+ /**
+ * Queries to bind. Overrides the default set from DOM Testing Library unless merged.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#queries
+ */
+ queries?: Q | undefined
+ /**
+ * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
+ * reusable custom render functions for common data providers. See setup for examples.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#wrapper
+ */
+ wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> | undefined
+}
+
+type Omit = Pick>
+
+/**
+ * Render into a container which is appended to document.body. It should be used with cleanup.
+ */
+export function render<
+ Q extends Queries = typeof queries,
+ Container extends RendererableContainer | HydrateableContainer = HTMLElement,
+ BaseElement extends RendererableContainer | HydrateableContainer = Container,
+>(
+ ui: React.ReactNode,
+ options: RenderOptions,
+): Promise>
+export function render(
+ ui: React.ReactNode,
+ options?: Omit | undefined,
+): Promise
+
+export interface RenderHookResult {
+ /**
+ * Triggers a re-render. The props will be passed to your renderHook callback.
+ */
+ rerender: (props?: Props) => Promise
+ /**
+ * This is a stable reference to the latest value returned by your renderHook
+ * callback
+ */
+ result: {
+ /**
+ * The value returned by your renderHook callback
+ */
+ current: Result
+ }
+ /**
+ * Unmounts the test component. This is useful for when you need to test
+ * any cleanup your useEffects have.
+ */
+ unmount: () => Promise
+}
+
+/** @deprecated */
+export type BaseRenderHookOptions<
+ Props,
+ Q extends Queries,
+ Container extends RendererableContainer | HydrateableContainer,
+ BaseElement extends Element | DocumentFragment,
+> = RenderHookOptions
+
+/** @deprecated */
+export interface ClientRenderHookOptions<
+ Props,
+ Q extends Queries,
+ Container extends Element | DocumentFragment,
+ BaseElement extends Element | DocumentFragment = Container,
+> extends BaseRenderHookOptions {
+ /**
+ * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
+ * rendering and use ReactDOM.hydrate to mount your components.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
+ */
+ hydrate?: false | undefined
+}
+
+/** @deprecated */
+export interface HydrateHookOptions<
+ Props,
+ Q extends Queries,
+ Container extends Element | DocumentFragment,
+ BaseElement extends Element | DocumentFragment = Container,
+> extends BaseRenderHookOptions {
+ /**
+ * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
+ * rendering and use ReactDOM.hydrate to mount your components.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
+ */
+ hydrate: true
+}
+
+export interface RenderHookOptions<
+ Props,
+ Q extends Queries = typeof queries,
+ Container extends RendererableContainer | HydrateableContainer = HTMLElement,
+ BaseElement extends RendererableContainer | HydrateableContainer = Container,
+> extends BaseRenderOptions {
+ /**
+ * The argument passed to the renderHook callback. Can be useful if you plan
+ * to use the rerender utility to change the values passed to your hook.
+ */
+ initialProps?: Props | undefined
+}
+
+/**
+ * Allows you to render a hook within a test React component without having to
+ * create that component yourself.
+ */
+export function renderHook<
+ Result,
+ Props,
+ Q extends Queries = typeof queries,
+ Container extends RendererableContainer | HydrateableContainer = HTMLElement,
+ BaseElement extends RendererableContainer | HydrateableContainer = Container,
+>(
+ render: (initialProps: Props) => Result,
+ options?: RenderHookOptions | undefined,
+): Promise>
+
+/**
+ * Unmounts React trees that were mounted with render.
+ */
+export function cleanup(): Promise
+
+export function act(cb: () => void | Promise): Promise
+
+export type AsyncFireFunction = (
+ element: Document | Element | Window | Node,
+ event: Event,
+) => Promise
+export type AsyncFireObject = {
+ [K in EventType]: (
+ element: Document | Element | Window | Node,
+ options?: {},
+ ) => Promise
+}
+
+export const fireEvent: AsyncFireFunction & AsyncFireObject
diff --git a/types/test.tsx b/types/test.tsx
index 2b3dd7ca..d3915889 100644
--- a/types/test.tsx
+++ b/types/test.tsx
@@ -1,4 +1,5 @@
import * as React from 'react'
+import * as async from '../async'
import {render, fireEvent, screen, waitFor, renderHook} from '.'
import * as pure from './pure'
@@ -259,6 +260,18 @@ export function testContainer() {
renderHook(() => null, {container: document, hydrate: true})
}
+export async function testAsync() {
+ await async.render().then(() => {})
+ await async.renderHook(() => null).then(() => {})
+ await async.fireEvent.click(document.createElement('div')).then(() => {})
+ await async
+ .fireEvent(document.createElement('div'), new MouseEvent('click'))
+ .then(() => {})
+ await async.cleanup().then(() => {})
+ await async.act(() => {}).then(() => {})
+ await async.act(async () => {}).then(() => {})
+}
+
/*
eslint
testing-library/prefer-explicit-assert: "off",