Skip to content

Commit b209c2e

Browse files
authored
Merge pull request #11 from testing-library/alpha
2 parents 5c44c14 + 61ff60e commit b209c2e

19 files changed

+632
-322
lines changed

README.md

+56-51
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
## What is this library?
44

5-
This library allows you to make render-per-render assertions on your React
6-
components and hooks. This is usually not necessary, but can be highly
7-
beneficial when testing hot code paths.
5+
This library allows you to make committed-render-to-committed-render assertions
6+
on your React components and hooks. This is usually not necessary, but can be
7+
highly beneficial when testing hot code paths.
88

99
## Who is this library for?
1010

@@ -36,7 +36,7 @@ test('iterate through renders with DOM snapshots', async () => {
3636
const {takeRender, render} = createRenderStream({
3737
snapshotDOM: true,
3838
})
39-
const utils = render(<Counter />)
39+
const utils = await render(<Counter />)
4040
const incrementButton = utils.getByText('Increment')
4141
await userEvent.click(incrementButton)
4242
await userEvent.click(incrementButton)
@@ -58,36 +58,14 @@ test('iterate through renders with DOM snapshots', async () => {
5858
})
5959
```
6060

61-
### `renderToRenderStream` as a shortcut for `createRenderStream` and calling `render`
62-
63-
In every place you would call
64-
65-
```js
66-
const renderStream = createRenderStream(options)
67-
const utils = renderStream.render(<Component />, options)
68-
```
69-
70-
you can also call
71-
72-
```js
73-
const renderStream = renderToRenderStream(<Component />, combinedOptions)
74-
// if required
75-
const utils = await renderStream.renderResultPromise
76-
```
77-
78-
This might be shorter (especially in cases where you don't need to access
79-
`utils`), but keep in mind that the render is executed **asynchronously** after
80-
calling `renderToRenderStream`, and that you need to `await renderResultPromise`
81-
if you need access to `utils` as returned by `render`.
82-
8361
### `renderHookToSnapshotStream`
8462

8563
Usage is very similar to RTL's `renderHook`, but you get a `snapshotStream`
8664
object back that you can iterate with `takeSnapshot` calls.
8765

8866
```jsx
8967
test('`useQuery` with `skip`', async () => {
90-
const {takeSnapshot, rerender} = renderHookToSnapshotStream(
68+
const {takeSnapshot, rerender} = await renderHookToSnapshotStream(
9169
({skip}) => useQuery(query, {skip}),
9270
{
9371
wrapper: ({children}) => <Provider client={client}>{children}</Provider>,
@@ -105,7 +83,7 @@ test('`useQuery` with `skip`', async () => {
10583
expect(result.data).toEqual({hello: 'world 1'})
10684
}
10785

108-
rerender({skip: true})
86+
await rerender({skip: true})
10987
{
11088
const snapshot = await takeSnapshot()
11189
expect(snapshot.loading).toBe(false)
@@ -146,7 +124,7 @@ test('`useTrackRenders` with suspense', async () => {
146124
}
147125

148126
const {takeRender, render} = createRenderStream()
149-
render(<App />)
127+
await render(<App />)
150128
{
151129
const {renderedComponents} = await takeRender()
152130
expect(renderedComponents).toEqual([App, LoadingComponent])
@@ -179,7 +157,7 @@ test('custom snapshots with `replaceSnapshot`', async () => {
179157
const {takeRender, replaceSnapshot, render} = createRenderStream<{
180158
value: number
181159
}>()
182-
const utils = render(<Counter />)
160+
const utils = await render(<Counter />)
183161
const incrementButton = utils.getByText('Increment')
184162
await userEvent.click(incrementButton)
185163
{
@@ -215,16 +193,14 @@ test('assertions in `onRender`', async () => {
215193
)
216194
}
217195

218-
const {takeRender, replaceSnapshot, renderResultPromise} =
219-
renderToRenderStream<{
220-
value: number
221-
}>({
222-
onRender(info) {
223-
// you can use `expect` here
224-
expect(info.count).toBe(info.snapshot.value + 1)
225-
},
226-
})
227-
const utils = await renderResultPromise
196+
const {takeRender, replaceSnapshot, utils} = await renderToRenderStream<{
197+
value: number
198+
}>({
199+
onRender(info) {
200+
// you can use `expect` here
201+
expect(info.count).toBe(info.snapshot.value + 1)
202+
},
203+
})
228204
const incrementButton = utils.getByText('Increment')
229205
await userEvent.click(incrementButton)
230206
await userEvent.click(incrementButton)
@@ -247,7 +223,7 @@ This library adds to matchers to `expect` that can be used like
247223

248224
```tsx
249225
test('basic functionality', async () => {
250-
const {takeRender} = renderToRenderStream(<RerenderingComponent />)
226+
const {takeRender} = await renderToRenderStream(<RerenderingComponent />)
251227

252228
await expect(takeRender).toRerender()
253229
await takeRender()
@@ -285,17 +261,46 @@ await expect(snapshotStream).toRerender()
285261
> [!TIP]
286262
>
287263
> If you don't want these matchers not to be automatically installed, you can
288-
> import from `@testing-library/react-render-stream` instead.
264+
> import from `@testing-library/react-render-stream/pure` instead.
265+
> Keep in mind that if you use the `/pure` import, you have to call the
266+
> `cleanup` export manually after each test.
267+
268+
## Usage side-by side with `@testing-library/react` or other tools that use `act` or set `IS_REACT_ACT_ENVIRONMENT`
289269
290-
## A note on `act`.
270+
This library should not be used with `act`, and it will throw an error if
271+
`IS_REACT_ACT_ENVIRONMENT` is `true`.
291272
292-
You might want to avoid using this library with `act`, as `act`
293-
[can end up batching multiple renders](https://github.com/facebook/react/issues/30031#issuecomment-2183951296)
294-
into one in a way that would not happen in a production application.
273+
React Testing Library sets `IS_REACT_ACT_ENVIRONMENT` to `true` globally, and
274+
wraps some helpers like `userEvent.click` in `act` calls.
275+
To use this library side-by-side with React Testing Library, we ship the
276+
`disableActEnvironment` helper to undo these changes temporarily.
295277
296-
While that is convenient in a normal test suite, it defeats the purpose of this
297-
library.
278+
It returns a `Disposable` and can be used together with the
279+
[`using` keyword](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management)
280+
to automatically clean up once the scope is left:
298281
299-
Keep in mind that tools like `userEvent.click` use `act` internally. Many of
300-
those calls would only trigger one render anyways, so it can be okay to use
301-
them, but avoid this for longer-running actions inside of `act` calls.
282+
```ts
283+
test('my test', () => {
284+
using _disabledAct = disableActEnvironment()
285+
286+
// your test code here
287+
288+
// as soon as this scope is left, the environment will be cleaned up
289+
})
290+
```
291+
292+
If you cannot use `using`, you can also manually call the returned `cleanup`
293+
function. We recommend using `finally` to ensure the act environment is cleaned
294+
up if your test fails, otherwise it could leak between tests:
295+
296+
```ts
297+
test('my test', () => {
298+
const {cleanup} = disableActEnvironment()
299+
300+
try {
301+
// your test code here
302+
} finally {
303+
cleanup()
304+
}
305+
})
306+
```

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@
8383
"pkg-pr-new": "^0.0.29",
8484
"prettier": "^3.3.3",
8585
"publint": "^0.2.11",
86-
"react": "^18.3.1",
87-
"react-dom": "^18.3.1",
86+
"react": "19.0.0",
87+
"react-dom": "19.0.0",
8888
"react-error-boundary": "^4.0.13",
8989
"ts-jest-resolver": "^2.0.1",
9090
"tsup": "^8.3.0",
@@ -93,8 +93,8 @@
9393
"peerDependencies": {
9494
"@jest/globals": "*",
9595
"expect": "*",
96-
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0",
97-
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0"
96+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc",
97+
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc"
9898
},
9999
"scripts": {
100100
"build": "tsup",

src/__testHelpers__/useShim.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as React from 'react'
2+
13
/* eslint-disable default-case */
24
/* eslint-disable consistent-return */
35
function isStatefulPromise(promise) {
@@ -33,7 +35,7 @@ function wrapPromiseWithState(promise) {
3335
* @param {Promise<T>} promise
3436
* @returns {T}
3537
*/
36-
export function __use(promise) {
38+
function _use(promise) {
3739
const statefulPromise = wrapPromiseWithState(promise)
3840
switch (statefulPromise.status) {
3941
case 'pending':
@@ -44,3 +46,5 @@ export function __use(promise) {
4446
return statefulPromise.value
4547
}
4648
}
49+
50+
export const __use = /** @type {{use?: typeof _use}} */ (React).use || _use

src/__testHelpers__/withDisabledActWarnings.ts

-14
This file was deleted.

src/__tests__/renderHookToSnapshotStream.test.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/* eslint-disable no-await-in-loop */
22
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
33
import {EventEmitter} from 'node:events'
4+
import {scheduler} from 'node:timers/promises'
45
import {test, expect} from '@jest/globals'
56
import {renderHookToSnapshotStream} from '@testing-library/react-render-stream'
67
import * as React from 'react'
7-
import {withDisabledActWarnings} from '../__testHelpers__/withDisabledActWarnings.js'
88

99
const testEvents = new EventEmitter<{
1010
rerenderWithValue: [unknown]
@@ -16,7 +16,7 @@ function useRerenderEvents(initialValue: unknown) {
1616
onChange => {
1717
const cb = (value: unknown) => {
1818
lastValueRef.current = value
19-
withDisabledActWarnings(onChange)
19+
onChange()
2020
}
2121
testEvents.addListener('rerenderWithValue', cb)
2222
return () => {
@@ -30,11 +30,11 @@ function useRerenderEvents(initialValue: unknown) {
3030
}
3131

3232
test('basic functionality', async () => {
33-
const {takeSnapshot} = renderHookToSnapshotStream(useRerenderEvents, {
33+
const {takeSnapshot} = await renderHookToSnapshotStream(useRerenderEvents, {
3434
initialProps: 'initial',
3535
})
3636
testEvents.emit('rerenderWithValue', 'value')
37-
await Promise.resolve()
37+
await scheduler.wait(10)
3838
testEvents.emit('rerenderWithValue', 'value2')
3939
{
4040
const snapshot = await takeSnapshot()
@@ -59,7 +59,7 @@ test.each<[type: string, initialValue: unknown, ...nextValues: unknown[]]>([
5959
['null/undefined', null, undefined, null],
6060
['undefined/null', undefined, null, undefined],
6161
])('works with %s', async (_, initialValue, ...nextValues) => {
62-
const {takeSnapshot} = renderHookToSnapshotStream(useRerenderEvents, {
62+
const {takeSnapshot} = await renderHookToSnapshotStream(useRerenderEvents, {
6363
initialProps: initialValue,
6464
})
6565
for (const nextValue of nextValues) {

src/__tests__/renderToRenderStream.test.tsx

-94
This file was deleted.

0 commit comments

Comments
 (0)