diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index aa639cb8d..4276eeb1a 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -629,6 +629,7 @@ describeWithDOM('mount', () => { const a =
; const b =
; const c =
; + expect(mount(a).equals(b)).to.equal(true); expect(mount(a).equals(c)).to.equal(false); }); @@ -656,7 +657,7 @@ describeWithDOM('mount', () => { expect(wrapper.equals(b)).to.equal(true); }); - it.skip('does not expand `node` content', () => { + it('does not expand `node` content', () => { class Bar extends React.Component { render() { return
; } } @@ -665,8 +666,9 @@ describeWithDOM('mount', () => { render() { return ; } } - expect(mount().equals()).to.equal(true); - expect(mount().equals()).to.equal(false); + const wrapper = mount().children(); + expect(wrapper.equals()).to.equal(true); + expect(wrapper.equals()).to.equal(false); }); describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { @@ -684,7 +686,7 @@ describeWithDOM('mount', () => { expect(wrapper.equals(b)).to.equal(true); }); - it.skip('does not expand `node` content', () => { + it('does not expand `node` content', () => { const Bar = () => (
); @@ -693,12 +695,13 @@ describeWithDOM('mount', () => { ); - expect(mount().equals()).to.equal(true); - expect(mount().equals()).to.equal(false); + const wrapper = mount().children(); + expect(wrapper.equals()).to.equal(true); + expect(wrapper.equals()).to.equal(false); }); }); - it.skip('flattens arrays of children to compare', () => { + it('flattens arrays of children to compare', () => { class TwoChildren extends React.Component { render() { return ( @@ -720,8 +723,8 @@ describeWithDOM('mount', () => { ); } } - const twoChildren = mount(); - const twoChildrenOneArrayed = mount(); + const twoChildren = mount().children(); + const twoChildrenOneArrayed = mount().children(); expect(twoChildren.equals(twoChildrenOneArrayed.getElement())).to.equal(true); expect(twoChildrenOneArrayed.equals(twoChildren.getElement())).to.equal(true); @@ -752,6 +755,100 @@ describeWithDOM('mount', () => { expect(hostNodes.filter('div')).to.have.lengthOf(1); expect(hostNodes.filter('span')).to.have.lengthOf(1); }); + + it('does NOT allow matches on a nested node', () => { + const wrapper = mount(( +
+
+
+ )); + const b =
; + expect(wrapper.equals(b)).to.equal(false); + }); + + it('matches composite components', () => { + class Foo extends React.Component { + render() { return
; } + } + const wrapper = mount(( +
+ +
+ )); + const b =
; + expect(wrapper.equals(b)).to.equal(true); + }); + + it.skip('does not expand `node` content', () => { + class Bar extends React.Component { + render() { return
; } + } + + class Foo extends React.Component { + render() { return ; } + } + + expect(mount().equals()).to.equal(true); + expect(mount().equals()).to.equal(false); + }); + + describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { + it('matches composite SFCs', () => { + const Foo = () => ( +
+ ); + + const wrapper = mount(( +
+ +
+ )); + const b =
; + expect(wrapper.equals(b)).to.equal(true); + }); + + it.skip('does not expand `node` content', () => { + const Bar = () => ( +
+ ); + + const Foo = () => ( + + ); + + expect(mount().equals()).to.equal(true); + expect(mount().equals()).to.equal(false); + }); + }); + + it.skip('flattens arrays of children to compare', () => { + class TwoChildren extends React.Component { + render() { + return ( +
+
+
+
+ ); + } + } + + class TwoChildrenOneArrayed extends React.Component { + render() { + return ( +
+
+ {[
]} +
+ ); + } + } + const twoChildren = mount(); + const twoChildrenOneArrayed = mount(); + + expect(twoChildren.equals(twoChildrenOneArrayed.getElement())).to.equal(true); + expect(twoChildrenOneArrayed.equals(twoChildren.getElement())).to.equal(true); + }); }); wrap() @@ -1729,7 +1826,7 @@ describeWithDOM('mount', () => { render() { return (
- {this.props.id} + {this.props.foo}
); } @@ -1740,6 +1837,144 @@ describeWithDOM('mount', () => { expect(wrapper.find('.bar')).to.have.lengthOf(1); }); + describe('merging props', () => { + it('merges, not replaces, props when rerendering', () => { + class Foo extends React.Component { + render() { + return ( +
+ {this.props.foo} +
+ ); + } + } + + const wrapper = mount(); + + expect(wrapper.children().debug()).to.equal(` +
+ bar +
+ `.trim()); + expect(wrapper.children().props()).to.eql({ + className: 'foo', + children: 'bar', + }); + expect(wrapper.instance().props).to.eql({ + id: 'foo', + foo: 'bar', + }); + + wrapper.setProps({ id: 'bar' }); + + expect(wrapper.children().debug()).to.equal(` +
+ bar +
+ `.trim()); + expect(wrapper.children().props()).to.eql({ + className: 'bar', + children: 'bar', + }); + expect(wrapper.instance().props).to.eql({ + id: 'bar', + foo: 'bar', + }); + }); + + itIf(is('> 0.13'), 'merges, not replaces, props on SFCs', () => { + function Foo({ id, foo }) { + return ( +
+ {foo} +
+ ); + } + const wrapper = mount(); + + expect(wrapper.children().debug()).to.equal(` +
+ bar +
+ `.trim()); + expect(wrapper.children().props()).to.eql({ + className: 'foo', + children: 'bar', + }); + if (is('< 16')) { + expect(wrapper.instance().props).to.eql({ + id: 'foo', + foo: 'bar', + }); + } + + wrapper.setProps({ id: 'bar' }); + + expect(wrapper.children().debug()).to.equal(` +
+ bar +
+ `.trim()); + expect(wrapper.children().props()).to.eql({ + className: 'bar', + children: 'bar', + }); + if (is('< 16')) { + expect(wrapper.instance().props).to.eql({ + id: 'bar', + foo: 'bar', + }); + } + }); + + it('merges, not replaces, props when no rerender is needed', () => { + class Foo extends React.Component { + shouldComponentUpdate() { + return false; + } + + render() { + return ( +
+ {this.props.foo} +
+ ); + } + } + const wrapper = mount(); + + expect(wrapper.children().debug()).to.equal(` +
+ bar +
+ `.trim()); + expect(wrapper.children().props()).to.eql({ + className: 'foo', + children: 'bar', + }); + expect(wrapper.instance().props).to.eql({ + id: 'foo', + foo: 'bar', + }); + + wrapper.setProps({ id: 'foo' }); + + expect(wrapper.children().debug()).to.equal(` +
+ bar +
+ `.trim()); + expect(wrapper.children().props()).to.eql({ + className: 'foo', + children: 'bar', + }); + expect(wrapper.instance().props).to.eql({ + id: 'foo', + foo: 'bar', + }); + }); + }); + it('calls componentWillReceiveProps for new renders', () => { const stateValue = {}; @@ -1798,7 +2033,9 @@ describeWithDOM('mount', () => { } class Bar extends React.Component { render() { - return
; + return ( +
+ ); } } @@ -1812,6 +2049,25 @@ describeWithDOM('mount', () => { expect(wrapper.props().d).to.equal('e'); }); + it('passes in old context', () => { + class Foo extends React.Component { + render() { + return ( +
{this.context.x}
+ ); + } + } + + Foo.contextTypes = { x: PropTypes.string }; + + const context = { x: 'yolo' }; + const wrapper = mount(, { context }); + expect(wrapper.first('div').text()).to.equal('yolo'); + + wrapper.setProps({ x: 5 }); // Just force a re-render + expect(wrapper.first('div').text()).to.equal('yolo'); + }); + it('uses defaultProps if new props includes undefined values', () => { const initialState = { a: 42 }; const context = { b: 7 }; @@ -4289,6 +4545,12 @@ describeWithDOM('mount', () => { }); }); + describe('.debug()', () => { + it('passes through to the debugNodes function', () => { + expect(mount(
).debug()).to.equal('
'); + }); + }); + describe('.html()', () => { it('returns html of straight DOM elements', () => { const wrapper = mount(( diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index 0f7dd2127..c60a3528f 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -510,6 +510,7 @@ describe('shallow', () => { const a =
; const b =
; const c =
; + expect(shallow(a).equals(b)).to.equal(true); expect(shallow(a).equals(c)).to.equal(false); }); @@ -546,8 +547,9 @@ describe('shallow', () => { render() { return ; } } - expect(shallow().equals()).to.equal(true); - expect(shallow().equals()).to.equal(false); + const wrapper = shallow(); + expect(wrapper.equals()).to.equal(true); + expect(wrapper.equals()).to.equal(false); }); describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { @@ -574,8 +576,9 @@ describe('shallow', () => { ); - expect(shallow().equals()).to.equal(true); - expect(shallow().equals()).to.equal(false); + const wrapper = shallow(); + expect(wrapper.equals()).to.equal(true); + expect(wrapper.equals()).to.equal(false); }); }); @@ -1522,7 +1525,7 @@ describe('shallow', () => { }); }); - describe('.setProps(newProps)', () => { + describe('.setProps(newProps[, callback)', () => { it('throws on a non-function callback', () => { class Foo extends React.Component { render() { @@ -6985,4 +6988,128 @@ describe('shallow', () => { expect(root.children().debug()).to.equal('\n\n\n'); }); }); + + describe('lifecycles', () => { + it('calls `componentDidUpdate` when component’s `setState` is called', () => { + class Foo extends React.Component { + constructor(props) { + super(props); + this.state = { + foo: 'init', + }; + } + + componentDidUpdate() {} + + onChange() { + this.setState({ foo: 'onChange update' }); + } + + render() { + return
{this.state.foo}
; + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); + + const wrapper = shallow(); + wrapper.setState({ foo: 'wrapper setState update' }); + expect(wrapper.state('foo')).to.equal('wrapper setState update'); + expect(spy).to.have.property('callCount', 1); + wrapper.instance().onChange(); + expect(wrapper.state('foo')).to.equal('onChange update'); + expect(spy).to.have.property('callCount', 2); + }); + + it('calls `componentDidUpdate` when component’s `setState` is called through a bound method', () => { + class Foo extends React.Component { + constructor(props) { + super(props); + this.state = { + foo: 'init', + }; + this.onChange = this.onChange.bind(this); + } + + componentDidUpdate() {} + + onChange() { + // enzyme can't handle the update because `this` is a ReactComponent instance, + // not a ShallowWrapper instance. + this.setState({ foo: 'onChange update' }); + } + + render() { + return ( +
+ {this.state.foo} + +
+ ); + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); + + const wrapper = shallow(); + wrapper.find('button').prop('onClick')(); + expect(wrapper.state('foo')).to.equal('onChange update'); + expect(spy).to.have.property('callCount', 1); + }); + + it('calls `componentDidUpdate` when component’s `setState` is called', () => { + class Foo extends React.Component { + constructor(props) { + super(props); + this.state = { + foo: 'init', + }; + this.update = () => this.setState({ foo: 'update' }); + } + + componentDidMount() { + this.update(); + } + + componentDidUpdate() {} + + render() { + return
{this.state.foo}
; + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); + + const wrapper = shallow(); + expect(spy).to.have.property('callCount', 1); + expect(wrapper.state('foo')).to.equal('update'); + }); + + it('does not call `componentDidMount` twice when a child component is created', () => { + class Foo extends React.Component { + constructor(props) { + super(props); + this.state = { + foo: 'init', + }; + } + + componentDidMount() {} + + render() { + return ( +
+ + {this.state.foo} +
+ ); + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidMount'); + + const wrapper = shallow(); + expect(spy).to.have.property('callCount', 1); + wrapper.find('button').prop('onClick')(); + expect(spy).to.have.property('callCount', 1); + }); + }); }); diff --git a/packages/enzyme/src/ReactWrapper.js b/packages/enzyme/src/ReactWrapper.js index 015f1b6b0..73a799e05 100644 --- a/packages/enzyme/src/ReactWrapper.js +++ b/packages/enzyme/src/ReactWrapper.js @@ -347,30 +347,6 @@ class ReactWrapper { return this; } - /** - * Whether or not a given react element matches the current render tree. - * It will determine if the wrapper root node "looks like" the expected - * element by checking if all props of the expected element are present - * on the wrapper root node and equals to each other. - * - * Example: - * ``` - * // MyComponent outputs
Hello
- * const wrapper = mount(); - * expect(wrapper.matchesElement(
Hello
)).to.equal(true); - * ``` - * - * @param {ReactElement} node - * @returns {Boolean} - */ - matchesElement(node) { - return this.single('matchesElement', () => { - const adapter = getAdapter(this[OPTIONS]); - const rstNode = adapter.elementToNode(node); - return nodeMatches(rstNode, this.getNodeInternal(), (a, b) => a <= b); - }); - } - /** * Whether or not a given react element exists in the mount render tree. * @@ -468,7 +444,7 @@ class ReactWrapper { } /** - * Whether or not a given react element exists in the current render tree. + * Whether or not a given react element exists in the render tree. * * Example: * ``` @@ -483,6 +459,31 @@ class ReactWrapper { return this.single('equals', () => nodeEqual(this.getNodeInternal(), node)); } + /** + * Whether or not a given react element matches the render tree. + * Match is based on the expected element and not on wrapper root node. + * It will determine if the wrapper root node "looks like" the expected + * element by checking if all props of the expected element are present + * on the wrapper root node and equals to each other. + * + * Example: + * ``` + * // MyComponent outputs
Hello
+ * const wrapper = mount(); + * expect(wrapper.matchesElement(
Hello
)).to.equal(true); + * ``` + * + * @param {ReactElement} node + * @returns {Boolean} + */ + matchesElement(node) { + return this.single('matchesElement', () => { + const adapter = getAdapter(this[OPTIONS]); + const rstNode = adapter.elementToNode(node); + return nodeMatches(rstNode, this.getNodeInternal(), (a, b) => a <= b); + }); + } + /** * Finds every node in the render tree of the current wrapper that matches the provided selector. * @@ -1089,7 +1090,6 @@ class ReactWrapper { } } - if (ITERATOR_SYMBOL) { Object.defineProperty(ReactWrapper.prototype, ITERATOR_SYMBOL, { configurable: true, diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js index ecc29f220..469de0b64 100644 --- a/packages/enzyme/src/ShallowWrapper.js +++ b/packages/enzyme/src/ShallowWrapper.js @@ -227,6 +227,11 @@ class ShallowWrapper { return this[ROOT]; } + /** + * Returns the wrapped component. + * + * @return {ReactComponent} + */ getNodeInternal() { if (this.length !== 1) { throw new Error('ShallowWrapper::getNode() can only be called when wrapping one node'); @@ -237,6 +242,18 @@ class ShallowWrapper { return this[NODE]; } + /** + * Returns the the wrapped components. + * + * @return {Array} + */ + getNodesInternal() { + if (this[ROOT] === this && this.length === 1) { + this.update(); + } + return this[NODES]; + } + /** * Returns the wrapped ReactElement. * @@ -263,13 +280,6 @@ class ShallowWrapper { throw new Error('ShallowWrapper::getNode() is no longer supported. Use ShallowWrapper::getElement() instead'); } - getNodesInternal() { - if (this[ROOT] === this && this.length === 1) { - this.update(); - } - return this[NODES]; - } - // eslint-disable-next-line class-methods-use-this getNodes() { throw new Error('ShallowWrapper::getNodes() is no longer supported. Use ShallowWrapper::getElements() instead'); @@ -314,6 +324,16 @@ class ShallowWrapper { return this; } + /** + * A method that unmounts the component. This can be used to simulate a component going through + * and unmount/mount lifecycle. + * @returns {ShallowWrapper} + */ + unmount() { + this[RENDERER].unmount(); + return this; + } + /** * A method is for re-render with new props and context. * This calls componentDidUpdate method if disableLifecycleMethods is not enabled. @@ -666,7 +686,7 @@ class ShallowWrapper { } /** - * Whether or not a given react element exists in the shallow render tree. + * Whether or not a given react element exists in the render tree. * * Example: * ``` @@ -682,7 +702,7 @@ class ShallowWrapper { } /** - * Whether or not a given react element matches the shallow render tree. + * Whether or not a given react element matches the render tree. * Match is based on the expected element and not on wrapper root node. * It will determine if the wrapper root node "looks like" the expected * element by checking if all props of the expected element are present @@ -814,16 +834,6 @@ class ShallowWrapper { return this.type() === null ? cheerio() : cheerio.load('')(this.html()); } - /** - * A method that unmounts the component. This can be used to simulate a component going through - * and unmount/mount lifecycle. - * @returns {ShallowWrapper} - */ - unmount() { - this[RENDERER].unmount(); - return this; - } - /** * Used to simulate events. Pass an eventname and (optionally) event arguments. This method of * testing events should be met with some skepticism.