Skip to content

Commit

Permalink
feat!: necessary api changes for React 19 compatibility (#10)
Browse files Browse the repository at this point in the history
Co-authored-by: Jerel Miller <[email protected]>
  • Loading branch information
phryneas and jerelmiller authored Dec 4, 2024
1 parent 5c44c14 commit a3b3275
Show file tree
Hide file tree
Showing 19 changed files with 628 additions and 318 deletions.
107 changes: 56 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

## What is this library?

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

## Who is this library for?

Expand Down Expand Up @@ -36,7 +36,7 @@ test('iterate through renders with DOM snapshots', async () => {
const {takeRender, render} = createRenderStream({
snapshotDOM: true,
})
const utils = render(<Counter />)
const utils = await render(<Counter />)
const incrementButton = utils.getByText('Increment')
await userEvent.click(incrementButton)
await userEvent.click(incrementButton)
Expand All @@ -58,36 +58,14 @@ test('iterate through renders with DOM snapshots', async () => {
})
```

### `renderToRenderStream` as a shortcut for `createRenderStream` and calling `render`

In every place you would call

```js
const renderStream = createRenderStream(options)
const utils = renderStream.render(<Component />, options)
```

you can also call

```js
const renderStream = renderToRenderStream(<Component />, combinedOptions)
// if required
const utils = await renderStream.renderResultPromise
```

This might be shorter (especially in cases where you don't need to access
`utils`), but keep in mind that the render is executed **asynchronously** after
calling `renderToRenderStream`, and that you need to `await renderResultPromise`
if you need access to `utils` as returned by `render`.

### `renderHookToSnapshotStream`

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

```jsx
test('`useQuery` with `skip`', async () => {
const {takeSnapshot, rerender} = renderHookToSnapshotStream(
const {takeSnapshot, rerender} = await renderHookToSnapshotStream(
({skip}) => useQuery(query, {skip}),
{
wrapper: ({children}) => <Provider client={client}>{children}</Provider>,
Expand All @@ -105,7 +83,7 @@ test('`useQuery` with `skip`', async () => {
expect(result.data).toEqual({hello: 'world 1'})
}

rerender({skip: true})
await rerender({skip: true})
{
const snapshot = await takeSnapshot()
expect(snapshot.loading).toBe(false)
Expand Down Expand Up @@ -146,7 +124,7 @@ test('`useTrackRenders` with suspense', async () => {
}

const {takeRender, render} = createRenderStream()
render(<App />)
await render(<App />)
{
const {renderedComponents} = await takeRender()
expect(renderedComponents).toEqual([App, LoadingComponent])
Expand Down Expand Up @@ -179,7 +157,7 @@ test('custom snapshots with `replaceSnapshot`', async () => {
const {takeRender, replaceSnapshot, render} = createRenderStream<{
value: number
}>()
const utils = render(<Counter />)
const utils = await render(<Counter />)
const incrementButton = utils.getByText('Increment')
await userEvent.click(incrementButton)
{
Expand Down Expand Up @@ -215,16 +193,14 @@ test('assertions in `onRender`', async () => {
)
}

const {takeRender, replaceSnapshot, renderResultPromise} =
renderToRenderStream<{
value: number
}>({
onRender(info) {
// you can use `expect` here
expect(info.count).toBe(info.snapshot.value + 1)
},
})
const utils = await renderResultPromise
const {takeRender, replaceSnapshot, utils} = await renderToRenderStream<{
value: number
}>({
onRender(info) {
// you can use `expect` here
expect(info.count).toBe(info.snapshot.value + 1)
},
})
const incrementButton = utils.getByText('Increment')
await userEvent.click(incrementButton)
await userEvent.click(incrementButton)
Expand All @@ -247,7 +223,7 @@ This library adds to matchers to `expect` that can be used like

```tsx
test('basic functionality', async () => {
const {takeRender} = renderToRenderStream(<RerenderingComponent />)
const {takeRender} = await renderToRenderStream(<RerenderingComponent />)

await expect(takeRender).toRerender()
await takeRender()
Expand Down Expand Up @@ -285,17 +261,46 @@ await expect(snapshotStream).toRerender()
> [!TIP]
>
> If you don't want these matchers not to be automatically installed, you can
> import from `@testing-library/react-render-stream` instead.
> import from `@testing-library/react-render-stream/pure` instead.
> Keep in mind that if you use the `/pure` import, you have to call the
> `cleanup` export manually after each test.
## Usage side-by side with `@testing-library/react` or other tools that use `act` or set `IS_REACT_ACT_ENVIRONMENT`
## A note on `act`.
This library should not be used with `act`, and it will throw an error if
`IS_REACT_ACT_ENVIRONMENT` is `true`.
You might want to avoid using this library with `act`, as `act`
[can end up batching multiple renders](https://github.com/facebook/react/issues/30031#issuecomment-2183951296)
into one in a way that would not happen in a production application.
React Testing Library sets `IS_REACT_ACT_ENVIRONMENT` to `true` globally, and
wraps some helpers like `userEvent.click` in `act` calls.
To use this library side-by-side with React Testing Library, we ship the
`disableActEnvironment` helper to undo these changes temporarily.
While that is convenient in a normal test suite, it defeats the purpose of this
library.
It returns a `Disposable` and can be used together with the
[`using` keyword](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management)
to automatically clean up once the scope is left:
Keep in mind that tools like `userEvent.click` use `act` internally. Many of
those calls would only trigger one render anyways, so it can be okay to use
them, but avoid this for longer-running actions inside of `act` calls.
```ts
test('my test', () => {
using _disabledAct = disableActEnvironment()
// your test code here
// as soon as this scope is left, the environment will be cleaned up
})
```
If you cannot use `using`, you can also manually call the returned `cleanup`
function. We recommend using `finally` to ensure the act environment is cleaned
up if your test fails, otherwise it could leak between tests:

```ts
test('my test', () => {
const {cleanup} = disableActEnvironment()

try {
// your test code here
} finally {
cleanup()
}
})
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@
"pkg-pr-new": "^0.0.29",
"prettier": "^3.3.3",
"publint": "^0.2.11",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"react-error-boundary": "^4.0.13",
"ts-jest-resolver": "^2.0.1",
"tsup": "^8.3.0",
Expand Down
6 changes: 5 additions & 1 deletion src/__testHelpers__/useShim.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as React from 'react'

/* eslint-disable default-case */
/* eslint-disable consistent-return */
function isStatefulPromise(promise) {
Expand Down Expand Up @@ -33,7 +35,7 @@ function wrapPromiseWithState(promise) {
* @param {Promise<T>} promise
* @returns {T}
*/
export function __use(promise) {
function _use(promise) {
const statefulPromise = wrapPromiseWithState(promise)
switch (statefulPromise.status) {
case 'pending':
Expand All @@ -44,3 +46,5 @@ export function __use(promise) {
return statefulPromise.value
}
}

export const __use = /** @type {{use?: typeof _use}} */ (React).use || _use
14 changes: 0 additions & 14 deletions src/__testHelpers__/withDisabledActWarnings.ts

This file was deleted.

10 changes: 5 additions & 5 deletions src/__tests__/renderHookToSnapshotStream.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import {EventEmitter} from 'node:events'
import {scheduler} from 'node:timers/promises'
import {test, expect} from '@jest/globals'
import {renderHookToSnapshotStream} from '@testing-library/react-render-stream'
import * as React from 'react'
import {withDisabledActWarnings} from '../__testHelpers__/withDisabledActWarnings.js'

const testEvents = new EventEmitter<{
rerenderWithValue: [unknown]
Expand All @@ -16,7 +16,7 @@ function useRerenderEvents(initialValue: unknown) {
onChange => {
const cb = (value: unknown) => {
lastValueRef.current = value
withDisabledActWarnings(onChange)
onChange()
}
testEvents.addListener('rerenderWithValue', cb)
return () => {
Expand All @@ -30,11 +30,11 @@ function useRerenderEvents(initialValue: unknown) {
}

test('basic functionality', async () => {
const {takeSnapshot} = renderHookToSnapshotStream(useRerenderEvents, {
const {takeSnapshot} = await renderHookToSnapshotStream(useRerenderEvents, {
initialProps: 'initial',
})
testEvents.emit('rerenderWithValue', 'value')
await Promise.resolve()
await scheduler.wait(10)
testEvents.emit('rerenderWithValue', 'value2')
{
const snapshot = await takeSnapshot()
Expand All @@ -59,7 +59,7 @@ test.each<[type: string, initialValue: unknown, ...nextValues: unknown[]]>([
['null/undefined', null, undefined, null],
['undefined/null', undefined, null, undefined],
])('works with %s', async (_, initialValue, ...nextValues) => {
const {takeSnapshot} = renderHookToSnapshotStream(useRerenderEvents, {
const {takeSnapshot} = await renderHookToSnapshotStream(useRerenderEvents, {
initialProps: initialValue,
})
for (const nextValue of nextValues) {
Expand Down
94 changes: 0 additions & 94 deletions src/__tests__/renderToRenderStream.test.tsx

This file was deleted.

Loading

0 comments on commit a3b3275

Please sign in to comment.