@samitouri / QOS-React-2 / commits / 03944bfb0b

Update Suspense Priority Warning to Include Component that Triggered Update (#16030)

Improved warning whenever lower priority events (ex. data fetching, page load) happen during a high priority update (ex. hover/click events) to include: 1.) Name of component that triggered the high priority update or 2.) Information that the update was triggered on the root

lunaruan committed Jul 22, 2019 at 14:17 UTC 03944bfb0bdacfe35b2a1722426ff744ae47d018
6 files changed +319 -49
packages/react-reconciler/src/ReactFiberHooks.js
+11
@@ -13,6 +13,7 @@ import type {Fiber} from './ReactFiber';
13 import type {ExpirationTime} from './ReactFiberExpirationTime';
14 import type {HookEffectTag} from './ReactHookEffectTags';
15 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
16 +import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
17
18 import ReactSharedInternals from 'shared/ReactSharedInternals';
19
@@ -48,6 +49,7 @@ import is from 'shared/objectIs';
49 import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork';
50 import {revertPassiveEffectsChange} from 'shared/ReactFeatureFlags';
51 import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
52 +import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration';
53
54 const {ReactCurrentDispatcher} = ReactSharedInternals;
55
@@ -96,6 +98,8 @@ type Update<S, A> = {
98 eagerReducer: ((S, A) => S) | null,
99 eagerState: S | null,
100 next: Update<S, A> | null,
101 +
102 + priority?: ReactPriorityLevel,
103 };
104
105 type UpdateQueue<S, A> = {
@@ -1140,6 +1144,9 @@ function dispatchAction<S, A>(
1144 eagerState: null,
1145 next: null,
1146 };
1147 + if (__DEV__) {
1148 + update.priority = getCurrentPriorityLevel();
1149 + }
1150 if (renderPhaseUpdates === null) {
1151 renderPhaseUpdates = new Map();
1152 }
@@ -1176,6 +1183,10 @@ function dispatchAction<S, A>(
1183 next: null,
1184 };
1185
1186 + if (__DEV__) {
1187 + update.priority = getCurrentPriorityLevel();
1188 + }
1189 +
1190 // Append the update to the end of the list.
1191 const last = queue.last;
1192 if (last === null) {
packages/react-reconciler/src/ReactFiberWorkLoop.js
+110 -32
@@ -795,7 +795,8 @@ function prepareFreshStack(root, expirationTime) {
795
796 if (__DEV__) {
797 ReactStrictModeWarnings.discardPendingWarnings();
798 - componentsWithSuspendedDiscreteUpdates = null;
798 + componentsThatSuspendedAtHighPri = null;
799 + componentsThatTriggeredHighPriSuspend = null;
800 }
801 }
802
@@ -982,6 +983,8 @@ function renderRoot(
983 // Set this to null to indicate there's no in-progress render.
984 workInProgressRoot = null;
985
986 + flushSuspensePriorityWarningInDEV();
987 +
988 switch (workInProgressRootExitStatus) {
989 case RootIncomplete: {
990 invariant(false, 'Should have a work-in-progress.');
@@ -1491,7 +1494,6 @@ function commitRoot(root) {
1494 function commitRootImpl(root) {
1495 flushPassiveEffects();
1496 flushRenderPhaseStrictModeWarningsInDEV();
1494 - flushSuspensePriorityWarningInDEV();
1497
1498 invariant(
1499 (executionContext & (RenderContext | CommitContext)) === NoContext,
@@ -2520,37 +2522,100 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2522
2523 export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV;
2524
2523 -let componentsWithSuspendedDiscreteUpdates = null;
2525 +let componentsThatSuspendedAtHighPri = null;
2526 +let componentsThatTriggeredHighPriSuspend = null;
2527 export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
2528 if (__DEV__) {
2529 + const currentPriorityLevel = getCurrentPriorityLevel();
2530 if (
2531 (sourceFiber.mode & ConcurrentMode) !== NoEffect &&
2528 - // Check if we're currently rendering a discrete update. Ideally, all we
2529 - // would need to do is check the current priority level. But we currently
2530 - // have no rigorous way to distinguish work that was scheduled at user-
2531 - // blocking priority from work that expired a bit and was "upgraded" to
2532 - // a higher priority. That's because we don't schedule separate callbacks
2533 - // for every level, only the highest priority level per root. The priority
2534 - // of subsequent levels is inferred from the expiration time, but this is
2535 - // an imprecise heuristic.
2536 - //
2537 - // However, we do store the last discrete pending update per root. So we
2538 - // can reliably compare to that one. (If we broaden this warning to include
2539 - // high pri updates that aren't discrete, then this won't be sufficient.)
2540 - //
2541 - // My rationale is that it's better for this warning to have false
2542 - // negatives than false positives.
2543 - rootsWithPendingDiscreteUpdates !== null &&
2544 - workInProgressRoot !== null &&
2545 - renderExpirationTime ===
2546 - rootsWithPendingDiscreteUpdates.get(workInProgressRoot)
2532 + (currentPriorityLevel === UserBlockingPriority ||
2533 + currentPriorityLevel === ImmediatePriority)
2534 ) {
2535 + let workInProgressNode = sourceFiber;
2536 + while (workInProgressNode !== null) {
2537 + // Add the component that triggered the suspense
2538 + const current = workInProgressNode.alternate;
2539 + if (current !== null) {
2540 + // TODO: warn component that triggers the high priority
2541 + // suspend is the HostRoot
2542 + switch (workInProgressNode.tag) {
2543 + case ClassComponent:
2544 + // Loop through the component's update queue and see whether the component
2545 + // has triggered any high priority updates
2546 + const updateQueue = current.updateQueue;
2547 + if (updateQueue !== null) {
2548 + let update = updateQueue.firstUpdate;
2549 + while (update !== null) {
2550 + const priorityLevel = update.priority;
2551 + if (
2552 + priorityLevel === UserBlockingPriority ||
2553 + priorityLevel === ImmediatePriority
2554 + ) {
2555 + if (componentsThatTriggeredHighPriSuspend === null) {
2556 + componentsThatTriggeredHighPriSuspend = new Set([
2557 + getComponentName(workInProgressNode.type),
2558 + ]);
2559 + } else {
2560 + componentsThatTriggeredHighPriSuspend.add(
2561 + getComponentName(workInProgressNode.type),
2562 + );
2563 + }
2564 + break;
2565 + }
2566 + update = update.next;
2567 + }
2568 + }
2569 + break;
2570 + case FunctionComponent:
2571 + case ForwardRef:
2572 + case SimpleMemoComponent:
2573 + if (
2574 + workInProgressNode.memoizedState !== null &&
2575 + workInProgressNode.memoizedState.baseUpdate !== null
2576 + ) {
2577 + let update = workInProgressNode.memoizedState.baseUpdate;
2578 + // Loop through the functional component's memoized state to see whether
2579 + // the component has triggered any high pri updates
2580 + while (update !== null) {
2581 + const priority = update.priority;
2582 + if (
2583 + priority === UserBlockingPriority ||
2584 + priority === ImmediatePriority
2585 + ) {
2586 + if (componentsThatTriggeredHighPriSuspend === null) {
2587 + componentsThatTriggeredHighPriSuspend = new Set([
2588 + getComponentName(workInProgressNode.type),
2589 + ]);
2590 + } else {
2591 + componentsThatTriggeredHighPriSuspend.add(
2592 + getComponentName(workInProgressNode.type),
2593 + );
2594 + }
2595 + break;
2596 + }
2597 + if (
2598 + update.next === workInProgressNode.memoizedState.baseUpdate
2599 + ) {
2600 + break;
2601 + }
2602 + update = update.next;
2603 + }
2604 + }
2605 + break;
2606 + default:
2607 + break;
2608 + }
2609 + }
2610 + workInProgressNode = workInProgressNode.return;
2611 + }
2612 +
2613 // Add the component name to a set.
2614 const componentName = getComponentName(sourceFiber.type);
2550 - if (componentsWithSuspendedDiscreteUpdates === null) {
2551 - componentsWithSuspendedDiscreteUpdates = new Set([componentName]);
2615 + if (componentsThatSuspendedAtHighPri === null) {
2616 + componentsThatSuspendedAtHighPri = new Set([componentName]);
2617 } else {
2553 - componentsWithSuspendedDiscreteUpdates.add(componentName);
2618 + componentsThatSuspendedAtHighPri.add(componentName);
2619 }
2620 }
2621 }
@@ -2558,20 +2623,32 @@ export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
2623
2624 function flushSuspensePriorityWarningInDEV() {
2625 if (__DEV__) {
2561 - if (componentsWithSuspendedDiscreteUpdates !== null) {
2626 + if (componentsThatSuspendedAtHighPri !== null) {
2627 const componentNames = [];
2563 - componentsWithSuspendedDiscreteUpdates.forEach(name => {
2628 + componentsThatSuspendedAtHighPri.forEach(name => {
2629 componentNames.push(name);
2630 });
2566 - componentsWithSuspendedDiscreteUpdates = null;
2631 + componentsThatSuspendedAtHighPri = null;
2632 +
2633 + const componentsThatTriggeredSuspendNames = [];
2634 + if (componentsThatTriggeredHighPriSuspend !== null) {
2635 + componentsThatTriggeredHighPriSuspend.forEach(name =>
2636 + componentsThatTriggeredSuspendNames.push(name),
2637 + );
2638 + }
2639 +
2640 + componentsThatTriggeredHighPriSuspend = null;
2641
2568 - // TODO: A more helpful version of this message could include the names of
2569 - // the component that were updated, not the ones that suspended. To do
2570 - // that we'd need to track all the components that updated during this
2571 - // render, perhaps using the same mechanism as `markRenderEventTime`.
2642 + const componentThatTriggeredSuspenseError =
2643 + componentsThatTriggeredSuspendNames.length > 0
2644 + ? '\n' +
2645 + 'The components that triggered the update: ' +
2646 + componentsThatTriggeredSuspendNames.sort().join(', ')
2647 + : '';
2648 warningWithoutStack(
2649 false,
2650 'The following components suspended during a user-blocking update: %s' +
2651 + '%s' +
2652 '\n\n' +
2653 'Updates triggered by user interactions (e.g. click events) are ' +
2654 'considered user-blocking by default. They should not suspend. ' +
@@ -2585,6 +2662,7 @@ function flushSuspensePriorityWarningInDEV() {
2662 'feedback, and another update to perform the actual change.',
2663 // TODO: Add link to React docs with more information, once it exists
2664 componentNames.sort().join(', '),
2665 + componentThatTriggeredSuspenseError,
2666 );
2667 }
2668 }
packages/react-reconciler/src/ReactUpdateQueue.js
+10 -1
@@ -87,6 +87,7 @@
87 import type {Fiber} from './ReactFiber';
88 import type {ExpirationTime} from './ReactFiberExpirationTime';
89 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
90 +import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
91
92 import {NoWork} from './ReactFiberExpirationTime';
93 import {
@@ -106,6 +107,7 @@ import {markRenderEventTimeAndConfig} from './ReactFiberWorkLoop';
107
108 import invariant from 'shared/invariant';
109 import warningWithoutStack from 'shared/warningWithoutStack';
110 +import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration';
111
112 export type Update<State> = {
113 expirationTime: ExpirationTime,
@@ -117,6 +119,9 @@ export type Update<State> = {
119
120 next: Update<State> | null,
121 nextEffect: Update<State> | null,
122 +
123 + //DEV only
124 + priority?: ReactPriorityLevel,
125 };
126
127 export type UpdateQueue<State> = {
@@ -197,7 +202,7 @@ export function createUpdate(
202 expirationTime: ExpirationTime,
203 suspenseConfig: null | SuspenseConfig,
204 ): Update<*> {
200 - return {
205 + let update: Update<*> = {
206 expirationTime,
207 suspenseConfig,
208
@@ -208,6 +213,10 @@ export function createUpdate(
213 next: null,
214 nextEffect: null,
215 };
216 + if (__DEV__) {
217 + update.priority = getCurrentPriorityLevel();
218 + }
219 + return update;
220 }
221
222 function appendUpdateToQueue<State>(
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+8
@@ -327,6 +327,7 @@ describe('ReactSuspense', () => {
327 });
328
329 it('throws if tree suspends and none of the Suspense ancestors have a fallback', () => {
330 + spyOnDev(console, 'error');
331 ReactTestRenderer.create(
332 <Suspense>
333 <AsyncText text="Hi" ms={1000} />
@@ -340,6 +341,13 @@ describe('ReactSuspense', () => {
341 'AsyncText suspended while rendering, but no fallback UI was specified.',
342 );
343 expect(Scheduler).toHaveYielded(['Suspend! [Hi]', 'Suspend! [Hi]']);
344 + if (__DEV__) {
345 + expect(console.error).toHaveBeenCalledTimes(2);
346 + expect(console.error.calls.argsFor(0)[0]).toContain(
347 + 'Warning: The following components suspended during a user-blocking update: ',
348 + );
349 + expect(console.error.calls.argsFor(0)[1]).toContain('AsyncText');
350 + }
351 });
352
353 describe('outside concurrent mode', () => {
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+170 -15
@@ -488,6 +488,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
488 });
489
490 it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => {
491 + spyOnDev(console, 'error');
492 function App(props) {
493 if (props.hide) {
494 return <Text text="(empty)" />;
@@ -515,6 +516,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
516 '(empty)',
517 ]);
518 expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
519 + if (__DEV__) {
520 + expect(console.error).toHaveBeenCalledTimes(1);
521 + expect(console.error.calls.argsFor(0)[0]).toContain(
522 + 'Warning: The following components suspended during a user-blocking update: ',
523 + );
524 + expect(console.error.calls.argsFor(0)[1]).toContain('AsyncText');
525 + }
526 });
527
528 it('forces an expiration after an update times out', async () => {
@@ -631,6 +639,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
639 });
640
641 it('renders an expiration boundary synchronously', async () => {
642 + spyOnDev(console, 'error');
643 // Synchronously render a tree that suspends
644 ReactNoop.flushSync(() =>
645 ReactNoop.render(
@@ -658,9 +667,19 @@ describe('ReactSuspenseWithNoopRenderer', () => {
667 expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
668 expect(Scheduler).toFlushAndYield(['Async']);
669 expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
670 +
671 + if (__DEV__) {
672 + expect(console.error).toHaveBeenCalledTimes(1);
673 + expect(console.error.calls.argsFor(0)[0]).toContain(
674 + 'Warning: The following components suspended during a user-blocking update: ',
675 + );
676 + expect(console.error.calls.argsFor(0)[1]).toContain('AsyncText');
677 + }
678 });
679
680 it('suspending inside an expired expiration boundary will bubble to the next one', async () => {
681 + spyOnDev(console, 'error');
682 +
683 ReactNoop.flushSync(() =>
684 ReactNoop.render(
685 <Fragment>
@@ -681,6 +700,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
700 ]);
701 // The tree commits synchronously
702 expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]);
703 +
704 + if (__DEV__) {
705 + expect(console.error).toHaveBeenCalledTimes(1);
706 + expect(console.error.calls.argsFor(0)[0]).toContain(
707 + 'Warning: The following components suspended during a user-blocking update: ',
708 + );
709 + expect(console.error.calls.argsFor(0)[1]).toContain('AsyncText');
710 + }
711 });
712
713 it('expires early by default', async () => {
@@ -758,11 +785,18 @@ describe('ReactSuspenseWithNoopRenderer', () => {
785 });
786
787 it('throws a helpful error when an update is suspends without a placeholder', () => {
761 - expect(() => {
762 - ReactNoop.flushSync(() => ReactNoop.render(<AsyncText text="Async" />));
763 - }).toThrow(
788 + spyOnDev(console, 'error');
789 + ReactNoop.render(<AsyncText ms={1000} text="Async" />);
790 + expect(Scheduler).toFlushAndThrow(
791 'AsyncText suspended while rendering, but no fallback UI was specified.',
792 );
793 + if (__DEV__) {
794 + expect(console.error).toHaveBeenCalledTimes(2);
795 + expect(console.error.calls.argsFor(0)[0]).toContain(
796 + 'Warning: The following components suspended during a user-blocking update: ',
797 + );
798 + expect(console.error.calls.argsFor(0)[1]).toContain('AsyncText');
799 + }
800 });
801
802 it('a Suspense component correctly handles more than one suspended child', async () => {
@@ -1590,24 +1624,25 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1624 Scheduler.unstable_advanceTime(100);
1625 await advanceTimers(100);
1626
1593 - expect(Scheduler).toFlushAndYield([
1594 - // A suspends
1595 - 'Suspend! [A]',
1596 - 'Loading...',
1597 - ]);
1598 - // We're now suspended and we haven't shown anything yet.
1599 - expect(ReactNoop.getChildren()).toEqual([]);
1600 -
1601 - // Flush some of the time
1602 - Scheduler.unstable_advanceTime(500);
1627 expect(() => {
1604 - jest.advanceTimersByTime(500);
1628 + expect(Scheduler).toFlushAndYield([
1629 + // A suspends
1630 + 'Suspend! [A]',
1631 + 'Loading...',
1632 + ]);
1633 }).toWarnDev(
1634 'The following components suspended during a user-blocking ' +
1635 'update: AsyncText',
1636 {withoutStack: true},
1637 );
1638
1639 + // We're now suspended and we haven't shown anything yet.
1640 + expect(ReactNoop.getChildren()).toEqual([]);
1641 +
1642 + // Flush some of the time
1643 + Scheduler.unstable_advanceTime(500);
1644 + jest.advanceTimersByTime(500);
1645 +
1646 // We should have already shown the fallback.
1647 // When we wrote this test, we inferred the start time of high priority
1648 // updates as way earlier in the past. This test ensures that we don't
@@ -1616,6 +1651,71 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1651 expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1652 });
1653
1654 + it('warns when a low priority update suspends inside a high priority update for functional components', async () => {
1655 + let _setShow;
1656 + function App() {
1657 + let [show, setShow] = React.useState(false);
1658 + _setShow = setShow;
1659 + return (
1660 + <Suspense fallback="Loading...">
1661 + {show && <AsyncText text="A" />}
1662 + </Suspense>
1663 + );
1664 + }
1665 +
1666 + await ReactNoop.act(async () => {
1667 + ReactNoop.render(<App />);
1668 + });
1669 +
1670 + expect(() => {
1671 + ReactNoop.act(() => {
1672 + Scheduler.unstable_runWithPriority(
1673 + Scheduler.unstable_UserBlockingPriority,
1674 + () => _setShow(true),
1675 + );
1676 + });
1677 + }).toWarnDev(
1678 + 'The following components suspended during a user-blocking update: AsyncText' +
1679 + '\n' +
1680 + 'The components that triggered the update: App',
1681 + {withoutStack: true},
1682 + );
1683 + });
1684 +
1685 + it('warns when a low priority update suspends inside a high priority update for class components', async () => {
1686 + let show;
1687 + class App extends React.Component {
1688 + state = {show: false};
1689 +
1690 + render() {
1691 + show = () => this.setState({show: true});
1692 + return (
1693 + <Suspense fallback="Loading...">
1694 + {this.state.show && <AsyncText text="A" />}
1695 + </Suspense>
1696 + );
1697 + }
1698 + }
1699 +
1700 + await ReactNoop.act(async () => {
1701 + ReactNoop.render(<App />);
1702 + });
1703 +
1704 + expect(() => {
1705 + ReactNoop.act(() => {
1706 + Scheduler.unstable_runWithPriority(
1707 + Scheduler.unstable_UserBlockingPriority,
1708 + () => show(),
1709 + );
1710 + });
1711 + }).toWarnDev(
1712 + 'The following components suspended during a user-blocking update: AsyncText' +
1713 + '\n' +
1714 + 'The components that triggered the update: App',
1715 + {withoutStack: true},
1716 + );
1717 + });
1718 +
1719 it('warns when suspending inside discrete update', async () => {
1720 function A() {
1721 Scheduler.unstable_yieldValue('A');
@@ -1654,11 +1754,66 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1754 expect(() => {
1755 Scheduler.unstable_flushAll();
1756 }).toWarnDev(
1657 - 'The following components suspended during a user-blocking update: A, C',
1757 + 'Warning: The following components suspended during a user-blocking update: A, C',
1758 {withoutStack: true},
1759 );
1760 });
1761
1762 + it('normal priority updates suspending do not warn for class components', async () => {
1763 + let show;
1764 + class App extends React.Component {
1765 + state = {show: false};
1766 +
1767 + render() {
1768 + show = () => this.setState({show: true});
1769 + return (
1770 + <Suspense fallback="Loading...">
1771 + {this.state.show && <AsyncText text="A" />}
1772 + </Suspense>
1773 + );
1774 + }
1775 + }
1776 +
1777 + await ReactNoop.act(async () => {
1778 + ReactNoop.render(<App />);
1779 + });
1780 +
1781 + // also make sure lowpriority is okay
1782 + await ReactNoop.act(async () => show(true));
1783 +
1784 + expect(Scheduler).toHaveYielded(['Suspend! [A]']);
1785 + Scheduler.unstable_advanceTime(100);
1786 + await advanceTimers(100);
1787 +
1788 + expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1789 + });
1790 +
1791 + it('normal priority updates suspending do not warn for functional components', async () => {
1792 + let _setShow;
1793 + function App() {
1794 + let [show, setShow] = React.useState(false);
1795 + _setShow = setShow;
1796 + return (
1797 + <Suspense fallback="Loading...">
1798 + {show && <AsyncText text="A" />}
1799 + </Suspense>
1800 + );
1801 + }
1802 +
1803 + await ReactNoop.act(async () => {
1804 + ReactNoop.render(<App />);
1805 + });
1806 +
1807 + // also make sure lowpriority is okay
1808 + await ReactNoop.act(async () => _setShow(true));
1809 +
1810 + expect(Scheduler).toHaveYielded(['Suspend! [A]']);
1811 + Scheduler.unstable_advanceTime(100);
1812 + await advanceTimers(100);
1813 +
1814 + expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1815 + });
1816 +
1817 it('shows the parent fallback if the inner fallback should be avoided', async () => {
1818 function Foo({showC}) {
1819 Scheduler.unstable_yieldValue('Foo');
packages/react/src/__tests__/ReactProfiler-test.internal.js
+10 -1
@@ -2628,7 +2628,8 @@ describe('Profiler', () => {
2628 });
2629
2630 it('handles high-pri renderers between suspended and resolved (async) trees', async () => {
2631 - // Set up an initial shell. We need to set this up before the test scenario
2631 + spyOnDev(console, 'error');
2632 + // Set up an initial shell. We need to set this up before the test sceanrio
2633 // because we want initial render to suspend on navigation to the initial state.
2634 let renderer = ReactTestRenderer.create(
2635 <React.Profiler id="app" onRender={() => {}}>
@@ -2730,6 +2731,14 @@ describe('Profiler', () => {
2731 expect(
2732 onInteractionScheduledWorkCompleted.mock.calls[1][0],
2733 ).toMatchInteraction(highPriUpdateInteraction);
2734 +
2735 + if (__DEV__) {
2736 + expect(console.error).toHaveBeenCalledTimes(1);
2737 + expect(console.error.calls.argsFor(0)[0]).toContain(
2738 + 'Warning: The following components suspended during a user-blocking update: ',
2739 + );
2740 + expect(console.error.calls.argsFor(0)[1]).toContain('AsyncText');
2741 + }
2742 });
2743 });
2744 });