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
});