Skip to content

release alpha as stable #11

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

Merged
merged 2 commits into from
Dec 6, 2024
Merged
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
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()
}
})
```
8 changes: 4 additions & 4 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",
"react-dom": "19.0.0",
"react-error-boundary": "^4.0.13",
"ts-jest-resolver": "^2.0.1",
"tsup": "^8.3.0",
Expand All @@ -93,8 +93,8 @@
"peerDependencies": {
"@jest/globals": "*",
"expect": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc"
},
"scripts": {
"build": "tsup",
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