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

feat!: necessary api changes for React 19 compatibility #10

Merged
merged 38 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ddf9df9
add `renderWithoutAct` helper
phryneas Nov 26, 2024
e8616b3
painful progress
phryneas Nov 26, 2024
5ff5420
make render async to wait for first render
phryneas Nov 27, 2024
c124751
all tests passing
phryneas Nov 27, 2024
20ef3eb
more tweaking
phryneas Nov 27, 2024
b086f98
fix type
phryneas Nov 27, 2024
6fb6fb1
also wrap asyncWrapper
phryneas Nov 27, 2024
8474b54
export `RenderWithoutActAsync` type
phryneas Nov 27, 2024
d926b40
build legacy root in React 16/17
phryneas Nov 28, 2024
1ad050f
disable act with confidence
phryneas Nov 28, 2024
9ef55bb
don't auto-disable act, throw error instead
phryneas Nov 29, 2024
2c5b29e
adjust imports
phryneas Nov 29, 2024
69a4c4b
wait a bunch longer so react doesn't batch
phryneas Nov 29, 2024
c064440
add comment
phryneas Nov 29, 2024
932f938
guard in tests against accidental `IS_REACT_ACT_ENVIRONMENT`
phryneas Nov 29, 2024
451d67a
drain the microtask queue before returning from `peekRender`
phryneas Nov 29, 2024
53d9f45
directly import from `@testing-library/dom` where possible
phryneas Nov 29, 2024
32daf10
move `useWithoutAct` into `disableActEnvironment`, reduce api size
phryneas Dec 2, 2024
084b691
keep `writable` at cleanup
phryneas Dec 2, 2024
563b934
early bailout in `cleanup`
phryneas Dec 2, 2024
0e9d9d7
keep `renderWithoutAct` private
phryneas Dec 2, 2024
94f35e5
`renderToRenderStream` also should be async
phryneas Dec 2, 2024
262ceba
make `rerender` wait for the render and return `Promise<void>`
phryneas Dec 2, 2024
c09b997
update README
phryneas Dec 2, 2024
200d9d2
remove unused type
phryneas Dec 2, 2024
0cf8f41
undo `renderToRenderStream` changes
phryneas Dec 2, 2024
20601bc
add type export back
phryneas Dec 2, 2024
e1cb39d
update type
phryneas Dec 2, 2024
fab8705
guard against sync rerenders
phryneas Dec 2, 2024
04c222d
remove `renderToRenderStream`
phryneas Dec 3, 2024
324ae88
add lint pr job
phryneas Dec 3, 2024
e1eae04
avoid uncaught promise rejection in test
phryneas Dec 3, 2024
7e9ac02
Merge branch 'main' into pr/noActRender
phryneas Dec 3, 2024
7ea9591
run tests with React 19 RC1
phryneas Dec 3, 2024
7ede425
use `use` over the shim when available
phryneas Dec 3, 2024
f3fb67d
Apply suggestions from code review
phryneas Dec 4, 2024
b0d022f
adjust import
phryneas Dec 4, 2024
4d2345b
review suggestions
phryneas Dec 4, 2024
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
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
phryneas marked this conversation as resolved.
Show resolved Hide resolved
`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
Loading