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

Add async render APIs #1365

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions async.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './types/pure-async'
2 changes: 2 additions & 0 deletions async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// makes it so people can import from '@testing-library/react/async'
module.exports = require('./dist/async')
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
1 change: 1 addition & 0 deletions pure-async.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './types/pure-async'
2 changes: 2 additions & 0 deletions pure-async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// makes it so people can import from '@testing-library/react/pure-async'
module.exports = require('./dist/pure-async')
73 changes: 73 additions & 0 deletions src/__tests__/async.js
Original file line number Diff line number Diff line change
@@ -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 <div>{value}</div>
}

const {container} = await render(
<React.Suspense fallback="loading...">
<Component />
</React.Suspense>,
)

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 (
<button
onClick={() =>
setPromise(
new Promise(_resolve => {
resolve = _resolve
}),
)
}
>
Value: {value}
</button>
)
}

const {container} = await render(
<React.Suspense fallback="loading...">
<Component />
</React.Suspense>,
)

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!')
})
42 changes: 42 additions & 0 deletions src/async.js
Original file line number Diff line number Diff line change
@@ -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'
70 changes: 70 additions & 0 deletions src/fire-event-async.js
Original file line number Diff line number Diff line change
@@ -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}
Loading