From 095d8a886e9694a71687f0670b35e180dfa27560 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Sat, 28 Feb 2026 17:49:11 +0100 Subject: [PATCH] [test] Add current behavior for faulty `onDefaultTransitionIndicator` implementation --- .../ReactDefaultTransitionIndicator-test.js | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/packages/react-reconciler/src/__tests__/ReactDefaultTransitionIndicator-test.js b/packages/react-reconciler/src/__tests__/ReactDefaultTransitionIndicator-test.js index fb698e821aa0..e8243e3f11e7 100644 --- a/packages/react-reconciler/src/__tests__/ReactDefaultTransitionIndicator-test.js +++ b/packages/react-reconciler/src/__tests__/ReactDefaultTransitionIndicator-test.js @@ -73,6 +73,121 @@ describe('ReactDefaultTransitionIndicator', () => { expect(root).toMatchRenderedOutput('Hello'); }); + // @gate enableDefaultTransitionIndicator + it('does not unmount the root if it has a faulty implementation', async () => { + const onUncaughtError = jest.fn(); + let setPromise; + function App() { + const [maybePromise, _setPromise] = useState(null); + setPromise = _setPromise; + let message = 'default'; + if (maybePromise !== null) { + message = use(maybePromise); + } + + return `Hello, ${message}!`; + } + + const root = ReactNoop.createRoot({ + onDefaultTransitionIndicator() { + Scheduler.log('start'); + throw new Error('Failed to start default transition indicator'); + }, + onUncaughtError, + }); + await act(() => { + root.render(); + }); + + expect(root).toMatchRenderedOutput('Hello, default!'); + + let resolve; + await expect( + act(() => { + React.startTransition(() => { + setPromise( + new Promise(_resolve => { + resolve = _resolve; + }), + ); + }); + }), + ).rejects.toThrow('Failed to start default transition indicator'); + expect(onUncaughtError).not.toHaveBeenCalled(); + + assertLog(['start']); + + expect(root).toMatchRenderedOutput('Hello, default!'); + + await act(() => { + React.startTransition(() => { + resolve('test'); + }); + }); + + expect(root).toMatchRenderedOutput('Hello, test!'); + }); + + // @gate enableDefaultTransitionIndicator + it('does not unmount the root if it has a faulty cleanup implementation', async () => { + const onUncaughtError = jest.fn(); + let setPromise; + function App() { + const [maybePromise, _setPromise] = useState(null); + setPromise = _setPromise; + let message = 'default'; + if (maybePromise !== null) { + message = use(maybePromise); + } + + return `Hello, ${message}!`; + } + + const root = ReactNoop.createRoot({ + onDefaultTransitionIndicator() { + Scheduler.log('start'); + return () => { + Scheduler.log('stop'); + throw new Error('Failed to stop default transition indicator'); + }; + }, + onUncaughtError, + }); + await act(() => { + root.render(); + }); + + expect(root).toMatchRenderedOutput('Hello, default!'); + + let resolve; + await act(() => { + React.startTransition(() => { + setPromise( + new Promise(_resolve => { + resolve = _resolve; + }), + ); + }); + }); + + assertLog(['start']); + + expect(root).toMatchRenderedOutput('Hello, default!'); + + await expect( + act(() => { + React.startTransition(() => { + resolve('test'); + }); + }), + ).rejects.toThrow('Failed to stop default transition indicator'); + expect(onUncaughtError).not.toHaveBeenCalled(); + + assertLog(['stop']); + + expect(root).toMatchRenderedOutput('Hello, test!'); + }); + // @gate enableDefaultTransitionIndicator it('does not trigger the default indicator if there is a sync mutation', async () => { const promiseA = Promise.resolve('Hi');