Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support React.lazy and React.Suspense #1975

Merged
merged 3 commits into from Apr 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/api/shallow.md
Expand Up @@ -50,8 +50,9 @@ describe('<MyComponent />', () => {
- `options.disableLifecycleMethods`: (`Boolean` [optional]): If set to true, `componentDidMount`
is not called on the component, and `componentDidUpdate` is not called after
[`setProps`](ShallowWrapper/setProps.md) and [`setContext`](ShallowWrapper/setContext.md). Default to `false`.
- `options.wrappingComponent`: (`ComponentType` [optional]): A component that will render as a parent of the `node`. It can be used to provide context to the `node`, among other things. See the [`getWrappingComponent()` docs](ShallowWrapper/getWrappingComponent.md) for an example. **Note**: `wrappingComponent` _must_ render its children.
- `options.wrappingComponentProps`: (`Object` [optional]): Initial props to pass to the `wrappingComponent` if it is specified.
- `options.wrappingComponent`: (`ComponentType` [optional]): A component that will render as a parent of the `node`. It can be used to provide context to the `node`, among other things. See the [`getWrappingComponent()` docs](ShallowWrapper/getWrappingComponent.md) for an example. **Note**: `wrappingComponent` _must_ render its children.
- `options.wrappingComponentProps`: (`Object` [optional]): Initial props to pass to the `wrappingComponent` if it is specified.
- `options.suspenseFallback`: (`Boolean` [optional]): If set to true, when rendering `Suspense` enzyme will replace all the lazy components in children with `fallback` element prop. Otherwise it won't handle fallback of lazy component. Default to `true`. Note: not supported in React < 16.6.

#### Returns

Expand Down
74 changes: 69 additions & 5 deletions packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
Expand Up @@ -10,6 +10,7 @@ import { version as testRendererVersion } from 'react-test-renderer/package.json
import TestUtils from 'react-dom/test-utils';
import semver from 'semver';
import checkPropTypes from 'prop-types/checkPropTypes';
import has from 'has';
import {
AsyncMode,
ConcurrentMode,
Expand All @@ -22,13 +23,17 @@ import {
isContextProvider,
isElement,
isForwardRef,
isLazy,
isMemo,
isPortal,
isSuspense,
isValidElementType,
Lazy,
Memo,
Portal,
Profiler,
StrictMode,
Suspense,
} from 'react-is';
import { EnzymeAdapter } from 'enzyme';
import { typeOfNode } from 'enzyme/build/Utils';
Expand Down Expand Up @@ -228,6 +233,19 @@ function toTree(vnode) {
rendered: childrenToTree(node.child),
};
}
case FiberTags.Suspense: {
return {
nodeType: 'function',
type: Suspense,
props: { ...node.memoizedProps },
key: ensureKeyOrUndefined(node.key),
ref: node.ref,
instance: null,
rendered: childrenToTree(node.child),
};
}
case FiberTags.Lazy:
return childrenToTree(node.child);
default:
throw new Error(`Enzyme Internal Error: unknown node with tag ${node.tag}`);
}
Expand Down Expand Up @@ -275,6 +293,25 @@ function nodeToHostNode(_node) {
return mapper(node);
}

function replaceLazyWithFallback(node, fallback) {
if (!node) {
return null;
}
if (Array.isArray(node)) {
return node.map(el => replaceLazyWithFallback(el, fallback));
}
if (isLazy(node.type)) {
return fallback;
}
return {
...node,
props: {
...node.props,
children: replaceLazyWithFallback(node.props.children, fallback),
},
};
}

const eventOptions = {
animation: true,
pointerEvents: is164,
Expand Down Expand Up @@ -351,6 +388,9 @@ class ReactSixteenAdapter extends EnzymeAdapter {

createMountRenderer(options) {
assertDomAvailable('mount');
if (has(options, 'suspenseFallback')) {
throw new TypeError('`suspenseFallback` is not supported by the `mount` renderer');
}
if (FiberTags === null) {
// Requires DOM.
FiberTags = detectFiberTags();
Expand Down Expand Up @@ -445,9 +485,13 @@ class ReactSixteenAdapter extends EnzymeAdapter {
};
}

createShallowRenderer(/* options */) {
createShallowRenderer(options = {}) {
const adapter = this;
const renderer = new ShallowRenderer();
const { suspenseFallback } = options;
if (typeof suspenseFallback !== 'undefined' && typeof suspenseFallback !== 'boolean') {
throw TypeError('`options.suspenseFallback` should be boolean or undefined');
}
let isDOM = false;
let cachedNode = null;

Expand Down Expand Up @@ -498,8 +542,20 @@ class ReactSixteenAdapter extends EnzymeAdapter {
return withSetStateAllowed(() => renderer.render({ ...el, type: MockConsumer }));
} else {
isDOM = false;
const { type: Component } = el;

let renderedEl = el;
if (isLazy(renderedEl)) {
throw TypeError('`React.lazy` is not supported by shallow rendering.');
}
if (isSuspense(renderedEl)) {
let { children } = renderedEl.props;
if (suspenseFallback) {
const { fallback } = renderedEl.props;
children = replaceLazyWithFallback(children, fallback);
}
const FakeSuspenseWrapper = () => children;
renderedEl = React.createElement(FakeSuspenseWrapper, null, children);
}
const { type: Component } = renderedEl;
const isStateful = Component.prototype && (
Component.prototype.isReactComponent
|| Array.isArray(Component.__reactAutoBindPairs) // fallback for createClass components
Expand All @@ -517,7 +573,7 @@ class ReactSixteenAdapter extends EnzymeAdapter {

if (!isStateful && typeof Component === 'function') {
return withSetStateAllowed(() => renderer.render(
{ ...el, type: wrapFunctionalComponent(Component) },
{ ...renderedEl, type: wrapFunctionalComponent(Component) },
context,
));
}
Expand Down Expand Up @@ -546,7 +602,7 @@ class ReactSixteenAdapter extends EnzymeAdapter {
});
}
}
return withSetStateAllowed(() => renderer.render(el, context));
return withSetStateAllowed(() => renderer.render(renderedEl, context));
}
},
unmount() {
Expand Down Expand Up @@ -609,6 +665,9 @@ class ReactSixteenAdapter extends EnzymeAdapter {
}

createStringRenderer(options) {
if (has(options, 'suspenseFallback')) {
throw new TypeError('`suspenseFallback` should not be specified in options of string renderer');
}
return {
render(el, context) {
if (options.context && (el.type.contextTypes || options.childContextTypes)) {
Expand Down Expand Up @@ -676,6 +735,7 @@ class ReactSixteenAdapter extends EnzymeAdapter {
case StrictMode || NaN: return 'StrictMode';
case Profiler || NaN: return 'Profiler';
case Portal || NaN: return 'Portal';
case Suspense || NaN: return 'Suspense';
default:
}
}
Expand All @@ -693,6 +753,9 @@ class ReactSixteenAdapter extends EnzymeAdapter {
const name = displayNameOfNode({ type: type.render });
return name ? `ForwardRef(${name})` : 'ForwardRef';
}
case Lazy || NaN: {
return 'lazy';
}
default: return displayNameOfNode(node);
}
}
Expand All @@ -716,6 +779,7 @@ class ReactSixteenAdapter extends EnzymeAdapter {
|| isForwardRef(fakeElement)
|| isContextProvider(fakeElement)
|| isContextConsumer(fakeElement)
|| isSuspense(fakeElement)
);
}

Expand Down
37 changes: 37 additions & 0 deletions packages/enzyme-adapter-react-16/src/detectFiberTags.js
@@ -1,5 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { fakeDynamicImport } from 'enzyme-adapter-utils';

function getFiber(element) {
const container = global.document.createElement('div');
Expand All @@ -14,12 +15,38 @@ function getFiber(element) {
return inst._reactInternalFiber.child;
}

function getLazyFiber(LazyComponent) {
const container = global.document.createElement('div');
let inst = null;
// eslint-disable-next-line react/prefer-stateless-function
class Tester extends React.Component {
render() {
inst = this;
return React.createElement(LazyComponent);
}
}
// eslint-disable-next-line react/prefer-stateless-function
class SuspenseWrapper extends React.Component {
render() {
return React.createElement(
React.Suspense,
{ fallback: false },
React.createElement(Tester),
);
}
}
ReactDOM.render(React.createElement(SuspenseWrapper), container);
return inst._reactInternalFiber.child;
}

module.exports = function detectFiberTags() {
const supportsMode = typeof React.StrictMode !== 'undefined';
const supportsContext = typeof React.createContext !== 'undefined';
const supportsForwardRef = typeof React.forwardRef !== 'undefined';
const supportsMemo = typeof React.memo !== 'undefined';
const supportsProfiler = typeof React.unstable_Profiler !== 'undefined';
const supportsSuspense = typeof React.Suspense !== 'undefined';
const supportsLazy = typeof React.lazy !== 'undefined';

function Fn() {
return null;
Expand All @@ -32,6 +59,7 @@ module.exports = function detectFiberTags() {
}
let Ctx = null;
let FwdRef = null;
let LazyComponent = null;
if (supportsContext) {
Ctx = React.createContext();
}
Expand All @@ -40,6 +68,9 @@ module.exports = function detectFiberTags() {
// eslint-disable-next-line no-unused-vars
FwdRef = React.forwardRef((props, ref) => null);
}
if (supportsLazy) {
LazyComponent = React.lazy(() => fakeDynamicImport(() => null));
}

return {
HostRoot: getFiber('test').return.return.tag, // Go two levels above to find the root
Expand Down Expand Up @@ -70,5 +101,11 @@ module.exports = function detectFiberTags() {
Profiler: supportsProfiler
? getFiber(React.createElement(React.unstable_Profiler, { id: 'mock', onRender() {} })).tag
: -1,
Suspense: supportsSuspense
? getFiber(React.createElement(React.Suspense, { fallback: false })).tag
: -1,
Lazy: supportsLazy
? getLazyFiber(LazyComponent).tag
: -1,
};
};
4 changes: 4 additions & 0 deletions packages/enzyme-adapter-utils/src/Utils.js
Expand Up @@ -363,3 +363,7 @@ export function getWrappingComponentMountRenderer({ toTree, getMountWrapperInsta
},
};
}

export function fakeDynamicImport(moduleToImport) {
return Promise.resolve({ default: moduleToImport });
}
26 changes: 21 additions & 5 deletions packages/enzyme-test-suite/test/Adapter-spec.jsx
Expand Up @@ -9,20 +9,22 @@ import {
} from 'react-is';
import PropTypes from 'prop-types';
import wrap from 'mocha-wrap';
import { wrapWithWrappingComponent, RootFinder } from 'enzyme-adapter-utils';
import { fakeDynamicImport, wrapWithWrappingComponent, RootFinder } from 'enzyme-adapter-utils';

import './_helpers/setupAdapters';
import Adapter from './_helpers/adapter';
import {
renderToString,
AsyncMode,
ConcurrentMode,
createContext,
createPortal,
forwardRef,
Fragment,
StrictMode,
AsyncMode,
ConcurrentMode,
lazy,
Profiler,
renderToString,
StrictMode,
Suspense,
} from './_helpers/react-compat';
import { is } from './_helpers/version';
import { itIf, describeWithDOM, describeIf } from './_helpers';
Expand Down Expand Up @@ -1063,6 +1065,20 @@ describe('Adapter', () => {
itIf(is('>= 16.6'), 'supports ConcurrentMode', () => {
expect(getDisplayName(<ConcurrentMode />)).to.equal('ConcurrentMode');
});

itIf(is('>= 16.6'), 'supports Suspense', () => {
expect(getDisplayName(<Suspense />)).to.equal('Suspense');
});

itIf(is('>= 16.6'), 'supports lazy', () => {
class DynamicComponent extends React.Component {
render() {
return <div>DynamicComponent</div>;
}
}
const LazyComponent = lazy(() => fakeDynamicImport(DynamicComponent));
expect(getDisplayName(<LazyComponent />)).to.equal('lazy');
});
});

describeIf(is('>= 16.2'), 'determines if node isFragment', () => {
Expand Down