From e0c9b2f921ae8188b465fa009ad0c4b052262cce Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 4 Dec 2024 10:13:02 +0100 Subject: [PATCH 1/2] feat: necessary api changes for React 19 compatibility (#10) BREAKING CHANGE: This release has to break some old APIs to stay compatible with React 19. \#\# `render` is now `async` and should be `await`ed This is the core change - due to the impementation of sibling prerendering on the React side, rendering has become more `async` and tests that interacted with `render` in a synchronous fashion would end up with not-resolving suspense boundaries. Please adjust your tests accordingly: ```diff const {takeRender, render} = createRenderStream({ /* ... */ }) - const utils = render() + const utils = await render() ``` \#\# enforcing of `IS_REACT_ACT_ENVIRONMENT == false` In combination with [issues we have have seen in the past](https://github.com/facebook/react/issues/29855), we have deduced that the testing approach of this library only really works in a "real-world" scenario, not in an `act` environment. As a result, we will now throw an error if you try to use it in an environment where `IS_REACT_ACT_ENVIRONMENT` is truthy. We are shipping a new tool, `disableActEnvironment` to prepare your environment for the duration of a test in a safe manner. This helper can either be used with explicit resource management using the `using` keyword: ```ts test('my test', () => { using _disabledAct = disableActEnvironment() // your test code here // as soon as this scope is left, the environment will be cleaned up }) ``` of by manually calling `cleanup`: ```ts test('my test', () => { const {cleanup} = disableActEnvironment() try { // your test code here } finally { cleanup() } }) ``` This function does not only adjust your `IS_REACT_ACT_ENVIRONMENT` value, but it will also temporarily adjust the `@testing-library/dom` configuration in a way so that e.g. calls to `userEvent` will not automatically be wrapped in `act`. Of course you can also use this tool on a per-file basis instead of a per-test basis, but keep in mind that doing so will break any React Testing Library tests that might be in the same file. \#\# `render` is now it's own implementation Previously, we used the `render` function of React Testing Library, but with the new restrictions this is no longer possible and we are shipping our own implementation. As a result, some less-common options are not supported in the new implementation. If you have a need for these, please let us know! * `hydrate` was removed * `legacyRoot` was removed. If you are using React 17, it will automatically switch to `ReactDOM.render`, otherwise we will use `createRoot` > [!CAUTION] > React 17 does not look for `IS_REACT_ACT_ENVIRONMENT` to determine if it is running in an `act`-environment, but rather `typeof jest !== "undefined"`. > If you have to test React 17, we recommend to patch it with a [`patch-package` patch](https://github.com/apollographql/apollo-client/blob/8a4738a8ad7284d247513671628a4ac5917e104c/patches/react-dom-17+17.0.2.patch) \#\# `renderToRenderStream` was removed As you now have to `await` the `render` call, `renderToRenderStream` had no real value anymore. Where previously, the second line of ```js const renderStream = renderToRenderStream(, combinedOptions) // this was optional in the past const utils = await renderStream.renderResultPromise ``` could be omitted and could save you some code, now that second line would become required. This now was not any shorter than calling `createRenderStream` and `renderStream.render` instead, and as both of these APIs now did the same thing in a different fashion, this would lead to confusion to no more benefit, so the API was removed. --- README.md | 107 ++++---- package.json | 4 +- src/__testHelpers__/useShim.js | 6 +- .../withDisabledActWarnings.ts | 14 - .../renderHookToSnapshotStream.test.tsx | 10 +- src/__tests__/renderToRenderStream.test.tsx | 94 ------- src/disableActEnvironment.ts | 171 ++++++++++++ .../__tests__/renderStreamMatchers.test.tsx | 27 +- src/index.ts | 6 + src/pure.ts | 11 +- src/renderHookToSnapshotStream.tsx | 10 +- .../__tests__/createRenderStream.test.tsx | 32 ++- .../__tests__/useTrackRenders.test.tsx | 13 +- src/renderStream/createRenderStream.tsx | 65 +++-- src/renderStream/disableActWarnings.ts | 16 -- src/renderToRenderStream.ts | 57 ---- src/renderWithoutAct.tsx | 250 ++++++++++++++++++ tests/setup-env.js | 14 + yarn.lock | 39 ++- 19 files changed, 628 insertions(+), 318 deletions(-) delete mode 100644 src/__testHelpers__/withDisabledActWarnings.ts delete mode 100644 src/__tests__/renderToRenderStream.test.tsx create mode 100644 src/disableActEnvironment.ts delete mode 100644 src/renderStream/disableActWarnings.ts delete mode 100644 src/renderToRenderStream.ts create mode 100644 src/renderWithoutAct.tsx 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 From 61ff60e1d5c7154beab3dad0ed417ffa61c3d512 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 6 Dec 2024 10:47:10 +0100 Subject: [PATCH 2/2] feat: bump React `peerDepencency` to 19 stable (#12) --- package.json | 8 ++++---- yarn.lock | 36 ++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 0c9fa678f..b1e93df2f 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": "19.0.0-rc.1", - "react-dom": "19.0.0-rc.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", @@ -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", diff --git a/yarn.lock b/yarn.lock index 9f9465c0c..4e0e5e050 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:19.0.0-rc.1" - react-dom: "npm:19.0.0-rc.1" + react: "npm:19.0.0" + react-dom: "npm:19.0.0" react-error-boundary: "npm:^4.0.13" rehackt: "npm:^0.1.0" ts-jest-resolver: "npm:^2.0.1" @@ -2644,8 +2644,8 @@ __metadata: 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 languageName: unknown linkType: soft @@ -9169,14 +9169,14 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:19.0.0-rc.1": - version: 19.0.0-rc.1 - resolution: "react-dom@npm:19.0.0-rc.1" +"react-dom@npm:19.0.0": + version: 19.0.0 + resolution: "react-dom@npm:19.0.0" dependencies: - scheduler: "npm:0.25.0-rc.1" + scheduler: "npm:^0.25.0" peerDependencies: - react: 19.0.0-rc.1 - checksum: 10c0/26fba423f41c8b3c7a47278ea490146506333175028e111e49eb39310110b6bda956cc772c77412abc8a7f28244eea80ebade15ec5b4382d79e2ac1b39bd2d0c + react: ^19.0.0 + checksum: 10c0/a36ce7ab507b237ae2759c984cdaad4af4096d8199fb65b3815c16825e5cfeb7293da790a3fc2184b52bfba7ba3ff31c058c01947aff6fd1a3701632aabaa6a9 languageName: node linkType: hard @@ -9212,10 +9212,10 @@ __metadata: languageName: node linkType: hard -"react@npm:19.0.0-rc.1": - version: 19.0.0-rc.1 - resolution: "react@npm:19.0.0-rc.1" - checksum: 10c0/8f453ee0ff05ec4b11701cf4240fb0039217e73f576d2a9b871a91e34ff17d66e16294ebd0fc72c237918a3a997cc42f0328c860bc0a2cfa18e6a41f49a1c871 +"react@npm:19.0.0": + version: 19.0.0 + resolution: "react@npm:19.0.0" + checksum: 10c0/9cad8f103e8e3a16d15cb18a0d8115d8bd9f9e1ce3420310aea381eb42aa0a4f812cf047bb5441349257a05fba8a291515691e3cb51267279b2d2c3253f38471 languageName: node linkType: hard @@ -9709,10 +9709,10 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:0.25.0-rc.1": - version: 0.25.0-rc.1 - resolution: "scheduler@npm:0.25.0-rc.1" - checksum: 10c0/dd4549eeb54cf3019c04257c622c4bbee12ef99dc547c4a96e1d5da8985ae44534111275ebcc4fea84b0d1299197b69071f912928adc5da4cab1e8168d09a44d +"scheduler@npm:^0.25.0": + version: 0.25.0 + resolution: "scheduler@npm:0.25.0" + checksum: 10c0/a4bb1da406b613ce72c1299db43759526058fdcc413999c3c3e0db8956df7633acf395cb20eb2303b6a65d658d66b6585d344460abaee8080b4aa931f10eaafe languageName: node linkType: hard