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:
parent
d0289c7e3a
commit
3f4852fa5f
|
@ -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.');
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue