diff --git a/.travis.yml b/.travis.yml index e5c88bfae..09465f811 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,8 @@ matrix: env: REACT=16 - node_js: "lts/*" env: REACT=16.8.5 RENDERER=16.8.5 + - node_js: "lts/*" + env: REACT=16.8.5 RENDERER=16.8.3 - node_js: "lts/*" env: REACT=16.8.3 - node_js: "lts/*" @@ -65,6 +67,10 @@ matrix: env: KARMA=true REACT=15 - node_js: "6" env: KARMA=true REACT=16 + - node_js: "lts/*" + env: REACT=16.8.5 RENDERER=16.8.3 + - node_js: "lts/*" + env: REACT=16.8 RENDERER=16.7 exclude: - node_js: "6" env: REACT=0.13 diff --git a/docs/api/ReactWrapper/debug.md b/docs/api/ReactWrapper/debug.md index 3e8dc8b23..b6445bec0 100644 --- a/docs/api/ReactWrapper/debug.md +++ b/docs/api/ReactWrapper/debug.md @@ -31,7 +31,7 @@ function Bar() { return (
Non-Foo - +
); } @@ -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 (

{title}

- {pages && } + {pages && ( + + )}
); } @@ -56,8 +62,8 @@ console.log(wrapper.debug()); Outputs to console: ```text
-

Huckleberry Finn

- +

Huckleberry Finn

+
``` @@ -73,7 +79,25 @@ console.log(wrapper.debug({ ignoreProps: true })); Outputs to console: ```text
-

Huckleberry Finn

- +

Huckleberry Finn

+ +
+``` + + +```jsx +const wrapper = shallow(( + +)); +console.log(wrapper.debug({ verbose: true })); +``` +Outputs to console: +```text +
+

Huckleberry Finn

+
``` 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
DynamicComponent
; + } + } + const LazyComponent = lazy(() => fakeDynamicImport(DynamicComponent)); + expect(getDisplayName()).to.equal('lazy'); + }); }); describeIf(is('>= 16.2'), 'determines if node isFragment', () => { diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index f61d2e591..bddcc7988 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -15,17 +15,20 @@ import { withSetStateAllowed, } from 'enzyme/build/Utils'; import getAdapter from 'enzyme/build/getAdapter'; +import { fakeDynamicImport } from 'enzyme-adapter-utils'; import './_helpers/setupAdapters'; import { createClass, createContext, createPortal, - Fragment, forwardRef, + Fragment, + lazy, memo, Profiler, PureComponent, + Suspense, useEffect, useState, } from './_helpers/react-compat'; @@ -34,6 +37,7 @@ import { describeIf, itIf, } from './_helpers'; +import getLoadedLazyComponent from './_helpers/getLoadedLazyComponent'; import describeMethods from './_helpers/describeMethods'; import { is, @@ -1078,6 +1082,132 @@ describeWithDOM('mount', () => { 'wrap', ); + describeIf(is('>= 16.6'), 'Suspense & lazy', () => { + class DynamicComponent extends React.Component { + render() { + return ( +
Dynamic Component
+ ); + } + } + class Fallback extends React.Component { + render() { + return ( +
Fallback
+ ); + } + } + it('finds Suspense and its children when no lazy component', () => { + class Component extends React.Component { + render() { + return ( +
test
+ ); + } + } + + const SuspenseComponent = () => ( + + + + ); + + const wrapper = mount(); + + expect(wrapper.is(SuspenseComponent)).to.equal(true); + expect(wrapper.find(Component)).to.have.lengthOf(1); + expect(wrapper.find(Fallback)).to.have.lengthOf(0); + }); + + it('can mount Suspense directly', () => { + const wrapper = mount(); + expect(wrapper.is(Suspense)).to.equal(true); + }); + + it('finds fallback when given lazy component in initial mount', () => { + const LazyComponent = lazy(() => fakeDynamicImport(DynamicComponent)); + const SuspenseComponent = () => ( + }> + + + ); + + const wrapper = mount(); + + expect(wrapper.is(SuspenseComponent)).to.equal(true); + expect(wrapper.find(LazyComponent)).to.have.lengthOf(0); + expect(wrapper.find(Fallback)).to.have.lengthOf(1); + }); + + it('return fallback string when given lazy component in initial mount and call .debug()', () => { + const LazyComponent = lazy(() => fakeDynamicImport(DynamicComponent)); + const SuspenseComponent = () => ( + }> + + + ); + + const wrapper = mount(); + + expect(wrapper.debug()).to.equal(` + + +
+ Fallback +
+
+
+
`); + }); + + it('return wrapped component when given loaded lazy component in initial mount', () => { + const LazyComponent = getLoadedLazyComponent(DynamicComponent); + const SuspenseComponent = () => ( + }> + + + ); + + const wrapper = mount(); + + expect(wrapper.is(SuspenseComponent)).to.equal(true); + expect(wrapper.find(LazyComponent)).to.have.lengthOf(0); + expect(wrapper.find(DynamicComponent)).to.have.lengthOf(1); + expect(wrapper.find(Fallback)).to.have.lengthOf(0); + }); + + it('return wrapped component string when given loaded lazy component in initial mount and call .debug()', () => { + const LazyComponent = getLoadedLazyComponent(DynamicComponent); + const SuspenseComponent = () => ( + }> + + + ); + + const wrapper = mount(); + + expect(wrapper.debug()).to.equal(` + + +
+ Dynamic Component +
+
+
+
`); + }); + + it('throws if options.suspenseFallback is specified', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + const SuspenseComponent = () => ( + }> + + + ); + expect(() => mount(, { suspenseFallback: false })).to.throw(); + }); + }); + describe('.mount()', () => { it('calls componentWillUnmount()', () => { const willMount = sinon.spy(); diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index f676be4f1..095976675 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -14,15 +14,18 @@ import { withSetStateAllowed, } from 'enzyme/build/Utils'; import getAdapter from 'enzyme/build/getAdapter'; +import { fakeDynamicImport } from 'enzyme-adapter-utils'; import './_helpers/setupAdapters'; import { createClass, createContext, createPortal, - Fragment, forwardRef, + Fragment, + lazy, PureComponent, + Suspense, useEffect, useState, Profiler, @@ -1591,6 +1594,209 @@ describe('shallow', () => { }); }); + describeIf(is('>= 16.6'), 'Suspense & lazy', () => { + class DynamicComponent extends React.Component { + render() { + return ( +
Dynamic Component
+ ); + } + } + + class Fallback extends React.Component { + render() { + return ( +
Fallback
+ ); + } + } + + it('finds Suspense and its children when no lazy component', () => { + class Component extends React.Component { + render() { + return ( +
test
+ ); + } + } + + const SuspenseComponent = () => ( + }> + + + ); + + const wrapper = shallow(); + + expect(wrapper.is(Suspense)).to.equal(true); + expect(wrapper.find(Component)).to.have.lengthOf(1); + expect(wrapper.find(Fallback)).to.have.lengthOf(0); + }); + + it('finds LazyComponent when render component wrapping lazy component', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + const SuspenseComponent = () => ( + }> + + + ); + + const wrapper = shallow(); + + expect(wrapper.is(Suspense)).to.equal(true); + expect(wrapper.find(LazyComponent)).to.have.lengthOf(1); + expect(wrapper.find(Fallback)).to.have.lengthOf(0); + }); + + it('returns suspense and lazy component string when debug() is called', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + const SuspenseComponent = () => ( + }> + + + ); + + const wrapper = shallow(); + + expect(wrapper.debug()).to.equal(` + +`); + }); + + it('renders lazy component when render Suspense without option', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + + const wrapper = shallow(( + }> + + + )); + + expect(wrapper.find(LazyComponent)).to.have.lengthOf(1); + expect(wrapper.find(Fallback)).to.have.lengthOf(0); + }); + + it('returns lazy component string when debug() is called', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + + const wrapper = shallow(( + }> + + + )); + + expect(wrapper.debug()).to.equal(''); + }); + + it('replaces LazyComponent with Fallback when render Suspense if options.suspenseFallback=true', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + + const wrapper = shallow(( + }> + + + ), { suspenseFallback: true }); + + expect(wrapper.find(LazyComponent)).to.have.lengthOf(0); + expect(wrapper.find(Fallback)).to.have.lengthOf(1); + }); + + it('returns fallback component string when debug() is called if options.suspenseFallback=true', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + + const wrapper = shallow(( + }> + + + ), { suspenseFallback: true }); + + expect(wrapper.debug()).to.equal(''); + }); + + it('throws if options.suspenseFallback is not boolean or undefined', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + const SuspenseComponent = () => ( + }> + + + ); + expect(() => shallow(, { suspenseFallback: 'true' })).to.throw(); + }); + + it('finds fallback after dive into functional component wrapping Suspense', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + const SuspenseComponent = () => ( + }> + + + ); + + const wrapper = shallow(, { suspenseFallback: true }); + const inner = wrapper.dive(); + + expect(inner.find(LazyComponent)).to.have.lengthOf(0); + expect(inner.find(Fallback)).to.have.lengthOf(1); + }); + + it('replaces nested LazyComponent with Fallback when render Suspense with options.suspenseFallback=true', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + + const wrapper = shallow(( + }> +
+ +
+ +
+
+ + ), { suspenseFallback: true }); + + expect(wrapper.find(LazyComponent)).to.have.lengthOf(0); + expect(wrapper.find(Fallback)).to.have.lengthOf(2); + expect(wrapper.find('.should-be-rendered')).to.have.lengthOf(2); + expect(wrapper.find('.should-be-rendered > .inner')).to.have.lengthOf(1); + }); + + it('does not replace LazyComponent with Fallback when render Suspense if options.suspenseFallback = false', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + + const wrapper = shallow(( + }> + + + ), { suspenseFallback: false }); + + expect(wrapper.find(LazyComponent)).to.have.lengthOf(1); + expect(wrapper.find(Fallback)).to.have.lengthOf(0); + }); + + it('does not replace nested LazyComponent with Fallback when render Suspense if option.suspenseFallback = false', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + + const wrapper = shallow(( + }> +
+ +
+ +
+
+ + ), { suspenseFallback: false }); + + expect(wrapper.find(LazyComponent)).to.have.lengthOf(2); + expect(wrapper.find(Fallback)).to.have.lengthOf(0); + expect(wrapper.find('.should-be-rendered')).to.have.lengthOf(2); + expect(wrapper.find('.should-be-rendered > .inner')).to.have.lengthOf(1); + }); + + it('throws when rendering lazy component', () => { + const LazyComponent = lazy(fakeDynamicImport(DynamicComponent)); + expect(() => shallow()).to.throw(); + }); + }); + describe('lifecycle methods', () => { describe('disableLifecycleMethods option', () => { describe('validation', () => { diff --git a/packages/enzyme-test-suite/test/_helpers/getLoadedLazyComponent.js b/packages/enzyme-test-suite/test/_helpers/getLoadedLazyComponent.js new file mode 100644 index 000000000..d9ca7765e --- /dev/null +++ b/packages/enzyme-test-suite/test/_helpers/getLoadedLazyComponent.js @@ -0,0 +1,31 @@ +import { fakeDynamicImport } from 'enzyme-adapter-utils'; +import { lazy } from './react-compat'; +import { is, VERSION } from './version'; + +function fakeSyncThenable(result) { + return { + then(resolve) { + return resolve({ default: result }); + }, + }; +} + +export default function getLoadedLazyComponent(wrappedComponent) { + if (is('>= 16.8')) { + return lazy(() => fakeSyncThenable(wrappedComponent)); + } + if (is('>= 16.6')) { + const LazyComponent = lazy(() => fakeDynamicImport(wrappedComponent)); + /** + * Before React v16.8 there's no public api to synchronously / await + * loaded lazy component. + * So we have to hack this by setting `_result` and `_status` implementation. + */ + /* eslint-disable no-underscore-dangle */ + LazyComponent._result = wrappedComponent; + /* eslint-disable no-underscore-dangle */ + LazyComponent._status = 1; + return LazyComponent; + } + throw Error(`Current React version ${VERSION} doesn't support \`lazy()\` api.`); +} diff --git a/packages/enzyme-test-suite/test/_helpers/index.jsx b/packages/enzyme-test-suite/test/_helpers/index.jsx index 686148c78..25046d9b2 100644 --- a/packages/enzyme-test-suite/test/_helpers/index.jsx +++ b/packages/enzyme-test-suite/test/_helpers/index.jsx @@ -14,7 +14,7 @@ export function describeIf(test, a, b) { if (test) { describe(a, b); } else { - describe.skip(a, b); + describe.skip(a); } } @@ -26,7 +26,9 @@ describeIf.only = (test, a, b) => { if (test) { describe.only(a, b); } else { - describe.skip(a, b); + describe.only('only:', () => { + describe.skip(a); + }); } }; diff --git a/packages/enzyme-test-suite/test/adapter-utils-spec.jsx b/packages/enzyme-test-suite/test/adapter-utils-spec.jsx index be23428d1..976508588 100644 --- a/packages/enzyme-test-suite/test/adapter-utils-spec.jsx +++ b/packages/enzyme-test-suite/test/adapter-utils-spec.jsx @@ -12,6 +12,7 @@ import { getNodeFromRootFinder, wrapWithWrappingComponent, getWrappingComponentMountRenderer, + fakeDynamicImport, } from 'enzyme-adapter-utils'; import './_helpers/setupAdapters'; @@ -378,4 +379,22 @@ describe('enzyme-adapter-utils', () => { }); }); }); + + describe('fakeDynamicImport', () => { + it('is a function', () => { + expect(fakeDynamicImport).to.be.a('function'); + }); + + it('returns a promise', () => { + const promise = fakeDynamicImport(); + expect(Promise.resolve(promise)).to.equal(promise); + }); + + it('returns a promise for an object containing the provided argument', () => { + const signal = {}; + return fakeDynamicImport(signal).then((actual) => { + expect(actual).to.have.property('default', signal); + }); + }); + }); }); diff --git a/packages/enzyme-test-suite/test/shared/methods/debug.jsx b/packages/enzyme-test-suite/test/shared/methods/debug.jsx index 433953001..3f36b93c5 100644 --- a/packages/enzyme-test-suite/test/shared/methods/debug.jsx +++ b/packages/enzyme-test-suite/test/shared/methods/debug.jsx @@ -4,16 +4,19 @@ import { expect } from 'chai'; import { debugNodes } from 'enzyme/build/Debug'; import { + describeIf, itIf, } from '../../_helpers'; import { is } from '../../_helpers/version'; import { createClass, + memo, } from '../../_helpers/react-compat'; export default function describeDebug({ Wrap, + WrapRendered, isShallow, }) { describe('.debug()', () => { @@ -65,5 +68,49 @@ export default function describeDebug({ expect(wrapper.debug()).to.equal(debugNodes(wrapper.getNodesInternal())); }); }); + + describeIf(is('>= 16.6'), 'React.memo', () => { + function Add({ a, b, c }) { + return
{String(a)}|{String(b)}|{String(c)}
; + } + Add.defaultProps = { + b: 2, + c: 3, + }; + const MemoAdd = memo(Add); + + it('applies defaultProps to the component', () => { + const wrapper = WrapRendered(); + expect(wrapper.debug()).to.equal(`
+ undefined + | + 2 + | + 3 +
`); + }); + + it('applies defaultProps to the memoized component', () => { + const wrapper = WrapRendered(); + expect(wrapper.debug()).to.equal(`
+ undefined + | + 2 + | + 3 +
`); + }); + + it('applies defaultProps to the memoized component and does not override real props', () => { + const wrapper = WrapRendered(); + expect(wrapper.debug()).to.equal(`
+ 10 + | + 20 + | + 3 +
`); + }); + }); }); } diff --git a/packages/enzyme-test-suite/test/shared/methods/find.jsx b/packages/enzyme-test-suite/test/shared/methods/find.jsx index 0b58969d4..0558dead1 100644 --- a/packages/enzyme-test-suite/test/shared/methods/find.jsx +++ b/packages/enzyme-test-suite/test/shared/methods/find.jsx @@ -905,7 +905,7 @@ export default function describeFind({ function InnerComp({ message }) { return
{message}
; } - const InnerMemo = React.memo(InnerComp); + const InnerMemo = memo(InnerComp); const InnerFoo = ({ foo }) => (
diff --git a/packages/enzyme-test-suite/test/shared/methods/simulate.jsx b/packages/enzyme-test-suite/test/shared/methods/simulate.jsx index df7dcd3d3..b0aa9ebc7 100644 --- a/packages/enzyme-test-suite/test/shared/methods/simulate.jsx +++ b/packages/enzyme-test-suite/test/shared/methods/simulate.jsx @@ -11,6 +11,10 @@ import { REACT16, } from '../../_helpers/version'; +import { + memo, +} from '../../_helpers/react-compat'; + export default function describeSimulate({ Wrap, WrapperName, @@ -287,5 +291,30 @@ export default function describeSimulate({ expect(onClick).to.have.property('callCount', 1); }); }); + + describeIf(is('>= 16.6'), 'React.memo', () => { + itIf(isMount, 'can simulate events', () => { + function Child({ onClick }) { + return