);
}
@@ -44,13 +44,13 @@ console.log(mount().debug());
Would output the following to the console:
-```jsx
+```text
Non-Foo
-
+
Foo
@@ -68,7 +68,7 @@ console.log(mount().find(Foo).debug());
```
Would output the following to the console:
-```jsx
+```text
@@ -83,7 +83,7 @@ console.log(mount().find(Foo).debug({ ignoreProps: true }));
```
Would output the following to the console:
-```jsx
+```text
@@ -92,3 +92,19 @@ Would output the following to the console:
```
+
+and:
+```jsx
+console.log(mount().find(Foo).debug({ verbose: true }));
+```
+Would output the following to the console:
+
+```text
+
+
+
+ Foo
+
+
+
+```
diff --git a/docs/api/ShallowWrapper/debug.md b/docs/api/ShallowWrapper/debug.md
index 68ef624bd..4b4f17eda 100644
--- a/docs/api/ShallowWrapper/debug.md
+++ b/docs/api/ShallowWrapper/debug.md
@@ -8,6 +8,7 @@ console when tests are not passing when you expect them to.
`options` (`Object` [optional]):
- `options.ignoreProps`: (`Boolean` [optional]): Whether props should be omitted in the resulting string. Props are included by default.
+- `options.verbose`: (`Boolean` [optional]): Whether arrays and objects passed as props should be verbosely printed.
#### Returns
@@ -21,7 +22,12 @@ function Book({ title, pages }) {
return (
```
diff --git a/docs/api/shallow.md b/docs/api/shallow.md
index 0756bb06b..53ee3f86a 100644
--- a/docs/api/shallow.md
+++ b/docs/api/shallow.md
@@ -50,8 +50,9 @@ describe('', () => {
- `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
diff --git a/docs/guides/migration-from-2-to-3.md b/docs/guides/migration-from-2-to-3.md
index 25df052ec..be1d09c89 100644
--- a/docs/guides/migration-from-2-to-3.md
+++ b/docs/guides/migration-from-2-to-3.md
@@ -102,6 +102,49 @@ Although this is a breaking change, I believe the new behavior is closer to what
actually expect and want. Having enzyme wrappers be immutable results in more deterministic tests
that are less prone to flakiness from external factors.
+### Calling `props()` after a state change
+
+In `enzyme` v2, executing an event that would change a component state (and in turn update props) would return those updated props via the `.props` method.
+
+Now, in `enzyme` v3, you are required to re-find the component; for example:
+
+```jsx
+class Toggler extends React.Component {
+ constructor(...args) {
+ super(...args);
+ this.state = { on: false };
+ }
+
+ toggle() {
+ this.setState(({ on }) => ({ on: !on }));
+ }
+
+ render() {
+ const { on } = this.state;
+ return (
{on ? 'on' : 'off'}
);
+ }
+}
+
+it('passes in enzyme v2, fails in v3', () => {
+ const wrapper = mount();
+ const root = wrapper.find('#root');
+ expect(root.text()).to.equal('off');
+
+ wrapper.instance().toggle();
+
+ expect(root.text()).to.equal('on');
+});
+
+it('passes in v2 and v3', () => {
+ const wrapper = mount();
+ expect(wrapper.find('#root').text()).to.equal('off');
+
+ wrapper.instance().toggle();
+
+ expect(wrapper.find('#root').text()).to.equal('on');
+});
+```
+
## `children()` now has slightly different meaning
enzyme has a `.children()` method which is intended to return the rendered children of a wrapper.
diff --git a/env.js b/env.js
index a63c93a2a..f44ac26c7 100755
--- a/env.js
+++ b/env.js
@@ -129,19 +129,20 @@ Promise.resolve()
.map(key => `${key}@${key.startsWith('react') ? reactVersion : peerDeps[key]}`);
if (process.env.RENDERER) {
- installs.push(`react-test-renderer@${process.env.RENDERER}`);
+ // eslint-disable-next-line no-param-reassign
+ adapterJson.dependencies['react-test-renderer'] = process.env.RENDERER;
}
// eslint-disable-next-line no-param-reassign
testJson.dependencies[adapterName] = adapterJson.version;
- return Promise.all([
+ return writeJSON(adapterPackageJsonPath, adapterJson, true).then(() => Promise.all([
// npm install the peer deps at the root
run('npm', 'i', '--no-save', ...installs),
// add the adapter to the dependencies of the test suite
writeJSON(testPackageJsonPath, testJson, true),
- ]);
+ ]));
})
.then(() => run('lerna', 'bootstrap', '--hoist=\'react*\''))
.then(() => getJSON(testPackageJsonPath))
diff --git a/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js b/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js
index 8f8fdabfc..aad993511 100644
--- a/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js
+++ b/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js
@@ -326,7 +326,7 @@ class ReactSixteenOneAdapter extends EnzymeAdapter {
throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
}
// eslint-disable-next-line react/no-find-dom-node
- eventFn(nodeToHostNode(node), mock);
+ eventFn(adapter.nodeToHostNode(node), mock);
},
batchedUpdates(fn) {
return fn();
diff --git a/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js b/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js
index bf86b1e35..78cb4a7ac 100644
--- a/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js
+++ b/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js
@@ -328,7 +328,7 @@ class ReactSixteenTwoAdapter extends EnzymeAdapter {
throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
}
// eslint-disable-next-line react/no-find-dom-node
- eventFn(nodeToHostNode(node), mock);
+ eventFn(adapter.nodeToHostNode(node), mock);
},
batchedUpdates(fn) {
return fn();
diff --git a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
index 797df863b..9087b0e87 100644
--- a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
+++ b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
@@ -347,7 +347,7 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter {
throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
}
// eslint-disable-next-line react/no-find-dom-node
- eventFn(nodeToHostNode(node), mock);
+ eventFn(adapter.nodeToHostNode(node), mock);
},
batchedUpdates(fn) {
return fn();
diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
index 0d4de3502..85aa4e096 100644
--- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
+++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
@@ -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,
@@ -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, shallowEqual } from 'enzyme/build/Utils';
@@ -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}`);
}
@@ -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,
@@ -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();
@@ -427,7 +467,7 @@ class ReactSixteenAdapter extends EnzymeAdapter {
if (!eventFn) {
throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
}
- eventFn(nodeToHostNode(node), mock);
+ eventFn(adapter.nodeToHostNode(node), mock);
},
batchedUpdates(fn) {
return fn();
@@ -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;
@@ -457,6 +501,17 @@ class ReactSixteenAdapter extends EnzymeAdapter {
// Wrap functional components on versions prior to 16.5,
// to avoid inadvertently pass a `this` instance to it.
const wrapFunctionalComponent = (Component) => {
+ if (is166 && has(Component, 'defaultProps')) {
+ if (lastComponent !== Component) {
+ wrappedComponent = Object.assign(
+ // eslint-disable-next-line new-cap
+ (props, ...args) => Component({ ...Component.defaultProps, ...props }, ...args),
+ Component,
+ );
+ lastComponent = Component;
+ }
+ return wrappedComponent;
+ }
if (is165) {
return Component;
}
@@ -498,8 +553,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
@@ -517,7 +584,7 @@ class ReactSixteenAdapter extends EnzymeAdapter {
if (!isStateful && typeof Component === 'function') {
return withSetStateAllowed(() => renderer.render(
- { ...el, type: wrapFunctionalComponent(Component) },
+ { ...renderedEl, type: wrapFunctionalComponent(Component) },
context,
));
}
@@ -546,7 +613,7 @@ class ReactSixteenAdapter extends EnzymeAdapter {
});
}
}
- return withSetStateAllowed(() => renderer.render(el, context));
+ return withSetStateAllowed(() => renderer.render(renderedEl, context));
}
},
unmount() {
@@ -609,6 +676,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)) {
@@ -676,6 +746,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:
}
}
@@ -693,6 +764,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);
}
}
@@ -728,6 +802,7 @@ class ReactSixteenAdapter extends EnzymeAdapter {
|| isForwardRef(fakeElement)
|| isContextProvider(fakeElement)
|| isContextConsumer(fakeElement)
+ || isSuspense(fakeElement)
);
}
diff --git a/packages/enzyme-adapter-react-16/src/detectFiberTags.js b/packages/enzyme-adapter-react-16/src/detectFiberTags.js
index f1368da17..92c3cce84 100644
--- a/packages/enzyme-adapter-react-16/src/detectFiberTags.js
+++ b/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');
@@ -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;
@@ -32,6 +59,7 @@ module.exports = function detectFiberTags() {
}
let Ctx = null;
let FwdRef = null;
+ let LazyComponent = null;
if (supportsContext) {
Ctx = React.createContext();
}
@@ -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
@@ -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,
};
};
diff --git a/packages/enzyme-adapter-utils/src/Utils.js b/packages/enzyme-adapter-utils/src/Utils.js
index b9a27df87..83671c99d 100644
--- a/packages/enzyme-adapter-utils/src/Utils.js
+++ b/packages/enzyme-adapter-utils/src/Utils.js
@@ -363,3 +363,7 @@ export function getWrappingComponentMountRenderer({ toTree, getMountWrapperInsta
},
};
}
+
+export function fakeDynamicImport(moduleToImport) {
+ return Promise.resolve({ default: moduleToImport });
+}
diff --git a/packages/enzyme-test-suite/package.json b/packages/enzyme-test-suite/package.json
index 2e487f5a1..b560a5dc6 100644
--- a/packages/enzyme-test-suite/package.json
+++ b/packages/enzyme-test-suite/package.json
@@ -42,9 +42,9 @@
"object.assign": "^4.1.0",
"prop-types": "^15.7.2",
"react-is": "^16.8.6",
- "semver": "^6.0.0",
- "sinon-sandbox": "^2.0.0",
- "sinon": "^5.1.1"
+ "semver": "^5.7.0",
+ "sinon": "^5.1.1",
+ "sinon-sandbox": "^2.0.0"
},
"peerDependencies": {
"react": "^15.5.0",
diff --git a/packages/enzyme-test-suite/test/Adapter-spec.jsx b/packages/enzyme-test-suite/test/Adapter-spec.jsx
index 8f083953a..016d2505c 100644
--- a/packages/enzyme-test-suite/test/Adapter-spec.jsx
+++ b/packages/enzyme-test-suite/test/Adapter-spec.jsx
@@ -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';
@@ -1063,6 +1065,20 @@ describe('Adapter', () => {
itIf(is('>= 16.6'), 'supports ConcurrentMode', () => {
expect(getDisplayName()).to.equal('ConcurrentMode');
});
+
+ itIf(is('>= 16.6'), 'supports Suspense', () => {
+ expect(getDisplayName()).to.equal('Suspense');
+ });
+
+ itIf(is('>= 16.6'), 'supports lazy', () => {
+ class DynamicComponent extends React.Component {
+ render() {
+ return
`);
+ });
+
+ it('applies defaultProps to the memoized component and does not override real props', () => {
+ const wrapper = WrapRendered();
+ expect(wrapper.debug()).to.equal(`