Skip to content

Commit bdabd1f

Browse files
committed
init README
1 parent 5b7c735 commit bdabd1f

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed

README.md

+267
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# @testing-library/react-render-stream
2+
3+
## What is this library?
4+
5+
This library allows you to make render-per-render asserions on your React
6+
components and hooks. This is usually not necessary, but can be highly
7+
beneficial when testing hot code paths.
8+
9+
## Who is this library for?
10+
11+
This library is intended to test libraries or library-like code. It requires you
12+
to write additional components so you can test how your components interact with
13+
other components in specific scenarios.
14+
15+
As such, it is not intended to be used for end-to-end testing of your
16+
application.
17+
18+
## Brought to you by Apollo
19+
20+
<a style="display: flex; justify-content: center; height: 40px; margin: 1em" href="https://www.apollographql.com/">
21+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 253 87" fill="none"><path fill="#15252D" d="M141.613 50.612a16.232 16.232 0 0 1-6.217 6.445c-2.663 1.55-5.695 2.325-9.099 2.325-3.403 0-6.435-.775-9.098-2.325a16.215 16.215 0 0 1-6.217-6.445c-1.483-2.746-2.225-5.82-2.224-9.224 0-3.404.742-6.478 2.224-9.225a16.227 16.227 0 0 1 6.217-6.444c2.662-1.55 5.695-2.325 9.098-2.325 3.404 0 6.437.774 9.099 2.325a16.244 16.244 0 0 1 6.217 6.444c1.482 2.747 2.224 5.822 2.224 9.225 0 3.404-.741 6.479-2.224 9.224Zm-23.908-3.26c.809 1.753 1.955 3.126 3.437 4.12 1.483.993 3.201 1.49 5.156 1.49 1.921 0 3.622-.496 5.105-1.49 1.483-.994 2.628-2.367 3.437-4.12.809-1.752 1.213-3.74 1.213-5.964s-.404-4.212-1.213-5.964c-.809-1.752-1.954-3.126-3.437-4.12-1.483-.994-3.184-1.491-5.105-1.49-1.955 0-3.673.496-5.156 1.49-1.482.994-2.628 2.367-3.437 4.12-.809 1.752-1.213 3.74-1.213 5.964s.404 4.212 1.213 5.964ZM249.781 50.612a16.232 16.232 0 0 1-6.217 6.445c-2.663 1.55-5.695 2.325-9.099 2.325-3.403 0-6.435-.775-9.098-2.325a16.215 16.215 0 0 1-6.217-6.445c-1.483-2.746-2.225-5.82-2.224-9.224 0-3.404.742-6.478 2.224-9.225a16.227 16.227 0 0 1 6.217-6.444c2.662-1.55 5.695-2.325 9.098-2.325 3.404 0 6.437.774 9.099 2.325a16.244 16.244 0 0 1 6.217 6.444c1.482 2.747 2.224 5.822 2.224 9.225 0 3.404-.741 6.479-2.224 9.224Zm-23.908-3.26c.809 1.753 1.955 3.126 3.437 4.12 1.483.993 3.201 1.49 5.156 1.49 1.921 0 3.622-.496 5.105-1.49 1.483-.994 2.628-2.367 3.437-4.12.809-1.752 1.213-3.74 1.213-5.964s-.404-4.212-1.213-5.964c-.809-1.752-1.954-3.126-3.437-4.12-1.483-.994-3.184-1.491-5.105-1.49-1.955 0-3.673.496-5.156 1.49-1.482.994-2.628 2.367-3.437 4.12-.809 1.752-1.213 3.74-1.213 5.964s.404 4.212 1.213 5.964ZM58.86 52.87l-1.669-4.742-2.197-6.244-1.59-4.52-2.081-5.913-2.417-6.87H37.18l-1.872 5.32-1.669 4.742-8.287 23.551h7.733l2.426-6.874H47.24l-2.081-5.914h-7.574l1.59-4.519 3.32-9.436.549-1.562.549 1.562 9.406 26.738.002.006h7.734L58.86 52.87ZM97.536 27.589c1.752 2.005 2.628 4.591 2.628 7.758 0 3.336-.868 6.024-2.603 8.062-1.736 2.04-4.423 3.058-8.062 3.058h-10.11v11.727h-7.733V24.58H89.5c3.605 0 6.284 1.003 8.037 3.008ZM87.073 40.503c1.786 0 3.083-.455 3.892-1.365.809-.91 1.213-2.156 1.213-3.74 0-3.235-1.685-4.853-5.054-4.853H79.39v9.958h7.683ZM161.94 24.581v27.497h15.72v6.116h-23.453V24.581h7.733ZM193.944 24.581v27.497h15.72v6.116h-23.453V24.581h7.733Z"/><path fill="#15252D" d="M66.644 13.647a4.482 4.482 0 1 0-2.562-8.16 43.052 43.052 0 1 0 3.566 72.898 1.756 1.756 0 0 0-2.01-2.88 39.534 39.534 0 1 1-3.423-67.039 4.485 4.485 0 0 0 4.428 5.18Z"/>
22+
<style>
23+
path {
24+
fill: #15252D;
25+
}
26+
@media (prefers-color-scheme: dark) {
27+
path {
28+
fill: #feeadb;
29+
}
30+
}
31+
</style>
32+
</svg>
33+
</a>
34+
35+
This library originally was part of the Apollo Client test suite and is
36+
maintained by the Apollo Client team.
37+
38+
### Usage examples:
39+
40+
#### `createRenderStream` with DOM snapshots
41+
42+
If used with `snapshotDOM`, RSTL will create a snapshot of your DOM after every
43+
render, and you can iterate through all the intermediate states of your DOM at
44+
your own pace, independenly of how fast these renders actually happened.
45+
46+
```jsx
47+
test('iterate through renders with DOM snapshots', async () => {
48+
const {takeRender, render} = createRenderStream({
49+
snapshotDOM: true,
50+
})
51+
const utils = render(<Counter />)
52+
const incrementButton = utils.getByText('Increment')
53+
await userEvent.click(incrementButton)
54+
await userEvent.click(incrementButton)
55+
{
56+
const {withinDOM} = await takeRender()
57+
const input = withinDOM().getByLabelText('Value')
58+
expect(input.value).toBe('0')
59+
}
60+
{
61+
const {withinDOM} = await takeRender()
62+
const input = withinDOM().getByLabelText('Value')
63+
expect(input.value).toBe('1')
64+
}
65+
{
66+
const {withinDOM} = await takeRender()
67+
const input = withinDOM().getByLabelText('Value')
68+
expect(input.value).toBe('2')
69+
}
70+
})
71+
```
72+
73+
### `renderToRenderStream` as a shortcut for `createRenderStream` and calling `render`
74+
75+
In every place you would call
76+
77+
```js
78+
const renderStream = createRenderStream(options)
79+
const utils = renderStream.render(<Component />, options)
80+
```
81+
82+
you can also call
83+
84+
```js
85+
const renderStream = renderToRenderStream(<Component />, combinedOptions)
86+
// if required
87+
const utils = await renderStream.renderResultPromise
88+
```
89+
90+
This might be shorter (especially in cases where you don't need to access
91+
`utils`), but keep in mind that the render is executed **asynchronously** after
92+
calling `renderToRenderStream`, and that you need to `await renderResultPromise`
93+
if you need access to `utils` as returned by `render`.
94+
95+
### `renderHookToSnapshotStream`
96+
97+
Usage is very similar to RTL's `renderHook`, but you get a `snapshotStream`
98+
object back that you can iterate with `takeSnapshot` calls.
99+
100+
```jsx
101+
test('`useQuery` with `skip`', async () => {
102+
const {takeSnapshot, rerender} = renderHookToSnapshotStream(
103+
({skip}) => useQuery(query, {skip}),
104+
{
105+
wrapper: ({children}) => <Provider client={client}>{children}</Provider>,
106+
},
107+
)
108+
109+
{
110+
const result = await takeSnapshot()
111+
expect(result.loading).toBe(true)
112+
expect(result.data).toBe(undefined)
113+
}
114+
{
115+
const result = await takeSnapshot()
116+
expect(result.loading).toBe(false)
117+
expect(result.data).toEqual({hello: 'world 1'})
118+
}
119+
120+
rerender({skip: true})
121+
{
122+
const snapshot = await takeSnapshot()
123+
expect(snapshot.loading).toBe(false)
124+
expect(snapshot.data).toEqual(undefined)
125+
}
126+
})
127+
```
128+
129+
### Tracking which components rerender with `useTrackRenders`
130+
131+
You can track if a component was rerendered during a specific render by calling
132+
`useTrackRenders` within it.
133+
134+
```jsx
135+
test('`useTrackRenders` with suspense', async () => {
136+
function ErrorComponent() {
137+
useTrackRenders()
138+
// return ...
139+
}
140+
function DataComponent() {
141+
useTrackRenders()
142+
const data = useSuspenseQuery(someQuery)
143+
// return ...
144+
}
145+
function LoadingComponent() {
146+
useTrackRenders()
147+
// return ...
148+
}
149+
function App() {
150+
useTrackRenders()
151+
return (
152+
<ErrorBoundary FallbackComponent={ErrorComponent}>
153+
<React.Suspense fallback={<LoadingComponent />}>
154+
<DataComponent />
155+
</React.Suspense>
156+
</ErrorBoundary>
157+
)
158+
}
159+
160+
const {takeRender, render} = createRenderStream()
161+
render(<App />)
162+
{
163+
const {renderedComponents} = await takeRender()
164+
expect(renderedComponents).toEqual([App, LoadingComponent])
165+
}
166+
{
167+
const {renderedComponents} = await takeRender()
168+
expect(renderedComponents).toEqual([DataComponent])
169+
}
170+
})
171+
```
172+
173+
> [!NOTE]
174+
>
175+
> The order of components in `renderedComponents` is the order of execution of
176+
> `useLayoutEffect`. Keep in mind that this might not be the order you would
177+
> expect.
178+
179+
### taking custom snapshots inside of helper Components with `replaceSnapshot`
180+
181+
If you need to, you can also take custom snapshots of data in each render.
182+
183+
```tsx
184+
test('custom snapshots with `replaceSnapshot`', async () => {
185+
function Counter() {
186+
const [value, setValue] = React.useState(0)
187+
replaceSnapshot({value})
188+
// return ...
189+
}
190+
191+
const {takeRender, replaceSnapshot, render} = createRenderStream<{
192+
value: number
193+
}>()
194+
const utils = render(<Counter />)
195+
const incrementButton = utils.getByText('Increment')
196+
await userEvent.click(incrementButton)
197+
{
198+
const {snapshot} = await takeRender()
199+
expect(snapshot).toEqual({value: 0})
200+
}
201+
{
202+
const {snapshot} = await takeRender()
203+
expect(snapshot).toEqual({value: 1})
204+
}
205+
})
206+
```
207+
208+
> [!TIP]
209+
>
210+
> `replaceSnapshot` can also be called with a callback that gives you access to
211+
> the last snapshot value.
212+
213+
> [!TIP]
214+
>
215+
> You can also use `mergeSnapshot`, which shallowly merges the last snapshot
216+
> with the new one instead of replacing it.
217+
218+
### Making assertions directly after a render with `onRender`
219+
220+
```tsx
221+
test('assertions in `onRender`', async () => {
222+
function Counter() {
223+
const [value, setValue] = React.useState(0)
224+
replaceSnapshot({value})
225+
return (
226+
<CounterForm value={value} onIncrement={() => setValue(v => v + 1)} />
227+
)
228+
}
229+
230+
const {takeRender, replaceSnapshot, renderResultPromise} =
231+
renderToRenderStream<{
232+
value: number
233+
}>({
234+
onRender(info) {
235+
// you can use `expect` here
236+
expect(info.count).toBe(info.snapshot.value + 1)
237+
},
238+
})
239+
const utils = await renderResultPromise
240+
const incrementButton = utils.getByText('Increment')
241+
await userEvent.click(incrementButton)
242+
await userEvent.click(incrementButton)
243+
await takeRender()
244+
await takeRender()
245+
await takeRender()
246+
})
247+
```
248+
249+
> [!INFO]
250+
>
251+
> `info` contains the
252+
> [base profiling information](https://react.dev/reference/react/Profiler#onrender-parameters)
253+
> passed into `onRender` of React's `Profiler` component, as well as `snapshot`,
254+
> `replaceSnapshot` and `mergeSnapshot`
255+
256+
## A note on `act`.
257+
258+
You might want to avoid using this library with `act`, as `act`
259+
[can end up batching multiple renders](https://github.com/facebook/react/issues/30031#issuecomment-2183951296)
260+
into one in a way that would not happen in a production application.
261+
262+
While that is convenient in a normal test suite, it defeats the purpose of this
263+
library.
264+
265+
Keep in mind that tools like `userEvent.click` use `act` internally. Many of
266+
those calls would only trigger one render anyways, so it can be okay to use
267+
them, but avoid this for longer-running actions inside of `act` calls.

0 commit comments

Comments
 (0)