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:
Andrew Clark 2019-03-11 10:56:34 -07:00 committed by GitHub
parent 3f4852fa5f
commit bc8bd24c14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 593 additions and 681 deletions

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 () => {

View File

@ -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

View File

@ -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;

View File

@ -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')

View File

@ -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'),
],
});

View File

@ -0,0 +1,7 @@
'use strict';
jest.mock('react-noop-renderer', () =>
require.requireActual('react-noop-renderer/persistent')
);
global.__PERSISTENT__ = true;