Run Placeholder tests in persistent mode, too (#15013)

* Convert ReactSuspensePlaceholder tests to use noop

Instead of the test renderer, since test renderer does not support
running in persistent mode.

* Run Placeholder tests in persistent mode, too

* Fix Flow and lint

* Hidden text instances should have correct host context

Adds a test for a subtle edge case that only occurs in persistent mode.

* createHiddenTextInstance -> cloneHiddenTextInstance

This sidesteps the problem where createHiddenTextInstance needs access
to the host context.
This commit is contained in:
Andrew Clark 2019-03-08 18:53:14 -08:00 committed by GitHub
parent d0289c7e3a
commit 3f4852fa5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 328 additions and 167 deletions

View File

@ -406,10 +406,17 @@ export function cloneUnhiddenInstance(
};
}
export function createHiddenTextInstance(
export function cloneHiddenTextInstance(
instance: Instance,
text: string,
internalInstanceHandle: Object,
): TextInstance {
throw new Error('Not yet implemented.');
}
export function cloneUnhiddenTextInstance(
instance: Instance,
text: string,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): TextInstance {
throw new Error('Not yet implemented.');

View File

@ -41,10 +41,18 @@ type Instance = {|
text: string | null,
prop: any,
hidden: boolean,
context: HostContext,
|};
type TextInstance = {|text: string, id: number, hidden: boolean|};
type TextInstance = {|
text: string,
id: number,
hidden: boolean,
context: HostContext,
|};
type HostContext = Object;
const NO_CONTEXT = {};
const UPPERCASE_CONTEXT = {};
const UPDATE_SIGNAL = {};
if (__DEV__) {
Object.freeze(NO_CONTEXT);
@ -190,10 +198,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
type: type,
children: keepChildren ? instance.children : [],
text: shouldSetTextContent(type, newProps)
? (newProps.children: any) + ''
? computeText((newProps.children: any) + '', instance.context)
: null,
prop: newProps.prop,
hidden: newProps.hidden === true,
context: instance.context,
};
Object.defineProperty(clone, 'id', {
value: clone.id,
@ -203,6 +212,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
value: clone.text,
enumerable: false,
});
Object.defineProperty(clone, 'context', {
value: clone.context,
enumerable: false,
});
hostCloneCounter++;
return clone;
}
@ -216,12 +229,23 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
);
}
function computeText(rawText, hostContext) {
return hostContext === UPPERCASE_CONTEXT ? rawText.toUpperCase() : rawText;
}
const sharedHostConfig = {
getRootHostContext() {
return NO_CONTEXT;
},
getChildHostContext() {
getChildHostContext(
parentHostContext: HostContext,
type: string,
rootcontainerInstance: Container,
) {
if (type === 'uppercase') {
return UPPERCASE_CONTEXT;
}
return NO_CONTEXT;
},
@ -229,7 +253,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return instance;
},
createInstance(type: string, props: Props): Instance {
createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): Instance {
if (type === 'errorInCompletePhase') {
throw new Error('Error in host config.');
}
@ -238,10 +267,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
type: type,
children: [],
text: shouldSetTextContent(type, props)
? (props.children: any) + ''
? computeText((props.children: any) + '', hostContext)
: null,
prop: props.prop,
hidden: props.hidden === true,
context: hostContext,
};
// Hide from unit tests
Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
@ -249,6 +279,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
value: inst.text,
enumerable: false,
});
Object.defineProperty(inst, 'context', {
value: inst.context,
enumerable: false,
});
return inst;
},
@ -298,9 +332,21 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
hostContext: Object,
internalInstanceHandle: Object,
): TextInstance {
const inst = {text: text, id: instanceCounter++, hidden: false};
if (hostContext === UPPERCASE_CONTEXT) {
text = text.toUpperCase();
}
const inst = {
text: text,
id: instanceCounter++,
hidden: false,
context: hostContext,
};
// Hide from unit tests
Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
Object.defineProperty(inst, 'context', {
value: inst.context,
enumerable: false,
});
return inst;
},
@ -343,7 +389,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
instance.prop = newProps.prop;
instance.hidden = newProps.hidden === true;
if (shouldSetTextContent(type, newProps)) {
instance.text = (newProps.children: any) + '';
instance.text = computeText(
(newProps.children: any) + '',
instance.context,
);
}
},
@ -353,7 +402,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
newText: string,
): void {
hostUpdateCounter++;
textInstance.text = newText;
textInstance.text = computeText(newText, textInstance.context);
},
appendChild,
@ -453,23 +502,54 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
true,
null,
);
clone.hidden = props.hidden;
clone.hidden = props.hidden === true;
return clone;
},
createHiddenTextInstance(
cloneHiddenTextInstance(
instance: TextInstance,
text: string,
rootContainerInstance: Container,
hostContext: Object,
internalInstanceHandle: Object,
): TextInstance {
const inst = {text: text, id: instanceCounter++, hidden: true};
const clone = {
text: instance.text,
id: instanceCounter++,
hidden: true,
context: instance.context,
};
// Hide from unit tests
Object.defineProperty(inst, 'id', {
value: inst.id,
Object.defineProperty(clone, 'id', {
value: clone.id,
enumerable: false,
});
return inst;
Object.defineProperty(clone, 'context', {
value: clone.context,
enumerable: false,
});
return clone;
},
cloneUnhiddenTextInstance(
instance: TextInstance,
text: string,
internalInstanceHandle: Object,
): TextInstance {
const clone = {
text: instance.text,
id: instanceCounter++,
hidden: false,
context: instance.context,
};
// Hide from unit tests
Object.defineProperty(clone, 'id', {
value: clone.id,
enumerable: false,
});
Object.defineProperty(clone, 'context', {
value: clone.context,
enumerable: false,
});
return clone;
},
};

View File

@ -1131,6 +1131,13 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
commitSuspenseComponent(finishedWork);
return;
}
}
commitContainer(finishedWork);
@ -1199,50 +1206,7 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
return;
}
case SuspenseComponent: {
let newState: SuspenseState | null = finishedWork.memoizedState;
let newDidTimeout;
let primaryChildParent = finishedWork;
if (newState === null) {
newDidTimeout = false;
} else {
newDidTimeout = true;
primaryChildParent = finishedWork.child;
if (newState.timedOutAt === NoWork) {
// If the children had not already timed out, record the time.
// This is used to compute the elapsed time during subsequent
// attempts to render the children.
newState.timedOutAt = requestCurrentTime();
}
}
if (primaryChildParent !== null) {
hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
}
// If this boundary just timed out, then it will have a set of thenables.
// For each thenable, attach a listener so that when it resolves, React
// attempts to re-render the boundary in the primary (pre-timeout) state.
const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
if (thenables !== null) {
finishedWork.updateQueue = null;
let retryCache = finishedWork.stateNode;
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
thenables.forEach(thenable => {
// Memoize using the boundary fiber to prevent redundant listeners.
let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
if (enableSchedulerTracing) {
retry = Schedule_tracing_wrap(retry);
}
if (!retryCache.has(thenable)) {
retryCache.add(thenable);
thenable.then(retry, retry);
}
});
}
commitSuspenseComponent(finishedWork);
return;
}
case IncompleteClassComponent: {
@ -1258,6 +1222,52 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
}
function commitSuspenseComponent(finishedWork: Fiber) {
let newState: SuspenseState | null = finishedWork.memoizedState;
let newDidTimeout;
let primaryChildParent = finishedWork;
if (newState === null) {
newDidTimeout = false;
} else {
newDidTimeout = true;
primaryChildParent = finishedWork.child;
if (newState.timedOutAt === NoWork) {
// If the children had not already timed out, record the time.
// This is used to compute the elapsed time during subsequent
// attempts to render the children.
newState.timedOutAt = requestCurrentTime();
}
}
if (supportsMutation && primaryChildParent !== null) {
hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
}
// If this boundary just timed out, then it will have a set of thenables.
// For each thenable, attach a listener so that when it resolves, React
// attempts to re-render the boundary in the primary (pre-timeout) state.
const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
if (thenables !== null) {
finishedWork.updateQueue = null;
let retryCache = finishedWork.stateNode;
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
thenables.forEach(thenable => {
// Memoize using the boundary fiber to prevent redundant listeners.
let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
if (enableSchedulerTracing) {
retry = Schedule_tracing_wrap(retry);
}
if (!retryCache.has(thenable)) {
retryCache.add(thenable);
thenable.then(retry, retry);
}
});
}
}
function commitResetTextContent(current: Fiber) {
if (!supportsMutation) {
return;

View File

@ -17,7 +17,6 @@ import type {
Container,
ChildSet,
} from './ReactFiberHostConfig';
import type {SuspenseState} from './ReactFiberSuspenseComponent';
import {
IndeterminateComponent,
@ -53,7 +52,6 @@ import invariant from 'shared/invariant';
import {
createInstance,
createTextInstance,
createHiddenTextInstance,
appendInitialChild,
finalizeInitialChildren,
prepareUpdate,
@ -62,6 +60,8 @@ import {
cloneInstance,
cloneHiddenInstance,
cloneUnhiddenInstance,
cloneHiddenTextInstance,
cloneUnhiddenTextInstance,
createContainerChildSet,
appendChildToContainerChildSet,
finalizeContainerChildren,
@ -228,22 +228,10 @@ if (supportsMutation) {
let instance = node.stateNode;
if (needsVisibilityToggle) {
const text = node.memoizedProps;
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
if (isHidden) {
instance = createHiddenTextInstance(
text,
rootContainerInstance,
currentHostContext,
workInProgress,
);
instance = cloneHiddenTextInstance(instance, text, node);
} else {
instance = createTextInstance(
text,
rootContainerInstance,
currentHostContext,
workInProgress,
);
instance = cloneUnhiddenTextInstance(instance, text, node);
}
node.stateNode = instance;
}
@ -253,20 +241,19 @@ if (supportsMutation) {
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.tag === SuspenseComponent) {
const current = node.alternate;
if (current !== null) {
const oldState: SuspenseState = current.memoizedState;
const newState: SuspenseState = node.memoizedState;
const oldIsHidden = oldState !== null;
const newIsHidden = newState !== null;
if (oldIsHidden !== newIsHidden) {
// The placeholder either just timed out or switched back to the normal
// children after having previously timed out. Toggle the visibility of
// the direct host children.
const primaryChildParent = newIsHidden ? node.child : node;
if ((node.effectTag & Update) !== NoEffect) {
// Need to toggle the visibility of the primary children.
const newIsHidden = node.memoizedState !== null;
if (newIsHidden) {
const primaryChildParent = node.child;
if (primaryChildParent !== null) {
appendAllChildren(parent, primaryChildParent, true, newIsHidden);
node = primaryChildParent.sibling;
continue;
}
} else {
const primaryChildParent = node;
appendAllChildren(parent, primaryChildParent, true, newIsHidden);
// eslint-disable-next-line no-labels
break branches;
}
@ -331,22 +318,10 @@ if (supportsMutation) {
let instance = node.stateNode;
if (needsVisibilityToggle) {
const text = node.memoizedProps;
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
if (isHidden) {
instance = createHiddenTextInstance(
text,
rootContainerInstance,
currentHostContext,
workInProgress,
);
instance = cloneHiddenTextInstance(instance, text, node);
} else {
instance = createTextInstance(
text,
rootContainerInstance,
currentHostContext,
workInProgress,
);
instance = cloneUnhiddenTextInstance(instance, text, node);
}
node.stateNode = instance;
}
@ -356,17 +331,11 @@ if (supportsMutation) {
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.tag === SuspenseComponent) {
const current = node.alternate;
if (current !== null) {
const oldState: SuspenseState = current.memoizedState;
const newState: SuspenseState = node.memoizedState;
const oldIsHidden = oldState !== null;
const newIsHidden = newState !== null;
if (oldIsHidden !== newIsHidden) {
// The placeholder either just timed out or switched back to the normal
// children after having previously timed out. Toggle the visibility of
// the direct host children.
const primaryChildParent = newIsHidden ? node.child : node;
if ((node.effectTag & Update) !== NoEffect) {
// Need to toggle the visibility of the primary children.
const newIsHidden = node.memoizedState !== null;
if (newIsHidden) {
const primaryChildParent = node.child;
if (primaryChildParent !== null) {
appendAllChildrenToContainer(
containerChildSet,
@ -374,7 +343,17 @@ if (supportsMutation) {
true,
newIsHidden,
);
node = primaryChildParent.sibling;
continue;
}
} else {
const primaryChildParent = node;
appendAllChildrenToContainer(
containerChildSet,
primaryChildParent,
true,
newIsHidden,
);
// eslint-disable-next-line no-labels
break branches;
}

View File

@ -8,12 +8,9 @@
* @jest-environment node
*/
// TODO: This does nothing since it was migrated from noop renderer to test
// renderer! Switch back to noop renderer, or add persistent mode to test
// renderer, or merge the two renderers into one somehow.
// runPlaceholderTests('ReactSuspensePlaceholder (mutation)', () =>
// require('react-noop-renderer'),
// );
runPlaceholderTests('ReactSuspensePlaceholder (mutation)', () =>
require('react-noop-renderer'),
);
runPlaceholderTests('ReactSuspensePlaceholder (persistence)', () =>
require('react-noop-renderer/persistent'),
);
@ -21,7 +18,7 @@ runPlaceholderTests('ReactSuspensePlaceholder (persistence)', () =>
function runPlaceholderTests(suiteLabel, loadReactNoop) {
let Profiler;
let React;
let ReactTestRenderer;
let ReactNoop;
let Scheduler;
let ReactFeatureFlags;
let ReactCache;
@ -38,7 +35,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
ReactFeatureFlags.enableProfilerTimer = true;
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
React = require('react');
ReactTestRenderer = require('react-test-renderer');
ReactNoop = loadReactNoop();
Scheduler = require('scheduler');
ReactCache = require('react-cache');
@ -134,9 +131,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
}
// Initial mount
const root = ReactTestRenderer.create(<App middleText="B" />, {
unstable_isConcurrent: true,
});
ReactNoop.render(<App middleText="B" />);
expect(Scheduler).toFlushAndYield([
'A',
@ -144,14 +139,14 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
'C',
'Loading...',
]);
expect(root).toMatchRenderedOutput(null);
expect(ReactNoop).toMatchRenderedOutput(null);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
expect(root).toMatchRenderedOutput(
expect(ReactNoop).toMatchRenderedOutput(
<React.Fragment>
<span hidden={true}>A</span>
<span>B</span>
@ -160,13 +155,20 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
);
// Update
root.update(<App middleText="B2" />);
ReactNoop.render(<App middleText="B2" />);
expect(Scheduler).toFlushAndYield(['Suspend! [B2]', 'C', 'Loading...']);
// Time out the update
jest.advanceTimersByTime(750);
expect(Scheduler).toFlushAndYield([]);
expect(root).toMatchRenderedOutput('Loading...');
expect(ReactNoop).toMatchRenderedOutput(
<React.Fragment>
<span hidden={true}>A</span>
<span hidden={true}>B</span>
<span hidden={true}>C</span>
Loading...
</React.Fragment>,
);
// Resolve the promise
jest.advanceTimersByTime(1000);
@ -175,7 +177,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
// Render the final update. A should still be hidden, because it was
// given a `hidden` prop.
expect(root).toMatchRenderedOutput(
expect(ReactNoop).toMatchRenderedOutput(
<React.Fragment>
<span hidden={true}>A</span>
<span>B2</span>
@ -196,9 +198,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
}
// Initial mount
const root = ReactTestRenderer.create(<App middleText="B" />, {
unstable_isConcurrent: true,
});
ReactNoop.render(<App middleText="B" />);
expect(Scheduler).toFlushAndYield([
'A',
@ -207,15 +207,15 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
'Loading...',
]);
expect(root).toMatchRenderedOutput(null);
expect(ReactNoop).toMatchRenderedOutput(null);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
expect(root).toMatchRenderedOutput('ABC');
expect(ReactNoop).toMatchRenderedOutput('ABC');
// Update
root.update(<App middleText="B2" />);
ReactNoop.render(<App middleText="B2" />);
expect(Scheduler).toFlushAndYield([
'A',
'Suspend! [B2]',
@ -225,7 +225,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
// Time out the update
jest.advanceTimersByTime(750);
expect(Scheduler).toFlushAndYield([]);
expect(root).toMatchRenderedOutput('Loading...');
expect(ReactNoop).toMatchRenderedOutput('Loading...');
// Resolve the promise
jest.advanceTimersByTime(1000);
@ -234,7 +234,64 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
// Render the final update. A should still be hidden, because it was
// given a `hidden` prop.
expect(root).toMatchRenderedOutput('AB2C');
expect(ReactNoop).toMatchRenderedOutput('AB2C');
});
it('preserves host context for text nodes', () => {
function App(props) {
return (
// uppercase is a special type that causes React Noop to render child
// text nodes as uppercase.
<uppercase>
<Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
<Text text="a" />
<AsyncText ms={1000} text={props.middleText} />
<Text text="c" />
</Suspense>
</uppercase>
);
}
// Initial mount
ReactNoop.render(<App middleText="b" />);
expect(Scheduler).toFlushAndYield([
'a',
'Suspend! [b]',
'c',
'Loading...',
]);
expect(ReactNoop).toMatchRenderedOutput(null);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [b]']);
expect(Scheduler).toFlushAndYield(['a', 'b', 'c']);
expect(ReactNoop).toMatchRenderedOutput(<uppercase>ABC</uppercase>);
// Update
ReactNoop.render(<App middleText="b2" />);
expect(Scheduler).toFlushAndYield([
'a',
'Suspend! [b2]',
'c',
'Loading...',
]);
// Time out the update
jest.advanceTimersByTime(750);
expect(Scheduler).toFlushAndYield([]);
expect(ReactNoop).toMatchRenderedOutput(
<uppercase>LOADING...</uppercase>,
);
// Resolve the promise
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [b2]']);
expect(Scheduler).toFlushAndYield(['a', 'b2', 'c']);
// Render the final update. A should still be hidden, because it was
// given a `hidden` prop.
expect(ReactNoop).toMatchRenderedOutput(<uppercase>AB2C</uppercase>);
});
describe('profiler durations', () => {
@ -272,8 +329,15 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
describe('when suspending during mount', () => {
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
const root = ReactTestRenderer.create(<App shouldSuspend={true} />);
expect(root.toJSON()).toEqual('Loading...');
ReactNoop.renderLegacySyncRoot(<App shouldSuspend={true} />);
expect(Scheduler).toHaveYielded([
'App',
'Suspending',
'Suspend! [Loaded]',
'Text',
'Fallback',
]);
expect(ReactNoop).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(1);
// Initial mount only shows the "Loading..." Fallback.
@ -284,7 +348,11 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
jest.advanceTimersByTime(1000);
expect(root.toJSON()).toEqual(['Loaded', 'Text']);
expect(Scheduler).toHaveYielded([
'Promise resolved [Loaded]',
'Loaded',
]);
expect(ReactNoop).toMatchRenderedOutput('LoadedText');
expect(onRender).toHaveBeenCalledTimes(2);
// When the suspending data is resolved and our final UI is rendered,
@ -295,9 +363,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
});
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
const root = ReactTestRenderer.create(<App shouldSuspend={true} />, {
unstable_isConcurrent: true,
});
ReactNoop.render(<App shouldSuspend={true} />);
expect(Scheduler).toFlushAndYield([
'App',
@ -306,11 +372,11 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
'Text',
'Fallback',
]);
expect(root).toMatchRenderedOutput(null);
expect(ReactNoop).toMatchRenderedOutput(null);
// Show the fallback UI.
jest.advanceTimersByTime(750);
expect(root).toMatchRenderedOutput('Loading...');
expect(ReactNoop).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(1);
// Initial mount only shows the "Loading..." Fallback.
@ -323,7 +389,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
jest.advanceTimersByTime(250);
expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
expect(Scheduler).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
expect(root).toMatchRenderedOutput('LoadedText');
expect(ReactNoop).toMatchRenderedOutput('LoadedText');
expect(onRender).toHaveBeenCalledTimes(2);
// When the suspending data is resolved and our final UI is rendered,
@ -335,10 +401,11 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
describe('when suspending during update', () => {
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
const root = ReactTestRenderer.create(
ReactNoop.renderLegacySyncRoot(
<App shouldSuspend={false} textRenderDuration={5} />,
);
expect(root.toJSON()).toEqual('Text');
expect(Scheduler).toHaveYielded(['App', 'Text']);
expect(ReactNoop).toMatchRenderedOutput('Text');
expect(onRender).toHaveBeenCalledTimes(1);
// Initial mount only shows the "Text" text.
@ -346,8 +413,15 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
expect(onRender.mock.calls[0][2]).toBe(5);
expect(onRender.mock.calls[0][3]).toBe(5);
root.update(<App shouldSuspend={true} textRenderDuration={5} />);
expect(root.toJSON()).toEqual('Loading...');
ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
expect(Scheduler).toHaveYielded([
'App',
'Suspending',
'Suspend! [Loaded]',
'Text',
'Fallback',
]);
expect(ReactNoop).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(2);
// The suspense update should only show the "Loading..." Fallback.
@ -356,10 +430,17 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
expect(onRender.mock.calls[1][2]).toBe(18);
expect(onRender.mock.calls[1][3]).toBe(18);
root.update(
ReactNoop.renderLegacySyncRoot(
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
);
expect(root.toJSON()).toEqual('Loading...');
expect(Scheduler).toHaveYielded([
'App',
'Suspending',
'Suspend! [Loaded]',
'New',
'Fallback',
]);
expect(ReactNoop).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(3);
// If we force another update while still timed out,
@ -370,7 +451,11 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
jest.advanceTimersByTime(1000);
expect(root.toJSON()).toEqual(['Loaded', 'New']);
expect(Scheduler).toHaveYielded([
'Promise resolved [Loaded]',
'Loaded',
]);
expect(ReactNoop).toMatchRenderedOutput('LoadedNew');
expect(onRender).toHaveBeenCalledTimes(4);
// When the suspending data is resolved and our final UI is rendered,
@ -381,15 +466,12 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
});
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
const root = ReactTestRenderer.create(
ReactNoop.render(
<App shouldSuspend={false} textRenderDuration={5} />,
{
unstable_isConcurrent: true,
},
);
expect(Scheduler).toFlushAndYield(['App', 'Text']);
expect(root).toMatchRenderedOutput('Text');
expect(ReactNoop).toMatchRenderedOutput('Text');
expect(onRender).toHaveBeenCalledTimes(1);
// Initial mount only shows the "Text" text.
@ -397,7 +479,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
expect(onRender.mock.calls[0][2]).toBe(5);
expect(onRender.mock.calls[0][3]).toBe(5);
root.update(<App shouldSuspend={true} textRenderDuration={5} />);
ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
expect(Scheduler).toFlushAndYield([
'App',
'Suspending',
@ -405,11 +487,11 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
'Text',
'Fallback',
]);
expect(root).toMatchRenderedOutput('Text');
expect(ReactNoop).toMatchRenderedOutput('Text');
// Show the fallback UI.
jest.advanceTimersByTime(750);
expect(root).toMatchRenderedOutput('Loading...');
expect(ReactNoop).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(2);
// The suspense update should only show the "Loading..." Fallback.
@ -421,7 +503,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
expect(onRender.mock.calls[1][3]).toBe(15);
// Update again while timed out.
root.update(
ReactNoop.render(
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
);
expect(Scheduler).toFlushAndYield([
@ -431,7 +513,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
'New',
'Fallback',
]);
expect(root).toMatchRenderedOutput('Loading...');
expect(ReactNoop).toMatchRenderedOutput('Loading...');
expect(onRender).toHaveBeenCalledTimes(2);
// Resolve the pending promise.

View File

@ -92,7 +92,9 @@ export const finalizeContainerChildren =
export const replaceContainerChildren = $$$hostConfig.replaceContainerChildren;
export const cloneHiddenInstance = $$$hostConfig.cloneHiddenInstance;
export const cloneUnhiddenInstance = $$$hostConfig.cloneUnhiddenInstance;
export const createHiddenTextInstance = $$$hostConfig.createHiddenTextInstance;
export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
export const cloneUnhiddenTextInstance =
$$$hostConfig.cloneUnhiddenTextInstance;
// -------------------
// Hydration

View File

@ -30,4 +30,5 @@ export const finalizeContainerChildren = shim;
export const replaceContainerChildren = shim;
export const cloneHiddenInstance = shim;
export const cloneUnhiddenInstance = shim;
export const createHiddenTextInstance = shim;
export const cloneHiddenTextInstance = shim;
export const cloneUnhiddenTextInstance = shim;