diff --git a/README.md b/README.md
index 7832a32ee..b89004303 100644
--- a/README.md
+++ b/README.md
@@ -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?
@@ -36,7 +36,7 @@ test('iterate through renders with DOM snapshots', async () => {
const {takeRender, render} = createRenderStream({
snapshotDOM: true,
})
- const utils = render()
+ const utils = await render()
const incrementButton = utils.getByText('Increment')
await userEvent.click(incrementButton)
await userEvent.click(incrementButton)
@@ -58,28 +58,6 @@ 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(, options)
-```
-
-you can also call
-
-```js
-const renderStream = renderToRenderStream(, 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`
@@ -87,7 +65,7 @@ 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}) => {children},
@@ -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)
@@ -146,7 +124,7 @@ test('`useTrackRenders` with suspense', async () => {
}
const {takeRender, render} = createRenderStream()
- render()
+ await render()
{
const {renderedComponents} = await takeRender()
expect(renderedComponents).toEqual([App, LoadingComponent])
@@ -179,7 +157,7 @@ test('custom snapshots with `replaceSnapshot`', async () => {
const {takeRender, replaceSnapshot, render} = createRenderStream<{
value: number
}>()
- const utils = render()
+ const utils = await render()
const incrementButton = utils.getByText('Increment')
await userEvent.click(incrementButton)
{
@@ -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)
@@ -247,7 +223,7 @@ This library adds to matchers to `expect` that can be used like
```tsx
test('basic functionality', async () => {
- const {takeRender} = renderToRenderStream()
+ const {takeRender} = await renderToRenderStream()
await expect(takeRender).toRerender()
await takeRender()
@@ -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()
+ }
+})
+```
diff --git a/package.json b/package.json
index ca6237a4a..0c9fa678f 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/__testHelpers__/useShim.js b/src/__testHelpers__/useShim.js
index efeb62e68..115a7f93e 100644
--- a/src/__testHelpers__/useShim.js
+++ b/src/__testHelpers__/useShim.js
@@ -1,3 +1,5 @@
+import * as React from 'react'
+
/* eslint-disable default-case */
/* eslint-disable consistent-return */
function isStatefulPromise(promise) {
@@ -33,7 +35,7 @@ function wrapPromiseWithState(promise) {
* @param {Promise} promise
* @returns {T}
*/
-export function __use(promise) {
+function _use(promise) {
const statefulPromise = wrapPromiseWithState(promise)
switch (statefulPromise.status) {
case 'pending':
@@ -44,3 +46,5 @@ export function __use(promise) {
return statefulPromise.value
}
}
+
+export const __use = /** @type {{use?: typeof _use}} */ (React).use || _use
diff --git a/src/__testHelpers__/withDisabledActWarnings.ts b/src/__testHelpers__/withDisabledActWarnings.ts
deleted file mode 100644
index ee76a9411..000000000
--- a/src/__testHelpers__/withDisabledActWarnings.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {disableActWarnings} from '../renderStream/disableActWarnings.js'
-
-export function withDisabledActWarnings(cb: () => T): T {
- const disabledActWarnings = disableActWarnings()
- let result: T
- try {
- result = cb()
- return result instanceof Promise
- ? (result.finally(disabledActWarnings.cleanup) as T)
- : result
- } finally {
- disabledActWarnings.cleanup()
- }
-}
diff --git a/src/__tests__/renderHookToSnapshotStream.test.tsx b/src/__tests__/renderHookToSnapshotStream.test.tsx
index d09a84aee..108da49e8 100644
--- a/src/__tests__/renderHookToSnapshotStream.test.tsx
+++ b/src/__tests__/renderHookToSnapshotStream.test.tsx
@@ -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]
@@ -16,7 +16,7 @@ function useRerenderEvents(initialValue: unknown) {
onChange => {
const cb = (value: unknown) => {
lastValueRef.current = value
- withDisabledActWarnings(onChange)
+ onChange()
}
testEvents.addListener('rerenderWithValue', cb)
return () => {
@@ -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()
@@ -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) {
diff --git a/src/__tests__/renderToRenderStream.test.tsx b/src/__tests__/renderToRenderStream.test.tsx
deleted file mode 100644
index 195ed6ad4..000000000
--- a/src/__tests__/renderToRenderStream.test.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-/* eslint-disable @typescript-eslint/no-use-before-define */
-import {describe, test, expect} from '@jest/globals'
-import {renderToRenderStream} from '@testing-library/react-render-stream'
-import {userEvent} from '@testing-library/user-event'
-import * as React from 'react'
-function CounterForm({
- value,
- onIncrement,
-}: {
- value: number
- onIncrement: () => void
-}) {
- return (
-
- )
-}
-
-describe('snapshotDOM', () => {
- test('basic functionality', async () => {
- function Counter() {
- const [value, setValue] = React.useState(0)
- return (
- setValue(v => v + 1)} />
- )
- }
-
- const {takeRender, renderResultPromise} = renderToRenderStream(
- ,
- {
- snapshotDOM: true,
- },
- )
- const utils = await renderResultPromise
- const incrementButton = utils.getByText('Increment')
- await userEvent.click(incrementButton)
- await userEvent.click(incrementButton)
- {
- const {withinDOM} = await takeRender()
- const input = withinDOM().getByLabelText('Value')
- expect(input.value).toBe('0')
- }
- {
- const {withinDOM} = await takeRender()
- const input = withinDOM().getByLabelText('Value')
- expect(input.value).toBe('1')
- }
- {
- const {withinDOM} = await takeRender()
- const input = withinDOM().getByLabelText('Value')
- expect(input.value).toBe('2')
- }
- })
-
- test('queries option', async () => {
- function Component() {
- return null
- }
- const queries = {
- foo: (_: any) => {
- return null
- },
- }
- const {takeRender, renderResultPromise} = renderToRenderStream(
- ,
- {
- queries,
- snapshotDOM: true,
- },
- )
- const utils = await renderResultPromise
- expect(utils.foo()).toBe(null)
- const {withinDOM} = await takeRender()
- expect(withinDOM().foo()).toBe(null)
- function _typeTest() {
- // @ts-expect-error should not be present
- utils.getByText
- // @ts-expect-error should not be present
- withinDOM().getByText
- utils.debug()
- withinDOM().debug()
- const _str: string = withinDOM().logTestingPlaygroundURL()
- }
- })
-})
-
-// for more tests, see the `createRenderStream` test suite, as `renderToRenderStream` is just a wrapper around that
diff --git a/src/disableActEnvironment.ts b/src/disableActEnvironment.ts
new file mode 100644
index 000000000..08503c29c
--- /dev/null
+++ b/src/disableActEnvironment.ts
@@ -0,0 +1,171 @@
+import {getConfig} from '@testing-library/dom'
+
+const dispose: typeof Symbol.dispose =
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ Symbol.dispose ?? Symbol.for('nodejs.dispose')
+
+export interface DisableActEnvironmentOptions {
+ /**
+ * If `true`, all modifications of values set by `disableActEnvironment`
+ * will be prevented until `cleanup` is called.
+ *
+ * @default true
+ */
+ preventModification?: boolean
+
+ /**
+ * If `true`, will change the configuration of the testing library to
+ * prevent auto-wrapping e.g. `userEvent` calls in `act`.
+ *
+ * @default true
+ */
+ adjustTestingLibConfig?: boolean
+}
+
+/**
+ * Helper to temporarily disable a React 18+ act environment.
+ *
+ * By default, this also adjusts the configuration of @testing-library/dom
+ * to prevent auto-wrapping of user events in `act`, as well as preventing
+ * all modifications of values set by this method until `cleanup` is called
+ * or the returned `Disposable` is disposed of.
+ *
+ * Both of these behaviors can be disabled with the option, of the defaults
+ * can be changed for all calls to this method by modifying
+ * `disableActEnvironment.defaultOptions`.
+ *
+ * This returns a disposable and can be used in combination with `using` to
+ * automatically restore the state from before this method call after your test.
+ *
+ * @example
+ * ```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 can not use the explicit resouce management keyword `using`,
+ * you can also manually call `cleanup`:
+ *
+ * @example
+ * ```ts
+ * test("my test", () => {
+ * const { cleanup } = disableActEnvironment();
+ *
+ * try {
+ * // your test code here
+ * } finally {
+ * cleanup();
+ * }
+ * })
+ * ```
+ *
+ * For more context on what `act` is and why you shouldn't use it in renderStream tests,
+ * https://github.com/reactwg/react-18/discussions/102 is probably the best resource we have.
+ */
+export function disableActEnvironment({
+ preventModification = disableActEnvironment.defaultOptions
+ .preventModification,
+ adjustTestingLibConfig = disableActEnvironment.defaultOptions
+ .adjustTestingLibConfig,
+}: DisableActEnvironmentOptions = {}): {cleanup: () => void} & Disposable {
+ const typedGlobal = globalThis as any as {IS_REACT_ACT_ENVIRONMENT?: boolean}
+ const cleanupFns: Array<() => void> = []
+
+ // core functionality
+ {
+ const previous = typedGlobal.IS_REACT_ACT_ENVIRONMENT
+ cleanupFns.push(() => {
+ Object.defineProperty(typedGlobal, 'IS_REACT_ACT_ENVIRONMENT', {
+ value: previous,
+ writable: true,
+ configurable: true,
+ })
+ })
+ Object.defineProperty(
+ typedGlobal,
+ 'IS_REACT_ACT_ENVIRONMENT',
+ getNewPropertyDescriptor(false, preventModification),
+ )
+ }
+
+ if (adjustTestingLibConfig) {
+ const config = getConfig()
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ const {asyncWrapper, eventWrapper} = config
+ cleanupFns.push(() => {
+ Object.defineProperty(config, 'asyncWrapper', {
+ value: asyncWrapper,
+ writable: true,
+ configurable: true,
+ })
+ Object.defineProperty(config, 'eventWrapper', {
+ value: eventWrapper,
+ writable: true,
+ configurable: true,
+ })
+ })
+
+ Object.defineProperty(
+ config,
+ 'asyncWrapper',
+ getNewPropertyDescriptor(
+ fn => fn(),
+ preventModification,
+ ),
+ )
+ Object.defineProperty(
+ config,
+ 'eventWrapper',
+ getNewPropertyDescriptor(
+ fn => fn(),
+ preventModification,
+ ),
+ )
+ }
+
+ function cleanup() {
+ while (cleanupFns.length > 0) {
+ cleanupFns.pop()!()
+ }
+ }
+ return {
+ cleanup,
+ [dispose]: cleanup,
+ }
+}
+
+/**
+ * Default options for `disableActEnvironment`.
+ *
+ * This can be modified to change the default options for all calls to `disableActEnvironment`.
+ */
+disableActEnvironment.defaultOptions = {
+ preventModification: true,
+ adjustTestingLibConfig: true,
+} satisfies Required as Required
+
+function getNewPropertyDescriptor(
+ value: T,
+ preventModification: boolean,
+): PropertyDescriptor {
+ return preventModification
+ ? {
+ configurable: true,
+ enumerable: true,
+ get() {
+ return value
+ },
+ set() {},
+ }
+ : {
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ value,
+ }
+}
diff --git a/src/expect/__tests__/renderStreamMatchers.test.tsx b/src/expect/__tests__/renderStreamMatchers.test.tsx
index 6466df652..ea19ced1b 100644
--- a/src/expect/__tests__/renderStreamMatchers.test.tsx
+++ b/src/expect/__tests__/renderStreamMatchers.test.tsx
@@ -7,7 +7,6 @@ import {
} from '@testing-library/react-render-stream'
import * as React from 'react'
import {getExpectErrorMessage} from '../../__testHelpers__/getCleanedErrorMessage.js'
-import {withDisabledActWarnings} from '../../__testHelpers__/withDisabledActWarnings.js'
const testEvents = new EventEmitter<{
rerender: []
@@ -16,7 +15,7 @@ const testEvents = new EventEmitter<{
function useRerender() {
const [, rerender] = React.useReducer(c => c + 1, 0)
React.useEffect(() => {
- const cb = () => void withDisabledActWarnings(rerender)
+ const cb = () => void rerender()
testEvents.addListener('rerender', cb)
return () => {
@@ -34,7 +33,7 @@ describe('toRerender', () => {
test('basic functionality', async () => {
const {takeRender, render} = createRenderStream({})
- render()
+ await render()
await expect(takeRender).toRerender()
await takeRender()
@@ -48,7 +47,7 @@ describe('toRerender', () => {
test('works with renderStream object', async () => {
const renderStream = createRenderStream({})
- renderStream.render()
+ await renderStream.render()
await expect(renderStream).toRerender()
await renderStream.takeRender()
@@ -60,7 +59,7 @@ describe('toRerender', () => {
})
test('works with takeSnapshot function', async () => {
- const {takeSnapshot} = renderHookToSnapshotStream(() => useRerender())
+ const {takeSnapshot} = await renderHookToSnapshotStream(() => useRerender())
await expect(takeSnapshot).toRerender()
await takeSnapshot()
@@ -73,7 +72,7 @@ describe('toRerender', () => {
})
test('works with snapshotStream', async () => {
- const snapshotStream = renderHookToSnapshotStream(() => useRerender())
+ const snapshotStream = await renderHookToSnapshotStream(() => useRerender())
await expect(snapshotStream).toRerender()
await snapshotStream.takeSnapshot()
@@ -88,7 +87,7 @@ describe('toRerender', () => {
test("errors when it rerenders, but shouldn't", async () => {
const {takeRender, render} = createRenderStream({})
- render()
+ await render()
await expect(takeRender).toRerender()
await takeRender()
@@ -106,7 +105,7 @@ Expected component to not rerender, but it did.
test("errors when it should rerender, but doesn't", async () => {
const {takeRender, render} = createRenderStream({})
- render()
+ await render()
await expect(takeRender).toRerender()
await takeRender()
@@ -123,7 +122,7 @@ describe('toRenderExactlyTimes', () => {
test('basic functionality', async () => {
const {takeRender, render} = createRenderStream({})
- render()
+ await render()
testEvents.emit('rerender')
await expect(takeRender).toRenderExactlyTimes(2)
@@ -132,21 +131,21 @@ describe('toRenderExactlyTimes', () => {
test('works with renderStream object', async () => {
const renderStream = createRenderStream({})
- renderStream.render()
+ await renderStream.render()
testEvents.emit('rerender')
await expect(renderStream).toRenderExactlyTimes(2)
})
test('works with takeSnapshot function', async () => {
- const {takeSnapshot} = renderHookToSnapshotStream(() => useRerender())
+ const {takeSnapshot} = await renderHookToSnapshotStream(() => useRerender())
testEvents.emit('rerender')
await expect(takeSnapshot).toRenderExactlyTimes(2)
})
test('works with snapshotStream', async () => {
- const snapshotStream = renderHookToSnapshotStream(() => useRerender())
+ const snapshotStream = await renderHookToSnapshotStream(() => useRerender())
testEvents.emit('rerender')
await expect(snapshotStream).toRenderExactlyTimes(2)
@@ -155,7 +154,7 @@ describe('toRenderExactlyTimes', () => {
test('errors when the count of rerenders is wrong', async () => {
const {takeRender, render} = createRenderStream({})
- render()
+ await render()
testEvents.emit('rerender')
const error = await getExpectErrorMessage(
@@ -172,7 +171,7 @@ It rendered 2 times.
test('errors when the count of rerenders is right (inverted)', async () => {
const {takeRender, render} = createRenderStream({})
- render()
+ await render()
testEvents.emit('rerender')
const error = await getExpectErrorMessage(
diff --git a/src/index.ts b/src/index.ts
index 9f3c829af..f172c2f3f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,2 +1,8 @@
import '@testing-library/react-render-stream/expect'
+import {cleanup} from '@testing-library/react-render-stream/pure'
export * from '@testing-library/react-render-stream/pure'
+
+const global = globalThis as {afterEach?: (fn: () => void) => void}
+if (global.afterEach) {
+ global.afterEach(cleanup)
+}
diff --git a/src/pure.ts b/src/pure.ts
index 9b2c0fca4..7e1dce71f 100644
--- a/src/pure.ts
+++ b/src/pure.ts
@@ -12,9 +12,16 @@ export {useTrackRenders} from './renderStream/useTrackRenders.js'
export type {SyncScreen} from './renderStream/Render.js'
-export {renderToRenderStream} from './renderToRenderStream.js'
-export type {RenderStreamWithRenderResult} from './renderToRenderStream.js'
export {renderHookToSnapshotStream} from './renderHookToSnapshotStream.js'
export type {SnapshotStream} from './renderHookToSnapshotStream.js'
export type {Assertable} from './assertable.js'
+
+export {
+ cleanup,
+ type RenderWithoutActAsync as AsyncRenderFn,
+} from './renderWithoutAct.js'
+export {
+ disableActEnvironment,
+ type DisableActEnvironmentOptions,
+} from './disableActEnvironment.js'
diff --git a/src/renderHookToSnapshotStream.tsx b/src/renderHookToSnapshotStream.tsx
index 3d8f6edb7..ecf62d382 100644
--- a/src/renderHookToSnapshotStream.tsx
+++ b/src/renderHookToSnapshotStream.tsx
@@ -1,4 +1,4 @@
-import {RenderHookOptions} from '@testing-library/react'
+import {type RenderHookOptions} from '@testing-library/react/pure.js'
import React from 'rehackt'
import {createRenderStream} from './renderStream/createRenderStream.js'
import {type NextRenderOptions} from './renderStream/createRenderStream.js'
@@ -41,14 +41,14 @@ export interface SnapshotStream extends Assertable {
* Does not advance the render iterator.
*/
waitForNextSnapshot(options?: NextRenderOptions): Promise
- rerender: (rerenderCallbackProps: Props) => void
+ rerender: (rerenderCallbackProps: Props) => Promise
unmount: () => void
}
-export function renderHookToSnapshotStream(
+export async function renderHookToSnapshotStream(
renderCallback: (props: Props) => ReturnValue,
{initialProps, ...renderOptions}: RenderHookOptions = {},
-): SnapshotStream {
+): Promise> {
const {render, ...stream} = createRenderStream<{value: ReturnValue}, never>()
const HookComponent: React.FC<{arg: Props}> = props => {
@@ -56,7 +56,7 @@ export function renderHookToSnapshotStream(
return null
}
- const {rerender: baseRerender, unmount} = render(
+ const {rerender: baseRerender, unmount} = await render(
,
renderOptions,
)
diff --git a/src/renderStream/__tests__/createRenderStream.test.tsx b/src/renderStream/__tests__/createRenderStream.test.tsx
index c7f0908cf..a15a52919 100644
--- a/src/renderStream/__tests__/createRenderStream.test.tsx
+++ b/src/renderStream/__tests__/createRenderStream.test.tsx
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import {jest, describe, test, expect} from '@jest/globals'
import {createRenderStream} from '@testing-library/react-render-stream'
-import {userEvent} from '@testing-library/user-event'
import * as React from 'react'
import {ErrorBoundary} from 'react-error-boundary'
+import {userEvent} from '@testing-library/user-event'
import {getExpectErrorMessage} from '../../__testHelpers__/getCleanedErrorMessage.js'
function CounterForm({
@@ -38,7 +38,7 @@ describe('snapshotDOM', () => {
const {takeRender, render} = createRenderStream({
snapshotDOM: true,
})
- const utils = render()
+ const utils = await render()
const incrementButton = utils.getByText('Increment')
await userEvent.click(incrementButton)
await userEvent.click(incrementButton)
@@ -71,7 +71,7 @@ describe('snapshotDOM', () => {
const {takeRender, render} = createRenderStream({
snapshotDOM: true,
})
- render()
+ await render()
{
const {withinDOM} = await takeRender()
const snapshotIncrementButton = withinDOM().getByText('Increment')
@@ -103,7 +103,7 @@ describe('snapshotDOM', () => {
snapshotDOM: true,
queries,
})
- render()
+ await render()
const {withinDOM} = await takeRender()
expect(withinDOM().foo()).toBe(null)
@@ -129,7 +129,7 @@ describe('replaceSnapshot', () => {
const {takeRender, replaceSnapshot, render} = createRenderStream<{
value: number
}>()
- const utils = render()
+ const utils = await render()
const incrementButton = utils.getByText('Increment')
await userEvent.click(incrementButton)
await userEvent.click(incrementButton)
@@ -159,7 +159,7 @@ describe('replaceSnapshot', () => {
const {takeRender, replaceSnapshot, render} = createRenderStream({
initialSnapshot: {unrelatedValue: 'unrelated', value: -1},
})
- const utils = render()
+ const utils = await render()
const incrementButton = utils.getByText('Increment')
await userEvent.click(incrementButton)
await userEvent.click(incrementButton)
@@ -192,7 +192,7 @@ describe('replaceSnapshot', () => {
const spy = jest.spyOn(console, 'error')
spy.mockImplementation(() => {})
- render(
+ await render(
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@@ -230,7 +230,7 @@ describe('onRender', () => {
expect(info.count).toBe(info.snapshot.value + 1)
},
})
- const utils = render()
+ const utils = await render()
const incrementButton = utils.getByText('Increment')
await userEvent.click(incrementButton)
await userEvent.click(incrementButton)
@@ -253,7 +253,7 @@ describe('onRender', () => {
},
})
- const utils = render()
+ const utils = await render()
const incrementButton = utils.getByText('Increment')
await userEvent.click(incrementButton)
await userEvent.click(incrementButton)
@@ -267,4 +267,18 @@ Expected: 1
Received: 2
`)
})
+
+ test('returned `rerender` returns a promise that resolves', async () => {
+ function Component() {
+ return null
+ }
+
+ const {takeRender, render} = createRenderStream()
+ const {rerender} = await render()
+ await takeRender()
+ const promise: Promise = rerender()
+ expect(promise).toBeInstanceOf(Promise)
+ await promise
+ await takeRender()
+ })
})
diff --git a/src/renderStream/__tests__/useTrackRenders.test.tsx b/src/renderStream/__tests__/useTrackRenders.test.tsx
index 97b8a3488..c05c90ba6 100644
--- a/src/renderStream/__tests__/useTrackRenders.test.tsx
+++ b/src/renderStream/__tests__/useTrackRenders.test.tsx
@@ -17,6 +17,9 @@ describe('non-suspense use cases', () => {
let asyncAction = Promise.withResolvers()
beforeEach(() => {
asyncAction = Promise.withResolvers()
+ void asyncAction.promise.catch(() => {
+ /* avoid uncaught promise rejection */
+ })
})
function ErrorComponent() {
useTrackRenders()
@@ -61,7 +64,7 @@ describe('non-suspense use cases', () => {
test('basic functionality', async () => {
const {takeRender, render} = createRenderStream()
- render()
+ await render()
asyncAction.resolve('data')
{
const {renderedComponents} = await takeRender()
@@ -75,7 +78,7 @@ describe('non-suspense use cases', () => {
test('error path', async () => {
const {takeRender, render} = createRenderStream()
- render()
+ await render()
asyncAction.reject(new Error('error'))
{
const {renderedComponents} = await takeRender()
@@ -119,7 +122,7 @@ describe('suspense use cases', () => {
test('basic functionality', async () => {
const {takeRender, render} = createRenderStream()
- render()
+ await render()
asyncAction.resolve('data')
{
const {renderedComponents} = await takeRender()
@@ -133,7 +136,7 @@ describe('suspense use cases', () => {
test('ErrorBoundary', async () => {
const {takeRender, render} = createRenderStream()
- render()
+ await render()
const spy = jest.spyOn(console, 'error')
spy.mockImplementation(() => {})
@@ -156,7 +159,7 @@ test('specifying the `name` option', async () => {
return <>{children}>
}
const {takeRender, render} = createRenderStream()
- render(
+ await render(
<>
diff --git a/src/renderStream/createRenderStream.tsx b/src/renderStream/createRenderStream.tsx
index 2c87ffe46..e6518146d 100644
--- a/src/renderStream/createRenderStream.tsx
+++ b/src/renderStream/createRenderStream.tsx
@@ -1,11 +1,14 @@
import * as React from 'rehackt'
-import {render as baseRender, RenderOptions} from '@testing-library/react'
+import {type RenderOptions} from '@testing-library/react/pure.js'
import {Assertable, markAssertable} from '../assertable.js'
+import {
+ renderWithoutAct,
+ type RenderWithoutActAsync,
+} from '../renderWithoutAct.js'
import {RenderInstance, type Render, type BaseRender} from './Render.js'
import {type RenderStreamContextValue} from './context.js'
import {RenderStreamContextProvider} from './context.js'
-import {disableActWarnings} from './disableActWarnings.js'
import {syncQueries, type Queries, type SyncQueries} from './syncQueries.js'
export type ValidSnapshot =
@@ -81,7 +84,7 @@ export interface RenderStreamWithRenderFn<
Snapshot extends ValidSnapshot,
Q extends Queries = SyncQueries,
> extends RenderStream {
- render: typeof baseRender
+ render: RenderWithoutActAsync
}
export type RenderStreamOptions<
@@ -247,11 +250,11 @@ export function createRenderStream<
)
}
- const render = ((
+ const render: RenderWithoutActAsync = (async (
ui: React.ReactNode,
options?: RenderOptions,
) => {
- return baseRender(ui, {
+ const ret = await renderWithoutAct(ui, {
...options,
wrapper: props => {
const ParentWrapper = options?.wrapper ?? React.Fragment
@@ -262,7 +265,24 @@ export function createRenderStream<
)
},
})
- }) as typeof baseRender
+ if (stream.renders.length === 0) {
+ await stream.waitForNextRender()
+ }
+ const origRerender = ret.rerender
+ ret.rerender = async function rerender(rerenderUi: React.ReactNode) {
+ const previousRenderCount = stream.renders.length
+ try {
+ return await origRerender(rerenderUi)
+ } finally {
+ // only wait for the next render if the rerender was not
+ // synchronous (React 17)
+ if (previousRenderCount === stream.renders.length) {
+ await stream.waitForNextRender()
+ }
+ }
+ }
+ return ret
+ }) as unknown as RenderWithoutActAsync // TODO
Object.assign(stream, {
replaceSnapshot,
@@ -275,27 +295,31 @@ export function createRenderStream<
return stream.renders.length
},
async peekRender(options: NextRenderOptions = {}) {
- if (iteratorPosition < stream.renders.length) {
- const peekedRender = stream.renders[iteratorPosition]
+ try {
+ if (iteratorPosition < stream.renders.length) {
+ const peekedRender = stream.renders[iteratorPosition]
- if (peekedRender.phase === 'snapshotError') {
- throw peekedRender.error
- }
+ if (peekedRender.phase === 'snapshotError') {
+ throw peekedRender.error
+ }
- return peekedRender
+ return peekedRender
+ }
+ return await stream
+ .waitForNextRender(options)
+ .catch(rethrowWithCapturedStackTrace(stream.peekRender))
+ } finally {
+ /** drain microtask queue */
+ await new Promise(resolve => {
+ setTimeout(() => {
+ resolve()
+ }, 0)
+ })
}
- return stream
- .waitForNextRender(options)
- .catch(rethrowWithCapturedStackTrace(stream.peekRender))
},
takeRender: markAssertable(async function takeRender(
options: NextRenderOptions = {},
) {
- // In many cases we do not control the resolution of the suspended
- // promise which results in noisy tests when the profiler due to
- // repeated act warnings.
- const disabledActWarnings = disableActWarnings()
-
let error: unknown
try {
@@ -312,7 +336,6 @@ export function createRenderStream<
if (!(error && error instanceof WaitForRenderTimeoutError)) {
iteratorPosition++
}
- disabledActWarnings.cleanup()
}
}, stream),
getCurrentRender() {
diff --git a/src/renderStream/disableActWarnings.ts b/src/renderStream/disableActWarnings.ts
deleted file mode 100644
index 99b8cd65e..000000000
--- a/src/renderStream/disableActWarnings.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Temporarily disable act warnings.
- *
- * https://github.com/reactwg/react-18/discussions/102
- */
-export function disableActWarnings() {
- const anyThis = globalThis as any as {IS_REACT_ACT_ENVIRONMENT?: boolean}
- const prevActEnv = anyThis.IS_REACT_ACT_ENVIRONMENT
- anyThis.IS_REACT_ACT_ENVIRONMENT = false
-
- return {
- cleanup: () => {
- anyThis.IS_REACT_ACT_ENVIRONMENT = prevActEnv
- },
- }
-}
diff --git a/src/renderToRenderStream.ts b/src/renderToRenderStream.ts
deleted file mode 100644
index 9692918f3..000000000
--- a/src/renderToRenderStream.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import {
- Queries,
- type RenderOptions as BaseOptions,
- type RenderResult as BaseResult,
-} from '@testing-library/react'
-import {
- createRenderStream,
- type RenderStreamOptions,
- type RenderStream,
- type ValidSnapshot,
-} from './renderStream/createRenderStream.js'
-import {SyncQueries} from './renderStream/syncQueries.js'
-
-type RenderOptions<
- Snapshot extends ValidSnapshot = void,
- Q extends Queries = SyncQueries,
-> = BaseOptions & RenderStreamOptions
-
-export interface RenderStreamWithRenderResult<
- Snapshot extends ValidSnapshot = void,
- Q extends Queries = SyncQueries,
-> extends RenderStream {
- renderResultPromise: Promise>
-}
-
-/**
- * Render into a container which is appended to document.body. It should be used with cleanup.
- */
-export function renderToRenderStream<
- Snapshot extends ValidSnapshot = void,
- Q extends Queries = SyncQueries,
->(
- ui: React.ReactNode,
- {
- onRender,
- snapshotDOM,
- initialSnapshot,
- skipNonTrackingRenders,
- queries,
- ...options
- }: RenderOptions = {},
-): RenderStreamWithRenderResult {
- const {render, ...stream} = createRenderStream({
- onRender,
- snapshotDOM,
- initialSnapshot,
- skipNonTrackingRenders,
- queries,
- })
- // `render` needs to be called asynchronously here, because the definition of `ui`
- // might contain components that reference the return value of `renderToRenderStream`
- // itself, e.g. `replaceSnapshot` or `mergeSnapshot`.
- const renderResultPromise = Promise.resolve().then(() =>
- render(ui, {...options, queries}),
- )
- return {...stream, renderResultPromise}
-}
diff --git a/src/renderWithoutAct.tsx b/src/renderWithoutAct.tsx
new file mode 100644
index 000000000..26e9d3cb5
--- /dev/null
+++ b/src/renderWithoutAct.tsx
@@ -0,0 +1,250 @@
+import * as ReactDOMClient from 'react-dom/client'
+import * as ReactDOM from 'react-dom'
+import {type RenderOptions} from '@testing-library/react/pure.js'
+import {
+ BoundFunction,
+ getQueriesForElement,
+ prettyDOM,
+ prettyFormat,
+ type Queries,
+} from '@testing-library/dom'
+import React from 'react'
+import {SyncQueries} from './renderStream/syncQueries.js'
+import {
+ disableActEnvironment,
+ DisableActEnvironmentOptions,
+} from './disableActEnvironment.js'
+
+// Ideally we'd just use a WeakMap where containers are keys and roots are values.
+// We use two variables so that we can bail out in constant time when we render with a new container (most common use case)
+
+const mountedContainers: Set = new Set()
+const mountedRootEntries: Array<{
+ container: import('react-dom').Container
+ root: ReturnType
+}> = []
+
+export type AsyncRenderResult<
+ Q extends Queries = SyncQueries,
+ Container extends ReactDOMClient.Container = HTMLElement,
+ BaseElement extends ReactDOMClient.Container = Container,
+> = {
+ container: Container
+ baseElement: BaseElement
+ debug: (
+ baseElement?:
+ | ReactDOMClient.Container
+ | Array
+ | undefined,
+ maxLength?: number | undefined,
+ options?: prettyFormat.OptionsReceived | undefined,
+ ) => void
+ rerender: (rerenderUi: React.ReactNode) => Promise
+ unmount: () => void
+ asFragment: () => DocumentFragment
+} & {[P in keyof Q]: BoundFunction}
+
+function renderRoot(
+ ui: React.ReactNode,
+ {
+ baseElement,
+ container,
+ queries,
+ wrapper: WrapperComponent,
+ root,
+ }: Pick, 'queries' | 'wrapper'> & {
+ baseElement: ReactDOMClient.Container
+ container: ReactDOMClient.Container
+ root: ReturnType
+ },
+): AsyncRenderResult<{}, any, any> {
+ root.render(
+ WrapperComponent ? React.createElement(WrapperComponent, null, ui) : ui,
+ )
+
+ return {
+ container,
+ baseElement,
+ debug: (el = baseElement, maxLength, options) =>
+ Array.isArray(el)
+ ? // eslint-disable-next-line no-console
+ el.forEach(e =>
+ console.log(prettyDOM(e as Element, maxLength, options)),
+ )
+ : // eslint-disable-next-line no-console,
+ console.log(prettyDOM(el as Element, maxLength, options)),
+ unmount: () => {
+ root.unmount()
+ },
+ rerender: async rerenderUi => {
+ renderRoot(rerenderUi, {
+ container,
+ baseElement,
+ root,
+ wrapper: WrapperComponent,
+ })
+ // Intentionally do not return anything to avoid unnecessarily complicating the API.
+ // folks can use all the same utilities we return in the first place that are bound to the container
+ },
+ asFragment: () => {
+ /* istanbul ignore else (old jsdom limitation) */
+ if (typeof document.createRange === 'function') {
+ return document
+ .createRange()
+ .createContextualFragment((container as HTMLElement).innerHTML)
+ } else {
+ const template = document.createElement('template')
+ template.innerHTML = (container as HTMLElement).innerHTML
+ return template.content
+ }
+ },
+ ...getQueriesForElement(baseElement as HTMLElement, queries),
+ }
+}
+
+export type RenderWithoutActAsync = {
+ <
+ Q extends Queries = SyncQueries,
+ Container extends ReactDOMClient.Container = HTMLElement,
+ BaseElement extends ReactDOMClient.Container = Container,
+ >(
+ this: any,
+ ui: React.ReactNode,
+ options: Pick<
+ RenderOptions,
+ 'container' | 'baseElement' | 'queries' | 'wrapper'
+ >,
+ ): Promise>
+ (
+ this: any,
+ ui: React.ReactNode,
+ options?:
+ | Pick
+ | undefined,
+ ): Promise<
+ AsyncRenderResult<
+ SyncQueries,
+ ReactDOMClient.Container,
+ ReactDOMClient.Container
+ >
+ >
+}
+
+export const renderWithoutAct =
+ _renderWithoutAct as unknown as RenderWithoutActAsync
+
+async function _renderWithoutAct(
+ ui: React.ReactNode,
+ {
+ container,
+ baseElement = container,
+ queries,
+ wrapper,
+ }: Pick<
+ RenderOptions,
+ 'container' | 'baseElement' | 'wrapper' | 'queries'
+ > = {},
+): Promise> {
+ if (!baseElement) {
+ // default to document.body instead of documentElement to avoid output of potentially-large
+ // head elements (such as JSS style blocks) in debug output
+ baseElement = document.body
+ }
+ if (!container) {
+ container = baseElement.appendChild(document.createElement('div'))
+ }
+
+ let root: ReturnType
+ // eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first.
+ if (!mountedContainers.has(container)) {
+ root = (
+ ReactDOM.version.startsWith('16') || ReactDOM.version.startsWith('17')
+ ? createLegacyRoot
+ : createConcurrentRoot
+ )(container)
+ mountedRootEntries.push({container, root})
+ // we'll add it to the mounted containers regardless of whether it's actually
+ // added to document.body so the cleanup method works regardless of whether
+ // they're passing us a custom container or not.
+ mountedContainers.add(container)
+ } else {
+ mountedRootEntries.forEach(rootEntry => {
+ // Else is unreachable since `mountedContainers` has the `container`.
+ // Only reachable if one would accidentally add the container to `mountedContainers` but not the root to `mountedRootEntries`
+ /* istanbul ignore else */
+ if (rootEntry.container === container) {
+ root = rootEntry.root
+ }
+ })
+ }
+
+ return renderRoot(ui, {
+ baseElement,
+ container,
+ queries,
+ wrapper,
+ root: root!,
+ })
+}
+
+function createLegacyRoot(container: ReactDOMClient.Container) {
+ return {
+ render(element: React.ReactNode) {
+ ReactDOM.render(element as unknown as React.ReactElement, container)
+ },
+ unmount() {
+ ReactDOM.unmountComponentAtNode(container)
+ },
+ }
+}
+
+function createConcurrentRoot(container: ReactDOMClient.Container) {
+ const anyThis = globalThis as any as {IS_REACT_ACT_ENVIRONMENT?: boolean}
+ if (anyThis.IS_REACT_ACT_ENVIRONMENT) {
+ throw new Error(`Tried to create a React root for a render stream inside a React act environment.
+This is not supported. Please use \`disableActEnvironment\` to disable the act environment for this test.`)
+ }
+ const root = ReactDOMClient.createRoot(container)
+
+ return {
+ render(element: React.ReactNode) {
+ if (anyThis.IS_REACT_ACT_ENVIRONMENT) {
+ throw new Error(`Tried to render a render stream inside a React act environment.
+ This is not supported. Please use \`disableActEnvironment\` to disable the act environment for this test.`)
+ }
+ root.render(element)
+ },
+ unmount() {
+ root.unmount()
+ },
+ }
+}
+
+export function cleanup() {
+ if (!mountedRootEntries.length) {
+ // nothing to clean up
+ return
+ }
+
+ // there is a good chance this happens outside of a test, where the user
+ // has no control over enabling or disabling the React Act environment,
+ // so we do it for them here.
+
+ const disabledAct = disableActEnvironment({
+ preventModification: false,
+ adjustTestingLibConfig: false,
+ } satisfies /* ensure that all possible options are passed here in case we add more in the future */ Required)
+ try {
+ for (const {root, container} of mountedRootEntries) {
+ root.unmount()
+
+ if (container.parentNode === document.body) {
+ document.body.removeChild(container)
+ }
+ }
+ mountedRootEntries.length = 0
+ mountedContainers.clear()
+ } finally {
+ disabledAct.cleanup()
+ }
+}
diff --git a/tests/setup-env.js b/tests/setup-env.js
index 24c394c47..d3ba8c0ab 100644
--- a/tests/setup-env.js
+++ b/tests/setup-env.js
@@ -1 +1,15 @@
import './polyfill.js'
+
+Object.defineProperty(global, 'IS_REACT_ACT_ENVIRONMENT', {
+ get() {
+ return false
+ },
+ set(value) {
+ if (!!value) {
+ throw new Error(
+ 'Cannot set IS_REACT_ACT_ENVIRONMENT to true, this probably pulled in some RTL dependency?',
+ )
+ }
+ },
+ configurable: true,
+})
diff --git a/yarn.lock b/yarn.lock
index 28d19d6f1..9f9465c0c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2634,8 +2634,8 @@ __metadata:
pkg-pr-new: "npm:^0.0.29"
prettier: "npm:^3.3.3"
publint: "npm:^0.2.11"
- react: "npm:^18.3.1"
- react-dom: "npm:^18.3.1"
+ react: "npm:19.0.0-rc.1"
+ react-dom: "npm:19.0.0-rc.1"
react-error-boundary: "npm:^4.0.13"
rehackt: "npm:^0.1.0"
ts-jest-resolver: "npm:^2.0.1"
@@ -7769,7 +7769,7 @@ __metadata:
languageName: node
linkType: hard
-"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
+"loose-envify@npm:^1.4.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
@@ -9169,15 +9169,14 @@ __metadata:
languageName: node
linkType: hard
-"react-dom@npm:^18.3.1":
- version: 18.3.1
- resolution: "react-dom@npm:18.3.1"
+"react-dom@npm:19.0.0-rc.1":
+ version: 19.0.0-rc.1
+ resolution: "react-dom@npm:19.0.0-rc.1"
dependencies:
- loose-envify: "npm:^1.1.0"
- scheduler: "npm:^0.23.2"
+ scheduler: "npm:0.25.0-rc.1"
peerDependencies:
- react: ^18.3.1
- checksum: 10c0/a752496c1941f958f2e8ac56239172296fcddce1365ce45222d04a1947e0cc5547df3e8447f855a81d6d39f008d7c32eab43db3712077f09e3f67c4874973e85
+ react: 19.0.0-rc.1
+ checksum: 10c0/26fba423f41c8b3c7a47278ea490146506333175028e111e49eb39310110b6bda956cc772c77412abc8a7f28244eea80ebade15ec5b4382d79e2ac1b39bd2d0c
languageName: node
linkType: hard
@@ -9213,12 +9212,10 @@ __metadata:
languageName: node
linkType: hard
-"react@npm:^18.3.1":
- version: 18.3.1
- resolution: "react@npm:18.3.1"
- dependencies:
- loose-envify: "npm:^1.1.0"
- checksum: 10c0/283e8c5efcf37802c9d1ce767f302dd569dd97a70d9bb8c7be79a789b9902451e0d16334b05d73299b20f048cbc3c7d288bbbde10b701fa194e2089c237dbea3
+"react@npm:19.0.0-rc.1":
+ version: 19.0.0-rc.1
+ resolution: "react@npm:19.0.0-rc.1"
+ checksum: 10c0/8f453ee0ff05ec4b11701cf4240fb0039217e73f576d2a9b871a91e34ff17d66e16294ebd0fc72c237918a3a997cc42f0328c860bc0a2cfa18e6a41f49a1c871
languageName: node
linkType: hard
@@ -9712,12 +9709,10 @@ __metadata:
languageName: node
linkType: hard
-"scheduler@npm:^0.23.2":
- version: 0.23.2
- resolution: "scheduler@npm:0.23.2"
- dependencies:
- loose-envify: "npm:^1.1.0"
- checksum: 10c0/26383305e249651d4c58e6705d5f8425f153211aef95f15161c151f7b8de885f24751b377e4a0b3dd42cce09aad3f87a61dab7636859c0d89b7daf1a1e2a5c78
+"scheduler@npm:0.25.0-rc.1":
+ version: 0.25.0-rc.1
+ resolution: "scheduler@npm:0.25.0-rc.1"
+ checksum: 10c0/dd4549eeb54cf3019c04257c622c4bbee12ef99dc547c4a96e1d5da8985ae44534111275ebcc4fea84b0d1299197b69071f912928adc5da4cab1e8168d09a44d
languageName: node
linkType: hard