mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Make time-slicing opt-in (#21072)
* Add enableSyncDefaultUpdates feature flag * Add enableSyncDefaultUpdates implementation * Fix tests * Switch feature flag to true by default * Finish concurrent render whenever for non-sync lanes * Also return DefaultLane with eventLane * Gate interruption test * Add continuout native event test * Fix tests from rebasing main * Hardcode lanes, remove added export * Sync forks
This commit is contained in:
@@ -268,6 +268,7 @@ describe('createSubscription', () => {
|
||||
expect(Scheduler).toFlushAndYield(['b-1']);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should ignore values emitted by a new subscribable until the commit phase', () => {
|
||||
const log = [];
|
||||
|
||||
@@ -325,7 +326,13 @@ describe('createSubscription', () => {
|
||||
expect(log).toEqual(['Parent.componentDidMount']);
|
||||
|
||||
// Start React update, but don't finish
|
||||
ReactNoop.render(<Parent observed={observableB} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Parent observed={observableB} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Parent observed={observableB} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
|
||||
expect(log).toEqual(['Parent.componentDidMount']);
|
||||
|
||||
@@ -355,6 +362,7 @@ describe('createSubscription', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should not drop values emitted between updates', () => {
|
||||
const log = [];
|
||||
|
||||
@@ -412,7 +420,13 @@ describe('createSubscription', () => {
|
||||
expect(log).toEqual(['Parent.componentDidMount']);
|
||||
|
||||
// Start React update, but don't finish
|
||||
ReactNoop.render(<Parent observed={observableB} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Parent observed={observableB} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Parent observed={observableB} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
|
||||
expect(log).toEqual(['Parent.componentDidMount']);
|
||||
|
||||
|
||||
@@ -360,6 +360,7 @@ describe('ReactART', () => {
|
||||
expect(onClick2).toBeCalled();
|
||||
});
|
||||
|
||||
// @gate !enableSyncDefaultUpdates
|
||||
it('can concurrently render with a "primary" renderer while sharing context', () => {
|
||||
const CurrentRendererContext = React.createContext(null);
|
||||
|
||||
|
||||
@@ -284,6 +284,46 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
|
||||
expect(container.textContent).toEqual('hovered');
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('continuous native events flush as expected', async () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
|
||||
const target = React.createRef(null);
|
||||
function Foo({hovered}) {
|
||||
const hoverString = hovered ? 'hovered' : 'not hovered';
|
||||
Scheduler.unstable_yieldValue(hoverString);
|
||||
return <div ref={target}>{hoverString}</div>;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
root.render(<Foo hovered={false} />);
|
||||
});
|
||||
expect(container.textContent).toEqual('not hovered');
|
||||
|
||||
await act(async () => {
|
||||
// Note: React does not use native mouseenter/mouseleave events
|
||||
// but we should still correctly determine their priority.
|
||||
const mouseEnterEvent = document.createEvent('MouseEvents');
|
||||
mouseEnterEvent.initEvent('mouseover', true, true);
|
||||
target.current.addEventListener('mouseover', () => {
|
||||
root.render(<Foo hovered={true} />);
|
||||
});
|
||||
dispatchAndSetCurrentEvent(target.current, mouseEnterEvent);
|
||||
|
||||
// Since mouse end is not discrete, should not have updated yet
|
||||
expect(Scheduler).toHaveYielded(['not hovered']);
|
||||
expect(container.textContent).toEqual('not hovered');
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['hovered']);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
expect(container.textContent).toEqual('hovered');
|
||||
} else {
|
||||
expect(container.textContent).toEqual('not hovered');
|
||||
}
|
||||
});
|
||||
expect(container.textContent).toEqual('hovered');
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('should batch inside native events', async () => {
|
||||
const root = ReactDOM.unstable_createRoot(container);
|
||||
|
||||
@@ -1865,11 +1865,21 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
suspend = true;
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App />);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Before']);
|
||||
// This took a long time to render.
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
expect(Scheduler).toFlushAndYield(['After']);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App />);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Before', 'After']);
|
||||
} else {
|
||||
root.render(<App />);
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Before']);
|
||||
// This took a long time to render.
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
expect(Scheduler).toFlushAndYield(['After']);
|
||||
}
|
||||
|
||||
// This will cause us to skip the second row completely.
|
||||
});
|
||||
|
||||
|
||||
@@ -1934,7 +1934,13 @@ describe('DOMPluginEventSystem', () => {
|
||||
log.length = 0;
|
||||
|
||||
// Increase counter
|
||||
root.render(<Test counter={1} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<Test counter={1} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<Test counter={1} />);
|
||||
}
|
||||
// Yield before committing
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Test']);
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export const NoLane: Lane = /* */ 0b0000000000000000000
|
||||
|
||||
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
|
||||
|
||||
const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
|
||||
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
|
||||
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
|
||||
|
||||
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
|
||||
|
||||
@@ -52,7 +52,7 @@ export const NoLane: Lane = /* */ 0b0000000000000000000
|
||||
|
||||
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
|
||||
|
||||
const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
|
||||
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
|
||||
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
|
||||
|
||||
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
disableSchedulerTimeoutInWorkLoop,
|
||||
enableStrictEffects,
|
||||
skipUnmountedBoundaries,
|
||||
enableSyncDefaultUpdates,
|
||||
enableUpdaterTracking,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
@@ -138,6 +139,10 @@ import {
|
||||
NoLanes,
|
||||
NoLane,
|
||||
SyncLane,
|
||||
DefaultLane,
|
||||
DefaultHydrationLane,
|
||||
InputContinuousLane,
|
||||
InputContinuousHydrationLane,
|
||||
NoTimestamp,
|
||||
claimNextTransitionLane,
|
||||
claimNextRetryLane,
|
||||
@@ -433,6 +438,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
|
||||
// TODO: Move this type conversion to the event priority module.
|
||||
const updateLane: Lane = (getCurrentUpdatePriority(): any);
|
||||
if (updateLane !== NoLane) {
|
||||
if (
|
||||
enableSyncDefaultUpdates &&
|
||||
(updateLane === InputContinuousLane ||
|
||||
updateLane === InputContinuousHydrationLane)
|
||||
) {
|
||||
return DefaultLane;
|
||||
}
|
||||
return updateLane;
|
||||
}
|
||||
|
||||
@@ -443,6 +455,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
|
||||
// use that directly.
|
||||
// TODO: Move this type conversion to the event priority module.
|
||||
const eventLane: Lane = (getCurrentEventPriority(): any);
|
||||
if (
|
||||
enableSyncDefaultUpdates &&
|
||||
(eventLane === InputContinuousLane ||
|
||||
eventLane === InputContinuousHydrationLane)
|
||||
) {
|
||||
return DefaultLane;
|
||||
}
|
||||
return eventLane;
|
||||
}
|
||||
|
||||
@@ -695,7 +714,16 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
|
||||
|
||||
// Schedule a new callback.
|
||||
let newCallbackNode;
|
||||
if (newCallbackPriority === SyncLane) {
|
||||
if (
|
||||
enableSyncDefaultUpdates &&
|
||||
(newCallbackPriority === DefaultLane ||
|
||||
newCallbackPriority === DefaultHydrationLane)
|
||||
) {
|
||||
newCallbackNode = scheduleCallback(
|
||||
ImmediateSchedulerPriority,
|
||||
performSyncWorkOnRoot.bind(null, root),
|
||||
);
|
||||
} else if (newCallbackPriority === SyncLane) {
|
||||
// Special case: Sync React callbacks are scheduled on a special
|
||||
// internal queue
|
||||
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
|
||||
@@ -1030,7 +1058,11 @@ function performSyncWorkOnRoot(root) {
|
||||
const finishedWork: Fiber = (root.current.alternate: any);
|
||||
root.finishedWork = finishedWork;
|
||||
root.finishedLanes = lanes;
|
||||
commitRoot(root);
|
||||
if (enableSyncDefaultUpdates && !includesSomeLane(lanes, SyncLane)) {
|
||||
finishConcurrentRender(root, exitStatus, lanes);
|
||||
} else {
|
||||
commitRoot(root);
|
||||
}
|
||||
|
||||
// Before exiting, make sure there's a callback scheduled for the next
|
||||
// pending level.
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
disableSchedulerTimeoutInWorkLoop,
|
||||
enableStrictEffects,
|
||||
skipUnmountedBoundaries,
|
||||
enableSyncDefaultUpdates,
|
||||
enableUpdaterTracking,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
@@ -138,6 +139,10 @@ import {
|
||||
NoLanes,
|
||||
NoLane,
|
||||
SyncLane,
|
||||
DefaultLane,
|
||||
DefaultHydrationLane,
|
||||
InputContinuousLane,
|
||||
InputContinuousHydrationLane,
|
||||
NoTimestamp,
|
||||
claimNextTransitionLane,
|
||||
claimNextRetryLane,
|
||||
@@ -433,6 +438,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
|
||||
// TODO: Move this type conversion to the event priority module.
|
||||
const updateLane: Lane = (getCurrentUpdatePriority(): any);
|
||||
if (updateLane !== NoLane) {
|
||||
if (
|
||||
enableSyncDefaultUpdates &&
|
||||
(updateLane === InputContinuousLane ||
|
||||
updateLane === InputContinuousHydrationLane)
|
||||
) {
|
||||
return DefaultLane;
|
||||
}
|
||||
return updateLane;
|
||||
}
|
||||
|
||||
@@ -443,6 +455,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
|
||||
// use that directly.
|
||||
// TODO: Move this type conversion to the event priority module.
|
||||
const eventLane: Lane = (getCurrentEventPriority(): any);
|
||||
if (
|
||||
enableSyncDefaultUpdates &&
|
||||
(eventLane === InputContinuousLane ||
|
||||
eventLane === InputContinuousHydrationLane)
|
||||
) {
|
||||
return DefaultLane;
|
||||
}
|
||||
return eventLane;
|
||||
}
|
||||
|
||||
@@ -695,7 +714,16 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
|
||||
|
||||
// Schedule a new callback.
|
||||
let newCallbackNode;
|
||||
if (newCallbackPriority === SyncLane) {
|
||||
if (
|
||||
enableSyncDefaultUpdates &&
|
||||
(newCallbackPriority === DefaultLane ||
|
||||
newCallbackPriority === DefaultHydrationLane)
|
||||
) {
|
||||
newCallbackNode = scheduleCallback(
|
||||
ImmediateSchedulerPriority,
|
||||
performSyncWorkOnRoot.bind(null, root),
|
||||
);
|
||||
} else if (newCallbackPriority === SyncLane) {
|
||||
// Special case: Sync React callbacks are scheduled on a special
|
||||
// internal queue
|
||||
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
|
||||
@@ -1030,7 +1058,11 @@ function performSyncWorkOnRoot(root) {
|
||||
const finishedWork: Fiber = (root.current.alternate: any);
|
||||
root.finishedWork = finishedWork;
|
||||
root.finishedLanes = lanes;
|
||||
commitRoot(root);
|
||||
if (enableSyncDefaultUpdates && !includesSomeLane(lanes, SyncLane)) {
|
||||
finishConcurrentRender(root, exitStatus, lanes);
|
||||
} else {
|
||||
commitRoot(root);
|
||||
}
|
||||
|
||||
// Before exiting, make sure there's a callback scheduled for the next
|
||||
// pending level.
|
||||
|
||||
+8
-1
@@ -45,6 +45,7 @@ describe('ReactSuspenseList', () => {
|
||||
return Component;
|
||||
}
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('appends rendering tasks to the end of the priority queue', async () => {
|
||||
const A = createAsyncText('A');
|
||||
const B = createAsyncText('B');
|
||||
@@ -63,7 +64,13 @@ describe('ReactSuspenseList', () => {
|
||||
root.render(<App show={false} />);
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
|
||||
root.render(<App show={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App show={true} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App show={true} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Suspend! [A]',
|
||||
'Suspend! [B]',
|
||||
|
||||
+109
-16
@@ -111,9 +111,15 @@ describe('ReactExpiration', () => {
|
||||
// Flush the sync task.
|
||||
ReactNoop.flushSync();
|
||||
}
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('increases priority of updates as time progresses', () => {
|
||||
ReactNoop.render(<span prop="done" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<span prop="done" />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<span prop="done" />);
|
||||
}
|
||||
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
|
||||
@@ -132,6 +138,7 @@ describe('ReactExpiration', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('done')]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('two updates of like priority in the same event always flush within the same batch', () => {
|
||||
class TextClass extends React.Component {
|
||||
componentDidMount() {
|
||||
@@ -154,7 +161,13 @@ describe('ReactExpiration', () => {
|
||||
|
||||
// First, show what happens for updates in two separate events.
|
||||
// Schedule an update.
|
||||
ReactNoop.render(<TextClass text="A" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<TextClass text="A" />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<TextClass text="A" />);
|
||||
}
|
||||
// Advance the timer.
|
||||
Scheduler.unstable_advanceTime(2000);
|
||||
// Partially flush the first update, then interrupt it.
|
||||
@@ -184,6 +197,7 @@ describe('ReactExpiration', () => {
|
||||
expect(Scheduler).toFlushAndYield(['B [render]', 'B [commit]']);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it(
|
||||
'two updates of like priority in the same event always flush within the ' +
|
||||
"same batch, even if there's a sync update in between",
|
||||
@@ -209,7 +223,13 @@ describe('ReactExpiration', () => {
|
||||
|
||||
// First, show what happens for updates in two separate events.
|
||||
// Schedule an update.
|
||||
ReactNoop.render(<TextClass text="A" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<TextClass text="A" />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<TextClass text="A" />);
|
||||
}
|
||||
// Advance the timer.
|
||||
Scheduler.unstable_advanceTime(2000);
|
||||
// Partially flush the first update, then interrupt it.
|
||||
@@ -245,6 +265,7 @@ describe('ReactExpiration', () => {
|
||||
},
|
||||
);
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('cannot update at the same expiration time that is already rendering', () => {
|
||||
const store = {text: 'initial'};
|
||||
const subscribers = [];
|
||||
@@ -281,7 +302,13 @@ describe('ReactExpiration', () => {
|
||||
}
|
||||
|
||||
// Initial mount
|
||||
ReactNoop.render(<App />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<App />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'initial [A] [render]',
|
||||
'initial [B] [render]',
|
||||
@@ -294,7 +321,13 @@ describe('ReactExpiration', () => {
|
||||
]);
|
||||
|
||||
// Partial update
|
||||
subscribers.forEach(s => s.setState({text: '1'}));
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
subscribers.forEach(s => s.setState({text: '1'}));
|
||||
});
|
||||
} else {
|
||||
subscribers.forEach(s => s.setState({text: '1'}));
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'1 [A] [render]',
|
||||
'1 [B] [render]',
|
||||
@@ -310,6 +343,7 @@ describe('ReactExpiration', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('stops yielding if CPU-bound update takes too long to finish', () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
function App() {
|
||||
@@ -324,7 +358,13 @@ describe('ReactExpiration', () => {
|
||||
);
|
||||
}
|
||||
|
||||
root.render(<App />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App />);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A']);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['B']);
|
||||
@@ -337,6 +377,7 @@ describe('ReactExpiration', () => {
|
||||
expect(root).toMatchRenderedOutput('ABCDE');
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('root expiration is measured from the time of the first update', () => {
|
||||
Scheduler.unstable_advanceTime(10000);
|
||||
|
||||
@@ -352,8 +393,13 @@ describe('ReactExpiration', () => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
root.render(<App />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App />);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A']);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['B']);
|
||||
@@ -366,6 +412,7 @@ describe('ReactExpiration', () => {
|
||||
expect(root).toMatchRenderedOutput('ABCDE');
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should measure expiration times relative to module initialization', () => {
|
||||
// Tests an implementation detail where expiration times are computed using
|
||||
// bitwise operations.
|
||||
@@ -381,12 +428,24 @@ describe('ReactExpiration', () => {
|
||||
// current time.
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
|
||||
ReactNoop.render('Hi');
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render('Hi');
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render('Hi');
|
||||
}
|
||||
|
||||
// The update should not have expired yet.
|
||||
flushNextRenderIfExpired();
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
// TODO: Why is this flushed?
|
||||
expect(ReactNoop).toMatchRenderedOutput('Hi');
|
||||
} else {
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
}
|
||||
|
||||
// Advance the time some more to expire the update.
|
||||
Scheduler.unstable_advanceTime(10000);
|
||||
@@ -395,6 +454,7 @@ describe('ReactExpiration', () => {
|
||||
expect(ReactNoop).toMatchRenderedOutput('Hi');
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should measure callback timeout relative to current time, not start-up time', () => {
|
||||
// Corresponds to a bugfix: https://github.com/facebook/react/pull/15479
|
||||
// The bug wasn't caught by other tests because we use virtual times that
|
||||
@@ -403,7 +463,13 @@ describe('ReactExpiration', () => {
|
||||
// Before scheduling an update, advance the current time.
|
||||
Scheduler.unstable_advanceTime(10000);
|
||||
|
||||
ReactNoop.render('Hi');
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render('Hi');
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render('Hi');
|
||||
}
|
||||
flushNextRenderIfExpired();
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
@@ -418,6 +484,7 @@ describe('ReactExpiration', () => {
|
||||
expect(ReactNoop).toMatchRenderedOutput('Hi');
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('prevents starvation by sync updates', async () => {
|
||||
const {useState} = React;
|
||||
|
||||
@@ -450,7 +517,13 @@ describe('ReactExpiration', () => {
|
||||
|
||||
// First demonstrate what happens when there's no starvation
|
||||
await ReactNoop.act(async () => {
|
||||
updateNormalPri();
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
updateNormalPri();
|
||||
});
|
||||
} else {
|
||||
updateNormalPri();
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Sync pri: 0']);
|
||||
updateSyncPri();
|
||||
});
|
||||
@@ -466,7 +539,13 @@ describe('ReactExpiration', () => {
|
||||
|
||||
// Do the same thing, but starve the first update
|
||||
await ReactNoop.act(async () => {
|
||||
updateNormalPri();
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
updateNormalPri();
|
||||
});
|
||||
} else {
|
||||
updateNormalPri();
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Sync pri: 1']);
|
||||
|
||||
// This time, a lot of time has elapsed since the normal pri update
|
||||
@@ -628,6 +707,7 @@ describe('ReactExpiration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('detects starvation in multiple batches', async () => {
|
||||
const {useState} = React;
|
||||
|
||||
@@ -666,7 +746,13 @@ describe('ReactExpiration', () => {
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
// Partially render an update
|
||||
updateNormalPri();
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
updateNormalPri();
|
||||
});
|
||||
} else {
|
||||
updateNormalPri();
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['High pri: 0']);
|
||||
// Some time goes by. In an interleaved event, schedule another update.
|
||||
// This will be placed into a separate batch.
|
||||
@@ -693,6 +779,7 @@ describe('ReactExpiration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('updates do not expire while they are IO-bound', async () => {
|
||||
const {Suspense} = React;
|
||||
|
||||
@@ -715,7 +802,13 @@ describe('ReactExpiration', () => {
|
||||
expect(root).toMatchRenderedOutput('A, Sibling');
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
root.render(<App text="B" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App text="B" />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App text="B" />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Suspend! [B]',
|
||||
'Sibling',
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('ReactFlushSync', () => {
|
||||
return text;
|
||||
}
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
test('changes priority of updates in useEffect', async () => {
|
||||
function App() {
|
||||
const [syncState, setSyncState] = useState(0);
|
||||
@@ -37,22 +38,37 @@ describe('ReactFlushSync', () => {
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
await ReactNoop.act(async () => {
|
||||
root.render(<App />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App />);
|
||||
}
|
||||
// This will yield right before the passive effect fires
|
||||
expect(Scheduler).toFlushUntilNextPaint(['0, 0']);
|
||||
|
||||
// The passive effect will schedule a sync update and a normal update.
|
||||
// They should commit in two separate batches. First the sync one.
|
||||
expect(() =>
|
||||
expect(Scheduler).toFlushUntilNextPaint(['1, 0']),
|
||||
).toErrorDev('flushSync was called from inside a lifecycle method');
|
||||
expect(() => {
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
expect(Scheduler).toFlushUntilNextPaint(['1, 0', '1, 1']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushUntilNextPaint(['1, 0']);
|
||||
}
|
||||
}).toErrorDev('flushSync was called from inside a lifecycle method');
|
||||
|
||||
// The remaining update is not sync
|
||||
ReactNoop.flushSync();
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
|
||||
// Now flush it.
|
||||
expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
// With sync default updates, passive effects are synchronously flushed.
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
} else {
|
||||
expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
|
||||
}
|
||||
});
|
||||
expect(root).toMatchRenderedOutput('1, 1');
|
||||
});
|
||||
|
||||
+108
-24
@@ -151,6 +151,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
return Promise.resolve().then(() => {});
|
||||
}
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('resumes after an interruption', () => {
|
||||
function Counter(props, ref) {
|
||||
const [count, updateCount] = useState(0);
|
||||
@@ -166,10 +167,20 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
// Schedule some updates
|
||||
ReactNoop.batchedUpdates(() => {
|
||||
counter.current.updateCount(1);
|
||||
counter.current.updateCount(count => count + 10);
|
||||
});
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
// TODO: Batched updates need to be inside startTransition?
|
||||
ReactNoop.batchedUpdates(() => {
|
||||
counter.current.updateCount(1);
|
||||
counter.current.updateCount(count => count + 10);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ReactNoop.batchedUpdates(() => {
|
||||
counter.current.updateCount(1);
|
||||
counter.current.updateCount(count => count + 10);
|
||||
});
|
||||
}
|
||||
|
||||
// Partially flush without committing
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Count: 11']);
|
||||
@@ -690,6 +701,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span(22)]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('discards render phase updates if something suspends', async () => {
|
||||
const thenable = {then() {}};
|
||||
function Foo({signal}) {
|
||||
@@ -725,15 +737,28 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
expect(Scheduler).toFlushAndYield([0]);
|
||||
expect(root).toMatchRenderedOutput(<span prop={0} />);
|
||||
|
||||
root.render(<Foo signal={false} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<Foo signal={false} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<Foo signal={false} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['Suspend!']);
|
||||
expect(root).toMatchRenderedOutput(<span prop={0} />);
|
||||
|
||||
// Rendering again should suspend again.
|
||||
root.render(<Foo signal={false} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<Foo signal={false} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<Foo signal={false} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['Suspend!']);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
|
||||
const thenable = {then() {}};
|
||||
function Foo({signal}) {
|
||||
@@ -776,19 +801,38 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
root.render(<Foo signal={false} />);
|
||||
setLabel('B');
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<Foo signal={false} />);
|
||||
setLabel('B');
|
||||
});
|
||||
} else {
|
||||
root.render(<Foo signal={false} />);
|
||||
setLabel('B');
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Suspend!']);
|
||||
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
|
||||
|
||||
// Rendering again should suspend again.
|
||||
root.render(<Foo signal={false} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<Foo signal={false} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<Foo signal={false} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['Suspend!']);
|
||||
|
||||
// Flip the signal back to "cancel" the update. However, the update to
|
||||
// label should still proceed. It shouldn't have been dropped.
|
||||
root.render(<Foo signal={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<Foo signal={true} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<Foo signal={true} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['B:0']);
|
||||
expect(root).toMatchRenderedOutput(<span prop="B:0" />);
|
||||
});
|
||||
@@ -1284,6 +1328,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('does not warn about state updates for unmounted components with pending passive unmounts for alternates', () => {
|
||||
let setParentState = null;
|
||||
const setChildStates = [];
|
||||
@@ -1350,17 +1395,34 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
]);
|
||||
|
||||
// Schedule another update for children, and partially process it.
|
||||
setChildStates.forEach(setChildState => setChildState(2));
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
setChildStates.forEach(setChildState => setChildState(2));
|
||||
});
|
||||
} else {
|
||||
setChildStates.forEach(setChildState => setChildState(2));
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Child one render']);
|
||||
|
||||
// Schedule unmount for the parent that unmounts children with pending update.
|
||||
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => {
|
||||
setParentState(false);
|
||||
});
|
||||
expect(Scheduler).toFlushUntilNextPaint([
|
||||
'Parent false render',
|
||||
'Parent false commit',
|
||||
]);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
expect(Scheduler).toFlushUntilNextPaint([
|
||||
// TODO: why do the children render and fire effects?
|
||||
'Child two render',
|
||||
'Child one commit',
|
||||
'Child two commit',
|
||||
'Parent false render',
|
||||
'Parent false commit',
|
||||
]);
|
||||
} else {
|
||||
expect(Scheduler).toFlushUntilNextPaint([
|
||||
'Parent false render',
|
||||
'Parent false commit',
|
||||
]);
|
||||
}
|
||||
|
||||
// Schedule updates for children too (which should be ignored)
|
||||
setChildStates.forEach(setChildState => setChildState(2));
|
||||
@@ -1647,6 +1709,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('updates have async priority even if effects are flushed early', () => {
|
||||
function Counter(props) {
|
||||
const [count, updateCount] = useState('(empty)');
|
||||
@@ -1667,20 +1730,41 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
|
||||
|
||||
// Rendering again should flush the previous commit's effects
|
||||
ReactNoop.render(<Counter count={1} />, () =>
|
||||
Scheduler.unstable_yieldValue('Sync effect'),
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Counter count={1} />, () =>
|
||||
Scheduler.unstable_yieldValue('Sync effect'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Counter count={1} />, () =>
|
||||
Scheduler.unstable_yieldValue('Sync effect'),
|
||||
);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Schedule update [0]',
|
||||
'Count: 0',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
ReactNoop.flushPassiveEffects();
|
||||
expect(Scheduler).toHaveYielded(['Schedule update [1]']);
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Count: 0',
|
||||
'Sync effect',
|
||||
'Schedule update [1]',
|
||||
'Count: 1',
|
||||
]);
|
||||
} else {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
ReactNoop.flushPassiveEffects();
|
||||
expect(Scheduler).toHaveYielded(['Schedule update [1]']);
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
}
|
||||
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
});
|
||||
});
|
||||
|
||||
+141
-40
@@ -50,6 +50,7 @@ describe('ReactIncremental', () => {
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should render a simple component, in steps if needed', () => {
|
||||
function Bar() {
|
||||
Scheduler.unstable_yieldValue('Bar');
|
||||
@@ -65,7 +66,17 @@ describe('ReactIncremental', () => {
|
||||
return [<Bar key="a" isBar={true} />, <Bar key="b" isBar={true} />];
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo />, () => Scheduler.unstable_yieldValue('callback'));
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo />, () =>
|
||||
Scheduler.unstable_yieldValue('callback'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo />, () =>
|
||||
Scheduler.unstable_yieldValue('callback'),
|
||||
);
|
||||
}
|
||||
// Do one step of work.
|
||||
expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
|
||||
|
||||
@@ -132,6 +143,7 @@ describe('ReactIncremental', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('can cancel partially rendered work and restart', () => {
|
||||
function Bar(props) {
|
||||
Scheduler.unstable_yieldValue('Bar');
|
||||
@@ -152,13 +164,26 @@ describe('ReactIncremental', () => {
|
||||
ReactNoop.render(<Foo text="foo" />);
|
||||
expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'Bar']);
|
||||
|
||||
ReactNoop.render(<Foo text="bar" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo text="bar" />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo text="bar" />);
|
||||
}
|
||||
// Flush part of the work
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar']);
|
||||
|
||||
// This will abort the previous work and restart
|
||||
ReactNoop.flushSync(() => ReactNoop.render(null));
|
||||
ReactNoop.render(<Foo text="baz" />);
|
||||
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo text="baz" />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo text="baz" />);
|
||||
}
|
||||
|
||||
// Flush part of the new work
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar']);
|
||||
@@ -167,6 +192,7 @@ describe('ReactIncremental', () => {
|
||||
expect(Scheduler).toFlushAndYield(['Bar']);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should call callbacks even if updates are aborted', () => {
|
||||
let inst;
|
||||
|
||||
@@ -192,26 +218,50 @@ describe('ReactIncremental', () => {
|
||||
ReactNoop.render(<Foo />);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
|
||||
inst.setState(
|
||||
() => {
|
||||
Scheduler.unstable_yieldValue('setState1');
|
||||
return {text: 'bar'};
|
||||
},
|
||||
() => Scheduler.unstable_yieldValue('callback1'),
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
inst.setState(
|
||||
() => {
|
||||
Scheduler.unstable_yieldValue('setState1');
|
||||
return {text: 'bar'};
|
||||
},
|
||||
() => Scheduler.unstable_yieldValue('callback1'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
inst.setState(
|
||||
() => {
|
||||
Scheduler.unstable_yieldValue('setState1');
|
||||
return {text: 'bar'};
|
||||
},
|
||||
() => Scheduler.unstable_yieldValue('callback1'),
|
||||
);
|
||||
}
|
||||
|
||||
// Flush part of the work
|
||||
expect(Scheduler).toFlushAndYieldThrough(['setState1']);
|
||||
|
||||
// This will abort the previous work and restart
|
||||
ReactNoop.flushSync(() => ReactNoop.render(<Foo />));
|
||||
inst.setState(
|
||||
() => {
|
||||
Scheduler.unstable_yieldValue('setState2');
|
||||
return {text2: 'baz'};
|
||||
},
|
||||
() => Scheduler.unstable_yieldValue('callback2'),
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
inst.setState(
|
||||
() => {
|
||||
Scheduler.unstable_yieldValue('setState2');
|
||||
return {text2: 'baz'};
|
||||
},
|
||||
() => Scheduler.unstable_yieldValue('callback2'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
inst.setState(
|
||||
() => {
|
||||
Scheduler.unstable_yieldValue('setState2');
|
||||
return {text2: 'baz'};
|
||||
},
|
||||
() => Scheduler.unstable_yieldValue('callback2'),
|
||||
);
|
||||
}
|
||||
|
||||
// Flush the rest of the work which now includes the low priority
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
@@ -1714,6 +1764,7 @@ describe('ReactIncremental', () => {
|
||||
expect(instance.state.n).toEqual(3);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('merges and masks context', () => {
|
||||
class Intl extends React.Component {
|
||||
static childContextTypes = {
|
||||
@@ -1838,15 +1889,27 @@ describe('ReactIncremental', () => {
|
||||
'ShowLocale {"locale":"de"}',
|
||||
'ShowBoth {"locale":"de"}',
|
||||
]);
|
||||
|
||||
ReactNoop.render(
|
||||
<Intl locale="sv">
|
||||
<ShowLocale />
|
||||
<div>
|
||||
<ShowBoth />
|
||||
</div>
|
||||
</Intl>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<Intl locale="sv">
|
||||
<ShowLocale />
|
||||
<div>
|
||||
<ShowBoth />
|
||||
</div>
|
||||
</Intl>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<Intl locale="sv">
|
||||
<ShowLocale />
|
||||
<div>
|
||||
<ShowBoth />
|
||||
</div>
|
||||
</Intl>,
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Intl {}']);
|
||||
|
||||
ReactNoop.render(
|
||||
@@ -1994,18 +2057,35 @@ describe('ReactIncremental', () => {
|
||||
}
|
||||
}
|
||||
|
||||
ReactNoop.render(
|
||||
<Intl locale="fr">
|
||||
<ShowLocale />
|
||||
<LegacyHiddenDiv mode="hidden">
|
||||
<ShowLocale />
|
||||
<Intl locale="ru">
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<Intl locale="fr">
|
||||
<ShowLocale />
|
||||
</Intl>
|
||||
</LegacyHiddenDiv>
|
||||
<ShowLocale />
|
||||
</Intl>,
|
||||
);
|
||||
<LegacyHiddenDiv mode="hidden">
|
||||
<ShowLocale />
|
||||
<Intl locale="ru">
|
||||
<ShowLocale />
|
||||
</Intl>
|
||||
</LegacyHiddenDiv>
|
||||
<ShowLocale />
|
||||
</Intl>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<Intl locale="fr">
|
||||
<ShowLocale />
|
||||
<LegacyHiddenDiv mode="hidden">
|
||||
<ShowLocale />
|
||||
<Intl locale="ru">
|
||||
<ShowLocale />
|
||||
</Intl>
|
||||
</LegacyHiddenDiv>
|
||||
<ShowLocale />
|
||||
</Intl>,
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Intl {}',
|
||||
'ShowLocale {"locale":"fr"}',
|
||||
@@ -2748,6 +2828,7 @@ describe('ReactIncremental', () => {
|
||||
expect(Scheduler).toFlushAndYield(['count:1, name:not brian']);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('does not interrupt for update at same priority', () => {
|
||||
function Parent(props) {
|
||||
Scheduler.unstable_yieldValue('Parent: ' + props.step);
|
||||
@@ -2759,7 +2840,13 @@ describe('ReactIncremental', () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Parent step={1} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Parent step={1} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Parent step={1} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Parent: 1']);
|
||||
|
||||
// Interrupt at same priority
|
||||
@@ -2768,6 +2855,7 @@ describe('ReactIncremental', () => {
|
||||
expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('does not interrupt for update at lower priority', () => {
|
||||
function Parent(props) {
|
||||
Scheduler.unstable_yieldValue('Parent: ' + props.step);
|
||||
@@ -2779,7 +2867,13 @@ describe('ReactIncremental', () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Parent step={1} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Parent step={1} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Parent step={1} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Parent: 1']);
|
||||
|
||||
// Interrupt at lower priority
|
||||
@@ -2789,6 +2883,7 @@ describe('ReactIncremental', () => {
|
||||
expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('does interrupt for update at higher priority', () => {
|
||||
function Parent(props) {
|
||||
Scheduler.unstable_yieldValue('Parent: ' + props.step);
|
||||
@@ -2800,7 +2895,13 @@ describe('ReactIncremental', () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Parent step={1} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Parent step={1} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Parent step={1} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Parent: 1']);
|
||||
|
||||
// Interrupt at higher priority
|
||||
|
||||
+121
-59
@@ -58,6 +58,7 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
);
|
||||
}
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('recovers from errors asynchronously', () => {
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {error: null};
|
||||
@@ -90,19 +91,37 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
throw new Error('oops!');
|
||||
}
|
||||
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<Indirection>
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<Indirection>
|
||||
<Indirection>
|
||||
<Indirection>
|
||||
<BadRender />
|
||||
<Indirection />
|
||||
<Indirection />
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<Indirection>
|
||||
<Indirection>
|
||||
<BadRender />
|
||||
<Indirection />
|
||||
<Indirection />
|
||||
<Indirection>
|
||||
<BadRender />
|
||||
<Indirection />
|
||||
<Indirection />
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
// Start rendering asynchronously
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
@@ -152,6 +171,7 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('recovers from errors asynchronously (legacy, no getDerivedStateFromError)', () => {
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {error: null};
|
||||
@@ -184,19 +204,37 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
throw new Error('oops!');
|
||||
}
|
||||
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<Indirection>
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<Indirection>
|
||||
<Indirection>
|
||||
<Indirection>
|
||||
<BadRender />
|
||||
<Indirection />
|
||||
<Indirection />
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<Indirection>
|
||||
<Indirection>
|
||||
<BadRender />
|
||||
<Indirection />
|
||||
<Indirection />
|
||||
<Indirection>
|
||||
<BadRender />
|
||||
<Indirection />
|
||||
<Indirection />
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</Indirection>
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
// Start rendering asynchronously
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
@@ -247,27 +285,16 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
Scheduler.unstable_yieldValue('commit');
|
||||
}
|
||||
|
||||
function interrupt() {
|
||||
ReactNoop.flushSync(() => {
|
||||
ReactNoop.renderToRootWithID(null, 'other-root');
|
||||
});
|
||||
}
|
||||
|
||||
ReactNoop.render(<App isBroken={true} />, onCommit);
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App isBroken={true} />, onCommit);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYieldThrough(['error']);
|
||||
interrupt();
|
||||
|
||||
React.unstable_startTransition(() => {
|
||||
// This update is in a separate batch
|
||||
ReactNoop.render(<App isBroken={false} />, onCommit);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
// The first render fails. But because there's a lower priority pending
|
||||
// update, it doesn't throw.
|
||||
'error',
|
||||
]);
|
||||
|
||||
// React will try to recover by rendering all the pending updates in a
|
||||
// single batch, synchronously. This time it succeeds.
|
||||
//
|
||||
@@ -306,15 +333,10 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
Scheduler.unstable_yieldValue('commit');
|
||||
}
|
||||
|
||||
function interrupt() {
|
||||
ReactNoop.flushSync(() => {
|
||||
ReactNoop.renderToRootWithID(null, 'other-root');
|
||||
});
|
||||
}
|
||||
|
||||
ReactNoop.render(<App isBroken={true} />, onCommit);
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App isBroken={true} />, onCommit);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYieldThrough(['error']);
|
||||
interrupt();
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
|
||||
@@ -323,12 +345,6 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
ReactNoop.render(<App isBroken={false} />, onCommit);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
// The first render fails. But because there's a lower priority pending
|
||||
// update, it doesn't throw.
|
||||
'error',
|
||||
]);
|
||||
|
||||
// React will try to recover by rendering all the pending updates in a
|
||||
// single batch, synchronously. This time it succeeds.
|
||||
//
|
||||
@@ -362,6 +378,7 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('retries one more time before handling error', () => {
|
||||
function BadRender({unused}) {
|
||||
Scheduler.unstable_yieldValue('BadRender');
|
||||
@@ -383,7 +400,17 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<Parent />, () => Scheduler.unstable_yieldValue('commit'));
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Parent />, () =>
|
||||
Scheduler.unstable_yieldValue('commit'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Parent />, () =>
|
||||
Scheduler.unstable_yieldValue('commit'),
|
||||
);
|
||||
}
|
||||
|
||||
// Render the bad component asynchronously
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Parent', 'BadRender']);
|
||||
@@ -402,6 +429,7 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('retries one more time if an error occurs during a render that expires midway through the tree', async () => {
|
||||
function Oops({unused}) {
|
||||
Scheduler.unstable_yieldValue('Oops');
|
||||
@@ -425,7 +453,13 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<App />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<App />);
|
||||
}
|
||||
|
||||
// Render part of the tree
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
|
||||
@@ -532,6 +566,7 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('catches render error in a boundary during partial deferred mounting', () => {
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {error: null};
|
||||
@@ -556,11 +591,21 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
throw new Error('Hello');
|
||||
}
|
||||
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<BrokenRender />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<BrokenRender />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<ErrorBoundary>
|
||||
<BrokenRender />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['ErrorBoundary render success']);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
@@ -712,6 +757,7 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('propagates an error from a noop error boundary during partial deferred mounting', () => {
|
||||
class RethrowErrorBoundary extends React.Component {
|
||||
componentDidCatch(error) {
|
||||
@@ -729,11 +775,21 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
throw new Error('Hello');
|
||||
}
|
||||
|
||||
ReactNoop.render(
|
||||
<RethrowErrorBoundary>
|
||||
<BrokenRender />
|
||||
</RethrowErrorBoundary>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<RethrowErrorBoundary>
|
||||
<BrokenRender />
|
||||
</RethrowErrorBoundary>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<RethrowErrorBoundary>
|
||||
<BrokenRender />
|
||||
</RethrowErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['RethrowErrorBoundary render']);
|
||||
|
||||
@@ -1805,7 +1861,13 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||
}
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
root.render(<Oops />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<Oops />);
|
||||
});
|
||||
} else {
|
||||
root.render(<Oops />);
|
||||
}
|
||||
|
||||
// Render past the component that throws, then yield.
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Oops']);
|
||||
|
||||
+38
-5
@@ -34,6 +34,7 @@ describe('ReactIncrementalReflection', () => {
|
||||
return {type: 'span', children: [], prop, hidden: false};
|
||||
}
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('handles isMounted even when the initial render is deferred', () => {
|
||||
const instances = [];
|
||||
|
||||
@@ -63,7 +64,13 @@ describe('ReactIncrementalReflection', () => {
|
||||
return <Component />;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo />);
|
||||
}
|
||||
|
||||
// Render part way through but don't yet commit the updates.
|
||||
expect(Scheduler).toFlushAndYieldThrough(['componentWillMount: false']);
|
||||
@@ -81,6 +88,7 @@ describe('ReactIncrementalReflection', () => {
|
||||
expect(instances[0]._isMounted()).toBe(true);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('handles isMounted when an unmount is deferred', () => {
|
||||
const instances = [];
|
||||
|
||||
@@ -121,7 +129,13 @@ describe('ReactIncrementalReflection', () => {
|
||||
|
||||
expect(instances[0]._isMounted()).toBe(true);
|
||||
|
||||
ReactNoop.render(<Foo mount={false} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo mount={false} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo mount={false} />);
|
||||
}
|
||||
// Render part way through but don't yet commit the updates so it is not
|
||||
// fully unmounted yet.
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Other']);
|
||||
@@ -134,6 +148,7 @@ describe('ReactIncrementalReflection', () => {
|
||||
expect(instances[0]._isMounted()).toBe(false);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('finds no node before insertion and correct node before deletion', () => {
|
||||
let classInstance = null;
|
||||
|
||||
@@ -204,7 +219,13 @@ describe('ReactIncrementalReflection', () => {
|
||||
return [<Component key="a" step={props.step} />, <Sibling key="b" />];
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo step={0} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo step={0} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo step={0} />);
|
||||
}
|
||||
// Flush past Component but don't complete rendering everything yet.
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
['componentWillMount', null],
|
||||
@@ -246,7 +267,13 @@ describe('ReactIncrementalReflection', () => {
|
||||
|
||||
// The next step will render a new host node but won't get committed yet.
|
||||
// We expect this to mutate the original Fiber.
|
||||
ReactNoop.render(<Foo step={2} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo step={2} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo step={2} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
['componentWillUpdate', hostSpan],
|
||||
'render',
|
||||
@@ -267,7 +294,13 @@ describe('ReactIncrementalReflection', () => {
|
||||
expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
|
||||
|
||||
// Render to null but don't commit it yet.
|
||||
ReactNoop.render(<Foo step={3} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo step={3} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo step={3} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
['componentWillUpdate', hostDiv],
|
||||
'render',
|
||||
|
||||
+71
-17
@@ -85,6 +85,7 @@ describe('ReactIncrementalScheduling', () => {
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('works on deferred roots in the order they were scheduled', () => {
|
||||
const {useEffect} = React;
|
||||
function Text({text}) {
|
||||
@@ -107,8 +108,15 @@ describe('ReactIncrementalScheduling', () => {
|
||||
|
||||
// Schedule deferred work in the reverse order
|
||||
ReactNoop.act(() => {
|
||||
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
|
||||
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
|
||||
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
|
||||
});
|
||||
} else {
|
||||
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
|
||||
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
|
||||
}
|
||||
// Ensure it starts in the order it was scheduled
|
||||
expect(Scheduler).toFlushAndYieldThrough(['c:2']);
|
||||
|
||||
@@ -117,7 +125,13 @@ describe('ReactIncrementalScheduling', () => {
|
||||
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
|
||||
// Schedule last bit of work, it will get processed the last
|
||||
|
||||
ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
|
||||
});
|
||||
} else {
|
||||
ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
|
||||
}
|
||||
|
||||
// Keep performing work in the order it was scheduled
|
||||
expect(Scheduler).toFlushAndYieldThrough(['b:2']);
|
||||
@@ -132,6 +146,7 @@ describe('ReactIncrementalScheduling', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('schedules sync updates when inside componentDidMount/Update', () => {
|
||||
let instance;
|
||||
|
||||
@@ -170,7 +185,13 @@ describe('ReactIncrementalScheduling', () => {
|
||||
}
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo />);
|
||||
}
|
||||
// Render without committing
|
||||
expect(Scheduler).toFlushAndYieldThrough(['render: 0']);
|
||||
|
||||
@@ -184,7 +205,13 @@ describe('ReactIncrementalScheduling', () => {
|
||||
'componentDidUpdate: 1',
|
||||
]);
|
||||
|
||||
instance.setState({tick: 2});
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
instance.setState({tick: 2});
|
||||
});
|
||||
} else {
|
||||
instance.setState({tick: 2});
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['render: 2']);
|
||||
expect(ReactNoop.flushNextYield()).toEqual([
|
||||
'componentDidUpdate: 2',
|
||||
@@ -197,6 +224,7 @@ describe('ReactIncrementalScheduling', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('can opt-in to async scheduling inside componentDidMount/Update', () => {
|
||||
let instance;
|
||||
class Foo extends React.Component {
|
||||
@@ -255,20 +283,39 @@ describe('ReactIncrementalScheduling', () => {
|
||||
|
||||
// Increment the tick to 2. This will trigger an update inside cDU. Flush
|
||||
// the first update without flushing the second one.
|
||||
instance.setState({tick: 2});
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'render: 2',
|
||||
'componentDidUpdate: 2',
|
||||
'componentDidUpdate (before setState): 2',
|
||||
'componentDidUpdate (after setState): 2',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
instance.setState({tick: 2});
|
||||
});
|
||||
|
||||
// Now flush the cDU update.
|
||||
expect(Scheduler).toFlushAndYield(['render: 3', 'componentDidUpdate: 3']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
|
||||
// TODO: why does this flush sync?
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'render: 2',
|
||||
'componentDidUpdate: 2',
|
||||
'componentDidUpdate (before setState): 2',
|
||||
'componentDidUpdate (after setState): 2',
|
||||
'render: 3',
|
||||
'componentDidUpdate: 3',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
|
||||
} else {
|
||||
instance.setState({tick: 2});
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'render: 2',
|
||||
'componentDidUpdate: 2',
|
||||
'componentDidUpdate (before setState): 2',
|
||||
'componentDidUpdate (after setState): 2',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
|
||||
|
||||
// Now flush the cDU update.
|
||||
expect(Scheduler).toFlushAndYield(['render: 3', 'componentDidUpdate: 3']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
|
||||
}
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('performs Task work even after time runs out', () => {
|
||||
class Foo extends React.Component {
|
||||
state = {step: 1};
|
||||
@@ -286,7 +333,14 @@ describe('ReactIncrementalScheduling', () => {
|
||||
return <span prop={this.state.step} />;
|
||||
}
|
||||
}
|
||||
ReactNoop.render(<Foo />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo />);
|
||||
}
|
||||
|
||||
// This should be just enough to complete all the work, but not enough to
|
||||
// commit it.
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
|
||||
|
||||
+31
-4
@@ -384,6 +384,7 @@ describe('ReactIncrementalSideEffects', () => {
|
||||
expect(ReactNoop.getChildren('portalContainer')).toEqual([]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('does not update child nodes if a flush is aborted', () => {
|
||||
function Bar(props) {
|
||||
Scheduler.unstable_yieldValue('Bar');
|
||||
@@ -409,7 +410,13 @@ describe('ReactIncrementalSideEffects', () => {
|
||||
div(div(span('Hello'), span('Hello')), span('Yo')),
|
||||
]);
|
||||
|
||||
ReactNoop.render(<Foo text="World" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo text="World" />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo text="World" />);
|
||||
}
|
||||
|
||||
// Flush some of the work without committing
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar']);
|
||||
@@ -633,12 +640,19 @@ describe('ReactIncrementalSideEffects', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('can update a completed tree before it has a chance to commit', () => {
|
||||
function Foo(props) {
|
||||
Scheduler.unstable_yieldValue('Foo');
|
||||
return <span prop={props.step} />;
|
||||
}
|
||||
ReactNoop.render(<Foo step={1} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo step={1} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo step={1} />);
|
||||
}
|
||||
// This should be just enough to complete the tree without committing it
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
|
||||
expect(ReactNoop.getChildrenAsJSX()).toEqual(null);
|
||||
@@ -647,13 +661,26 @@ describe('ReactIncrementalSideEffects', () => {
|
||||
ReactNoop.flushNextYield();
|
||||
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
|
||||
|
||||
ReactNoop.render(<Foo step={2} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo step={2} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo step={2} />);
|
||||
}
|
||||
// This should be just enough to complete the tree without committing it
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
|
||||
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
|
||||
// This time, before we commit the tree, we update the root component with
|
||||
// new props
|
||||
ReactNoop.render(<Foo step={3} />);
|
||||
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo step={3} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo step={3} />);
|
||||
}
|
||||
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
|
||||
// Now let's commit. We already had a commit that was pending, which will
|
||||
// render 2.
|
||||
|
||||
@@ -61,9 +61,16 @@ describe('ReactIncrementalUpdates', () => {
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['commit']);
|
||||
expect(state).toEqual({a: 'a'});
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
|
||||
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
// TODO: should deferredUpdates flush sync with the default update?
|
||||
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
} else {
|
||||
expect(state).toEqual({a: 'a'});
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
|
||||
}
|
||||
});
|
||||
|
||||
it('applies updates with equal priority in insertion order', () => {
|
||||
@@ -130,6 +137,7 @@ describe('ReactIncrementalUpdates', () => {
|
||||
expect(instance.state).toEqual({c: 'c', d: 'd'});
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('can abort an update, schedule additional updates, and resume', () => {
|
||||
let instance;
|
||||
class Foo extends React.Component {
|
||||
@@ -159,33 +167,75 @@ describe('ReactIncrementalUpdates', () => {
|
||||
}
|
||||
|
||||
// Schedule some async updates
|
||||
instance.setState(createUpdate('a'));
|
||||
instance.setState(createUpdate('b'));
|
||||
instance.setState(createUpdate('c'));
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
instance.setState(createUpdate('a'));
|
||||
instance.setState(createUpdate('b'));
|
||||
instance.setState(createUpdate('c'));
|
||||
});
|
||||
} else {
|
||||
instance.setState(createUpdate('a'));
|
||||
instance.setState(createUpdate('b'));
|
||||
instance.setState(createUpdate('c'));
|
||||
}
|
||||
|
||||
// Begin the updates but don't flush them yet
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a', 'b', 'c']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('')]);
|
||||
|
||||
// Schedule some more updates at different priorities
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
instance.setState(createUpdate('f'));
|
||||
});
|
||||
instance.setState(createUpdate('g'));
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
instance.setState(createUpdate('f'));
|
||||
});
|
||||
React.unstable_startTransition(() => {
|
||||
instance.setState(createUpdate('g'));
|
||||
});
|
||||
|
||||
// The sync updates should have flushed, but not the async ones
|
||||
expect(Scheduler).toHaveYielded(['e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('ef')]);
|
||||
// The sync updates should have flushed, but not the async ones
|
||||
expect(Scheduler).toHaveYielded(['e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('ef')]);
|
||||
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
// TODO: should d, e, f be flushed again first?
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
|
||||
} else {
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
instance.setState(createUpdate('f'));
|
||||
});
|
||||
instance.setState(createUpdate('g'));
|
||||
|
||||
// The sync updates should have flushed, but not the async ones
|
||||
expect(Scheduler).toHaveYielded(['e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('ef')]);
|
||||
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
|
||||
}
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('can abort an update, schedule a replaceState, and resume', () => {
|
||||
let instance;
|
||||
class Foo extends React.Component {
|
||||
@@ -215,34 +265,79 @@ describe('ReactIncrementalUpdates', () => {
|
||||
}
|
||||
|
||||
// Schedule some async updates
|
||||
instance.setState(createUpdate('a'));
|
||||
instance.setState(createUpdate('b'));
|
||||
instance.setState(createUpdate('c'));
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
instance.setState(createUpdate('a'));
|
||||
instance.setState(createUpdate('b'));
|
||||
instance.setState(createUpdate('c'));
|
||||
});
|
||||
} else {
|
||||
instance.setState(createUpdate('a'));
|
||||
instance.setState(createUpdate('b'));
|
||||
instance.setState(createUpdate('c'));
|
||||
}
|
||||
|
||||
// Begin the updates but don't flush them yet
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a', 'b', 'c']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('')]);
|
||||
|
||||
// Schedule some more updates at different priorities{
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
// No longer a public API, but we can test that it works internally by
|
||||
// reaching into the updater.
|
||||
instance.updater.enqueueReplaceState(instance, createUpdate('f'));
|
||||
});
|
||||
instance.setState(createUpdate('g'));
|
||||
// Schedule some more updates at different priorities
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
instance.setState(createUpdate('d'));
|
||||
|
||||
// The sync updates should have flushed, but not the async ones. Update d
|
||||
// was dropped and replaced by e.
|
||||
expect(Scheduler).toHaveYielded(['e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('f')]);
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
// No longer a public API, but we can test that it works internally by
|
||||
// reaching into the updater.
|
||||
instance.updater.enqueueReplaceState(instance, createUpdate('f'));
|
||||
});
|
||||
React.unstable_startTransition(() => {
|
||||
instance.setState(createUpdate('g'));
|
||||
});
|
||||
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('fg')]);
|
||||
// The sync updates should have flushed, but not the async ones.
|
||||
// TODO: should 'd' have flushed?
|
||||
// TODO: should 'f' have flushed? I don't know what enqueueReplaceState is.
|
||||
expect(Scheduler).toHaveYielded(['e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('f')]);
|
||||
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('fg')]);
|
||||
} else {
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('e'));
|
||||
// No longer a public API, but we can test that it works internally by
|
||||
// reaching into the updater.
|
||||
instance.updater.enqueueReplaceState(instance, createUpdate('f'));
|
||||
});
|
||||
instance.setState(createUpdate('g'));
|
||||
|
||||
// The sync updates should have flushed, but not the async ones. Update d
|
||||
// was dropped and replaced by e.
|
||||
expect(Scheduler).toHaveYielded(['e', 'f']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('f')]);
|
||||
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
expect(Scheduler).toFlushAndYield(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('fg')]);
|
||||
}
|
||||
});
|
||||
|
||||
it('passes accumulation of previous updates to replaceState updater function', () => {
|
||||
@@ -459,6 +554,7 @@ describe('ReactIncrementalUpdates', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('derived state')]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('regression: does not expire soon due to layout effects in the last batch', () => {
|
||||
const {useState, useLayoutEffect} = React;
|
||||
|
||||
@@ -475,7 +571,13 @@ describe('ReactIncrementalUpdates', () => {
|
||||
}
|
||||
|
||||
ReactNoop.act(() => {
|
||||
ReactNoop.render(<App />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<App />);
|
||||
}
|
||||
flushNextRenderIfExpired();
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
@@ -485,13 +587,19 @@ describe('ReactIncrementalUpdates', () => {
|
||||
]);
|
||||
|
||||
Scheduler.unstable_advanceTime(10000);
|
||||
|
||||
setCount(2);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
setCount(2);
|
||||
});
|
||||
} else {
|
||||
setCount(2);
|
||||
}
|
||||
flushNextRenderIfExpired();
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
});
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('regression: does not expire soon due to previous flushSync', () => {
|
||||
function Text({text}) {
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
@@ -505,29 +613,49 @@ describe('ReactIncrementalUpdates', () => {
|
||||
|
||||
Scheduler.unstable_advanceTime(10000);
|
||||
|
||||
ReactNoop.render(<Text text="B" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Text text="B" />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Text text="B" />);
|
||||
}
|
||||
flushNextRenderIfExpired();
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('regression: does not expire soon due to previous expired work', () => {
|
||||
function Text({text}) {
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Text text="A" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Text text="A" />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Text text="A" />);
|
||||
}
|
||||
Scheduler.unstable_advanceTime(10000);
|
||||
flushNextRenderIfExpired();
|
||||
expect(Scheduler).toHaveYielded(['A']);
|
||||
|
||||
Scheduler.unstable_advanceTime(10000);
|
||||
|
||||
ReactNoop.render(<Text text="B" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Text text="B" />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Text text="B" />);
|
||||
}
|
||||
flushNextRenderIfExpired();
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('when rebasing, does not exclude updates that were already committed, regardless of priority', async () => {
|
||||
const {useState, useLayoutEffect} = React;
|
||||
|
||||
@@ -560,7 +688,13 @@ describe('ReactIncrementalUpdates', () => {
|
||||
expect(root).toMatchRenderedOutput('');
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
pushToLog('A');
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
pushToLog('A');
|
||||
});
|
||||
} else {
|
||||
pushToLog('A');
|
||||
}
|
||||
|
||||
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
|
||||
pushToLog('B'),
|
||||
@@ -584,6 +718,7 @@ describe('ReactIncrementalUpdates', () => {
|
||||
expect(root).toMatchRenderedOutput('ABCD');
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('when rebasing, does not exclude updates that were already committed, regardless of priority (classes)', async () => {
|
||||
let pushToLog;
|
||||
class App extends React.Component {
|
||||
@@ -615,7 +750,13 @@ describe('ReactIncrementalUpdates', () => {
|
||||
expect(root).toMatchRenderedOutput('');
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
pushToLog('A');
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
pushToLog('A');
|
||||
});
|
||||
} else {
|
||||
pushToLog('A');
|
||||
}
|
||||
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () =>
|
||||
pushToLog('B'),
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('ReactInterleavedUpdates', () => {
|
||||
return text;
|
||||
}
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
test('update during an interleaved event is not processed during the current render', async () => {
|
||||
const updaters = [];
|
||||
|
||||
@@ -55,13 +56,25 @@ describe('ReactInterleavedUpdates', () => {
|
||||
expect(root).toMatchRenderedOutput('000');
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
updateChildren(1);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
updateChildren(1);
|
||||
});
|
||||
} else {
|
||||
updateChildren(1);
|
||||
}
|
||||
// Partially render the children. Only the first one.
|
||||
expect(Scheduler).toFlushAndYieldThrough([1]);
|
||||
|
||||
// In an interleaved event, schedule an update on each of the children.
|
||||
// Including the two that haven't rendered yet.
|
||||
updateChildren(2);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
updateChildren(2);
|
||||
});
|
||||
} else {
|
||||
updateChildren(2);
|
||||
}
|
||||
|
||||
// We should continue rendering without including the interleaved updates.
|
||||
expect(Scheduler).toFlushUntilNextPaint([1, 1]);
|
||||
@@ -73,6 +86,7 @@ describe('ReactInterleavedUpdates', () => {
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
// @gate !enableSyncDefaultUpdates
|
||||
test('low priority update during an interleaved event is not processed during the current render', async () => {
|
||||
// Same as previous test, but the interleaved update is lower priority than
|
||||
// the in-progress render.
|
||||
|
||||
@@ -1475,6 +1475,7 @@ describe('ReactLazy', () => {
|
||||
});
|
||||
|
||||
// @gate enableLazyElements
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('mount and reorder lazy elements', async () => {
|
||||
class Child extends React.Component {
|
||||
componentDidMount() {
|
||||
@@ -1534,7 +1535,13 @@ describe('ReactLazy', () => {
|
||||
expect(root).toMatchRenderedOutput('AB');
|
||||
|
||||
// Swap the position of A and B
|
||||
root.update(<Parent swap={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.update(<Parent swap={true} />);
|
||||
});
|
||||
} else {
|
||||
root.update(<Parent swap={true} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
|
||||
await lazyChildB2;
|
||||
// We need to flush to trigger the second one to load.
|
||||
|
||||
@@ -828,6 +828,7 @@ describe('ReactNewContext', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('warns if multiple renderers concurrently render the same context', () => {
|
||||
spyOnDev(console, 'error');
|
||||
const Context = React.createContext(0);
|
||||
@@ -846,7 +847,13 @@ describe('ReactNewContext', () => {
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<App value={1} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App value={1} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<App value={1} />);
|
||||
}
|
||||
// Render past the Provider, but don't commit yet
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ describe('ReactSchedulerIntegration', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('requests a paint after committing', () => {
|
||||
const scheduleCallback = Scheduler.unstable_scheduleCallback;
|
||||
|
||||
@@ -100,7 +101,13 @@ describe('ReactSchedulerIntegration', () => {
|
||||
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C'));
|
||||
|
||||
// Schedule a React render. React will request a paint after committing it.
|
||||
root.render('Update');
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render('Update');
|
||||
});
|
||||
} else {
|
||||
root.render('Update');
|
||||
}
|
||||
|
||||
// Advance time just to be sure the next tasks have lower priority
|
||||
Scheduler.unstable_advanceTime(2000);
|
||||
|
||||
@@ -99,6 +99,7 @@ describe('ReactSuspense', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('suspends rendering and continues later', () => {
|
||||
function Bar(props) {
|
||||
Scheduler.unstable_yieldValue('Bar');
|
||||
@@ -129,7 +130,13 @@ describe('ReactSuspense', () => {
|
||||
|
||||
// Navigate the shell to now render the child content.
|
||||
// This should suspend.
|
||||
root.update(<Foo renderBar={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.update(<Foo renderBar={true} />);
|
||||
});
|
||||
} else {
|
||||
root.update(<Foo renderBar={true} />);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Foo',
|
||||
@@ -197,6 +204,7 @@ describe('ReactSuspense', () => {
|
||||
expect(root).toMatchRenderedOutput('AB');
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('interrupts current render if promise resolves before current render phase', () => {
|
||||
let didResolve = false;
|
||||
const listeners = [];
|
||||
@@ -238,15 +246,29 @@ describe('ReactSuspense', () => {
|
||||
expect(root).toMatchRenderedOutput('Initial');
|
||||
|
||||
// The update will suspend.
|
||||
root.update(
|
||||
<>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<Async />
|
||||
</Suspense>
|
||||
<Text text="After Suspense" />
|
||||
<Text text="Sibling" />
|
||||
</>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.update(
|
||||
<>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<Async />
|
||||
</Suspense>
|
||||
<Text text="After Suspense" />
|
||||
<Text text="Sibling" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
root.update(
|
||||
<>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<Async />
|
||||
</Suspense>
|
||||
<Text text="After Suspense" />
|
||||
<Text text="Sibling" />
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
// Yield past the Suspense boundary but don't complete the last sibling.
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
@@ -271,6 +293,7 @@ describe('ReactSuspense', () => {
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
// @gate !enableSyncDefaultUpdates
|
||||
it(
|
||||
'interrupts current render when something suspends with a ' +
|
||||
"delay and we've already skipped over a lower priority update in " +
|
||||
|
||||
@@ -1274,7 +1274,13 @@ describe('ReactSuspenseList', () => {
|
||||
}
|
||||
|
||||
// This render is only CPU bound. Nothing suspends.
|
||||
ReactNoop.render(<Foo />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo />);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A']);
|
||||
|
||||
@@ -1452,7 +1458,13 @@ describe('ReactSuspenseList', () => {
|
||||
}
|
||||
|
||||
// This render is only CPU bound. Nothing suspends.
|
||||
ReactNoop.render(<Foo />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo />);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A']);
|
||||
|
||||
@@ -2449,7 +2461,13 @@ describe('ReactSuspenseList', () => {
|
||||
|
||||
await ReactNoop.act(async () => {
|
||||
// Add a few items at the end.
|
||||
updateLowPri(true);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
updateLowPri(true);
|
||||
});
|
||||
} else {
|
||||
updateLowPri(true);
|
||||
}
|
||||
|
||||
// Flush partially through.
|
||||
expect(Scheduler).toFlushAndYieldThrough(['B', 'C']);
|
||||
@@ -2586,7 +2604,13 @@ describe('ReactSuspenseList', () => {
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<App />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<App />);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'App',
|
||||
@@ -2653,7 +2677,13 @@ describe('ReactSuspenseList', () => {
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<App />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<App />);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'App',
|
||||
|
||||
+224
-37
@@ -211,7 +211,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Foo',
|
||||
'Bar',
|
||||
@@ -277,7 +283,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
expect(Scheduler).toFlushAndYield(['Foo']);
|
||||
|
||||
// The update will suspend.
|
||||
ReactNoop.render(<Foo renderBar={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo renderBar={true} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo renderBar={true} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Foo',
|
||||
'Bar',
|
||||
@@ -341,14 +353,27 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
// B suspends. Continue rendering the remaining siblings.
|
||||
ReactNoop.render(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<Text text="A" />
|
||||
<AsyncText text="B" />
|
||||
<Text text="C" />
|
||||
<Text text="D" />
|
||||
</Suspense>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<Text text="A" />
|
||||
<AsyncText text="B" />
|
||||
<Text text="C" />
|
||||
<Text text="D" />
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<Text text="A" />
|
||||
<AsyncText text="B" />
|
||||
<Text text="C" />
|
||||
<Text text="D" />
|
||||
</Suspense>,
|
||||
);
|
||||
}
|
||||
// B suspends. Continue rendering the remaining siblings.
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'A',
|
||||
@@ -376,6 +401,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
// TODO: Delete this feature flag.
|
||||
// @gate !replayFailedUnitOfWorkWithInvokeGuardedCallback || !__DEV__
|
||||
// @gate enableCache
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('retries on error', async () => {
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {error: null};
|
||||
@@ -410,7 +436,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
|
||||
ReactNoop.render(<App renderContent={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App renderContent={true} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<App renderContent={true} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
|
||||
@@ -535,6 +567,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
});
|
||||
|
||||
// @gate enableCache
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('keeps working on lower priority work after being pinged', async () => {
|
||||
// Advance the virtual time so that we're close to the edge of a bucket.
|
||||
ReactNoop.expire(149);
|
||||
@@ -552,14 +585,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
|
||||
ReactNoop.render(<App showA={true} showB={false} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App showA={true} showB={false} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<App showA={true} showB={false} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
|
||||
// Advance React's virtual time by enough to fall into a new async bucket,
|
||||
// but not enough to expire the suspense timeout.
|
||||
ReactNoop.expire(120);
|
||||
ReactNoop.render(<App showA={true} showB={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<App showA={true} showB={true} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<App showA={true} showB={true} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'B', 'Loading...']);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
|
||||
@@ -602,6 +647,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
// times model. Might not make sense in the new model.
|
||||
// TODO: This test doesn't over what it was originally designed to test.
|
||||
// Either rewrite or delete.
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('tries each subsequent level after suspending', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
@@ -636,17 +682,35 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
|
||||
// Schedule an update at several distinct expiration times
|
||||
await ReactNoop.act(async () => {
|
||||
root.render(<App step={1} shouldSuspend={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App step={1} shouldSuspend={true} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App step={1} shouldSuspend={true} />);
|
||||
}
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
|
||||
interrupt();
|
||||
|
||||
root.render(<App step={2} shouldSuspend={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App step={2} shouldSuspend={true} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App step={2} shouldSuspend={true} />);
|
||||
}
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
|
||||
interrupt();
|
||||
|
||||
root.render(<App step={3} shouldSuspend={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App step={3} shouldSuspend={true} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App step={3} shouldSuspend={true} />);
|
||||
}
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
|
||||
interrupt();
|
||||
@@ -864,6 +928,52 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('does not expire for transitions', async () => {
|
||||
ReactNoop.render(
|
||||
<Fragment>
|
||||
<Suspense fallback={<Text text="Loading..." />} />
|
||||
</Fragment>,
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<Fragment>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText text="Async" />
|
||||
</Suspense>
|
||||
<Text text="Sync" />
|
||||
</Fragment>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// The async child suspends
|
||||
'Suspend! [Async]',
|
||||
'Loading...',
|
||||
// Continue on the sibling
|
||||
'Sync',
|
||||
]);
|
||||
// The update hasn't expired yet, so we commit nothing.
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
|
||||
// Advance both React's virtual time and Jest's timers,
|
||||
// but not by enough to flush the promise or reach the true expiration time.
|
||||
ReactNoop.expire(2000);
|
||||
await advanceTimers(2000);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
|
||||
// Even flushing won't yield a fallback in a transition.
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
|
||||
// Once the promise resolves, we render the suspended view
|
||||
await resolveText('Async');
|
||||
expect(Scheduler).toFlushAndYield(['Async', 'Sync']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
|
||||
});
|
||||
|
||||
// @gate enableCache
|
||||
it('resolves successfully even if fallback render is pending', async () => {
|
||||
ReactNoop.render(
|
||||
@@ -873,13 +983,25 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText text="Async" />
|
||||
</Suspense>
|
||||
</>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText text="Async" />
|
||||
</Suspense>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText text="Async" />
|
||||
</Suspense>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
|
||||
|
||||
await resolveText('Async');
|
||||
@@ -928,11 +1050,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
|
||||
ReactNoop.render(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText text="Async" />
|
||||
</Suspense>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText text="Async" />
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText text="Async" />
|
||||
</Suspense>,
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
|
||||
@@ -1725,6 +1857,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
});
|
||||
|
||||
// @gate enableCache
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('suspends for longer if something took a long (CPU bound) time to render', async () => {
|
||||
function Foo({renderContent}) {
|
||||
Scheduler.unstable_yieldValue('Foo');
|
||||
@@ -1738,7 +1871,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
ReactNoop.render(<Foo />);
|
||||
expect(Scheduler).toFlushAndYield(['Foo']);
|
||||
|
||||
ReactNoop.render(<Foo renderContent={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo renderContent={true} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo renderContent={true} />);
|
||||
}
|
||||
Scheduler.unstable_advanceTime(100);
|
||||
await advanceTimers(100);
|
||||
// Start rendering
|
||||
@@ -1767,12 +1906,22 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
await advanceTimers(500);
|
||||
// No need to rerender.
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
// Since this is a transition, we never fallback.
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
} else {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
|
||||
}
|
||||
|
||||
// Flush the promise completely
|
||||
await resolveText('A');
|
||||
// Renders successfully
|
||||
expect(Scheduler).toFlushAndYield(['A']);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
// TODO: Why does this render Foo
|
||||
expect(Scheduler).toFlushAndYield(['Foo', 'A']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield(['A']);
|
||||
}
|
||||
expect(ReactNoop.getChildren()).toEqual([span('A')]);
|
||||
});
|
||||
|
||||
@@ -1913,7 +2062,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
ReactNoop.render(<Foo />);
|
||||
expect(Scheduler).toFlushAndYield(['Foo']);
|
||||
|
||||
ReactNoop.render(<Foo renderContent={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo renderContent={true} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo renderContent={true} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
|
||||
|
||||
// Advance some time.
|
||||
@@ -1938,7 +2093,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
// updates as way earlier in the past. This test ensures that we don't
|
||||
// use this assumption to add a very long JND.
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
// Transitions never fallback.
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
} else {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: flip to "warns" when this is implemented again.
|
||||
@@ -2223,7 +2383,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
expect(Scheduler).toFlushAndYield(['Foo', 'A']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('A')]);
|
||||
|
||||
ReactNoop.render(<Foo showB={true} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo showB={true} />);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(<Foo showB={true} />);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Foo',
|
||||
@@ -2238,7 +2404,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
Scheduler.unstable_advanceTime(600);
|
||||
await advanceTimers(600);
|
||||
|
||||
expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
// Transitions never fall back.
|
||||
expect(ReactNoop.getChildren()).toEqual([span('A')]);
|
||||
} else {
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('A'),
|
||||
span('Loading B...'),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// @gate enableCache
|
||||
@@ -2757,14 +2931,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
await ReactNoop.act(async () => {
|
||||
// Update. Since showing a fallback would hide content that's already
|
||||
// visible, it should suspend for a JND without committing.
|
||||
root.render(<App text="First update" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App text="First update" />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App text="First update" />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [First update]']);
|
||||
|
||||
// Should not display a fallback
|
||||
expect(root).toMatchRenderedOutput(<span prop="Initial" />);
|
||||
|
||||
// Update again. This should also suspend for a JND.
|
||||
root.render(<App text="Second update" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App text="Second update" />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App text="Second update" />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [Second update]']);
|
||||
|
||||
// Should not display a fallback
|
||||
@@ -3540,6 +3726,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
|
||||
// @gate experimental
|
||||
// @gate enableCache
|
||||
// @gate !enableSyncDefaultUpdates
|
||||
it('regression: ping at high priority causes update to be dropped', async () => {
|
||||
const {useState, unstable_useTransition: useTransition} = React;
|
||||
|
||||
|
||||
@@ -452,7 +452,11 @@ describe('updaters', () => {
|
||||
};
|
||||
const LowPriorityUpdater = () => {
|
||||
const [count, setCount] = React.useState(0);
|
||||
triggerLowPriorityUpdate = () => setCount(prevCount => prevCount + 1);
|
||||
triggerLowPriorityUpdate = () => {
|
||||
React.unstable_startTransition(() => {
|
||||
setCount(prevCount => prevCount + 1);
|
||||
});
|
||||
};
|
||||
Scheduler.unstable_yieldValue(`LowPriorityUpdater ${count}`);
|
||||
return <Yield value={`LowPriority ${count}`} />;
|
||||
};
|
||||
|
||||
@@ -13,12 +13,19 @@
|
||||
// This test is *.internal so that it can import this shared file.
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
|
||||
// Hard-coding because importing will not work with bundle tests and to
|
||||
// avoid leaking exports for lanes that are only imported in this test.
|
||||
const ReactFiberLane = {
|
||||
SyncLane: /* */ 0b0000000000000000000000000000001,
|
||||
DefaultLane: /* */ 0b0000000000000000000000000010000,
|
||||
TransitionLane1: /* */ 0b0000000000000000000000001000000,
|
||||
};
|
||||
|
||||
describe('SchedulingProfiler', () => {
|
||||
let React;
|
||||
let ReactTestRenderer;
|
||||
let ReactNoop;
|
||||
let Scheduler;
|
||||
let ReactFiberLane;
|
||||
|
||||
let clearedMarks;
|
||||
let featureDetectionMarkName = null;
|
||||
@@ -82,11 +89,6 @@ describe('SchedulingProfiler', () => {
|
||||
|
||||
const SchedulingProfiler = require('react-reconciler/src/SchedulingProfiler');
|
||||
formatLanes = SchedulingProfiler.formatLanes;
|
||||
|
||||
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFiberLane = ReactFeatureFlags.enableNewReconciler
|
||||
? require('react-reconciler/src/ReactFiberLane.new')
|
||||
: require('react-reconciler/src/ReactFiberLane.old');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -147,6 +149,7 @@ describe('SchedulingProfiler', () => {
|
||||
});
|
||||
|
||||
// @gate enableSchedulingProfiler
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should mark render yields', async () => {
|
||||
function Bar() {
|
||||
Scheduler.unstable_yieldValue('Bar');
|
||||
@@ -158,16 +161,33 @@ describe('SchedulingProfiler', () => {
|
||||
return <Bar />;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
// Do one step of work.
|
||||
expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(<Foo />);
|
||||
});
|
||||
|
||||
expectMarksToEqual([
|
||||
`--react-init-${ReactVersion}`,
|
||||
`--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`,
|
||||
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
|
||||
'--render-yield',
|
||||
]);
|
||||
// Do one step of work.
|
||||
expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
|
||||
|
||||
expectMarksToEqual([
|
||||
`--react-init-${ReactVersion}`,
|
||||
`--schedule-render-${formatLanes(ReactFiberLane.TransitionLane1)}`,
|
||||
`--render-start-${formatLanes(ReactFiberLane.TransitionLane1)}`,
|
||||
'--render-yield',
|
||||
]);
|
||||
} else {
|
||||
ReactNoop.render(<Foo />);
|
||||
|
||||
// Do one step of work.
|
||||
expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
|
||||
|
||||
expectMarksToEqual([
|
||||
`--react-init-${ReactVersion}`,
|
||||
`--schedule-render-${formatLanes(ReactFiberLane.DefaultLane)}`,
|
||||
`--render-start-${formatLanes(ReactFiberLane.DefaultLane)}`,
|
||||
'--render-yield',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// @gate enableSchedulingProfiler
|
||||
|
||||
@@ -168,10 +168,18 @@ describe('SchedulingProfiler labels', () => {
|
||||
event.initEvent('mouseover', true, true);
|
||||
dispatchAndSetCurrentEvent(targetRef.current, event);
|
||||
});
|
||||
expect(clearedMarks).toContain(
|
||||
`--schedule-state-update-${formatLanes(
|
||||
ReactFiberLane.InputContinuousLane,
|
||||
)}-App`,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
expect(clearedMarks).toContain(
|
||||
`--schedule-state-update-${formatLanes(
|
||||
ReactFiberLane.DefaultLane,
|
||||
)}-App`,
|
||||
);
|
||||
} else {
|
||||
expect(clearedMarks).toContain(
|
||||
`--schedule-state-update-${formatLanes(
|
||||
ReactFiberLane.InputContinuousLane,
|
||||
)}-App`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -213,24 +213,45 @@ describe('useMutableSource', () => {
|
||||
const mutableSource = createMutableSource(source, param => param.version);
|
||||
|
||||
act(() => {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
() => Scheduler.unstable_yieldValue('Sync effect'),
|
||||
);
|
||||
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
() => Scheduler.unstable_yieldValue('Sync effect'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
() => Scheduler.unstable_yieldValue('Sync effect'),
|
||||
);
|
||||
}
|
||||
// Do enough work to read from one component
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
|
||||
|
||||
@@ -431,7 +452,13 @@ describe('useMutableSource', () => {
|
||||
|
||||
// Changing values should schedule an update with React.
|
||||
// Start working on this update but don't finish it.
|
||||
source.value = 'two';
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
source.value = 'two';
|
||||
});
|
||||
} else {
|
||||
source.value = 'two';
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a:two']);
|
||||
|
||||
// Re-renders that occur before the update is processed
|
||||
@@ -695,29 +722,57 @@ describe('useMutableSource', () => {
|
||||
|
||||
// Because the store has not changed yet, there are no pending updates,
|
||||
// so it is considered safe to read from when we start this render.
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={getSnapshotA}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeA}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
<Component
|
||||
label="c"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
</>,
|
||||
() => Scheduler.unstable_yieldValue('Sync effect'),
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={getSnapshotA}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeA}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
<Component
|
||||
label="c"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
</>,
|
||||
() => Scheduler.unstable_yieldValue('Sync effect'),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={getSnapshotA}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeA}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
<Component
|
||||
label="c"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
</>,
|
||||
() => Scheduler.unstable_yieldValue('Sync effect'),
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a:a:one', 'b:b:one']);
|
||||
|
||||
// Mutating the source should trigger a tear detection on the next read,
|
||||
@@ -808,22 +863,43 @@ describe('useMutableSource', () => {
|
||||
|
||||
act(() => {
|
||||
// Start a render that uses the mutable source.
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
|
||||
|
||||
// Mutate source
|
||||
@@ -1457,13 +1533,25 @@ describe('useMutableSource', () => {
|
||||
expect(root).toMatchRenderedOutput('a0');
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<>
|
||||
<Read getSnapshot={getSnapshotA} />
|
||||
<Read getSnapshot={getSnapshotB} />
|
||||
<Text text="c" />
|
||||
</>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(
|
||||
<>
|
||||
<Read getSnapshot={getSnapshotA} />
|
||||
<Read getSnapshot={getSnapshotB} />
|
||||
<Text text="c" />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
root.render(
|
||||
<>
|
||||
<Read getSnapshot={getSnapshotA} />
|
||||
<Read getSnapshot={getSnapshotB} />
|
||||
<Text text="c" />
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a0', 'b0']);
|
||||
// Mutate in an event. This schedules a subscription update on a, which
|
||||
@@ -1481,10 +1569,16 @@ describe('useMutableSource', () => {
|
||||
mutateB('b0');
|
||||
});
|
||||
// Finish the current render
|
||||
expect(Scheduler).toFlushUntilNextPaint(['c']);
|
||||
// a0 will re-render because of the mutation update. But it should show
|
||||
// the latest value, not the intermediate one, to avoid tearing with b.
|
||||
expect(Scheduler).toFlushUntilNextPaint(['a0']);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
// Default sync will flush both without yielding
|
||||
expect(Scheduler).toFlushUntilNextPaint(['c', 'a0']);
|
||||
} else {
|
||||
expect(Scheduler).toFlushUntilNextPaint(['c']);
|
||||
// a0 will re-render because of the mutation update. But it should show
|
||||
// the latest value, not the intermediate one, to avoid tearing with b.
|
||||
expect(Scheduler).toFlushUntilNextPaint(['a0']);
|
||||
}
|
||||
|
||||
expect(root).toMatchRenderedOutput('a0b0c');
|
||||
// We should be done.
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
@@ -1591,7 +1685,13 @@ describe('useMutableSource', () => {
|
||||
|
||||
await act(async () => {
|
||||
// Switch the parent and the child to read using the same config
|
||||
root.render(<App parentConfig={configB} childConfig={configB} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App parentConfig={configB} childConfig={configB} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App parentConfig={configB} childConfig={configB} />);
|
||||
}
|
||||
// Start rendering the parent, but yield before rendering the child
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Parent: 2']);
|
||||
|
||||
@@ -1602,25 +1702,41 @@ describe('useMutableSource', () => {
|
||||
source.valueB = '3';
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
// The partial render completes
|
||||
'Child: 2',
|
||||
'Commit: 2, 2',
|
||||
]);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
// In default sync mode, all of the updates flush sync.
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
// The partial render completes
|
||||
'Child: 2',
|
||||
'Commit: 2, 2',
|
||||
'Parent: 3',
|
||||
'Child: 3',
|
||||
]);
|
||||
|
||||
// Now there are two pending mutations at different priorities. But they
|
||||
// both read the same version of the mutable source, so we must render
|
||||
// them simultaneously.
|
||||
//
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Parent: 3',
|
||||
// Demonstrates that we can yield here
|
||||
]);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// Now finish the rest of the update
|
||||
'Child: 3',
|
||||
'Commit: 3, 3',
|
||||
]);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// Now finish the rest of the update
|
||||
'Commit: 3, 3',
|
||||
]);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
// The partial render completes
|
||||
'Child: 2',
|
||||
'Commit: 2, 2',
|
||||
]);
|
||||
|
||||
// Now there are two pending mutations at different priorities. But they
|
||||
// both read the same version of the mutable source, so we must render
|
||||
// them simultaneously.
|
||||
//
|
||||
expect(Scheduler).toFlushAndYieldThrough([
|
||||
'Parent: 3',
|
||||
// Demonstrates that we can yield here
|
||||
]);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// Now finish the rest of the update
|
||||
'Child: 3',
|
||||
'Commit: 3, 3',
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1855,22 +1971,43 @@ describe('useMutableSource', () => {
|
||||
|
||||
act(() => {
|
||||
// Start a render that uses the mutable source.
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
|
||||
|
||||
const PrevScheduler = Scheduler;
|
||||
@@ -1915,22 +2052,43 @@ describe('useMutableSource', () => {
|
||||
|
||||
act(() => {
|
||||
// Start a render that uses the mutable source.
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactNoop.render(
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
|
||||
|
||||
const PrevScheduler = Scheduler;
|
||||
|
||||
+52
-18
@@ -262,7 +262,13 @@ describe('useMutableSourceHydration', () => {
|
||||
});
|
||||
expect(() => {
|
||||
act(() => {
|
||||
root.render(<TestComponent />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<TestComponent />);
|
||||
});
|
||||
} else {
|
||||
root.render(<TestComponent />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
|
||||
source.value = 'two';
|
||||
});
|
||||
@@ -316,22 +322,43 @@ describe('useMutableSourceHydration', () => {
|
||||
});
|
||||
expect(() => {
|
||||
act(() => {
|
||||
root.render(
|
||||
<>
|
||||
<Component
|
||||
label="0"
|
||||
getSnapshot={getSnapshotA}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeA}
|
||||
/>
|
||||
<Component
|
||||
label="1"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(
|
||||
<>
|
||||
<Component
|
||||
label="0"
|
||||
getSnapshot={getSnapshotA}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeA}
|
||||
/>
|
||||
<Component
|
||||
label="1"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
root.render(
|
||||
<>
|
||||
<Component
|
||||
label="0"
|
||||
getSnapshot={getSnapshotA}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeA}
|
||||
/>
|
||||
<Component
|
||||
label="1"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['0:a:one']);
|
||||
source.valueB = 'b:two';
|
||||
});
|
||||
@@ -344,6 +371,7 @@ describe('useMutableSourceHydration', () => {
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
// @gate !enableSyncDefaultUpdates
|
||||
it('should detect a tear during a higher priority interruption', () => {
|
||||
const source = createSource('one');
|
||||
const mutableSource = createMutableSource(source, param => param.version);
|
||||
@@ -386,7 +414,13 @@ describe('useMutableSourceHydration', () => {
|
||||
|
||||
expect(() => {
|
||||
act(() => {
|
||||
root.render(<TestComponent flag={1} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<TestComponent flag={1} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<TestComponent flag={1} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough([1]);
|
||||
|
||||
// Render an update which will be higher priority than the hydration.
|
||||
|
||||
@@ -73,6 +73,7 @@ describe('ReactTestRendererAsync', () => {
|
||||
expect(renderer.toJSON()).toEqual(['A:2', 'B:2', 'C:2']);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('flushThrough flushes until the expected values is yielded', () => {
|
||||
function Child(props) {
|
||||
Scheduler.unstable_yieldValue(props.children);
|
||||
@@ -87,9 +88,19 @@ describe('ReactTestRendererAsync', () => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
const renderer = ReactTestRenderer.create(<Parent step={1} />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
|
||||
let renderer;
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
renderer = ReactTestRenderer.create(<Parent step={1} />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
renderer = ReactTestRenderer.create(<Parent step={1} />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Flush the first two siblings
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A:1', 'B:1']);
|
||||
@@ -101,6 +112,7 @@ describe('ReactTestRendererAsync', () => {
|
||||
expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('supports high priority interruptions', () => {
|
||||
function Child(props) {
|
||||
Scheduler.unstable_yieldValue(props.children);
|
||||
@@ -124,9 +136,18 @@ describe('ReactTestRendererAsync', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const renderer = ReactTestRenderer.create(<Example step={1} />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
let renderer;
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
renderer = ReactTestRenderer.create(<Example step={1} />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
renderer = ReactTestRenderer.create(<Example step={1} />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Flush the some of the changes, but don't commit
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A:1']);
|
||||
|
||||
@@ -123,11 +123,21 @@ describe('ReactDOMTracing', () => {
|
||||
SchedulerTracing.unstable_trace('initialization', 0, () => {
|
||||
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
|
||||
act(() => {
|
||||
root.render(
|
||||
<React.Profiler id="test" onRender={onRender}>
|
||||
<App />
|
||||
</React.Profiler>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(
|
||||
<React.Profiler id="test" onRender={onRender}>
|
||||
<App />
|
||||
</React.Profiler>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
root.render(
|
||||
<React.Profiler id="test" onRender={onRender}>
|
||||
<App />
|
||||
</React.Profiler>,
|
||||
);
|
||||
}
|
||||
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
|
||||
interaction,
|
||||
@@ -140,6 +150,7 @@ describe('ReactDOMTracing', () => {
|
||||
);
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Child', 'Child:mount']);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
expect(onRender).toHaveLastRenderedWithInteractions(
|
||||
new Set([interaction]),
|
||||
@@ -275,11 +286,21 @@ describe('ReactDOMTracing', () => {
|
||||
SchedulerTracing.unstable_trace('initialization', 0, () => {
|
||||
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
|
||||
act(() => {
|
||||
root.render(
|
||||
<React.Profiler id="test" onRender={onRender}>
|
||||
<App />
|
||||
</React.Profiler>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(
|
||||
<React.Profiler id="test" onRender={onRender}>
|
||||
<App />
|
||||
</React.Profiler>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
root.render(
|
||||
<React.Profiler id="test" onRender={onRender}>
|
||||
<App />
|
||||
</React.Profiler>,
|
||||
);
|
||||
}
|
||||
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
|
||||
interaction,
|
||||
@@ -307,16 +328,13 @@ describe('ReactDOMTracing', () => {
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(interaction);
|
||||
if (gate(flags => flags.enableUseJSStackToTrackPassiveDurations)) {
|
||||
expect(onRender).toHaveBeenCalledTimes(3);
|
||||
} else {
|
||||
// TODO: This is 4 instead of 3 because this update was scheduled at
|
||||
// idle priority, and idle updates are slightly higher priority than
|
||||
// offscreen work. So it takes two render passes to finish it. Profiler
|
||||
// calls `onRender` for the first render even though everything
|
||||
// bails out.
|
||||
expect(onRender).toHaveBeenCalledTimes(4);
|
||||
}
|
||||
|
||||
// TODO: This is 4 instead of 3 because this update was scheduled at
|
||||
// idle priority, and idle updates are slightly higher priority than
|
||||
// offscreen work. So it takes two render passes to finish it. Profiler
|
||||
// calls `onRender` for the first render even though everything
|
||||
// bails out.
|
||||
expect(onRender).toHaveBeenCalledTimes(4);
|
||||
expect(onRender).toHaveLastRenderedWithInteractions(
|
||||
new Set([interaction]),
|
||||
);
|
||||
@@ -503,7 +521,13 @@ describe('ReactDOMTracing', () => {
|
||||
scheduleUpdateWithHidden(),
|
||||
);
|
||||
});
|
||||
scheduleUpdate();
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
scheduleUpdate();
|
||||
});
|
||||
} else {
|
||||
scheduleUpdate();
|
||||
}
|
||||
expect(interaction).not.toBeNull();
|
||||
expect(onRender).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
|
||||
@@ -580,7 +604,13 @@ describe('ReactDOMTracing', () => {
|
||||
SchedulerTracing.unstable_trace('initialization', 0, () => {
|
||||
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
|
||||
// This render is only CPU bound. Nothing suspends.
|
||||
root.render(<App />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.render(<App />);
|
||||
});
|
||||
} else {
|
||||
root.render(<App />);
|
||||
}
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['A']);
|
||||
|
||||
@@ -246,6 +246,7 @@ describe('Profiler', () => {
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('is not invoked until the commit phase', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
@@ -254,15 +255,29 @@ describe('Profiler', () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
ReactTestRenderer.create(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield value="first" />
|
||||
<Yield value="last" />
|
||||
</React.Profiler>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactTestRenderer.create(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield value="first" />
|
||||
<Yield value="last" />
|
||||
</React.Profiler>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactTestRenderer.create(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield value="first" />
|
||||
<Yield value="last" />
|
||||
</React.Profiler>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Times are logged until a render is committed.
|
||||
expect(Scheduler).toFlushAndYieldThrough(['first']);
|
||||
@@ -796,6 +811,7 @@ describe('Profiler', () => {
|
||||
});
|
||||
|
||||
describe('with regard to interruptions', () => {
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should accumulate actual time after a scheduling interruptions', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
@@ -808,13 +824,25 @@ describe('Profiler', () => {
|
||||
Scheduler.unstable_advanceTime(5); // 0 -> 5
|
||||
|
||||
// Render partially, but run out of time before completing.
|
||||
ReactTestRenderer.create(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield renderTime={2} />
|
||||
<Yield renderTime={3} />
|
||||
</React.Profiler>,
|
||||
{unstable_isConcurrent: true},
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactTestRenderer.create(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield renderTime={2} />
|
||||
<Yield renderTime={3} />
|
||||
</React.Profiler>,
|
||||
{unstable_isConcurrent: true},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactTestRenderer.create(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield renderTime={2} />
|
||||
<Yield renderTime={3} />
|
||||
</React.Profiler>,
|
||||
{unstable_isConcurrent: true},
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Yield:2']);
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
|
||||
@@ -830,6 +858,7 @@ describe('Profiler', () => {
|
||||
expect(call[5]).toBe(10); // commit time
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should not include time between frames', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
@@ -843,16 +872,31 @@ describe('Profiler', () => {
|
||||
|
||||
// Render partially, but don't finish.
|
||||
// This partial render should take 5ms of simulated time.
|
||||
ReactTestRenderer.create(
|
||||
<React.Profiler id="outer" onRender={callback}>
|
||||
<Yield renderTime={5} />
|
||||
<Yield renderTime={10} />
|
||||
<React.Profiler id="inner" onRender={callback}>
|
||||
<Yield renderTime={17} />
|
||||
</React.Profiler>
|
||||
</React.Profiler>,
|
||||
{unstable_isConcurrent: true},
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
ReactTestRenderer.create(
|
||||
<React.Profiler id="outer" onRender={callback}>
|
||||
<Yield renderTime={5} />
|
||||
<Yield renderTime={10} />
|
||||
<React.Profiler id="inner" onRender={callback}>
|
||||
<Yield renderTime={17} />
|
||||
</React.Profiler>
|
||||
</React.Profiler>,
|
||||
{unstable_isConcurrent: true},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ReactTestRenderer.create(
|
||||
<React.Profiler id="outer" onRender={callback}>
|
||||
<Yield renderTime={5} />
|
||||
<Yield renderTime={10} />
|
||||
<React.Profiler id="inner" onRender={callback}>
|
||||
<Yield renderTime={17} />
|
||||
</React.Profiler>
|
||||
</React.Profiler>,
|
||||
{unstable_isConcurrent: true},
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Yield:5']);
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
|
||||
@@ -880,6 +924,7 @@ describe('Profiler', () => {
|
||||
expect(outerCall[5]).toBe(87); // commit time
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should report the expected times when a high-pri update replaces a mount in-progress', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
@@ -893,13 +938,26 @@ describe('Profiler', () => {
|
||||
|
||||
// Render a partially update, but don't finish.
|
||||
// This partial render should take 10ms of simulated time.
|
||||
const renderer = ReactTestRenderer.create(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield renderTime={10} />
|
||||
<Yield renderTime={20} />
|
||||
</React.Profiler>,
|
||||
{unstable_isConcurrent: true},
|
||||
);
|
||||
let renderer;
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
renderer = ReactTestRenderer.create(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield renderTime={10} />
|
||||
<Yield renderTime={20} />
|
||||
</React.Profiler>,
|
||||
{unstable_isConcurrent: true},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
renderer = ReactTestRenderer.create(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield renderTime={10} />
|
||||
<Yield renderTime={20} />
|
||||
</React.Profiler>,
|
||||
{unstable_isConcurrent: true},
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Yield:10']);
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
|
||||
@@ -933,6 +991,7 @@ describe('Profiler', () => {
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should report the expected times when a high-priority update replaces a low-priority update', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
@@ -968,13 +1027,25 @@ describe('Profiler', () => {
|
||||
|
||||
// Render a partially update, but don't finish.
|
||||
// This partial render should take 3ms of simulated time.
|
||||
renderer.update(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield renderTime={3} />
|
||||
<Yield renderTime={5} />
|
||||
<Yield renderTime={9} />
|
||||
</React.Profiler>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
renderer.update(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield renderTime={3} />
|
||||
<Yield renderTime={5} />
|
||||
<Yield renderTime={9} />
|
||||
</React.Profiler>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
renderer.update(
|
||||
<React.Profiler id="test" onRender={callback}>
|
||||
<Yield renderTime={3} />
|
||||
<Yield renderTime={5} />
|
||||
<Yield renderTime={9} />
|
||||
</React.Profiler>,
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Yield:3']);
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
|
||||
@@ -1014,6 +1085,7 @@ describe('Profiler', () => {
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
@@ -1080,7 +1152,13 @@ describe('Profiler', () => {
|
||||
|
||||
// Render a partially update, but don't finish.
|
||||
// This partial render will take 10ms of actual render time.
|
||||
first.setState({renderTime: 10});
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
first.setState({renderTime: 10});
|
||||
});
|
||||
} else {
|
||||
first.setState({renderTime: 10});
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']);
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
|
||||
@@ -3343,6 +3421,7 @@ describe('Profiler', () => {
|
||||
).toHaveBeenLastNotifiedOfInteraction(interaction);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should associate traced events with their subsequent commits', () => {
|
||||
let instance = null;
|
||||
|
||||
@@ -3459,10 +3538,19 @@ describe('Profiler', () => {
|
||||
interactionOne.name,
|
||||
Scheduler.unstable_now(),
|
||||
() => {
|
||||
instance.setState({count: 1});
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
instance.setState({count: 1});
|
||||
|
||||
// Update state again to verify our traced interaction isn't registered twice
|
||||
instance.setState({count: 2});
|
||||
// Update state again to verify our traced interaction isn't registered twice
|
||||
instance.setState({count: 2});
|
||||
});
|
||||
} else {
|
||||
instance.setState({count: 1});
|
||||
|
||||
// Update state again to verify our traced interaction isn't registered twice
|
||||
instance.setState({count: 2});
|
||||
}
|
||||
|
||||
// The scheduler/tracing package will notify of work started for the default thread,
|
||||
// But React shouldn't notify until it's been flushed.
|
||||
@@ -3655,6 +3743,7 @@ describe('Profiler', () => {
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should report the expected times when a high-priority update interrupts a low-priority update', () => {
|
||||
const onPostCommit = jest.fn(() => {
|
||||
Scheduler.unstable_yieldValue('onPostCommit');
|
||||
@@ -3715,7 +3804,13 @@ describe('Profiler', () => {
|
||||
Scheduler.unstable_now(),
|
||||
() => {
|
||||
// Render a partially update, but don't finish.
|
||||
first.setState({count: 1});
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
first.setState({count: 1});
|
||||
});
|
||||
} else {
|
||||
first.setState({count: 1});
|
||||
}
|
||||
|
||||
expect(onWorkScheduled).toHaveBeenCalled();
|
||||
expect(onWorkScheduled.mock.calls[0][0]).toMatchInteractions([
|
||||
@@ -4590,6 +4685,7 @@ describe('Profiler', () => {
|
||||
).toMatchInteraction(initialRenderInteraction);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('handles high-pri renderers between suspended and resolved (async) trees', async () => {
|
||||
// Set up an initial shell. We need to set this up before the test sceanrio
|
||||
// because we want initial render to suspend on navigation to the initial state.
|
||||
@@ -4614,14 +4710,27 @@ describe('Profiler', () => {
|
||||
initialRenderInteraction.name,
|
||||
initialRenderInteraction.timestamp,
|
||||
() => {
|
||||
renderer.update(
|
||||
<React.Profiler id="app" onCommit={onCommit}>
|
||||
<React.Suspense fallback={<Text text="loading" />}>
|
||||
<AsyncText text="loaded" ms={100} />
|
||||
</React.Suspense>
|
||||
<Text text="initial" />
|
||||
</React.Profiler>,
|
||||
);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
renderer.update(
|
||||
<React.Profiler id="app" onCommit={onCommit}>
|
||||
<React.Suspense fallback={<Text text="loading" />}>
|
||||
<AsyncText text="loaded" ms={100} />
|
||||
</React.Suspense>
|
||||
<Text text="initial" />
|
||||
</React.Profiler>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
renderer.update(
|
||||
<React.Profiler id="app" onCommit={onCommit}>
|
||||
<React.Suspense fallback={<Text text="loading" />}>
|
||||
<AsyncText text="loaded" ms={100} />
|
||||
</React.Suspense>
|
||||
<Text text="initial" />
|
||||
</React.Profiler>,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
|
||||
@@ -178,6 +178,7 @@ describe('ReactProfiler DevTools integration', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('regression test: #17159', () => {
|
||||
function Text({text}) {
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
@@ -195,7 +196,13 @@ describe('ReactProfiler DevTools integration', () => {
|
||||
// for updates.
|
||||
Scheduler.unstable_advanceTime(10000);
|
||||
// Schedule an update.
|
||||
root.update(<Text text="B" />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
root.update(<Text text="B" />);
|
||||
});
|
||||
} else {
|
||||
root.update(<Text text="B" />);
|
||||
}
|
||||
|
||||
// Update B should not instantly expire.
|
||||
expect(Scheduler).toFlushAndYieldThrough([]);
|
||||
|
||||
@@ -170,3 +170,5 @@ export const enableRecursiveCommitTraversal = false;
|
||||
export const disableSchedulerTimeoutInWorkLoop = false;
|
||||
|
||||
export const enableLazyContextPropagation = false;
|
||||
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
|
||||
@@ -60,6 +60,7 @@ export const enableUseRefAccessWarning = false;
|
||||
export const enableRecursiveCommitTraversal = false;
|
||||
export const disableSchedulerTimeoutInWorkLoop = false;
|
||||
export const enableLazyContextPropagation = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
|
||||
export const enableRecursiveCommitTraversal = false;
|
||||
export const disableSchedulerTimeoutInWorkLoop = false;
|
||||
export const enableLazyContextPropagation = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
|
||||
export const enableRecursiveCommitTraversal = false;
|
||||
export const disableSchedulerTimeoutInWorkLoop = false;
|
||||
export const enableLazyContextPropagation = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
|
||||
export const enableRecursiveCommitTraversal = false;
|
||||
export const disableSchedulerTimeoutInWorkLoop = false;
|
||||
export const enableLazyContextPropagation = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
|
||||
export const enableRecursiveCommitTraversal = false;
|
||||
export const disableSchedulerTimeoutInWorkLoop = false;
|
||||
export const enableLazyContextPropagation = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
|
||||
export const enableRecursiveCommitTraversal = false;
|
||||
export const disableSchedulerTimeoutInWorkLoop = false;
|
||||
export const enableLazyContextPropagation = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
|
||||
export const enableRecursiveCommitTraversal = false;
|
||||
export const disableSchedulerTimeoutInWorkLoop = false;
|
||||
export const enableLazyContextPropagation = false;
|
||||
export const enableSyncDefaultUpdates = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -59,3 +59,4 @@ export const deletedTreeCleanUpLevel = __VARIANT__ ? 3 : 1;
|
||||
export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
|
||||
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
|
||||
export const enableLazyContextPropagation = __VARIANT__;
|
||||
export const enableSyncDefaultUpdates = __VARIANT__;
|
||||
|
||||
@@ -34,6 +34,7 @@ export const {
|
||||
disableSchedulerTimeoutInWorkLoop,
|
||||
enableLazyContextPropagation,
|
||||
deletedTreeCleanUpLevel,
|
||||
enableSyncDefaultUpdates,
|
||||
} = dynamicFeatureFlags;
|
||||
|
||||
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
|
||||
|
||||
@@ -262,6 +262,7 @@ describe('useSubscription', () => {
|
||||
expect(subscriptions).toHaveLength(2);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should ignore values emitted by a new subscribable until the commit phase', () => {
|
||||
const log = [];
|
||||
|
||||
@@ -331,7 +332,14 @@ describe('useSubscription', () => {
|
||||
|
||||
// Start React update, but don't finish
|
||||
act(() => {
|
||||
renderer.update(<Parent observed={observableB} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
renderer.update(<Parent observed={observableB} />);
|
||||
});
|
||||
} else {
|
||||
renderer.update(<Parent observed={observableB} />);
|
||||
}
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Child: b-0']);
|
||||
expect(log).toEqual(['Parent.componentDidMount']);
|
||||
|
||||
@@ -362,6 +370,7 @@ describe('useSubscription', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should not drop values emitted between updates', () => {
|
||||
const log = [];
|
||||
|
||||
@@ -432,7 +441,13 @@ describe('useSubscription', () => {
|
||||
|
||||
// Start React update, but don't finish
|
||||
act(() => {
|
||||
renderer.update(<Parent observed={observableB} />);
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
renderer.update(<Parent observed={observableB} />);
|
||||
});
|
||||
} else {
|
||||
renderer.update(<Parent observed={observableB} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['Child: b-0']);
|
||||
expect(log).toEqual([]);
|
||||
|
||||
@@ -561,6 +576,7 @@ describe('useSubscription', () => {
|
||||
Scheduler.unstable_flushAll();
|
||||
});
|
||||
|
||||
// @gate experimental || !enableSyncDefaultUpdates
|
||||
it('should not tear if a mutation occurs during a concurrent update', () => {
|
||||
const input = document.createElement('input');
|
||||
|
||||
@@ -608,9 +624,21 @@ describe('useSubscription', () => {
|
||||
// Interrupt with a second mutation "C" -> "D".
|
||||
// This update will not be eagerly evaluated,
|
||||
// but useSubscription() should eagerly close over the updated value to avoid tearing.
|
||||
mutate('C');
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
mutate('C');
|
||||
});
|
||||
} else {
|
||||
mutate('C');
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['render:first:C']);
|
||||
mutate('D');
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.unstable_startTransition(() => {
|
||||
mutate('D');
|
||||
});
|
||||
} else {
|
||||
mutate('D');
|
||||
}
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'render:second:C',
|
||||
'render:first:D',
|
||||
|
||||
Reference in New Issue
Block a user