Skip to content

Commit 5ff5420

Browse files
committed
make render async to wait for first render
1 parent e8616b3 commit 5ff5420

File tree

8 files changed

+117
-59
lines changed

8 files changed

+117
-59
lines changed

src/__tests__/renderHookToSnapshotStream.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@ function useRerenderEvents(initialValue: unknown) {
3333
}
3434

3535
test('basic functionality', async () => {
36-
const {takeSnapshot} = renderHookToSnapshotStream(useRerenderEvents, {
36+
const {takeSnapshot} = await renderHookToSnapshotStream(useRerenderEvents, {
3737
initialProps: 'initial',
3838
})
39+
testEvents.emit('rerenderWithValue', 'value')
40+
await Promise.resolve()
41+
testEvents.emit('rerenderWithValue', 'value2')
3942
{
4043
const snapshot = await takeSnapshot()
4144
expect(snapshot).toBe('initial')
4245
}
43-
testEvents.emit('rerenderWithValue', 'value')
44-
await Promise.resolve()
45-
testEvents.emit('rerenderWithValue', 'value2')
4646
{
4747
const snapshot = await takeSnapshot()
4848
expect(snapshot).toBe('value')
@@ -62,15 +62,15 @@ test.each<[type: string, initialValue: unknown, ...nextValues: unknown[]]>([
6262
['null/undefined', null, undefined, null],
6363
['undefined/null', undefined, null, undefined],
6464
])('works with %s', async (_, initialValue, ...nextValues) => {
65-
const {takeSnapshot} = renderHookToSnapshotStream(useRerenderEvents, {
65+
const {takeSnapshot} = await renderHookToSnapshotStream(useRerenderEvents, {
6666
initialProps: initialValue,
6767
})
68-
expect(await takeSnapshot()).toBe(initialValue)
6968
for (const nextValue of nextValues) {
7069
testEvents.emit('rerenderWithValue', nextValue)
7170
// allow for a render to happen
7271
await Promise.resolve()
7372
}
73+
expect(await takeSnapshot()).toBe(initialValue)
7474
for (const nextValue of nextValues) {
7575
expect(await takeSnapshot()).toBe(nextValue)
7676
}

src/__tests__/renderToRenderStream.test.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
/* eslint-disable @typescript-eslint/no-use-before-define */
22
import {describe, test, expect} from '@jest/globals'
3-
import {renderToRenderStream} from '@testing-library/react-render-stream'
4-
import {userEvent} from '@testing-library/user-event'
3+
import {
4+
renderToRenderStream,
5+
userEventWithoutAct,
6+
} from '@testing-library/react-render-stream'
7+
import {userEvent as baseUserEvent} from '@testing-library/user-event'
58
import * as React from 'react'
69

710
// @ts-expect-error this is not defined anywhere
811
globalThis.IS_REACT_ACT_ENVIRONMENT = false
912

13+
const userEvent = userEventWithoutAct(baseUserEvent)
14+
1015
function CounterForm({
1116
value,
1217
onIncrement,
@@ -43,14 +48,14 @@ describe('snapshotDOM', () => {
4348
},
4449
)
4550
const utils = await renderResultPromise
51+
const incrementButton = utils.getByText('Increment')
52+
await userEvent.click(incrementButton)
53+
await userEvent.click(incrementButton)
4654
{
4755
const {withinDOM} = await takeRender()
4856
const input = withinDOM().getByLabelText<HTMLInputElement>('Value')
4957
expect(input.value).toBe('0')
5058
}
51-
const incrementButton = utils.getByText('Increment')
52-
await userEvent.click(incrementButton)
53-
await userEvent.click(incrementButton)
5459
{
5560
const {withinDOM} = await takeRender()
5661
const input = withinDOM().getByLabelText<HTMLInputElement>('Value')

src/pure.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export type {SnapshotStream} from './renderHookToSnapshotStream.js'
2020
export type {Assertable} from './assertable.js'
2121

2222
export {renderWithoutAct, cleanup} from './renderStream/renderWithoutAct.js'
23+
export {userEventWithoutAct} from './useWithoutAct.js'

src/renderHookToSnapshotStream.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,18 @@ export interface SnapshotStream<Snapshot, Props> extends Assertable {
4545
unmount: () => void
4646
}
4747

48-
export function renderHookToSnapshotStream<ReturnValue, Props>(
48+
export async function renderHookToSnapshotStream<ReturnValue, Props>(
4949
renderCallback: (props: Props) => ReturnValue,
5050
{initialProps, ...renderOptions}: RenderHookOptions<Props> = {},
51-
): SnapshotStream<ReturnValue, Props> {
51+
): Promise<SnapshotStream<ReturnValue, Props>> {
5252
const {render, ...stream} = createRenderStream<{value: ReturnValue}, never>()
5353

5454
const HookComponent: React.FC<{arg: Props}> = props => {
5555
stream.replaceSnapshot({value: renderCallback(props.arg)})
5656
return null
5757
}
5858

59-
const {rerender: baseRerender, unmount} = render(
59+
const {rerender: baseRerender, unmount} = await render(
6060
<HookComponent arg={initialProps!} />,
6161
renderOptions,
6262
)

src/renderStream/__tests__/createRenderStream.test.tsx

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
/* eslint-disable @typescript-eslint/no-use-before-define */
22
import {jest, describe, test, expect} from '@jest/globals'
3-
import {createRenderStream} from '@testing-library/react-render-stream'
4-
//import {userEvent} from '@testing-library/user-event'
3+
import {
4+
createRenderStream,
5+
userEventWithoutAct,
6+
} from '@testing-library/react-render-stream'
7+
import {userEvent as baseUserEvent} from '@testing-library/user-event'
58
import * as React from 'react'
69
import {ErrorBoundary} from 'react-error-boundary'
710
import {getExpectErrorMessage} from '../../__testHelpers__/getCleanedErrorMessage.js'
811

9-
// @ts-expect-error this is not defined anywhere
10-
globalThis.IS_REACT_ACT_ENVIRONMENT = false
11-
12-
async function click(element: HTMLElement) {
13-
const opts = {bubbles: true, cancelable: true, buttons: 1}
14-
element.dispatchEvent(new Event('mousedown', opts))
15-
await new Promise(r => setTimeout(r, 50))
16-
element.dispatchEvent(new Event('mouseup', opts))
17-
element.dispatchEvent(new Event('click', opts))
18-
}
12+
const userEvent = userEventWithoutAct(baseUserEvent)
1913

2014
function CounterForm({
2115
value,
@@ -49,15 +43,15 @@ describe('snapshotDOM', () => {
4943
const {takeRender, render} = createRenderStream({
5044
snapshotDOM: true,
5145
})
52-
const utils = render(<Counter />)
46+
const utils = await render(<Counter />)
47+
const incrementButton = utils.getByText('Increment')
48+
await userEvent.click(incrementButton)
49+
await userEvent.click(incrementButton)
5350
{
5451
const {withinDOM} = await takeRender()
5552
const input = withinDOM().getByLabelText<HTMLInputElement>('Value')
5653
expect(input.value).toBe('0')
5754
}
58-
const incrementButton = utils.getByText('Increment') as HTMLElement // TODO
59-
await click(incrementButton)
60-
await click(incrementButton)
6155
{
6256
const {withinDOM} = await takeRender()
6357
// a one-off to test that `queryBy` works and accepts a type argument
@@ -82,12 +76,12 @@ describe('snapshotDOM', () => {
8276
const {takeRender, render} = createRenderStream({
8377
snapshotDOM: true,
8478
})
85-
render(<Counter />)
79+
await render(<Counter />)
8680
{
8781
const {withinDOM} = await takeRender()
8882
const snapshotIncrementButton = withinDOM().getByText('Increment')
8983
try {
90-
await click(snapshotIncrementButton)
84+
await userEvent.click(snapshotIncrementButton)
9185
} catch (error) {
9286
expect(error).toMatchInlineSnapshot(`
9387
[Error: Uncaught [Error:
@@ -114,7 +108,7 @@ describe('snapshotDOM', () => {
114108
snapshotDOM: true,
115109
queries,
116110
})
117-
render(<Component />)
111+
await render(<Component />)
118112

119113
const {withinDOM} = await takeRender()
120114
expect(withinDOM().foo()).toBe(null)
@@ -140,14 +134,14 @@ describe('replaceSnapshot', () => {
140134
const {takeRender, replaceSnapshot, render} = createRenderStream<{
141135
value: number
142136
}>()
143-
const utils = render(<Counter />)
137+
const utils = await render(<Counter />)
138+
const incrementButton = utils.getByText('Increment')
139+
await userEvent.click(incrementButton)
140+
await userEvent.click(incrementButton)
144141
{
145142
const {snapshot} = await takeRender()
146143
expect(snapshot).toEqual({value: 0})
147144
}
148-
const incrementButton = utils.getByText('Increment') as HTMLElement // TODO
149-
await click(incrementButton)
150-
await click(incrementButton)
151145
{
152146
const {snapshot} = await takeRender()
153147
expect(snapshot).toEqual({value: 1})
@@ -170,15 +164,14 @@ describe('replaceSnapshot', () => {
170164
const {takeRender, replaceSnapshot, render} = createRenderStream({
171165
initialSnapshot: {unrelatedValue: 'unrelated', value: -1},
172166
})
173-
const utils = render(<Counter />)
167+
const utils = await render(<Counter />)
168+
const incrementButton = utils.getByText('Increment')
169+
await userEvent.click(incrementButton)
170+
await userEvent.click(incrementButton)
174171
{
175172
const {snapshot} = await takeRender()
176173
expect(snapshot).toEqual({unrelatedValue: 'unrelated', value: 0})
177174
}
178-
179-
const incrementButton = utils.getByText('Increment') as HTMLElement // TODO
180-
await click(incrementButton)
181-
await click(incrementButton)
182175
{
183176
const {snapshot} = await takeRender()
184177
expect(snapshot).toEqual({unrelatedValue: 'unrelated', value: 1})
@@ -204,7 +197,7 @@ describe('replaceSnapshot', () => {
204197

205198
const spy = jest.spyOn(console, 'error')
206199
spy.mockImplementation(() => {})
207-
render(
200+
await render(
208201
<ErrorBoundary
209202
fallbackRender={({error}) => {
210203
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@@ -215,8 +208,6 @@ describe('replaceSnapshot', () => {
215208
<Counter />
216209
</ErrorBoundary>,
217210
)
218-
await new Promise(r => setTimeout(r, 10))
219-
220211
spy.mockRestore()
221212

222213
expect(caughtError!).toMatchInlineSnapshot(
@@ -244,12 +235,11 @@ describe('onRender', () => {
244235
expect(info.count).toBe(info.snapshot.value + 1)
245236
},
246237
})
247-
const utils = render(<Counter />)
238+
const utils = await render(<Counter />)
239+
const incrementButton = utils.getByText('Increment')
240+
await userEvent.click(incrementButton)
241+
await userEvent.click(incrementButton)
248242
await takeRender()
249-
250-
const incrementButton = utils.getByText('Increment') as HTMLElement // TODO
251-
await click(incrementButton)
252-
await click(incrementButton)
253243
await takeRender()
254244
await takeRender()
255245
})
@@ -268,11 +258,11 @@ describe('onRender', () => {
268258
},
269259
})
270260

271-
const utils = render(<Counter />)
261+
const utils = await render(<Counter />)
262+
const incrementButton = utils.getByText('Increment')
263+
await userEvent.click(incrementButton)
264+
await userEvent.click(incrementButton)
272265
await takeRender()
273-
const incrementButton = utils.getByText('Increment') as HTMLElement // TODO
274-
await click(incrementButton)
275-
await click(incrementButton)
276266
const error = await getExpectErrorMessage(takeRender())
277267

278268
expect(error).toMatchInlineSnapshot(`

src/renderStream/createRenderStream.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {type RenderStreamContextValue} from './context.js'
77
import {RenderStreamContextProvider} from './context.js'
88
import {disableActWarnings} from './disableActWarnings.js'
99
import {syncQueries, type Queries, type SyncQueries} from './syncQueries.js'
10-
import {renderWithoutAct} from './renderWithoutAct.js'
10+
import {renderWithoutAct, RenderWithoutActAsync} from './renderWithoutAct.js'
1111

1212
export type ValidSnapshot =
1313
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
@@ -82,7 +82,7 @@ export interface RenderStreamWithRenderFn<
8282
Snapshot extends ValidSnapshot,
8383
Q extends Queries = SyncQueries,
8484
> extends RenderStream<Snapshot, Q> {
85-
render: typeof renderWithoutAct
85+
render: RenderWithoutActAsync
8686
}
8787

8888
export type RenderStreamOptions<
@@ -248,11 +248,11 @@ export function createRenderStream<
248248
)
249249
}
250250

251-
const render = ((
251+
const render: RenderWithoutActAsync = (async (
252252
ui: React.ReactNode,
253253
options?: RenderOptions<any, any, any>,
254254
) => {
255-
return renderWithoutAct(ui, {
255+
const ret = renderWithoutAct(ui, {
256256
...options,
257257
wrapper: props => {
258258
const ParentWrapper = options?.wrapper ?? React.Fragment
@@ -263,7 +263,11 @@ export function createRenderStream<
263263
)
264264
},
265265
})
266-
}) as typeof renderWithoutAct
266+
if (stream.renders.length === 0) {
267+
await stream.waitForNextRender()
268+
}
269+
return ret
270+
}) as unknown as RenderWithoutActAsync // TODO
267271

268272
Object.assign<typeof stream, typeof stream>(stream, {
269273
replaceSnapshot,

src/renderStream/renderWithoutAct.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,29 @@ function renderRoot(
7878
} as RenderResult<Queries, any, any> // TODO clean up more
7979
}
8080

81+
export type RenderWithoutActAsync = {
82+
<
83+
Q extends Queries = SyncQueries,
84+
Container extends ReactDOMClient.Container = HTMLElement,
85+
BaseElement extends ReactDOMClient.Container = Container,
86+
>(
87+
this: any,
88+
ui: React.ReactNode,
89+
options: //Omit<
90+
RenderOptions<Q, Container, BaseElement>,
91+
//'hydrate' | 'legacyRoot' >,
92+
): Promise<RenderResult<Q, Container, BaseElement>>
93+
(
94+
this: any,
95+
ui: React.ReactNode,
96+
options?:
97+
| Omit<RenderOptions, 'hydrate' | 'legacyRoot' | 'queries'>
98+
| undefined,
99+
): Promise<
100+
RenderResult<Queries, ReactDOMClient.Container, ReactDOMClient.Container>
101+
>
102+
}
103+
81104
export function renderWithoutAct<
82105
Q extends Queries = SyncQueries,
83106
Container extends ReactDOMClient.Container = HTMLElement,

src/useWithoutAct.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {getConfig} from '@testing-library/dom'
2+
import {UserEvent} from '@testing-library/user-event'
3+
4+
type AsyncUserEvent = {
5+
[K in keyof UserEvent as UserEvent[K] extends (...args: any[]) => Promise<any>
6+
? K
7+
: never]: UserEvent[K]
8+
}
9+
10+
export function userEventWithoutAct(
11+
userEvent: UserEvent | typeof import('@testing-library/user-event').userEvent,
12+
): AsyncUserEvent {
13+
return Object.fromEntries(
14+
Object.entries(userEvent).map(([key, value]) => {
15+
if (typeof value === 'function') {
16+
return [
17+
key,
18+
async function wrapped(this: any, ...args: any[]) {
19+
const config = getConfig()
20+
// eslint-disable-next-line @typescript-eslint/unbound-method
21+
const orig = config.eventWrapper
22+
try {
23+
config.eventWrapper = cb => cb()
24+
// eslint-disable-next-line @typescript-eslint/return-await
25+
return await (value as Function).apply(this, args)
26+
} finally {
27+
config.eventWrapper = orig
28+
}
29+
},
30+
]
31+
}
32+
return [key, value]
33+
}),
34+
) as AsyncUserEvent
35+
}

0 commit comments

Comments
 (0)