Run persistent mode tests in CI (#15029)
* Add command to run tests in persistent mode * Convert Suspense fuzz tester to use noop renderer So we can run it in persistent mode, too. * Don't mutate stateNode in appendAllChildren We can't mutate the stateNode in appendAllChildren because the children could be current. This is a bit weird because now the child that we append is different from the one on the fiber stateNode. I think this makes conceptual sense, but I suspect this likely breaks an assumption in Fabric. With this approach, we no longer need to clone to unhide the children, so I removed those host config methods. Fixes bug surfaced by fuzz tester. (The test case that failed was the one that's already hard coded.) * In persistent mode, disable test that reads a ref Refs behave differently in persistent mode. I added a TODO to write a persistent mode version of this test. * Run persistent mode tests in CI * test-persistent should skip files without noop If a file doesn't reference react-noop-renderer, we shouldn't bother running it in persistent mode, since the results will be identical to the normal test run. * Remove module constructor from placeholder tests We don't need this now that we have the ability to run any test file in either mutation or persistent mode. * Revert "test-persistent should skip files without noop" Seb objected to adding shelljs as a dep and I'm too lazy to worry about Windows support so whatever I'll just revert this. * Delete duplicate file
This commit is contained in:
parent
3f4852fa5f
commit
bc8bd24c14
|
@ -100,6 +100,7 @@
|
|||
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js",
|
||||
"debug-test": "cross-env NODE_ENV=development node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.source.js --runInBand",
|
||||
"test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js",
|
||||
"test-persistent": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-persistent.js",
|
||||
"test-fire": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-fire.js",
|
||||
"test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js",
|
||||
"test-fire-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source-fire.js",
|
||||
|
|
|
@ -387,25 +387,6 @@ export function cloneHiddenInstance(
|
|||
};
|
||||
}
|
||||
|
||||
export function cloneUnhiddenInstance(
|
||||
instance: Instance,
|
||||
type: string,
|
||||
props: Props,
|
||||
internalInstanceHandle: Object,
|
||||
): Instance {
|
||||
const viewConfig = instance.canonical.viewConfig;
|
||||
const node = instance.node;
|
||||
const updatePayload = diff(
|
||||
{...props, style: [props.style, {display: 'none'}]},
|
||||
props,
|
||||
viewConfig.validAttributes,
|
||||
);
|
||||
return {
|
||||
node: cloneNodeWithNewProps(node, updatePayload),
|
||||
canonical: instance.canonical,
|
||||
};
|
||||
}
|
||||
|
||||
export function cloneHiddenTextInstance(
|
||||
instance: Instance,
|
||||
text: string,
|
||||
|
@ -414,14 +395,6 @@ export function cloneHiddenTextInstance(
|
|||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function cloneUnhiddenTextInstance(
|
||||
instance: Instance,
|
||||
text: string,
|
||||
internalInstanceHandle: Object,
|
||||
): TextInstance {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function createContainerChildSet(container: Container): ChildSet {
|
||||
return createChildNodeSet(container);
|
||||
}
|
||||
|
|
|
@ -486,26 +486,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
return clone;
|
||||
},
|
||||
|
||||
cloneUnhiddenInstance(
|
||||
instance: Instance,
|
||||
type: string,
|
||||
props: Props,
|
||||
internalInstanceHandle: Object,
|
||||
): Instance {
|
||||
const clone = cloneInstance(
|
||||
instance,
|
||||
null,
|
||||
type,
|
||||
props,
|
||||
props,
|
||||
internalInstanceHandle,
|
||||
true,
|
||||
null,
|
||||
);
|
||||
clone.hidden = props.hidden === true;
|
||||
return clone;
|
||||
},
|
||||
|
||||
cloneHiddenTextInstance(
|
||||
instance: TextInstance,
|
||||
text: string,
|
||||
|
@ -528,29 +508,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
});
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
const NoopRenderer = reconciler(hostConfig);
|
||||
|
|
|
@ -59,9 +59,7 @@ import {
|
|||
supportsPersistence,
|
||||
cloneInstance,
|
||||
cloneHiddenInstance,
|
||||
cloneUnhiddenInstance,
|
||||
cloneHiddenTextInstance,
|
||||
cloneUnhiddenTextInstance,
|
||||
createContainerChildSet,
|
||||
appendChildToContainerChildSet,
|
||||
finalizeContainerChildren,
|
||||
|
@ -209,31 +207,19 @@ if (supportsMutation) {
|
|||
// eslint-disable-next-line no-labels
|
||||
branches: if (node.tag === HostComponent) {
|
||||
let instance = node.stateNode;
|
||||
if (needsVisibilityToggle) {
|
||||
if (needsVisibilityToggle && isHidden) {
|
||||
// This child is inside a timed out tree. Hide it.
|
||||
const props = node.memoizedProps;
|
||||
const type = node.type;
|
||||
if (isHidden) {
|
||||
// This child is inside a timed out tree. Hide it.
|
||||
instance = cloneHiddenInstance(instance, type, props, node);
|
||||
} else {
|
||||
// This child was previously inside a timed out tree. If it was not
|
||||
// updated during this render, it may need to be unhidden. Clone
|
||||
// again to be sure.
|
||||
instance = cloneUnhiddenInstance(instance, type, props, node);
|
||||
}
|
||||
node.stateNode = instance;
|
||||
instance = cloneHiddenInstance(instance, type, props, node);
|
||||
}
|
||||
appendInitialChild(parent, instance);
|
||||
} else if (node.tag === HostText) {
|
||||
let instance = node.stateNode;
|
||||
if (needsVisibilityToggle) {
|
||||
if (needsVisibilityToggle && isHidden) {
|
||||
// This child is inside a timed out tree. Hide it.
|
||||
const text = node.memoizedProps;
|
||||
if (isHidden) {
|
||||
instance = cloneHiddenTextInstance(instance, text, node);
|
||||
} else {
|
||||
instance = cloneUnhiddenTextInstance(instance, text, node);
|
||||
}
|
||||
node.stateNode = instance;
|
||||
instance = cloneHiddenTextInstance(instance, text, node);
|
||||
}
|
||||
appendInitialChild(parent, instance);
|
||||
} else if (node.tag === HostPortal) {
|
||||
|
@ -247,15 +233,22 @@ if (supportsMutation) {
|
|||
if (newIsHidden) {
|
||||
const primaryChildParent = node.child;
|
||||
if (primaryChildParent !== null) {
|
||||
appendAllChildren(parent, primaryChildParent, true, newIsHidden);
|
||||
node = primaryChildParent.sibling;
|
||||
continue;
|
||||
if (primaryChildParent.child !== null) {
|
||||
primaryChildParent.child.return = primaryChildParent;
|
||||
appendAllChildren(
|
||||
parent,
|
||||
primaryChildParent,
|
||||
true,
|
||||
newIsHidden,
|
||||
);
|
||||
}
|
||||
const fallbackChildParent = primaryChildParent.sibling;
|
||||
if (fallbackChildParent !== null) {
|
||||
fallbackChildParent.return = node;
|
||||
node = fallbackChildParent;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const primaryChildParent = node;
|
||||
appendAllChildren(parent, primaryChildParent, true, newIsHidden);
|
||||
// eslint-disable-next-line no-labels
|
||||
break branches;
|
||||
}
|
||||
}
|
||||
if (node.child !== null) {
|
||||
|
@ -299,31 +292,19 @@ if (supportsMutation) {
|
|||
// eslint-disable-next-line no-labels
|
||||
branches: if (node.tag === HostComponent) {
|
||||
let instance = node.stateNode;
|
||||
if (needsVisibilityToggle) {
|
||||
if (needsVisibilityToggle && isHidden) {
|
||||
// This child is inside a timed out tree. Hide it.
|
||||
const props = node.memoizedProps;
|
||||
const type = node.type;
|
||||
if (isHidden) {
|
||||
// This child is inside a timed out tree. Hide it.
|
||||
instance = cloneHiddenInstance(instance, type, props, node);
|
||||
} else {
|
||||
// This child was previously inside a timed out tree. If it was not
|
||||
// updated during this render, it may need to be unhidden. Clone
|
||||
// again to be sure.
|
||||
instance = cloneUnhiddenInstance(instance, type, props, node);
|
||||
}
|
||||
node.stateNode = instance;
|
||||
instance = cloneHiddenInstance(instance, type, props, node);
|
||||
}
|
||||
appendChildToContainerChildSet(containerChildSet, instance);
|
||||
} else if (node.tag === HostText) {
|
||||
let instance = node.stateNode;
|
||||
if (needsVisibilityToggle) {
|
||||
if (needsVisibilityToggle && isHidden) {
|
||||
// This child is inside a timed out tree. Hide it.
|
||||
const text = node.memoizedProps;
|
||||
if (isHidden) {
|
||||
instance = cloneHiddenTextInstance(instance, text, node);
|
||||
} else {
|
||||
instance = cloneUnhiddenTextInstance(instance, text, node);
|
||||
}
|
||||
node.stateNode = instance;
|
||||
instance = cloneHiddenTextInstance(instance, text, node);
|
||||
}
|
||||
appendChildToContainerChildSet(containerChildSet, instance);
|
||||
} else if (node.tag === HostPortal) {
|
||||
|
@ -337,25 +318,22 @@ if (supportsMutation) {
|
|||
if (newIsHidden) {
|
||||
const primaryChildParent = node.child;
|
||||
if (primaryChildParent !== null) {
|
||||
appendAllChildrenToContainer(
|
||||
containerChildSet,
|
||||
primaryChildParent,
|
||||
true,
|
||||
newIsHidden,
|
||||
);
|
||||
node = primaryChildParent.sibling;
|
||||
continue;
|
||||
if (primaryChildParent.child !== null) {
|
||||
primaryChildParent.child.return = primaryChildParent;
|
||||
appendAllChildrenToContainer(
|
||||
containerChildSet,
|
||||
primaryChildParent,
|
||||
true,
|
||||
newIsHidden,
|
||||
);
|
||||
}
|
||||
const fallbackChildParent = primaryChildParent.sibling;
|
||||
if (fallbackChildParent !== null) {
|
||||
fallbackChildParent.return = node;
|
||||
node = fallbackChildParent;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const primaryChildParent = node;
|
||||
appendAllChildrenToContainer(
|
||||
containerChildSet,
|
||||
primaryChildParent,
|
||||
true,
|
||||
newIsHidden,
|
||||
);
|
||||
// eslint-disable-next-line no-labels
|
||||
break branches;
|
||||
}
|
||||
}
|
||||
if (node.child !== null) {
|
||||
|
@ -714,11 +692,23 @@ function completeWork(
|
|||
}
|
||||
}
|
||||
|
||||
if (nextDidTimeout || prevDidTimeout) {
|
||||
// If the children are hidden, or if they were previous hidden, schedule
|
||||
// an effect to toggle their visibility. This is also used to attach a
|
||||
// retry listener to the promise.
|
||||
workInProgress.effectTag |= Update;
|
||||
if (supportsPersistence) {
|
||||
if (nextDidTimeout) {
|
||||
// If this boundary just timed out, schedule an effect to attach a
|
||||
// retry listener to the proimse. This flag is also used to hide the
|
||||
// primary children.
|
||||
workInProgress.effectTag |= Update;
|
||||
}
|
||||
}
|
||||
if (supportsMutation) {
|
||||
if (nextDidTimeout || prevDidTimeout) {
|
||||
// If this boundary just timed out, schedule an effect to attach a
|
||||
// retry listener to the proimse. This flag is also used to hide the
|
||||
// primary children. In mutation mode, we also need the flag to
|
||||
// *unhide* children that were previously hidden, so check if the
|
||||
// is currently timed out, too.
|
||||
workInProgress.effectTag |= Update;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
let React;
|
||||
let Suspense;
|
||||
let ReactTestRenderer;
|
||||
let ReactNoop;
|
||||
let Scheduler;
|
||||
let ReactFeatureFlags;
|
||||
let Random;
|
||||
|
@ -26,7 +26,7 @@ describe('ReactSuspenseFuzz', () => {
|
|||
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
React = require('react');
|
||||
Suspense = React.Suspense;
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
Scheduler = require('scheduler');
|
||||
Random = require('random-seed');
|
||||
});
|
||||
|
@ -143,28 +143,16 @@ describe('ReactSuspenseFuzz', () => {
|
|||
return resolvedText;
|
||||
}
|
||||
|
||||
function renderToRoot(
|
||||
root,
|
||||
children,
|
||||
{shouldSuspend} = {shouldSuspend: true},
|
||||
) {
|
||||
root.update(
|
||||
<ShouldSuspendContext.Provider value={shouldSuspend}>
|
||||
{children}
|
||||
</ShouldSuspendContext.Provider>,
|
||||
);
|
||||
function resolveAllTasks() {
|
||||
Scheduler.unstable_flushWithoutYielding();
|
||||
|
||||
let elapsedTime = 0;
|
||||
while (pendingTasks && pendingTasks.size > 0) {
|
||||
if ((elapsedTime += 1000) > 1000000) {
|
||||
throw new Error('Something did not resolve properly.');
|
||||
}
|
||||
ReactTestRenderer.act(() => jest.advanceTimersByTime(1000));
|
||||
ReactNoop.act(() => jest.advanceTimersByTime(1000));
|
||||
Scheduler.unstable_flushWithoutYielding();
|
||||
}
|
||||
|
||||
return root.toJSON();
|
||||
}
|
||||
|
||||
function testResolvedOutput(unwrappedChildren) {
|
||||
|
@ -172,25 +160,32 @@ describe('ReactSuspenseFuzz', () => {
|
|||
<Suspense fallback="Loading...">{unwrappedChildren}</Suspense>
|
||||
);
|
||||
|
||||
const expectedRoot = ReactTestRenderer.create(null);
|
||||
const expectedOutput = renderToRoot(expectedRoot, children, {
|
||||
shouldSuspend: false,
|
||||
});
|
||||
expectedRoot.unmount();
|
||||
resetCache();
|
||||
ReactNoop.renderToRootWithID(
|
||||
<ShouldSuspendContext.Provider value={false}>
|
||||
{children}
|
||||
</ShouldSuspendContext.Provider>,
|
||||
'expected',
|
||||
);
|
||||
resolveAllTasks();
|
||||
const expectedOutput = ReactNoop.getChildrenAsJSX('expected');
|
||||
ReactNoop.renderToRootWithID(null, 'expected');
|
||||
Scheduler.unstable_flushWithoutYielding();
|
||||
|
||||
resetCache();
|
||||
const syncRoot = ReactTestRenderer.create(null);
|
||||
const syncOutput = renderToRoot(syncRoot, children);
|
||||
ReactNoop.renderLegacySyncRoot(children);
|
||||
resolveAllTasks();
|
||||
const syncOutput = ReactNoop.getChildrenAsJSX();
|
||||
expect(syncOutput).toEqual(expectedOutput);
|
||||
syncRoot.unmount();
|
||||
ReactNoop.renderLegacySyncRoot(null);
|
||||
|
||||
resetCache();
|
||||
const concurrentRoot = ReactTestRenderer.create(null, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
const concurrentOutput = renderToRoot(concurrentRoot, children);
|
||||
ReactNoop.renderToRootWithID(children, 'concurrent');
|
||||
Scheduler.unstable_flushWithoutYielding();
|
||||
resolveAllTasks();
|
||||
const concurrentOutput = ReactNoop.getChildrenAsJSX('concurrent');
|
||||
expect(concurrentOutput).toEqual(expectedOutput);
|
||||
concurrentRoot.unmount();
|
||||
ReactNoop.renderToRootWithID(null, 'concurrent');
|
||||
Scheduler.unstable_flushWithoutYielding();
|
||||
}
|
||||
|
||||
|
|
|
@ -8,532 +8,504 @@
|
|||
* @jest-environment node
|
||||
*/
|
||||
|
||||
runPlaceholderTests('ReactSuspensePlaceholder (mutation)', () =>
|
||||
require('react-noop-renderer'),
|
||||
);
|
||||
runPlaceholderTests('ReactSuspensePlaceholder (persistence)', () =>
|
||||
require('react-noop-renderer/persistent'),
|
||||
);
|
||||
let Profiler;
|
||||
let React;
|
||||
let ReactNoop;
|
||||
let Scheduler;
|
||||
let ReactFeatureFlags;
|
||||
let ReactCache;
|
||||
let Suspense;
|
||||
let TextResource;
|
||||
let textResourceShouldFail;
|
||||
|
||||
function runPlaceholderTests(suiteLabel, loadReactNoop) {
|
||||
let Profiler;
|
||||
let React;
|
||||
let ReactNoop;
|
||||
let Scheduler;
|
||||
let ReactFeatureFlags;
|
||||
let ReactCache;
|
||||
let Suspense;
|
||||
let TextResource;
|
||||
let textResourceShouldFail;
|
||||
describe('ReactSuspensePlaceholder', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
describe(suiteLabel, () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
ReactFeatureFlags.enableProfilerTimer = true;
|
||||
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
Scheduler = require('scheduler');
|
||||
ReactCache = require('react-cache');
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
ReactFeatureFlags.enableProfilerTimer = true;
|
||||
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
React = require('react');
|
||||
ReactNoop = loadReactNoop();
|
||||
Scheduler = require('scheduler');
|
||||
ReactCache = require('react-cache');
|
||||
Profiler = React.unstable_Profiler;
|
||||
Suspense = React.Suspense;
|
||||
|
||||
Profiler = React.unstable_Profiler;
|
||||
Suspense = React.Suspense;
|
||||
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let status = 'pending';
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
switch (status) {
|
||||
case 'pending': {
|
||||
if (listeners === null) {
|
||||
listeners = [{resolve, reject}];
|
||||
setTimeout(() => {
|
||||
if (textResourceShouldFail) {
|
||||
Scheduler.yieldValue(`Promise rejected [${text}]`);
|
||||
status = 'rejected';
|
||||
value = new Error('Failed to load: ' + text);
|
||||
listeners.forEach(listener => listener.reject(value));
|
||||
} else {
|
||||
Scheduler.yieldValue(`Promise resolved [${text}]`);
|
||||
status = 'resolved';
|
||||
value = text;
|
||||
listeners.forEach(listener => listener.resolve(value));
|
||||
}
|
||||
}, ms);
|
||||
} else {
|
||||
listeners.push({resolve, reject});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'resolved': {
|
||||
resolve(value);
|
||||
break;
|
||||
}
|
||||
case 'rejected': {
|
||||
reject(value);
|
||||
break;
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let status = 'pending';
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
switch (status) {
|
||||
case 'pending': {
|
||||
if (listeners === null) {
|
||||
listeners = [{resolve, reject}];
|
||||
setTimeout(() => {
|
||||
if (textResourceShouldFail) {
|
||||
Scheduler.yieldValue(`Promise rejected [${text}]`);
|
||||
status = 'rejected';
|
||||
value = new Error('Failed to load: ' + text);
|
||||
listeners.forEach(listener => listener.reject(value));
|
||||
} else {
|
||||
Scheduler.yieldValue(`Promise resolved [${text}]`);
|
||||
status = 'resolved';
|
||||
value = text;
|
||||
listeners.forEach(listener => listener.resolve(value));
|
||||
}
|
||||
}, ms);
|
||||
} else {
|
||||
listeners.push({resolve, reject});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
}, ([text, ms]) => text);
|
||||
textResourceShouldFail = false;
|
||||
});
|
||||
case 'resolved': {
|
||||
resolve(value);
|
||||
break;
|
||||
}
|
||||
case 'rejected': {
|
||||
reject(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}, ([text, ms]) => text);
|
||||
textResourceShouldFail = false;
|
||||
});
|
||||
|
||||
function Text({fakeRenderDuration = 0, text = 'Text'}) {
|
||||
Scheduler.advanceTime(fakeRenderDuration);
|
||||
function Text({fakeRenderDuration = 0, text = 'Text'}) {
|
||||
Scheduler.advanceTime(fakeRenderDuration);
|
||||
Scheduler.yieldValue(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
function AsyncText({fakeRenderDuration = 0, ms, text}) {
|
||||
Scheduler.advanceTime(fakeRenderDuration);
|
||||
try {
|
||||
TextResource.read([text, ms]);
|
||||
Scheduler.yieldValue(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
Scheduler.yieldValue(`Suspend! [${text}]`);
|
||||
} else {
|
||||
Scheduler.yieldValue(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
function AsyncText({fakeRenderDuration = 0, ms, text}) {
|
||||
Scheduler.advanceTime(fakeRenderDuration);
|
||||
try {
|
||||
TextResource.read([text, ms]);
|
||||
it('times out children that are already hidden', () => {
|
||||
class HiddenText extends React.PureComponent {
|
||||
render() {
|
||||
const text = this.props.text;
|
||||
Scheduler.yieldValue(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
Scheduler.yieldValue(`Suspend! [${text}]`);
|
||||
} else {
|
||||
Scheduler.yieldValue(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
return <span hidden={true}>{text}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
it('times out children that are already hidden', () => {
|
||||
class HiddenText extends React.PureComponent {
|
||||
render() {
|
||||
const text = this.props.text;
|
||||
Scheduler.yieldValue(text);
|
||||
return <span hidden={true}>{text}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
function App(props) {
|
||||
return (
|
||||
<Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
|
||||
<HiddenText text="A" />
|
||||
<span>
|
||||
<AsyncText ms={1000} text={props.middleText} />
|
||||
</span>
|
||||
<span>
|
||||
<Text text="C" />
|
||||
</span>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// 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(
|
||||
<React.Fragment>
|
||||
<span hidden={true}>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
</React.Fragment>,
|
||||
);
|
||||
|
||||
// Update
|
||||
ReactNoop.render(<App middleText="B2" />);
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [B2]', 'C', 'Loading...']);
|
||||
|
||||
// Time out the update
|
||||
jest.advanceTimersByTime(750);
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
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);
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
|
||||
expect(Scheduler).toFlushAndYield(['B2', 'C']);
|
||||
|
||||
// Render the final update. A should still be hidden, because it was
|
||||
// given a `hidden` prop.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<React.Fragment>
|
||||
<span hidden={true}>A</span>
|
||||
<span>B2</span>
|
||||
<span>C</span>
|
||||
</React.Fragment>,
|
||||
);
|
||||
});
|
||||
|
||||
it('times out text nodes', async () => {
|
||||
function App(props) {
|
||||
return (
|
||||
<Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
|
||||
<Text text="A" />
|
||||
function App(props) {
|
||||
return (
|
||||
<Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
|
||||
<HiddenText text="A" />
|
||||
<span>
|
||||
<AsyncText ms={1000} text={props.middleText} />
|
||||
</span>
|
||||
<span>
|
||||
<Text text="C" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// 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('ABC');
|
||||
|
||||
// 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('Loading...');
|
||||
|
||||
// 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('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>,
|
||||
</span>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve the promise
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [b2]']);
|
||||
expect(Scheduler).toFlushAndYield(['a', 'b2', 'c']);
|
||||
// Initial mount
|
||||
ReactNoop.render(<App middleText="B" />);
|
||||
|
||||
// Render the final update. A should still be hidden, because it was
|
||||
// given a `hidden` prop.
|
||||
expect(ReactNoop).toMatchRenderedOutput(<uppercase>AB2C</uppercase>);
|
||||
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(
|
||||
<React.Fragment>
|
||||
<span hidden={true}>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
</React.Fragment>,
|
||||
);
|
||||
|
||||
// Update
|
||||
ReactNoop.render(<App middleText="B2" />);
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [B2]', 'C', 'Loading...']);
|
||||
|
||||
// Time out the update
|
||||
jest.advanceTimersByTime(750);
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
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);
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
|
||||
expect(Scheduler).toFlushAndYield(['B2', 'C']);
|
||||
|
||||
// Render the final update. A should still be hidden, because it was
|
||||
// given a `hidden` prop.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<React.Fragment>
|
||||
<span hidden={true}>A</span>
|
||||
<span>B2</span>
|
||||
<span>C</span>
|
||||
</React.Fragment>,
|
||||
);
|
||||
});
|
||||
|
||||
it('times out text nodes', async () => {
|
||||
function App(props) {
|
||||
return (
|
||||
<Suspense maxDuration={500} fallback={<Text text="Loading..." />}>
|
||||
<Text text="A" />
|
||||
<AsyncText ms={1000} text={props.middleText} />
|
||||
<Text text="C" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// 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('ABC');
|
||||
|
||||
// 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('Loading...');
|
||||
|
||||
// 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('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', () => {
|
||||
let App;
|
||||
let onRender;
|
||||
|
||||
beforeEach(() => {
|
||||
// Order of parameters: id, phase, actualDuration, treeBaseDuration
|
||||
onRender = jest.fn();
|
||||
|
||||
const Fallback = () => {
|
||||
Scheduler.yieldValue('Fallback');
|
||||
Scheduler.advanceTime(10);
|
||||
return 'Loading...';
|
||||
};
|
||||
|
||||
const Suspending = () => {
|
||||
Scheduler.yieldValue('Suspending');
|
||||
Scheduler.advanceTime(2);
|
||||
return <AsyncText ms={1000} text="Loaded" fakeRenderDuration={1} />;
|
||||
};
|
||||
|
||||
App = ({shouldSuspend, text = 'Text', textRenderDuration = 5}) => {
|
||||
Scheduler.yieldValue('App');
|
||||
return (
|
||||
<Profiler id="root" onRender={onRender}>
|
||||
<Suspense maxDuration={500} fallback={<Fallback />}>
|
||||
{shouldSuspend && <Suspending />}
|
||||
<Text fakeRenderDuration={textRenderDuration} text={text} />
|
||||
</Suspense>
|
||||
</Profiler>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
describe('profiler durations', () => {
|
||||
let App;
|
||||
let onRender;
|
||||
describe('when suspending during mount', () => {
|
||||
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
|
||||
ReactNoop.renderLegacySyncRoot(<App shouldSuspend={true} />);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'Text',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(1);
|
||||
|
||||
beforeEach(() => {
|
||||
// Order of parameters: id, phase, actualDuration, treeBaseDuration
|
||||
onRender = jest.fn();
|
||||
// Initial mount only shows the "Loading..." Fallback.
|
||||
// The treeBaseDuration then should be 10ms spent rendering Fallback,
|
||||
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
|
||||
expect(onRender.mock.calls[0][2]).toBe(18);
|
||||
expect(onRender.mock.calls[0][3]).toBe(10);
|
||||
|
||||
const Fallback = () => {
|
||||
Scheduler.yieldValue('Fallback');
|
||||
Scheduler.advanceTime(10);
|
||||
return 'Loading...';
|
||||
};
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
const Suspending = () => {
|
||||
Scheduler.yieldValue('Suspending');
|
||||
Scheduler.advanceTime(2);
|
||||
return <AsyncText ms={1000} text="Loaded" fakeRenderDuration={1} />;
|
||||
};
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Promise resolved [Loaded]',
|
||||
'Loaded',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('LoadedText');
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
|
||||
App = ({shouldSuspend, text = 'Text', textRenderDuration = 5}) => {
|
||||
Scheduler.yieldValue('App');
|
||||
return (
|
||||
<Profiler id="root" onRender={onRender}>
|
||||
<Suspense maxDuration={500} fallback={<Fallback />}>
|
||||
{shouldSuspend && <Suspending />}
|
||||
<Text fakeRenderDuration={textRenderDuration} text={text} />
|
||||
</Suspense>
|
||||
</Profiler>
|
||||
);
|
||||
};
|
||||
// When the suspending data is resolved and our final UI is rendered,
|
||||
// the baseDuration should only include the 1ms re-rendering AsyncText,
|
||||
// but the treeBaseDuration should include the full 8ms spent in the tree.
|
||||
expect(onRender.mock.calls[1][2]).toBe(1);
|
||||
expect(onRender.mock.calls[1][3]).toBe(8);
|
||||
});
|
||||
|
||||
describe('when suspending during mount', () => {
|
||||
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
|
||||
ReactNoop.renderLegacySyncRoot(<App shouldSuspend={true} />);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'Text',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(1);
|
||||
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
|
||||
ReactNoop.render(<App shouldSuspend={true} />);
|
||||
|
||||
// Initial mount only shows the "Loading..." Fallback.
|
||||
// The treeBaseDuration then should be 10ms spent rendering Fallback,
|
||||
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
|
||||
expect(onRender.mock.calls[0][2]).toBe(18);
|
||||
expect(onRender.mock.calls[0][3]).toBe(10);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'Text',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
// Show the fallback UI.
|
||||
jest.advanceTimersByTime(750);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Promise resolved [Loaded]',
|
||||
'Loaded',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('LoadedText');
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
// Initial mount only shows the "Loading..." Fallback.
|
||||
// The treeBaseDuration then should be 10ms spent rendering Fallback,
|
||||
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
|
||||
expect(onRender.mock.calls[0][2]).toBe(18);
|
||||
expect(onRender.mock.calls[0][3]).toBe(10);
|
||||
|
||||
// When the suspending data is resolved and our final UI is rendered,
|
||||
// the baseDuration should only include the 1ms re-rendering AsyncText,
|
||||
// but the treeBaseDuration should include the full 8ms spent in the tree.
|
||||
expect(onRender.mock.calls[1][2]).toBe(1);
|
||||
expect(onRender.mock.calls[1][3]).toBe(8);
|
||||
});
|
||||
// Resolve the pending promise.
|
||||
jest.advanceTimersByTime(250);
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
|
||||
expect(Scheduler).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
|
||||
expect(ReactNoop).toMatchRenderedOutput('LoadedText');
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
|
||||
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
|
||||
ReactNoop.render(<App shouldSuspend={true} />);
|
||||
// When the suspending data is resolved and our final UI is rendered,
|
||||
// both times should include the 8ms re-rendering Suspending and AsyncText.
|
||||
expect(onRender.mock.calls[1][2]).toBe(8);
|
||||
expect(onRender.mock.calls[1][3]).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'Text',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||
describe('when suspending during update', () => {
|
||||
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
|
||||
ReactNoop.renderLegacySyncRoot(
|
||||
<App shouldSuspend={false} textRenderDuration={5} />,
|
||||
);
|
||||
expect(Scheduler).toHaveYielded(['App', 'Text']);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Text');
|
||||
expect(onRender).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Show the fallback UI.
|
||||
jest.advanceTimersByTime(750);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(1);
|
||||
// Initial mount only shows the "Text" text.
|
||||
// It should take 5ms to render.
|
||||
expect(onRender.mock.calls[0][2]).toBe(5);
|
||||
expect(onRender.mock.calls[0][3]).toBe(5);
|
||||
|
||||
// Initial mount only shows the "Loading..." Fallback.
|
||||
// The treeBaseDuration then should be 10ms spent rendering Fallback,
|
||||
// but the actualDuration should also include the 8ms spent rendering the hidden tree.
|
||||
expect(onRender.mock.calls[0][2]).toBe(18);
|
||||
expect(onRender.mock.calls[0][3]).toBe(10);
|
||||
ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'Text',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Resolve the pending promise.
|
||||
jest.advanceTimersByTime(250);
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
|
||||
expect(Scheduler).toFlushAndYield(['Suspending', 'Loaded', 'Text']);
|
||||
expect(ReactNoop).toMatchRenderedOutput('LoadedText');
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
// The suspense update should only show the "Loading..." Fallback.
|
||||
// Both durations should include 10ms spent rendering Fallback
|
||||
// plus the 8ms rendering the (hidden) components.
|
||||
expect(onRender.mock.calls[1][2]).toBe(18);
|
||||
expect(onRender.mock.calls[1][3]).toBe(18);
|
||||
|
||||
// When the suspending data is resolved and our final UI is rendered,
|
||||
// both times should include the 8ms re-rendering Suspending and AsyncText.
|
||||
expect(onRender.mock.calls[1][2]).toBe(8);
|
||||
expect(onRender.mock.calls[1][3]).toBe(8);
|
||||
});
|
||||
ReactNoop.renderLegacySyncRoot(
|
||||
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
|
||||
);
|
||||
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,
|
||||
// but this time the Text component took 1ms longer to render.
|
||||
// This should impact both actualDuration and treeBaseDuration.
|
||||
expect(onRender.mock.calls[2][2]).toBe(19);
|
||||
expect(onRender.mock.calls[2][3]).toBe(19);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
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,
|
||||
// the baseDuration should only include the 1ms re-rendering AsyncText,
|
||||
// but the treeBaseDuration should include the full 9ms spent in the tree.
|
||||
expect(onRender.mock.calls[3][2]).toBe(1);
|
||||
expect(onRender.mock.calls[3][3]).toBe(9);
|
||||
});
|
||||
|
||||
describe('when suspending during update', () => {
|
||||
it('properly accounts for base durations when a suspended times out in a sync tree', () => {
|
||||
ReactNoop.renderLegacySyncRoot(
|
||||
<App shouldSuspend={false} textRenderDuration={5} />,
|
||||
);
|
||||
expect(Scheduler).toHaveYielded(['App', 'Text']);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Text');
|
||||
expect(onRender).toHaveBeenCalledTimes(1);
|
||||
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
|
||||
ReactNoop.render(<App shouldSuspend={false} textRenderDuration={5} />);
|
||||
|
||||
// Initial mount only shows the "Text" text.
|
||||
// It should take 5ms to render.
|
||||
expect(onRender.mock.calls[0][2]).toBe(5);
|
||||
expect(onRender.mock.calls[0][3]).toBe(5);
|
||||
expect(Scheduler).toFlushAndYield(['App', 'Text']);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Text');
|
||||
expect(onRender).toHaveBeenCalledTimes(1);
|
||||
|
||||
ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'Text',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
// Initial mount only shows the "Text" text.
|
||||
// It should take 5ms to render.
|
||||
expect(onRender.mock.calls[0][2]).toBe(5);
|
||||
expect(onRender.mock.calls[0][3]).toBe(5);
|
||||
|
||||
// The suspense update should only show the "Loading..." Fallback.
|
||||
// Both durations should include 10ms spent rendering Fallback
|
||||
// plus the 8ms rendering the (hidden) components.
|
||||
expect(onRender.mock.calls[1][2]).toBe(18);
|
||||
expect(onRender.mock.calls[1][3]).toBe(18);
|
||||
ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'Text',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Text');
|
||||
|
||||
ReactNoop.renderLegacySyncRoot(
|
||||
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
|
||||
);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'New',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(3);
|
||||
// Show the fallback UI.
|
||||
jest.advanceTimersByTime(750);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
|
||||
// If we force another update while still timed out,
|
||||
// but this time the Text component took 1ms longer to render.
|
||||
// This should impact both actualDuration and treeBaseDuration.
|
||||
expect(onRender.mock.calls[2][2]).toBe(19);
|
||||
expect(onRender.mock.calls[2][3]).toBe(19);
|
||||
// The suspense update should only show the "Loading..." Fallback.
|
||||
// The actual duration should include 10ms spent rendering Fallback,
|
||||
// plus the 8ms render all of the hidden, suspended subtree.
|
||||
// But the tree base duration should only include 10ms spent rendering Fallback,
|
||||
// plus the 5ms rendering the previously committed version of the hidden tree.
|
||||
expect(onRender.mock.calls[1][2]).toBe(18);
|
||||
expect(onRender.mock.calls[1][3]).toBe(15);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
// Update again while timed out.
|
||||
ReactNoop.render(
|
||||
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'New',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Promise resolved [Loaded]',
|
||||
'Loaded',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('LoadedNew');
|
||||
expect(onRender).toHaveBeenCalledTimes(4);
|
||||
// Resolve the pending promise.
|
||||
jest.advanceTimersByTime(250);
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Loaded',
|
||||
'New',
|
||||
]);
|
||||
expect(onRender).toHaveBeenCalledTimes(3);
|
||||
|
||||
// When the suspending data is resolved and our final UI is rendered,
|
||||
// the baseDuration should only include the 1ms re-rendering AsyncText,
|
||||
// but the treeBaseDuration should include the full 9ms spent in the tree.
|
||||
expect(onRender.mock.calls[3][2]).toBe(1);
|
||||
expect(onRender.mock.calls[3][3]).toBe(9);
|
||||
});
|
||||
|
||||
it('properly accounts for base durations when a suspended times out in a concurrent tree', () => {
|
||||
ReactNoop.render(
|
||||
<App shouldSuspend={false} textRenderDuration={5} />,
|
||||
);
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['App', 'Text']);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Text');
|
||||
expect(onRender).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Initial mount only shows the "Text" text.
|
||||
// It should take 5ms to render.
|
||||
expect(onRender.mock.calls[0][2]).toBe(5);
|
||||
expect(onRender.mock.calls[0][3]).toBe(5);
|
||||
|
||||
ReactNoop.render(<App shouldSuspend={true} textRenderDuration={5} />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'Text',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Text');
|
||||
|
||||
// Show the fallback UI.
|
||||
jest.advanceTimersByTime(750);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
|
||||
// The suspense update should only show the "Loading..." Fallback.
|
||||
// The actual duration should include 10ms spent rendering Fallback,
|
||||
// plus the 8ms render all of the hidden, suspended subtree.
|
||||
// But the tree base duration should only include 10ms spent rendering Fallback,
|
||||
// plus the 5ms rendering the previously committed version of the hidden tree.
|
||||
expect(onRender.mock.calls[1][2]).toBe(18);
|
||||
expect(onRender.mock.calls[1][3]).toBe(15);
|
||||
|
||||
// Update again while timed out.
|
||||
ReactNoop.render(
|
||||
<App shouldSuspend={true} text="New" textRenderDuration={6} />,
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Suspend! [Loaded]',
|
||||
'New',
|
||||
'Fallback',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
||||
expect(onRender).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Resolve the pending promise.
|
||||
jest.advanceTimersByTime(250);
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'App',
|
||||
'Suspending',
|
||||
'Loaded',
|
||||
'New',
|
||||
]);
|
||||
expect(onRender).toHaveBeenCalledTimes(3);
|
||||
|
||||
// When the suspending data is resolved and our final UI is rendered,
|
||||
// both times should include the 6ms rendering Text,
|
||||
// the 2ms rendering Suspending, and the 1ms rendering AsyncText.
|
||||
expect(onRender.mock.calls[2][2]).toBe(9);
|
||||
expect(onRender.mock.calls[2][3]).toBe(9);
|
||||
});
|
||||
// When the suspending data is resolved and our final UI is rendered,
|
||||
// both times should include the 6ms rendering Text,
|
||||
// the 2ms rendering Suspending, and the 1ms rendering AsyncText.
|
||||
expect(onRender.mock.calls[2][2]).toBe(9);
|
||||
expect(onRender.mock.calls[2][3]).toBe(9);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1387,44 +1387,47 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
|
||||
});
|
||||
|
||||
it('toggles visibility during the mutation phase', async () => {
|
||||
const {useRef, useLayoutEffect} = React;
|
||||
if (!global.__PERSISTENT__) {
|
||||
// TODO: Write persistent version of this test
|
||||
it('toggles visibility during the mutation phase', async () => {
|
||||
const {useRef, useLayoutEffect} = React;
|
||||
|
||||
function Parent() {
|
||||
const child = useRef(null);
|
||||
function Parent() {
|
||||
const child = useRef(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.yieldValue('Child is hidden: ' + child.current.hidden);
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.yieldValue('Child is hidden: ' + child.current.hidden);
|
||||
});
|
||||
|
||||
return (
|
||||
<span ref={child} hidden={false}>
|
||||
<AsyncText ms={1000} text="Hi" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span ref={child} hidden={false}>
|
||||
<AsyncText ms={1000} text="Hi" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function App(props) {
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<Parent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
function App(props) {
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<Parent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
|
||||
ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
|
||||
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Suspend! [Hi]',
|
||||
'Loading...',
|
||||
// The child should have already been hidden
|
||||
'Child is hidden: true',
|
||||
]);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Suspend! [Hi]',
|
||||
'Loading...',
|
||||
// The child should have already been hidden
|
||||
'Child is hidden: true',
|
||||
]);
|
||||
|
||||
await advanceTimers(1000);
|
||||
await advanceTimers(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [Hi]', 'Hi']);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('does not call lifecycles of a suspended component', async () => {
|
||||
|
|
|
@ -91,10 +91,7 @@ export const finalizeContainerChildren =
|
|||
$$$hostConfig.finalizeContainerChildren;
|
||||
export const replaceContainerChildren = $$$hostConfig.replaceContainerChildren;
|
||||
export const cloneHiddenInstance = $$$hostConfig.cloneHiddenInstance;
|
||||
export const cloneUnhiddenInstance = $$$hostConfig.cloneUnhiddenInstance;
|
||||
export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
|
||||
export const cloneUnhiddenTextInstance =
|
||||
$$$hostConfig.cloneUnhiddenTextInstance;
|
||||
|
||||
// -------------------
|
||||
// Hydration
|
||||
|
|
|
@ -29,6 +29,4 @@ export const appendChildToContainerChildSet = shim;
|
|||
export const finalizeContainerChildren = shim;
|
||||
export const replaceContainerChildren = shim;
|
||||
export const cloneHiddenInstance = shim;
|
||||
export const cloneUnhiddenInstance = shim;
|
||||
export const cloneHiddenTextInstance = shim;
|
||||
export const cloneUnhiddenTextInstance = shim;
|
||||
|
|
|
@ -11,6 +11,7 @@ if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
|
|||
COMMANDS_TO_RUN+=('node ./scripts/tasks/flow-ci')
|
||||
COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint')
|
||||
COMMANDS_TO_RUN+=('yarn test --maxWorkers=2')
|
||||
COMMANDS_TO_RUN+=('yarn test-persistent --maxWorkers=2')
|
||||
COMMANDS_TO_RUN+=('./scripts/circleci/check_license.sh')
|
||||
COMMANDS_TO_RUN+=('./scripts/circleci/check_modules.sh')
|
||||
COMMANDS_TO_RUN+=('./scripts/circleci/test_print_warnings.sh')
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
'use strict';
|
||||
|
||||
const baseConfig = require('./config.base');
|
||||
|
||||
module.exports = Object.assign({}, baseConfig, {
|
||||
modulePathIgnorePatterns: [
|
||||
'ReactIncrementalPerf',
|
||||
'ReactIncrementalUpdatesMinimalism',
|
||||
'ReactIncrementalTriangle',
|
||||
'ReactIncrementalReflection',
|
||||
'forwardRef',
|
||||
],
|
||||
setupFiles: [
|
||||
...baseConfig.setupFiles,
|
||||
require.resolve('./setupTests.persistent.js'),
|
||||
require.resolve('./setupHostConfigs.js'),
|
||||
],
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
jest.mock('react-noop-renderer', () =>
|
||||
require.requireActual('react-noop-renderer/persistent')
|
||||
);
|
||||
|
||||
global.__PERSISTENT__ = true;
|
Loading…
Reference in New Issue