+ describe('context', () => {
+ it('can pass in context', () => {
+ const Bar = (props, context) => (
+
+ );
+ Bar.contextTypes = { name: PropTypes.string };
+ const Foo = () => (
+
);
- }
- }
- class TwoChildrenOneArrayed extends React.Component {
- render() {
- return (
-
-
- {[
]}
+ const context = { name: 'foo' };
+ const wrapper = shallow(
);
+ expect(wrapper.find(Bar).shallow({ context }).text()).to.equal('foo');
+ });
+
+ it('does not throw if context is passed in but contextTypes is missing', () => {
+ const Bar = (props, context) => (
+
{context.name}
+ );
+ const Foo = () => (
+
+
);
- }
- }
- const twoChildren = shallow(
);
- const twoChildrenOneArrayed = shallow(
);
- expect(twoChildren.equals(twoChildrenOneArrayed.getElement())).to.equal(true);
- expect(twoChildrenOneArrayed.equals(twoChildren.getElement())).to.equal(true);
- });
- });
+ const context = { name: 'foo' };
+ const wrapper = shallow(
);
+ expect(() => wrapper.find(Bar).shallow({ context })).to.not.throw();
+ });
- describe('.hostNodes()', () => {
- it('strips out any non-hostNode', () => {
- class Foo extends React.Component {
- render() {
- return
;
- }
- }
- const wrapper = shallow((
-
- ));
+ itIf(is('< 16'), 'is introspectable through context API', () => {
+ const Bar = (props, context) => (
+
{context.name}
+ );
+ Bar.contextTypes = { name: PropTypes.string };
+ const Foo = () => (
+
+
+
+ );
- const foos = wrapper.find('.foo');
- expect(foos).to.have.lengthOf(2);
+ const context = { name: 'foo' };
+ const wrapper = shallow(
).find(Bar).shallow({ context });
- const hostNodes = foos.hostNodes();
- expect(hostNodes).to.have.lengthOf(1);
+ expect(wrapper.context().name).to.equal(context.name);
+ expect(wrapper.context('name')).to.equal(context.name);
+ });
- expect(hostNodes.is('div')).to.equal(true);
- expect(hostNodes.hasClass('foo')).to.equal(true);
- });
- });
-
- describe('.find(selector)', () => {
- it('matches the root DOM element', () => {
- const wrapper = shallow(
hello
);
- expect(wrapper.find('#ttt')).to.have.lengthOf(1);
- expect(wrapper.find('.ttt')).to.have.lengthOf(1);
- });
-
- it('finds an element based on a class name', () => {
- const wrapper = shallow((
-
-
-
- ));
- expect(wrapper.find('.foo').type()).to.equal('input');
- });
-
- it('finds an element that has dot in attribute', () => {
- const wrapper = shallow((
-
- ));
-
- const elements = wrapper.find('[data-baz="foo.bar"]');
- expect(elements).to.have.lengthOf(1);
- });
-
- it('finds an element that with class and attribute', () => {
- const wrapper = shallow((
-
- ));
-
- const elements = wrapper.find('.classBar[data-baz="bar"]');
- expect(elements).to.have.lengthOf(1);
- });
-
- it('finds an element that with multiple classes and one attribute', () => {
- const wrapper = shallow((
-
- ));
-
- const elements = wrapper.find('.classBar.classFoo[data-baz="bar"]');
- expect(elements).to.have.lengthOf(1);
- });
-
- it('finds an element that with class and class with hyphen', () => {
- const wrapper = shallow((
-
- ));
-
- const elements = wrapper.find('.classBar.class-Foo');
- expect(elements).to.have.lengthOf(1);
- });
-
- it('finds an element based on a tag name and class name', () => {
- const wrapper = shallow((
-
-
-
- ));
- expect(wrapper.find('input.foo')).to.have.lengthOf(1);
- });
-
- it('finds an element based on a tag name and id', () => {
- const wrapper = shallow((
-
-
-
- ));
- expect(wrapper.find('input#foo')).to.have.lengthOf(1);
- });
+ itIf(is('>= 16'), 'will throw when trying to inspect context', () => {
+ const Bar = (props, context) => (
+
{context.name}
+ );
+ Bar.contextTypes = { name: PropTypes.string };
+ const Foo = () => (
+
+
+
+ );
- it('finds an element based on a tag name, id, and class name', () => {
- const wrapper = shallow((
-
-
-
- ));
- expect(wrapper.find('input#foo.bar')).to.have.lengthOf(1);
- });
+ const context = { name: 'foo' };
+ const wrapper = shallow(
).find(Bar).shallow({ context });
- it('finds an element based on a tag name', () => {
- const wrapper = shallow((
-
-
- Button
-
-
-
- ));
- expect(wrapper.find('input').props().className).to.equal('foo');
- expect(wrapper.find('button').props().className).to.equal('bar');
- expect(wrapper.find('textarea').props().className).to.equal('magic');
- expect(wrapper.find('select').props().className).to.equal('reality');
+ expect(() => wrapper.context()).to.throw(
+ Error,
+ 'ShallowWrapper::context() can only be called on wrapped nodes that have a non-null instance',
+ );
+ expect(() => wrapper.context('name')).to.throw(
+ Error,
+ 'ShallowWrapper::context() can only be called on wrapped nodes that have a non-null instance',
+ );
+ });
+ });
});
+ });
- it('finds a component based on a constructor', () => {
- class Foo extends React.Component {
- render() { return
; }
+ describe('.dive()', () => {
+ class RendersDOM extends React.Component {
+ render() {
+ return
;
}
- const wrapper = shallow((
-
-
-
- ));
- expect(wrapper.find(Foo).type()).to.equal(Foo);
- });
-
- wrap()
- .withOverride(() => getAdapter(), 'isValidElementType', () => () => false)
- .it('throws when an adapter’s `isValidElementType` lies', () => {
- class Foo extends React.Component {
- render() { return
; }
- }
- const wrapper = shallow((
+ }
+ class RendersNull extends React.Component {
+ render() {
+ return null;
+ }
+ }
+ class RendersMultiple extends React.Component {
+ render() {
+ return (
-
+
+
- ));
-
- expect(() => wrapper.find(Foo)).to.throw(
- TypeError,
- 'Enzyme::Selector expects a string, object, or valid element type (Component Constructor)',
);
- });
-
- it('finds a component based on a component display name', () => {
- class Foo extends React.Component {
- render() { return
; }
}
- const wrapper = shallow((
-
-
-
- ));
- expect(wrapper.find('Foo').type()).to.equal(Foo);
- });
-
- it('finds multiple elements based on a class name', () => {
- const wrapper = shallow((
-
-
-
-
- ));
- expect(wrapper.find('.foo')).to.have.lengthOf(2);
- });
+ }
+ class RendersZero extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ class WrapsRendersDOM extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ WrapsRendersDOM.contextTypes = { foo: PropTypes.string };
+ class DoubleWrapsRendersDOM extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ class ContextWrapsRendersDOM extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ ContextWrapsRendersDOM.contextTypes = { foo: PropTypes.string };
- it('finds multiple elements based on a tag name', () => {
- const wrapper = shallow((
-
-
-
-
-
- ));
- expect(wrapper.find('input')).to.have.lengthOf(2);
- expect(wrapper.find('button')).to.have.lengthOf(1);
- });
+ it('throws on a DOM node', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.is('div')).to.equal(true);
- it('works on non-single nodes', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.a')).to.have.lengthOf(1);
- expect(wrapper.find('.b')).to.have.lengthOf(2);
- expect(wrapper.find('.b').find('.c')).to.have.lengthOf(6);
+ expect(() => { wrapper.dive(); }).to.throw(
+ TypeError,
+ 'ShallowWrapper::dive() can not be called on Host Components',
+ );
});
- it('finds component based on a react prop', () => {
- const wrapper = shallow((
-
-
-
- ));
-
- expect(wrapper.find('[title="foo"]')).to.have.lengthOf(1);
- expect(wrapper.find('[title]')).to.have.lengthOf(1);
- });
+ it('throws on a non-component', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.type()).to.equal(null);
- it('works with an adjacent sibling selector', () => {
- const a = 'some';
- const b = 'text';
- const wrapper = shallow((
-
-
- {a}
- {b}
-
-
- {a}
- {b}
-
-
- ));
- expect(wrapper.find('.row')).to.have.lengthOf(2);
- expect(wrapper.find('.row + .row')).to.have.lengthOf(1);
+ expect(() => { wrapper.dive(); }).to.throw(
+ TypeError,
+ 'ShallowWrapper::dive() can only be called on components',
+ );
});
- it('throws for non-numeric attribute values without quotes', () => {
- const wrapper = shallow((
-
-
-
-
-
- ));
- expect(() => wrapper.find('[type=text]')).to.throw(
+ it('throws on multiple children found', () => {
+ const wrapper = shallow(
).find('div').children();
+ expect(() => { wrapper.dive(); }).to.throw(
Error,
- 'Failed to parse selector: [type=text]',
+ 'Method “dive” is meant to be run on 1 node. 2 found instead.',
);
- expect(() => wrapper.find('[type=hidden]')).to.throw(
+ });
+
+ it('throws on zero children found', () => {
+ const wrapper = shallow(
).find('div').children();
+ expect(() => { wrapper.dive(); }).to.throw(
Error,
- 'Failed to parse selector: [type=hidden]',
+ 'Method “dive” is meant to be run on 1 node. 0 found instead.',
);
- expect(() => wrapper.find('[type="text"]')).to.not.throw(
+ });
+
+ it('throws on zero children found', () => {
+ const wrapper = shallow(
).find('div').children();
+ expect(() => { wrapper.dive(); }).to.throw(
Error,
- 'Failed to parse selector: [type="text"]',
+ 'Method “dive” is meant to be run on 1 node. 0 found instead.',
);
});
- it('errors sensibly if any of the search props are undefined', () => {
- const wrapper = shallow((
-
-
-
- ));
+ it('dives + shallow-renders when there is one component child', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.is(WrapsRendersDOM)).to.equal(true);
- expect(() => wrapper.find({ type: undefined })).to.throw(
- TypeError,
- 'Enzyme::Props can’t have `undefined` values. Try using ‘findWhere()’ instead.',
- );
+ const underwater = wrapper.dive();
+ expect(underwater.is(RendersDOM)).to.equal(true);
});
- it('compounds tag and prop selector', () => {
- const wrapper = shallow((
-
-
-
- ));
+ describeIf(is('>=16.3.0'), 'forwardRef Elements', () => {
+ const ForwardRefWrapsRendersDOM = forwardRef && forwardRef(() =>
);
+ const NestedForwarRefsWrapsRendersDom = forwardRef && forwardRef(() =>
);
- expect(wrapper.find('span[preserveAspectRatio="xMaxYMax"]')).to.have.lengthOf(1);
- expect(wrapper.find('span[preserveAspectRatio]')).to.have.lengthOf(1);
- });
+ if (forwardRef) {
+ NestedForwarRefsWrapsRendersDom.contextTypes = { foo: PropTypes.string };
+ ForwardRefWrapsRendersDOM.contextTypes = { foo: PropTypes.string };
+ }
- it('supports data prop selectors', () => {
- const wrapper = shallow((
-
-
-
- ));
+ it('dives + shallow-renders a forwardRef component', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.is(WrapsRendersDOM)).to.equal(true);
- expect(wrapper.find('[data-foo="bar"]')).to.have.lengthOf(1);
- expect(wrapper.find('[data-foo]')).to.have.lengthOf(1);
- });
+ const underwater = wrapper.dive();
+ expect(underwater.is(RendersDOM)).to.equal(true);
+ });
- it('finds components with multiple matching react props', () => {
- function noop() {}
- const wrapper = shallow((
-
-
-
- ));
+ it('dives + shallow-renders a with nested forwardRefs component', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.is(ForwardRefWrapsRendersDOM)).to.equal(true);
- expect(wrapper.find('span[htmlFor="foo"][onChange]')).to.have.lengthOf(1);
- expect(wrapper.find('span[htmlFor="foo"][preserveAspectRatio="xMaxYMax"]')).to.have.lengthOf(1);
- expect(wrapper.find('[htmlFor][preserveAspectRatio]')).to.have.lengthOf(1);
+ const underwater = wrapper.dive();
+ expect(underwater.is(WrapsRendersDOM)).to.equal(true);
+ });
});
- it('supports boolean and numeric values for matching props', () => {
- const wrapper = shallow((
-
- ));
+ it('merges and pass options through', () => {
+ const wrapper = shallow(
, { context: { foo: 'hello' } });
+ expect(wrapper.context()).to.deep.equal({ foo: 'hello' });
+
+ let underwater = wrapper.dive();
+ expect(underwater.context()).to.deep.equal({ foo: 'hello' });
- expect(wrapper.find('span[value=1]')).to.have.lengthOf(1);
- expect(wrapper.find('span[value=2]')).to.have.lengthOf(0);
- expect(wrapper.find('a[value=false]')).to.have.lengthOf(1);
- expect(wrapper.find('a[value=true]')).to.have.lengthOf(0);
+ underwater = wrapper.dive({ context: { foo: 'enzyme!' } });
+ expect(underwater.context()).to.deep.equal({ foo: 'enzyme!' });
});
+ });
- it('does not find key or ref via property selector', () => {
- const arrayOfComponents = [
,
];
-
- const wrapper = shallow((
-
-
- {arrayOfComponents}
-
- ));
-
- expect(wrapper.find('div[ref="foo"]')).to.have.lengthOf(0);
- expect(wrapper.find('div[key="1"]')).to.have.lengthOf(0);
- expect(wrapper.find('[ref]')).to.have.lengthOf(0);
- expect(wrapper.find('[key]')).to.have.lengthOf(0);
- });
-
- it('finds multiple elements based on a constructor', () => {
- const wrapper = shallow((
-
-
-
-
-
- ));
- expect(wrapper.find('input')).to.have.lengthOf(2);
- expect(wrapper.find('button')).to.have.lengthOf(1);
- });
-
- it('supports object property selectors', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find({ a: 1 })).to.have.lengthOf(0);
- expect(wrapper.find({ 'data-test': 'ref' })).to.have.lengthOf(7);
- expect(wrapper.find({ className: 'foo' })).to.have.lengthOf(1);
- expect(wrapper.find({ prop: null })).to.have.lengthOf(1);
- expect(wrapper.find({ prop: 123 })).to.have.lengthOf(1);
- expect(wrapper.find({ prop: false })).to.have.lengthOf(1);
- expect(wrapper.find({ prop: true })).to.have.lengthOf(1);
- });
-
- it('supports complex and nested object property selectors', () => {
- const testFunction = () => ({});
- const wrapper = shallow((
-
- ));
- expect(wrapper.find({ 'data-test': 'ref' })).to.have.lengthOf(4);
- expect(wrapper.find({ more: { a: 1 } })).to.have.lengthOf(0);
- expect(wrapper.find({ more: [{ id: 1 }] })).to.have.lengthOf(2);
- expect(wrapper.find({ more: { item: { id: 1 } } })).to.have.lengthOf(1);
- expect(wrapper.find({ style: { height: 20 } })).to.have.lengthOf(1);
- expect(wrapper.find({
- more: [{ id: 1 }],
- 'data-test': 'ref',
- prop: true,
- onChange: testFunction,
- })).to.have.lengthOf(1);
- });
-
- it('throws when given empty object, null, or an array', () => {
- const wrapper = shallow((
-
-
-
- ));
- expect(() => wrapper.find({})).to.throw(
- TypeError,
- 'Enzyme::Selector does not support an array, null, or empty object as a selector',
- );
- expect(() => wrapper.find([])).to.throw(
- TypeError,
- 'Enzyme::Selector does not support an array, null, or empty object as a selector',
- );
- expect(() => wrapper.find(null)).to.throw(
- TypeError,
- 'Enzyme::Selector does not support an array, null, or empty object as a selector',
- );
- });
-
- it('queries attributes with spaces in their values', () => {
- const wrapper = shallow((
-
-
Hello
- World
-
- ));
- expect(wrapper.find('[data-foo]')).to.have.lengthOf(2);
- expect(wrapper.find('[data-foo="foo bar"]')).to.have.lengthOf(1);
- expect(wrapper.find('[data-foo="bar baz quz"]')).to.have.lengthOf(1);
- expect(wrapper.find('[data-foo="bar baz"]')).to.have.lengthOf(0);
- expect(wrapper.find('[data-foo="foo bar"]')).to.have.lengthOf(0);
- expect(wrapper.find('[data-foo="bar baz quz"]')).to.have.lengthOf(0);
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('finds a component based on a constructor', () => {
- const Foo = () => (
-
- );
- const wrapper = shallow((
-
-
-
- ));
- expect(wrapper.find(Foo).type()).to.equal(Foo);
- });
-
- it('finds a component based on a display name', () => {
- const Foo = () => (
-
- );
- const wrapper = shallow((
-
-
-
- ));
- expect(wrapper.find('Foo').type()).to.equal(Foo);
- });
- });
-
- describe('works with attribute selectors containing #', () => {
- let wrapper;
- beforeEach(() => {
- wrapper = shallow((
-
- ));
- });
+ describe('lifecycle methods', () => {
+ describe('disableLifecycleMethods option', () => {
+ describe('validation', () => {
+ it('throws for a non-boolean value', () => {
+ ['value', 42, null].forEach((value) => {
+ expect(() => shallow(
, {
+ disableLifecycleMethods: value,
+ })).to.throw(/true or false/);
+ });
+ });
- it('works with an ID', () => {
- expect(wrapper.find('a#test')).to.have.lengthOf(1);
- });
+ it('does not throw for a boolean value or undefined', () => {
+ [true, false, undefined].forEach((value) => {
+ expect(() => shallow(
, {
+ disableLifecycleMethods: value,
+ })).not.to.throw();
+ });
+ });
- it('works with a normal attribute', () => {
- expect(wrapper.find('a[href="/page"]')).to.have.lengthOf(1);
- });
+ it('does not throw when no lifecycle flags are provided in options', () => {
+ expect(() => shallow(
, {})).not.to.throw();
+ });
- it('works with an attribute with a #', () => {
- expect(wrapper.find('a[href="/page#anchor"]')).to.have.lengthOf(1);
+ it('throws when used with lifecycleExperimental in invalid combinations', () => {
+ [true, false].forEach((value) => {
+ expect(() => shallow(
, {
+ lifecycleExperimental: value,
+ disableLifecycleMethods: value,
+ })).to.throw(/same value/);
+ });
+ });
});
- });
- describe('works with data- attributes', () => {
- class Foo extends React.Component {
- render() {
- return (
-
-
-
-
-
-
-
- );
- }
- }
+ describe('when disabled', () => {
+ let wrapper;
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ componentWillMount() { spy('componentWillMount'); }
- it('finds elements by data attribute', () => {
- const wrapper = shallow(
);
- expect(wrapper.html()).to.contain('data-custom-tag="bookIcon"'); // sanity check
- const elements = wrapper.find('[data-custom-tag="bookIcon"]');
- expect(elements).to.have.lengthOf(2);
- expect(elements.filter('i')).to.have.lengthOf(2);
- });
- });
+ componentDidMount() { spy('componentDidMount'); }
- describeIf(is('>= 16.2'), 'works with fragments', () => {
- const NestedFragmentComponent = () => (
-
-
- A span
- B span
- A div
-
- C span
-
-
-
D span
-
- );
+ componentWillReceiveProps() { spy('componentWillReceiveProps'); }
- it('finds descendant span inside React.Fragment', () => {
- const wrapper = shallow(
);
- expect(wrapper.find('.container span')).to.have.lengthOf(4);
- });
+ shouldComponentUpdate() {
+ spy('shouldComponentUpdate');
+ return true;
+ }
- it('finds nonexistent p inside React.Fragment', () => {
- const wrapper = shallow(
);
- expect(wrapper.find('.container p')).to.have.lengthOf(0);
- });
+ componentWillUpdate() { spy('componentWillUpdate'); }
- it('finds direct child span inside React.Fragment', () => {
- const wrapper = shallow(
);
- expect(wrapper.find('.container > span')).to.have.lengthOf(4);
- });
+ componentDidUpdate() { spy('componentDidUpdate'); }
- it('handles adjacent sibling selector inside React.Fragment', () => {
- const wrapper = shallow(
);
- expect(wrapper.find('.container span + div')).to.have.lengthOf(1);
- });
+ componentWillUnmount() { spy('componentWillUnmount'); }
- it('handles general sibling selector inside React.Fragment', () => {
- const wrapper = shallow(
);
- expect(wrapper.find('.container div ~ span')).to.have.lengthOf(2);
- });
+ render() {
+ spy('render');
+ return
foo
;
+ }
+ }
- it('handles fragments with no content', () => {
- const EmptyFragmentComponent = () => (
-
-
-
-
-
- );
- const wrapper = shallow(
);
+ const options = {
+ disableLifecycleMethods: true,
+ context: {
+ foo: 'foo',
+ },
+ };
- expect(wrapper.find('.container > span')).to.have.lengthOf(0);
- expect(wrapper.find('.container span')).to.have.lengthOf(0);
- expect(wrapper.children()).to.have.lengthOf(0);
- });
- });
+ beforeEach(() => {
+ wrapper = shallow(
, options);
+ spy.resetHistory();
+ });
- itIf(is('>= 16'), 'finds portals by name', () => {
- const containerDiv = { nodeType: 1 };
- const Foo = () => (
-
- {createPortal(
-
InPortal
,
- containerDiv,
- )}
-
- );
+ it('does not call componentDidMount when mounting', () => {
+ wrapper = shallow(
, options);
+ expect(spy.args).to.deep.equal([
+ ['componentWillMount'],
+ ['render'],
+ ]);
+ });
- const wrapper = shallow(
);
+ it('calls expected methods when receiving new props', () => {
+ wrapper.setProps({ foo: 'foo' });
+ expect(spy.args).to.deep.equal([
+ ['componentWillReceiveProps'],
+ ['shouldComponentUpdate'],
+ ['componentWillUpdate'],
+ ['render'],
+ ]);
+ });
- expect(wrapper.find('Portal')).to.have.lengthOf(1);
- });
+ describeIf(is('0.13 || 15 || > 16'), 'setContext', () => {
+ it('calls expected methods when receiving new context', () => {
+ wrapper.setContext({ foo: 'foo' });
+ expect(spy.args).to.deep.equal([
+ ['componentWillReceiveProps'],
+ ['shouldComponentUpdate'],
+ ['componentWillUpdate'],
+ ['render'],
+ ]);
+ });
+ });
- itIf(is('>= 16'), 'finds elements through portals', () => {
- const containerDiv = { nodeType: 1 };
- const Foo = () => (
-
- {createPortal(
-
-
Successful Portal!
-
- ,
- containerDiv,
- )}
-
- );
+ describeIf(is('16'), 'setContext', () => {
+ it('calls expected methods when receiving new context', () => {
+ wrapper.setContext({ foo: 'foo' });
+ expect(spy.args).to.deep.equal([
+ ['shouldComponentUpdate'],
+ ['componentWillUpdate'],
+ ['render'],
+ ]);
+ });
+ });
+ describeIf(is('0.14'), 'setContext', () => {
+ it('calls expected methods when receiving new context', () => {
+ wrapper.setContext({ foo: 'foo' });
+ expect(spy.args).to.deep.equal([
+ ['shouldComponentUpdate'],
+ ['componentWillUpdate'],
+ ['render'],
+ ]);
+ });
+ });
- const wrapper = shallow(
);
+ itIf(is('< 16'), 'calls expected methods for setState', () => {
+ wrapper.setState({ bar: 'bar' });
+ expect(spy.args).to.deep.equal([
+ ['shouldComponentUpdate'],
+ ['componentWillUpdate'],
+ ['render'],
+ ['componentDidUpdate'],
+ ]);
+ });
- expect(wrapper.find('h1')).to.have.lengthOf(1);
+ // componentDidUpdate is not called in react 16
+ itIf(is('>= 16'), 'calls expected methods for setState', () => {
+ wrapper.setState({ bar: 'bar' });
+ expect(spy.args).to.deep.equal([
+ ['shouldComponentUpdate'],
+ ['componentWillUpdate'],
+ ['render'],
+ ]);
+ });
- expect(wrapper.find('span')).to.have.lengthOf(1);
- });
+ it('calls expected methods when unmounting', () => {
+ wrapper.unmount();
+ expect(spy.args).to.deep.equal([
+ ['componentWillUnmount'],
+ ]);
+ });
+ });
- describeIf(is('>= 16.3'), 'forwardRef', () => {
- it('finds forwardRefs', () => {
- const Component = forwardRef(() =>
);
+ it('does not call when disableLifecycleMethods flag is true', () => {
+ const spy = sinon.spy();
class Foo extends React.Component {
+ componentDidMount() {
+ spy();
+ }
+
render() {
- return (
-
-
-
-
- );
+ return
foo
;
}
}
-
- const wrapper = shallow(
);
- expect(wrapper.find(Component)).to.have.lengthOf(2);
- expect(wrapper.find('ForwardRef')).to.have.lengthOf(2);
+ shallow(
, { disableLifecycleMethods: true });
+ expect(spy).to.have.property('callCount', 0);
});
- it('finds forwardRef by custom display name', () => {
- const Component = forwardRef(() =>
);
- Component.displayName = 'CustomForwardRef';
- class Foo extends React.Component {
+ it('calls `componentDidMount` directly when disableLifecycleMethods is true', () => {
+ class Table extends React.Component {
render() {
- return (
-
-
-
-
- );
+ return (
);
}
}
- const wrapper = shallow(
);
- expect(wrapper.find(Component)).to.have.lengthOf(2);
- expect(wrapper.find(Component.displayName)).to.have.lengthOf(2);
- });
- });
-
- describeWithDOM('find DOM elements by constructor', () => {
- const { elements, all } = getData();
-
- elements.filter(({ constructor: C }) => C && C !== all).forEach(({
- tag: Tag,
- constructorName: name,
- }) => {
- class Foo extends React.Component {
- render() {
- return
;
+ class MyComponent extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ showTable: false,
+ };
}
- }
- it(`${Tag}: found with \`${name}\``, () => {
- const wrapper = shallow(
);
-
- const rendered = wrapper;
- expect(rendered.type()).to.equal(Tag);
- expect(rendered.is(Tag)).to.equal(true);
- expect(wrapper.filter(Tag)).to.have.lengthOf(1);
- });
- });
- });
-
- describeIf(is('>= 16.6'), 'React.memo', () => {
- it('works with an SFC', () => {
- const InnerComp = () =>
Hello
;
- const InnerFoo = ({ foo }) => (
-
- );
- const Foo = memo(InnerFoo);
-
- const wrapper = shallow(
);
- expect(wrapper.debug()).to.equal(`
`);
- expect(wrapper.find('InnerComp')).to.have.lengthOf(1);
- expect(wrapper.find('.bar')).to.have.lengthOf(1);
- expect(wrapper.find('.qoo').text()).to.equal('qux');
- });
+ componentDidMount() {
+ this.setState({ showTable: true });
+ }
- it('throws with a class component', () => {
- class InnerComp extends React.Component {
render() {
- return
Hello
;
+ const { showTable } = this.state;
+ return (
);
}
}
+ const wrapper = shallow(
, { disableLifecycleMethods: true });
+ expect(wrapper.find(Table).length).to.equal(0);
+ wrapper.instance().componentDidMount();
+ expect(wrapper.find(Table).length).to.equal(1);
+ });
+ it('calls shouldComponentUpdate when disableLifecycleMethods flag is true', () => {
+ const spy = sinon.spy();
class Foo extends React.Component {
- render() {
- const { foo } = this.props;
- return (
-
- );
+ constructor(props) {
+ super(props);
+ this.state = {
+ foo: 'bar',
+ };
}
- }
- const FooMemo = memo(Foo);
-
- expect(() => shallow(
)).to.throw(TypeError);
- });
- it.skip('works with a class component', () => {
- class InnerComp extends React.Component {
- render() {
- return
Hello
;
+ shouldComponentUpdate() {
+ spy();
+ return false;
}
- }
- class Foo extends React.Component {
render() {
- const { foo } = this.props;
- return (
-
- );
+ return
{this.state.foo}
;
}
}
- const FooMemo = memo(Foo);
-
- const wrapper = shallow(
);
- expect(wrapper.debug()).to.equal(`
`);
- expect(wrapper.find('InnerComp')).to.have.lengthOf(1);
- expect(wrapper.find('.bar')).to.have.lengthOf(1);
- expect(wrapper.find('.qoo').text()).to.equal('qux');
+ const wrapper = shallow(
+
,
+ {
+ context: { foo: 'foo' },
+ disableLifecycleMethods: true,
+ },
+ );
+ expect(spy).to.have.property('callCount', 0);
+ wrapper.setProps({ foo: 'bar' });
+ expect(spy).to.have.property('callCount', 1);
+ wrapper.setState({ foo: 'bar' });
+ expect(spy).to.have.property('callCount', 2);
+ wrapper.setContext({ foo: 'bar' });
+ expect(spy).to.have.property('callCount', 3);
});
});
- });
-
- describe('.findWhere(predicate)', () => {
- it('returns all elements for a truthy test', () => {
- const wrapper = shallow((
-
-
-
-
- ));
- expect(wrapper.findWhere(() => true)).to.have.lengthOf(3);
- });
-
- it('returns no elements for a falsy test', () => {
- const wrapper = shallow((
-
-
-
-
- ));
- expect(wrapper.findWhere(() => false)).to.have.lengthOf(0);
- });
-
- it('does not pass empty wrappers', () => {
- class EditableText extends React.Component {
- render() {
- return
{''}
;
- }
- }
- const wrapper = shallow(
);
+ describe('lifecycleExperimental option', () => {
+ describe('validation', () => {
+ it('throws for a non-boolean value', () => {
+ ['value', 42, null].forEach((value) => {
+ expect(() => shallow(
, {
+ lifecycleExperimental: value,
+ })).to.throw(/true or false/);
+ });
+ });
- const stub = sinon.stub();
- wrapper.findWhere(stub);
- const passedNodeLengths = stub.getCalls().map(({ args: [firstArg] }) => firstArg.length);
- expect(passedNodeLengths).to.eql([1]);
+ it('does not throw for a boolean value or when not provided', () => {
+ [true, false, undefined].forEach((value) => {
+ expect(() => shallow(
, {
+ lifecycleExperimental: value,
+ })).not.to.throw();
+ });
+ });
+ });
});
- it('calls the predicate with the wrapped node as the first argument', () => {
- const wrapper = shallow((
-
- ));
+ describeIf(is('>= 16.3'), 'getDerivedStateFromProps', () => {
+ let spy;
- const stub = sinon.stub();
- stub.returns(true);
- const spy = sinon.spy(stub);
- wrapper.findWhere(spy);
- expect(spy).to.have.property('callCount', 4);
- expect(spy.args[0][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[1][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[2][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[3][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[1][0].hasClass('bar')).to.equal(true);
- expect(spy.args[2][0].hasClass('baz')).to.equal(true);
- expect(spy.args[3][0].hasClass('bux')).to.equal(true);
- });
+ beforeEach(() => {
+ spy = sinon.spy();
+ });
- it('finds nodes', () => {
- class Foo extends React.Component {
- render() {
- return (
-
-
-
-
- );
+ class Spy extends React.Component {
+ constructor(...args) {
+ super(...args);
+ this.state = { state: true }; // eslint-disable-line react/no-unused-state
+ spy('constructor');
}
- }
-
- const selector = 'blah';
- const wrapper = shallow(
);
- const foundSpan = wrapper.findWhere(n => (
- n.type() === 'span' && n.props()['data-foo'] === selector
- ));
- expect(foundSpan.type()).to.equal('span');
- const foundNotSpan = wrapper.findWhere(n => (
- n.type() !== 'span' && n.props()['data-foo'] === selector
- ));
- expect(foundNotSpan.type()).to.equal('i');
- });
+ shouldComponentUpdate(nextProps, nextState, nextContext) {
+ spy('shouldComponentUpdate', {
+ prevProps: this.props,
+ nextProps,
+ prevState: this.state,
+ nextState,
+ prevContext: this.context,
+ nextContext,
+ });
+ return true;
+ }
- describeIf(is('>= 16.2'), 'with fragments', () => {
- it('finds nodes', () => {
- class FragmentFoo extends React.Component {
- render() {
- return (
-
-
-
-
-
-
-
-
-
-
- );
- }
+ componentWillUpdate(nextProps, nextState, nextContext) {
+ spy('componentWillUpdate', {
+ prevProps: this.props,
+ nextProps,
+ prevState: this.state,
+ nextState,
+ prevContext: this.context,
+ nextContext,
+ });
}
- const selector = 'blah';
- const wrapper = shallow(
);
- const foundSpans = wrapper.findWhere(n => (
- n.type() === 'span' && n.props()['data-foo'] === selector
- ));
- expect(foundSpans).to.have.lengthOf(2);
- expect(foundSpans.get(0).type).to.equal('span');
- expect(foundSpans.get(1).type).to.equal('span');
-
- const foundNotSpans = wrapper.findWhere(n => (
- n.type() !== 'span' && n.props()['data-foo'] === selector
- ));
- expect(foundNotSpans).to.have.lengthOf(2);
- expect(foundNotSpans.get(0).type).to.equal('i');
- expect(foundNotSpans.get(1).type).to.equal('i');
- });
- });
+ componentDidUpdate(prevProps, prevState, prevContext) {
+ spy('componentDidUpdate', {
+ prevProps,
+ nextProps: this.props,
+ prevState,
+ nextState: this.state,
+ prevContext,
+ nextContext: this.context,
+ });
+ }
- it('finds nodes when conditionally rendered', () => {
- class Foo extends React.Component {
render() {
- return (
-
-
- {this.props.selector === 'baz' ? : null}
-
- );
+ spy('render');
+ return null;
}
}
- const selector = 'blah';
- const wrapper = shallow(
);
- const foundSpan = wrapper.findWhere(n => (
- n.type() === 'span' && n.props()['data-foo'] === selector
- ));
- expect(foundSpan.type()).to.equal('span');
-
- const foundNotSpan = wrapper.findWhere(n => (
- n.type() !== 'span' && n.props()['data-foo'] === selector
- ));
- expect(foundNotSpan).to.have.lengthOf(0);
- });
-
- it('does not get trapped when conditionally rendering using an empty string variable as the condition', () => {
- const emptyString = '';
-
- class Foo extends React.Component {
- render() {
- return (
-
- );
+ class CWRP extends Spy {
+ componentWillReceiveProps(nextProps, nextContext) {
+ spy('componentWillReceiveProps', {
+ prevProps: this.props,
+ nextProps,
+ prevState: this.state,
+ nextState: this.state,
+ prevContext: this.context,
+ nextContext,
+ });
}
}
- const selector = 'blah';
- const wrapper = shallow(
);
- const foundSpan = wrapper.findWhere(n => (
- n.type() === 'span'
- && n.props()['data-foo'] === selector
- ));
-
- expect(foundSpan.debug()).to.equal((
- `
- Test
- `
- ));
- });
-
- it('returns props object when props() is called', () => {
- class Foo extends React.Component {
- render() {
- return (
-
Test Component
- );
+ class U_CWRP extends Spy {
+ UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
+ spy('UNSAFE_componentWillReceiveProps', {
+ prevProps: this.props,
+ nextProps,
+ prevState: this.state,
+ nextState: this.state,
+ prevContext: this.context,
+ nextContext: undefined,
+ });
}
}
- const content = 'blah';
- const wrapper = shallow(
);
- // TODO: shallow has children, mount does not
- expect(wrapper.props()).to.deep.equal({ 'data-foo': content, children: 'Test Component' });
- });
-
- it('returns shallow rendered string when debug() is called', () => {
- class Foo extends React.Component {
- render() {
- return (
-
Test Component
- );
+ class GDSFP extends Spy {
+ static getDerivedStateFromProps(props, state) {
+ spy('getDerivedStateFromProps', { props, state });
+ return {};
}
}
- const content = 'blah';
- const wrapper = shallow(
);
- expect(wrapper.debug()).to.equal((
- `
- Test Component
-
`
- ));
- });
+ it('calls cWRP when expected', () => {
+ const prevProps = { a: 1 };
+ const wrapper = shallow(
);
+ expect(spy.args).to.deep.equal([
+ ['constructor'],
+ ['render'],
+ ]);
+ spy.resetHistory();
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('finds nodes', () => {
- const SFC = function SFC({ selector }) {
- return (
-
-
-
-
- );
- };
+ const foo = {};
+ const props = { foo };
+ const {
+ context: prevContext,
+ context: nextContext,
+ state: prevState,
+ state: nextState,
+ } = wrapper.instance();
- const selector = 'blah';
- const wrapper = shallow(
);
- const foundSpan = wrapper.findWhere(n => (
- n.type() === 'span' && n.props()['data-foo'] === selector
- ));
- expect(foundSpan.type()).to.equal('span');
-
- const foundNotSpan = wrapper.findWhere(n => (
- n.type() !== 'span' && n.props()['data-foo'] === selector
- ));
- expect(foundNotSpan.type()).to.equal('i');
- });
+ wrapper.setProps(props);
+ const nextProps = { ...prevProps, ...props };
- it('finds nodes when conditionally rendered', () => {
- const SFC = function SFC({ selector }) {
- return (
-
-
- {selector === 'baz' ? : null}
-
- );
+ const data = {
+ prevProps,
+ nextProps,
+ prevState,
+ nextState,
+ prevContext,
+ nextContext,
};
-
- const selector = 'blah';
- const wrapper = shallow(
);
- const foundSpan = wrapper.findWhere(n => (
- n.type() === 'span' && n.props()['data-foo'] === selector
- ));
- expect(foundSpan.type()).to.equal('span');
-
- const foundNotSpan = wrapper.findWhere(n => (
- n.type() !== 'span' && n.props()['data-foo'] === selector
- ));
- expect(foundNotSpan).to.have.lengthOf(0);
+ expect(spy.args).to.deep.equal([
+ ['componentWillReceiveProps', data],
+ ['shouldComponentUpdate', data],
+ ['componentWillUpdate', data],
+ ['render'],
+ ['componentDidUpdate', {
+ ...data,
+ prevContext: is('>= 16') ? undefined : prevContext,
+ }],
+ ]);
});
- it('returns props object when props() is called', () => {
- const SFC = function SFC({ data }) {
- return (
-
Test SFC
- );
- };
+ it('calls UNSAFE_cWRP when expected', () => {
+ const prevProps = { a: 1 };
+ // eslint-disable-next-line react/jsx-pascal-case
+ const wrapper = shallow(
);
+ expect(spy.args).to.deep.equal([
+ ['constructor'],
+ ['render'],
+ ]);
+ spy.resetHistory();
- const content = 'blah';
- const wrapper = shallow(
);
- // TODO: shallow has children, mount does not
- expect(wrapper.props()).to.deep.equal({ 'data-foo': content, children: 'Test SFC' });
- });
+ const foo = {};
+ const props = { foo };
+ const {
+ context: prevContext,
+ context: nextContext,
+ state: prevState,
+ state: nextState,
+ } = wrapper.instance();
- it('returns shallow rendered string when debug() is called', () => {
- const SFC = function SFC({ data }) {
- return (
-
Test SFC
- );
- };
+ wrapper.setProps(props);
+ const nextProps = { ...prevProps, ...props };
- const content = 'blah';
- const wrapper = shallow(
);
- expect(wrapper.debug()).to.equal((
- `
- Test SFC
-
`
- ));
+ const data = {
+ prevProps,
+ nextProps,
+ prevState,
+ nextState,
+ prevContext,
+ nextContext,
+ };
+ expect(spy.args).to.deep.equal([
+ ['UNSAFE_componentWillReceiveProps', {
+ ...data,
+ nextContext: is('>= 16') ? undefined : nextContext,
+ }],
+ ['shouldComponentUpdate', data],
+ ['componentWillUpdate', data],
+ ['render'],
+ ['componentDidUpdate', {
+ ...data,
+ prevContext: is('>= 16') ? undefined : prevContext,
+ }],
+ ]);
});
- it('works with a nested SFC', () => {
- const Bar = realArrowFunction(
Hello
);
- class Foo extends React.Component {
- render() { return
; }
- }
- const wrapper = shallow(
);
- expect(wrapper.is(Bar)).to.equal(true);
- expect(wrapper.dive().text()).to.equal('Hello');
- });
- });
+ it('calls gDSFP when expected', () => {
+ const prevProps = { a: 1 };
+ const state = { state: true };
+ const wrapper = shallow(
);
+ expect(spy.args).to.deep.equal([
+ ['constructor'],
+ ['getDerivedStateFromProps', {
+ props: prevProps,
+ state,
+ }],
+ ['render'],
+ ]);
+ spy.resetHistory();
- it('allows `.text()` to be called on text nodes', () => {
- const wrapper = shallow((
-
-
- foo bar
- {null}
- {false}
-
- ));
+ const foo = {};
+ const props = { foo };
+ const {
+ context: prevContext,
+ context: nextContext,
+ state: prevState,
+ state: nextState,
+ } = wrapper.instance();
- const stub = sinon.stub();
- wrapper.findWhere(stub);
+ wrapper.setProps(props);
+ const nextProps = { ...prevProps, ...props };
- const passedNodes = stub.getCalls().map(({ args: [firstArg] }) => firstArg);
+ const data = {
+ prevProps,
+ nextProps,
+ prevState,
+ nextState,
+ prevContext,
+ nextContext,
+ };
+ expect(spy.args).to.deep.equal([
+ ['getDerivedStateFromProps', {
+ props: nextProps,
+ state: nextState,
+ }],
+ ['shouldComponentUpdate', data],
+ ['render'],
+ ['componentDidUpdate', {
+ ...data,
+ prevContext: is('>= 16') ? undefined : prevContext,
+ }],
+ ]);
+ });
- const textContents = passedNodes.map(n => [n.debug(), n.text()]);
- const expected = [
- [wrapper.debug(), 'foo bar'], // root
- ['
', ''], // first div
- ['
\n foo bar\n
', 'foo bar'], // second div
- ['foo bar', 'foo bar'], // second div's contents
- ];
- expect(textContents).to.eql(expected);
- });
+ it('cDU’s nextState differs from `this.state` when gDSFP returns new state', () => {
+ class SimpleComponent extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { value: props.value };
+ }
- it('does not pass in null or false nodes', () => {
- const wrapper = shallow((
-
-
- foo bar
- {null}
- {false}
-
- ));
- const stub = sinon.stub();
- wrapper.findWhere(stub);
-
- const passedNodes = stub.getCalls().map(({ args: [firstArg] }) => firstArg);
- const hasElements = passedNodes.map(n => [n.debug(), n.getElement() && true]);
- const expected = [
- [wrapper.debug(), true], // root
- ['
', true], // first div
- ['
\n foo bar\n
', true], // second div
- ['foo bar', null], // second div's contents
- ];
- expect(hasElements).to.eql(expected);
-
- // the root, plus the 2 renderable children, plus the grandchild text
- expect(stub).to.have.property('callCount', 4);
- });
+ static getDerivedStateFromProps(props, state) {
+ return props.value === state.value ? null : { value: props.value };
+ }
- it('does not pass in null or false nodes', () => {
- const wrapper = shallow((
-
- ));
- const stub = sinon.stub();
- stub.returns(true);
- const spy = sinon.spy(stub);
- wrapper.findWhere(spy);
- expect(spy).to.have.property('callCount', 2);
- });
+ shouldComponentUpdate(nextProps, nextState) {
+ return nextState.value !== this.state.value;
+ }
- it('allows `.text()` to be called on text nodes', () => {
- const wrapper = shallow((
-
-
- foo bar
- {null}
- {false}
-
- ));
+ render() {
+ const { value } = this.state;
+ return (
);
+ }
+ }
+ const wrapper = shallow(
);
- const stub = sinon.stub();
- wrapper.findWhere(stub);
+ expect(wrapper.find('input').prop('value')).to.equal('initial');
- const passedNodes = stub.getCalls().map(({ args: [firstArg] }) => firstArg);
+ wrapper.setProps({ value: 'updated' });
- const textContents = passedNodes.map(n => [n.debug(), n.text()]);
- const expected = [
- [wrapper.debug(), 'foo bar'], // root
- ['
', ''], // first div
- ['
\n foo bar\n
', 'foo bar'], // second div
- ['foo bar', 'foo bar'], // second div's contents
- ];
- expect(textContents).to.eql(expected);
+ expect(wrapper.find('input').prop('value')).to.equal('updated');
+ });
});
- itIf(is('>= 16'), 'finds portals by react-is Portal type', () => {
- const containerDiv = { nodeType: 1 };
- const Foo = () => (
-
- {createPortal(
-
InPortal
,
- containerDiv,
- )}
-
- );
-
- const wrapper = shallow(
);
+ describeIf(is('>= 16'), 'componentDidCatch', () => {
+ describe('errors inside an error boundary', () => {
+ const errorToThrow = new EvalError('threw an error!');
- expect(wrapper.findWhere(node => node.type() === Portal)).to.have.lengthOf(1);
- });
- });
+ const hasFragments = is('>= 16.2');
+ const MaybeFragment = hasFragments ? Fragment : 'main';
- describe('.setProps(newProps[, callback)', () => {
- it('throws on a non-function callback', () => {
- class Foo extends React.Component {
- render() {
+ function Thrower({ throws }) {
+ if (throws) {
+ throw errorToThrow;
+ }
return null;
}
- }
- const wrapper = shallow(
);
- expect(() => wrapper.setProps({}, undefined)).to.throw();
- expect(() => wrapper.setProps({}, null)).to.throw();
- expect(() => wrapper.setProps({}, false)).to.throw();
- expect(() => wrapper.setProps({}, true)).to.throw();
- expect(() => wrapper.setProps({}, [])).to.throw();
- expect(() => wrapper.setProps({}, {})).to.throw();
- });
+ class ErrorBoundary extends React.Component {
+ constructor(...args) {
+ super(...args);
+ this.state = {
+ throws: false,
+ didThrow: false,
+ };
+ }
- it('sets props for a component multiple times', () => {
- class Foo extends React.Component {
- render() {
- return (
-
- {this.props.foo}
-
- );
- }
- }
- const wrapper = shallow(
);
- expect(wrapper.find('.foo')).to.have.lengthOf(1);
- wrapper.setProps({ id: 'bar', foo: 'bla' });
- expect(wrapper.find('.bar')).to.have.lengthOf(1);
- });
+ componentDidCatch(error, info) {
+ const { spy } = this.props;
+ spy(error, info);
+ this.setState({
+ throws: false,
+ didThrow: true,
+ });
+ }
- describe('merging props', () => {
- it('merges, not replaces, props when rerendering', () => {
- class Foo extends React.Component {
render() {
+ const {
+ didThrow,
+ throws,
+ } = this.state;
return (
-
- {this.props.foo}
+
+
+
+
+
+ {didThrow ? 'HasThrown' : 'HasNotThrown'}
+
+
+
);
}
}
- const wrapper = shallow(
);
+ describe('Thrower', () => {
+ it('does not throw when `throws` is `false`', () => {
+ expect(() => shallow(
)).not.to.throw();
+ });
- expect(wrapper.debug()).to.equal(`
-
- bar
-
- `.trim());
- expect(wrapper.props()).to.eql({
- className: 'foo',
- children: 'bar',
- });
- expect(wrapper.instance().props).to.eql({
- id: 'foo',
- foo: 'bar',
+ it('throws when `throws` is `true`', () => {
+ expect(() => shallow(
)).to.throw(errorToThrow);
+ });
});
- wrapper.setProps({ id: 'bar' });
+ it('catches a simulated error', () => {
+ const spy = sinon.spy();
+ const wrapper = shallow(
);
- expect(wrapper.debug()).to.equal(`
-
- bar
-
- `.trim());
- expect(wrapper.props()).to.eql({
- className: 'bar',
- children: 'bar',
- });
- expect(wrapper.instance().props).to.eql({
- id: 'bar',
- foo: 'bar',
- });
- });
+ expect(spy).to.have.property('callCount', 0);
- itIf(is('> 0.13'), 'merges, not replaces, props on SFCs', () => {
- function Foo({ id, foo }) {
- return (
-
- {foo}
-
- );
- }
- const wrapper = shallow(
);
+ expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
- expect(wrapper.debug()).to.equal(`
-
- bar
-
- `.trim());
- expect(wrapper.props()).to.eql({
- className: 'foo',
- children: 'bar',
- });
- if (is('< 16')) {
- expect(wrapper.instance().props).to.eql({
- id: 'foo',
- foo: 'bar',
+ expect(spy).to.have.property('callCount', 1);
+
+ expect(spy.args).to.be.an('array').and.have.lengthOf(1);
+ const [[actualError, info]] = spy.args;
+ expect(() => { throw actualError; }).to.throw(errorToThrow);
+ expect(info).to.deep.equal({
+ componentStack: `
+ in Thrower (created by ErrorBoundary)
+ in span (created by ErrorBoundary)${hasFragments ? '' : `
+ in main (created by ErrorBoundary)`}
+ in div (created by ErrorBoundary)
+ in ErrorBoundary (created by WrapperComponent)
+ in WrapperComponent`,
});
- }
+ });
+
+ it('rerenders on a simulated error', () => {
+ const wrapper = shallow(
);
- wrapper.setProps({ id: 'bar' });
+ expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0);
+ expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1);
- expect(wrapper.debug()).to.equal(`
-
- bar
-
- `.trim());
- expect(wrapper.props()).to.eql({
- className: 'bar',
- children: 'bar',
+ expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
+
+ expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(1);
+ expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(0);
});
- 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;
- }
+ it('does not catch errors during shallow render', () => {
+ const spy = sinon.spy();
+ const wrapper = shallow(
);
- render() {
- return (
-
- {this.props.foo}
-
- );
- }
- }
- const wrapper = shallow(
);
+ expect(spy).to.have.property('callCount', 0);
- expect(wrapper.debug()).to.equal(`
-
- bar
-
- `.trim());
- expect(wrapper.props()).to.eql({
- className: 'foo',
- children: 'bar',
- });
- expect(wrapper.instance().props).to.eql({
- id: 'foo',
- foo: 'bar',
- });
+ wrapper.setState({ throws: true });
- wrapper.setProps({ id: 'foo' });
+ expect(spy).to.have.property('callCount', 0);
- expect(wrapper.debug()).to.equal(`
-
- bar
-
- `.trim());
- expect(wrapper.props()).to.eql({
- className: 'foo',
- children: 'bar',
- });
- expect(wrapper.instance().props).to.eql({
- id: 'foo',
- foo: 'bar',
- });
- });
- });
+ const thrower = wrapper.find(Thrower);
+ expect(thrower).to.have.lengthOf(1);
+ expect(thrower.props()).to.have.property('throws', true);
- it('calls componentWillReceiveProps for new renders', () => {
- const stateValue = {};
+ expect(() => thrower.dive()).to.throw(errorToThrow);
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = { stateValue };
- }
+ expect(spy).to.have.property('callCount', 0);
- componentWillReceiveProps() {}
+ expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0);
+ expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1);
+ });
+ });
+ });
- UNSAFE_componentWillReceiveProps() {} // eslint-disable-line camelcase
+ describeIf(is('>= 16.6'), 'getDerivedStateFromError', () => {
+ describe('errors inside an error boundary', () => {
+ const errorToThrow = new EvalError('threw an error!');
- render() {
- const { id } = this.props;
- const { stateValue: val } = this.state;
- return (
-
- {String(val)}
-
- );
+ function Thrower({ throws }) {
+ if (throws) {
+ throw errorToThrow;
+ }
+ return null;
}
- }
- Foo.contextTypes = {
- foo() { return null; },
- };
- const cWRP = sinon.stub(Foo.prototype, 'componentWillReceiveProps');
- // eslint-disable-next-line camelcase
- const U_cWRP = sinon.stub(Foo.prototype, 'UNSAFE_componentWillReceiveProps');
- const nextProps = { id: 'bar', foo: 'bla' };
- const context = { foo: 'bar' };
- const wrapper = shallow(
, { context });
+ function getErrorBoundary() {
+ return class ErrorBoundary extends React.Component {
+ static getDerivedStateFromError() {
+ return {
+ throws: false,
+ didThrow: true,
+ };
+ }
- expect(cWRP).to.have.property('callCount', 0);
- expect(U_cWRP).to.have.property('callCount', 0);
+ constructor(props) {
+ super(props);
+ this.state = {
+ throws: false,
+ didThrow: false,
+ };
+ }
- wrapper.setProps(nextProps);
+ render() {
+ const {
+ didThrow,
+ throws,
+ } = this.state;
- expect(cWRP).to.have.property('callCount', 1);
- expect(cWRP.calledWith(nextProps, context)).to.equal(true);
+ return (
+
+
+
+
+
+ {didThrow ? 'HasThrown' : 'HasNotThrown'}
+
+
+
+
+ );
+ }
+ };
+ }
- if (is('>= 16.3')) {
- expect(U_cWRP).to.have.property('callCount', 1);
- expect(U_cWRP.calledWith(nextProps, context)).to.equal(true);
- }
- });
+ describe('Thrower', () => {
+ it('does not throw when `throws` is `false`', () => {
+ expect(() => shallow(
)).not.to.throw();
+ });
- it('merges newProps with oldProps', () => {
- class Foo extends React.Component {
- render() {
- return (
-
- );
- }
- }
- class Bar extends React.Component {
- render() {
- return (
-
- );
- }
- }
+ it('throws when `throws` is `true`', () => {
+ expect(() => shallow(
)).to.throw();
+ });
+ });
- const wrapper = shallow(
);
- expect(wrapper.props().a).to.equal('a');
- expect(wrapper.props().b).to.equal('b');
+ it('catches a simulated error', () => {
+ const ErrorBoundary = getErrorBoundary();
- wrapper.setProps({ b: 'c', d: 'e' });
- expect(wrapper.props().a).to.equal('a');
- expect(wrapper.props().b).to.equal('c');
- expect(wrapper.props().d).to.equal('e');
- });
+ const spy = sinon.spy(ErrorBoundary, 'getDerivedStateFromError');
+ const wrapper = shallow(
);
- it('passes in old context', () => {
- class Foo extends React.Component {
- render() {
- return (
-
{this.context.x}
- );
- }
- }
+ expect(spy).to.have.property('callCount', 0);
- Foo.contextTypes = { x: PropTypes.string };
+ expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
- const context = { x: 'yolo' };
- const wrapper = shallow(
, { context });
- expect(wrapper.first('div').text()).to.equal('yolo');
+ expect(spy).to.have.property('callCount', 1);
- wrapper.setProps({ x: 5 }); // Just force a re-render
- expect(wrapper.first('div').text()).to.equal('yolo');
- });
+ expect(spy.args).to.be.an('array').and.have.lengthOf(1);
+ const [[actualError]] = spy.args;
+ expect(actualError).to.equal(errorToThrow);
+ });
- it('uses defaultProps if new props includes undefined values', () => {
- const initialState = { a: 42 };
- const context = { b: 7 };
- class Foo extends React.Component {
- constructor(...args) {
- super(...args);
- this.state = initialState;
- }
+ it('rerenders on a simulated error', () => {
+ const ErrorBoundary = getErrorBoundary();
- componentWillReceiveProps() {}
+ const wrapper = shallow(
);
- render() {
- return
;
- }
- }
+ expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0);
+ expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1);
- const cWRP = sinon.stub(Foo.prototype, 'componentWillReceiveProps');
+ expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
- Foo.defaultProps = {
- className: 'default-class',
- };
- Foo.contextTypes = {
- b: PropTypes.number,
- };
+ expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(1);
+ expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(0);
+ });
- const wrapper = shallow(
, { context });
+ it('does not catch errors during shallow render', () => {
+ const ErrorBoundary = getErrorBoundary();
- // Set undefined in order to use defaultProps if any
- wrapper.setProps({ className: undefined });
+ const spy = sinon.spy(ErrorBoundary, 'getDerivedStateFromError');
+ const wrapper = shallow(
);
- expect(cWRP).to.have.property('callCount', 1);
- const [args] = cWRP.args;
- expect(args).to.eql([
- { className: Foo.defaultProps.className },
- context,
- ]);
- });
+ expect(spy).to.have.property('callCount', 0);
- it('throws if an exception occurs during render', () => {
- let error;
- class Trainwreck extends React.Component {
- render() {
- const { user } = this.props;
- try {
- return (
-
- {user.name.givenName}
-
- );
- } catch (e) {
- error = e;
- throw e;
- }
- }
- }
+ wrapper.setState({ throws: true });
- const validUser = {
- name: {
- givenName: 'Brian',
- },
- };
+ expect(spy).to.have.property('callCount', 0);
- const wrapper = shallow(
);
+ const thrower = wrapper.find(Thrower);
+ expect(thrower).to.have.lengthOf(1);
+ expect(thrower.props()).to.have.property('throws', true);
- expect(() => wrapper.setProps({ user: { name: {} } })).not.to.throw();
- expect(() => wrapper.setProps({ user: {} })).to.throw(error);
- });
+ expect(() => thrower.dive()).to.throw(errorToThrow);
- it('calls the callback when setProps has completed', () => {
- class Foo extends React.Component {
- render() {
- const { id } = this.props;
- return (
-
- {id}
-
- );
- }
- }
- const wrapper = shallow(
);
- expect(wrapper.find('.foo')).to.have.lengthOf(1);
+ expect(spy).to.have.property('callCount', 0);
- wrapper[sym('__renderer__')].batchedUpdates(() => {
- wrapper.setProps({ id: 'bar', foo: 'bla' }, () => {
- expect(wrapper.find('.bar')).to.have.lengthOf(1);
+ expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0);
+ expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1);
});
});
- expect(wrapper.find('.foo')).to.have.lengthOf(0);
});
- it('calls componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, and componentDidUpdate with merged newProps', () => {
- const spy = sinon.spy();
-
- class Foo extends React.Component {
- componentWillReceiveProps(nextProps) {
- spy('componentWillReceiveProps', this.props, nextProps);
- }
-
- shouldComponentUpdate(nextProps) {
- spy('shouldComponentUpdate', this.props, nextProps);
- return true;
- }
-
- componentWillUpdate(nextProps) {
- spy('componentWillUpdate', this.props, nextProps);
- }
+ describeIf(is('>= 16.6'), 'getDerivedStateFromError and componentDidCatch combined', () => {
- componentDidUpdate(prevProps) {
- spy('componentDidUpdate', prevProps, this.props);
- }
+ const errorToThrow = new EvalError('threw an error!');
+ const expectedInfo = {
+ componentStack: `
+ in Thrower (created by ErrorBoundary)
+ in div (created by ErrorBoundary)
+ in ErrorBoundary (created by WrapperComponent)
+ in WrapperComponent`,
+ };
- render() {
- return (
-
- );
+ function Thrower({ throws }) {
+ if (throws) {
+ throw errorToThrow;
}
+ return null;
}
- const wrapper = shallow(
);
-
- wrapper.setProps({ b: 'c', d: 'e' });
-
- expect(spy.args).to.deep.equal([
- [
- 'componentWillReceiveProps',
- { a: 'a', b: 'b' },
- { a: 'a', b: 'c', d: 'e' },
- ],
- [
- 'shouldComponentUpdate',
- { a: 'a', b: 'b' },
- { a: 'a', b: 'c', d: 'e' },
- ],
- [
- 'componentWillUpdate',
- { a: 'a', b: 'b' },
- { a: 'a', b: 'c', d: 'e' },
- ],
- [
- 'componentDidUpdate',
- { a: 'a', b: 'b' },
- { a: 'a', b: 'c', d: 'e' },
- ],
- ]);
- });
+ describe('errors inside error boundary when getDerivedStateFromProps returns update', () => {
+ let lifecycleSpy;
+ let stateSpy;
- describe('setProps does not call componentDidUpdate twice', () => {
- it('when setState is called in cWRP', () => {
- class Dummy extends React.Component {
- constructor(...args) {
- super(...args);
+ beforeEach(() => {
+ lifecycleSpy = sinon.spy();
+ stateSpy = sinon.spy();
+ });
- this.state = {
- someState: '',
+ class ErrorBoundary extends React.Component {
+ static getDerivedStateFromError(error) {
+ lifecycleSpy('getDerivedStateFromError', error);
+ return {
+ didThrow: true,
+ throws: false,
};
}
- componentWillReceiveProps({ myProp: someState }) {
- this.setState({ someState });
- }
-
- componentDidUpdate() {}
-
- render() {
- const { myProp } = this.props;
- const { someState } = this.state;
- return (
-
- myProp: {myProp}
- someState: {someState}
-
- );
- }
- }
-
- const spy = sinon.spy(Dummy.prototype, 'componentDidUpdate');
- const wrapper = shallow(
);
- expect(spy).to.have.property('callCount', 0);
- return new Promise((resolve) => {
- wrapper.setProps({ myProp: 'Prop Value' }, resolve);
- }).then(() => {
- expect(spy).to.have.property('callCount', 1);
- });
- });
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('sets props for a component multiple times', () => {
- const Foo = props => (
-
- {props.id}
-
- );
-
- const wrapper = shallow(
);
- expect(wrapper.find('.foo')).to.have.lengthOf(1);
- wrapper.setProps({ id: 'bar', foo: 'bla' });
- expect(wrapper.find('.bar')).to.have.lengthOf(1);
- });
-
- it('merges newProps with oldProps', () => {
- const Foo = props => (
-
- );
- const Bar = () => (
-
- );
-
- const wrapper = shallow(
);
- expect(wrapper.props().a).to.equal('a');
- expect(wrapper.props().b).to.equal('b');
+ constructor(props) {
+ super(props);
+ this.state = {
+ didThrow: false,
+ throws: false,
+ };
- wrapper.setProps({ b: 'c', d: 'e' });
- expect(wrapper.props().a).to.equal('a');
- expect(wrapper.props().b).to.equal('c');
- expect(wrapper.props().d).to.equal('e');
- });
+ lifecycleSpy('constructor');
+ }
- it('passes in old context', () => {
- const Foo = (props, context) => (
-
{context.x}
- );
- Foo.contextTypes = { x: PropTypes.string };
+ componentDidCatch(error, info) {
+ lifecycleSpy('componentDidCatch', error, info);
+ stateSpy({ ...this.state });
+ }
- const context = { x: 'yolo' };
- const wrapper = shallow(
, { context });
- expect(wrapper.first('div').text()).to.equal('yolo');
+ render() {
+ lifecycleSpy('render');
- wrapper.setProps({ x: 5 }); // Just force a re-render
- expect(wrapper.first('div').text()).to.equal('yolo');
- });
+ const {
+ throws,
+ } = this.state;
- it('throws if an exception occurs during render', () => {
- let error;
- const Trainwreck = ({ user }) => {
- try {
return (
- {user.name.givenName}
+
);
- } catch (e) {
- error = e;
- throw e;
}
- };
+ }
- const validUser = {
- name: {
- givenName: 'Brian',
- },
- };
+ it('does not catch errors during shallow render', () => {
+ const wrapper = shallow(
);
- const wrapper = shallow(
);
+ expect(lifecycleSpy).to.have.property('callCount', 2);
+ expect(lifecycleSpy.args).to.deep.equal([
+ ['constructor'],
+ ['render'],
+ ]);
- expect(() => wrapper.setProps({ user: { name: {} } })).not.to.throw();
- expect(() => wrapper.setProps({ user: {} })).to.throw(error);
- });
- });
- });
+ expect(stateSpy).to.have.property('callCount', 0);
- describe('.setContext(newContext)', () => {
- const SimpleComponent = createClass({
- contextTypes: {
- name: PropTypes.string,
- },
- render() {
- return
{this.context.name}
;
- },
- });
+ lifecycleSpy.resetHistory();
- it('sets context for a component multiple times', () => {
- const context = { name: 'foo' };
- const wrapper = shallow(
, { context });
- expect(wrapper.text()).to.equal('foo');
- wrapper.setContext({ name: 'bar' });
- expect(wrapper.text()).to.equal('bar');
- wrapper.setContext({ name: 'baz' });
- expect(wrapper.text()).to.equal('baz');
- });
+ wrapper.setState({ throws: true });
- it('throws if it is called when shallow didn’t include context', () => {
- const wrapper = shallow(
);
- expect(() => wrapper.setContext({ name: 'bar' })).to.throw(
- Error,
- 'ShallowWrapper::setContext() can only be called on a wrapper that was originally passed a context option', // eslint-disable-line max-len
- );
- });
+ const thrower = wrapper.find(Thrower);
+ expect(thrower).to.have.lengthOf(1);
+ expect(thrower.props()).to.have.property('throws', true);
- describeIf(is('> 0.13'), 'stateless functional components', () => {
- const SFC = (props, context) => (
-
{context.name}
- );
- SFC.contextTypes = { name: PropTypes.string };
+ expect(() => thrower.dive()).to.throw(errorToThrow);
- it('sets context for a component multiple times', () => {
- const context = { name: 'foo' };
- const wrapper = shallow(
, { context });
- expect(wrapper.text()).to.equal('foo');
- wrapper.setContext({ name: 'bar' });
- expect(wrapper.text()).to.equal('bar');
- wrapper.setContext({ name: 'baz' });
- expect(wrapper.text()).to.equal('baz');
- });
+ expect(lifecycleSpy).to.have.property('callCount', 1);
+ expect(lifecycleSpy.args).to.deep.equal([
+ ['render'],
+ ]);
+ });
- it('throws if it is called when shallow didn’t include context', () => {
- const wrapper = shallow(
);
- expect(() => wrapper.setContext({ name: 'bar' })).to.throw(
- Error,
- 'ShallowWrapper::setContext() can only be called on a wrapper that was originally passed a context option', // eslint-disable-line max-len
- );
- });
- });
- });
+ it('calls getDerivedStateFromError first and then componentDidCatch for simulated error', () => {
+ const wrapper = shallow(
);
- describe('.simulate(eventName, data)', () => {
- it('simulates events', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = { count: 0 };
- this.incrementCount = this.incrementCount.bind(this);
- }
+ expect(lifecycleSpy).to.have.property('callCount', 2);
+ expect(lifecycleSpy.args).to.deep.equal([
+ ['constructor'],
+ ['render'],
+ ]);
- incrementCount() {
- this.setState({ count: this.state.count + 1 });
- }
+ expect(stateSpy).to.have.property('callCount', 0);
- render() {
- return (
-
- foo
-
- );
- }
- }
+ lifecycleSpy.resetHistory();
- const wrapper = shallow(
);
+ expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
- expect(wrapper.find('.clicks-0')).to.have.lengthOf(1);
- wrapper.simulate('click');
- expect(wrapper.find('.clicks-1')).to.have.lengthOf(1);
- });
+ expect(lifecycleSpy).to.have.property('callCount', 3);
+ expect(lifecycleSpy.args).to.deep.equal([
+ ['getDerivedStateFromError', errorToThrow],
+ ['render'],
+ ['componentDidCatch', errorToThrow, expectedInfo],
+ ]);
+ expect(stateSpy).to.have.property('callCount', 1);
+ expect(stateSpy.args).to.deep.equal([
+ [{
+ throws: false,
+ didThrow: true,
+ }],
+ ]);
+ });
+ });
- it('passes in event data', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- render() {
- return (
-
foo
- );
- }
- }
+ describe('errors inside error boundary when getDerivedStateFromError does not return update', () => {
+ let spy;
- const wrapper = shallow(
);
- const a = {};
- const b = {};
+ beforeEach(() => {
+ spy = sinon.spy();
+ });
- wrapper.simulate('click', a, b);
- expect(spy.args[0][0]).to.equal(a);
- expect(spy.args[0][1]).to.equal(b);
- });
+ class ErrorBoundary extends React.Component {
+ static getDerivedStateFromError(error) {
+ spy('getDerivedStateFromError', error);
+ return null;
+ }
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('simulates events', () => {
- const spy = sinon.spy();
- const Foo = props => (
-
foo
- );
+ constructor(props) {
+ super(props);
+ this.state = {
+ didThrow: false,
+ throws: false,
+ };
- const wrapper = shallow(
);
+ spy('constructor');
+ }
- expect(spy).to.have.property('callCount', 0);
- wrapper.find('a').simulate('click');
- expect(spy).to.have.property('callCount', 1);
- });
+ componentDidCatch(error, info) {
+ spy('componentDidCatch', error, info);
- it('passes in event data', () => {
- const spy = sinon.spy();
- const Foo = () => (
-
foo
- );
+ this.setState({
+ didThrow: true,
+ throws: false,
+ });
+ }
- const wrapper = shallow(
);
- const a = {};
- const b = {};
+ render() {
+ spy('render');
- wrapper.simulate('click', a, b);
- expect(spy.args[0][0]).to.equal(a);
- expect(spy.args[0][1]).to.equal(b);
- });
- });
+ const {
+ didThrow,
+ throws,
+ } = this.state;
- describe('Normalizing JS event names', () => {
- it('converts lowercase events to React camelcase', () => {
- const spy = sinon.spy();
- const clickSpy = sinon.spy();
- class Foo extends React.Component {
- render() {
return (
-
foo
+
+
+
+ {didThrow ? 'HasThrown' : 'HasNotThrown'}
+
+
);
}
}
- const wrapper = shallow(
);
-
- wrapper.simulate('dblclick');
- expect(spy).to.have.property('callCount', 1);
-
- wrapper.simulate('click');
- expect(clickSpy).to.have.property('callCount', 1);
- });
+ it('does not catch errors during shallow render', () => {
+ const wrapper = shallow(
);
- describeIf(is('> 0.13'), 'normalizing mouseenter', () => {
- it('converts lowercase events to React camelcase', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- render() {
- return (
-
foo
- );
- }
- }
+ expect(spy).to.have.property('callCount', 2);
+ expect(spy.args).to.deep.equal([
+ ['constructor'],
+ ['render'],
+ ]);
- const wrapper = shallow(
);
+ spy.resetHistory();
- wrapper.simulate('mouseenter');
- expect(spy).to.have.property('callCount', 1);
- });
+ wrapper.setState({ throws: true });
- it('converts lowercase events to React camelcase in SFCs', () => {
- const spy = sinon.spy();
- const Foo = () => (
-
foo
- );
+ const thrower = wrapper.find(Thrower);
+ expect(thrower).to.have.lengthOf(1);
+ expect(thrower.props()).to.have.property('throws', true);
- const wrapper = shallow(
);
+ expect(() => thrower.dive()).to.throw(errorToThrow);
- wrapper.simulate('mouseenter');
expect(spy).to.have.property('callCount', 1);
+ expect(spy.args).to.deep.equal([
+ ['render'],
+ ]);
});
- });
- describeIf(is('>= 15'), 'animation events', () => {
- it('converts lowercase events to React camelcase', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- render() {
- return (
-
foo
- );
- }
- }
+ it('rerenders on a simulated error', () => {
+ const wrapper = shallow(
);
- const wrapper = shallow(
);
+ expect(spy).to.have.property('callCount', 2);
+ expect(spy.args).to.deep.equal([
+ ['constructor'],
+ ['render'],
+ ]);
- wrapper.simulate('animationiteration');
- expect(spy).to.have.property('callCount', 1);
- });
+ spy.resetHistory();
- it('converts lowercase events to React camelcase in stateless components', () => {
- const spy = sinon.spy();
- const Foo = () => (
-
foo
- );
+ const thrower = wrapper.find(Thrower);
- const wrapper = shallow(
);
+ expect(() => thrower.simulateError(errorToThrow)).not.to.throw(errorToThrow);
- wrapper.simulate('animationiteration');
- expect(spy).to.have.property('callCount', 1);
+ expect(spy).to.have.property('callCount', 3);
+ expect(spy.args).to.deep.equal([
+ ['getDerivedStateFromError', errorToThrow],
+ ['componentDidCatch', errorToThrow, expectedInfo],
+ ['render'],
+ ]);
});
});
+ });
- describeIf(is('>= 16.4'), 'pointer events', () => {
- it('converts lowercase events to React camelcase', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- render() {
- return (
-
foo
- );
- }
+ context('mounting phase', () => {
+ it('calls componentWillMount and componentDidMount', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ componentWillMount() {
+ spy('componentWillMount');
}
- const wrapper = shallow(
);
+ componentDidMount() {
+ spy('componentDidMount');
+ }
- wrapper.simulate('gotpointercapture');
- expect(spy).to.have.property('callCount', 1);
- });
+ render() {
+ spy('render');
+ return
foo
;
+ }
+ }
+ shallow(
);
+ expect(spy.args).to.deep.equal([
+ ['componentWillMount'],
+ ['render'],
+ ['componentDidMount'],
+ ]);
+ });
- it('converts lowercase events to React camelcase in stateless components', () => {
- const spy = sinon.spy();
- const Foo = () => (
-
foo
- );
+ itIf(BATCHING, 'is batching updates', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ count: 0,
+ };
+ }
- const wrapper = shallow(
);
+ componentWillMount() {
+ this.setState({ count: this.state.count + 1 });
+ this.setState({ count: this.state.count + 1 });
+ }
- wrapper.simulate('gotpointercapture');
- expect(spy).to.have.property('callCount', 1);
- });
+ componentDidMount() {
+ this.setState({ count: this.state.count + 1 });
+ this.setState({ count: this.state.count + 1 });
+ }
+
+ render() {
+ spy();
+ return
{this.state.count}
;
+ }
+ }
+ const result = shallow(
);
+ expect(result.state('count')).to.equal(2);
+ expect(spy).to.have.property('callCount', 2);
});
});
- itIf(BATCHING, 'has batched updates', () => {
- let renderCount = 0;
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- count: 0,
- };
- this.onClick = this.onClick.bind(this);
- }
+ context('updating props', () => {
+ it('calls shouldComponentUpdate, componentWillUpdate, and componentDidUpdate', () => {
+ const spy = sinon.spy();
- onClick() {
- this.setState({ count: this.state.count + 1 });
- this.setState({ count: this.state.count + 1 });
- }
+ class Foo extends React.Component {
+ constructor(...args) {
+ super(...args);
+ this.state = {
+ foo: 'state',
+ };
+ }
- render() {
- renderCount += 1;
- return (
-
{this.state.count}
- );
- }
- }
+ componentWillReceiveProps(nextProps, nextContext) {
+ spy('componentWillReceiveProps', this.props, nextProps, nextContext);
+ }
- const wrapper = shallow(
);
- wrapper.simulate('click');
- expect(wrapper.text()).to.equal('1');
- expect(renderCount).to.equal(2);
- });
+ shouldComponentUpdate(nextProps, nextState, nextContext) {
+ spy('shouldComponentUpdate', this.props, nextProps, this.state, nextState, nextContext);
+ return true;
+ }
- it('chains', () => {
- const wrapper = shallow(
);
- expect(wrapper.simulate('click')).to.equal(wrapper);
- });
+ componentWillUpdate(nextProps, nextState, nextContext) {
+ spy('componentWillUpdate', this.props, nextProps, this.state, nextState, nextContext);
+ }
- describe('works with .parent()/.parents()/.closest()', () => {
- function getWrapper() {
- const onClick = sinon.stub();
- const wrapper = shallow((
-
-
- click me
-
-
- ));
- return { wrapper, onClick };
- }
+ componentDidUpdate(prevProps, prevState, prevContext) {
+ spy('componentDidUpdate', prevProps, this.props, prevState, this.state, prevContext);
+ }
- it.skip('child should fire onClick', () => {
- const { wrapper, onClick } = getWrapper();
+ render() {
+ spy('render');
+ return
{this.state.foo}
;
+ }
+ }
+ Foo.contextTypes = {
+ foo: PropTypes.string,
+ };
- wrapper.find('.child-elem').simulate('click');
- expect(onClick).to.have.property('callCount', 1);
+ const wrapper = shallow(
+
,
+ {
+ context: { foo: 'context' },
+ },
+ );
+ wrapper.setProps({ foo: 'baz' });
+ wrapper.setProps({ foo: 'bax' });
+ expect(spy.args).to.deep.equal([
+ [
+ 'render',
+ ],
+ [
+ 'componentWillReceiveProps',
+ { foo: 'bar' }, { foo: 'baz' },
+ { foo: 'context' }, // this will be fixed
+ ],
+ [
+ 'shouldComponentUpdate',
+ { foo: 'bar' }, { foo: 'baz' },
+ { foo: 'state' }, { foo: 'state' },
+ { foo: 'context' },
+ ],
+ [
+ 'componentWillUpdate',
+ { foo: 'bar' }, { foo: 'baz' },
+ { foo: 'state' }, { foo: 'state' },
+ { foo: 'context' },
+ ],
+ [
+ 'render',
+ ],
+ [
+ 'componentDidUpdate',
+ { foo: 'bar' }, { foo: 'baz' },
+ { foo: 'state' }, { foo: 'state' },
+ is('>= 16') ? undefined : { foo: 'context' },
+ ],
+ [
+ 'componentWillReceiveProps',
+ { foo: 'baz' }, { foo: 'bax' },
+ { foo: 'context' },
+ ],
+ [
+ 'shouldComponentUpdate',
+ { foo: 'baz' }, { foo: 'bax' },
+ { foo: 'state' }, { foo: 'state' },
+ { foo: 'context' },
+ ],
+ [
+ 'componentWillUpdate',
+ { foo: 'baz' }, { foo: 'bax' },
+ { foo: 'state' }, { foo: 'state' },
+ { foo: 'context' },
+ ],
+ [
+ 'render',
+ ],
+ [
+ 'componentDidUpdate',
+ { foo: 'baz' }, { foo: 'bax' },
+ { foo: 'state' }, { foo: 'state' },
+ is('>= 16') ? undefined : { foo: 'context' },
+ ],
+ ]);
});
- it('parents should fire onClick', () => {
- const { wrapper, onClick } = getWrapper();
+ it('calls componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate and componentDidUpdate with merged props', () => {
+ const spy = sinon.spy();
- wrapper.find('.child-elem').parents('.parent-elem').simulate('click');
- expect(onClick).to.have.property('callCount', 1);
- });
+ class Foo extends React.Component {
+ componentWillReceiveProps(nextProps) {
+ spy('componentWillReceiveProps', this.props, nextProps);
+ }
- it('closest should fire onClick', () => {
- const { wrapper, onClick } = getWrapper();
+ shouldComponentUpdate(nextProps) {
+ spy('shouldComponentUpdate', this.props, nextProps);
+ return true;
+ }
- wrapper.find('.child-elem').closest('.parent-elem').simulate('click');
- expect(onClick).to.have.property('callCount', 1);
- });
+ componentWillUpdate(nextProps) {
+ spy('componentWillUpdate', this.props, nextProps);
+ }
- it('parent should fire onClick', () => {
- const { wrapper, onClick } = getWrapper();
+ componentDidUpdate(prevProps) {
+ spy('componentDidUpdate', prevProps, this.props);
+ }
- wrapper.find('.child-elem').parent().simulate('click');
- expect(onClick).to.have.property('callCount', 1);
- });
- });
- });
+ render() {
+ return (
+
+ );
+ }
+ }
- describe('.simulateError(error)', () => {
- class Div extends React.Component {
- render() {
- return
{this.props.children}
;
- }
- }
+ const wrapper = shallow(
);
- class Spans extends React.Component {
- render() {
- return
;
- }
- }
+ wrapper.setProps({ b: 'c', d: 'e' });
- class Nested extends React.Component {
- render() {
- return
;
- }
- }
+ expect(spy.args).to.deep.equal([
+ [
+ 'componentWillReceiveProps',
+ { a: 'a', b: 'b' },
+ { a: 'a', b: 'c', d: 'e' },
+ ],
+ [
+ 'shouldComponentUpdate',
+ { a: 'a', b: 'b' },
+ { a: 'a', b: 'c', d: 'e' },
+ ],
+ [
+ 'componentWillUpdate',
+ { a: 'a', b: 'b' },
+ { a: 'a', b: 'c', d: 'e' },
+ ],
+ [
+ 'componentDidUpdate',
+ { a: 'a', b: 'b' },
+ { a: 'a', b: 'c', d: 'e' },
+ ],
+ ]);
+ });
- it('throws on host elements', () => {
- const wrapper = shallow(
);
- expect(wrapper.is('div')).to.equal(true);
- expect(() => wrapper.simulateError()).to.throw();
- });
+ it('cancels rendering when Component returns false in shouldComponentUpdate', () => {
+ const spy = sinon.spy();
- it('throws on "not one" node', () => {
- const wrapper = shallow(
);
+ class Foo extends React.Component {
+ componentWillReceiveProps() {
+ spy('componentWillReceiveProps');
+ }
- const spans = wrapper.find('span');
- expect(spans).to.have.lengthOf(2);
- expect(() => spans.simulateError()).to.throw();
+ shouldComponentUpdate() {
+ spy('shouldComponentUpdate');
+ return false;
+ }
- const navs = wrapper.find('nav');
- expect(navs).to.have.lengthOf(0);
- expect(() => navs.simulateError()).to.throw();
- });
+ componentWillUpdate() {
+ spy('componentWillUpdate');
+ }
- it('throws when the renderer lacks `simulateError`', () => {
- const wrapper = shallow(
);
- delete wrapper[sym('__renderer__')].simulateError;
- expect(() => wrapper.simulateError()).to.throw();
- try {
- wrapper.simulateError();
- } catch (e) {
- expect(e).not.to.equal(undefined);
- }
- });
+ componentDidUpdate() {
+ spy('componentDidUpdate');
+ }
- it('calls through to renderer’s `simulateError`', () => {
- const wrapper = shallow(
);
- const stub = sinon.stub().callsFake((_, __, e) => { throw e; });
- wrapper[sym('__renderer__')].simulateError = stub;
- const error = new Error('hi');
- expect(() => wrapper.simulateError(error)).to.throw(error);
- expect(stub).to.have.property('callCount', 1);
-
- const [args] = stub.args;
- expect(args).to.have.lengthOf(3);
- const [hierarchy, rootNode, actualError] = args;
- expect(actualError).to.equal(error);
- expect(rootNode).to.eql(wrapper[sym('__root__')].getNodeInternal());
- expect(hierarchy).to.have.lengthOf(1);
- const [node] = hierarchy;
- expect(node).to.contain.keys({
- type: Div,
- nodeType: 'class',
- rendered: {
- type: Spans,
- nodeType: 'class',
- rendered: null,
- },
- });
- });
-
- it('returns the wrapper', () => {
- const wrapper = shallow(
);
- wrapper[sym('__renderer__')].simulateError = sinon.stub();
- expect(wrapper.simulateError()).to.equal(wrapper);
- });
- });
-
- describe('.setState(newState[, callback])', () => {
- it('throws on a non-function callback', () => {
- class Foo extends React.Component {
- render() {
- return null;
- }
- }
- const wrapper = shallow(
);
-
- expect(() => wrapper.setState({}, undefined)).to.throw();
- expect(() => wrapper.setState({}, null)).to.throw();
- expect(() => wrapper.setState({}, false)).to.throw();
- expect(() => wrapper.setState({}, true)).to.throw();
- expect(() => wrapper.setState({}, [])).to.throw();
- expect(() => wrapper.setState({}, {})).to.throw();
- });
-
- it('sets the state of the root node', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = { id: 'foo' };
- }
-
- render() {
- return (
-
- );
- }
- }
- const wrapper = shallow(
);
- expect(wrapper.find('.foo')).to.have.lengthOf(1);
- wrapper.setState({ id: 'bar' });
- expect(wrapper.find('.bar')).to.have.lengthOf(1);
- });
-
- it('allows setState inside of componentDidMount', () => {
- class MySharona extends React.Component {
- constructor(props) {
- super(props);
- this.state = { mounted: false };
- }
-
- componentDidMount() {
- this.setState({ mounted: true });
- }
-
- render() {
- return
{this.state.mounted ? 'a' : 'b'}
;
- }
- }
- const wrapper = shallow(
);
- expect(wrapper.find('div').text()).to.equal('a');
- });
-
- it('calls the callback when setState has completed', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = { id: 'foo' };
+ render() {
+ spy('render');
+ return
foo
;
+ }
}
- render() {
- return (
-
- );
- }
- }
- const wrapper = shallow(
);
- expect(wrapper.state()).to.eql({ id: 'foo' });
- return new Promise((resolve) => {
- wrapper.setState({ id: 'bar' }, function callback(...args) {
- expect(wrapper.state()).to.eql({ id: 'bar' });
- expect(this).to.equal(wrapper.instance());
- expect(this.state).to.eql({ id: 'bar' });
- expect(wrapper.find('div').prop('className')).to.equal('bar');
- expect(args).to.eql(CALLING_SETSTATE_CALLBACK_WITH_UNDEFINED ? [undefined] : []);
- resolve();
- });
+ const wrapper = shallow(
);
+ expect(wrapper.instance().props.foo).to.equal('bar');
+ wrapper.setProps({ foo: 'baz' });
+ expect(wrapper.instance().props.foo).to.equal('baz');
+ wrapper.setProps({ foo: 'bax' });
+ expect(wrapper.instance().props.foo).to.equal('bax');
+ expect(spy.args).to.deep.equal([
+ ['render'],
+ ['componentWillReceiveProps'],
+ ['shouldComponentUpdate'],
+ ['componentWillReceiveProps'],
+ ['shouldComponentUpdate'],
+ ]);
});
- });
-
- it('prevents the update if nextState is null or undefined', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = { id: 'foo' };
- }
-
- componentDidUpdate() {}
-
- render() {
- return (
-
- );
- }
- }
-
- const wrapper = shallow(
);
- const spy = sinon.spy(wrapper.instance(), 'componentDidUpdate');
- const callback = sinon.spy();
- wrapper.setState(() => ({ id: 'bar' }), callback);
- expect(spy).to.have.property('callCount', 1);
- expect(callback).to.have.property('callCount', 1);
-
- wrapper.setState(() => null, callback);
- expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 2);
- expect(callback).to.have.property('callCount', 2);
-
- wrapper.setState(() => undefined, callback);
- expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 3);
- expect(callback).to.have.property('callCount', 3);
- });
-
- itIf(is('>= 16'), 'prevents an infinite loop if nextState is null or undefined from setState in CDU', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = { id: 'foo' };
- }
- componentDidUpdate() {}
-
- render() {
- return (
-
- );
- }
- }
-
- let payload;
- const stub = sinon.stub(Foo.prototype, 'componentDidUpdate')
- .callsFake(function componentDidUpdate() { this.setState(() => payload); });
-
- const wrapper = shallow(
);
-
- wrapper.setState(() => ({ id: 'bar' }));
- expect(stub).to.have.property('callCount', 1);
-
- payload = null;
- wrapper.setState(() => ({ id: 'bar' }));
- expect(stub).to.have.property('callCount', 2);
- });
-
- describe('does not call componentWillReceiveProps after setState is called', () => {
- it('does not call componentWillReceiveProps upon rerender', () => {
- class A extends React.Component {
+ itIf(BATCHING, 'does not provoke another renders to call setState in componentWillReceiveProps', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
constructor(props) {
super(props);
-
- this.state = { a: 0 };
+ this.state = {
+ count: 0,
+ };
}
componentWillReceiveProps() {
- this.setState({ a: 1 });
+ this.setState({ count: this.state.count + 1 });
+ this.setState({ count: this.state.count + 1 });
}
render() {
- return (
{this.state.a}
);
+ spy();
+ return
{this.props.foo}
;
}
}
- const spy = sinon.spy(A.prototype, 'componentWillReceiveProps');
-
- const wrapper = shallow(
, { disableLifecycleMethods: true });
-
- wrapper.setState({ a: 2 });
- expect(wrapper.state('a')).to.equal(2);
-
- expect(spy).to.have.property('callCount', 0);
- wrapper.setProps({});
+ const result = shallow(
);
expect(spy).to.have.property('callCount', 1);
- expect(wrapper.state('a')).to.equal(1);
-
- return new Promise((resolve) => {
- wrapper.setState({ a: 3 }, resolve);
- }).then(() => {
- expect(spy).to.have.property('callCount', 1);
- expect(wrapper.state('a')).to.equal(3);
- });
+ result.setProps({ name: 'bar' });
+ expect(spy).to.have.property('callCount', 2);
+ expect(result.state('count')).to.equal(1);
});
- it('does not call componentWillReceiveProps with multiple keys in props', () => {
- class B extends React.Component {
+ itIf(BATCHING, 'provokes an another render to call setState twice in componentWillUpdate', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
constructor(props) {
super(props);
- this.state = { a: 0, b: 1 };
+ this.updated = false;
+ this.state = {
+ count: 0,
+ };
}
- componentWillReceiveProps() {
- this.setState({ b: 0, a: 1 });
+ componentWillUpdate() {
+ if (!this.updated) {
+ this.updated = true;
+ this.setState({ count: this.state.count + 1 });
+ this.setState({ count: this.state.count + 1 });
+ }
}
render() {
- return (
-
- {this.state.a + this.state.b}
-
- );
+ spy();
+ return
{this.props.foo}
;
}
}
- const spy = sinon.spy(B.prototype, 'componentWillReceiveProps');
-
- const wrapper = shallow(
, { disableLifecycleMethods: true });
-
- wrapper.setState({ a: 2 });
- expect(wrapper.state('a')).to.equal(2);
- expect(wrapper.state('b')).to.equal(1);
-
- expect(spy).to.have.property('callCount', 0);
- wrapper.setProps({});
+ const result = shallow(
);
expect(spy).to.have.property('callCount', 1);
- expect(wrapper.state('a')).to.equal(1);
-
- return Promise.all([
- new Promise((resolve) => { wrapper.setState({ b: 5 }, resolve); }),
- new Promise((resolve) => { wrapper.setState({ a: 10 }, resolve); }),
- ]).then(() => {
- expect(spy).to.have.property('callCount', 1);
- expect(wrapper.state('b')).to.equal(5);
- expect(wrapper.state('a')).to.equal(10);
- });
- });
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('throws when trying to access state', () => {
- const Foo = () => (
-
abc
- );
-
- const wrapper = shallow(
);
-
- expect(() => wrapper.state()).to.throw(
- Error,
- 'ShallowWrapper::state() can only be called on class components',
- );
+ result.setProps({ name: 'bar' });
+ expect(spy).to.have.property('callCount', 3);
+ expect(result.state('count')).to.equal(1);
});
- it('throws when trying to set state', () => {
- const Foo = () => (
-
abc
- );
+ itIf(BATCHING, 'provokes an another render to call setState twice in componentDidUpdate', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.updated = false;
+ this.state = {
+ count: 0,
+ };
+ }
- const wrapper = shallow(
);
+ componentDidUpdate() {
+ if (!this.updated) {
+ this.updated = true;
+ /* eslint-disable react/no-did-update-set-state */
+ this.setState({ count: this.state.count + 1 });
+ this.setState({ count: this.state.count + 1 });
+ /* eslint-enable react/no-did-update-set-state */
+ }
+ }
- expect(() => wrapper.setState({ a: 1 })).to.throw(
- Error,
- 'ShallowWrapper::setState() can only be called on class components',
- );
+ render() {
+ spy();
+ return
{this.props.foo}
;
+ }
+ }
+ const result = shallow(
);
+ expect(spy).to.have.property('callCount', 1);
+ result.setProps({ name: 'bar' });
+ expect(spy).to.have.property('callCount', 3);
+ expect(result.state('count')).to.equal(1);
});
});
- it('throws an error when cb is not a function', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = { id: 'foo' };
- }
-
- setBadState() {
- this.setState({}, 1);
- }
+ context('updating state', () => {
+ it('calls shouldComponentUpdate, componentWillUpdate and componentDidUpdate', () => {
+ const spy = sinon.spy();
- render() {
- return (
-
- );
- }
- }
- const wrapper = shallow(
);
- expect(wrapper.state()).to.eql({ id: 'foo' });
- expect(() => wrapper.setState({ id: 'bar' }, 1)).to.throw(Error);
- expect(() => wrapper.instance().setBadState()).to.throw(Error);
- });
+ class Foo extends React.Component {
+ constructor(...args) {
+ super(...args);
+ this.state = {
+ foo: 'bar',
+ };
+ }
- it('does not throw with a null/undefined callback', () => {
- class Foo extends React.Component {
- constructor() {
- super();
+ shouldComponentUpdate(nextProps, nextState, nextContext) {
+ spy('shouldComponentUpdate', this.props, nextProps, this.state, nextState, nextContext);
+ return true;
+ }
- this.state = {};
- }
+ componentWillUpdate(nextProps, nextState, nextContext) {
+ spy('componentWillUpdate', this.props, nextProps, this.state, nextState, nextContext);
+ }
- setStateWithNullishCallback() {
- this.setState({}, null);
- this.setState({}, undefined);
- }
+ componentDidUpdate(prevProps, prevState, prevContext) {
+ spy('componentDidUpdate', prevProps, this.props, prevState, this.state, prevContext);
+ }
- render() {
- return null;
+ render() {
+ spy('render');
+ return
{this.state.foo}
;
+ }
}
- }
+ Foo.contextTypes = {
+ foo: PropTypes.string,
+ };
- const wrapper = shallow(
);
- expect(() => wrapper.instance().setStateWithNullishCallback()).not.to.throw();
- });
-
- it('preserves the receiver', () => {
- class Comp extends React.Component {
- constructor(...args) {
- super(...args);
-
- this.state = {
- key: '',
- };
-
- this.instanceFunction = () => this.setState(() => ({ key: 'value' }));
- }
-
- componentDidMount() {
- this.instanceFunction();
- }
-
- render() {
- const { key } = this.state;
- return key ? null : null;
- }
- }
-
- expect(shallow(
).debug()).to.equal('');
- });
-
- describe('child components', () => {
- class Child extends React.Component {
- constructor(...args) {
- super(...args);
- this.state = { state: 'a' };
- }
-
- render() {
- const { prop } = this.props;
- const { state } = this.state;
- return (
-
- {prop} - {state}
-
- );
- }
- }
-
- class Parent extends React.Component {
- constructor(...args) {
- super(...args);
- this.state = { childProp: 1 };
- }
-
- render() {
- const { childProp } = this.state;
- return
;
- }
- }
-
- it('sets the state of a stateful root', () => {
- const wrapper = shallow(
);
-
- expect(wrapper.debug()).to.equal('
');
-
- return new Promise((resolve) => {
- wrapper.setState({ childProp: 2 }, () => {
- expect(wrapper.debug()).to.equal('
');
- resolve();
- });
- });
- });
-
- it('can not set the state of the stateful child of a stateful root', () => {
- const wrapper = shallow(
);
-
- expect(wrapper.debug()).to.equal('
');
-
- const child = wrapper.find(Child);
- expect(() => child.setState({ state: 'b' })).to.throw(
- Error,
- 'ShallowWrapper::setState() can only be called on the root',
- );
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- function SFC(props) {
- return
;
- }
-
- it('can not set the state of the stateful child of a stateless root', () => {
- const wrapper = shallow(
);
-
- expect(wrapper.text().trim()).to.equal('
');
-
- const child = wrapper.find(Child);
- expect(() => child.setState({ state: 'b' })).to.throw(
- Error,
- 'ShallowWrapper::setState() can only be called on the root',
- );
- });
- });
- });
- });
-
- describe('.is(selector)', () => {
- it('returns true when selector matches current element', () => {
- const wrapper = shallow(
);
- expect(wrapper.is('.foo')).to.equal(true);
- });
-
- it('allows for compound selectors', () => {
- const wrapper = shallow(
);
- expect(wrapper.is('.foo.bar')).to.equal(true);
- });
-
- it('ignores insignificant whitespace', () => {
- const className = `foo
- `;
- const wrapper = shallow(
);
- expect(wrapper.is('.foo')).to.equal(true);
- });
-
- it('handles all significant whitespace', () => {
- const className = `foo
-
- bar
- baz`;
- const wrapper = shallow(
);
- expect(wrapper.is('.foo.bar.baz')).to.equal(true);
- });
-
- it('returns false when selector does not match', () => {
- const wrapper = shallow(
);
- expect(wrapper.is('.foo')).to.equal(false);
- });
- });
-
- describe('.isEmptyRender()', () => {
- const emptyRenderValues = generateEmptyRenderData();
-
- itWithData(emptyRenderValues, 'when a React class component returns: ', (data) => {
- const Foo = createClass({
- render() {
- return data.value;
- },
- });
- const wrapper = shallow(
);
- expect(wrapper.isEmptyRender()).to.equal(data.expectResponse);
- });
-
- itWithData(emptyRenderValues, 'when an ES2015 class component returns: ', (data) => {
- class Foo extends React.Component {
- render() {
- return data.value;
- }
- }
- const wrapper = shallow(
);
- expect(wrapper.isEmptyRender()).to.equal(data.expectResponse);
- });
-
- describe('nested nodes', () => {
- class RenderChildren extends React.Component {
- render() {
- return this.props.children;
- }
- }
-
- class RenderNull extends React.Component {
- render() {
- return null;
- }
- }
-
- it('returns false for nested elements that return null', () => {
- const wrapper = shallow((
-
-
-
- ));
-
- expect(wrapper.isEmptyRender()).to.equal(false);
- });
-
- it('returns false for multiple nested elements that all return null', () => {
- const wrapper = shallow((
-
-
-
- ));
-
- expect(wrapper.isEmptyRender()).to.equal(false);
- });
-
- it('returns false for multiple nested elements where one fringe returns a non null value', () => {
- const wrapper = shallow((
-
- Hello
-
- ));
-
- expect(wrapper.isEmptyRender()).to.equal(false);
- });
-
- itIf(is('>= 16'), 'returns false for multiple nested elements that all return null', () => {
- const wrapper = shallow((
-
-
-
-
-
-
-
- ));
-
- expect(wrapper.isEmptyRender()).to.equal(false);
- });
-
- itIf(is('>= 16'), 'returns false for multiple nested elements where one fringe returns a non null value', () => {
- const wrapper = shallow((
-
-
-
-
-
-
-
-
-
-
-
-
- Hello
-
-
-
- ));
-
- expect(wrapper.isEmptyRender()).to.equal(false);
- });
-
- itIf(is('>= 16'), 'returns false for multiple nested elements where all values are null', () => {
- const wrapper = shallow((
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ));
-
- expect(wrapper.isEmptyRender()).to.equal(false);
- });
- });
-
- it('does not return true for HTML elements', () => {
- const wrapper = shallow(
);
- expect(wrapper.isEmptyRender()).to.equal(false);
- });
-
- describeIf(is('>=15 || ^16.0.0-alpha'), 'stateless function components (SFCs)', () => {
- itWithData(emptyRenderValues, 'when a component returns: ', (data) => {
- function Foo() {
- return data.value;
- }
- const wrapper = shallow(
);
- expect(wrapper.isEmptyRender()).to.equal(data.expectResponse);
- });
- });
- });
-
- describe('.not(selector)', () => {
- it('filters to things not matching a selector', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.find('.foo').not('.bar')).to.have.lengthOf(1);
- expect(wrapper.find('.baz').not('.foo')).to.have.lengthOf(2);
- expect(wrapper.find('.foo').not('div')).to.have.lengthOf(0);
- });
- });
-
- describe('.filter(selector)', () => {
- it('returns a new wrapper of just the nodes that matched the selector', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.find('.foo').filter('.bar')).to.have.lengthOf(3);
- expect(wrapper.find('.bar').filter('.foo')).to.have.lengthOf(3);
- expect(wrapper.find('.bar').filter('.bax')).to.have.lengthOf(0);
- expect(wrapper.find('.foo').filter('.baz.bar')).to.have.lengthOf(2);
- });
-
- it('only looks in the current wrappers nodes, not their children', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.find('.foo').filter('.bar')).to.have.lengthOf(1);
- });
- });
-
- describe('.filterWhere(predicate)', () => {
- it('filters only the nodes of the wrapper', () => {
- const wrapper = shallow((
-
- ));
-
- const stub = sinon.stub();
- stub.onCall(0).returns(false);
- stub.onCall(1).returns(true);
- stub.onCall(2).returns(false);
-
- const baz = wrapper.find('.foo').filterWhere(stub);
- expect(baz).to.have.lengthOf(1);
- expect(baz.hasClass('baz')).to.equal(true);
- });
-
- it('calls the predicate with the wrapped node as the first argument', () => {
- const wrapper = shallow((
-
- ));
-
- const stub = sinon.stub();
- stub.returns(true);
- const spy = sinon.spy(stub);
- wrapper.find('.foo').filterWhere(spy);
- expect(spy).to.have.property('callCount', 3);
- expect(spy.args[0][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[1][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[2][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[0][0].hasClass('bar')).to.equal(true);
- expect(spy.args[1][0].hasClass('baz')).to.equal(true);
- expect(spy.args[2][0].hasClass('bux')).to.equal(true);
- });
- });
-
- describe('.text()', () => {
- const matchesRender = function matchesRender(node) {
- const actual = shallow(node).text();
- const expected = render(node).text();
- expect(expected).to.equal(actual);
- };
-
- it('handles simple text nodes', () => {
- const wrapper = shallow((
-
some text
- ));
- expect(wrapper.text()).to.equal('some text');
- });
-
- it('handles nodes with mapped children', () => {
- class Foo extends React.Component {
- render() {
- return (
-
- {this.props.items.map(x => x)}
-
- );
- }
- }
- matchesRender(
);
- matchesRender((
-
abc,
- def ,
- hij ,
- ]}
- />
- ));
- });
-
- it('renders composite components dumbly', () => {
- class Foo extends React.Component {
- render() { return
; }
- }
- const wrapper = shallow((
-
- ));
- expect(wrapper.text()).to.equal(' test');
- });
-
- it('handles html entities', () => {
- matchesRender(>
);
- });
-
- it('handles spaces the same between shallow and mount', () => {
- const Space = (
-
-
test
-
Hello
-
-
- World
-
-
Hello World
-
Hello
- World
-
-
Hello World
-
-
-
- );
-
- const wrapper = shallow(Space);
-
- expect(wrapper.text()).to.equal(' test Hello WorldHello WorldHello WorldHello World ');
- });
-
- it('handles non-breaking spaces correctly', () => {
- class Foo extends React.Component {
- render() {
- return (
-
-
-
- );
- }
- }
- const wrapper = shallow( );
- const charCodes = wrapper.text().split('').map(x => x.charCodeAt(0));
- expect(charCodes).to.eql([
- 0x00a0, // non-breaking space
- 0x20, // normal space
- 0x00a0, // non-breaking space
- ]);
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('handles nodes with mapped children', () => {
- const Foo = props => (
-
- {props.items.map(x => x)}
-
- );
- matchesRender( );
- matchesRender((
- abc,
- def ,
- hij ,
- ]}
- />
- ));
- });
-
- it('renders composite components dumbly', () => {
- const Foo = () => (
- foo
- );
-
- const wrapper = shallow((
-
- ));
- expect(wrapper.text()).to.equal(' test');
- });
- });
-
- it('renders falsy numbers', () => {
- [0, -0, '0', NaN].forEach((x) => {
- const wrapper = shallow({x}
);
- expect(wrapper.text()).to.equal(String(x));
- });
- });
-
- describe('text content with curly braces', () => {
- it('handles literal strings', () => {
- const wrapper = shallow();
- expect(wrapper.text()).to.equal('{}');
- });
-
- it.skip('handles innerHTML', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.text()).to.equal('{ some text }');
- });
- });
-
- describeIf(is('> 16.2'), 'fragments', () => {
- class FragmentClassExample extends React.Component {
- render() {
- return (
-
- Foo
- Bar
-
- );
- }
- }
-
- const FragmentConstExample = () => (
-
- Foo
- Bar
-
- );
-
- it('correctly gets text for both children for class', () => {
- const classWrapper = shallow( );
- expect(classWrapper.text()).to.include('Foo');
- expect(classWrapper.text()).to.include('Bar');
- });
-
- it('correctly gets text for both children for const', () => {
- const constWrapper = shallow( );
- expect(constWrapper.text()).to.include('Foo');
- expect(constWrapper.text()).to.include('Bar');
- });
-
- it('works with a nested component', () => {
- const Title = ({ children }) => {children} ;
- const Foobar = () => (
-
- Foo
- Bar
-
- );
-
- const wrapper = shallow( );
- const text = wrapper.text();
- expect(wrapper.debug()).to.equal(`
-
- Foo
-
- Bar
- `);
- expect(text).to.equal(' Bar');
- });
- });
- });
-
- describe('.props()', () => {
- it('returns the props object', () => {
- const fn = () => ({});
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.props().className).to.equal('bax');
- expect(wrapper.props().onClick).to.equal(fn);
- expect(wrapper.props().id).to.equal('fooId');
-
- });
-
- it('is allowed to be used on an inner node', () => {
- const fn = () => ({});
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.find('.baz').props().onClick).to.equal(fn);
- expect(wrapper.find('.foo').props().id).to.equal('fooId');
- });
-
- it('returns props of root rendered node', () => {
- class Foo extends React.Component {
- render() {
- return (
-
- );
- }
- }
-
- const wrapper = shallow( );
-
- expect(wrapper.props()).to.eql({ className: 'bye', id: 'hi' });
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('returns props of root rendered node', () => {
- const Foo = ({ bar, foo }) => (
-
- );
-
- const wrapper = shallow( );
-
- expect(wrapper.props()).to.eql({ className: 'bye', id: 'hi' });
- });
-
- const SloppyReceiver = sloppyReturnThis((
- receiver,
- props = { NO_PROPS: true },
- context,
- ) => (
-
- ));
-
- const StrictReceiver = function SFC(
- props = { NO_PROPS: true },
- context,
- ) {
- return (
-
- );
- };
-
- it('does not provide a `this` to a sloppy-mode SFC', () => {
- const wrapper = shallow( );
- expect(wrapper.props()).to.be.an('object').that.has.all.keys({
- 'data-is-global': true,
- 'data-is-undefined': false,
- });
- });
-
- it('does not provide a `this` to a strict-mode SFC', () => {
- const wrapper = shallow( );
- expect(wrapper.props()).to.be.an('object').that.has.all.keys({
- 'data-is-global': false,
- 'data-is-undefined': true,
- });
- });
- });
- });
-
- describe('.prop(name)', () => {
- it('returns the props of key `name`', () => {
- const fn = () => ({});
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.prop('className')).to.equal('bax');
- expect(wrapper.prop('onClick')).to.equal(fn);
- expect(wrapper.prop('id')).to.equal('fooId');
-
- });
-
- it('is allowed to be used on an inner node', () => {
- const fn = () => ({});
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.find('.baz').prop('onClick')).to.equal(fn);
- expect(wrapper.find('.foo').prop('id')).to.equal('fooId');
- });
-
- it('returns props of root rendered node', () => {
- class Foo extends React.Component {
- render() {
- return (
-
- );
- }
- }
-
- const wrapper = shallow( );
-
- expect(wrapper.prop('className')).to.equal('bye');
- expect(wrapper.prop('id')).to.equal('hi');
- expect(wrapper.prop('foo')).to.equal(undefined);
- expect(wrapper.prop('bar')).to.equal(undefined);
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('returns props of root rendered node', () => {
- const Foo = ({ bar, foo }) => (
-
- );
-
- const wrapper = shallow( );
-
- expect(wrapper.prop('className')).to.equal('bye');
- expect(wrapper.prop('id')).to.equal('hi');
- expect(wrapper.prop('foo')).to.equal(undefined);
- expect(wrapper.prop('bar')).to.equal(undefined);
- });
- });
- });
-
- describe('.state([name])', () => {
- it('returns the state object', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = { foo: 'foo' };
- }
-
- render() { return {this.state.foo}
; }
- }
- const wrapper = shallow( );
- expect(wrapper.state()).to.eql({ foo: 'foo' });
- });
-
- it('returns the current state after state transitions', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = { foo: 'foo' };
- }
-
- render() { return {this.state.foo}
; }
- }
- const wrapper = shallow( );
- wrapper.setState({ foo: 'bar' });
- expect(wrapper.state()).to.eql({ foo: 'bar' });
- });
-
- it('allows a state property name be passed in as an argument', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = { foo: 'foo' };
- }
-
- render() { return {this.state.foo}
; }
- }
- const wrapper = shallow( );
- expect(wrapper.state('foo')).to.equal('foo');
- });
-
- it('throws on host nodes', () => {
- const wrapper = shallow(
);
-
- expect(() => wrapper.state()).to.throw(Error, 'ShallowWrapper::state() can only be called on class components');
- });
-
- itIf(is('>= 16'), 'throws on Portals', () => {
- const containerDiv = { nodeType: 1 };
- const portal = createPortal(
-
,
- containerDiv,
- );
-
- const wrapper = shallow({portal}
);
- expect(() => wrapper.state()).to.throw(Error, 'ShallowWrapper::state() can only be called on class components');
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('throws on SFCs', () => {
- function Foo() {
- return
;
- }
-
- const wrapper = shallow( );
- expect(() => wrapper.state()).to.throw(Error, 'ShallowWrapper::state() can only be called on class components');
- });
- });
-
- describe('child components', () => {
- class Child extends React.Component {
- constructor(...args) {
- super(...args);
- this.state = { state: 'a' };
- }
-
- render() {
- const { prop } = this.props;
- const { state } = this.state;
- return (
-
- {prop} - {state}
-
- );
- }
- }
-
- class Parent extends React.Component {
- constructor(...args) {
- super(...args);
- this.state = { childProp: 1 };
- }
-
- render() {
- const { childProp } = this.state;
- return ;
- }
- }
-
- it('gets the state of a stateful parent', () => {
- const wrapper = shallow( );
-
- expect(wrapper.state()).to.eql({ childProp: 1 });
- });
-
- it('can not get the state of the stateful child of a stateful root', () => {
- const wrapper = shallow( );
-
- const child = wrapper.find(Child);
- expect(() => child.state()).to.throw(Error, 'ShallowWrapper::state() can only be called on the root');
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- function StatelessParent(props) {
- return ;
- }
-
- it('can not get the state of the stateful child of a stateless root', () => {
- const wrapper = shallow( );
-
- const child = wrapper.find(Child);
- expect(() => child.state()).to.throw(Error, 'ShallowWrapper::state() can only be called on the root');
- });
- });
- });
- });
-
- describe('.children([selector])', () => {
- it('returns empty wrapper for node with no children', () => {
- const wrapper = shallow(
);
- expect(wrapper.children()).to.have.lengthOf(0);
- });
-
- it('skips the falsy children', () => {
- const wrapper = shallow((
-
-
- {false}
- {[false, false]}
-
foo
-
-
- {undefined}
- {[undefined, undefined]}
-
bar
-
-
- {null}
- {[null, null]}
-
baz
-
-
- ));
- expect(wrapper.childAt(0).children()).to.have.lengthOf(1);
- expect(wrapper.childAt(1).children()).to.have.lengthOf(1);
- expect(wrapper.childAt(2).children()).to.have.lengthOf(1);
- });
-
- it('returns the children nodes of the root', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.children()).to.have.lengthOf(3);
- expect(wrapper.children().at(0).hasClass('foo')).to.equal(true);
- expect(wrapper.children().at(1).hasClass('bar')).to.equal(true);
- expect(wrapper.children().at(2).hasClass('baz')).to.equal(true);
- });
-
- it('does not return any of the children of children', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.children()).to.have.lengthOf(2);
- expect(wrapper.children().at(0).hasClass('foo')).to.equal(true);
- expect(wrapper.children().at(1).hasClass('baz')).to.equal(true);
- });
-
- it('handles mixed children with and without arrays', () => {
- class Foo extends React.Component {
- render() {
- return (
-
-
- {this.props.items.map(x => x)}
-
- );
- }
- }
- const wrapper = shallow((
- abc,
- def ,
- ]}
- />
- ));
- expect(wrapper.children()).to.have.lengthOf(3);
- expect(wrapper.children().at(0).hasClass('foo')).to.equal(true);
- expect(wrapper.children().at(1).hasClass('bar')).to.equal(true);
- expect(wrapper.children().at(2).hasClass('baz')).to.equal(true);
- });
-
- it('optionally allows a selector to filter by', () => {
- const wrapper = shallow((
-
- ));
- const children = wrapper.children('.bip');
- expect(children).to.have.lengthOf(2);
- expect(children.at(0).hasClass('bar')).to.equal(true);
- expect(children.at(1).hasClass('baz')).to.equal(true);
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('handles mixed children with and without arrays', () => {
- const Foo = props => (
-
-
- {props.items.map(x => x)}
-
- );
-
- const wrapper = shallow((
- abc,
- def ,
- ]}
- />
- ));
- expect(wrapper.children()).to.have.lengthOf(3);
- expect(wrapper.children().at(0).hasClass('foo')).to.equal(true);
- expect(wrapper.children().at(1).hasClass('bar')).to.equal(true);
- expect(wrapper.children().at(2).hasClass('baz')).to.equal(true);
- });
- });
-
- it('returns duplicates untouched', () => {
- class Foo extends React.Component {
- render() {
- const foo = 'Foo';
- return (
-
- {foo} Bar {foo} Bar {foo}
-
- );
- }
- }
-
- const wrapper = shallow( );
- const children = wrapper.children();
- const textNodes = children.map(x => x.text());
- expect(textNodes).to.eql(['Foo', ' Bar ', 'Foo', ' Bar ', 'Foo']);
- });
-
- it('renders children separated by spaces', () => {
- class JustificationRow extends React.Component {
- render() {
- const { children } = this.props;
- const wrappedChildren = React.Children.map(
- children,
- child => child && {child} ,
- );
-
- const justifiedChildren = [];
- React.Children.forEach(wrappedChildren, (child) => {
- if (child) {
- justifiedChildren.push(child, ' ');
- }
- });
- justifiedChildren.pop();
-
- return {justifiedChildren}
;
- }
- }
-
- const wrapper = shallow((
-
- foo
- bar
- baz
-
- ));
-
- expect(wrapper.children().map(x => x.debug())).to.eql([
- `
-
- foo
-
- `,
- ' ',
- `
-
- bar
-
- `,
- ' ',
- `
-
- baz
-
- `,
- ]);
- });
- });
-
- describe('.childAt(index)', () => {
- it('gets a wrapped node at the specified index', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.childAt(0).hasClass('bar')).to.equal(true);
- expect(wrapper.childAt(1).hasClass('baz')).to.equal(true);
- });
- });
-
- describe('.parents([selector])', () => {
- it('returns an array of current node’s ancestors', () => {
- const wrapper = shallow((
-
- ));
-
- const parents = wrapper.find('.baz').parents();
-
- expect(parents).to.have.lengthOf(3);
- expect(parents.at(0).hasClass('bar')).to.equal(true);
- expect(parents.at(1).hasClass('foo')).to.equal(true);
- expect(parents.at(2).hasClass('bax')).to.equal(true);
- });
-
- it('works for non-leaf nodes as well', () => {
- const wrapper = shallow((
-
- ));
-
- const parents = wrapper.find('.bar').parents();
-
- expect(parents).to.have.lengthOf(2);
- expect(parents.at(0).hasClass('foo')).to.equal(true);
- expect(parents.at(1).hasClass('bax')).to.equal(true);
- });
-
- it('optionally allows a selector', () => {
- const wrapper = shallow((
-
- ));
-
- const parents = wrapper.find('.baz').parents('.foo');
-
- expect(parents).to.have.lengthOf(2);
- expect(parents.at(0).hasClass('foo')).to.equal(true);
- expect(parents.at(1).hasClass('bax')).to.equal(true);
- });
-
- it('works when called sequentially on two sibling nodes', () => {
- class Test extends React.Component {
- render() {
- return (
-
- );
- }
- }
-
- const wrapper = shallow( );
-
- const aChild = wrapper.find({ children: 'A child' });
- expect(aChild.debug()).to.equal(`
- A child
-
`);
- expect(aChild).to.have.lengthOf(1);
-
- const bChild = wrapper.find({ children: 'B child' });
- expect(bChild.debug()).to.equal(`
- B child
-
`);
- expect(bChild).to.have.lengthOf(1);
-
- const bChildParents = bChild.parents('.b');
- expect(bChildParents.debug()).to.equal(``);
- expect(bChildParents).to.have.lengthOf(1);
-
- const aChildParents = aChild.parents('.a');
- expect(aChildParents.debug()).to.equal(``);
- expect(aChildParents).to.have.lengthOf(1);
- });
- });
-
- describe('.parent()', () => {
- it('returns only the immediate parent of the node', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.baz').parent().hasClass('bar')).to.equal(true);
- });
-
- it('works when the sibling node has children', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.find('.baz').parent().hasClass('bar')).to.equal(true);
- });
-
- it('works for multiple nodes', () => {
- const wrapper = shallow((
-
- ));
-
- const parents = wrapper.find('.baz').parent();
- expect(parents).to.have.lengthOf(3);
- expect(parents.at(0).hasClass('foo')).to.equal(true);
- expect(parents.at(1).hasClass('bar')).to.equal(true);
- expect(parents.at(2).hasClass('bax')).to.equal(true);
- });
-
- it('works with component', () => {
- const Foo = createClass({
- render() {
- return
;
- },
- });
- const wrapper = shallow( );
- expect(wrapper.find('.bar')).to.have.lengthOf(1);
- expect(wrapper.find('.bar').parent()).to.have.lengthOf(0);
- });
- });
-
- describe('.closest(selector)', () => {
- it('returns the closest ancestor for a given selector', () => {
- const wrapper = shallow((
-
- ));
-
- const closestFoo = wrapper.find('.bar').closest('.foo');
- expect(closestFoo).to.have.lengthOf(1);
- expect(closestFoo.hasClass('baz')).to.equal(true);
- });
-
- it('only ever returns a wrapper of a single node', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.find('.baz').parent().hasClass('bar')).to.equal(true);
- });
-
- it('returns itself if matching', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.find('.bux').closest('.baz').hasClass('bux')).to.equal(true);
- });
-
- it('does not find a nonexistent match', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.find('.fooooo')).to.have.lengthOf(0);
-
- const bar = wrapper.find('.bar');
- expect(bar).to.have.lengthOf(1);
-
- expect(bar.closest('.fooooo')).to.have.lengthOf(0);
- });
- });
-
- describe('.hasClass(className)', () => {
- context('when using a DOM component', () => {
- it('returns whether or not node has a certain class', () => {
- const wrapper = shallow(
);
-
- expect(wrapper.hasClass('foo')).to.equal(true);
- expect(wrapper.hasClass('bar')).to.equal(true);
- expect(wrapper.hasClass('baz')).to.equal(true);
- expect(wrapper.hasClass('some-long-string')).to.equal(true);
- expect(wrapper.hasClass('FoOo')).to.equal(true);
- expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
- });
- });
-
- describeIf(is('> 0.13'), 'with stateless function components (SFCs)', () => {
- it('returns whether or not node has a certain class', () => {
- const Foo = () =>
;
- const wrapper = shallow( );
-
- expect(wrapper.hasClass('foo')).to.equal(false);
- expect(wrapper.hasClass('bar')).to.equal(false);
- expect(wrapper.hasClass('baz')).to.equal(false);
- expect(wrapper.hasClass('some-long-string')).to.equal(false);
- expect(wrapper.hasClass('FoOo')).to.equal(false);
- expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
-
- expect(wrapper.children().hasClass('foo')).to.equal(true);
- expect(wrapper.children().hasClass('bar')).to.equal(true);
- expect(wrapper.children().hasClass('baz')).to.equal(true);
- expect(wrapper.children().hasClass('some-long-string')).to.equal(true);
- expect(wrapper.children().hasClass('FoOo')).to.equal(true);
- expect(wrapper.children().hasClass('doesnt-exist')).to.equal(false);
- });
- });
-
- context('when using a Composite class component', () => {
- it('returns whether or not node has a certain class', () => {
- class Foo extends React.Component {
- render() {
- return (
);
- }
- }
- const wrapper = shallow( );
-
- expect(wrapper.hasClass('foo')).to.equal(false);
- expect(wrapper.hasClass('bar')).to.equal(false);
- expect(wrapper.hasClass('baz')).to.equal(false);
- expect(wrapper.hasClass('some-long-string')).to.equal(false);
- expect(wrapper.hasClass('FoOo')).to.equal(false);
- expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
-
- expect(wrapper.children().hasClass('foo')).to.equal(true);
- expect(wrapper.children().hasClass('bar')).to.equal(true);
- expect(wrapper.children().hasClass('baz')).to.equal(true);
- expect(wrapper.children().hasClass('some-long-string')).to.equal(true);
- expect(wrapper.children().hasClass('FoOo')).to.equal(true);
- expect(wrapper.children().hasClass('doesnt-exist')).to.equal(false);
- });
- });
-
- it('returns whether or not node has a certain class', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.hasClass('foo')).to.equal(true);
- expect(wrapper.hasClass('bar')).to.equal(true);
- expect(wrapper.hasClass('baz')).to.equal(true);
- expect(wrapper.hasClass('some-long-string')).to.equal(true);
- expect(wrapper.hasClass('FoOo')).to.equal(true);
- expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
- });
-
- context('when using a Composite component that renders null', () => {
- it('returns whether or not node has a certain class', () => {
- class Foo extends React.Component {
- render() {
- return null;
- }
- }
- const wrapper = shallow( );
-
- expect(wrapper.hasClass('foo')).to.equal(false);
- });
- });
-
- it('works with a non-string `className` prop', () => {
- class Foo extends React.Component {
- render() {
- return
;
- }
- }
- const obj = { classA: true, classB: false };
- const wrapper = shallow( );
- expect(wrapper.hasClass('foo')).to.equal(false);
- expect(wrapper.hasClass('classA')).to.equal(false);
- expect(wrapper.hasClass('classB')).to.equal(false);
- expect(wrapper.hasClass(String(obj))).to.equal(true);
- });
-
- it('allows hyphens', () => {
- const wrapper = shallow(
);
- expect(wrapper.hasClass('foo-bar')).to.equal(true);
- });
-
- it('works if className has a function in toString property', () => {
- function classes() {}
- classes.toString = () => 'foo-bar';
- const wrapper = shallow(
);
- expect(wrapper.hasClass('foo-bar')).to.equal(true);
- });
-
- it('works if searching with a RegExp', () => {
- const wrapper = shallow(
);
- expect(wrapper.hasClass(/(ComponentName)-(classname)-(\d+)/)).to.equal(true);
- expect(wrapper.hasClass(/(ComponentName)-(other)-(\d+)/)).to.equal(false);
- });
- });
-
- describe('.forEach(fn)', () => {
- it('calls a function for each node in the wrapper', () => {
- const wrapper = shallow((
-
- ));
- const spy = sinon.spy();
-
- wrapper.find('.foo').forEach(spy);
-
- expect(spy).to.have.property('callCount', 3);
- expect(spy.args[0][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[0][0].hasClass('bax')).to.equal(true);
- expect(spy.args[0][1]).to.equal(0);
- expect(spy.args[1][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[1][0].hasClass('bar')).to.equal(true);
- expect(spy.args[1][1]).to.equal(1);
- expect(spy.args[2][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[2][0].hasClass('baz')).to.equal(true);
- expect(spy.args[2][1]).to.equal(2);
- });
- });
-
- describe('.map(fn)', () => {
- it('calls a function with a wrapper for each node in the wrapper', () => {
- const wrapper = shallow((
-
- ));
- const spy = sinon.spy();
-
- wrapper.find('.foo').map(spy);
-
- expect(spy).to.have.property('callCount', 3);
- expect(spy.args[0][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[0][0].hasClass('bax')).to.equal(true);
- expect(spy.args[0][1]).to.equal(0);
- expect(spy.args[1][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[1][0].hasClass('bar')).to.equal(true);
- expect(spy.args[1][1]).to.equal(1);
- expect(spy.args[2][0]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[2][0].hasClass('baz')).to.equal(true);
- expect(spy.args[2][1]).to.equal(2);
- });
-
- it('returns an array with the mapped values', () => {
- const wrapper = shallow((
-
- ));
- const result = wrapper.find('.foo').map(w => w.props().className);
-
- expect(result).to.eql([
- 'foo bax',
- 'foo bar',
- 'foo baz',
- ]);
- });
- });
-
- describe('.reduce(fn[, initialValue])', () => {
- it('has the right length', () => {
- expect(ShallowWrapper.prototype.reduce).to.have.lengthOf(1);
- });
-
- it('calls a function with a wrapper for each node in the wrapper', () => {
- const wrapper = shallow((
-
- ));
- const spy = sinon.spy(n => n + 1);
-
- wrapper.find('.foo').reduce(spy, 0);
-
- expect(spy).to.have.property('callCount', 3);
- expect(spy.args[0][1]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[0][1].hasClass('bax')).to.equal(true);
- expect(spy.args[1][1]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[1][1].hasClass('bar')).to.equal(true);
- expect(spy.args[2][1]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[2][1].hasClass('baz')).to.equal(true);
- });
-
- it('accumulates a value', () => {
- const wrapper = shallow((
-
- ));
- const result = wrapper.find('.foo').reduce(
- (obj, n) => {
- obj[n.prop('id')] = n.prop('className');
- return obj;
- },
- {},
- );
-
- expect(result).to.eql({
- bax: 'foo qoo',
- bar: 'foo boo',
- baz: 'foo hoo',
- });
- });
-
- it('allows the initialValue to be omitted', () => {
- const one = (
);
- const two = (
);
- const three = (
);
- const wrapper = shallow((
-
- {one}
- {two}
- {three}
-
- ));
- const counter = ( );
- const result = wrapper
- .find('.foo')
- .reduce((acc, n) => [].concat(acc, n, new ShallowWrapper(counter)))
- .map(getWrapperPropSelector('id'));
-
- expect(result).to.eql([one, two, counter, three, counter].map(getElementPropSelector('id')));
- });
- });
-
- describe('.reduceRight(fn[, initialValue])', () => {
- it('has the right length', () => {
- expect(ShallowWrapper.prototype.reduceRight).to.have.lengthOf(1);
- });
-
- it('calls a function with a wrapper for each node in the wrapper in reverse', () => {
- const wrapper = shallow((
-
- ));
- const spy = sinon.spy(n => n + 1);
-
- wrapper.find('.foo').reduceRight(spy, 0);
-
- expect(spy).to.have.property('callCount', 3);
- expect(spy.args[0][1]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[0][1].hasClass('baz')).to.equal(true);
- expect(spy.args[1][1]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[1][1].hasClass('bar')).to.equal(true);
- expect(spy.args[2][1]).to.be.instanceOf(ShallowWrapper);
- expect(spy.args[2][1].hasClass('bax')).to.equal(true);
- });
-
- it('accumulates a value', () => {
- const wrapper = shallow((
-
- ));
- const result = wrapper.find('.foo').reduceRight(
- (obj, n) => {
- obj[n.prop('id')] = n.prop('className');
- return obj;
- },
- {},
- );
-
- expect(result).to.eql({
- bax: 'foo qoo',
- bar: 'foo boo',
- baz: 'foo hoo',
- });
- });
-
- it('allows the initialValue to be omitted', () => {
- const one = (
);
- const two = (
);
- const three = (
);
- const wrapper = shallow((
-
- {one}
- {two}
- {three}
-
- ));
- const counter = ( );
- const result = wrapper
- .find('.foo')
- .reduceRight((acc, n) => [].concat(acc, n, new ShallowWrapper(counter)))
- .map(getWrapperPropSelector('id'));
-
- expect(result).to.eql([three, two, counter, one, counter].map(getElementPropSelector('id')));
- });
- });
-
- describe('.slice([begin[, end]])', () => {
- it('returns an identical wrapper if no params are set', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.foo').slice()).to.have.lengthOf(3);
- expect(wrapper.find('.foo').slice().at(0).hasClass('bax')).to.equal(true);
- expect(wrapper.find('.foo').slice().at(1).hasClass('bar')).to.equal(true);
- expect(wrapper.find('.foo').slice().at(2).hasClass('baz')).to.equal(true);
- });
-
- it('returns a new wrapper if begin is set', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.foo').slice(1)).to.have.lengthOf(2);
- expect(wrapper.find('.foo').slice(1).at(0).hasClass('bar')).to.equal(true);
- expect(wrapper.find('.foo').slice(1).at(1).hasClass('baz')).to.equal(true);
- });
-
- it('returns a new wrapper if begin and end are set', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.foo').slice(1, 2)).to.have.lengthOf(1);
- expect(wrapper.find('.foo').slice(1, 2).at(0).hasClass('bar')).to.equal(true);
- });
-
- it('returns a new wrapper if begin and end are set (negative)', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.foo').slice(-2, -1)).to.have.lengthOf(1);
- expect(wrapper.find('.foo').slice(-2, -1).at(0).hasClass('bar')).to.equal(true);
- });
- });
-
- describe('.some(selector)', () => {
- it('returns if a node matches a selector', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.foo').some('.qoo')).to.equal(true);
- expect(wrapper.find('.foo').some('.foo')).to.equal(true);
- expect(wrapper.find('.foo').some('.bar')).to.equal(false);
- });
-
- it('throws if called on root', () => {
- const wrapper = shallow((
-
- ));
- expect(() => wrapper.some('.foo')).to.throw(
- Error,
- 'ShallowWrapper::some() can not be called on the root',
- );
- });
- });
-
- describe('.someWhere(predicate)', () => {
- it('returns if a node matches a predicate', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.foo').someWhere(n => n.hasClass('qoo'))).to.equal(true);
- expect(wrapper.find('.foo').someWhere(n => n.hasClass('foo'))).to.equal(true);
- expect(wrapper.find('.foo').someWhere(n => n.hasClass('bar'))).to.equal(false);
- });
- });
-
- describe('.every(selector)', () => {
- it('returns if every node matches a selector', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.foo').every('.foo')).to.equal(true);
- expect(wrapper.find('.foo').every('.qoo')).to.equal(false);
- expect(wrapper.find('.foo').every('.bar')).to.equal(false);
- });
- });
-
- describe('.everyWhere(predicate)', () => {
- it('returns if every node matches a predicate', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.foo').everyWhere(n => n.hasClass('foo'))).to.equal(true);
- expect(wrapper.find('.foo').everyWhere(n => n.hasClass('qoo'))).to.equal(false);
- expect(wrapper.find('.foo').everyWhere(n => n.hasClass('bar'))).to.equal(false);
- });
- });
-
- describe('.flatMap(fn)', () => {
- it('returns a wrapper with the mapped and flattened nodes', () => {
- const wrapper = shallow((
-
- ));
-
- const nodes = wrapper.find('.foo').flatMap(w => w.children().getElements());
-
- expect(nodes).to.have.lengthOf(6);
- expect(nodes.at(0).hasClass('bar')).to.equal(true);
- expect(nodes.at(1).hasClass('bar')).to.equal(true);
- expect(nodes.at(2).hasClass('baz')).to.equal(true);
- expect(nodes.at(3).hasClass('baz')).to.equal(true);
- expect(nodes.at(4).hasClass('bax')).to.equal(true);
- expect(nodes.at(5).hasClass('bax')).to.equal(true);
- });
- });
-
- describe('.shallow()', () => {
- it('returns a shallow rendered instance of the current node', () => {
- class Bar extends React.Component {
- render() {
- return (
-
- );
- }
- }
- class Foo extends React.Component {
- render() {
- return (
-
-
-
- );
- }
- }
- const wrapper = shallow( );
- expect(wrapper.find('.in-bar')).to.have.lengthOf(0);
- expect(wrapper.find(Bar)).to.have.lengthOf(1);
- expect(wrapper.find(Bar).shallow().find('.in-bar')).to.have.lengthOf(1);
- });
-
- describe('context', () => {
- it('can pass in context', () => {
- class Bar extends React.Component {
- render() {
- return {this.context.name}
;
- }
- }
- Bar.contextTypes = {
- name: PropTypes.string,
- };
- class Foo extends React.Component {
- render() {
- return (
-
-
-
- );
- }
- }
-
- const context = { name: 'foo' };
- const wrapper = shallow( );
- expect(wrapper.find(Bar)).to.have.lengthOf(1);
- expect(wrapper.find(Bar).shallow({ context }).text()).to.equal('foo');
- });
-
- it('does not throw if context is passed in but contextTypes is missing', () => {
- class Bar extends React.Component {
- render() {
- return {this.context.name}
;
- }
- }
- class Foo extends React.Component {
- render() {
- return (
-
-
-
- );
- }
- }
-
- const context = { name: 'foo' };
- const wrapper = shallow( );
- expect(() => wrapper.find(Bar).shallow({ context })).to.not.throw();
- });
-
- it('is introspectable through context API', () => {
- class Bar extends React.Component {
- render() {
- return {this.context.name}
;
- }
- }
- Bar.contextTypes = {
- name: PropTypes.string,
- };
- class Foo extends React.Component {
- render() {
- return (
-
-
-
- );
- }
- }
-
- const context = { name: 'foo' };
- const wrapper = shallow( ).find(Bar).shallow({ context });
-
- expect(wrapper.context().name).to.equal(context.name);
- expect(wrapper.context('name')).to.equal(context.name);
- });
-
- it('filters context to childContextTypes', () => {
- class Bar extends React.Component {
- render() {
- return
;
- }
- }
- Bar.contextTypes = {
- name: PropTypes.string,
- };
- class Foo extends React.Component {
- render() {
- return (
-
-
-
- );
- }
- }
-
- const context = { name: 'foo', hello: 'world' };
- const wrapper = shallow( , { context });
- expect(wrapper.find(Bar).dive().context()).to.eql({ name: 'foo' });
- });
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('returns a shallow rendered instance of the current node', () => {
- const Bar = () => (
-
- );
- const Foo = () => (
-
-
-
- );
-
- const wrapper = shallow( );
- expect(wrapper.find('.in-bar')).to.have.lengthOf(0);
- expect(wrapper.find(Bar)).to.have.lengthOf(1);
- expect(wrapper.find(Bar).shallow().find('.in-bar')).to.have.lengthOf(1);
- });
-
- describe('context', () => {
- it('can pass in context', () => {
- const Bar = (props, context) => (
- {context.name}
- );
- Bar.contextTypes = { name: PropTypes.string };
- const Foo = () => (
-
-
-
- );
-
- const context = { name: 'foo' };
- const wrapper = shallow( );
- expect(wrapper.find(Bar).shallow({ context }).text()).to.equal('foo');
- });
-
- it('does not throw if context is passed in but contextTypes is missing', () => {
- const Bar = (props, context) => (
- {context.name}
- );
- const Foo = () => (
-
-
-
- );
-
- const context = { name: 'foo' };
- const wrapper = shallow( );
- expect(() => wrapper.find(Bar).shallow({ context })).to.not.throw();
- });
-
- itIf(is('< 16'), 'is introspectable through context API', () => {
- const Bar = (props, context) => (
- {context.name}
- );
- Bar.contextTypes = { name: PropTypes.string };
- const Foo = () => (
-
-
-
- );
-
- const context = { name: 'foo' };
- const wrapper = shallow( ).find(Bar).shallow({ context });
-
- expect(wrapper.context().name).to.equal(context.name);
- expect(wrapper.context('name')).to.equal(context.name);
- });
-
- itIf(is('>= 16'), 'will throw when trying to inspect context', () => {
- const Bar = (props, context) => (
- {context.name}
- );
- Bar.contextTypes = { name: PropTypes.string };
- const Foo = () => (
-
-
-
- );
-
- const context = { name: 'foo' };
- const wrapper = shallow( ).find(Bar).shallow({ context });
-
- expect(() => wrapper.context()).to.throw(
- Error,
- 'ShallowWrapper::context() can only be called on wrapped nodes that have a non-null instance',
- );
- expect(() => wrapper.context('name')).to.throw(
- Error,
- 'ShallowWrapper::context() can only be called on wrapped nodes that have a non-null instance',
- );
- });
- });
- });
- });
-
- describe('.first()', () => {
- it('returns the first node in the current set', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.bar').first().hasClass('baz')).to.equal(true);
- });
- });
-
- describe('.last()', () => {
- it('returns the last node in the current set', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.bar').last().hasClass('baz')).to.equal(true);
- });
- });
-
- describe('.isEmpty()', () => {
- let warningStub;
- let fooNode;
- let missingNode;
-
- beforeEach(() => {
- warningStub = sinon.stub(console, 'warn');
- const wrapper = shallow((
-
- ));
- fooNode = wrapper.find('.foo');
- missingNode = wrapper.find('.missing');
- });
- afterEach(() => {
- warningStub.restore();
- });
-
- it('displays a deprecation warning', () => {
- fooNode.isEmpty();
- expect(warningStub.calledWith('Enzyme::Deprecated method isEmpty() called, use exists() instead.')).to.equal(true);
- });
-
- it('calls exists() instead', () => {
- const existsSpy = sinon.spy();
- fooNode.exists = existsSpy;
- fooNode.isEmpty();
- expect(existsSpy.called).to.equal(true);
- });
-
- it('returns true if wrapper is empty', () => {
- expect(fooNode.isEmpty()).to.equal(false);
- expect(missingNode.isEmpty()).to.equal(true);
- });
- });
-
- describe('.exists()', () => {
- it('has no required arguments', () => {
- expect(ShallowWrapper.prototype.exists).to.have.lengthOf(0);
- });
-
- describe('without argument', () => {
- it('returns true if node exists in wrapper', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.bar').exists()).to.equal(false);
- expect(wrapper.find('.foo').exists()).to.equal(true);
- });
- });
- describe('with argument', () => {
- it('throws on invalid EnzymeSelector', () => {
- const wrapper = shallow(
);
-
- expect(() => wrapper.exists(null)).to.throw(TypeError);
- expect(() => wrapper.exists(undefined)).to.throw(TypeError);
- expect(() => wrapper.exists(45)).to.throw(TypeError);
- expect(() => wrapper.exists({})).to.throw(TypeError);
- });
-
- it('returns .find(arg).exists() instead', () => {
- const wrapper = shallow(
);
- const fakeFindExistsReturnVal = Symbol('fake .find(arg).exists() return value');
- const fakeSelector = '.someClass';
- wrapper.find = sinon.stub().returns({ exists: () => fakeFindExistsReturnVal });
- const existsResult = wrapper.exists(fakeSelector);
- expect(wrapper.find).to.have.property('callCount', 1);
- expect(wrapper.find.firstCall.args[0]).to.equal(fakeSelector);
- expect(existsResult).to.equal(fakeFindExistsReturnVal);
- });
- });
- });
-
- describe('.at(index)', () => {
- it('gets a wrapper of the node at the specified index', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.bar').at(0).hasClass('foo')).to.equal(true);
- expect(wrapper.find('.bar').at(1).hasClass('bax')).to.equal(true);
- expect(wrapper.find('.bar').at(2).hasClass('bux')).to.equal(true);
- expect(wrapper.find('.bar').at(3).hasClass('baz')).to.equal(true);
- });
-
- it('`.at()` does not affect the results of `.exists()`', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.bar').exists()).to.equal(false);
- expect(wrapper.find('.bar').at(0).exists()).to.equal(false);
-
- expect(wrapper.find('.foo').exists()).to.equal(true);
- expect(wrapper.find('.foo').at(0).exists()).to.equal(true);
- });
- });
-
- describe('.get(index)', () => {
- it('gets the node at the specified index', () => {
- const wrapper = shallow((
-
- ));
- expect(wrapper.find('.bar').get(0)).to.deep.equal(wrapper.find('.foo').getElement());
- expect(wrapper.find('.bar').get(1)).to.deep.equal(wrapper.find('.bax').getElement());
- expect(wrapper.find('.bar').get(2)).to.deep.equal(wrapper.find('.bux').getElement());
- expect(wrapper.find('.bar').get(3)).to.deep.equal(wrapper.find('.baz').getElement());
- });
-
- it('does not add a "null" key to elements with a ref and no key', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.setRef = this.setRef.bind(this);
- }
-
- setRef(node) {
- this.node = node;
- }
-
- render() {
- return (
-
- );
- }
- }
- const wrapper = shallow( );
- expect(wrapper.get(0).key).to.equal(null);
- });
- });
-
- describe('.debug()', () => {
- it('passes through to the debugNodes function', () => {
- expect(shallow(
).debug()).to.equal('
');
- });
- });
-
- describe('.html()', () => {
- it('returns html of straight DOM elements', () => {
- const wrapper = shallow((
-
- Hello World!
-
- ));
- expect(wrapper.html()).to.equal((
- 'Hello World!
'
- ));
- });
-
- it('renders out nested composite components', () => {
- class Foo extends React.Component {
- render() {
- return (
);
- }
- }
- class Bar extends React.Component {
- render() {
- return (
-
-
-
- );
- }
- }
- const wrapper = shallow( );
- expect(wrapper.html()).to.equal((
- ''
- ));
- expect(wrapper.find(Foo).html()).to.equal((
- '
'
- ));
- });
-
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('renders out nested composite components', () => {
- const Foo = () => (
-
- );
- const Bar = () => (
-
-
-
- );
-
- const wrapper = shallow( );
- expect(wrapper.html()).to.equal((
- ''
- ));
- expect(wrapper.find(Foo).html()).to.equal((
- '
'
- ));
- });
- });
-
- describeIf(is('>16.2'), 'Fragments', () => {
- class FragmentClassExample extends React.Component {
- render() {
- return (
-
- Foo
- Bar
-
- );
- }
- }
-
- const FragmentConstExample = () => (
-
- Foo
- Bar
-
- );
-
- class ClassChild extends React.Component {
- render() {
- return Class child
;
- }
- }
-
- function SFCChild() {
- return SFC child
;
- }
-
- class FragmentWithCustomChildClass extends React.Component {
- render() {
- return (
-
-
-
-
- );
- }
- }
-
- it('correctly renders html for both children for class', () => {
- const classWrapper = shallow( );
- expect(classWrapper.html()).to.equal('Foo
Bar
');
- });
-
- it('correctly renders html for both children for const', () => {
- const constWrapper = shallow( );
- expect(constWrapper.html()).to.equal('Foo
Bar
');
- });
-
- it('correctly renders html for custom component children', () => {
- const withChildrenWrapper = shallow( );
- expect(withChildrenWrapper.html()).to.equal('Class child
SFC child
');
- });
- });
- });
-
- describe('.unmount()', () => {
- it('calls componentWillUnmount()', () => {
- const spy = sinon.spy();
-
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.componentWillUnmount = spy;
- }
-
- render() {
- return (
-
- {this.props.id}
-
- );
- }
- }
- const wrapper = shallow( );
- expect(spy).to.have.property('callCount', 0);
- wrapper.unmount();
- expect(spy).to.have.property('callCount', 1);
- });
- });
-
- describe('.render()', () => {
- it('returns a cheerio wrapper around the current node', () => {
- class Foo extends React.Component {
- render() {
- return (
);
- }
- }
-
- class Bar extends React.Component {
- render() {
- return (
-
-
-
- );
- }
- }
-
- const wrapper = shallow( );
-
- expect(wrapper.render().find('.in-foo')).to.have.lengthOf(1);
-
- const rendered = wrapper.render();
- expect(rendered.is('.in-bar')).to.equal(true);
- expect(rendered).to.have.lengthOf(1);
-
- const renderedFoo = wrapper.find(Foo).render();
- expect(renderedFoo.is('.in-foo')).to.equal(true);
- expect(renderedFoo.is('.in-bar')).to.equal(false);
- expect(renderedFoo.find('.in-bar')).to.have.lengthOf(0);
- });
-
- describeIf(is('> 0.13'), 'stateless functional components', () => {
- it('returns a cheerio wrapper around the current node', () => {
- const Foo = () => (
-
- );
-
- const Bar = () => (
-
-
-
- );
-
- const wrapper = shallow( );
-
- expect(wrapper.render().find('.in-foo')).to.have.lengthOf(1);
- expect(wrapper.render().is('.in-bar')).to.equal(true);
-
- const renderedFoo = wrapper.find(Foo).render();
- expect(renderedFoo.is('.in-foo')).to.equal(true);
- expect(renderedFoo.is('.in-bar')).to.equal(false);
- expect(renderedFoo.find('.in-bar')).to.have.lengthOf(0);
- });
- });
- });
-
- wrap()
- .withConsoleThrows()
- .describe('.renderProp()', () => {
- it('returns a wrapper around the node returned from the render prop', () => {
- class Foo extends React.Component {
- render() {
- return
;
- }
- }
- class Bar extends React.Component {
- render() {
- const { render: r } = this.props;
- return {r()}
;
- }
- }
-
- const wrapperA = shallow();
- const renderPropWrapperA = wrapperA.find(Bar).renderProp('render')();
- expect(renderPropWrapperA.find(Foo)).to.have.lengthOf(1);
-
- const wrapperB = shallow( } />
);
- const renderPropWrapperB = wrapperB.find(Bar).renderProp('render')();
- expect(renderPropWrapperB.find(Foo)).to.have.lengthOf(1);
-
- const stub = sinon.stub().returns(
);
- const wrapperC = shallow(
);
- stub.resetHistory();
- wrapperC.find(Bar).renderProp('render')('one', 'two');
- expect(stub.args).to.deep.equal([['one', 'two']]);
- });
-
- it('throws on host elements', () => {
- class Div extends React.Component {
- render() {
- const { children } = this.props;
- return {children}
;
- }
- }
-
- const wrapper = shallow(
);
- expect(wrapper.is('div')).to.equal(true);
- expect(() => wrapper.renderProp('foo')).to.throw();
- });
-
- wrap()
- .withOverride(() => getAdapter(), 'wrap', () => undefined)
- .it('throws with a react adapter that lacks a `.wrap`', () => {
- class Foo extends React.Component {
- render() {
- return
;
- }
- }
- class Bar extends React.Component {
- render() {
- const { render: r } = this.props;
- return {r()}
;
- }
- }
-
- const wrapper = shallow();
- expect(() => wrapper.find(Bar).renderProp('render')).to.throw(RangeError);
- });
-
- describeIf(is('>= 16'), 'allows non-nodes', () => {
- function MyComponent({ val }) {
- return x} />;
- }
-
- function ComponentWithRenderProp({ val, r }) {
- return r(val);
- }
-
- it('works with strings', () => {
- const wrapper = shallow( );
-
- wrapper.find(ComponentWithRenderProp).renderProp('r')('foo');
-
- wrapper.find(ComponentWithRenderProp).renderProp('r')('');
- });
-
- it('works with numbers', () => {
- const wrapper = shallow( );
-
- wrapper.find(ComponentWithRenderProp).renderProp('r')(42);
-
- wrapper.find(ComponentWithRenderProp).renderProp('r')(0);
-
- wrapper.find(ComponentWithRenderProp).renderProp('r')(NaN);
- });
-
- it('works with null', () => {
- const wrapper = shallow( );
-
- wrapper.find(ComponentWithRenderProp).renderProp('r')(null);
- });
-
- it('throws with undefined', () => {
- const wrapper = shallow( );
-
- expect(() => wrapper.find(ComponentWithRenderProp).renderProp('r')(undefined).shallow()).to.throw();
- });
-
- it('works with arrays', () => {
- const wrapper = shallow( );
-
- wrapper.find(ComponentWithRenderProp).renderProp('r')([]);
-
- wrapper.find(ComponentWithRenderProp).renderProp('r')(['a']);
-
- wrapper.find(ComponentWithRenderProp).renderProp('r')([Infinity]);
- });
-
- it('works with false', () => {
- const wrapper = shallow( );
-
- wrapper.find(ComponentWithRenderProp).renderProp('r')(false);
- });
-
- it('throws with true', () => {
- const wrapper = shallow( );
-
- expect(() => wrapper.find(ComponentWithRenderProp).renderProp('r')(true).shallow()).to.throw();
- });
- });
- });
-
- describe('lifecycle methods', () => {
- describe('disableLifecycleMethods option', () => {
- describe('validation', () => {
- it('throws for a non-boolean value', () => {
- ['value', 42, null].forEach((value) => {
- expect(() => shallow(
, {
- disableLifecycleMethods: value,
- })).to.throw(/true or false/);
- });
- });
-
- it('does not throw for a boolean value or undefined', () => {
- [true, false, undefined].forEach((value) => {
- expect(() => shallow(
, {
- disableLifecycleMethods: value,
- })).not.to.throw();
- });
- });
-
- it('does not throw when no lifecycle flags are provided in options', () => {
- expect(() => shallow(
, {})).not.to.throw();
- });
-
- it('throws when used with lifecycleExperimental in invalid combinations', () => {
- [true, false].forEach((value) => {
- expect(() => shallow(
, {
- lifecycleExperimental: value,
- disableLifecycleMethods: value,
- })).to.throw(/same value/);
- });
- });
- });
-
- describe('when disabled', () => {
- let wrapper;
- const spy = sinon.spy();
- class Foo extends React.Component {
- componentWillMount() { spy('componentWillMount'); }
-
- componentDidMount() { spy('componentDidMount'); }
-
- componentWillReceiveProps() { spy('componentWillReceiveProps'); }
-
- shouldComponentUpdate() {
- spy('shouldComponentUpdate');
- return true;
- }
-
- componentWillUpdate() { spy('componentWillUpdate'); }
-
- componentDidUpdate() { spy('componentDidUpdate'); }
-
- componentWillUnmount() { spy('componentWillUnmount'); }
-
- render() {
- spy('render');
- return foo
;
- }
- }
-
- const options = {
- disableLifecycleMethods: true,
- context: {
- foo: 'foo',
- },
- };
-
- beforeEach(() => {
- wrapper = shallow( , options);
- spy.resetHistory();
- });
-
- it('does not call componentDidMount when mounting', () => {
- wrapper = shallow( , options);
- expect(spy.args).to.deep.equal([
- ['componentWillMount'],
- ['render'],
- ]);
- });
-
- it('calls expected methods when receiving new props', () => {
- wrapper.setProps({ foo: 'foo' });
- expect(spy.args).to.deep.equal([
- ['componentWillReceiveProps'],
- ['shouldComponentUpdate'],
- ['componentWillUpdate'],
- ['render'],
- ]);
- });
-
- describeIf(is('0.13 || 15 || > 16'), 'setContext', () => {
- it('calls expected methods when receiving new context', () => {
- wrapper.setContext({ foo: 'foo' });
- expect(spy.args).to.deep.equal([
- ['componentWillReceiveProps'],
- ['shouldComponentUpdate'],
- ['componentWillUpdate'],
- ['render'],
- ]);
- });
- });
-
- describeIf(is('16'), 'setContext', () => {
- it('calls expected methods when receiving new context', () => {
- wrapper.setContext({ foo: 'foo' });
- expect(spy.args).to.deep.equal([
- ['shouldComponentUpdate'],
- ['componentWillUpdate'],
- ['render'],
- ]);
- });
- });
-
- describeIf(is('0.14'), 'setContext', () => {
- it('calls expected methods when receiving new context', () => {
- wrapper.setContext({ foo: 'foo' });
- expect(spy.args).to.deep.equal([
- ['shouldComponentUpdate'],
- ['componentWillUpdate'],
- ['render'],
- ]);
- });
- });
-
- itIf(is('< 16'), 'calls expected methods for setState', () => {
- wrapper.setState({ bar: 'bar' });
- expect(spy.args).to.deep.equal([
- ['shouldComponentUpdate'],
- ['componentWillUpdate'],
- ['render'],
- ['componentDidUpdate'],
- ]);
- });
-
- // componentDidUpdate is not called in react 16
- itIf(is('>= 16'), 'calls expected methods for setState', () => {
- wrapper.setState({ bar: 'bar' });
- expect(spy.args).to.deep.equal([
- ['shouldComponentUpdate'],
- ['componentWillUpdate'],
- ['render'],
- ]);
- });
-
- it('calls expected methods when unmounting', () => {
- wrapper.unmount();
- expect(spy.args).to.deep.equal([
- ['componentWillUnmount'],
- ]);
- });
- });
-
- it('does not call when disableLifecycleMethods flag is true', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- componentDidMount() {
- spy();
- }
-
- render() {
- return foo
;
- }
- }
- shallow( , { disableLifecycleMethods: true });
- expect(spy).to.have.property('callCount', 0);
- });
-
- it('calls `componentDidMount` directly when disableLifecycleMethods is true', () => {
- class Table extends React.Component {
- render() {
- return ();
- }
- }
-
- class MyComponent extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- showTable: false,
- };
- }
-
- componentDidMount() {
- this.setState({ showTable: true });
- }
-
- render() {
- const { showTable } = this.state;
- return ();
- }
- }
- const wrapper = shallow( , { disableLifecycleMethods: true });
- expect(wrapper.find(Table).length).to.equal(0);
- wrapper.instance().componentDidMount();
- expect(wrapper.find(Table).length).to.equal(1);
- });
-
- it('calls shouldComponentUpdate when disableLifecycleMethods flag is true', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- foo: 'bar',
- };
- }
-
- shouldComponentUpdate() {
- spy();
- return false;
- }
-
- render() {
- return {this.state.foo}
;
- }
- }
- const wrapper = shallow(
- ,
- {
- context: { foo: 'foo' },
- disableLifecycleMethods: true,
- },
- );
- expect(spy).to.have.property('callCount', 0);
- wrapper.setProps({ foo: 'bar' });
- expect(spy).to.have.property('callCount', 1);
- wrapper.setState({ foo: 'bar' });
- expect(spy).to.have.property('callCount', 2);
- wrapper.setContext({ foo: 'bar' });
- expect(spy).to.have.property('callCount', 3);
- });
- });
-
- describe('lifecycleExperimental option', () => {
- describe('validation', () => {
- it('throws for a non-boolean value', () => {
- ['value', 42, null].forEach((value) => {
- expect(() => shallow(
, {
- lifecycleExperimental: value,
- })).to.throw(/true or false/);
- });
- });
-
- it('does not throw for a boolean value or when not provided', () => {
- [true, false, undefined].forEach((value) => {
- expect(() => shallow(
, {
- lifecycleExperimental: value,
- })).not.to.throw();
- });
- });
- });
- });
-
- describeIf(is('>= 16.3'), 'getDerivedStateFromProps', () => {
- let spy;
-
- beforeEach(() => {
- spy = sinon.spy();
- });
-
- class Spy extends React.Component {
- constructor(...args) {
- super(...args);
- this.state = { state: true }; // eslint-disable-line react/no-unused-state
- spy('constructor');
- }
-
- shouldComponentUpdate(nextProps, nextState, nextContext) {
- spy('shouldComponentUpdate', {
- prevProps: this.props,
- nextProps,
- prevState: this.state,
- nextState,
- prevContext: this.context,
- nextContext,
- });
- return true;
- }
-
- componentWillUpdate(nextProps, nextState, nextContext) {
- spy('componentWillUpdate', {
- prevProps: this.props,
- nextProps,
- prevState: this.state,
- nextState,
- prevContext: this.context,
- nextContext,
- });
- }
-
- componentDidUpdate(prevProps, prevState, prevContext) {
- spy('componentDidUpdate', {
- prevProps,
- nextProps: this.props,
- prevState,
- nextState: this.state,
- prevContext,
- nextContext: this.context,
- });
- }
-
- render() {
- spy('render');
- return null;
- }
- }
-
- class CWRP extends Spy {
- componentWillReceiveProps(nextProps, nextContext) {
- spy('componentWillReceiveProps', {
- prevProps: this.props,
- nextProps,
- prevState: this.state,
- nextState: this.state,
- prevContext: this.context,
- nextContext,
- });
- }
- }
-
- class U_CWRP extends Spy {
- UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
- spy('UNSAFE_componentWillReceiveProps', {
- prevProps: this.props,
- nextProps,
- prevState: this.state,
- nextState: this.state,
- prevContext: this.context,
- nextContext: undefined,
- });
- }
- }
-
- class GDSFP extends Spy {
- static getDerivedStateFromProps(props, state) {
- spy('getDerivedStateFromProps', { props, state });
- return {};
- }
- }
-
- it('calls cWRP when expected', () => {
- const prevProps = { a: 1 };
- const wrapper = shallow( );
- expect(spy.args).to.deep.equal([
- ['constructor'],
- ['render'],
- ]);
- spy.resetHistory();
-
- const foo = {};
- const props = { foo };
- const {
- context: prevContext,
- context: nextContext,
- state: prevState,
- state: nextState,
- } = wrapper.instance();
-
- wrapper.setProps(props);
- const nextProps = { ...prevProps, ...props };
-
- const data = {
- prevProps,
- nextProps,
- prevState,
- nextState,
- prevContext,
- nextContext,
- };
- expect(spy.args).to.deep.equal([
- ['componentWillReceiveProps', data],
- ['shouldComponentUpdate', data],
- ['componentWillUpdate', data],
- ['render'],
- ['componentDidUpdate', {
- ...data,
- prevContext: is('>= 16') ? undefined : prevContext,
- }],
- ]);
- });
-
- it('calls UNSAFE_cWRP when expected', () => {
- const prevProps = { a: 1 };
- // eslint-disable-next-line react/jsx-pascal-case
- const wrapper = shallow( );
- expect(spy.args).to.deep.equal([
- ['constructor'],
- ['render'],
- ]);
- spy.resetHistory();
-
- const foo = {};
- const props = { foo };
- const {
- context: prevContext,
- context: nextContext,
- state: prevState,
- state: nextState,
- } = wrapper.instance();
-
- wrapper.setProps(props);
- const nextProps = { ...prevProps, ...props };
-
- const data = {
- prevProps,
- nextProps,
- prevState,
- nextState,
- prevContext,
- nextContext,
- };
- expect(spy.args).to.deep.equal([
- ['UNSAFE_componentWillReceiveProps', {
- ...data,
- nextContext: is('>= 16') ? undefined : nextContext,
- }],
- ['shouldComponentUpdate', data],
- ['componentWillUpdate', data],
- ['render'],
- ['componentDidUpdate', {
- ...data,
- prevContext: is('>= 16') ? undefined : prevContext,
- }],
- ]);
- });
-
- it('calls gDSFP when expected', () => {
- const prevProps = { a: 1 };
- const state = { state: true };
- const wrapper = shallow( );
- expect(spy.args).to.deep.equal([
- ['constructor'],
- ['getDerivedStateFromProps', {
- props: prevProps,
- state,
- }],
- ['render'],
- ]);
- spy.resetHistory();
-
- const foo = {};
- const props = { foo };
- const {
- context: prevContext,
- context: nextContext,
- state: prevState,
- state: nextState,
- } = wrapper.instance();
-
- wrapper.setProps(props);
- const nextProps = { ...prevProps, ...props };
-
- const data = {
- prevProps,
- nextProps,
- prevState,
- nextState,
- prevContext,
- nextContext,
- };
- expect(spy.args).to.deep.equal([
- ['getDerivedStateFromProps', {
- props: nextProps,
- state: nextState,
- }],
- ['shouldComponentUpdate', data],
- ['render'],
- ['componentDidUpdate', {
- ...data,
- prevContext: is('>= 16') ? undefined : prevContext,
- }],
- ]);
- });
-
- it('cDU’s nextState differs from `this.state` when gDSFP returns new state', () => {
- class SimpleComponent extends React.Component {
- constructor(props) {
- super(props);
- this.state = { value: props.value };
- }
-
- static getDerivedStateFromProps(props, state) {
- return props.value === state.value ? null : { value: props.value };
- }
-
- shouldComponentUpdate(nextProps, nextState) {
- return nextState.value !== this.state.value;
- }
-
- render() {
- const { value } = this.state;
- return ( );
- }
- }
- const wrapper = shallow( );
-
- expect(wrapper.find('input').prop('value')).to.equal('initial');
-
- wrapper.setProps({ value: 'updated' });
-
- expect(wrapper.find('input').prop('value')).to.equal('updated');
- });
- });
-
- describeIf(is('>= 16'), 'componentDidCatch', () => {
- describe('errors inside an error boundary', () => {
- const errorToThrow = new EvalError('threw an error!');
-
- const hasFragments = is('>= 16.2');
- const MaybeFragment = hasFragments ? Fragment : 'main';
-
- function Thrower({ throws }) {
- if (throws) {
- throw errorToThrow;
- }
- return null;
- }
-
- class ErrorBoundary extends React.Component {
- constructor(...args) {
- super(...args);
- this.state = {
- throws: false,
- didThrow: false,
- };
- }
-
- componentDidCatch(error, info) {
- const { spy } = this.props;
- spy(error, info);
- this.setState({
- throws: false,
- didThrow: true,
- });
- }
-
- render() {
- const {
- didThrow,
- throws,
- } = this.state;
- return (
-
-
-
-
-
- {didThrow ? 'HasThrown' : 'HasNotThrown'}
-
-
-
-
- );
- }
- }
-
- describe('Thrower', () => {
- it('does not throw when `throws` is `false`', () => {
- expect(() => shallow( )).not.to.throw();
- });
-
- it('throws when `throws` is `true`', () => {
- expect(() => shallow( )).to.throw(errorToThrow);
- });
- });
-
- it('catches a simulated error', () => {
- const spy = sinon.spy();
- const wrapper = shallow( );
-
- expect(spy).to.have.property('callCount', 0);
-
- expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
-
- expect(spy).to.have.property('callCount', 1);
-
- expect(spy.args).to.be.an('array').and.have.lengthOf(1);
- const [[actualError, info]] = spy.args;
- expect(() => { throw actualError; }).to.throw(errorToThrow);
- expect(info).to.deep.equal({
- componentStack: `
- in Thrower (created by ErrorBoundary)
- in span (created by ErrorBoundary)${hasFragments ? '' : `
- in main (created by ErrorBoundary)`}
- in div (created by ErrorBoundary)
- in ErrorBoundary (created by WrapperComponent)
- in WrapperComponent`,
- });
- });
-
- it('rerenders on a simulated error', () => {
- const wrapper = shallow( );
-
- expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0);
- expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1);
-
- expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
-
- expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(1);
- expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(0);
- });
-
- it('does not catch errors during shallow render', () => {
- const spy = sinon.spy();
- const wrapper = shallow( );
-
- expect(spy).to.have.property('callCount', 0);
-
- wrapper.setState({ throws: true });
-
- expect(spy).to.have.property('callCount', 0);
-
- const thrower = wrapper.find(Thrower);
- expect(thrower).to.have.lengthOf(1);
- expect(thrower.props()).to.have.property('throws', true);
-
- expect(() => thrower.dive()).to.throw(errorToThrow);
-
- expect(spy).to.have.property('callCount', 0);
-
- expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0);
- expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1);
- });
- });
- });
-
- describeIf(is('>= 16.6'), 'getDerivedStateFromError', () => {
- describe('errors inside an error boundary', () => {
- const errorToThrow = new EvalError('threw an error!');
-
- function Thrower({ throws }) {
- if (throws) {
- throw errorToThrow;
- }
- return null;
- }
-
- function getErrorBoundary() {
- return class ErrorBoundary extends React.Component {
- static getDerivedStateFromError() {
- return {
- throws: false,
- didThrow: true,
- };
- }
-
- constructor(props) {
- super(props);
- this.state = {
- throws: false,
- didThrow: false,
- };
- }
-
- render() {
- const {
- didThrow,
- throws,
- } = this.state;
-
- return (
-
-
-
-
-
- {didThrow ? 'HasThrown' : 'HasNotThrown'}
-
-
-
-
- );
- }
- };
- }
-
- describe('Thrower', () => {
- it('does not throw when `throws` is `false`', () => {
- expect(() => shallow( )).not.to.throw();
- });
-
- it('throws when `throws` is `true`', () => {
- expect(() => shallow( )).to.throw();
- });
- });
-
- it('catches a simulated error', () => {
- const ErrorBoundary = getErrorBoundary();
-
- const spy = sinon.spy(ErrorBoundary, 'getDerivedStateFromError');
- const wrapper = shallow( );
-
- expect(spy).to.have.property('callCount', 0);
-
- expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
-
- expect(spy).to.have.property('callCount', 1);
-
- expect(spy.args).to.be.an('array').and.have.lengthOf(1);
- const [[actualError]] = spy.args;
- expect(actualError).to.equal(errorToThrow);
- });
-
- it('rerenders on a simulated error', () => {
- const ErrorBoundary = getErrorBoundary();
-
- const wrapper = shallow( );
-
- expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0);
- expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1);
-
- expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
-
- expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(1);
- expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(0);
- });
-
- it('does not catch errors during shallow render', () => {
- const ErrorBoundary = getErrorBoundary();
-
- const spy = sinon.spy(ErrorBoundary, 'getDerivedStateFromError');
- const wrapper = shallow( );
-
- expect(spy).to.have.property('callCount', 0);
-
- wrapper.setState({ throws: true });
-
- expect(spy).to.have.property('callCount', 0);
-
- const thrower = wrapper.find(Thrower);
- expect(thrower).to.have.lengthOf(1);
- expect(thrower.props()).to.have.property('throws', true);
-
- expect(() => thrower.dive()).to.throw(errorToThrow);
-
- expect(spy).to.have.property('callCount', 0);
-
- expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0);
- expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1);
- });
- });
- });
-
- describeIf(is('>= 16.6'), 'getDerivedStateFromError and componentDidCatch combined', () => {
-
- const errorToThrow = new EvalError('threw an error!');
- const expectedInfo = {
- componentStack: `
- in Thrower (created by ErrorBoundary)
- in div (created by ErrorBoundary)
- in ErrorBoundary (created by WrapperComponent)
- in WrapperComponent`,
- };
-
- function Thrower({ throws }) {
- if (throws) {
- throw errorToThrow;
- }
- return null;
- }
-
- describe('errors inside error boundary when getDerivedStateFromProps returns update', () => {
- let lifecycleSpy;
- let stateSpy;
-
- beforeEach(() => {
- lifecycleSpy = sinon.spy();
- stateSpy = sinon.spy();
- });
-
- class ErrorBoundary extends React.Component {
- static getDerivedStateFromError(error) {
- lifecycleSpy('getDerivedStateFromError', error);
- return {
- didThrow: true,
- throws: false,
- };
- }
-
- constructor(props) {
- super(props);
- this.state = {
- didThrow: false,
- throws: false,
- };
-
- lifecycleSpy('constructor');
- }
-
- componentDidCatch(error, info) {
- lifecycleSpy('componentDidCatch', error, info);
- stateSpy({ ...this.state });
- }
-
- render() {
- lifecycleSpy('render');
-
- const {
- throws,
- } = this.state;
-
- return (
-
-
-
- );
- }
- }
-
- it('does not catch errors during shallow render', () => {
- const wrapper = shallow( );
-
- expect(lifecycleSpy).to.have.property('callCount', 2);
- expect(lifecycleSpy.args).to.deep.equal([
- ['constructor'],
- ['render'],
- ]);
-
- expect(stateSpy).to.have.property('callCount', 0);
-
- lifecycleSpy.resetHistory();
-
- wrapper.setState({ throws: true });
-
- const thrower = wrapper.find(Thrower);
- expect(thrower).to.have.lengthOf(1);
- expect(thrower.props()).to.have.property('throws', true);
-
- expect(() => thrower.dive()).to.throw(errorToThrow);
-
- expect(lifecycleSpy).to.have.property('callCount', 1);
- expect(lifecycleSpy.args).to.deep.equal([
- ['render'],
- ]);
- });
-
- it('calls getDerivedStateFromError first and then componentDidCatch for simulated error', () => {
- const wrapper = shallow( );
-
- expect(lifecycleSpy).to.have.property('callCount', 2);
- expect(lifecycleSpy.args).to.deep.equal([
- ['constructor'],
- ['render'],
- ]);
-
- expect(stateSpy).to.have.property('callCount', 0);
-
- lifecycleSpy.resetHistory();
-
- expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw();
-
- expect(lifecycleSpy).to.have.property('callCount', 3);
- expect(lifecycleSpy.args).to.deep.equal([
- ['getDerivedStateFromError', errorToThrow],
- ['render'],
- ['componentDidCatch', errorToThrow, expectedInfo],
- ]);
-
- expect(stateSpy).to.have.property('callCount', 1);
- expect(stateSpy.args).to.deep.equal([
- [{
- throws: false,
- didThrow: true,
- }],
- ]);
- });
- });
-
- describe('errors inside error boundary when getDerivedStateFromError does not return update', () => {
- let spy;
-
- beforeEach(() => {
- spy = sinon.spy();
- });
-
- class ErrorBoundary extends React.Component {
- static getDerivedStateFromError(error) {
- spy('getDerivedStateFromError', error);
- return null;
- }
-
- constructor(props) {
- super(props);
- this.state = {
- didThrow: false,
- throws: false,
- };
-
- spy('constructor');
- }
-
- componentDidCatch(error, info) {
- spy('componentDidCatch', error, info);
-
- this.setState({
- didThrow: true,
- throws: false,
- });
- }
-
- render() {
- spy('render');
-
- const {
- didThrow,
- throws,
- } = this.state;
-
- return (
-
-
-
- {didThrow ? 'HasThrown' : 'HasNotThrown'}
-
-
- );
- }
- }
-
- it('does not catch errors during shallow render', () => {
- const wrapper = shallow( );
-
- expect(spy).to.have.property('callCount', 2);
- expect(spy.args).to.deep.equal([
- ['constructor'],
- ['render'],
- ]);
-
- spy.resetHistory();
-
- wrapper.setState({ throws: true });
-
- const thrower = wrapper.find(Thrower);
- expect(thrower).to.have.lengthOf(1);
- expect(thrower.props()).to.have.property('throws', true);
-
- expect(() => thrower.dive()).to.throw(errorToThrow);
-
- expect(spy).to.have.property('callCount', 1);
- expect(spy.args).to.deep.equal([
- ['render'],
- ]);
- });
-
- it('rerenders on a simulated error', () => {
- const wrapper = shallow( );
-
- expect(spy).to.have.property('callCount', 2);
- expect(spy.args).to.deep.equal([
- ['constructor'],
- ['render'],
- ]);
-
- spy.resetHistory();
-
- const thrower = wrapper.find(Thrower);
-
- expect(() => thrower.simulateError(errorToThrow)).not.to.throw(errorToThrow);
-
- expect(spy).to.have.property('callCount', 3);
- expect(spy.args).to.deep.equal([
- ['getDerivedStateFromError', errorToThrow],
- ['componentDidCatch', errorToThrow, expectedInfo],
- ['render'],
- ]);
- });
- });
- });
-
- context('mounting phase', () => {
- it('calls componentWillMount and componentDidMount', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- componentWillMount() {
- spy('componentWillMount');
- }
-
- componentDidMount() {
- spy('componentDidMount');
- }
-
- render() {
- spy('render');
- return foo
;
- }
- }
- shallow( );
- expect(spy.args).to.deep.equal([
- ['componentWillMount'],
- ['render'],
- ['componentDidMount'],
- ]);
- });
-
- itIf(BATCHING, 'is batching updates', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- count: 0,
- };
- }
-
- componentWillMount() {
- this.setState({ count: this.state.count + 1 });
- this.setState({ count: this.state.count + 1 });
- }
-
- componentDidMount() {
- this.setState({ count: this.state.count + 1 });
- this.setState({ count: this.state.count + 1 });
- }
-
- render() {
- spy();
- return {this.state.count}
;
- }
- }
- const result = shallow( );
- expect(result.state('count')).to.equal(2);
- expect(spy).to.have.property('callCount', 2);
- });
- });
-
- context('updating props', () => {
- it('calls shouldComponentUpdate, componentWillUpdate, and componentDidUpdate', () => {
- const spy = sinon.spy();
-
- class Foo extends React.Component {
- constructor(...args) {
- super(...args);
- this.state = {
- foo: 'state',
- };
- }
-
- componentWillReceiveProps(nextProps, nextContext) {
- spy('componentWillReceiveProps', this.props, nextProps, nextContext);
- }
-
- shouldComponentUpdate(nextProps, nextState, nextContext) {
- spy('shouldComponentUpdate', this.props, nextProps, this.state, nextState, nextContext);
- return true;
- }
-
- componentWillUpdate(nextProps, nextState, nextContext) {
- spy('componentWillUpdate', this.props, nextProps, this.state, nextState, nextContext);
- }
-
- componentDidUpdate(prevProps, prevState, prevContext) {
- spy('componentDidUpdate', prevProps, this.props, prevState, this.state, prevContext);
- }
-
- render() {
- spy('render');
- return {this.state.foo}
;
- }
- }
- Foo.contextTypes = {
- foo: PropTypes.string,
- };
-
- const wrapper = shallow(
- ,
- {
- context: { foo: 'context' },
- },
- );
- wrapper.setProps({ foo: 'baz' });
- wrapper.setProps({ foo: 'bax' });
- expect(spy.args).to.deep.equal([
- [
- 'render',
- ],
- [
- 'componentWillReceiveProps',
- { foo: 'bar' }, { foo: 'baz' },
- { foo: 'context' }, // this will be fixed
- ],
- [
- 'shouldComponentUpdate',
- { foo: 'bar' }, { foo: 'baz' },
- { foo: 'state' }, { foo: 'state' },
- { foo: 'context' },
- ],
- [
- 'componentWillUpdate',
- { foo: 'bar' }, { foo: 'baz' },
- { foo: 'state' }, { foo: 'state' },
- { foo: 'context' },
- ],
- [
- 'render',
- ],
- [
- 'componentDidUpdate',
- { foo: 'bar' }, { foo: 'baz' },
- { foo: 'state' }, { foo: 'state' },
- is('>= 16') ? undefined : { foo: 'context' },
- ],
- [
- 'componentWillReceiveProps',
- { foo: 'baz' }, { foo: 'bax' },
- { foo: 'context' },
- ],
- [
- 'shouldComponentUpdate',
- { foo: 'baz' }, { foo: 'bax' },
- { foo: 'state' }, { foo: 'state' },
- { foo: 'context' },
- ],
- [
- 'componentWillUpdate',
- { foo: 'baz' }, { foo: 'bax' },
- { foo: 'state' }, { foo: 'state' },
- { foo: 'context' },
- ],
- [
- 'render',
- ],
- [
- 'componentDidUpdate',
- { foo: 'baz' }, { foo: 'bax' },
- { foo: 'state' }, { foo: 'state' },
- is('>= 16') ? undefined : { foo: 'context' },
- ],
- ]);
- });
-
- it('calls componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate and componentDidUpdate with merged props', () => {
- const spy = sinon.spy();
-
- class Foo extends React.Component {
- componentWillReceiveProps(nextProps) {
- spy('componentWillReceiveProps', this.props, nextProps);
- }
-
- shouldComponentUpdate(nextProps) {
- spy('shouldComponentUpdate', this.props, nextProps);
- return true;
- }
-
- componentWillUpdate(nextProps) {
- spy('componentWillUpdate', this.props, nextProps);
- }
-
- componentDidUpdate(prevProps) {
- spy('componentDidUpdate', prevProps, this.props);
- }
-
- render() {
- return (
-
- );
- }
- }
-
- const wrapper = shallow( );
-
- wrapper.setProps({ b: 'c', d: 'e' });
-
- expect(spy.args).to.deep.equal([
- [
- 'componentWillReceiveProps',
- { a: 'a', b: 'b' },
- { a: 'a', b: 'c', d: 'e' },
- ],
- [
- 'shouldComponentUpdate',
- { a: 'a', b: 'b' },
- { a: 'a', b: 'c', d: 'e' },
- ],
- [
- 'componentWillUpdate',
- { a: 'a', b: 'b' },
- { a: 'a', b: 'c', d: 'e' },
- ],
- [
- 'componentDidUpdate',
- { a: 'a', b: 'b' },
- { a: 'a', b: 'c', d: 'e' },
- ],
- ]);
- });
-
- it('cancels rendering when Component returns false in shouldComponentUpdate', () => {
- const spy = sinon.spy();
-
- class Foo extends React.Component {
- componentWillReceiveProps() {
- spy('componentWillReceiveProps');
- }
-
- shouldComponentUpdate() {
- spy('shouldComponentUpdate');
- return false;
- }
-
- componentWillUpdate() {
- spy('componentWillUpdate');
- }
-
- componentDidUpdate() {
- spy('componentDidUpdate');
- }
-
- render() {
- spy('render');
- return foo
;
- }
- }
-
- const wrapper = shallow( );
- expect(wrapper.instance().props.foo).to.equal('bar');
- wrapper.setProps({ foo: 'baz' });
- expect(wrapper.instance().props.foo).to.equal('baz');
- wrapper.setProps({ foo: 'bax' });
- expect(wrapper.instance().props.foo).to.equal('bax');
- expect(spy.args).to.deep.equal([
- ['render'],
- ['componentWillReceiveProps'],
- ['shouldComponentUpdate'],
- ['componentWillReceiveProps'],
- ['shouldComponentUpdate'],
- ]);
- });
-
- itIf(BATCHING, 'does not provoke another renders to call setState in componentWillReceiveProps', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- count: 0,
- };
- }
-
- componentWillReceiveProps() {
- this.setState({ count: this.state.count + 1 });
- this.setState({ count: this.state.count + 1 });
- }
-
- render() {
- spy();
- return {this.props.foo}
;
- }
- }
- const result = shallow( );
- expect(spy).to.have.property('callCount', 1);
- result.setProps({ name: 'bar' });
- expect(spy).to.have.property('callCount', 2);
- expect(result.state('count')).to.equal(1);
- });
-
- itIf(BATCHING, 'provokes an another render to call setState twice in componentWillUpdate', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.updated = false;
- this.state = {
- count: 0,
- };
- }
-
- componentWillUpdate() {
- if (!this.updated) {
- this.updated = true;
- this.setState({ count: this.state.count + 1 });
- this.setState({ count: this.state.count + 1 });
- }
- }
-
- render() {
- spy();
- return {this.props.foo}
;
- }
- }
- const result = shallow( );
- expect(spy).to.have.property('callCount', 1);
- result.setProps({ name: 'bar' });
- expect(spy).to.have.property('callCount', 3);
- expect(result.state('count')).to.equal(1);
- });
-
- itIf(BATCHING, 'provokes an another render to call setState twice in componentDidUpdate', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.updated = false;
- this.state = {
- count: 0,
- };
- }
-
- componentDidUpdate() {
- if (!this.updated) {
- this.updated = true;
- /* eslint-disable react/no-did-update-set-state */
- this.setState({ count: this.state.count + 1 });
- this.setState({ count: this.state.count + 1 });
- /* eslint-enable react/no-did-update-set-state */
- }
- }
-
- render() {
- spy();
- return {this.props.foo}
;
- }
- }
- const result = shallow( );
- expect(spy).to.have.property('callCount', 1);
- result.setProps({ name: 'bar' });
- expect(spy).to.have.property('callCount', 3);
- expect(result.state('count')).to.equal(1);
- });
- });
-
- context('updating state', () => {
- it('calls shouldComponentUpdate, componentWillUpdate and componentDidUpdate', () => {
- const spy = sinon.spy();
-
- class Foo extends React.Component {
- constructor(...args) {
- super(...args);
- this.state = {
- foo: 'bar',
- };
- }
-
- shouldComponentUpdate(nextProps, nextState, nextContext) {
- spy('shouldComponentUpdate', this.props, nextProps, this.state, nextState, nextContext);
- return true;
- }
-
- componentWillUpdate(nextProps, nextState, nextContext) {
- spy('componentWillUpdate', this.props, nextProps, this.state, nextState, nextContext);
- }
-
- componentDidUpdate(prevProps, prevState, prevContext) {
- spy('componentDidUpdate', prevProps, this.props, prevState, this.state, prevContext);
- }
-
- render() {
- spy('render');
- return {this.state.foo}
;
- }
- }
- Foo.contextTypes = {
- foo: PropTypes.string,
- };
-
- const wrapper = shallow(
- ,
- {
- context: { foo: 'context' },
- },
- );
- wrapper.setState({ foo: 'baz' });
- const expected = [
- [
- 'render',
- ],
- [
- 'shouldComponentUpdate',
- { foo: 'props' }, { foo: 'props' },
- { foo: 'bar' }, { foo: 'baz' },
- { foo: 'context' },
- ],
- [
- 'componentWillUpdate',
- { foo: 'props' }, { foo: 'props' },
- { foo: 'bar' }, { foo: 'baz' },
- { foo: 'context' },
- ],
- [
- 'render',
- ],
- [
- 'componentDidUpdate',
- { foo: 'props' }, { foo: 'props' },
- { foo: 'bar' }, { foo: 'baz' },
- is('>= 16') ? undefined : { foo: 'context' },
- ],
- ];
- expect(spy.args).to.deep.equal(expected);
- });
-
- it('cancels rendering when Component returns false in shouldComponentUpdate', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- foo: 'bar',
- };
- }
-
- shouldComponentUpdate() {
- spy('shouldComponentUpdate');
- return false;
- }
-
- componentWillUpdate() {
- spy('componentWillUpdate');
- }
-
- componentDidUpdate() {
- spy('componentDidUpdate');
- }
-
- render() {
- spy('render');
- return {this.state.foo}
;
- }
- }
- const wrapper = shallow( );
- expect(wrapper.instance().state.foo).to.equal('bar');
- wrapper.setState({ foo: 'baz' });
- expect(wrapper.instance().state.foo).to.equal('baz');
- expect(spy.args).to.deep.equal([['render'], ['shouldComponentUpdate']]);
- });
-
- itIf(BATCHING, 'provokes an another render to call setState twice in componentWillUpdate', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.updated = false;
- this.state = {
- name: 'foo',
- count: 0,
- };
- }
-
- componentWillUpdate() {
- if (!this.updated) {
- this.updated = true;
- this.setState({ count: this.state.count + 1 });
- this.setState({ count: this.state.count + 1 });
- }
- }
-
- render() {
- spy();
- return {this.state.name}
;
- }
- }
- const result = shallow( );
- expect(spy).to.have.property('callCount', 1);
- result.setState({ name: 'bar' });
- expect(spy).to.have.property('callCount', 3);
- expect(result.state('count')).to.equal(1);
- });
-
- itIf(BATCHING, 'provokes an another render to call setState twice in componentDidUpdate', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.updated = false;
- this.state = {
- name: 'foo',
- count: 0,
- };
- }
-
- componentDidUpdate() {
- if (!this.updated) {
- this.updated = true;
- /* eslint-disable react/no-did-update-set-state */
- this.setState({ count: this.state.count + 1 });
- this.setState({ count: this.state.count + 1 });
- /* eslint-enable react/no-did-update-set-state */
- }
- }
-
- render() {
- spy();
- return {this.state.name}
;
- }
- }
- const result = shallow( );
- expect(spy).to.have.property('callCount', 1);
- result.setState({ name: 'bar' });
- expect(spy).to.have.property('callCount', 3);
- expect(result.state('count')).to.equal(1);
- });
- });
-
- context('updating context', () => {
- it('calls shouldComponentUpdate, componentWillUpdate and componentDidUpdate', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(...args) {
- super(...args);
- this.state = {
- foo: 'state',
- };
- }
-
- shouldComponentUpdate(nextProps, nextState, nextContext) {
- spy('shouldComponentUpdate', this.props, nextProps, this.state, nextState, nextContext);
- return true;
- }
-
- componentWillUpdate(nextProps, nextState, nextContext) {
- spy('componentWillUpdate', this.props, nextProps, this.state, nextState, nextContext);
- }
-
- componentDidUpdate(prevProps, prevState, prevContext) {
- spy('componentDidUpdate', prevProps, this.props, prevState, this.state, prevContext);
- }
-
- render() {
- spy('render');
- return {this.state.foo}
;
- }
- }
- Foo.contextTypes = {
- foo: PropTypes.string,
- };
const wrapper = shallow(
,
{
- context: { foo: 'bar' },
+ context: { foo: 'context' },
},
);
- expect(wrapper.instance().context.foo).to.equal('bar');
- wrapper.setContext({ foo: 'baz' });
- expect(wrapper.instance().context.foo).to.equal('baz');
- expect(spy.args).to.deep.equal([
+ wrapper.setState({ foo: 'baz' });
+ const expected = [
[
'render',
],
[
'shouldComponentUpdate',
{ foo: 'props' }, { foo: 'props' },
- { foo: 'state' }, { foo: 'state' },
- { foo: 'baz' },
+ { foo: 'bar' }, { foo: 'baz' },
+ { foo: 'context' },
],
[
'componentWillUpdate',
{ foo: 'props' }, { foo: 'props' },
- { foo: 'state' }, { foo: 'state' },
- { foo: 'baz' },
+ { foo: 'bar' }, { foo: 'baz' },
+ { foo: 'context' },
],
[
'render',
@@ -7383,1402 +2492,748 @@ describe('shallow', () => {
[
'componentDidUpdate',
{ foo: 'props' }, { foo: 'props' },
- { foo: 'state' }, { foo: 'state' },
- is('>= 16') ? undefined : { foo: 'bar' },
+ { foo: 'bar' }, { foo: 'baz' },
+ is('>= 16') ? undefined : { foo: 'context' },
],
- ]);
- });
-
- it('cancels rendering when Component returns false in shouldComponentUpdate', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- shouldComponentUpdate() {
- spy('shouldComponentUpdate');
- return false;
- }
-
- componentWillUpdate() {
- spy('componentWillUpdate');
- }
-
- componentDidUpdate() {
- spy('componentDidUpdate');
- }
-
- render() {
- spy('render');
- return foo
;
- }
- }
- Foo.contextTypes = {
- foo: PropTypes.string,
- };
- const wrapper = shallow(
- ,
- {
- context: { foo: 'bar' },
- },
- );
- wrapper.setContext({ foo: 'baz' });
- expect(spy.args).to.deep.equal([['render'], ['shouldComponentUpdate']]);
- });
-
- itIf(BATCHING, 'provokes an another render to call setState twice in componentWillUpdate', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.updated = false;
- this.state = {
- count: 0,
- };
- }
-
- componentWillUpdate() {
- if (!this.updated) {
- this.updated = true;
- this.setState({ count: this.state.count + 1 });
- this.setState({ count: this.state.count + 1 });
- }
- }
-
- render() {
- spy();
- return {this.state.name}
;
- }
- }
- const result = shallow(
- ,
- {
- context: { foo: 'bar' },
- },
- );
- expect(spy).to.have.property('callCount', 1);
- result.setContext({ foo: 'baz' });
- expect(spy).to.have.property('callCount', 3);
- expect(result.state('count')).to.equal(1);
- });
-
- itIf(BATCHING, 'provokes an another render to call setState twice in componentDidUpdate', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.updated = false;
- this.state = {
- count: 0,
- };
- }
-
- componentDidUpdate() {
- if (!this.updated) {
- this.updated = true;
- /* eslint-disable react/no-did-update-set-state */
- this.setState({ count: this.state.count + 1 });
- this.setState({ count: this.state.count + 1 });
- /* eslint-enable react/no-did-update-set-state */
- }
- }
-
- render() {
- spy();
- return {this.state.name}
;
- }
- }
- const result = shallow(
- ,
- {
- context: { foo: 'bar' },
- },
- );
- expect(spy).to.have.property('callCount', 1);
- result.setContext({ foo: 'baz' });
- expect(spy).to.have.property('callCount', 3);
- expect(result.state('count')).to.equal(1);
- });
- });
-
- context('unmounting phase', () => {
- it('calls componentWillUnmount', () => {
- const spy = sinon.spy();
- class Foo extends React.Component {
- componentWillUnmount() {
- spy();
- }
-
- render() {
- return foo
;
- }
- }
- const wrapper = shallow( );
- wrapper.unmount();
- expect(spy).to.have.property('callCount', 1);
- });
- });
-
- context('component instance', () => {
- it('calls `componentDidUpdate` when component’s `setState` is called', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- foo: 'init',
- };
- }
-
- 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.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}
- click
-
- );
- }
- }
- 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');
+ ];
+ expect(spy.args).to.deep.equal(expected);
});
- it('does not call `componentDidMount` twice when a child component is created', () => {
+ it('cancels rendering when Component returns false in shouldComponentUpdate', () => {
+ const spy = sinon.spy();
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
- foo: 'init',
+ foo: 'bar',
};
}
- componentDidMount() {}
+ shouldComponentUpdate() {
+ spy('shouldComponentUpdate');
+ return false;
+ }
+
+ componentWillUpdate() {
+ spy('componentWillUpdate');
+ }
+
+ componentDidUpdate() {
+ spy('componentDidUpdate');
+ }
render() {
- return (
-
- this.setState({ foo: 'update2' })}>
- click
-
- {this.state.foo}
-
- );
+ spy('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);
+ expect(wrapper.instance().state.foo).to.equal('bar');
+ wrapper.setState({ foo: 'baz' });
+ expect(wrapper.instance().state.foo).to.equal('baz');
+ expect(spy.args).to.deep.equal([['render'], ['shouldComponentUpdate']]);
});
- });
- describeIf(is('>= 15.3'), 'PureComponent', () => {
- it('does not update when state and props did not change', () => {
- class Foo extends PureComponent {
+ itIf(BATCHING, 'provokes an another render to call setState twice in componentWillUpdate', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
constructor(props) {
super(props);
+ this.updated = false;
this.state = {
- foo: 'init',
+ name: 'foo',
+ count: 0,
};
}
- componentDidUpdate() {}
-
- render() {
- return (
-
- {this.state.foo}
-
- );
- }
- }
- const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
- const wrapper = shallow( );
- wrapper.setState({ foo: 'update' });
- expect(spy).to.have.property('callCount', 1);
- wrapper.setState({ foo: 'update' });
- expect(spy).to.have.property('callCount', 1);
-
- wrapper.setProps({ id: 2 });
- expect(spy).to.have.property('callCount', 2);
- wrapper.setProps({ id: 2 });
- expect(spy).to.have.property('callCount', 2);
- });
-
- class Test extends PureComponent {
- constructor(...args) {
- super(...args);
-
- this.state = { a: { b: { c: 1 } } };
- }
-
- componentDidUpdate() {
- const { onUpdate } = this.props;
- onUpdate();
- }
-
- setDeepEqualState() {
- this.setState({ a: { b: { c: 1 } } });
- }
-
- setDeepDifferentState() {
- this.setState({ a: { b: { c: 2 } } });
- }
-
- render() {
- const { a: { b: { c } } } = this.state;
- return {c}
;
- }
- }
-
- it('rerenders on setState when new state is !==, but deeply equal to existing state', () => {
- const updateSpy = sinon.spy();
- const wrapper = shallow( );
- wrapper.instance().setDeepEqualState();
- expect(updateSpy).to.have.property('callCount', 1);
- });
-
- it('rerenders when setState is called with an object that doesnt have deep equality', () => {
- const updateSpy = sinon.spy();
- const wrapper = shallow( );
- wrapper.instance().setDeepDifferentState();
- expect(updateSpy).to.have.property('callCount', 1);
- });
-
- describeIf(is('>= 16.3'), 'setProps calls `componentDidUpdate` when `getDerivedStateFromProps` is defined', () => {
- class DummyComp extends PureComponent {
- constructor(...args) {
- super(...args);
- this.state = { state: -1 };
- }
-
- static getDerivedStateFromProps({ changeState, counter }) {
- return changeState ? { state: counter * 10 } : null;
+ componentWillUpdate() {
+ if (!this.updated) {
+ this.updated = true;
+ this.setState({ count: this.state.count + 1 });
+ this.setState({ count: this.state.count + 1 });
+ }
}
- componentDidUpdate() {}
-
render() {
- const { counter } = this.props;
- const { state } = this.state;
- return (
-
- {counter}
- {state}
-
- );
+ spy();
+ return {this.state.name}
;
}
}
-
- let cDU;
- let gDSFP;
-
- beforeEach(() => { // eslint-disable-line mocha/no-sibling-hooks
- cDU = sinon.spy(DummyComp.prototype, 'componentDidUpdate');
- gDSFP = sinon.spy(DummyComp, 'getDerivedStateFromProps');
- });
-
- it('with no state changes, calls both methods with a sync and async setProps', () => {
- const wrapper = shallow( );
-
- expect(gDSFP).to.have.property('callCount', 1);
- const [firstCall] = gDSFP.args;
- expect(firstCall).to.eql([{
- changeState: false,
- counter: 0,
- }, {
- state: -1,
- }]);
- expect(wrapper.state()).to.eql({ state: -1 });
-
- wrapper.setProps({ counter: 1 });
-
- expect(cDU).to.have.property('callCount', 1);
- expect(gDSFP).to.have.property('callCount', 2);
- const [, secondCall] = gDSFP.args;
- expect(secondCall).to.eql([{
- changeState: false,
- counter: 1,
- }, {
- state: -1,
- }]);
- expect(wrapper.state()).to.eql({ state: -1 });
-
- return new Promise((resolve) => {
- wrapper.setProps({ counter: 2 }, resolve);
- }).then(() => {
- expect(cDU).to.have.property('callCount', 2);
- expect(gDSFP).to.have.property('callCount', 3);
- const [, , thirdCall] = gDSFP.args;
- expect(thirdCall).to.eql([{
- changeState: false,
- counter: 2,
- }, {
- state: -1,
- }]);
- expect(wrapper.state()).to.eql({ state: -1 });
- });
- });
-
- it('with a state changes, calls both methods with a sync and async setProps', () => {
- const wrapper = shallow( );
-
- expect(cDU).to.have.property('callCount', 0);
- expect(gDSFP).to.have.property('callCount', 1);
- const [firstCall] = gDSFP.args;
- expect(firstCall).to.eql([{
- changeState: true,
- counter: 0,
- }, {
- state: -1,
- }]);
- expect(wrapper.state()).to.eql({ state: 0 });
-
- wrapper.setProps({ counter: 1 });
-
- expect(cDU).to.have.property('callCount', 1);
- expect(gDSFP).to.have.property('callCount', 2);
- const [, secondCall] = gDSFP.args;
- expect(secondCall).to.eql([{
- changeState: true,
- counter: 1,
- }, {
- state: 0,
- }]);
- expect(wrapper.state()).to.eql({ state: 10 });
-
- return new Promise((resolve) => {
- wrapper.setProps({ counter: 2 }, resolve);
- }).then(() => {
- expect(cDU).to.have.property('callCount', 2);
- expect(gDSFP).to.have.property('callCount', 3);
- const [, , thirdCall] = gDSFP.args;
- expect(thirdCall).to.eql([{
- changeState: true,
- counter: 2,
- }, {
- state: 10,
- }]);
- expect(wrapper.state()).to.eql({ state: 20 });
- });
- });
+ const result = shallow( );
+ expect(spy).to.have.property('callCount', 1);
+ result.setState({ name: 'bar' });
+ expect(spy).to.have.property('callCount', 3);
+ expect(result.state('count')).to.equal(1);
});
- });
- describe('Own PureComponent implementation', () => {
- it('does not update when state and props did not change', () => {
+ itIf(BATCHING, 'provokes an another render to call setState twice in componentDidUpdate', () => {
+ const spy = sinon.spy();
class Foo extends React.Component {
constructor(props) {
super(props);
+ this.updated = false;
this.state = {
- foo: 'init',
+ name: 'foo',
+ count: 0,
};
}
- shouldComponentUpdate(nextProps, nextState) {
- return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
+ componentDidUpdate() {
+ if (!this.updated) {
+ this.updated = true;
+ /* eslint-disable react/no-did-update-set-state */
+ this.setState({ count: this.state.count + 1 });
+ this.setState({ count: this.state.count + 1 });
+ /* eslint-enable react/no-did-update-set-state */
+ }
}
- componentDidUpdate() {}
-
render() {
- return (
-
- {this.state.foo}
-
- );
+ spy();
+ return {this.state.name}
;
}
}
- const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
- const wrapper = shallow( );
- wrapper.setState({ foo: 'update' });
- expect(spy).to.have.property('callCount', 1);
- wrapper.setState({ foo: 'update' });
+ const result = shallow( );
expect(spy).to.have.property('callCount', 1);
-
- wrapper.setProps({ id: 2 });
- expect(spy).to.have.property('callCount', 2);
- wrapper.setProps({ id: 2 });
- expect(spy).to.have.property('callCount', 2);
+ result.setState({ name: 'bar' });
+ expect(spy).to.have.property('callCount', 3);
+ expect(result.state('count')).to.equal(1);
});
});
- describeIf(is('>= 16.3'), 'support getSnapshotBeforeUpdate', () => {
- it('calls getSnapshotBeforeUpdate and pass snapshot to componentDidUpdate', () => {
+ context('updating context', () => {
+ it('calls shouldComponentUpdate, componentWillUpdate and componentDidUpdate', () => {
const spy = sinon.spy();
class Foo extends React.Component {
- constructor(props) {
- super(props);
+ constructor(...args) {
+ super(...args);
this.state = {
- foo: 'bar',
+ foo: 'state',
};
}
- componentDidUpdate(prevProps, prevState, snapshot) {
- spy('componentDidUpdate', prevProps, this.props, prevState, this.state, snapshot);
+ shouldComponentUpdate(nextProps, nextState, nextContext) {
+ spy('shouldComponentUpdate', this.props, nextProps, this.state, nextState, nextContext);
+ return true;
}
- getSnapshotBeforeUpdate(prevProps, prevState) {
- spy('getSnapshotBeforeUpdate', prevProps, this.props, prevState, this.state);
- return { snapshot: 'ok' };
+ componentWillUpdate(nextProps, nextState, nextContext) {
+ spy('componentWillUpdate', this.props, nextProps, this.state, nextState, nextContext);
+ }
+
+ componentDidUpdate(prevProps, prevState, prevContext) {
+ spy('componentDidUpdate', prevProps, this.props, prevState, this.state, prevContext);
}
render() {
spy('render');
- return foo
;
+ return {this.state.foo}
;
}
}
- const wrapper = shallow( );
- spy.resetHistory();
- wrapper.setProps({ name: 'bar' });
- expect(spy.args).to.deep.equal([
- ['render'],
- ['getSnapshotBeforeUpdate', { name: 'foo' }, { name: 'bar' }, { foo: 'bar' }, { foo: 'bar' }],
- ['componentDidUpdate', { name: 'foo' }, { name: 'bar' }, { foo: 'bar' }, { foo: 'bar' }, { snapshot: 'ok' }],
- ]);
- spy.resetHistory();
- wrapper.setState({ foo: 'baz' });
+ Foo.contextTypes = {
+ foo: PropTypes.string,
+ };
+ const wrapper = shallow(
+ ,
+ {
+ context: { foo: 'bar' },
+ },
+ );
+ expect(wrapper.instance().context.foo).to.equal('bar');
+ wrapper.setContext({ foo: 'baz' });
+ expect(wrapper.instance().context.foo).to.equal('baz');
expect(spy.args).to.deep.equal([
- ['render'],
- ['getSnapshotBeforeUpdate', { name: 'bar' }, { name: 'bar' }, { foo: 'bar' }, { foo: 'baz' }],
- ['componentDidUpdate', { name: 'bar' }, { name: 'bar' }, { foo: 'bar' }, { foo: 'baz' }, { snapshot: 'ok' }],
- ]);
- });
- });
- });
-
- it('works with class components that return null', () => {
- class Foo extends React.Component {
- render() {
- return null;
- }
- }
- const wrapper = shallow( );
- expect(wrapper).to.have.lengthOf(1);
- expect(wrapper.html()).to.equal(null);
- expect(wrapper.type()).to.equal(null);
- const rendered = wrapper.render();
- expect(rendered).to.have.lengthOf(0);
- expect(rendered.html()).to.equal(null);
- });
-
- itIf(is('>= 16'), 'works with class components that return arrays', () => {
- class Foo extends React.Component {
- render() {
- return [
,
];
- }
- }
- const wrapper = shallow( );
- expect(wrapper).to.have.lengthOf(2);
- expect(wrapper.find('div')).to.have.lengthOf(2);
- });
-
- itIf(is('>=15 || ^16.0.0-alpha'), 'works with SFCs that return null', () => {
- const Foo = () => null;
-
- const wrapper = shallow( );
- expect(wrapper).to.have.lengthOf(1);
- expect(wrapper.html()).to.equal(null);
- expect(wrapper.type()).to.equal(null);
- const rendered = wrapper.render();
- expect(rendered).to.have.lengthOf(0);
- expect(rendered.html()).to.equal(null);
- });
-
- describe('.tap()', () => {
- it('calls the passed function with current ShallowWrapper and returns itself', () => {
- const spy = sinon.spy();
- const wrapper = shallow((
-
- )).find('li');
- const result = wrapper.tap(spy);
- expect(spy.calledWith(wrapper)).to.equal(true);
- expect(result).to.equal(wrapper);
- });
- });
-
- describe('.key()', () => {
- it('returns the key of the node', () => {
- const wrapper = shallow((
-
- {['foo', 'bar', ''].map(s => {s} )}
-
- )).find('li');
- expect(wrapper.at(0).key()).to.equal('foo');
- expect(wrapper.at(1).key()).to.equal('bar');
- expect(wrapper.at(2).key()).to.equal('');
- });
-
- it('returns null when no key is specified', () => {
- const wrapper = shallow((
-
- )).find('li');
- expect(wrapper.key()).to.equal(null);
- });
- });
-
- describe('.matchesElement(node)', () => {
- it('matches on a root node that looks like the rendered one', () => {
- const spy = sinon.spy();
- const wrapper = shallow((
-
- )).first();
- expect(wrapper.matchesElement()).to.equal(true);
- expect(wrapper.matchesElement((
-
- ))).to.equal(true);
- expect(wrapper.matchesElement((
-
- ))).to.equal(true);
- expect(wrapper.matchesElement((
-
- ))).to.equal(true);
- expect(spy).to.have.property('callCount', 0);
- });
-
- it('does not match on a root node that doesn’t looks like the rendered one', () => {
- const spy = sinon.spy();
- const spy2 = sinon.spy();
- const wrapper = shallow((
-
- )).first();
- expect(wrapper.matchesElement()).to.equal(false);
- expect(wrapper.matchesElement((
-
- ))).to.equal(false);
- expect(wrapper.matchesElement((
-
- ))).to.equal(false);
- expect(wrapper.matchesElement((
-
- ))).to.equal(false);
- expect(spy).to.have.property('callCount', 0);
- expect(spy2).to.have.property('callCount', 0);
- });
-
- it('matches a simple node', () => {
- class Test extends React.Component {
- render() {
- return test ;
- }
- }
- const wrapper = shallow( );
- expect(wrapper.matchesElement(test )).to.equal(true);
- });
- });
-
- describe('.containsMatchingElement(node)', () => {
- it('matches a root node that looks like the rendered one', () => {
- const spy1 = sinon.spy();
- const spy2 = sinon.spy();
- const wrapper = shallow((
-
-
Hello World
-
Goodbye World
-
- ));
- expect(wrapper.containsMatchingElement((
-
-
Hello World
-
Goodbye World
-
- ))).to.equal(true);
- expect(wrapper.containsMatchingElement((
-
-
Hello World
-
Goodbye World
-
- ))).to.equal(true);
- expect(wrapper.containsMatchingElement((
-
-
Hello World
-
Goodbye World
-
- ))).to.equal(true);
- expect(wrapper.containsMatchingElement((
-
-
Hello World
-
Goodbye World
-
- ))).to.equal(true);
- expect(wrapper.containsMatchingElement((
-
-
Hello World
-
Goodbye World
-
- ))).to.equal(true);
- expect(wrapper.containsMatchingElement((
-
-
Hello World
-
Goodbye World
-
- ))).to.equal(true);
- expect(spy1).to.have.property('callCount', 0);
- expect(spy2).to.have.property('callCount', 0);
- });
-
- it('matches on a single node that looks like a rendered one', () => {
- const spy1 = sinon.spy();
- const spy2 = sinon.spy();
- const wrapper = shallow((
-
-
Hello World
-
Goodbye World
-
- ));
- expect(wrapper.containsMatchingElement(Hello World
)).to.equal(true);
- expect(wrapper.containsMatchingElement(Goodbye World
)).to.equal(true);
- expect(wrapper.containsMatchingElement((
- Hello World
- ))).to.equal(true);
- expect(wrapper.containsMatchingElement((
- Hello World
- ))).to.equal(true);
- expect(wrapper.containsMatchingElement((
- Goodbye World
- ))).to.equal(true);
- expect(wrapper.containsMatchingElement((
- Goodbye World
- ))).to.equal(true);
- expect(spy1).to.have.property('callCount', 0);
- expect(spy2).to.have.property('callCount', 0);
- });
-
- it('does not match on a single node that doesn\'t looks like a rendered one', () => {
- const spy1 = sinon.spy();
- const spy2 = sinon.spy();
- const wrapper = shallow((
-
-
Hello World
-
Goodbye World
-
- ));
- expect(wrapper.containsMatchingElement(Bonjour le monde
)).to.equal(false);
- expect(wrapper.containsMatchingElement((
- Au revoir le monde
- ))).to.equal(false);
- });
-
- it('does not differentiate between absence, null, or undefined', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.containsMatchingElement(
)).to.equal(true);
-
- expect(wrapper.containsMatchingElement(
)).to.equal(true);
- expect(wrapper.containsMatchingElement(
)).to.equal(true);
- expect(wrapper.containsMatchingElement(
)).to.equal(true);
-
- expect(wrapper.containsMatchingElement(
)).to.equal(true);
- expect(wrapper.containsMatchingElement(
)).to.equal(true);
- expect(wrapper.containsMatchingElement(
)).to.equal(true);
-
- expect(wrapper.containsMatchingElement(
)).to.equal(true);
- expect(wrapper.containsMatchingElement(
)).to.equal(true);
- expect(wrapper.containsMatchingElement(
)).to.equal(true);
- });
-
- it('works with leading and trailing spaces', () => {
- const wrapper = shallow((
-
- All Operations
-
- ));
-
- expect(wrapper.containsMatchingElement( All Operations )).to.equal(true);
- });
-
- it('works with leading and trailing newlines', () => {
- const wrapper = shallow((
-
-
- All Operations
-
-
- ));
-
- expect(wrapper.containsMatchingElement( All Operations )).to.equal(true);
- });
- });
-
- describe('.containsAllMatchingElements(nodes)', () => {
- it('throws TypeError if non-array passed in', () => {
- const wrapper = shallow((
-
- Hello
-
- ));
-
- expect(() => wrapper.containsAllMatchingElements((
-
- Hello
-
- ))).to.throw(TypeError, 'nodes should be an Array');
- });
-
- it('matches on array of nodes that each look like rendered nodes, with nested elements', () => {
- const wrapper = shallow((
-
- ));
-
- expect(wrapper.containsAllMatchingElements([
- Hello
,
- Goodbye
,
- ])).to.equal(true);
- });
-
- it('matches on an array of nodes that all looks like one of rendered nodes', () => {
- const spy1 = sinon.spy();
- const spy2 = sinon.spy();
- const wrapper = shallow((
-
-
Hello World
-
Goodbye World
-
- ));
- expect(wrapper.containsAllMatchingElements([
- Hello World
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAllMatchingElements([
- Hello World
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAllMatchingElements([
- Hello World
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAllMatchingElements([
- Hello World
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAllMatchingElements([
- Hello World
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAllMatchingElements([
- Hello World
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAllMatchingElements([
- Hello World
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAllMatchingElements([
- Hello World
,
- Goodbye World
,
- ])).to.equal(true);
- expect(spy1).to.have.property('callCount', 0);
- expect(spy2).to.have.property('callCount', 0);
- });
-
- it('does not match on nodes that doesn\'t all looks like one of rendered nodes', () => {
- const spy1 = sinon.spy();
- const spy2 = sinon.spy();
- const wrapper = shallow((
-
-
Hello World
-
Goodbye World
-
- ));
- expect(wrapper.containsAllMatchingElements([
- Hello World
,
- Bonjour le monde
,
- Goodbye World
,
- ])).to.equal(false);
- expect(spy1).to.have.property('callCount', 0);
- expect(spy2).to.have.property('callCount', 0);
- });
- });
+ [
+ 'render',
+ ],
+ [
+ 'shouldComponentUpdate',
+ { foo: 'props' }, { foo: 'props' },
+ { foo: 'state' }, { foo: 'state' },
+ { foo: 'baz' },
+ ],
+ [
+ 'componentWillUpdate',
+ { foo: 'props' }, { foo: 'props' },
+ { foo: 'state' }, { foo: 'state' },
+ { foo: 'baz' },
+ ],
+ [
+ 'render',
+ ],
+ [
+ 'componentDidUpdate',
+ { foo: 'props' }, { foo: 'props' },
+ { foo: 'state' }, { foo: 'state' },
+ is('>= 16') ? undefined : { foo: 'bar' },
+ ],
+ ]);
+ });
- describe('.containsAnyMatchingElements(nodes)', () => {
- it('matches on an array with at least one node that looks like a rendered nodes', () => {
- const spy1 = sinon.spy();
- const spy2 = sinon.spy();
- const wrapper = shallow((
-
-
Hello World
-
Goodbye World
-
- ));
- expect(wrapper.containsAnyMatchingElements([
- Bonjour le monde
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAnyMatchingElements([
- Bonjour le monde
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAnyMatchingElements([
- Bonjour le monde
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAnyMatchingElements([
- Bonjour le monde
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAnyMatchingElements([
- Bonjour le monde
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAnyMatchingElements([
- Bonjour le monde
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAnyMatchingElements([
- Bonjour le monde
,
- Goodbye World
,
- ])).to.equal(true);
- expect(wrapper.containsAnyMatchingElements([
- Bonjour le monde
,
- Goodbye World
,
- ])).to.equal(true);
- expect(spy1).to.have.property('callCount', 0);
- expect(spy2).to.have.property('callCount', 0);
- });
- it('does not match on an array with no nodes that looks like a rendered nodes', () => {
- const spy1 = sinon.spy();
- const spy2 = sinon.spy();
- const wrapper = shallow((
-
-
Hello World
-
Goodbye World
-
- ));
- expect(wrapper.containsAnyMatchingElements([
- Bonjour le monde
,
- Au revoir le monde
,
- ])).to.equal(false);
- expect(spy1).to.have.property('callCount', 0);
- expect(spy2).to.have.property('callCount', 0);
- });
- });
+ it('cancels rendering when Component returns false in shouldComponentUpdate', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ shouldComponentUpdate() {
+ spy('shouldComponentUpdate');
+ return false;
+ }
- wrap()
- .withOverride(() => getAdapter(), 'displayNameOfNode', () => undefined)
- .describe('.name()', () => {
- describe('node with displayName', () => {
- it('returns the displayName of the node', () => {
- class Foo extends React.Component {
- render() { return
; }
+ componentWillUpdate() {
+ spy('componentWillUpdate');
}
- class Wrapper extends React.Component {
- render() { return ; }
+ componentDidUpdate() {
+ spy('componentDidUpdate');
}
- Foo.displayName = 'CustomWrapper';
+ render() {
+ spy('render');
+ return foo
;
+ }
+ }
+ Foo.contextTypes = {
+ foo: PropTypes.string,
+ };
+ const wrapper = shallow(
+ ,
+ {
+ context: { foo: 'bar' },
+ },
+ );
+ wrapper.setContext({ foo: 'baz' });
+ expect(spy.args).to.deep.equal([['render'], ['shouldComponentUpdate']]);
+ });
- const wrapper = shallow( );
- expect(wrapper.name()).to.equal('CustomWrapper');
- });
+ itIf(BATCHING, 'provokes an another render to call setState twice in componentWillUpdate', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.updated = false;
+ this.state = {
+ count: 0,
+ };
+ }
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('returns the name of the node', () => {
- function SFC() {
- return
;
+ componentWillUpdate() {
+ if (!this.updated) {
+ this.updated = true;
+ this.setState({ count: this.state.count + 1 });
+ this.setState({ count: this.state.count + 1 });
}
- const Wrapper = () => ;
+ }
- SFC.displayName = 'CustomWrapper';
+ render() {
+ spy();
+ return {this.state.name}
;
+ }
+ }
+ const result = shallow(
+ ,
+ {
+ context: { foo: 'bar' },
+ },
+ );
+ expect(spy).to.have.property('callCount', 1);
+ result.setContext({ foo: 'baz' });
+ expect(spy).to.have.property('callCount', 3);
+ expect(result.state('count')).to.equal(1);
+ });
- const wrapper = shallow( );
- expect(wrapper.name()).to.equal('CustomWrapper');
- });
- });
+ itIf(BATCHING, 'provokes an another render to call setState twice in componentDidUpdate', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.updated = false;
+ this.state = {
+ count: 0,
+ };
+ }
- describe('createClass', () => {
- it('returns the name of the node', () => {
- const Foo = createClass({
- displayName: 'CustomWrapper',
- render() {
- return
;
- },
- });
- const Wrapper = createClass({
- render() {
- return ;
- },
- });
+ componentDidUpdate() {
+ if (!this.updated) {
+ this.updated = true;
+ /* eslint-disable react/no-did-update-set-state */
+ this.setState({ count: this.state.count + 1 });
+ this.setState({ count: this.state.count + 1 });
+ /* eslint-enable react/no-did-update-set-state */
+ }
+ }
- const wrapper = shallow( );
- expect(wrapper.name()).to.equal('CustomWrapper');
- });
- });
+ render() {
+ spy();
+ return {this.state.name}
;
+ }
+ }
+ const result = shallow(
+ ,
+ {
+ context: { foo: 'bar' },
+ },
+ );
+ expect(spy).to.have.property('callCount', 1);
+ result.setContext({ foo: 'baz' });
+ expect(spy).to.have.property('callCount', 3);
+ expect(result.state('count')).to.equal(1);
+ });
+ });
- wrap()
- .withOverride(() => getAdapter(), 'displayNameOfNode', () => sinon.stub())
- .describe('adapter has `displayNameOfNode`', () => {
- it('delegates to the adapter’s `displayNameOfNode`', () => {
- class Foo extends React.Component {
- render() { return
; }
- }
- const stub = getAdapter().displayNameOfNode;
- const sentinel = {};
- stub.returns(sentinel);
+ context('unmounting phase', () => {
+ it('calls componentWillUnmount', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ componentWillUnmount() {
+ spy();
+ }
- const wrapper = shallow( );
+ render() {
+ return foo
;
+ }
+ }
+ const wrapper = shallow( );
+ wrapper.unmount();
+ expect(spy).to.have.property('callCount', 1);
+ });
+ });
- expect(wrapper.name()).to.equal(sentinel);
+ context('component instance', () => {
+ it('calls `componentDidUpdate` when component’s `setState` is called', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ foo: 'init',
+ };
+ }
- expect(stub).to.have.property('callCount', 1);
- const { args } = stub.firstCall;
- expect(args).to.eql([wrapper.getNodeInternal()]);
- });
- });
- });
+ componentDidUpdate() {}
- describe('node without displayName', () => {
- it('returns the name of the node', () => {
- class Foo extends React.Component {
- render() { return
; }
+ onChange() {
+ // enzyme can't handle the update because `this` is a ReactComponent instance,
+ // not a ShallowWrapper instance.
+ this.setState({ foo: 'onChange update' });
}
- class Wrapper extends React.Component {
- render() { return ; }
+ render() {
+ return {this.state.foo}
;
}
+ }
+ const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
- const wrapper = shallow( );
- expect(wrapper.name()).to.equal('Foo');
- });
+ 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);
+ });
- describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
- it('returns the name of the node', () => {
- function SFC() {
- return
;
- }
- const Wrapper = () => ;
+ 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);
+ }
- const wrapper = shallow( );
- expect(wrapper.name()).to.equal('SFC');
- });
- });
- });
+ componentDidUpdate() {}
- describe('DOM node', () => {
- it('returns the name of the node', () => {
- const wrapper = shallow(
);
- expect(wrapper.name()).to.equal('div');
- });
- });
- });
+ onChange() {
+ // enzyme can't handle the update because `this` is a ReactComponent instance,
+ // not a ShallowWrapper instance.
+ this.setState({ foo: 'onChange update' });
+ }
- describe('.dive()', () => {
- class RendersDOM extends React.Component {
- render() {
- return
;
- }
- }
- class RendersNull extends React.Component {
- render() {
- return null;
- }
- }
- class RendersMultiple extends React.Component {
- render() {
- return (
-
-
-
-
- );
- }
- }
- class RendersZero extends React.Component {
- render() {
- return
;
- }
- }
- class WrapsRendersDOM extends React.Component {
- render() {
- return ;
- }
- }
- WrapsRendersDOM.contextTypes = { foo: PropTypes.string };
- class DoubleWrapsRendersDOM extends React.Component {
- render() {
- return ;
- }
- }
- class ContextWrapsRendersDOM extends React.Component {
- render() {
- return ;
- }
- }
- ContextWrapsRendersDOM.contextTypes = { foo: PropTypes.string };
+ render() {
+ return (
+
+ {this.state.foo}
+ click
+
+ );
+ }
+ }
+ 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('throws on a DOM node', () => {
- const wrapper = shallow( );
- expect(wrapper.is('div')).to.equal(true);
+ 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' });
+ }
- expect(() => { wrapper.dive(); }).to.throw(
- TypeError,
- 'ShallowWrapper::dive() can not be called on Host Components',
- );
- });
+ componentDidMount() {
+ this.update();
+ }
- it('throws on a non-component', () => {
- const wrapper = shallow( );
- expect(wrapper.type()).to.equal(null);
+ componentDidUpdate() {}
- expect(() => { wrapper.dive(); }).to.throw(
- TypeError,
- 'ShallowWrapper::dive() can only be called on components',
- );
- });
+ render() {
+ return {this.state.foo}
;
+ }
+ }
+ const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
- it('throws on multiple children found', () => {
- const wrapper = shallow( ).find('div').children();
- expect(() => { wrapper.dive(); }).to.throw(
- Error,
- 'Method “dive” is meant to be run on 1 node. 2 found instead.',
- );
- });
+ const wrapper = shallow( );
+ expect(spy).to.have.property('callCount', 1);
+ expect(wrapper.state('foo')).to.equal('update');
+ });
- it('throws on zero children found', () => {
- const wrapper = shallow( ).find('div').children();
- expect(() => { wrapper.dive(); }).to.throw(
- Error,
- 'Method “dive” is meant to be run on 1 node. 0 found instead.',
- );
- });
+ 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',
+ };
+ }
- it('throws on zero children found', () => {
- const wrapper = shallow( ).find('div').children();
- expect(() => { wrapper.dive(); }).to.throw(
- Error,
- 'Method “dive” is meant to be run on 1 node. 0 found instead.',
- );
- });
+ componentDidMount() {}
- it('dives + shallow-renders when there is one component child', () => {
- const wrapper = shallow( );
- expect(wrapper.is(WrapsRendersDOM)).to.equal(true);
+ render() {
+ return (
+
+ this.setState({ foo: 'update2' })}>
+ click
+
+ {this.state.foo}
+
+ );
+ }
+ }
+ const spy = sinon.spy(Foo.prototype, 'componentDidMount');
- const underwater = wrapper.dive();
- expect(underwater.is(RendersDOM)).to.equal(true);
+ const wrapper = shallow( );
+ expect(spy).to.have.property('callCount', 1);
+ wrapper.find('button').prop('onClick')();
+ expect(spy).to.have.property('callCount', 1);
+ });
});
- describeIf(is('>=16.3.0'), 'forwardRef Elements', () => {
- const ForwardRefWrapsRendersDOM = forwardRef && forwardRef(() => );
- const NestedForwarRefsWrapsRendersDom = forwardRef && forwardRef(() => );
+ describeIf(is('>= 15.3'), 'PureComponent', () => {
+ it('does not update when state and props did not change', () => {
+ class Foo extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ foo: 'init',
+ };
+ }
- if (forwardRef) {
- NestedForwarRefsWrapsRendersDom.contextTypes = { foo: PropTypes.string };
- ForwardRefWrapsRendersDOM.contextTypes = { foo: PropTypes.string };
- }
+ componentDidUpdate() {}
- it('dives + shallow-renders a forwardRef component', () => {
- const wrapper = shallow( );
- expect(wrapper.is(WrapsRendersDOM)).to.equal(true);
+ render() {
+ return (
+
+ {this.state.foo}
+
+ );
+ }
+ }
+ const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
+ const wrapper = shallow( );
+ wrapper.setState({ foo: 'update' });
+ expect(spy).to.have.property('callCount', 1);
+ wrapper.setState({ foo: 'update' });
+ expect(spy).to.have.property('callCount', 1);
- const underwater = wrapper.dive();
- expect(underwater.is(RendersDOM)).to.equal(true);
+ wrapper.setProps({ id: 2 });
+ expect(spy).to.have.property('callCount', 2);
+ wrapper.setProps({ id: 2 });
+ expect(spy).to.have.property('callCount', 2);
});
- it('dives + shallow-renders a with nested forwardRefs component', () => {
- const wrapper = shallow( );
- expect(wrapper.is(ForwardRefWrapsRendersDOM)).to.equal(true);
+ class Test extends PureComponent {
+ constructor(...args) {
+ super(...args);
- const underwater = wrapper.dive();
- expect(underwater.is(WrapsRendersDOM)).to.equal(true);
- });
- });
+ this.state = { a: { b: { c: 1 } } };
+ }
- it('merges and pass options through', () => {
- const wrapper = shallow( , { context: { foo: 'hello' } });
- expect(wrapper.context()).to.deep.equal({ foo: 'hello' });
+ componentDidUpdate() {
+ const { onUpdate } = this.props;
+ onUpdate();
+ }
- let underwater = wrapper.dive();
- expect(underwater.context()).to.deep.equal({ foo: 'hello' });
+ setDeepEqualState() {
+ this.setState({ a: { b: { c: 1 } } });
+ }
- underwater = wrapper.dive({ context: { foo: 'enzyme!' } });
- expect(underwater.context()).to.deep.equal({ foo: 'enzyme!' });
- });
- });
+ setDeepDifferentState() {
+ this.setState({ a: { b: { c: 2 } } });
+ }
- describeIf(!!ITERATOR_SYMBOL, '@@iterator', () => {
- it('is iterable', () => {
- class Foo extends React.Component {
render() {
- return (
-
- );
+ const { a: { b: { c } } } = this.state;
+ return {c}
;
}
}
- const wrapper = shallow( );
- const [a, b, c, d] = wrapper.find('a');
- const a1 = wrapper.find('a').get(0);
- const b1 = wrapper.find('a').get(1);
- const c1 = wrapper.find('a').get(2);
- const d1 = wrapper.find('a').get(3);
- expect(a1).to.deep.equal(a);
- expect(b1).to.deep.equal(b);
- expect(c1).to.deep.equal(c);
- expect(d1).to.deep.equal(d);
- });
- it('returns an iterable iterator', () => {
- class Foo extends React.Component {
- render() {
- return (
-
- );
+ it('rerenders on setState when new state is !==, but deeply equal to existing state', () => {
+ const updateSpy = sinon.spy();
+ const wrapper = shallow( );
+ wrapper.instance().setDeepEqualState();
+ expect(updateSpy).to.have.property('callCount', 1);
+ });
+
+ it('rerenders when setState is called with an object that doesnt have deep equality', () => {
+ const updateSpy = sinon.spy();
+ const wrapper = shallow( );
+ wrapper.instance().setDeepDifferentState();
+ expect(updateSpy).to.have.property('callCount', 1);
+ });
+
+ describeIf(is('>= 16.3'), 'setProps calls `componentDidUpdate` when `getDerivedStateFromProps` is defined', () => {
+ class DummyComp extends PureComponent {
+ constructor(...args) {
+ super(...args);
+ this.state = { state: -1 };
+ }
+
+ static getDerivedStateFromProps({ changeState, counter }) {
+ return changeState ? { state: counter * 10 } : null;
+ }
+
+ componentDidUpdate() {}
+
+ render() {
+ const { counter } = this.props;
+ const { state } = this.state;
+ return (
+
+ {counter}
+ {state}
+
+ );
+ }
}
- }
- const wrapper = shallow( );
- const iter = wrapper[ITERATOR_SYMBOL]();
- expect(iter).to.have.property(ITERATOR_SYMBOL).and.be.a('function');
- expect(iter[ITERATOR_SYMBOL]()).to.equal(iter);
- });
- });
+ let cDU;
+ let gDSFP;
+
+ beforeEach(() => { // eslint-disable-line mocha/no-sibling-hooks
+ cDU = sinon.spy(DummyComp.prototype, 'componentDidUpdate');
+ gDSFP = sinon.spy(DummyComp, 'getDerivedStateFromProps');
+ });
+
+ it('with no state changes, calls both methods with a sync and async setProps', () => {
+ const wrapper = shallow( );
+
+ expect(gDSFP).to.have.property('callCount', 1);
+ const [firstCall] = gDSFP.args;
+ expect(firstCall).to.eql([{
+ changeState: false,
+ counter: 0,
+ }, {
+ state: -1,
+ }]);
+ expect(wrapper.state()).to.eql({ state: -1 });
+
+ wrapper.setProps({ counter: 1 });
+
+ expect(cDU).to.have.property('callCount', 1);
+ expect(gDSFP).to.have.property('callCount', 2);
+ const [, secondCall] = gDSFP.args;
+ expect(secondCall).to.eql([{
+ changeState: false,
+ counter: 1,
+ }, {
+ state: -1,
+ }]);
+ expect(wrapper.state()).to.eql({ state: -1 });
+
+ return new Promise((resolve) => {
+ wrapper.setProps({ counter: 2 }, resolve);
+ }).then(() => {
+ expect(cDU).to.have.property('callCount', 2);
+ expect(gDSFP).to.have.property('callCount', 3);
+ const [, , thirdCall] = gDSFP.args;
+ expect(thirdCall).to.eql([{
+ changeState: false,
+ counter: 2,
+ }, {
+ state: -1,
+ }]);
+ expect(wrapper.state()).to.eql({ state: -1 });
+ });
+ });
- describe('.instance()', () => {
- it('returns the component instance', () => {
- class Foo extends React.Component {
- render() { return
; }
- }
+ it('with a state changes, calls both methods with a sync and async setProps', () => {
+ const wrapper = shallow( );
- const wrapper = shallow( );
- expect(wrapper.instance()).to.be.instanceof(Foo);
- expect(wrapper.instance().render).to.equal(Foo.prototype.render);
- });
+ expect(cDU).to.have.property('callCount', 0);
+ expect(gDSFP).to.have.property('callCount', 1);
+ const [firstCall] = gDSFP.args;
+ expect(firstCall).to.eql([{
+ changeState: true,
+ counter: 0,
+ }, {
+ state: -1,
+ }]);
+ expect(wrapper.state()).to.eql({ state: 0 });
- it('throws if called on something other than the root node', () => {
- class Foo extends React.Component {
- render() { return ; }
- }
+ wrapper.setProps({ counter: 1 });
- const wrapper = shallow( );
- const div = wrapper.find('div');
+ expect(cDU).to.have.property('callCount', 1);
+ expect(gDSFP).to.have.property('callCount', 2);
+ const [, secondCall] = gDSFP.args;
+ expect(secondCall).to.eql([{
+ changeState: true,
+ counter: 1,
+ }, {
+ state: 0,
+ }]);
+ expect(wrapper.state()).to.eql({ state: 10 });
- expect(() => div.instance()).to.throw(
- Error,
- 'ShallowWrapper::instance() can only be called on the root',
- );
+ return new Promise((resolve) => {
+ wrapper.setProps({ counter: 2 }, resolve);
+ }).then(() => {
+ expect(cDU).to.have.property('callCount', 2);
+ expect(gDSFP).to.have.property('callCount', 3);
+ const [, , thirdCall] = gDSFP.args;
+ expect(thirdCall).to.eql([{
+ changeState: true,
+ counter: 2,
+ }, {
+ state: 10,
+ }]);
+ expect(wrapper.state()).to.eql({ state: 20 });
+ });
+ });
+ });
});
- });
- describe('.getElement()', () => {
- it('returns nodes with refs as well', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.setRef = this.setRef.bind(this);
- this.node = null;
- }
+ describe('Own PureComponent implementation', () => {
+ it('does not update when state and props did not change', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ foo: 'init',
+ };
+ }
- setRef(node) {
- this.node = node;
- }
+ shouldComponentUpdate(nextProps, nextState) {
+ return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
+ }
- render() {
- return (
-
- );
- }
- }
- const wrapper = shallow( );
- const mockNode = { mock: true };
- wrapper.find('.foo').getElement().ref(mockNode);
- expect(wrapper.instance().node).to.equal(mockNode);
- });
+ componentDidUpdate() {}
- itIf(is('>= 16.3'), 'returns nodes with createRefs as well', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.setRef = createRef();
+ render() {
+ return (
+
+ {this.state.foo}
+
+ );
+ }
}
+ const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
+ const wrapper = shallow( );
+ wrapper.setState({ foo: 'update' });
+ expect(spy).to.have.property('callCount', 1);
+ wrapper.setState({ foo: 'update' });
+ expect(spy).to.have.property('callCount', 1);
- render() {
- return (
-
- );
- }
- }
- const wrapper = shallow( );
- // shallow rendering does not invoke refs
- expect(wrapper.instance().setRef).to.have.property('current', null);
+ wrapper.setProps({ id: 2 });
+ expect(spy).to.have.property('callCount', 2);
+ wrapper.setProps({ id: 2 });
+ expect(spy).to.have.property('callCount', 2);
+ });
});
- it('does not add a "null" key to elements with a ref and no key', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.setRef = this.setRef.bind(this);
- }
-
- setRef(node) {
- this.node = node;
- }
+ describeIf(is('>= 16.3'), 'support getSnapshotBeforeUpdate', () => {
+ it('calls getSnapshotBeforeUpdate and pass snapshot to componentDidUpdate', () => {
+ const spy = sinon.spy();
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ foo: 'bar',
+ };
+ }
- render() {
- return (
-
- );
- }
- }
- const wrapper = shallow( );
- expect(wrapper.getElement()).to.have.property('key', null);
- });
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ spy('componentDidUpdate', prevProps, this.props, prevState, this.state, snapshot);
+ }
- itIf(is('>= 16.3'), 'does not add a "null" key to elements with a createRef and no key', () => {
- class Foo extends React.Component {
- constructor(props) {
- super(props);
- this.setRef = createRef();
- }
+ getSnapshotBeforeUpdate(prevProps, prevState) {
+ spy('getSnapshotBeforeUpdate', prevProps, this.props, prevState, this.state);
+ return { snapshot: 'ok' };
+ }
- render() {
- return (
-
- );
+ render() {
+ spy('render');
+ return foo
;
+ }
}
- }
- const wrapper = shallow( );
- expect(wrapper.getElement()).to.have.property('key', null);
+ const wrapper = shallow( );
+ spy.resetHistory();
+ wrapper.setProps({ name: 'bar' });
+ expect(spy.args).to.deep.equal([
+ ['render'],
+ ['getSnapshotBeforeUpdate', { name: 'foo' }, { name: 'bar' }, { foo: 'bar' }, { foo: 'bar' }],
+ ['componentDidUpdate', { name: 'foo' }, { name: 'bar' }, { foo: 'bar' }, { foo: 'bar' }, { snapshot: 'ok' }],
+ ]);
+ spy.resetHistory();
+ wrapper.setState({ foo: 'baz' });
+ expect(spy.args).to.deep.equal([
+ ['render'],
+ ['getSnapshotBeforeUpdate', { name: 'bar' }, { name: 'bar' }, { foo: 'bar' }, { foo: 'baz' }],
+ ['componentDidUpdate', { name: 'bar' }, { name: 'bar' }, { foo: 'bar' }, { foo: 'baz' }, { snapshot: 'ok' }],
+ ]);
+ });
});
});
- describe('.getElements()', () => {
- it('returns the wrapped elements', () => {
- const one = ;
- const two = ;
+ it('works with class components that return null', () => {
+ class Foo extends React.Component {
+ render() {
+ return null;
+ }
+ }
+ const wrapper = shallow( );
+ expect(wrapper).to.have.lengthOf(1);
+ expect(wrapper.html()).to.equal(null);
+ expect(wrapper.type()).to.equal(null);
+ const rendered = wrapper.render();
+ expect(rendered).to.have.lengthOf(0);
+ expect(rendered.html()).to.equal(null);
+ });
- class Test extends React.Component {
- render() {
- return (
-
- { one }
- { two }
-
- );
- }
+ itIf(is('>= 16'), 'works with class components that return arrays', () => {
+ class Foo extends React.Component {
+ render() {
+ return [
,
];
}
+ }
+ const wrapper = shallow( );
+ expect(wrapper).to.have.lengthOf(2);
+ expect(wrapper.find('div')).to.have.lengthOf(2);
+ });
- const wrapper = shallow( );
- expect(wrapper.find('span').getElements()).to.deep.equal([one, two]);
- });
+ itIf(is('>=15 || ^16.0.0-alpha'), 'works with SFCs that return null', () => {
+ const Foo = () => null;
+
+ const wrapper = shallow( );
+ expect(wrapper).to.have.lengthOf(1);
+ expect(wrapper.html()).to.equal(null);
+ expect(wrapper.type()).to.equal(null);
+ const rendered = wrapper.render();
+ expect(rendered).to.have.lengthOf(0);
+ expect(rendered.html()).to.equal(null);
});
describe('out-of-band state updates', () => {
@@ -8841,49 +3296,6 @@ describe('shallow', () => {
});
});
- describe('#single()', () => {
- it('throws if run on multiple nodes', () => {
- const wrapper = shallow(
).children();
- expect(wrapper).to.have.lengthOf(2);
- expect(() => wrapper.single('name!')).to.throw(
- Error,
- 'Method “name!” is meant to be run on 1 node. 2 found instead.',
- );
- });
-
- it('throws if run on zero nodes', () => {
- const wrapper = shallow(
).children();
- expect(wrapper).to.have.lengthOf(0);
- expect(() => wrapper.single('name!')).to.throw(
- Error,
- 'Method “name!” is meant to be run on 1 node. 0 found instead.',
- );
- });
-
- it('throws if run on zero nodes', () => {
- const wrapper = shallow(
).children();
- expect(wrapper).to.have.lengthOf(0);
- expect(() => wrapper.single('name!')).to.throw(
- Error,
- 'Method “name!” is meant to be run on 1 node. 0 found instead.',
- );
- });
-
- it('works with a name', () => {
- const wrapper = shallow(
);
- wrapper.single('foo', (node) => {
- expect(node).to.equal(wrapper[sym('__node__')]);
- });
- });
-
- it('works without a name', () => {
- const wrapper = shallow(
);
- wrapper.single((node) => {
- expect(node).to.equal(wrapper[sym('__node__')]);
- });
- });
- });
-
describe('setState through a props method', () => {
class Child extends React.Component {
render() {
@@ -8971,35 +3383,6 @@ describe('shallow', () => {
});
});
- describe('#wrap()', () => {
- class Foo extends React.Component {
- render() {
- return (
-
- );
- }
- }
-
- it('returns itself when it is already a ShallowWrapper', () => {
- const wrapperDiv = shallow(
);
- const wrapperFoo = shallow( );
- expect(wrapperDiv.wrap(wrapperFoo)).to.equal(wrapperFoo);
- expect(wrapperFoo.wrap(wrapperDiv)).to.equal(wrapperDiv);
- });
-
- it('wraps when it is not already a ShallowWrapper', () => {
- const wrapper = shallow( );
- const el = wrapper.find('a').at(1);
- const wrappedEl = wrapper.wrap(el.getElement());
- expect(wrappedEl).to.be.instanceOf(ShallowWrapper);
- expect(wrappedEl.props()).to.eql(el.props());
- expect(wrappedEl.shallow().debug()).to.equal(el.debug());
- });
- });
-
describe('cloning elements', () => {
class Foo extends React.Component {
render() {
@@ -9055,20 +3438,6 @@ describe('shallow', () => {
});
});
- describe('.root()', () => {
- it('returns the root DOM node', () => {
- class Fixture extends React.Component {
- render() {
- return
;
- }
- }
- const wrapper = shallow( );
- const root = wrapper.root();
- expect(root.is('div')).to.equal(true);
- 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 {
diff --git a/packages/enzyme-test-suite/test/_helpers/describeMethods.js b/packages/enzyme-test-suite/test/_helpers/describeMethods.js
new file mode 100644
index 000000000..8ff1eba34
--- /dev/null
+++ b/packages/enzyme-test-suite/test/_helpers/describeMethods.js
@@ -0,0 +1,24 @@
+export default function describeMethods({
+ Wrap,
+ Wrapper,
+}, ...methods) {
+ const WrapperName = Wrapper.name;
+ const isShallow = WrapperName === 'ShallowWrapper';
+ const isMount = WrapperName === 'ReactWrapper';
+ const hasDOM = isMount;
+ const makeDOMElement = () => (hasDOM ? global.document.createElement('div') : { nodeType: 1 });
+
+ methods.forEach((method) => {
+ // eslint-disable-next-line global-require, import/no-dynamic-require
+ require(`../shared/methods/${method}`).default({
+ Wrap,
+ WrapRendered: isShallow ? Wrap : (...args) => Wrap(...args).children(),
+ Wrapper,
+ WrapperName,
+ isShallow,
+ isMount,
+ hasDOM,
+ makeDOMElement,
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/_helpers/selectors.js b/packages/enzyme-test-suite/test/_helpers/selectors.js
new file mode 100644
index 000000000..a16b80c39
--- /dev/null
+++ b/packages/enzyme-test-suite/test/_helpers/selectors.js
@@ -0,0 +1,3 @@
+export const getElementPropSelector = prop => x => x.props[prop];
+
+export const getWrapperPropSelector = prop => x => x.prop(prop);
diff --git a/packages/enzyme-test-suite/test/shared/methods/@@iterator.jsx b/packages/enzyme-test-suite/test/shared/methods/@@iterator.jsx
new file mode 100644
index 000000000..94c31a1b0
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/@@iterator.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ ITERATOR_SYMBOL,
+} from 'enzyme/build/Utils';
+
+import {
+ describeIf,
+} from '../../_helpers';
+
+export default function describeIterator({
+ Wrap,
+}) {
+ describeIf(!!ITERATOR_SYMBOL, '@@iterator', () => {
+ it('is iterable', () => {
+ class Foo extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+ const [a, b, c, d, ...e] = wrapper.find('a');
+ const a1 = wrapper.find('a').get(0);
+ const b1 = wrapper.find('a').get(1);
+ const c1 = wrapper.find('a').get(2);
+ const d1 = wrapper.find('a').get(3);
+ expect(a1).to.deep.equal(a);
+ expect(b1).to.deep.equal(b);
+ expect(c1).to.deep.equal(c);
+ expect(d1).to.deep.equal(d);
+ expect(e).to.eql([]);
+ });
+
+ it('returns an iterable iterator', () => {
+ class Foo extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+
+ const iter = wrapper[ITERATOR_SYMBOL]();
+ expect(iter).to.have.property(ITERATOR_SYMBOL).and.be.a('function');
+ expect(iter[ITERATOR_SYMBOL]()).to.equal(iter);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/_method.template b/packages/enzyme-test-suite/test/shared/methods/_method.template
new file mode 100644
index 000000000..bd4d8b1cc
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/_method.template
@@ -0,0 +1,42 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { expect } from 'chai';
+import wrap from 'mocha-wrap';
+import sinon from 'sinon-sandbox';
+import { Portal } from 'react-is';
+
+import { render } from 'enzyme';
+import getAdapter from 'enzyme/build/getAdapter';
+import {
+ ITERATOR_SYMBOL,
+ sym,
+} from 'enzyme/build/Utils';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import realArrowFunction from '../../_helpers/realArrowFunction';
+import { getElementPropSelector, getWrapperPropSelector } from '../../_helpers/selectors';
+import {
+ is,
+ REACT16,
+} from '../../_helpers/version';
+
+import {
+ createClass,
+ createPortal,
+ createRef,
+ Fragment,
+} from '../../_helpers/react-compat';
+
+export default function describe$Method({
+ Wrap,
+ WrapRendered,
+ Wrapper,
+ WrapperName,
+ isShallow,
+ isMount,
+ makeDOMElement,
+}) {
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/at.jsx b/packages/enzyme-test-suite/test/shared/methods/at.jsx
new file mode 100644
index 000000000..3f3d7aedb
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/at.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeAt({
+ Wrap,
+}) {
+ describe('.at(index)', () => {
+ it('gets a wrapper of the node at the specified index', () => {
+ const wrapper = Wrap((
+
+ ));
+ const bar = wrapper.find('.bar');
+ expect(bar.at(0).hasClass('foo')).to.equal(true);
+ expect(bar.at(1).hasClass('bax')).to.equal(true);
+ expect(bar.at(2).hasClass('bux')).to.equal(true);
+ expect(bar.at(3).hasClass('baz')).to.equal(true);
+ });
+
+ it('`.at()` does not affect the results of `.exists()`', () => {
+ const wrapper = Wrap((
+
+ ));
+ const bar = wrapper.find('.bar');
+ expect(bar.exists()).to.equal(false);
+ expect(bar.at(0).exists()).to.equal(false);
+
+ const foo = wrapper.find('.foo');
+ expect(foo.exists()).to.equal(true);
+ expect(foo.at(0).exists()).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/childAt.jsx b/packages/enzyme-test-suite/test/shared/methods/childAt.jsx
new file mode 100644
index 000000000..e54dda70f
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/childAt.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeChildAt({
+ Wrap,
+}) {
+ describe('.childAt(index)', () => {
+ it('gets a wrapped node at the specified index', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.childAt(0).hasClass('bar')).to.equal(true);
+ expect(wrapper.childAt(1).hasClass('baz')).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/children.jsx b/packages/enzyme-test-suite/test/shared/methods/children.jsx
new file mode 100644
index 000000000..dc32de08d
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/children.jsx
@@ -0,0 +1,212 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+export default function describeChildren({
+ Wrap,
+ WrapRendered,
+ isShallow,
+}) {
+ describe('.children([selector])', () => {
+ it('returns empty wrapper for node with no children', () => {
+ const wrapper = Wrap(
);
+ expect(wrapper.children()).to.have.lengthOf(0);
+ });
+
+ it('does not attempt to get an instance for text nodes', () => {
+ const wrapper = WrapRendered(B C
);
+ expect(wrapper).to.have.lengthOf(1);
+ });
+
+ it('skips the falsy children', () => {
+ const wrapper = Wrap((
+
+
+ {false}
+ {[false, false]}
+
foo
+
+
+ {undefined}
+ {[undefined, undefined]}
+
bar
+
+
+ {null}
+ {[null, null]}
+
baz
+
+
+ ));
+ expect(wrapper.childAt(0).children()).to.have.lengthOf(1);
+ expect(wrapper.childAt(1).children()).to.have.lengthOf(1);
+ expect(wrapper.childAt(2).children()).to.have.lengthOf(1);
+ });
+
+ it('returns the children nodes of the root', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.children()).to.have.lengthOf(3);
+ expect(wrapper.children().at(0).hasClass('foo')).to.equal(true);
+ expect(wrapper.children().at(1).hasClass('bar')).to.equal(true);
+ expect(wrapper.children().at(2).hasClass('baz')).to.equal(true);
+ });
+
+ it('does not return any of the children of children', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.children()).to.have.lengthOf(2);
+ expect(wrapper.children().at(0).hasClass('foo')).to.equal(true);
+ expect(wrapper.children().at(1).hasClass('baz')).to.equal(true);
+ });
+
+ it('handles mixed children with and without arrays', () => {
+ class Foo extends React.Component {
+ render() {
+ const { items } = this.props;
+ return (
+
+
+ {items.map(x => x)}
+
+ );
+ }
+ }
+ const wrapper = WrapRendered((
+ abc,
+ def ,
+ ]}
+ />
+ ));
+ expect(wrapper.children()).to.have.lengthOf(3);
+ expect(wrapper.children().at(0).hasClass('foo')).to.equal(true);
+ expect(wrapper.children().at(1).hasClass('bar')).to.equal(true);
+ expect(wrapper.children().at(2).hasClass('baz')).to.equal(true);
+ });
+
+ it('optionally allows a selector to filter by', () => {
+ const wrapper = Wrap((
+
+ ));
+ const children = wrapper.children('.bip');
+ expect(children).to.have.lengthOf(2);
+ expect(children.at(0).hasClass('bar')).to.equal(true);
+ expect(children.at(1).hasClass('baz')).to.equal(true);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('handles mixed children with and without arrays', () => {
+ const Foo = ({ items }) => (
+
+
+ {items.map(x => x)}
+
+ );
+
+ const wrapper = WrapRendered((
+ abc,
+ def ,
+ ]}
+ />
+ ));
+ expect(wrapper.children()).to.have.lengthOf(3);
+ expect(wrapper.children().at(0).hasClass('foo')).to.equal(true);
+ expect(wrapper.children().at(1).hasClass('bar')).to.equal(true);
+ expect(wrapper.children().at(2).hasClass('baz')).to.equal(true);
+ });
+ });
+
+ it('returns duplicates untouched', () => {
+ class Foo extends React.Component {
+ render() {
+ const foo = 'Foo';
+ return (
+
+ {foo} Bar {foo} Bar {foo}
+
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+ const children = wrapper.children();
+ const textNodes = children.map(x => x.text());
+ const expectedShallowNodes = ['Foo', ' Bar ', 'Foo', ' Bar ', 'Foo'];
+ const expectedTextNodes = isShallow ? expectedShallowNodes : [expectedShallowNodes.join('')];
+ expect(textNodes).to.eql(expectedTextNodes);
+ });
+
+ it('renders children separated by spaces', () => {
+ class JustificationRow extends React.Component {
+ render() {
+ const { children } = this.props;
+ const wrappedChildren = React.Children.map(
+ children,
+ child => child && {child} ,
+ );
+
+ const justifiedChildren = [];
+ React.Children.forEach(wrappedChildren, (child) => {
+ if (child) {
+ justifiedChildren.push(child, ' ');
+ }
+ });
+ justifiedChildren.pop();
+
+ return {justifiedChildren}
;
+ }
+ }
+
+ const wrapper = WrapRendered((
+
+ foo
+ bar
+ baz
+
+ ));
+
+ expect(wrapper.children().map(x => x.debug())).to.eql([
+ `
+
+ foo
+
+ `,
+ ...(isShallow ? [' '] : []),
+ `
+
+ bar
+
+ `,
+ ...(isShallow ? [' '] : []),
+ `
+
+ baz
+
+ `,
+ ]);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/closest.jsx b/packages/enzyme-test-suite/test/shared/methods/closest.jsx
new file mode 100644
index 000000000..a958e4cae
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/closest.jsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeClosest({
+ Wrap,
+}) {
+ describe('.closest(selector)', () => {
+ it('returns the closest ancestor for a given selector', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const closestFoo = wrapper.find('.bar').closest('.foo');
+ expect(closestFoo).to.have.lengthOf(1);
+ expect(closestFoo.hasClass('baz')).to.equal(true);
+ });
+
+ it('only ever returns a wrapper of a single node', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('.baz').parent().hasClass('bar')).to.equal(true);
+ });
+
+ it('returns itself if matching', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('.bux').closest('.baz').hasClass('bux')).to.equal(true);
+ });
+
+ it('does not find a nonexistent match', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('.fooooo')).to.have.lengthOf(0);
+
+ const bar = wrapper.find('.bar');
+ expect(bar).to.have.lengthOf(1);
+
+ expect(bar.closest('.fooooo')).to.have.lengthOf(0);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/contains.jsx b/packages/enzyme-test-suite/test/shared/methods/contains.jsx
new file mode 100644
index 000000000..7500557d2
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/contains.jsx
@@ -0,0 +1,151 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+export default function describeContains({
+ Wrap,
+ WrapperName,
+ isMount,
+}) {
+ describe('.contains(node)', () => {
+ it('allows matches on the root node', () => {
+ const a =
;
+ const b =
;
+ const c =
;
+ const wrapper = Wrap(a);
+ expect(wrapper.contains(b)).to.equal(true);
+ expect(wrapper.contains(c)).to.equal(false);
+ });
+
+ it('allows matches on a nested node', () => {
+ const wrapper = Wrap((
+
+ ));
+ const b =
;
+ expect(wrapper.contains(b)).to.equal(true);
+ });
+
+ it('matches composite components', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+ const wrapper = Wrap((
+
+
+
+ ));
+ const b = ;
+ expect(wrapper.contains(b)).to.equal(true);
+ });
+
+ it('works with strings', () => {
+ const wrapper = Wrap(foo
);
+
+ expect(wrapper.contains('foo')).to.equal(true);
+ expect(wrapper.contains('bar')).to.equal(false);
+ });
+
+ it('works with numbers', () => {
+ const wrapper = Wrap({1}
);
+
+ expect(wrapper.contains(1)).to.equal(true);
+ expect(wrapper.contains(2)).to.equal(false);
+ expect(wrapper.contains('1')).to.equal(false);
+ });
+
+ it('works with nested strings & numbers', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.contains('foo')).to.equal(true);
+ expect(wrapper.contains(foo
)).to.equal(true);
+
+ expect(wrapper.contains(5)).to.equal(true);
+ expect(wrapper.contains({5}
)).to.equal(true);
+ });
+
+ it('does something with arrays of nodes', () => {
+ const wrapper = Wrap((
+
+
Hello
+
Goodbye
+
More
+
+ ));
+ const fails = [
+ wrong ,
+ Goodbye
,
+ ];
+
+ const passes1 = [
+ Hello ,
+ Goodbye
,
+ ];
+ const passes2 = [
+ Goodbye
,
+ More ,
+ ];
+
+ expect(wrapper.contains(fails)).to.equal(false);
+ expect(wrapper.contains(passes1)).to.equal(true);
+ expect(wrapper.contains(passes2)).to.equal(true);
+ });
+
+ // FIXME: fix on mount
+ itIf(!isMount, 'throws on invalid argument', () => {
+ const wrapper = Wrap(
);
+
+ expect(() => wrapper.contains({})).to.throw(
+ Error,
+ `${WrapperName}::contains() can only be called with a ReactElement (or an array of them), a string, or a number as an argument.`,
+ );
+ expect(() => wrapper.contains(() => ({}))).to.throw(
+ Error,
+ `${WrapperName}::contains() can only be called with a ReactElement (or an array of them), a string, or a number as an argument.`,
+ );
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('matches composite components', () => {
+ function Foo() {
+ return
;
+ }
+
+ const wrapper = Wrap((
+
+
+
+ ));
+ const b = ;
+ expect(wrapper.contains(b)).to.equal(true);
+ });
+
+ it('matches composite components if rendered by function', () => {
+ function Foo() {
+ return
;
+ }
+ const renderStatelessComponent = () => ;
+ const wrapper = Wrap((
+
+ {renderStatelessComponent()}
+
+ ));
+ const b = ;
+ expect(wrapper.contains(b)).to.equal(true);
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/containsAllMatchingElements.jsx b/packages/enzyme-test-suite/test/shared/methods/containsAllMatchingElements.jsx
new file mode 100644
index 000000000..26a826fd2
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/containsAllMatchingElements.jsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeContainsAllMatchingElements({
+ Wrap,
+}) {
+ describe('.containsAllMatchingElements(nodes)', () => {
+ it('throws TypeError if non-array passed in', () => {
+ const wrapper = Wrap((
+
+ Hello
+
+ ));
+
+ expect(() => wrapper.containsAllMatchingElements((
+
+ Hello
+
+ ))).to.throw(TypeError, 'nodes should be an Array');
+ });
+
+ it('matches on array of nodes that each look like rendered nodes, with nested elements', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.containsAllMatchingElements([
+ Hello
,
+ Goodbye
,
+ ])).to.equal(true);
+ });
+
+ it('matches on an array of nodes that all look like one of the rendered nodes', () => {
+ const spy1 = sinon.spy();
+ const spy2 = sinon.spy();
+ const wrapper = Wrap((
+
+
Hello World
+
Goodbye World
+
+ ));
+ expect(wrapper.containsAllMatchingElements([
+ Hello World
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAllMatchingElements([
+ Hello World
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAllMatchingElements([
+ Hello World
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAllMatchingElements([
+ Hello World
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAllMatchingElements([
+ Hello World
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAllMatchingElements([
+ Hello World
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAllMatchingElements([
+ Hello World
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAllMatchingElements([
+ Hello World
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(spy1).to.have.property('callCount', 0);
+ expect(spy2).to.have.property('callCount', 0);
+ });
+
+ it('does not match on nodes that do not all look like one of the rendered nodes', () => {
+ const spy1 = sinon.spy();
+ const spy2 = sinon.spy();
+ const wrapper = Wrap((
+
+
Hello World
+
Goodbye World
+
+ ));
+ expect(wrapper.containsAllMatchingElements([
+ Hello World
,
+ Bonjour le monde
,
+ Goodbye World
,
+ ])).to.equal(false);
+ expect(spy1).to.have.property('callCount', 0);
+ expect(spy2).to.have.property('callCount', 0);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/containsAnyMatchingElements.jsx b/packages/enzyme-test-suite/test/shared/methods/containsAnyMatchingElements.jsx
new file mode 100644
index 000000000..b6ca653ea
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/containsAnyMatchingElements.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeContainsAnyMatchingElements({
+ Wrap,
+}) {
+ describe('.containsAnyMatchingElements(nodes)', () => {
+ it('matches on an array with at least one node that looks like a rendered node', () => {
+ const spy1 = sinon.spy();
+ const spy2 = sinon.spy();
+ const wrapper = Wrap((
+
+
Hello World
+
Goodbye World
+
+ ));
+ expect(wrapper.containsAnyMatchingElements([
+ Bonjour le monde
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAnyMatchingElements([
+ Bonjour le monde
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAnyMatchingElements([
+ Bonjour le monde
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAnyMatchingElements([
+ Bonjour le monde
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAnyMatchingElements([
+ Bonjour le monde
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAnyMatchingElements([
+ Bonjour le monde
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAnyMatchingElements([
+ Bonjour le monde
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(wrapper.containsAnyMatchingElements([
+ Bonjour le monde
,
+ Goodbye World
,
+ ])).to.equal(true);
+ expect(spy1).to.have.property('callCount', 0);
+ expect(spy2).to.have.property('callCount', 0);
+ });
+
+ it('does not match on an array with no nodes that look like a rendered node', () => {
+ const spy1 = sinon.spy();
+ const spy2 = sinon.spy();
+ const wrapper = Wrap((
+
+
Hello World
+
Goodbye World
+
+ ));
+ expect(wrapper.containsAnyMatchingElements([
+ Bonjour le monde
,
+ Au revoir le monde
,
+ ])).to.equal(false);
+ expect(spy1).to.have.property('callCount', 0);
+ expect(spy2).to.have.property('callCount', 0);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/containsMatchingElement.jsx b/packages/enzyme-test-suite/test/shared/methods/containsMatchingElement.jsx
new file mode 100644
index 000000000..6e2c6912b
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/containsMatchingElement.jsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeContainsMatchingElement({
+ Wrap,
+}) {
+ describe('.containsMatchingElement(node)', () => {
+ it('matches a root node that looks like the rendered one', () => {
+ const spy1 = sinon.spy();
+ const spy2 = sinon.spy();
+ const wrapper = Wrap((
+
+
Hello World
+
Goodbye World
+
+ ));
+ expect(wrapper.containsMatchingElement((
+
+
Hello World
+
Goodbye World
+
+ ))).to.equal(true);
+ expect(wrapper.containsMatchingElement((
+
+
Hello World
+
Goodbye World
+
+ ))).to.equal(true);
+ expect(wrapper.containsMatchingElement((
+
+
Hello World
+
Goodbye World
+
+ ))).to.equal(true);
+ expect(wrapper.containsMatchingElement((
+
+
Hello World
+
Goodbye World
+
+ ))).to.equal(true);
+ expect(wrapper.containsMatchingElement((
+
+
Hello World
+
Goodbye World
+
+ ))).to.equal(true);
+ expect(wrapper.containsMatchingElement((
+
+
Hello World
+
Goodbye World
+
+ ))).to.equal(true);
+ expect(spy1).to.have.property('callCount', 0);
+ expect(spy2).to.have.property('callCount', 0);
+ });
+
+ it('matches on a single node that looks like a rendered one', () => {
+ const spy1 = sinon.spy();
+ const spy2 = sinon.spy();
+ const wrapper = Wrap((
+
+
Hello World
+
Goodbye World
+
+ ));
+ expect(wrapper.containsMatchingElement(Hello World
)).to.equal(true);
+ expect(wrapper.containsMatchingElement(Goodbye World
)).to.equal(true);
+ expect(wrapper.containsMatchingElement((
+ Hello World
+ ))).to.equal(true);
+ expect(wrapper.containsMatchingElement((
+ Hello World
+ ))).to.equal(true);
+ expect(wrapper.containsMatchingElement((
+ Goodbye World
+ ))).to.equal(true);
+ expect(wrapper.containsMatchingElement((
+ Goodbye World
+ ))).to.equal(true);
+ expect(spy1).to.have.property('callCount', 0);
+ expect(spy2).to.have.property('callCount', 0);
+ });
+
+ it('does not match on a single node that doesn\'t looks like a rendered one', () => {
+ const spy1 = sinon.spy();
+ const spy2 = sinon.spy();
+ const wrapper = Wrap((
+
+
Hello World
+
Goodbye World
+
+ ));
+ expect(wrapper.containsMatchingElement(Bonjour le monde
)).to.equal(false);
+ expect(wrapper.containsMatchingElement((
+ Au revoir le monde
+ ))).to.equal(false);
+ });
+
+ it('does not differentiate between absence, null, or undefined', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.containsMatchingElement(
)).to.equal(true);
+
+ expect(wrapper.containsMatchingElement(
)).to.equal(true);
+ expect(wrapper.containsMatchingElement(
)).to.equal(true);
+ expect(wrapper.containsMatchingElement(
)).to.equal(true);
+
+ expect(wrapper.containsMatchingElement(
)).to.equal(true);
+ expect(wrapper.containsMatchingElement(
)).to.equal(true);
+ expect(wrapper.containsMatchingElement(
)).to.equal(true);
+
+ expect(wrapper.containsMatchingElement(
)).to.equal(true);
+ expect(wrapper.containsMatchingElement(
)).to.equal(true);
+ expect(wrapper.containsMatchingElement(
)).to.equal(true);
+ });
+
+ it('works with leading and trailing spaces', () => {
+ const wrapper = Wrap((
+
+ All Operations
+
+ ));
+
+ expect(wrapper.containsMatchingElement( All Operations )).to.equal(true);
+ });
+
+ it('works with leading and trailing newlines', () => {
+ const wrapper = Wrap((
+
+
+ All Operations
+
+
+ ));
+
+ expect(wrapper.containsMatchingElement( All Operations )).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/context.jsx b/packages/enzyme-test-suite/test/shared/methods/context.jsx
new file mode 100644
index 000000000..055544962
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/context.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { expect } from 'chai';
+
+import {
+ itIf,
+} from '../../_helpers';
+import {
+ is,
+} from '../../_helpers/version';
+
+import {
+ createClass,
+} from '../../_helpers/react-compat';
+
+export default function describeContext({
+ Wrap,
+ WrapperName,
+}) {
+ describe('.context()', () => {
+ const contextTypes = {
+ name: PropTypes.string,
+ };
+ const SimpleComponent = createClass({
+ contextTypes,
+ render() {
+ const { name } = this.context;
+ return {name}
;
+ },
+ });
+
+ function SimpleComponentSFC(props, { name }) {
+ return {name}
;
+ }
+ SimpleComponentSFC.contextTypes = contextTypes;
+
+ it('throws when not called on the root', () => {
+ const context = { name: };
+ const wrapper = Wrap( , { context });
+ const main = wrapper.find('main');
+ expect(main).to.have.lengthOf(1);
+ expect(() => main.context()).to.throw(
+ Error,
+ `${WrapperName}::context() can only be called on the root`,
+ );
+ });
+
+ it('throws if it is called when wrapper didn’t include context', () => {
+ const wrapper = Wrap( );
+ expect(() => wrapper.context()).to.throw(
+ Error,
+ `${WrapperName}::context() can only be called on a wrapper that was originally passed a context option`,
+ );
+ });
+
+ itIf(is('>= 16'), 'throws on SFCs that lack an instance', () => {
+ const context = { name: 'bob' };
+ const wrapper = Wrap( , { context });
+ expect(() => wrapper.context()).to.throw(
+ Error,
+ `${WrapperName}::context() can only be called on wrapped nodes that have a non-null instance`,
+ );
+ });
+
+ it('works with no arguments', () => {
+ const context = { name: {} };
+ const wrapper = Wrap( , { context });
+ expect(wrapper.context()).to.eql(context);
+ });
+
+ it('works with a key name', () => {
+ const context = { name: {} };
+ const wrapper = Wrap( , { context });
+ expect(wrapper.context('name')).to.equal(context.name);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/debug.jsx b/packages/enzyme-test-suite/test/shared/methods/debug.jsx
new file mode 100644
index 000000000..433953001
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/debug.jsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import { debugNodes } from 'enzyme/build/Debug';
+
+import {
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+import {
+ createClass,
+} from '../../_helpers/react-compat';
+
+export default function describeDebug({
+ Wrap,
+ isShallow,
+}) {
+ describe('.debug()', () => {
+ context('passes through to the debugNodes function', () => {
+ it('with wrapping an HTML element', () => {
+ const wrapper = Wrap(
);
+
+ expect(wrapper.debug()).to.equal('
');
+ expect(wrapper.debug()).to.equal(debugNodes(wrapper.getNodesInternal()));
+ });
+
+ it('with wrapping a createClass component', () => {
+ const Foo = createClass({
+ displayName: 'Bar',
+ render() { return
; },
+ });
+ const wrapper = Wrap( );
+
+ const expectedDebug = isShallow
+ ? '
'
+ : `
+
+ `;
+ expect(wrapper.debug()).to.equal(expectedDebug);
+ expect(wrapper.debug()).to.equal(debugNodes(wrapper.getNodesInternal()));
+ });
+
+ it('with wrapping a class component', () => {
+ class Foo extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ const wrapper = Wrap( );
+
+ const expectedDebug = isShallow
+ ? '
'
+ : `
+
+ `;
+ expect(wrapper.debug()).to.equal(expectedDebug);
+ expect(wrapper.debug()).to.equal(debugNodes(wrapper.getNodesInternal()));
+ });
+
+ itIf(is('> 0.13'), 'with wrapping a stateless function component (SFC)', () => {
+ const wrapper = Wrap(
);
+
+ expect(wrapper.debug()).to.equal('
');
+ expect(wrapper.debug()).to.equal(debugNodes(wrapper.getNodesInternal()));
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/equals.jsx b/packages/enzyme-test-suite/test/shared/methods/equals.jsx
new file mode 100644
index 000000000..a7eff0ca8
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/equals.jsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+export default function describeEquals({
+ Wrap,
+ WrapRendered,
+}) {
+ describe('.equals(node)', () => {
+ it('allows matches on the root node', () => {
+ const a =
;
+ const b =
;
+ const c =
;
+
+ expect(Wrap(a).equals(b)).to.equal(true);
+ expect(Wrap(a).equals(c)).to.equal(false);
+ });
+
+ it('does NOT allow matches on a nested node', () => {
+ const wrapper = Wrap((
+
+ ));
+ const b =
;
+ expect(wrapper.equals(b)).to.equal(false);
+ });
+
+ it('matches composite components', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+ const wrapper = Wrap((
+
+
+
+ ));
+ const b =
;
+ expect(wrapper.equals(b)).to.equal(true);
+ });
+
+ it('does not expand `node` content', () => {
+ class Bar extends React.Component {
+ render() { return
; }
+ }
+
+ class Foo extends React.Component {
+ render() { return ; }
+ }
+
+ const wrapper = WrapRendered( );
+ expect(wrapper.equals( )).to.equal(true);
+ expect(wrapper.equals( )).to.equal(false);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('matches composite SFCs', () => {
+ const Foo = () => (
+
+ );
+
+ const wrapper = Wrap((
+
+
+
+ ));
+ const b =
;
+ expect(wrapper.equals(b)).to.equal(true);
+ });
+
+ it('does not expand `node` content', () => {
+ const Bar = () => (
+
+ );
+
+ const Foo = () => (
+
+ );
+
+ const wrapper = WrapRendered( );
+ expect(wrapper.equals( )).to.equal(true);
+ expect(wrapper.equals( )).to.equal(false);
+ });
+ });
+
+ it('flattens arrays of children to compare', () => {
+ class TwoChildren extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ class TwoChildrenOneArrayed extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+ const twoChildren = WrapRendered( );
+ const twoChildrenOneArrayed = WrapRendered( );
+
+ expect(twoChildren.equals(twoChildrenOneArrayed.getElement())).to.equal(true);
+ expect(twoChildrenOneArrayed.equals(twoChildren.getElement())).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/every.jsx b/packages/enzyme-test-suite/test/shared/methods/every.jsx
new file mode 100644
index 000000000..79fbe86f8
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/every.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeEvery({
+ Wrap,
+}) {
+ describe('.every(selector)', () => {
+ it('returns if every node matches a selector', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.find('.foo').every('.foo')).to.equal(true);
+ expect(wrapper.find('.foo').every('.qoo')).to.equal(false);
+ expect(wrapper.find('.foo').every('.bar')).to.equal(false);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/everyWhere.jsx b/packages/enzyme-test-suite/test/shared/methods/everyWhere.jsx
new file mode 100644
index 000000000..f65dffbb7
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/everyWhere.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeEveryWhere({
+ Wrap,
+}) {
+ describe('.everyWhere(predicate)', () => {
+ it('returns if every node matches a predicate', () => {
+ const wrapper = Wrap((
+
+ ));
+ const foo = wrapper.find('.foo');
+ expect(foo.everyWhere(n => n.hasClass('foo'))).to.equal(true);
+ expect(foo.everyWhere(n => n.hasClass('qoo'))).to.equal(false);
+ expect(foo.everyWhere(n => n.hasClass('bar'))).to.equal(false);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/exists.jsx b/packages/enzyme-test-suite/test/shared/methods/exists.jsx
new file mode 100644
index 000000000..29b4d0ad5
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/exists.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeExists({
+ Wrap,
+ Wrapper,
+}) {
+ describe('.exists()', () => {
+ it('has no required arguments', () => {
+ expect(Wrapper.prototype.exists).to.have.lengthOf(0);
+ });
+
+ describe('without argument', () => {
+ it('returns true if node exists in wrapper', () => {
+ const wrapper = Wrap(
);
+ expect(wrapper.find('.bar').exists()).to.equal(false);
+ expect(wrapper.find('.foo').exists()).to.equal(true);
+ });
+ });
+ describe('with argument', () => {
+ it('throws on invalid EnzymeSelector', () => {
+ const wrapper = Wrap(
);
+
+ expect(() => wrapper.exists(null)).to.throw(TypeError);
+ expect(() => wrapper.exists(undefined)).to.throw(TypeError);
+ expect(() => wrapper.exists(45)).to.throw(TypeError);
+ expect(() => wrapper.exists({})).to.throw(TypeError);
+ });
+
+ it('returns .find(arg).exists() instead', () => {
+ const wrapper = Wrap(
);
+ const fakeFindExistsReturnVal = { toString() { return 'fake .find(arg).exists() return value'; } };
+ const fakeSelector = '.someClass';
+ wrapper.find = sinon.stub().returns({ exists() { return fakeFindExistsReturnVal; } });
+ const existsResult = wrapper.exists(fakeSelector);
+ expect(wrapper.find).to.have.property('callCount', 1);
+ expect(wrapper.find.firstCall.args[0]).to.equal(fakeSelector);
+ expect(existsResult).to.equal(fakeFindExistsReturnVal);
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/filter.jsx b/packages/enzyme-test-suite/test/shared/methods/filter.jsx
new file mode 100644
index 000000000..6395934ce
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/filter.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeFilter({
+ Wrap,
+}) {
+ describe('.filter(selector)', () => {
+ it('returns a new wrapper of just the nodes that matched the selector', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('.foo').filter('.bar')).to.have.lengthOf(3);
+ expect(wrapper.find('.bar').filter('.foo')).to.have.lengthOf(3);
+ expect(wrapper.find('.bar').filter('.bax')).to.have.lengthOf(0);
+ expect(wrapper.find('.foo').filter('.baz.bar')).to.have.lengthOf(2);
+ });
+
+ it('only looks in the current wrappers nodes, not their children', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('.foo').filter('.bar')).to.have.lengthOf(1);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/filterWhere.jsx b/packages/enzyme-test-suite/test/shared/methods/filterWhere.jsx
new file mode 100644
index 000000000..e97e6b0a4
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/filterWhere.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeFilterWhere({
+ Wrap,
+ Wrapper,
+}) {
+ describe('.filterWhere(predicate)', () => {
+ it('filters only the nodes of the wrapper', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const stub = sinon.stub();
+ stub.onCall(0).returns(false);
+ stub.onCall(1).returns(true);
+ stub.onCall(2).returns(false);
+
+ const baz = wrapper.find('.foo').filterWhere(stub);
+ expect(baz).to.have.lengthOf(1);
+ expect(baz.hasClass('baz')).to.equal(true);
+ });
+
+ it('calls the predicate with the wrapped node as the first argument', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const stub = sinon.stub();
+ stub.returns(true);
+ const spy = sinon.spy(stub);
+ wrapper.find('.foo').filterWhere(spy);
+ expect(spy).to.have.property('callCount', 3);
+ expect(spy.args[0][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[1][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[2][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[0][0].hasClass('bar')).to.equal(true);
+ expect(spy.args[1][0].hasClass('baz')).to.equal(true);
+ expect(spy.args[2][0].hasClass('bux')).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/find.jsx b/packages/enzyme-test-suite/test/shared/methods/find.jsx
new file mode 100644
index 000000000..c511f2973
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/find.jsx
@@ -0,0 +1,933 @@
+import React from 'react';
+import { expect } from 'chai';
+import wrap from 'mocha-wrap';
+import getData from 'html-element-map/getData';
+
+import getAdapter from 'enzyme/build/getAdapter';
+
+import {
+ describeIf,
+ describeWithDOM,
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+import {
+ createPortal,
+ Fragment,
+ forwardRef,
+ memo,
+} from '../../_helpers/react-compat';
+
+export default function describeFind({
+ Wrap,
+ WrapRendered,
+ isShallow,
+ isMount,
+ hasDOM,
+ makeDOMElement,
+}) {
+ describe('.find(selector)', () => {
+ it('matches the root DOM element', () => {
+ const wrapper = Wrap(hello
);
+ expect(wrapper.find('#ttt')).to.have.lengthOf(1);
+ expect(wrapper.find('.ttt')).to.have.lengthOf(1);
+ });
+
+ it('finds an element based on a class name', () => {
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(wrapper.find('.foo').type()).to.equal('input');
+ });
+
+ it('finds an SVG element based on a class name', () => {
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(wrapper.find('.foo').type()).to.equal('svg');
+ });
+
+ it('finds an element that has dot in attribute', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const elements = wrapper.find('[data-baz="foo.bar"]');
+ expect(elements).to.have.lengthOf(1);
+ });
+
+ it('finds an element based on a tag name', () => {
+ const wrapper = Wrap((
+
+
+ Button
+
+
+
+ ));
+ expect(wrapper.find('input').props()).to.eql({ className: 'foo' });
+ expect(wrapper.find('button').props()).to.eql({
+ className: 'bar',
+ children: 'Button',
+ type: 'button',
+ });
+ expect(wrapper.find('textarea').props()).to.eql({ className: 'magic' });
+ expect(wrapper.find('select').props()).to.eql({ className: 'reality' });
+ });
+
+ it('finds an element based on a tag name and class name', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.find('input.foo')).to.have.lengthOf(1);
+ });
+
+ it('finds an element based on a tag name and id', () => {
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(wrapper.find('input#foo')).to.have.lengthOf(1);
+ });
+
+ it('finds an element based on a tag name, id, and class name', () => {
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(wrapper.find('input#foo.bar')).to.have.lengthOf(1);
+ });
+
+ it('finds an element that with class and attribute', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const elements = wrapper.find('.classBar[data-baz="bar"]');
+ expect(elements).to.have.lengthOf(1);
+ });
+
+ it('finds an element that with multiple classes and one attribute', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const elements = wrapper.find('.classBar.classFoo[data-baz="bar"]');
+ expect(elements).to.have.lengthOf(1);
+ });
+
+ it('finds an element that with class and class with hyphen', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const elements = wrapper.find('.classBar.class-Foo');
+ expect(elements).to.have.lengthOf(1);
+ });
+
+ it('finds a component based on a constructor', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(wrapper.find(Foo).type()).to.equal(Foo);
+ });
+
+ wrap()
+ .withOverride(() => getAdapter(), 'isValidElementType', () => () => false)
+ .it('throws when an adapter’s `isValidElementType` lies', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+ const wrapper = Wrap((
+
+
+
+ ));
+
+ expect(() => wrapper.find(Foo)).to.throw(
+ TypeError,
+ 'Enzyme::Selector expects a string, object, or valid element type (Component Constructor)',
+ );
+ });
+
+ it('finds a component based on a component function name', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(wrapper.find('Foo').type()).to.equal(Foo);
+ });
+
+ it('finds a component based on a component displayName', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+ Foo.displayName = 'Bar';
+
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(wrapper.find('Bar').type()).to.equal(Foo);
+ });
+
+ it('finds multiple elements based on a class name', () => {
+ const wrapper = Wrap((
+
+
+
+
+ ));
+ expect(wrapper.find('.foo')).to.have.lengthOf(2);
+ });
+
+ it('finds multiple elements based on a tag name', () => {
+ const wrapper = Wrap((
+
+
+
+
+
+ ));
+ expect(wrapper.find('input')).to.have.lengthOf(2);
+ expect(wrapper.find('button')).to.have.lengthOf(1);
+ });
+
+ it('finds multiple elements based on a constructor', () => {
+ class Foo extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ class Bar extends React.Component {
+ render() {
+ return
;
+ }
+ }
+
+ const wrapper = Wrap((
+
+
+
+
+
+ ));
+ expect(wrapper.find(Foo)).to.have.lengthOf(2);
+ expect(wrapper.find(Bar)).to.have.lengthOf(1);
+ });
+
+ it('finds multiple elements based on a string prop', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('[htmlFor="foo"]')).to.have.lengthOf(1);
+ expect(wrapper.find('[htmlFor]')).to.have.lengthOf(2);
+ expect(wrapper.find('[title="foo"]')).to.have.lengthOf(1);
+ expect(wrapper.find('[title]')).to.have.lengthOf(1);
+ });
+
+ it('finds multiple elements with multiple matching react props', () => {
+ function noop() {}
+ const wrapper = Wrap((
+
+
+
+ ));
+
+ expect(wrapper.find('span[htmlFor="foo"][onChange]')).to.have.lengthOf(1);
+ expect(wrapper.find('span[htmlFor="foo"][preserveAspectRatio="xMaxYMax"]')).to.have.lengthOf(1);
+ expect(wrapper.find('[htmlFor][preserveAspectRatio]')).to.have.lengthOf(1);
+ });
+
+ it('works on non-single nodes', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.find('.a')).to.have.lengthOf(1);
+ expect(wrapper.find('.b')).to.have.lengthOf(2);
+ expect(wrapper.find('.b').find('.c')).to.have.lengthOf(6);
+ });
+
+ it('works with an adjacent sibling selector', () => {
+ const a = 'some';
+ const b = 'text';
+ const wrapper = Wrap((
+
+
+ {a}
+ {b}
+
+
+ {a}
+ {b}
+
+
+ ));
+ expect(wrapper.find('.row')).to.have.lengthOf(2);
+ expect(wrapper.find('.row + .row')).to.have.lengthOf(1);
+ });
+
+ it('throws for non-numeric attribute values without quotes', () => {
+ const wrapper = Wrap((
+
+
+
+
+
+ ));
+ expect(() => wrapper.find('[type=text]')).to.throw(
+ Error,
+ 'Failed to parse selector: [type=text]',
+ );
+ expect(() => wrapper.find('[type=hidden]')).to.throw(
+ Error,
+ 'Failed to parse selector: [type=hidden]',
+ );
+ expect(() => wrapper.find('[type="text"]')).to.not.throw(
+ Error,
+ 'Failed to parse selector: [type="text"]',
+ );
+ });
+
+ it('errors sensibly if any of the search props are undefined', () => {
+ const wrapper = Wrap((
+
+
+
+ ));
+
+ expect(() => wrapper.find({ type: undefined })).to.throw(
+ TypeError,
+ 'Enzyme::Props can’t have `undefined` values. Try using ‘findWhere()’ instead.',
+ );
+ });
+
+ it('does not find property when undefined', () => {
+ const wrapper = Wrap((
+
+
+
+ ));
+
+ expect(wrapper.find('[data-foo]')).to.have.lengthOf(0);
+ });
+
+ it('compounds tag and prop selector', () => {
+ const wrapper = Wrap((
+
+
+
+
+ ));
+
+ expect(wrapper.find('span[preserveAspectRatio="xMaxYMax"]')).to.have.lengthOf(1);
+ expect(wrapper.find('span[preserveAspectRatio]')).to.have.lengthOf(1);
+
+ expect(wrapper.find('span[htmlFor="foo"]')).to.have.lengthOf(1);
+ expect(wrapper.find('span[htmlFor]')).to.have.lengthOf(1);
+ });
+
+ it('supports data prop selectors', () => {
+ const wrapper = Wrap((
+
+
+
+
+
+
+ ));
+
+ expect(wrapper.find('[data-foo="bar"]')).to.have.lengthOf(1);
+ expect(wrapper.find('[data-foo]')).to.have.lengthOf(1);
+
+ expect(wrapper.find('[data-foo-123]')).to.have.lengthOf(1);
+ expect(wrapper.find('[data-foo-123="bar2"]')).to.have.lengthOf(1);
+
+ expect(wrapper.find('[data-123-foo]')).to.have.lengthOf(1);
+ expect(wrapper.find('[data-123-foo="bar3"]')).to.have.lengthOf(1);
+
+ expect(wrapper.find('[data-foo_bar]')).to.have.lengthOf(1);
+ expect(wrapper.find('[data-foo_bar="bar4"]')).to.have.lengthOf(1);
+ });
+
+ it('supports boolean and numeric values for matching props', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('span[value=1]')).to.have.lengthOf(1);
+ expect(wrapper.find('span[value=2]')).to.have.lengthOf(0);
+ expect(wrapper.find('a[value=false]')).to.have.lengthOf(1);
+ expect(wrapper.find('a[value=true]')).to.have.lengthOf(0);
+ });
+
+ it('does not find key or ref via property selector', () => {
+ const arrayOfComponents = [
,
];
+
+ class Foo extends React.Component {
+ render() {
+ return (
+
+
+ {arrayOfComponents}
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+
+ expect(wrapper.find('div[ref="foo"]')).to.have.lengthOf(0);
+ expect(wrapper.find('div[key="1"]')).to.have.lengthOf(0);
+ expect(wrapper.find('[ref]')).to.have.lengthOf(0);
+ expect(wrapper.find('[key]')).to.have.lengthOf(0);
+ });
+
+ it('supports object property selectors', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.find({ a: 1 })).to.have.lengthOf(0);
+ expect(wrapper.find({ 'data-test': 'ref' })).to.have.lengthOf(7);
+ expect(wrapper.find({ className: 'foo' })).to.have.lengthOf(1);
+ expect(wrapper.find({ 'data-prop': null })).to.have.lengthOf(1);
+ expect(wrapper.find({ 'data-prop': 123 })).to.have.lengthOf(1);
+ expect(wrapper.find({ 'data-prop': false })).to.have.lengthOf(1);
+ expect(wrapper.find({ 'data-prop': true })).to.have.lengthOf(1);
+ });
+
+ it('supports complex and nested object property selectors', () => {
+ const testFunction = () => ({});
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.find({ 'data-test': 'ref' })).to.have.lengthOf(4);
+ expect(wrapper.find({ 'data-more': { a: 1 } })).to.have.lengthOf(0);
+ expect(wrapper.find({ 'data-more': [{ id: 1 }] })).to.have.lengthOf(2);
+ expect(wrapper.find({ 'data-more': { item: { id: 1 } } })).to.have.lengthOf(1);
+ expect(wrapper.find({ 'data-more': { height: 20 } })).to.have.lengthOf(1);
+ expect(wrapper.find({
+ 'data-more': [{ id: 1 }],
+ 'data-test': 'ref',
+ 'data-prop': true,
+ onChange: testFunction,
+ })).to.have.lengthOf(1);
+ });
+
+ it('throws when given empty object, null, or an array', () => {
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(() => wrapper.find({})).to.throw(
+ TypeError,
+ 'Enzyme::Selector does not support an array, null, or empty object as a selector',
+ );
+ expect(() => wrapper.find([])).to.throw(
+ TypeError,
+ 'Enzyme::Selector does not support an array, null, or empty object as a selector',
+ );
+ expect(() => wrapper.find(null)).to.throw(
+ TypeError,
+ 'Enzyme::Selector does not support an array, null, or empty object as a selector',
+ );
+ });
+
+ it('queries attributes with spaces in their values', () => {
+ const wrapper = Wrap((
+
+
Hello
+ World
+
+ ));
+ expect(wrapper.find('[data-foo]')).to.have.lengthOf(2);
+ expect(wrapper.find('[data-foo="foo bar"]')).to.have.lengthOf(1);
+ expect(wrapper.find('[data-foo="bar baz quz"]')).to.have.lengthOf(1);
+ expect(wrapper.find('[data-foo="bar baz"]')).to.have.lengthOf(0);
+ expect(wrapper.find('[data-foo="foo bar"]')).to.have.lengthOf(0);
+ expect(wrapper.find('[data-foo="bar baz quz"]')).to.have.lengthOf(0);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('finds a component based on a constructor', () => {
+ const Foo = () => (
);
+
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(wrapper.find(Foo).type()).to.equal(Foo);
+ });
+
+ it('finds a component based on a function name', () => {
+ const Foo = () => (
);
+
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(wrapper.find('Foo').type()).to.equal(Foo);
+ });
+
+ it('finds a component based on a displayName', () => {
+ const Foo = () => (
);
+ Foo.displayName = 'Bar';
+
+ const wrapper = Wrap((
+
+
+
+ ));
+ expect(wrapper.find('Bar').type()).to.equal(Foo);
+ });
+
+ itIf(!isShallow, 'finds a stateless component based on a component displayName if rendered by function', () => {
+ const Foo = () =>
;
+ const renderStatelessComponent = () => ;
+ const wrapper = Wrap((
+
+ {renderStatelessComponent()}
+
+ ));
+ expect(wrapper.find('Foo').type()).to.equal(Foo);
+ });
+
+ it('does not find key via property selector', () => {
+ const arrayOfComponents = [
,
];
+
+ const Foo = () => (
+
+ {arrayOfComponents}
+
+ );
+
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.find('div[key="1"]')).to.have.lengthOf(0);
+ expect(wrapper.find('[key]')).to.have.lengthOf(0);
+ });
+ });
+
+ describe('works with attribute selectors containing #', () => {
+ let wrapper;
+ beforeEach(() => {
+ wrapper = Wrap((
+
+ ));
+ });
+
+ it('works with an ID', () => {
+ expect(wrapper.find('a#test')).to.have.lengthOf(1);
+ });
+
+ it('works with a normal attribute', () => {
+ expect(wrapper.find('a[href="/page"]')).to.have.lengthOf(1);
+ });
+
+ it('works with an attribute with a #', () => {
+ expect(wrapper.find('a[href="/page#anchor"]')).to.have.lengthOf(1);
+ });
+ });
+
+ describe('works with data- attributes', () => {
+ class Foo extends React.Component {
+ render() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ }
+
+ it('finds elements by data attribute', () => {
+ const wrapper = WrapRendered( );
+ expect(wrapper.html()).to.contain('data-custom-tag="bookIcon"'); // sanity check
+ const elements = wrapper.find('[data-custom-tag="bookIcon"]');
+ expect(elements).to.have.lengthOf(2);
+ expect(elements.filter('i')).to.have.lengthOf(2);
+ });
+ });
+
+ describeIf(is('>= 16.2'), 'works with Fragments', () => {
+ const NestedFragmentComponent = () => (
+
+
+ A span
+ B span
+ A div
+
+ C span
+
+
+
D span
+
+ );
+
+ it('finds descendant span inside React.Fragment', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.find('.container span')).to.have.lengthOf(4);
+ });
+
+ it('does not find nonexistent p inside React.Fragment', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.find('.container p')).to.have.lengthOf(0);
+ });
+
+ it('finds direct child span inside React.Fragment', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.find('.container > span')).to.have.lengthOf(4);
+ });
+
+ it('handles adjacent sibling selector inside React.Fragment', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.find('.container span + div')).to.have.lengthOf(1);
+ });
+
+ it('handles general sibling selector inside React.Fragment', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.find('.container div ~ span')).to.have.lengthOf(2);
+ });
+
+ // FIXME: investigate if/why mount requires the version range
+ itIf(!isMount || is('>= 16.4.1'), 'handles fragments with no content', () => {
+ const EmptyFragmentComponent = () => (
+
+
+
+
+
+ );
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.find('.container > span')).to.have.lengthOf(0);
+ expect(wrapper.find('.container span')).to.have.lengthOf(0);
+ expect(wrapper).to.have.lengthOf(1);
+ expect(wrapper.children()).to.have.lengthOf(0);
+ });
+ });
+
+ describeIf(is('>= 16'), 'works with Portals', () => {
+ it('finds portals by name', () => {
+ const containerDiv = makeDOMElement();
+ const Foo = () => (
+
+ {createPortal(
+
InPortal
,
+ containerDiv,
+ )}
+
+ );
+
+ const wrapper = Wrap( );
+
+ expect(wrapper.find('Portal')).to.have.lengthOf(1);
+ });
+
+ context('finding through portals', () => {
+ let containerDiv;
+ let FooPortal;
+ let wrapper;
+ beforeEach(() => {
+ containerDiv = makeDOMElement();
+ FooPortal = class FooPortalContainer extends React.Component {
+ render() {
+ const { children } = this.props;
+ return createPortal(children, containerDiv);
+ }
+ };
+ wrapper = Wrap((
+
+ Successful Portal!
+
+
+ ));
+ });
+
+ it('finds elements', () => {
+ expect(wrapper.find('h1')).to.have.lengthOf(1);
+ expect(wrapper.find('span')).to.have.lengthOf(1);
+ });
+
+ itIf(hasDOM, 'finds elements with qSA', () => {
+ expect(containerDiv.querySelectorAll('h1')).to.have.lengthOf(1);
+ });
+ });
+ });
+
+ describeIf(is('>= 16.3'), 'forwardRef', () => {
+ it('finds forwardRefs', () => {
+ const Component = forwardRef(() =>
);
+ class Foo extends React.Component {
+ render() {
+ return (
+
+
+
+
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+ expect(wrapper.find(Component)).to.have.lengthOf(2);
+ expect(wrapper.find('ForwardRef')).to.have.lengthOf(2);
+ });
+
+ it('finds forwardRef by custom display name', () => {
+ const Component = forwardRef(() =>
);
+ Component.displayName = 'CustomForwardRef';
+ class Foo extends React.Component {
+ render() {
+ return (
+
+
+
+
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+ expect(wrapper.find(Component)).to.have.lengthOf(2);
+ expect(wrapper.find(Component.displayName)).to.have.lengthOf(2);
+ });
+ });
+
+ describeWithDOM('find DOM elements by constructor', () => {
+ // in React 0.13 and 0.14, these HTML tags get moved around by the DOM, and React fails
+ // they're tested in `shallow`, and in React 15+, so we can skip them here.
+ const tagsWithRenderError = new Set([
+ 'body',
+ 'frame',
+ 'frameset',
+ 'head',
+ 'html',
+ 'caption',
+ 'td',
+ 'th',
+ 'tr',
+ 'col',
+ 'colgroup',
+ 'tbody',
+ 'thead',
+ 'tfoot',
+ ]);
+ function hasRenderError(Tag) {
+ return isMount && is('< 15') && tagsWithRenderError.has(Tag);
+ }
+
+ const { elements, all } = getData();
+
+ elements.filter(({ constructor: C }) => C && C !== all).forEach(({
+ tag: Tag,
+ constructorName: name,
+ }) => {
+ class Foo extends React.Component {
+ render() {
+ return ;
+ }
+ }
+
+ itIf(!hasRenderError(Tag), `${Tag}: found with \`${name}\``, () => {
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.type()).to.equal(Tag);
+ expect(wrapper.is(Tag)).to.equal(true);
+ expect(wrapper.filter(Tag)).to.have.lengthOf(1);
+ });
+ });
+ });
+
+ describeIf(is('>= 16.6'), 'React.memo', () => {
+ // FIXME: fix bug for mount
+ itIf(!isMount, 'works with an SFC', () => {
+ const InnerComp = () => Hello
;
+ const InnerFoo = ({ foo }) => (
+
+ );
+ const Foo = memo(InnerFoo);
+
+ const wrapper = Wrap( );
+ const expectedDebug = isShallow
+ ? ``
+ : `
+
+
+
+
+ Hello
+
+
+
+
+ bar
+
+
+ qux
+
+
+ `;
+ expect(wrapper.debug()).to.equal(expectedDebug);
+ expect(wrapper.find('InnerComp')).to.have.lengthOf(1);
+ expect(wrapper.find('.bar')).to.have.lengthOf(1);
+ expect(wrapper.find('.qoo').text()).to.equal('qux');
+ });
+
+ // TODO; reevaluate
+ itIf(isShallow, 'throws with a class component', () => {
+ class InnerComp extends React.Component {
+ render() {
+ return Hello
;
+ }
+ }
+
+ class Foo extends React.Component {
+ render() {
+ const { foo } = this.props;
+ return (
+
+ );
+ }
+ }
+ const FooMemo = memo(Foo);
+
+ expect(() => Wrap( )).to.throw(TypeError);
+ });
+
+ // FIXME: fix for shallow
+ itIf(!isShallow, 'works with a class component', () => {
+ class InnerComp extends React.Component {
+ render() {
+ return Hello
;
+ }
+ }
+
+ class Foo extends React.Component {
+ render() {
+ const { foo } = this.props;
+ return (
+
+ );
+ }
+ }
+ const FooMemo = memo(Foo);
+
+ const wrapper = Wrap( );
+ expect(wrapper.debug()).to.equal(`
+
+
+
+
+ Hello
+
+
+
+
+ bar
+
+
+ qux
+
+
+ `);
+ expect(wrapper.find('InnerComp')).to.have.lengthOf(1);
+ expect(wrapper.find('.bar')).to.have.lengthOf(1);
+ expect(wrapper.find('.qoo').text()).to.equal('qux');
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/findWhere.jsx b/packages/enzyme-test-suite/test/shared/methods/findWhere.jsx
new file mode 100644
index 000000000..92e628bf4
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/findWhere.jsx
@@ -0,0 +1,420 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+import { Portal } from 'react-is';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import realArrowFunction from '../../_helpers/realArrowFunction';
+import { is } from '../../_helpers/version';
+
+import {
+ createPortal,
+ Fragment,
+} from '../../_helpers/react-compat';
+
+export default function describeFindWhere({
+ Wrap,
+ WrapRendered,
+ Wrapper,
+ isShallow,
+ makeDOMElement,
+}) {
+ describe('.findWhere(predicate)', () => {
+ it('returns all elements for a truthy test', () => {
+ const wrapper = Wrap((
+
+
+
+
+ ));
+ expect(wrapper.findWhere(() => true)).to.have.lengthOf(3);
+ });
+
+ it('returns no elements for a falsy test', () => {
+ const wrapper = Wrap((
+
+
+
+
+ ));
+ expect(wrapper.findWhere(() => false)).to.have.lengthOf(0);
+ });
+
+ it('does not pass empty wrappers', () => {
+ class EditableText extends React.Component {
+ render() {
+ return {''}
;
+ }
+ }
+
+ const wrapper = WrapRendered( );
+
+ const stub = sinon.stub();
+ wrapper.findWhere(stub);
+ const passedNodeLengths = stub.getCalls().map(({ args: [firstArg] }) => firstArg.length);
+ expect(passedNodeLengths).to.eql([1]);
+ });
+
+ it('calls the predicate with the wrapped node as the first argument', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const stub = sinon.stub();
+ stub.returns(true);
+ const spy = sinon.spy(stub);
+ wrapper.findWhere(spy);
+ expect(spy).to.have.property('callCount', 4);
+ expect(spy.args[0][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[1][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[2][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[3][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[1][0].hasClass('bar')).to.equal(true);
+ expect(spy.args[2][0].hasClass('baz')).to.equal(true);
+ expect(spy.args[3][0].hasClass('bux')).to.equal(true);
+ });
+
+ it('finds nodes', () => {
+ class Foo extends React.Component {
+ render() {
+ const { selector } = this.props;
+ return (
+
+
+
+
+ );
+ }
+ }
+
+ const selector = 'blah';
+ const wrapper = Wrap( );
+ const foundSpan = wrapper.findWhere(n => (
+ n.type() === 'span' && n.props()['data-foo'] === selector
+ ));
+ expect(foundSpan.type()).to.equal('span');
+
+ const foundNotSpan = wrapper.findWhere(n => (
+ n.type() !== 'span' && n.props()['data-foo'] === selector
+ ));
+ expect(foundNotSpan.type()).to.equal('i');
+ });
+
+ describeIf(is('>= 16.2'), 'with fragments', () => {
+ it('finds nodes', () => {
+ class FragmentFoo extends React.Component {
+ render() {
+ const { selector } = this.props;
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ }
+
+ const selector = 'blah';
+ const wrapper = Wrap( );
+ const foundSpans = wrapper.findWhere(n => (
+ n.type() === 'span' && n.props()['data-foo'] === selector
+ ));
+ expect(foundSpans).to.have.lengthOf(2);
+ expect(foundSpans.get(0).type).to.equal('span');
+ expect(foundSpans.get(1).type).to.equal('span');
+
+ const foundNotSpans = wrapper.findWhere(n => (
+ n.type() !== 'span' && n.props()['data-foo'] === selector
+ ));
+ expect(foundNotSpans).to.have.lengthOf(2);
+ expect(foundNotSpans.get(0).type).to.equal('i');
+ expect(foundNotSpans.get(1).type).to.equal('i');
+ });
+ });
+
+ it('finds nodes when conditionally rendered', () => {
+ class Foo extends React.Component {
+ render() {
+ const { selector } = this.props;
+ return (
+
+
+ {selector === 'baz' ? : null}
+
+ );
+ }
+ }
+
+ const selector = 'blah';
+ const wrapper = Wrap( );
+ const foundSpan = wrapper.findWhere(n => (
+ n.type() === 'span' && n.props()['data-foo'] === selector
+ ));
+ expect(foundSpan.type()).to.equal('span');
+
+ const foundNotSpan = wrapper.findWhere(n => (
+ n.type() !== 'span' && n.props()['data-foo'] === selector
+ ));
+ expect(foundNotSpan).to.have.lengthOf(0);
+ });
+
+ it('does not get trapped when conditionally rendering using an empty string variable as the condition', () => {
+ const emptyString = '';
+
+ class Foo extends React.Component {
+ render() {
+ const { selector } = this.props;
+ return (
+
+ );
+ }
+ }
+
+ const selector = 'blah';
+ const wrapper = Wrap( );
+ const foundSpan = wrapper.findWhere(n => (
+ n.type() === 'span'
+ && n.props()['data-foo'] === selector
+ ));
+
+ expect(foundSpan.debug()).to.equal((
+ `
+ Test
+ `
+ ));
+ });
+
+ class HasDataFoo extends React.Component {
+ render() {
+ const { data } = this.props;
+ return (
+ Test Component
+ );
+ }
+ }
+
+ it('returns props object when props() is called', () => {
+ const content = 'blah';
+ const wrapper = WrapRendered( );
+ expect(wrapper.props()).to.deep.equal({ 'data-foo': content, children: 'Test Component' });
+ });
+
+ it('returns shallow rendered string when debug() is called', () => {
+ const content = 'blah';
+ const wrapper = Wrap( );
+ const expectedDebug = isShallow
+ ? `
+ Test Component
+
`
+ : `
+
+ Test Component
+
+ `;
+ expect(wrapper.debug()).to.equal(expectedDebug);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('finds nodes', () => {
+ const SFC = function SFC({ selector }) {
+ return (
+
+
+
+
+ );
+ };
+
+ const selector = 'blah';
+ const wrapper = Wrap( );
+ const foundSpan = wrapper.findWhere(n => (
+ n.type() === 'span' && n.props()['data-foo'] === selector
+ ));
+ expect(foundSpan.type()).to.equal('span');
+
+ const foundNotSpan = wrapper.findWhere(n => (
+ n.type() !== 'span' && n.props()['data-foo'] === selector
+ ));
+ expect(foundNotSpan.type()).to.equal('i');
+ });
+
+ it('finds nodes when conditionally rendered', () => {
+ const SFC = function SFC({ selector }) {
+ return (
+
+
+ {selector === 'baz' ? : null}
+
+ );
+ };
+
+ const selector = 'blah';
+ const wrapper = Wrap( );
+ const foundSpan = wrapper.findWhere(n => (
+ n.type() === 'span' && n.props()['data-foo'] === selector
+ ));
+ expect(foundSpan.type()).to.equal('span');
+
+ const foundNotSpan = wrapper.findWhere(n => (
+ n.type() !== 'span' && n.props()['data-foo'] === selector
+ ));
+ expect(foundNotSpan).to.have.lengthOf(0);
+ });
+
+ it('returns props object when props() is called', () => {
+ const SFC = function SFC({ data }) {
+ return (
+ Test SFC
+ );
+ };
+
+ const content = 'blah';
+ const wrapper = WrapRendered( );
+ expect(wrapper.props()).to.deep.equal({ 'data-foo': content, children: 'Test SFC' });
+ });
+
+ it('returns shallow rendered string when debug() is called', () => {
+ const SFC = function SFC({ data }) {
+ return (
+ Test SFC
+ );
+ };
+
+ const content = 'blah';
+ const wrapper = Wrap( );
+ const expectedDebug = isShallow
+ ? `
+ Test SFC
+
`
+ : `
+
+ Test SFC
+
+ `;
+ expect(wrapper.debug()).to.equal(expectedDebug);
+ });
+
+ context('works with a nested SFC', () => {
+ const Bar = realArrowFunction(Hello
);
+ class Foo extends React.Component {
+ render() { return ; }
+ }
+
+ itIf(isShallow, 'works in shallow', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.is(Bar)).to.equal(true);
+ expect(wrapper.dive().text()).to.equal('Hello');
+ });
+
+ itIf(!isShallow, 'works in non-shallow', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.text()).to.equal('Hello');
+ });
+ });
+ });
+
+ it('allows `.text()` to be called on text nodes', () => {
+ const wrapper = Wrap((
+
+
+ foo bar
+ {null}
+ {false}
+
+ ));
+
+ const stub = sinon.stub();
+ wrapper.findWhere(stub);
+
+ const passedNodes = stub.getCalls().map(({ args: [firstArg] }) => firstArg);
+
+ const textContents = passedNodes.map(n => [n.debug(), n.text()]);
+ const expected = [
+ [wrapper.debug(), 'foo bar'], // root
+ ['
', ''], // first div
+ ['\n foo bar\n
', 'foo bar'], // second div
+ ['foo bar', 'foo bar'], // second div's contents
+ ];
+ expect(textContents).to.eql(expected);
+ });
+
+ it('does not pass in null or false nodes', () => {
+ const wrapper = Wrap((
+
+
+ foo bar
+ {null}
+ {false}
+
+ ));
+ const stub = sinon.stub();
+ wrapper.findWhere(stub);
+
+ const passedNodes = stub.getCalls().map(({ args: [firstArg] }) => firstArg);
+ const getElement = n => (isShallow ? n.getElement() : n.getDOMNode());
+ const hasElements = passedNodes.map(n => [n.debug(), getElement(n) && true]);
+ const expected = [
+ [wrapper.debug(), true], // root
+ ['
', true], // first div
+ ['\n foo bar\n
', true], // second div
+ ['foo bar', null], // second div's contents
+ ];
+ expect(hasElements).to.eql(expected);
+
+ // the root, plus the 2 renderable children, plus the grandchild text
+ expect(stub).to.have.property('callCount', 4);
+ });
+
+ it('does not pass in null or false nodes', () => {
+ const wrapper = Wrap((
+
+ ));
+ const stub = sinon.stub();
+ stub.returns(true);
+ const spy = sinon.spy(stub);
+ wrapper.findWhere(spy);
+ expect(spy).to.have.property('callCount', 2);
+ });
+
+ itIf(is('>= 16'), 'finds portals by react-is Portal type', () => {
+ const containerDiv = makeDOMElement();
+ const Foo = () => (
+
+ {createPortal(
+
InPortal
,
+ containerDiv,
+ )}
+
+ );
+
+ const wrapper = Wrap( );
+
+ expect(wrapper.findWhere(node => node.type() === Portal)).to.have.lengthOf(1);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/first.jsx b/packages/enzyme-test-suite/test/shared/methods/first.jsx
new file mode 100644
index 000000000..a39d40546
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/first.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeFirst({
+ Wrap,
+}) {
+ describe('.first()', () => {
+ it('returns the first node in the current set', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.find('.bar').first().hasClass('baz')).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/flatMap.jsx b/packages/enzyme-test-suite/test/shared/methods/flatMap.jsx
new file mode 100644
index 000000000..0398cf5ba
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/flatMap.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeFlatMap({
+ Wrap,
+}) {
+ describe('.flatMap(fn)', () => {
+ it('returns a wrapper with the mapped and flattened nodes', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const nodes = wrapper.find('.foo').flatMap(w => w.children().getElements());
+
+ expect(nodes).to.have.lengthOf(6);
+ expect(nodes.at(0).hasClass('bar')).to.equal(true);
+ expect(nodes.at(1).hasClass('bar')).to.equal(true);
+ expect(nodes.at(2).hasClass('baz')).to.equal(true);
+ expect(nodes.at(3).hasClass('baz')).to.equal(true);
+ expect(nodes.at(4).hasClass('bax')).to.equal(true);
+ expect(nodes.at(5).hasClass('bax')).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/forEach.jsx b/packages/enzyme-test-suite/test/shared/methods/forEach.jsx
new file mode 100644
index 000000000..8f8299a3d
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/forEach.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeForEach({
+ Wrap,
+ Wrapper,
+}) {
+ describe('.forEach(fn)', () => {
+ it('calls a function for each node in the wrapper', () => {
+ const wrapper = Wrap((
+
+ ));
+ const spy = sinon.spy();
+
+ wrapper.find('.foo').forEach(spy);
+
+ expect(spy).to.have.property('callCount', 3);
+ expect(spy.args[0][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[0][0].hasClass('bax')).to.equal(true);
+ expect(spy.args[0][1]).to.equal(0);
+ expect(spy.args[1][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[1][0].hasClass('bar')).to.equal(true);
+ expect(spy.args[1][1]).to.equal(1);
+ expect(spy.args[2][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[2][0].hasClass('baz')).to.equal(true);
+ expect(spy.args[2][1]).to.equal(2);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/get.jsx b/packages/enzyme-test-suite/test/shared/methods/get.jsx
new file mode 100644
index 000000000..1c31ec133
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/get.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeGet({
+ Wrap,
+}) {
+ describe('.get(index)', () => {
+ it('gets the node at the specified index', () => {
+ const wrapper = Wrap((
+
+ ));
+ const bar = wrapper.find('.bar');
+ expect(bar.get(0)).to.deep.equal(wrapper.find('.foo').getElement());
+ expect(bar.get(1)).to.deep.equal(wrapper.find('.bax').getElement());
+ expect(bar.get(2)).to.deep.equal(wrapper.find('.bux').getElement());
+ expect(bar.get(3)).to.deep.equal(wrapper.find('.baz').getElement());
+ });
+
+ it('does not add a "null" key to elements with a ref and no key', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.setRef = this.setRef.bind(this);
+ }
+
+ setRef(node) {
+ this.node = node;
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+ expect(wrapper.get(0)).to.have.property('key', null);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/getElement.jsx b/packages/enzyme-test-suite/test/shared/methods/getElement.jsx
new file mode 100644
index 000000000..b659fcb56
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/getElement.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+import {
+ createRef,
+} from '../../_helpers/react-compat';
+
+export default function describeGetElement({
+ Wrap,
+ isShallow,
+}) {
+ describe('.getElement()', () => {
+ it('returns nodes with refs as well', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.setRef = this.setRef.bind(this);
+ this.node = null;
+ }
+
+ setRef(node) {
+ this.node = node;
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+ const mockNode = { mock: true };
+ wrapper.find('.foo').getElement().ref(mockNode);
+ expect(wrapper.instance().node).to.equal(mockNode);
+ });
+
+ itIf(is('>= 16.3'), 'returns nodes with createRefs as well', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.setRef = createRef();
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+ // shallow rendering does not invoke refs
+ if (isShallow) {
+ expect(wrapper.instance().setRef).to.have.property('current', null);
+ } else {
+ const element = wrapper.find('.foo').instance();
+ expect(wrapper.instance().setRef).to.have.property('current', element);
+ }
+ });
+
+ it('does not add a "null" key to elements with a ref and no key', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.setRef = this.setRef.bind(this);
+ }
+
+ setRef(node) {
+ this.node = node;
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+ expect(wrapper.getElement()).to.have.property('key', null);
+ });
+
+ itIf(is('>= 16.3'), 'does not add a "null" key to elements with a createRef and no key', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.setRef = createRef();
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+ expect(wrapper.getElement()).to.have.property('key', null);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/getElements.jsx b/packages/enzyme-test-suite/test/shared/methods/getElements.jsx
new file mode 100644
index 000000000..8c22496c2
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/getElements.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeGetElements({
+ Wrap,
+}) {
+ describe('.getElements()', () => {
+ it('returns the wrapped elements', () => {
+ const one = ;
+ const two = ;
+
+ class Test extends React.Component {
+ render() {
+ return (
+
+ {one}
+ {two}
+
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+ expect(wrapper.find('span').getElements()).to.deep.equal([one, two]);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/getNode.jsx b/packages/enzyme-test-suite/test/shared/methods/getNode.jsx
new file mode 100644
index 000000000..0aed6e93d
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/getNode.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeGetNode({
+ Wrap,
+ WrapperName,
+ isShallow,
+}) {
+ describe('.getNode()', () => {
+ it('throws', () => {
+ const wrapper = Wrap(
);
+ expect(() => wrapper.getNode()).to.throw(
+ `${WrapperName}::getNode() is no longer supported. Use ${WrapperName}::${isShallow ? 'getElement' : 'instance'}() instead`,
+ );
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/getNodes.jsx b/packages/enzyme-test-suite/test/shared/methods/getNodes.jsx
new file mode 100644
index 000000000..0db2ad34f
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/getNodes.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeGetNodes({
+ Wrap,
+ WrapperName,
+}) {
+ describe('.getNodes()', () => {
+ it('throws', () => {
+ const wrapper = Wrap(
);
+ expect(() => wrapper.getNodes()).to.throw(
+ `${WrapperName}::getNodes() is no longer supported.`,
+ );
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/hasClass.jsx b/packages/enzyme-test-suite/test/shared/methods/hasClass.jsx
new file mode 100644
index 000000000..e2bf9a254
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/hasClass.jsx
@@ -0,0 +1,176 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+export default function describeHasClass({
+ Wrap,
+ WrapRendered,
+ isShallow,
+ isMount,
+}) {
+ describe('.hasClass(className)', () => {
+ function FooSFC() {
+ return
;
+ }
+ class Foo extends React.Component {
+ render() {
+ return FooSFC();
+ }
+ }
+ class Bar extends React.Component {
+ render() {
+ return ;
+ }
+ }
+ class RendersNull extends React.Component {
+ render() {
+ return null;
+ }
+ }
+
+ context('when using a DOM component', () => {
+ it('returns whether or not node has a certain class', () => {
+ const wrapper = Wrap(
);
+
+ expect(wrapper.hasClass('foo')).to.equal(true);
+ expect(wrapper.hasClass('bar')).to.equal(true);
+ expect(wrapper.hasClass('baz')).to.equal(true);
+ expect(wrapper.hasClass('some-long-string')).to.equal(true);
+ expect(wrapper.hasClass('FoOo')).to.equal(true);
+ expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
+ });
+ });
+
+ describeIf(is('> 0.13'), 'with stateless function components (SFCs)', () => {
+ it('returns whether or not rendered node has a certain class', () => {
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.hasClass('root')).to.equal(false);
+ expect(wrapper.hasClass('foo')).to.equal(true);
+ expect(wrapper.hasClass('bar')).to.equal(true);
+ expect(wrapper.hasClass('baz')).to.equal(true);
+ expect(wrapper.hasClass('some-long-string')).to.equal(true);
+ expect(wrapper.hasClass('FoOo')).to.equal(true);
+ expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
+ });
+
+ itIf(isMount, 'returns whether or not root node has a certain class', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.hasClass('root')).to.equal(true);
+ expect(wrapper.hasClass('foo')).to.equal(false);
+ expect(wrapper.hasClass('bar')).to.equal(false);
+ expect(wrapper.hasClass('baz')).to.equal(false);
+ expect(wrapper.hasClass('some-long-string')).to.equal(false);
+ expect(wrapper.hasClass('FoOo')).to.equal(false);
+ expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
+ });
+ });
+
+ context('when using a Composite class component', () => {
+ it('returns whether or not rendered node has a certain class', () => {
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.hasClass('root')).to.equal(false);
+ expect(wrapper.hasClass('foo')).to.equal(true);
+ expect(wrapper.hasClass('bar')).to.equal(true);
+ expect(wrapper.hasClass('baz')).to.equal(true);
+ expect(wrapper.hasClass('some-long-string')).to.equal(true);
+ expect(wrapper.hasClass('FoOo')).to.equal(true);
+ expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
+ });
+
+ itIf(isMount, 'returns whether or not root node has a certain class', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.hasClass('root')).to.equal(true);
+ expect(wrapper.hasClass('foo')).to.equal(false);
+ expect(wrapper.hasClass('bar')).to.equal(false);
+ expect(wrapper.hasClass('baz')).to.equal(false);
+ expect(wrapper.hasClass('some-long-string')).to.equal(false);
+ expect(wrapper.hasClass('FoOo')).to.equal(false);
+ expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
+ });
+ });
+
+ context('when using nested composite components', () => {
+ it('returns whether or not node has a certain class', () => {
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.hasClass('root')).to.equal(false);
+ expect(wrapper.hasClass('isFoo')).to.equal(true);
+ });
+
+ itIf(!isShallow, 'returns whether or not nested node has a certain class', () => {
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.hasClass('root')).to.equal(false);
+ expect(wrapper.hasClass('isFoo')).to.equal(true);
+
+ expect(wrapper.children().hasClass('foo')).to.equal(true);
+ expect(wrapper.children().hasClass('bar')).to.equal(true);
+ expect(wrapper.children().hasClass('baz')).to.equal(true);
+ expect(wrapper.children().hasClass('some-long-string')).to.equal(true);
+ expect(wrapper.children().hasClass('FoOo')).to.equal(true);
+ expect(wrapper.children().hasClass('doesnt-exist')).to.equal(false);
+ });
+
+ itIf(isMount, 'returns whether or not root node has a certain class', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.hasClass('root')).to.equal(true);
+ expect(wrapper.hasClass('foo')).to.equal(false);
+ expect(wrapper.hasClass('bar')).to.equal(false);
+ expect(wrapper.hasClass('baz')).to.equal(false);
+ expect(wrapper.hasClass('some-long-string')).to.equal(false);
+ expect(wrapper.hasClass('FoOo')).to.equal(false);
+ expect(wrapper.hasClass('doesnt-exist')).to.equal(false);
+ });
+ });
+
+ context('when using a Composite component that renders null', () => {
+ it('returns whether or not node has a certain class', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.hasClass('foo')).to.equal(false);
+ });
+ });
+
+ it('works with a non-string `className` prop', () => {
+ class SpreadsProps extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ const obj = { classA: true, classB: false };
+ const wrapper = Wrap( );
+ expect(wrapper.hasClass('foo')).to.equal(false);
+ expect(wrapper.hasClass('classA')).to.equal(false);
+ expect(wrapper.hasClass('classB')).to.equal(false);
+ expect(wrapper.hasClass(String(obj))).to.equal(true);
+ });
+
+ it('allows hyphens', () => {
+ const wrapper = Wrap(
);
+ expect(wrapper.hasClass('foo-bar')).to.equal(true);
+ });
+
+ it('works if className has a function in toString property', () => {
+ function classes() {}
+ classes.toString = () => 'foo-bar';
+ const wrapper = Wrap(
);
+ expect(wrapper.hasClass('foo-bar')).to.equal(true);
+ });
+
+ it('works if searching with a RegExp', () => {
+ const wrapper = Wrap(
);
+ expect(wrapper.hasClass(/(ComponentName)-(classname)-(\d+)/)).to.equal(true);
+ expect(wrapper.hasClass(/(ComponentName)-(other)-(\d+)/)).to.equal(false);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/hostNodes.jsx b/packages/enzyme-test-suite/test/shared/methods/hostNodes.jsx
new file mode 100644
index 000000000..4e58f4acf
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/hostNodes.jsx
@@ -0,0 +1,132 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+export default function describeHostNodes({
+ Wrap,
+ WrapRendered,
+}) {
+ describe('.hostNodes()', () => {
+ it('strips out any non-hostNode', () => {
+ class Foo extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ const wrapper = Wrap((
+
+
+
+
+ ));
+
+ const foos = wrapper.find('main > .foo');
+ expect(foos).to.have.lengthOf(2);
+
+ const hostNodes = foos.hostNodes();
+ expect(hostNodes).to.have.lengthOf(1);
+
+ expect(hostNodes.is('div')).to.equal(true);
+ expect(hostNodes.hasClass('foo')).to.equal(true);
+ });
+
+ it('does NOT allow matches on a nested node', () => {
+ const wrapper = Wrap((
+
+ ));
+ const b =
;
+ expect(wrapper.equals(b)).to.equal(false);
+ });
+
+ it('matches composite components', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+
+ const wrapper = Wrap((
+
+
+
+ ));
+ const b =
;
+ expect(wrapper.equals(b)).to.equal(true);
+ });
+
+ it('does not expand `node` content', () => {
+ class Bar extends React.Component {
+ render() { return
; }
+ }
+
+ class Foo extends React.Component {
+ render() { return ; }
+ }
+
+ expect(WrapRendered( ).equals( )).to.equal(true);
+ expect(WrapRendered( ).equals( )).to.equal(false);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('matches composite SFCs', () => {
+ const Foo = () => (
+
+ );
+
+ const wrapper = Wrap((
+
+
+
+ ));
+ const b =
;
+ expect(wrapper.equals(b)).to.equal(true);
+ });
+
+ it('does not expand `node` content', () => {
+ const Bar = () => (
+
+ );
+
+ const Foo = () => (
+
+ );
+
+ expect(WrapRendered( ).equals( )).to.equal(true);
+ expect(WrapRendered( ).equals( )).to.equal(false);
+ });
+ });
+
+ it('flattens arrays of children to compare', () => {
+ class TwoChildren extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ class TwoChildrenOneArrayed extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+ const twoChildren = WrapRendered( );
+ const twoChildrenOneArrayed = WrapRendered( );
+
+ expect(twoChildren.equals(twoChildrenOneArrayed.getElement())).to.equal(true);
+ expect(twoChildrenOneArrayed.equals(twoChildren.getElement())).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/html.jsx b/packages/enzyme-test-suite/test/shared/methods/html.jsx
new file mode 100644
index 000000000..151fcf82d
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/html.jsx
@@ -0,0 +1,117 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+import {
+ Fragment,
+} from '../../_helpers/react-compat';
+
+export default function describeHTML({
+ Wrap,
+}) {
+ describe('.html()', () => {
+ it('returns html of straight DOM elements', () => {
+ const wrapper = Wrap((
+
+ Hello World!
+
+ ));
+ expect(wrapper.html()).to.equal('Hello World!
');
+ });
+
+ it('renders out nested composite components', () => {
+ class Foo extends React.Component {
+ render() {
+ return (
);
+ }
+ }
+ class Bar extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+ expect(wrapper.html()).to.equal('');
+ expect(wrapper.find(Foo).html()).to.equal('
');
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('renders out nested composite components', () => {
+ const Foo = () =>
;
+ const Bar = () => (
+
+
+
+ );
+
+ const wrapper = Wrap( );
+ expect(wrapper.html()).to.equal('');
+ expect(wrapper.find(Foo).html()).to.equal('
');
+ });
+ });
+
+ describeIf(is('>16.2'), 'Fragments', () => {
+ class FragmentClassExample extends React.Component {
+ render() {
+ return (
+
+ Foo
+ Bar
+
+ );
+ }
+ }
+
+ const FragmentConstExample = () => (
+
+ Foo
+ Bar
+
+ );
+
+ class ClassChild extends React.Component {
+ render() {
+ return Class child
;
+ }
+ }
+
+ function SFCChild() {
+ return SFC child
;
+ }
+
+ class FragmentWithCustomChildClass extends React.Component {
+ render() {
+ return (
+
+
+
+
+ );
+ }
+ }
+
+ it('correctly renders html for both children for class', () => {
+ const classWrapper = Wrap( );
+ expect(classWrapper.html()).to.equal('Foo
Bar
');
+ });
+
+ it('correctly renders html for both children for const', () => {
+ const constWrapper = Wrap( );
+ expect(constWrapper.html()).to.equal('Foo
Bar
');
+ });
+
+ it('correctly renders html for custom component children', () => {
+ const withChildrenWrapper = Wrap( );
+ expect(withChildrenWrapper.html()).to.equal('Class child
SFC child
');
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/instance.jsx b/packages/enzyme-test-suite/test/shared/methods/instance.jsx
new file mode 100644
index 000000000..b535bb54b
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/instance.jsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+export default function describeInstance({
+ Wrap,
+ WrapperName,
+ isShallow,
+}) {
+ describe('.instance()', () => {
+ it('returns the component instance', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+
+ const wrapper = Wrap( );
+ expect(wrapper.instance()).to.be.instanceof(Foo);
+ expect(wrapper.instance().render).to.equal(Foo.prototype.render);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ function SFC() {
+ return
;
+ }
+
+ itIf(is('>= 16'), 'has no instance', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.instance()).to.equal(null);
+ });
+
+ itIf(is('< 16'), 'has an instance', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.instance()).not.to.equal(null);
+ });
+ });
+
+ itIf(!isShallow, 'throws when wrapping multiple elements', () => {
+ class Test extends React.Component {
+ render() {
+ return (
+
+
+
+
+ );
+ }
+ }
+
+ const wrapper = Wrap( ).find('span');
+ expect(() => wrapper.instance()).to.throw(
+ Error,
+ 'Method “instance” is meant to be run on 1 node. 2 found instead.',
+ );
+ });
+
+ itIf(isShallow, 'throws if called on something other than the root node', () => {
+ class Foo extends React.Component {
+ render() { return ; }
+ }
+
+ const wrapper = Wrap( );
+ const div = wrapper.find('div');
+
+ expect(() => div.instance()).to.throw(
+ Error,
+ `${WrapperName}::instance() can only be called on the root`,
+ );
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/is.jsx b/packages/enzyme-test-suite/test/shared/methods/is.jsx
new file mode 100644
index 000000000..b5ea45811
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/is.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeIs({
+ Wrap,
+}) {
+ describe('.is(selector)', () => {
+ it('returns true when selector matches current element', () => {
+ const wrapper = Wrap(
);
+ expect(wrapper.is('.foo')).to.equal(true);
+ });
+
+ it('allows for compound selectors', () => {
+ const wrapper = Wrap(
);
+ expect(wrapper.is('.foo.bar')).to.equal(true);
+ });
+
+ it('ignores insignificant whitespace', () => {
+ const className = `
+ foo
+ `;
+ const wrapper = Wrap(
);
+ expect(wrapper.is('.foo')).to.equal(true);
+ });
+
+ it('handles all significant whitespace', () => {
+ const className = `foo
+
+ bar
+ baz`;
+ const wrapper = Wrap(
);
+ expect(wrapper.is('.foo.bar.baz')).to.equal(true);
+ });
+
+ it('returns false when selector does not match', () => {
+ const wrapper = Wrap(
);
+ expect(wrapper.is('.foo')).to.equal(false);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/isEmpty.jsx b/packages/enzyme-test-suite/test/shared/methods/isEmpty.jsx
new file mode 100644
index 000000000..f02d338e5
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/isEmpty.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeLast({
+ Wrap,
+}) {
+ describe('.isEmpty()', () => {
+ let warningStub;
+ let fooNode;
+ let missingNode;
+
+ beforeEach(() => {
+ warningStub = sinon.stub(console, 'warn');
+ const wrapper = Wrap(
);
+ fooNode = wrapper.find('.foo');
+ missingNode = wrapper.find('.missing');
+ });
+ afterEach(() => {
+ warningStub.restore();
+ });
+
+ it('displays a deprecation warning', () => {
+ fooNode.isEmpty();
+ expect(warningStub.calledWith('Enzyme::Deprecated method isEmpty() called, use exists() instead.')).to.equal(true);
+ });
+
+ it('calls exists() instead', () => {
+ const existsSpy = sinon.spy();
+ fooNode.exists = existsSpy;
+ expect(fooNode.isEmpty()).to.equal(true);
+ expect(existsSpy).to.have.property('called', true);
+ });
+
+ it('returns true if wrapper is empty', () => {
+ expect(fooNode.isEmpty()).to.equal(false);
+ expect(missingNode.isEmpty()).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/isEmptyRender.jsx b/packages/enzyme-test-suite/test/shared/methods/isEmptyRender.jsx
new file mode 100644
index 000000000..b1e282500
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/isEmptyRender.jsx
@@ -0,0 +1,162 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+ itIf,
+ itWithData,
+ generateEmptyRenderData,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+import {
+ createClass,
+} from '../../_helpers/react-compat';
+
+export default function describeIsEmptyRender({
+ Wrap,
+ isShallow,
+}) {
+ describe('.isEmptyRender()', () => {
+ const emptyRenderValues = generateEmptyRenderData();
+
+ itWithData(emptyRenderValues, 'when a React createClass component returns: ', (data) => {
+ const Foo = createClass({
+ render() {
+ return data.value;
+ },
+ });
+ const wrapper = Wrap( );
+ expect(wrapper.isEmptyRender()).to.equal(data.expectResponse);
+ });
+
+ itWithData(emptyRenderValues, 'when an ES2015 class component returns: ', (data) => {
+ class Foo extends React.Component {
+ render() {
+ return data.value;
+ }
+ }
+ const wrapper = Wrap( );
+ expect(wrapper.isEmptyRender()).to.equal(data.expectResponse);
+ });
+
+ describe('nested nodes', () => {
+ class RenderChildren extends React.Component {
+ render() {
+ const { children } = this.props;
+ return children;
+ }
+ }
+
+ class RenderNull extends React.Component {
+ render() {
+ return null;
+ }
+ }
+
+ it(`returns ${!isShallow} for nested elements that return null`, () => {
+ const wrapper = Wrap((
+
+
+
+ ));
+
+ expect(wrapper.isEmptyRender()).to.equal(!isShallow);
+ });
+
+ it('returns false for multiple nested elements that all return null', () => {
+ const wrapper = Wrap((
+
+
+
+ ));
+
+ expect(wrapper.isEmptyRender()).to.equal(false);
+ });
+
+ it('returns false for multiple nested elements where one fringe returns a non null value', () => {
+ const wrapper = Wrap((
+
+ Hello
+
+ ));
+
+ expect(wrapper.isEmptyRender()).to.equal(false);
+ });
+
+ itIf(is('>= 16'), 'returns false for multiple nested elements that all return null', () => {
+ const wrapper = Wrap((
+
+
+
+
+
+
+
+ ));
+
+ expect(wrapper.isEmptyRender()).to.equal(false);
+ });
+
+ itIf(is('>= 16'), 'returns false for multiple nested elements where one fringe returns a non null value', () => {
+ const wrapper = Wrap((
+
+
+
+
+
+
+
+
+
+
+
+
+ Hello
+
+
+
+ ));
+
+ expect(wrapper.isEmptyRender()).to.equal(false);
+ });
+
+ itIf(is('>= 16'), `returns ${!isShallow} for multiple nested elements where all values are null`, () => {
+ const wrapper = Wrap((
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ));
+
+ expect(wrapper.isEmptyRender()).to.equal(!isShallow);
+ });
+ });
+
+ it('does not return true for HTML elements', () => {
+ const wrapper = Wrap(
);
+ expect(wrapper.isEmptyRender()).to.equal(false);
+ });
+
+ describeIf(is('>=15 || ^16.0.0-alpha'), 'stateless function components (SFCs)', () => {
+ itWithData(emptyRenderValues, 'when a component returns: ', (data) => {
+ function Foo() {
+ return data.value;
+ }
+ const wrapper = Wrap( );
+ expect(wrapper.isEmptyRender()).to.equal(data.expectResponse);
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/key.jsx b/packages/enzyme-test-suite/test/shared/methods/key.jsx
new file mode 100644
index 000000000..1c40ceadb
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/key.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeKey({
+ Wrap,
+}) {
+ describe('.key()', () => {
+ it('returns the key of the node', () => {
+ const wrapper = Wrap((
+
+ {['foo', 'bar', ''].map(s => {s} )}
+
+ )).find('li');
+ expect(wrapper.at(0).key()).to.equal('foo');
+ expect(wrapper.at(1).key()).to.equal('bar');
+ expect(wrapper.at(2).key()).to.equal('');
+ });
+
+ it('returns null when no key is specified', () => {
+ const wrapper = Wrap((
+
+ )).find('li');
+ expect(wrapper.key()).to.equal(null);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/last.jsx b/packages/enzyme-test-suite/test/shared/methods/last.jsx
new file mode 100644
index 000000000..f89c9a88e
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/last.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeLast({
+ Wrap,
+}) {
+ describe('.last()', () => {
+ it('returns the last node in the current set', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.find('.bar').last().hasClass('baz')).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/map.jsx b/packages/enzyme-test-suite/test/shared/methods/map.jsx
new file mode 100644
index 000000000..2a13d7b2e
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/map.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeMap({
+ Wrap,
+ Wrapper,
+}) {
+ describe('.map(fn)', () => {
+ it('calls a function with a wrapper for each node in the wrapper', () => {
+ const wrapper = Wrap((
+
+ ));
+ const spy = sinon.spy();
+
+ wrapper.find('.foo').map(spy);
+
+ expect(spy).to.have.property('callCount', 3);
+ expect(spy.args[0][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[0][0].hasClass('bax')).to.equal(true);
+ expect(spy.args[0][1]).to.equal(0);
+ expect(spy.args[1][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[1][0].hasClass('bar')).to.equal(true);
+ expect(spy.args[1][1]).to.equal(1);
+ expect(spy.args[2][0]).to.be.instanceOf(Wrapper);
+ expect(spy.args[2][0].hasClass('baz')).to.equal(true);
+ expect(spy.args[2][1]).to.equal(2);
+ });
+
+ it('returns an array with the mapped values', () => {
+ const wrapper = Wrap((
+
+ ));
+ const result = wrapper.find('.foo').map(w => w.props().className);
+
+ expect(result).to.eql([
+ 'foo bax',
+ 'foo bar',
+ 'foo baz',
+ ]);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/matchesElement.jsx b/packages/enzyme-test-suite/test/shared/methods/matchesElement.jsx
new file mode 100644
index 000000000..bd71d93cb
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/matchesElement.jsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeMatchesElement({
+ Wrap,
+ WrapRendered,
+}) {
+ describe('.matchesElement(node)', () => {
+ it('matches on a root node that looks like the rendered one', () => {
+ const spy = sinon.spy();
+ const wrapper = Wrap((
+
+ )).first();
+ expect(wrapper.matchesElement()).to.equal(true);
+ expect(wrapper.matchesElement((
+
+ ))).to.equal(true);
+ expect(wrapper.matchesElement((
+
+ ))).to.equal(true);
+ expect(wrapper.matchesElement((
+
+ ))).to.equal(true);
+ expect(spy).to.have.property('callCount', 0);
+ });
+
+ it('does not match on a root node that doesn’t looks like the rendered one', () => {
+ const spy = sinon.spy();
+ const spy2 = sinon.spy();
+ const wrapper = Wrap((
+
+ )).first();
+ expect(wrapper.matchesElement()).to.equal(false);
+ expect(wrapper.matchesElement((
+
+ ))).to.equal(false);
+ expect(wrapper.matchesElement((
+
+ ))).to.equal(false);
+ expect(wrapper.matchesElement((
+
+ ))).to.equal(false);
+ expect(spy).to.have.property('callCount', 0);
+ expect(spy2).to.have.property('callCount', 0);
+ });
+
+ it('matches a simple node', () => {
+ class Test extends React.Component {
+ render() {
+ return test ;
+ }
+ }
+ const wrapper = WrapRendered( );
+ expect(wrapper.matchesElement(test )).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/name.jsx b/packages/enzyme-test-suite/test/shared/methods/name.jsx
new file mode 100644
index 000000000..78da42027
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/name.jsx
@@ -0,0 +1,129 @@
+import React from 'react';
+import { expect } from 'chai';
+import wrap from 'mocha-wrap';
+import sinon from 'sinon-sandbox';
+
+import getAdapter from 'enzyme/build/getAdapter';
+
+import {
+ describeIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+import {
+ createClass,
+} from '../../_helpers/react-compat';
+
+export default function describeName({
+ Wrap,
+ WrapRendered,
+}) {
+ wrap()
+ .withOverride(() => getAdapter(), 'displayNameOfNode', () => undefined)
+ .describe('.name()', () => {
+ describe('node with displayName', () => {
+ it('returns the displayName of the node', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+
+ class FooWrapper extends React.Component {
+ render() { return ; }
+ }
+
+ Foo.displayName = 'CustomWrapper';
+
+ const wrapper = WrapRendered( );
+ expect(wrapper.name()).to.equal('CustomWrapper');
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('returns the name of the node', () => {
+ function SFC() {
+ return
;
+ }
+ const SFCWrapper = () => ;
+
+ SFC.displayName = 'CustomWrapper';
+
+ const wrapper = WrapRendered( );
+ expect(wrapper.name()).to.equal('CustomWrapper');
+ });
+ });
+
+ describe('createClass', () => {
+ it('returns the name of the node', () => {
+ const Foo = createClass({
+ displayName: 'CustomWrapper',
+ render() {
+ return
;
+ },
+ });
+ const FooWrapper = createClass({
+ render() {
+ return ;
+ },
+ });
+
+ const wrapper = WrapRendered( );
+ expect(wrapper.name()).to.equal('CustomWrapper');
+ });
+ });
+
+ wrap()
+ .withOverride(() => getAdapter(), 'displayNameOfNode', () => sinon.stub())
+ .describe('adapter has `displayNameOfNode`', () => {
+ it('delegates to the adapter’s `displayNameOfNode`', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+ const stub = getAdapter().displayNameOfNode;
+ const sentinel = {};
+ stub.returns(sentinel);
+
+ const wrapper = Wrap( );
+
+ expect(wrapper.name()).to.equal(sentinel);
+
+ expect(stub).to.have.property('callCount', 1);
+ const { args } = stub.firstCall;
+ expect(args).to.eql([wrapper.getNodeInternal()]);
+ });
+ });
+ });
+
+ describe('node without displayName', () => {
+ it('returns the name of the node', () => {
+ class Foo extends React.Component {
+ render() { return
; }
+ }
+
+ class FooWrapper extends React.Component {
+ render() { return ; }
+ }
+
+ const wrapper = WrapRendered( );
+ expect(wrapper.name()).to.equal('Foo');
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('returns the name of the node', () => {
+ function SFC() {
+ return
;
+ }
+ const SFCWrapper = () => ;
+
+ const wrapper = WrapRendered( );
+ expect(wrapper.name()).to.equal('SFC');
+ });
+ });
+ });
+
+ describe('DOM node', () => {
+ it('returns the name of the node', () => {
+ const wrapper = Wrap(
);
+ expect(wrapper.name()).to.equal('div');
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/not.jsx b/packages/enzyme-test-suite/test/shared/methods/not.jsx
new file mode 100644
index 000000000..63d8a09cd
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/not.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeNot({
+ Wrap,
+}) {
+ describe('.not(selector)', () => {
+ it('filters to things not matching a selector', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('.foo').not('.bar')).to.have.lengthOf(1);
+ expect(wrapper.find('.baz').not('.foo')).to.have.lengthOf(2);
+ expect(wrapper.find('.foo').not('div')).to.have.lengthOf(0);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/parent.jsx b/packages/enzyme-test-suite/test/shared/methods/parent.jsx
new file mode 100644
index 000000000..7ff26689a
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/parent.jsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ itIf,
+} from '../../_helpers';
+
+import {
+ createClass,
+} from '../../_helpers/react-compat';
+
+export default function describeParent({
+ Wrap,
+ isShallow,
+}) {
+ describe('.parent()', () => {
+ it('returns only the immediate parent of the node', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.find('.baz').parent().hasClass('bar')).to.equal(true);
+ });
+
+ it('works when the sibling node has children', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('.baz').parent().hasClass('bar')).to.equal(true);
+ });
+
+ it('works for multiple nodes', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const parents = wrapper.find('.baz').parent();
+ expect(parents).to.have.lengthOf(3);
+ expect(parents.at(0).hasClass('foo')).to.equal(true);
+ expect(parents.at(1).hasClass('bar')).to.equal(true);
+ expect(parents.at(2).hasClass('bax')).to.equal(true);
+ });
+
+ itIf(isShallow, 'works with component', () => {
+ const Foo = createClass({
+ render() {
+ return
;
+ },
+ });
+ const wrapper = Wrap( );
+ expect(wrapper.find('.bar')).to.have.lengthOf(1);
+ expect(wrapper.find('.bar').parent()).to.have.lengthOf(0);
+ expect(wrapper.parent()).to.have.lengthOf(0);
+ });
+
+ itIf(!isShallow, 'works with component', () => {
+ const Foo = createClass({
+ render() {
+ return
;
+ },
+ });
+ const wrapper = Wrap( );
+ expect(wrapper.find('.bar')).to.have.lengthOf(1);
+ expect(wrapper.find('.bar').parent()).to.have.lengthOf(1);
+ expect(wrapper.parent()).to.have.lengthOf(0);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/parents.jsx b/packages/enzyme-test-suite/test/shared/methods/parents.jsx
new file mode 100644
index 000000000..3cc4683ab
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/parents.jsx
@@ -0,0 +1,172 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ itIf,
+} from '../../_helpers';
+
+import {
+ createClass,
+} from '../../_helpers/react-compat';
+
+export default function describeParents({
+ Wrap,
+ isShallow,
+}) {
+ describe('.parents([selector])', () => {
+ it('returns an array of current node’s ancestors', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const parents = wrapper.find('.baz').parents();
+
+ expect(parents).to.have.lengthOf(3);
+ expect(parents.at(0).hasClass('bar')).to.equal(true);
+ expect(parents.at(1).hasClass('foo')).to.equal(true);
+ expect(parents.at(2).hasClass('bax')).to.equal(true);
+ });
+
+ it('works for non-leaf nodes as well', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const parents = wrapper.find('.bar').parents();
+
+ expect(parents).to.have.lengthOf(2);
+ expect(parents.at(0).hasClass('foo')).to.equal(true);
+ expect(parents.at(1).hasClass('bax')).to.equal(true);
+ });
+
+ it('optionally allows a selector', () => {
+ const wrapper = Wrap((
+
+ ));
+
+ const parents = wrapper.find('.baz').parents('.foo');
+
+ expect(parents).to.have.lengthOf(2);
+ expect(parents.at(0).hasClass('foo')).to.equal(true);
+ expect(parents.at(1).hasClass('bax')).to.equal(true);
+ });
+
+ it('works when called sequentially on two sibling nodes', () => {
+ class Test extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+
+ const aChild = wrapper.find({ children: 'A child' });
+ expect(aChild.debug()).to.equal(`
+ A child
+
`);
+ expect(aChild).to.have.lengthOf(1);
+
+ const bChild = wrapper.find({ children: 'B child' });
+ expect(bChild.debug()).to.equal(`
+ B child
+
`);
+ expect(bChild).to.have.lengthOf(1);
+
+ const bChildParents = bChild.parents('.b');
+ expect(bChildParents.debug()).to.equal(``);
+ expect(bChildParents).to.have.lengthOf(1);
+
+ const aChildParents = aChild.parents('.a');
+ expect(aChildParents.debug()).to.equal(``);
+ expect(aChildParents).to.have.lengthOf(1);
+ });
+
+ itIf(!isShallow, 'works with components in the tree', () => {
+ const Foo = createClass({
+ render() {
+ return
;
+ },
+ });
+ const wrapper = Wrap((
+
+
+
+ ));
+ const rootDiv = wrapper.find('.root');
+ expect(rootDiv).to.have.lengthOf(1);
+ expect(rootDiv.hasClass('root')).to.equal(true);
+ expect(rootDiv.hasClass('bar')).to.equal(false);
+
+ const bar = rootDiv.find('.bar');
+ expect(bar).to.have.lengthOf(1);
+ expect(bar.parents('.root')).to.have.lengthOf(1);
+ });
+
+ itIf(!isShallow, 'finds parents up a tree through a custom component boundary', () => {
+ class CustomForm extends React.Component {
+ render() {
+ const { children } = this.props;
+ return (
+
+ );
+ }
+ }
+
+ const wrapper = Wrap((
+
+
+
+
+
+ ));
+
+ const formDown = wrapper.find('form');
+ expect(formDown).to.have.lengthOf(1);
+
+ const input = wrapper.find('input');
+ expect(input).to.have.lengthOf(1);
+ const formUp = input.parents('form');
+ expect(formUp).to.have.lengthOf(1);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/prop.jsx b/packages/enzyme-test-suite/test/shared/methods/prop.jsx
new file mode 100644
index 000000000..a32e7f79f
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/prop.jsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+export default function describeProp({
+ Wrap,
+ WrapRendered,
+ isMount,
+}) {
+ describe('.prop(name)', () => {
+ it('returns the prop value for [name]', () => {
+ const fn = () => {};
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.prop('className')).to.equal('bax');
+ expect(wrapper.prop('onClick')).to.equal(fn);
+ expect(wrapper.prop('id')).to.equal('fooId');
+ });
+
+ it('is allowed to be used on an inner node', () => {
+ const fn = () => {};
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('.baz').prop('onClick')).to.equal(fn);
+ expect(wrapper.find('.foo').prop('id')).to.equal('fooId');
+ });
+
+ class Foo extends React.Component {
+ render() {
+ const { bar, foo } = this.props;
+ return (
+
+ );
+ }
+ }
+
+ itIf(isMount, 'called on root should return props of root node', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.prop('foo')).to.equal('hi');
+ expect(wrapper.prop('bar')).to.equal('bye');
+ expect(wrapper.prop('className')).to.equal(undefined);
+ expect(wrapper.prop('id')).to.equal(undefined);
+ });
+
+ it('returns prop value of root rendered node', () => {
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.prop('className')).to.equal('bye');
+ expect(wrapper.prop('id')).to.equal('hi');
+ expect(wrapper.prop('foo')).to.equal(undefined);
+ expect(wrapper.prop('bar')).to.equal(undefined);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ const FooSFC = ({ bar, foo }) => (
+
+ );
+
+ itIf(isMount, 'called on root should return props of root node', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.prop('foo')).to.equal('hi');
+ expect(wrapper.prop('bar')).to.equal('bye');
+ expect(wrapper.prop('className')).to.equal(undefined);
+ expect(wrapper.prop('id')).to.equal(undefined);
+ });
+
+ it('returns props of root rendered node', () => {
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.prop('className')).to.equal('bye');
+ expect(wrapper.prop('id')).to.equal('hi');
+ expect(wrapper.prop('foo')).to.equal(undefined);
+ expect(wrapper.prop('bar')).to.equal(undefined);
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/props.jsx b/packages/enzyme-test-suite/test/shared/methods/props.jsx
new file mode 100644
index 000000000..63858efb5
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/props.jsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import sloppyReturnThis from '../../_helpers/untranspiledSloppyReturnThis';
+import { is } from '../../_helpers/version';
+
+export default function describeProps({
+ Wrap,
+ WrapRendered,
+ isMount,
+}) {
+ describe('.props()', () => {
+ it('returns the props object', () => {
+ const fn = () => ({});
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.props().className).to.equal('bax');
+ expect(wrapper.props().onClick).to.equal(fn);
+ expect(wrapper.props().id).to.equal('fooId');
+ });
+
+ it('is allowed to be used on an inner node', () => {
+ const fn = () => ({});
+ const wrapper = Wrap((
+
+ ));
+
+ expect(wrapper.find('.baz').props().onClick).to.equal(fn);
+ expect(wrapper.find('.foo').props().id).to.equal('fooId');
+ });
+
+ class Foo extends React.Component {
+ render() {
+ const { bar, foo } = this.props;
+ return (
+
+ );
+ }
+ }
+
+ itIf(isMount, 'called on root should return props of root node', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.props()).to.eql({ bar: 'bye', foo: 'hi' });
+ });
+
+ it('returns props of root rendered node', () => {
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.props()).to.eql({ className: 'bye', id: 'hi' });
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ const FooSFC = ({ bar, foo }) => (
+
+ );
+
+ itIf(isMount, 'called on root should return props of root node', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.props()).to.eql({ bar: 'bye', foo: 'hi' });
+ });
+
+ it('returns props of root rendered node', () => {
+ const wrapper = WrapRendered( );
+
+ expect(wrapper.props()).to.eql({ className: 'bye', id: 'hi' });
+ });
+
+ const SloppyReceiver = sloppyReturnThis((
+ receiver,
+ props = { NO_PROPS: true },
+ context,
+ ) => (
+
+ ));
+
+ const StrictReceiver = function SFC(
+ props = { NO_PROPS: true },
+ context,
+ ) {
+ return (
+
+ );
+ };
+
+ it('does not provide a `this` to a sloppy-mode SFC', () => {
+ const wrapper = WrapRendered( );
+ expect(wrapper.props()).to.be.an('object').that.has.all.keys({
+ 'data-is-global': true,
+ 'data-is-undefined': false,
+ });
+ });
+
+ it('does not provide a `this` to a strict-mode SFC', () => {
+ const wrapper = WrapRendered( );
+ expect(wrapper.props()).to.be.an('object').that.has.all.keys({
+ 'data-is-global': false,
+ 'data-is-undefined': true,
+ });
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/reduce.jsx b/packages/enzyme-test-suite/test/shared/methods/reduce.jsx
new file mode 100644
index 000000000..b813ee378
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/reduce.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+import { getElementPropSelector, getWrapperPropSelector } from '../../_helpers/selectors';
+
+export default function describeReduce({
+ Wrap,
+ Wrapper,
+}) {
+ describe('.reduce(fn[, initialValue])', () => {
+ it('has the right length', () => {
+ expect(Wrapper.prototype.reduce).to.have.lengthOf(1);
+ });
+
+ it('calls a function with a wrapper for each node in the wrapper', () => {
+ const wrapper = Wrap((
+
+ ));
+ const spy = sinon.spy(n => n + 1);
+
+ wrapper.find('.foo').reduce(spy, 0);
+
+ expect(spy).to.have.property('callCount', 3);
+ expect(spy.args[0][1]).to.be.instanceOf(Wrapper);
+ expect(spy.args[0][1].hasClass('bax')).to.equal(true);
+ expect(spy.args[1][1]).to.be.instanceOf(Wrapper);
+ expect(spy.args[1][1].hasClass('bar')).to.equal(true);
+ expect(spy.args[2][1]).to.be.instanceOf(Wrapper);
+ expect(spy.args[2][1].hasClass('baz')).to.equal(true);
+ });
+
+ it('accumulates a value', () => {
+ const wrapper = Wrap((
+
+ ));
+ const result = wrapper.find('.foo').reduce((obj, n) => {
+ obj[n.prop('id')] = n.prop('className');
+ return obj;
+ }, {});
+
+ expect(result).to.eql({
+ bax: 'foo qoo',
+ bar: 'foo boo',
+ baz: 'foo hoo',
+ });
+ });
+
+ it('allows the initialValue to be omitted', () => {
+ const one = (
);
+ const two = (
);
+ const three = (
);
+ const wrapper = Wrap((
+
+ {one}
+ {two}
+ {three}
+
+ ));
+ const counter = ( );
+ const result = wrapper
+ .find('.foo')
+ .reduce((acc, n) => [].concat(acc, n, new Wrapper(counter)))
+ .map(getWrapperPropSelector('id'));
+
+ expect(result).to.eql([one, two, counter, three, counter].map(getElementPropSelector('id')));
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/reduceRight.jsx b/packages/enzyme-test-suite/test/shared/methods/reduceRight.jsx
new file mode 100644
index 000000000..383e0b235
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/reduceRight.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+import { getElementPropSelector, getWrapperPropSelector } from '../../_helpers/selectors';
+
+export default function describeReduceRight({
+ Wrap,
+ Wrapper,
+}) {
+ describe('.reduceRight(fn[, initialValue])', () => {
+ it('has the right length', () => {
+ expect(Wrapper.prototype.reduceRight).to.have.lengthOf(1);
+ });
+
+ it('calls a function with a wrapper for each node in the wrapper in reverse', () => {
+ const wrapper = Wrap((
+
+ ));
+ const spy = sinon.spy(n => n + 1);
+
+ wrapper.find('.foo').reduceRight(spy, 0);
+
+ expect(spy).to.have.property('callCount', 3);
+ expect(spy.args[0][1]).to.be.instanceOf(Wrapper);
+ expect(spy.args[0][1].hasClass('baz')).to.equal(true);
+ expect(spy.args[1][1]).to.be.instanceOf(Wrapper);
+ expect(spy.args[1][1].hasClass('bar')).to.equal(true);
+ expect(spy.args[2][1]).to.be.instanceOf(Wrapper);
+ expect(spy.args[2][1].hasClass('bax')).to.equal(true);
+ });
+
+ it('accumulates a value', () => {
+ const wrapper = Wrap((
+
+ ));
+ const result = wrapper.find('.foo').reduceRight((obj, n) => {
+ obj[n.prop('id')] = n.prop('className');
+ return obj;
+ }, {});
+
+ expect(result).to.eql({
+ bax: 'foo qoo',
+ bar: 'foo boo',
+ baz: 'foo hoo',
+ });
+ });
+
+ it('allows the initialValue to be omitted', () => {
+ const one = (
);
+ const two = (
);
+ const three = (
);
+ const wrapper = Wrap((
+
+ {one}
+ {two}
+ {three}
+
+ ));
+ const counter = ( );
+ const result = wrapper
+ .find('.foo')
+ .reduceRight((acc, n) => [].concat(acc, n, new Wrapper(counter)))
+ .map(getWrapperPropSelector('id'));
+
+ expect(result).to.eql([three, two, counter, one, counter].map(getElementPropSelector('id')));
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/render.jsx b/packages/enzyme-test-suite/test/shared/methods/render.jsx
new file mode 100644
index 000000000..6a54dd25a
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/render.jsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+export default function describeRender({
+ Wrap,
+}) {
+ describe('.render()', () => {
+ it('returns a cheerio wrapper around the current node', () => {
+ class Foo extends React.Component {
+ render() {
+ return (
);
+ }
+ }
+
+ class Bar extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+
+ expect(wrapper.render().find('.in-foo')).to.have.lengthOf(1);
+
+ const rendered = wrapper.render();
+ expect(rendered.is('.in-bar')).to.equal(true);
+ expect(rendered).to.have.lengthOf(1);
+
+ const renderedFoo = wrapper.find(Foo).render();
+ expect(renderedFoo.is('.in-foo')).to.equal(true);
+ expect(renderedFoo.is('.in-bar')).to.equal(false);
+ expect(renderedFoo.find('.in-bar')).to.have.lengthOf(0);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('returns a cheerio wrapper around the current node', () => {
+ const Foo = () => (
+
+ );
+
+ const Bar = () => (
+
+
+
+ );
+
+ const wrapper = Wrap( );
+
+ expect(wrapper.render().find('.in-foo')).to.have.lengthOf(1);
+ expect(wrapper.render().is('.in-bar')).to.equal(true);
+
+ const renderedFoo = wrapper.find(Foo).render();
+ expect(renderedFoo.is('.in-foo')).to.equal(true);
+ expect(renderedFoo.is('.in-bar')).to.equal(false);
+ expect(renderedFoo.find('.in-bar')).to.have.lengthOf(0);
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/renderProp.jsx b/packages/enzyme-test-suite/test/shared/methods/renderProp.jsx
new file mode 100644
index 000000000..7006accd2
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/renderProp.jsx
@@ -0,0 +1,161 @@
+import React from 'react';
+import { expect } from 'chai';
+import wrap from 'mocha-wrap';
+import sinon from 'sinon-sandbox';
+
+import getAdapter from 'enzyme/build/getAdapter';
+
+import {
+ describeIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+export default function describeRenderProp({
+ Wrap,
+ WrapRendered,
+ WrapperName,
+}) {
+ wrap()
+ .withConsoleThrows()
+ .describe('.renderProp()', () => {
+ class Foo extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ class Bar extends React.Component {
+ render() {
+ const { render: r } = this.props;
+ return {typeof r === 'function' && r()}
;
+ }
+ }
+ class RendersBar extends React.Component {
+ render() {
+ return ;
+ }
+ }
+
+ it('returns a wrapper around the node returned from the render prop', () => {
+ const wrapperA = Wrap();
+ const renderPropWrapperA = wrapperA.find(Bar).renderProp('render')();
+ expect(renderPropWrapperA.find(Foo)).to.have.lengthOf(1);
+
+ const wrapperB = Wrap( } />
);
+ const renderPropWrapperB = wrapperB.find(Bar).renderProp('render')();
+ expect(renderPropWrapperB.find(Foo)).to.have.lengthOf(1);
+
+ const stub = sinon.stub().returns(
);
+ const wrapperC = Wrap(
);
+ stub.resetHistory();
+ wrapperC.find(Bar).renderProp('render')('one', 'two');
+ expect(stub.args).to.deep.equal([['one', 'two']]);
+ });
+
+ it('throws on a non-string prop name', () => {
+ const wrapper = Wrap( {}} />);
+ expect(() => wrapper.renderProp()).to.throw(
+ TypeError,
+ `${WrapperName}::renderProp(): \`propName\` must be a string`,
+ );
+ });
+
+ it('throws on a missing prop', () => {
+ const wrapper = Wrap( {}} />);
+ expect(() => wrapper.renderProp('nope')).to.throw(
+ Error,
+ `${WrapperName}::renderProp(): no prop called “nope“ found`,
+ );
+ });
+
+ it('throws on a non-function render prop value', () => {
+ const wrapper = Wrap( );
+ expect(() => wrapper.renderProp('render')).to.throw(
+ TypeError,
+ `${WrapperName}::renderProp(): expected prop “render“ to contain a function, but it holds “object“`,
+ );
+ });
+
+ it('throws on host elements', () => {
+ class Div extends React.Component {
+ render() {
+ const { children } = this.props;
+ return {children}
;
+ }
+ }
+
+ const wrapper = WrapRendered(
);
+ expect(wrapper.is('div')).to.equal(true);
+ expect(() => wrapper.renderProp('foo')).to.throw();
+ });
+
+ wrap()
+ .withOverride(() => getAdapter(), 'wrap', () => undefined)
+ .it('throws with a react adapter that lacks a `.wrap`', () => {
+ const wrapper = Wrap();
+ expect(() => wrapper.find(Bar).renderProp('render')).to.throw(RangeError);
+ });
+
+ describeIf(is('>= 16'), 'allows non-nodes', () => {
+ function MyComponent({ val }) {
+ return x} />;
+ }
+
+ function ComponentWithRenderProp({ val, r }) {
+ return r(val);
+ }
+
+ it('works with strings', () => {
+ const wrapper = Wrap( );
+
+ wrapper.find(ComponentWithRenderProp).renderProp('r')('foo');
+
+ wrapper.find(ComponentWithRenderProp).renderProp('r')('');
+ });
+
+ it('works with numbers', () => {
+ const wrapper = Wrap( );
+
+ wrapper.find(ComponentWithRenderProp).renderProp('r')(42);
+
+ wrapper.find(ComponentWithRenderProp).renderProp('r')(0);
+
+ wrapper.find(ComponentWithRenderProp).renderProp('r')(NaN);
+ });
+
+ it('works with null', () => {
+ const wrapper = Wrap( );
+
+ wrapper.find(ComponentWithRenderProp).renderProp('r')(null);
+ });
+
+ // FIXME: figure out how to test this reliably
+ it.skip('throws with undefined', () => {
+ const wrapper = Wrap( );
+
+ expect(() => wrapper.find(ComponentWithRenderProp).renderProp('r')(undefined)).to.throw();
+ });
+
+ it('works with arrays', () => {
+ const wrapper = Wrap( );
+
+ wrapper.find(ComponentWithRenderProp).renderProp('r')([]);
+
+ wrapper.find(ComponentWithRenderProp).renderProp('r')(['a']);
+
+ wrapper.find(ComponentWithRenderProp).renderProp('r')([Infinity]);
+ });
+
+ it('works with false', () => {
+ const wrapper = Wrap( );
+
+ wrapper.find(ComponentWithRenderProp).renderProp('r')(false);
+ });
+
+ it('throws with true', () => {
+ const wrapper = Wrap( );
+
+ expect(() => wrapper.find(ComponentWithRenderProp).renderProp('r')(true).Wrap()).to.throw();
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/root.jsx b/packages/enzyme-test-suite/test/shared/methods/root.jsx
new file mode 100644
index 000000000..12a21fc39
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/root.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ itIf,
+} from '../../_helpers';
+
+export default function describeRoot({
+ Wrap,
+ isMount,
+}) {
+ describe('.root()', () => {
+ class Fixture extends React.Component {
+ render() {
+ return
;
+ }
+ }
+
+ itIf(isMount, 'returns the root component instance', () => {
+ const wrapper = Wrap( );
+ const root = wrapper.root();
+ expect(root.is(Fixture)).to.equal(true);
+ expect(root.childAt(0).children().debug()).to.equal(' \n\n\n ');
+
+ expect(wrapper.find('span').root()).to.equal(root);
+ });
+
+ itIf(!isMount, 'returns the root rendered node', () => {
+ const wrapper = Wrap( );
+ const root = wrapper.root();
+ expect(root.is('div')).to.equal(true);
+ expect(root.children().debug()).to.equal(' \n\n\n ');
+
+ expect(wrapper.find('span').root()).to.equal(root);
+ });
+
+ it('returns the root wrapper from a child', () => {
+ const wrapper = Wrap( );
+ const root = wrapper.root();
+ expect(wrapper.find('span').root()).to.equal(root);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/setContext.jsx b/packages/enzyme-test-suite/test/shared/methods/setContext.jsx
new file mode 100644
index 000000000..2918f5960
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/setContext.jsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+import {
+ createClass,
+} from '../../_helpers/react-compat';
+
+export default function describeSetContext({
+ Wrap,
+ WrapperName,
+}) {
+ describe('.setContext(newContext)', () => {
+ const SimpleComponent = createClass({
+ contextTypes: {
+ name: PropTypes.string,
+ },
+ render() {
+ const { name } = this.context;
+ return {name}
;
+ },
+ });
+
+ it('sets context for a component multiple times', () => {
+ const context = { name: 'foo' };
+ const wrapper = Wrap( , { context });
+ expect(wrapper.text()).to.equal('foo');
+ wrapper.setContext({ name: 'bar' });
+ expect(wrapper.text()).to.equal('bar');
+ wrapper.setContext({ name: 'baz' });
+ expect(wrapper.text()).to.equal('baz');
+ });
+
+ it('throws if it is called when wrapper didn’t include context', () => {
+ const wrapper = Wrap( );
+ expect(() => wrapper.setContext({ name: 'bar' })).to.throw(
+ Error,
+ `${WrapperName}::setContext() can only be called on a wrapper that was originally passed a context option`,
+ );
+ });
+
+ it('throws when not called on the root', () => {
+ const context = { name: };
+ const wrapper = Wrap( , { context });
+ const main = wrapper.find('main');
+ expect(main).to.have.lengthOf(1);
+ expect(() => main.setContext()).to.throw(
+ Error,
+ `${WrapperName}::setContext() can only be called on the root`,
+ );
+ });
+
+ describeIf(is('> 0.13'), 'stateless functional components', () => {
+ const SFC = (props, { name }) => (
+ {name}
+ );
+ SFC.contextTypes = { name: PropTypes.string };
+
+ it('sets context for a component multiple times', () => {
+ const context = { name: 'foo' };
+ const wrapper = Wrap( , { context });
+ expect(wrapper.text()).to.equal('foo');
+ wrapper.setContext({ name: 'bar' });
+ expect(wrapper.text()).to.equal('bar');
+ wrapper.setContext({ name: 'baz' });
+ expect(wrapper.text()).to.equal('baz');
+ });
+
+ it('throws if it is called when shallow didn’t include context', () => {
+ const wrapper = Wrap( );
+ expect(() => wrapper.setContext({ name: 'bar' })).to.throw(
+ Error,
+ `${WrapperName}::setContext() can only be called on a wrapper that was originally passed a context option`,
+ );
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/setProps.jsx b/packages/enzyme-test-suite/test/shared/methods/setProps.jsx
new file mode 100644
index 000000000..6a5d855bd
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/setProps.jsx
@@ -0,0 +1,587 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+import {
+ sym,
+} from 'enzyme/build/Utils';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+export default function describeSetProps({
+ Wrap,
+ WrapperName,
+ isShallow,
+}) {
+ describe('.setProps(newProps[, callback)', () => {
+ class RendersNull extends React.Component {
+ render() {
+ return null;
+ }
+ }
+
+ class Foo extends React.Component {
+ render() {
+ const { id, foo } = this.props;
+ return (
+
+ {foo}
+
+ );
+ }
+ }
+
+ function FooSFC({ id, foo }) {
+ return (
+
+ {foo}
+
+ );
+ }
+
+ class RendersFoo extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ it('throws on a non-function callback', () => {
+ const wrapper = Wrap( );
+
+ expect(() => wrapper.setProps({}, undefined)).to.throw();
+ expect(() => wrapper.setProps({}, null)).to.throw();
+ expect(() => wrapper.setProps({}, false)).to.throw();
+ expect(() => wrapper.setProps({}, true)).to.throw();
+ expect(() => wrapper.setProps({}, [])).to.throw();
+ expect(() => wrapper.setProps({}, {})).to.throw();
+ });
+
+ it('throws when not called on the root', () => {
+ const wrapper = Wrap( );
+ const child = wrapper.find(Foo);
+ expect(child).to.have.lengthOf(1);
+ expect(() => child.setProps({})).to.throw(
+ Error,
+ `${WrapperName}::setProps() can only be called on the root`,
+ );
+ });
+
+ it('sets props for a component multiple times', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.find('.foo')).to.have.lengthOf(1);
+ wrapper.setProps({ id: 'bar', foo: 'bla' });
+ expect(wrapper.find('.bar')).to.have.lengthOf(1);
+ });
+
+ describe('merging props', () => {
+ it('merges, not replaces, props when rerendering', () => {
+ const wrapper = Wrap( );
+ const rendered = () => (isShallow ? wrapper : wrapper.children());
+
+ const expectedPreDebug = isShallow
+ ? `
+
+ bar
+
+ `.trim()
+ : `
+
+
+ bar
+
+
+ `.trim();
+ expect(wrapper.debug()).to.equal(expectedPreDebug);
+ expect(rendered().props()).to.eql({
+ className: 'foo',
+ children: 'bar',
+ });
+ expect(wrapper.instance().props).to.eql({
+ id: 'foo',
+ foo: 'bar',
+ });
+
+ wrapper.setProps({ id: 'bar' });
+
+ const expectedPostDebug = isShallow
+ ? `
+
+ bar
+
+ `.trim()
+ : `
+
+
+ bar
+
+
+ `.trim();
+ expect(wrapper.debug()).to.equal(expectedPostDebug);
+ expect(rendered().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', () => {
+ const wrapper = Wrap( );
+ const rendered = () => (isShallow ? wrapper : wrapper.children());
+
+ const expectedPreDebug = isShallow
+ ? `
+
+ bar
+
+ `.trim()
+ : `
+
+
+ bar
+
+
+ `.trim();
+ expect(wrapper.debug()).to.equal(expectedPreDebug);
+ expect(rendered().props()).to.eql({
+ className: 'foo',
+ children: 'bar',
+ });
+ if (is('< 16')) {
+ expect(wrapper.instance().props).to.eql({
+ id: 'foo',
+ foo: 'bar',
+ });
+ }
+
+ wrapper.setProps({ id: 'bar' });
+
+ const expectedPostDebug = isShallow
+ ? `
+
+ bar
+
+ `.trim()
+ : `
+
+
+ bar
+
+
+ `.trim();
+ expect(wrapper.debug()).to.equal(expectedPostDebug);
+ expect(rendered().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 FooNoUpdate extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+
+ render() {
+ const { id, foo } = this.props;
+ return (
+
+ {foo}
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+ const rendered = () => (isShallow ? wrapper : wrapper.children());
+
+ const expectedPreDebug = isShallow
+ ? `
+
+ bar
+
+ `.trim()
+ : `
+
+
+ bar
+
+
+ `.trim();
+ expect(wrapper.debug()).to.equal(expectedPreDebug);
+ expect(rendered().props()).to.eql({
+ className: 'foo',
+ children: 'bar',
+ });
+ expect(wrapper.instance().props).to.eql({
+ id: 'foo',
+ foo: 'bar',
+ });
+
+ wrapper.setProps({ id: 'foo' });
+
+ expect(wrapper.debug()).to.equal(expectedPreDebug);
+ expect(rendered().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 = {};
+
+ class FooWithLifecycles extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { stateValue };
+ }
+
+ componentWillReceiveProps() {}
+
+ UNSAFE_componentWillReceiveProps() {} // eslint-disable-line camelcase
+
+ render() {
+ const { id } = this.props;
+ const { stateValue: val } = this.state;
+ return (
+
+ {String(val)}
+
+ );
+ }
+ }
+ FooWithLifecycles.contextTypes = {
+ foo() { return null; },
+ };
+ const cWRP = sinon.stub(FooWithLifecycles.prototype, 'componentWillReceiveProps');
+ // eslint-disable-next-line camelcase
+ const U_cWRP = sinon.stub(FooWithLifecycles.prototype, 'UNSAFE_componentWillReceiveProps');
+
+ const nextProps = { id: 'bar', foo: 'bla' };
+ const context = { foo: 'bar' };
+ const wrapper = Wrap( , { context });
+
+ expect(cWRP).to.have.property('callCount', 0);
+ expect(U_cWRP).to.have.property('callCount', 0);
+
+ wrapper.setProps(nextProps);
+
+ expect(cWRP).to.have.property('callCount', 1);
+ expect(cWRP.calledWith(nextProps, context)).to.equal(true);
+
+ if (is('>= 16.3')) {
+ expect(U_cWRP).to.have.property('callCount', 1);
+ expect(U_cWRP.calledWith(nextProps, context)).to.equal(true);
+ }
+ });
+
+ it('merges newProps with oldProps', () => {
+ class RendersBar extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+ class Bar extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+ expect(wrapper.props().a).to.equal('a');
+ expect(wrapper.props().b).to.equal('b');
+
+ wrapper.setProps({ b: 'c', d: 'e' });
+ expect(wrapper.props().a).to.equal('a');
+ expect(wrapper.props().b).to.equal('c');
+ expect(wrapper.props().d).to.equal('e');
+ });
+
+ it('passes in old context', () => {
+ class HasContextX extends React.Component {
+ render() {
+ const { x } = this.context;
+ return (
+ {x}
+ );
+ }
+ }
+ HasContextX.contextTypes = { x: PropTypes.string };
+
+ const context = { x: 'yolo' };
+ const wrapper = Wrap( , { 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 };
+ class HasInitialState extends React.Component {
+ constructor(...args) {
+ super(...args);
+ this.state = initialState;
+ }
+
+ componentWillReceiveProps() {}
+
+ render() {
+ const { className } = this.props;
+ return
;
+ }
+ }
+
+ const cWRP = sinon.stub(HasInitialState.prototype, 'componentWillReceiveProps');
+
+ HasInitialState.defaultProps = {
+ className: 'default-class',
+ };
+ HasInitialState.contextTypes = {
+ b: PropTypes.number,
+ };
+
+ const wrapper = Wrap( , { context });
+
+ // Set undefined in order to use defaultProps if any
+ wrapper.setProps({ className: undefined });
+
+ expect(cWRP).to.have.property('callCount', 1);
+ const [args] = cWRP.args;
+ expect(args).to.eql([
+ { className: HasInitialState.defaultProps.className },
+ context,
+ ]);
+ });
+
+ it('throws if an exception occurs during render', () => {
+ let error;
+ class Trainwreck extends React.Component {
+ render() {
+ const { user } = this.props;
+ try {
+ return (
+
+ {user.name.givenName}
+
+ );
+ } catch (e) {
+ error = e;
+ throw e;
+ }
+ }
+ }
+
+ const validUser = {
+ name: {
+ givenName: 'Brian',
+ },
+ };
+
+ const wrapper = Wrap( );
+
+ expect(() => wrapper.setProps({ user: { name: {} } })).not.to.throw();
+ expect(() => wrapper.setProps({ user: {} })).to.throw(error);
+ });
+
+ it('calls the callback when setProps has completed', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.find('.foo')).to.have.lengthOf(1);
+
+ wrapper[sym('__renderer__')].batchedUpdates(() => {
+ wrapper.setProps({ id: 'bar', foo: 'bla' }, () => {
+ expect(wrapper.find('.bar')).to.have.lengthOf(1);
+ });
+ });
+ expect(wrapper.find('.foo')).to.have.lengthOf(0);
+ });
+
+ it('calls componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, and componentDidUpdate with merged newProps', () => {
+ const spy = sinon.spy();
+
+ class HasLifecycleSpies extends React.Component {
+ componentWillReceiveProps(nextProps) {
+ spy('componentWillReceiveProps', this.props, nextProps);
+ }
+
+ shouldComponentUpdate(nextProps) {
+ spy('shouldComponentUpdate', this.props, nextProps);
+ return true;
+ }
+
+ componentWillUpdate(nextProps) {
+ spy('componentWillUpdate', this.props, nextProps);
+ }
+
+ componentDidUpdate(prevProps) {
+ spy('componentDidUpdate', prevProps, this.props);
+ }
+
+ render() {
+ return (
);
+ }
+ }
+
+ const wrapper = Wrap( );
+
+ wrapper.setProps({ b: 'c', d: 'e' });
+
+ expect(spy.args).to.deep.equal([
+ [
+ 'componentWillReceiveProps',
+ { a: 'a', b: 'b' },
+ { a: 'a', b: 'c', d: 'e' },
+ ],
+ [
+ 'shouldComponentUpdate',
+ { a: 'a', b: 'b' },
+ { a: 'a', b: 'c', d: 'e' },
+ ],
+ [
+ 'componentWillUpdate',
+ { a: 'a', b: 'b' },
+ { a: 'a', b: 'c', d: 'e' },
+ ],
+ [
+ 'componentDidUpdate',
+ { a: 'a', b: 'b' },
+ { a: 'a', b: 'c', d: 'e' },
+ ],
+ ]);
+ });
+
+ describe('setProps does not call componentDidUpdate twice', () => {
+ it('when setState is called in cWRP', () => {
+ class Dummy extends React.Component {
+ constructor(...args) {
+ super(...args);
+
+ this.state = {
+ someState: '',
+ };
+ }
+
+ componentWillReceiveProps({ myProp: someState }) {
+ this.setState({ someState });
+ }
+
+ componentDidUpdate() {}
+
+ render() {
+ const { myProp } = this.props;
+ const { someState } = this.state;
+ return (
+
+ myProp: {myProp}
+ someState: {someState}
+
+ );
+ }
+ }
+
+ const spy = sinon.spy(Dummy.prototype, 'componentDidUpdate');
+ const wrapper = Wrap( );
+ expect(spy).to.have.property('callCount', 0);
+ return new Promise((resolve) => {
+ wrapper.setProps({ myProp: 'Prop Value' }, resolve);
+ }).then(() => {
+ expect(spy).to.have.property('callCount', 1);
+ });
+ });
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('sets props for a component multiple times', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.find('.foo')).to.have.lengthOf(1);
+ wrapper.setProps({ id: 'bar', foo: 'bla' });
+ expect(wrapper.find('.bar')).to.have.lengthOf(1);
+ });
+
+ it('merges newProps with oldProps', () => {
+ const RendersBarSFC = props => (
+
+ );
+ const BarSFC = () => (
+
+ );
+
+ const wrapper = Wrap( );
+ expect(wrapper.props().a).to.equal('a');
+ expect(wrapper.props().b).to.equal('b');
+
+ wrapper.setProps({ b: 'c', d: 'e' });
+ expect(wrapper.props().a).to.equal('a');
+ expect(wrapper.props().b).to.equal('c');
+ expect(wrapper.props().d).to.equal('e');
+ });
+
+ it('passes in old context', () => {
+ const HasContextXSFC = (props, { x }) => (
+ {x}
+ );
+ HasContextXSFC.contextTypes = { x: PropTypes.string };
+
+ const context = { x: 'yolo' };
+ const wrapper = Wrap( , { 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('throws if an exception occurs during render', () => {
+ let error;
+ const Trainwreck = ({ user }) => {
+ try {
+ return (
+
+ {user.name.givenName}
+
+ );
+ } catch (e) {
+ error = e;
+ throw e;
+ }
+ };
+
+ const validUser = {
+ name: {
+ givenName: 'Brian',
+ },
+ };
+
+ const wrapper = Wrap( );
+
+ expect(() => wrapper.setProps({ user: { name: {} } })).not.to.throw();
+ expect(() => wrapper.setProps({ user: {} })).to.throw(error);
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/setState.jsx b/packages/enzyme-test-suite/test/shared/methods/setState.jsx
new file mode 100644
index 000000000..c45cd3753
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/setState.jsx
@@ -0,0 +1,421 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+// some React versions pass undefined as an argument of setState callback.
+const CALLING_SETSTATE_CALLBACK_WITH_UNDEFINED = is('^15.5');
+
+export default function describeSetState({
+ Wrap,
+ WrapperName,
+ isShallow,
+}) {
+ describe('.setState(newState[, callback])', () => {
+ class HasIDState extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { id: 'foo' };
+ }
+
+ componentDidUpdate() {}
+
+ setBadState() {
+ this.setState({}, 1);
+ }
+
+ render() {
+ const { id } = this.state;
+ return (
+
+ );
+ }
+ }
+
+ class HasMountedState extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { mounted: false };
+ }
+
+ componentDidMount() {
+ this.setState({ mounted: true });
+ }
+
+ render() {
+ const { mounted } = this.state;
+ return {mounted ? 'a' : 'b'}
;
+ }
+ }
+
+ class RendersNull extends React.Component {
+ render() {
+ return null;
+ }
+ }
+
+ it('throws on a non-function callback', () => {
+ const wrapper = Wrap( );
+
+ expect(() => wrapper.setState({}, undefined)).to.throw();
+ expect(() => wrapper.setState({}, null)).to.throw();
+ expect(() => wrapper.setState({}, false)).to.throw();
+ expect(() => wrapper.setState({}, true)).to.throw();
+ expect(() => wrapper.setState({}, [])).to.throw();
+ expect(() => wrapper.setState({}, {})).to.throw();
+ });
+
+ it('sets the state of the root node', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.find('.foo')).to.have.lengthOf(1);
+ wrapper.setState({ id: 'bar' });
+ expect(wrapper.find('.bar')).to.have.lengthOf(1);
+ });
+
+ it('allows setState inside of componentDidMount', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.find('div').text()).to.equal('a');
+ });
+
+ it('calls the callback when setState has completed', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.state()).to.eql({ id: 'foo' });
+ return new Promise((resolve) => {
+ wrapper.setState({ id: 'bar' }, function callback(...args) {
+ expect(wrapper.state()).to.eql({ id: 'bar' });
+ expect(this).to.equal(wrapper.instance());
+ expect(this.state).to.eql({ id: 'bar' });
+ expect(wrapper.find('div').prop('className')).to.equal('bar');
+ expect(args).to.eql(CALLING_SETSTATE_CALLBACK_WITH_UNDEFINED ? [undefined] : []);
+ resolve();
+ });
+ });
+ });
+
+ it('prevents the update if nextState is null or undefined', () => {
+ const wrapper = Wrap( );
+ const spy = sinon.spy(wrapper.instance(), 'componentDidUpdate');
+ const callback = sinon.spy();
+ wrapper.setState(() => ({ id: 'bar' }), callback);
+ expect(spy).to.have.property('callCount', 1);
+ expect(callback).to.have.property('callCount', 1);
+
+ wrapper.setState(() => null, callback);
+ expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 2);
+ expect(callback).to.have.property('callCount', 2);
+
+ wrapper.setState(() => undefined, callback);
+ expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 3);
+ expect(callback).to.have.property('callCount', 3);
+ });
+
+ itIf(is('>= 16'), 'prevents an infinite loop if nextState is null or undefined from setState in CDU', () => {
+ let payload;
+ const stub = sinon.stub(HasIDState.prototype, 'componentDidUpdate')
+ .callsFake(function componentDidUpdate() { this.setState(() => payload); });
+
+ const wrapper = Wrap( );
+
+ wrapper.setState(() => ({ id: 'bar' }));
+ expect(stub).to.have.property('callCount', 1);
+
+ payload = null;
+ wrapper.setState(() => ({ id: 'bar' }));
+ expect(stub).to.have.property('callCount', 2);
+ });
+
+ describe('does not call componentWillReceiveProps after setState is called', () => {
+ it('does not call componentWillReceiveProps upon rerender', () => {
+ class A extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = { a: 0 };
+ }
+
+ componentWillReceiveProps() {
+ this.setState({ a: 1 });
+ }
+
+ render() {
+ const { a } = this.state;
+ return ({a}
);
+ }
+ }
+ const spy = sinon.spy(A.prototype, 'componentWillReceiveProps');
+
+ const wrapper = Wrap( , { disableLifecycleMethods: true });
+
+ wrapper.setState({ a: 2 });
+ expect(wrapper.state('a')).to.equal(2);
+
+ expect(spy).to.have.property('callCount', 0);
+ wrapper.setProps({});
+ expect(spy).to.have.property('callCount', 1);
+ expect(wrapper.state('a')).to.equal(1);
+
+ return new Promise((resolve) => {
+ wrapper.setState({ a: 3 }, resolve);
+ }).then(() => {
+ expect(spy).to.have.property('callCount', 1);
+ expect(wrapper.state('a')).to.equal(3);
+ });
+ });
+
+ it('does not call componentWillReceiveProps with multiple keys in props', () => {
+ class B extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { a: 0, b: 1 };
+ }
+
+ componentWillReceiveProps() {
+ this.setState({ b: 0, a: 1 });
+ }
+
+ render() {
+ const { a, b } = this.state;
+ return (
+
+ {a + b}
+
+ );
+ }
+ }
+ const spy = sinon.spy(B.prototype, 'componentWillReceiveProps');
+
+ const wrapper = Wrap( , { disableLifecycleMethods: true });
+
+ wrapper.setState({ a: 2 });
+ expect(wrapper.state('a')).to.equal(2);
+ expect(wrapper.state('b')).to.equal(1);
+
+ expect(spy).to.have.property('callCount', 0);
+ wrapper.setProps({});
+ expect(spy).to.have.property('callCount', 1);
+ expect(wrapper.state('a')).to.equal(1);
+
+ return Promise.all([
+ new Promise((resolve) => { wrapper.setState({ b: 5 }, resolve); }),
+ new Promise((resolve) => { wrapper.setState({ a: 10 }, resolve); }),
+ ]).then(() => {
+ expect(spy).to.have.property('callCount', 1);
+ expect(wrapper.state('b')).to.equal(5);
+ expect(wrapper.state('a')).to.equal(10);
+ });
+ });
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('throws when trying to access state', () => {
+ const Foo = () => (
+ abc
+ );
+
+ const wrapper = Wrap( );
+
+ expect(() => wrapper.state()).to.throw(
+ Error,
+ `${WrapperName}::state() can only be called on class components`,
+ );
+ });
+
+ it('throws when trying to set state', () => {
+ const Foo = () => (
+ abc
+ );
+
+ const wrapper = Wrap( );
+
+ expect(() => wrapper.setState({ a: 1 })).to.throw(
+ Error,
+ `${WrapperName}::setState() can only be called on class components`,
+ );
+ });
+ });
+
+ it('throws an error when cb is not a function', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.state()).to.eql({ id: 'foo' });
+ expect(() => wrapper.setState({ id: 'bar' }, 1)).to.throw(Error);
+ expect(() => wrapper.instance().setBadState()).to.throw(Error);
+ });
+
+ it('does not throw with a null/undefined callback', () => {
+ class Foo extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {};
+ }
+
+ setStateWithNullishCallback() {
+ this.setState({}, null);
+ this.setState({}, undefined);
+ }
+
+ render() {
+ return null;
+ }
+ }
+
+ const wrapper = Wrap( );
+ expect(() => wrapper.instance().setStateWithNullishCallback()).not.to.throw();
+ });
+
+ it('preserves the receiver', () => {
+ class Comp extends React.Component {
+ constructor(...args) {
+ super(...args);
+
+ this.state = {
+ key: '',
+ };
+
+ this.instanceFunction = () => this.setState(() => ({ key: 'value' }));
+ }
+
+ componentDidMount() {
+ this.instanceFunction();
+ }
+
+ render() {
+ const { key } = this.state;
+ // FIXME: is this right?
+ return key ? null : null;
+ }
+ }
+
+ expect(Wrap( ).debug()).to.equal(isShallow ? '' : ' ');
+ });
+
+ describe('child components', () => {
+ class Child extends React.Component {
+ constructor(...args) {
+ super(...args);
+ this.state = { state: 'a' };
+ }
+
+ render() {
+ const { prop } = this.props;
+ const { state } = this.state;
+ return (
+
+ {prop} - {state}
+
+ );
+ }
+ }
+
+ class Parent extends React.Component {
+ constructor(...args) {
+ super(...args);
+ this.state = { childProp: 1 };
+ }
+
+ render() {
+ const { childProp } = this.state;
+ return ;
+ }
+ }
+
+ it('sets the state of a stateful root', () => {
+ const wrapper = Wrap( );
+
+ const expectedDebug = isShallow
+ ? ' '
+ : `
+
+
+ 1
+ -${' '}
+ a
+
+
+ `;
+ expect(wrapper.debug()).to.equal(expectedDebug);
+
+ return new Promise((resolve) => {
+ wrapper.setState({ childProp: 2 }, () => {
+ const expectedPostDebug = isShallow
+ ? ' '
+ : `
+
+
+ 2
+ -${' '}
+ a
+
+
+ `;
+ expect(wrapper.debug()).to.equal(expectedPostDebug);
+ resolve();
+ });
+ });
+ });
+
+ itIf(isShallow, 'can not set the state of the stateful child of a stateful root', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.debug()).to.equal(' ');
+
+ const child = wrapper.find(Child);
+ expect(() => child.setState({ state: 'b' })).to.throw(
+ Error,
+ `${WrapperName}::setState() can only be called on the root`,
+ );
+ });
+
+ itIf(!isShallow, 'sets the state of the stateful child of a stateful root', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.text().trim()).to.equal('1 - a');
+
+ const child = wrapper.find(Child);
+ return new Promise((resolve) => {
+ child.setState({ state: 'b' }, () => {
+ expect(wrapper.text().trim()).to.equal('1 - b');
+ resolve();
+ });
+ });
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ function SFC(props) {
+ return ;
+ }
+
+ itIf(isShallow, 'can not set the state of the stateful child of a stateless root', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.text().trim()).to.equal(' ');
+
+ const child = wrapper.find(Child);
+ expect(() => child.setState({ state: 'b' })).to.throw(
+ Error,
+ `${WrapperName}::setState() can only be called on the root`,
+ );
+ });
+
+ itIf(!isShallow, 'sets the state of the stateful child of a stateless root', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.text().trim()).to.equal('1 - a');
+
+ const child = wrapper.find(Child);
+ return new Promise((resolve) => {
+ child.setState({ state: 'b' }, () => {
+ expect(wrapper.text().trim()).to.equal('1 - b');
+ resolve();
+ });
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/simulate.jsx b/packages/enzyme-test-suite/test/shared/methods/simulate.jsx
new file mode 100644
index 000000000..df7dcd3d3
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/simulate.jsx
@@ -0,0 +1,291 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import {
+ is,
+ REACT16,
+} from '../../_helpers/version';
+
+export default function describeSimulate({
+ Wrap,
+ WrapperName,
+ isShallow,
+ isMount,
+}) {
+ // The shallow renderer in react 16 does not yet support batched updates. When it does,
+ // we should be able to go un-skip all of the tests that are skipped with this flag.
+ // FIXME: fix this
+ const BATCHING = !isShallow || !REACT16;
+
+ describe('.simulate(eventName, data)', () => {
+ it('simulates events', () => {
+ class ClickCounter extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { count: 0 };
+ this.incrementCount = this.incrementCount.bind(this);
+ }
+
+ incrementCount() {
+ this.setState(({ count }) => ({ count: count + 1 }));
+ }
+
+ render() {
+ const { count } = this.state;
+ return (
+
+ foo
+
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+
+ expect(wrapper.find('a').prop('data-count')).to.have.equal(0);
+ wrapper.simulate('click');
+ expect(wrapper.find('a').prop('data-count')).to.have.equal(1);
+ });
+
+ itIf(!isShallow, 'throws a descriptive error for invalid events', () => {
+ const wrapper = Wrap(foo
);
+ expect(() => wrapper.simulate('invalidEvent')).to.throw(
+ TypeError,
+ `${WrapperName}::simulate() event 'invalidEvent' does not exist`,
+ );
+ });
+
+ // FIXME: figure out why this hangs forever
+ itIf(!isMount, 'passes in event data', () => {
+ const spy = sinon.spy();
+ class Clicker extends React.Component {
+ render() {
+ return (foo );
+ }
+ }
+
+ const wrapper = Wrap( );
+ const a = {};
+ const b = {};
+
+ wrapper.simulate('click', a, b);
+ expect(spy).to.have.property('callCount', 1);
+ expect(spy.args[0][0]).to.equal(a);
+ if (!isMount) {
+ expect(spy.args[0][1]).to.equal(b);
+ }
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ const ClickerSFC = ({ onClick }) => (foo );
+
+ it('simulates events', () => {
+ const spy = sinon.spy();
+ const wrapper = Wrap( );
+
+ expect(spy).to.have.property('callCount', 0);
+ wrapper.find('a').simulate('click');
+ expect(spy).to.have.property('callCount', 1);
+ });
+
+ // FIXME: figure out why this hangs forever
+ itIf(!isMount, 'passes in event data', () => {
+ const spy = sinon.spy();
+ const wrapper = Wrap( );
+ const a = {};
+ const b = {};
+
+ wrapper.simulate('click', a, b);
+ expect(spy).to.have.property('callCount', 1);
+ expect(spy.args[0][0]).to.equal(a);
+ if (!isMount) {
+ expect(spy.args[0][1]).to.equal(b);
+ }
+ });
+ });
+
+ describe('Normalizing JS event names', () => {
+ it('converts lowercase events to React camelcase', () => {
+ const spy = sinon.spy();
+ const clickSpy = sinon.spy();
+ class Clicks extends React.Component {
+ render() {
+ return (foo );
+ }
+ }
+
+ const wrapper = Wrap( );
+
+ wrapper.simulate('dblclick');
+ expect(spy).to.have.property('callCount', 1);
+
+ wrapper.simulate('click');
+ expect(clickSpy).to.have.property('callCount', 1);
+ });
+
+ describeIf(is('> 0.13'), 'normalizing mouseenter', () => {
+ it('converts lowercase events to React camelcase', () => {
+ const spy = sinon.spy();
+ class Mousetrap extends React.Component {
+ render() {
+ return (
+ foo
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+
+ wrapper.simulate('mouseenter');
+ expect(spy).to.have.property('callCount', 1);
+ });
+
+ it('converts lowercase events to React camelcase in SFCs', () => {
+ const spy = sinon.spy();
+ const MousetrapSFC = () => (
+ foo
+ );
+
+ const wrapper = Wrap( );
+
+ wrapper.simulate('mouseenter');
+ expect(spy).to.have.property('callCount', 1);
+ });
+ });
+
+ describeIf(is('>= 15'), 'animation events', () => {
+ it('converts lowercase events to React camelcase', () => {
+ const spy = sinon.spy();
+ class Animator extends React.Component {
+ render() {
+ return (
+ foo
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+
+ wrapper.simulate('animationiteration');
+ expect(spy).to.have.property('callCount', 1);
+ });
+
+ it('converts lowercase events to React camelcase in stateless components', () => {
+ const spy = sinon.spy();
+ const AnimatorSFC = () => (foo );
+
+ const wrapper = Wrap( );
+
+ wrapper.simulate('animationiteration');
+ expect(spy).to.have.property('callCount', 1);
+ });
+ });
+
+ describeIf(is('>= 16.4'), 'pointer events', () => {
+ it('converts lowercase events to React camelcase', () => {
+ const spy = sinon.spy();
+ class Fingertrap extends React.Component {
+ render() {
+ return (foo );
+ }
+ }
+
+ const wrapper = Wrap( );
+
+ wrapper.simulate('gotpointercapture');
+ expect(spy).to.have.property('callCount', 1);
+ });
+
+ it('converts lowercase events to React camelcase in stateless components', () => {
+ const spy = sinon.spy();
+ const FingertrapSFC = () => (foo );
+
+ const wrapper = Wrap( );
+
+ wrapper.simulate('gotpointercapture');
+ expect(spy).to.have.property('callCount', 1);
+ });
+ });
+ });
+
+ itIf(BATCHING, 'has batched updates', () => {
+ let renderCount = 0;
+ class Multistate extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { count: 0 };
+ this.onClick = this.onClick.bind(this);
+ }
+
+ onClick() {
+ // eslint-disable-next-line react/destructuring-assignment, react/no-access-state-in-setstate
+ this.setState({ count: this.state.count + 1 });
+ // eslint-disable-next-line react/destructuring-assignment, react/no-access-state-in-setstate
+ this.setState({ count: this.state.count + 1 });
+ }
+
+ render() {
+ renderCount += 1;
+ const { count } = this.state;
+ return (
+ {count}
+ );
+ }
+ }
+
+ const wrapper = Wrap( );
+ wrapper.simulate('click');
+ expect(wrapper.text()).to.equal('1');
+ expect(renderCount).to.equal(2);
+ });
+
+ it('chains', () => {
+ const wrapper = Wrap(
);
+ expect(wrapper.simulate('click')).to.equal(wrapper);
+ });
+
+ describe('works with .parent()/.parents()/.closest()', () => {
+ let onClick;
+ let wrapper;
+ beforeEach(() => {
+ onClick = sinon.stub();
+ wrapper = Wrap((
+
+
+ click me
+
+
+ ));
+ });
+
+ itIf(!isShallow, 'child should fire onClick', () => {
+ wrapper.find('.child-elem').simulate('click');
+ expect(onClick).to.have.property('callCount', 1);
+ });
+
+ it('parents should fire onClick', () => {
+ wrapper.find('.child-elem').parents('.parent-elem').simulate('click');
+ expect(onClick).to.have.property('callCount', 1);
+ });
+
+ it('closest should fire onClick', () => {
+ wrapper.find('.child-elem').closest('.parent-elem').simulate('click');
+ expect(onClick).to.have.property('callCount', 1);
+ });
+
+ // FIXME: figure out why this breaks in `mount` with "Cannot read property '__reactInternalInstance$ukkwkcm5yvc' of null"
+ itIf(!isMount, 'parent should fire onClick', () => {
+ wrapper.find('.child-elem').parent().simulate('click');
+ expect(onClick).to.have.property('callCount', 1);
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/simulateError.jsx b/packages/enzyme-test-suite/test/shared/methods/simulateError.jsx
new file mode 100644
index 000000000..f892279b1
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/simulateError.jsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+import {
+ sym,
+} from 'enzyme/build/Utils';
+
+import {
+ itIf,
+} from '../../_helpers';
+
+export default function describeSimulateError({
+ Wrap,
+ WrapRendered,
+ isShallow,
+}) {
+ describe('.simulateError(error)', () => {
+ class Div extends React.Component {
+ render() {
+ const { children } = this.props;
+ return {children}
;
+ }
+ }
+
+ class Spans extends React.Component {
+ render() {
+ return
;
+ }
+ }
+
+ class Nested extends React.Component {
+ render() {
+ return
;
+ }
+ }
+
+ it('throws on host elements', () => {
+ const wrapper = WrapRendered(
);
+ expect(wrapper.is('div')).to.equal(true);
+ expect(() => wrapper.simulateError()).to.throw();
+ });
+
+ it('throws on "not one" node', () => {
+ const wrapper = Wrap( );
+
+ const spans = wrapper.find('span');
+ expect(spans).to.have.lengthOf(2);
+ expect(() => spans.simulateError()).to.throw();
+
+ const navs = wrapper.find('nav');
+ expect(navs).to.have.lengthOf(0);
+ expect(() => navs.simulateError()).to.throw();
+ });
+
+ it('throws when the renderer lacks `simulateError`', () => {
+ const wrapper = Wrap( );
+ delete wrapper[sym('__renderer__')].simulateError;
+ expect(() => wrapper.simulateError()).to.throw();
+ try {
+ wrapper.simulateError();
+ } catch (e) {
+ expect(e).not.to.equal(undefined);
+ }
+ });
+
+ context('calls through to renderer’s `simulateError`', () => {
+ let hierarchy;
+ beforeEach(() => {
+ const wrapper = WrapRendered( );
+ const stub = sinon.stub().callsFake((_, __, e) => { throw e; });
+ wrapper[sym('__renderer__')].simulateError = stub;
+ const error = new Error('hi');
+ expect(() => wrapper.simulateError(error)).to.throw(error);
+ expect(stub).to.have.property('callCount', 1);
+
+ const [args] = stub.args;
+ expect(args).to.have.lengthOf(3);
+ const [h, rootNode, actualError] = args;
+ expect(actualError).to.equal(error);
+ expect(rootNode).to.eql(wrapper[sym('__root__')].getNodeInternal());
+
+ hierarchy = h;
+ expect(hierarchy).not.to.have.lengthOf(0);
+
+ const [divNode] = hierarchy;
+ expect(divNode).to.contain.keys({
+ type: Div,
+ nodeType: 'class',
+ rendered: {
+ type: Spans,
+ nodeType: 'class',
+ rendered: null,
+ },
+ });
+ });
+
+ itIf(isShallow, 'calls through to renderer’s `simulateError`', () => {
+ expect(hierarchy).to.have.lengthOf(1);
+ });
+
+ itIf(!isShallow, 'calls through to renderer’s `simulateError`', () => {
+ expect(hierarchy).to.have.lengthOf(2);
+ const [, spanNode] = hierarchy;
+ expect(spanNode).to.contain.keys({
+ type: Spans,
+ nodeType: 'class',
+ rendered: null,
+ });
+ });
+ });
+
+ it('returns the wrapper', () => {
+ const wrapper = WrapRendered( );
+ wrapper[sym('__renderer__')].simulateError = sinon.stub();
+ expect(wrapper.simulateError()).to.equal(wrapper);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/single.jsx b/packages/enzyme-test-suite/test/shared/methods/single.jsx
new file mode 100644
index 000000000..e4db9b12b
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/single.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ sym,
+} from 'enzyme/build/Utils';
+
+export default function describeSingle({
+ Wrap,
+}) {
+ describe('#single()', () => {
+ it('throws if run on multiple nodes', () => {
+ const wrapper = Wrap(
).children();
+ expect(wrapper).to.have.lengthOf(2);
+ expect(() => wrapper.single('name!')).to.throw(
+ Error,
+ 'Method “name!” is meant to be run on 1 node. 2 found instead.',
+ );
+ });
+
+ it('throws if run on zero nodes', () => {
+ const wrapper = Wrap(
).children();
+ expect(wrapper).to.have.lengthOf(0);
+ expect(() => wrapper.single('name!')).to.throw(
+ Error,
+ 'Method “name!” is meant to be run on 1 node. 0 found instead.',
+ );
+ });
+
+ it('throws if run on zero nodes', () => {
+ const wrapper = Wrap(
).children();
+ expect(wrapper).to.have.lengthOf(0);
+ expect(() => wrapper.single('name!')).to.throw(
+ Error,
+ 'Method “name!” is meant to be run on 1 node. 0 found instead.',
+ );
+ });
+
+ it('works with a name', () => {
+ const wrapper = Wrap(
);
+ wrapper.single('foo', (node) => {
+ expect(node).to.equal(wrapper[sym('__node__')]);
+ });
+ });
+
+ it('works without a name', () => {
+ const wrapper = Wrap(
);
+ wrapper.single((node) => {
+ expect(node).to.equal(wrapper[sym('__node__')]);
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/slice.jsx b/packages/enzyme-test-suite/test/shared/methods/slice.jsx
new file mode 100644
index 000000000..ff3fb0061
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/slice.jsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeSlice({
+ Wrap,
+}) {
+ describe('.slice([begin[, end]])', () => {
+ it('returns an identical wrapper if no params are set', () => {
+ const wrapper = Wrap((
+
+ ));
+ const slice = wrapper.find('.foo').slice();
+ expect(slice).to.have.lengthOf(3);
+ expect(slice.at(0).hasClass('bax')).to.equal(true);
+ expect(slice.at(1).hasClass('bar')).to.equal(true);
+ expect(slice.at(2).hasClass('baz')).to.equal(true);
+ });
+
+ it('returns a new wrapper if begin is set', () => {
+ const wrapper = Wrap((
+
+ ));
+ const slice = wrapper.find('.foo').slice(1);
+ expect(slice).to.have.lengthOf(2);
+ expect(slice.at(0).hasClass('bar')).to.equal(true);
+ expect(slice.at(1).hasClass('baz')).to.equal(true);
+ });
+
+ it('returns a new wrapper if begin and end are set', () => {
+ const wrapper = Wrap((
+
+ ));
+ const slice = wrapper.find('.foo').slice(1, 2);
+ expect(slice).to.have.lengthOf(1);
+ expect(slice.at(0).hasClass('bar')).to.equal(true);
+ });
+
+ it('returns a new wrapper if begin and end are set (negative)', () => {
+ const wrapper = Wrap((
+
+ ));
+ const slice = wrapper.find('.foo').slice(-2, -1);
+ expect(slice).to.have.lengthOf(1);
+ expect(slice.at(0).hasClass('bar')).to.equal(true);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/some.jsx b/packages/enzyme-test-suite/test/shared/methods/some.jsx
new file mode 100644
index 000000000..229b48cd1
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/some.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeSome({
+ Wrap,
+ WrapperName,
+}) {
+ describe('.some(selector)', () => {
+ it('returns if a node matches a selector', () => {
+ const wrapper = Wrap((
+
+ ));
+ const foo = wrapper.find('.foo');
+ expect(foo.some('.qoo')).to.equal(true);
+ expect(foo.some('.foo')).to.equal(true);
+ expect(foo.some('.bar')).to.equal(false);
+ });
+
+ it('throws if called on root', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(() => wrapper.some('.foo')).to.throw(
+ Error,
+ `${WrapperName}::some() can not be called on the root`,
+ );
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/someWhere.jsx b/packages/enzyme-test-suite/test/shared/methods/someWhere.jsx
new file mode 100644
index 000000000..48207c1c2
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/someWhere.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { expect } from 'chai';
+
+export default function describeSomeWhere({
+ Wrap,
+}) {
+ describe('.someWhere(predicate)', () => {
+ it('returns if a node matches a predicate', () => {
+ const wrapper = Wrap((
+
+ ));
+ const foo = wrapper.find('.foo');
+ expect(foo.someWhere(n => n.hasClass('qoo'))).to.equal(true);
+ expect(foo.someWhere(n => n.hasClass('foo'))).to.equal(true);
+ expect(foo.someWhere(n => n.hasClass('bar'))).to.equal(false);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/state.jsx b/packages/enzyme-test-suite/test/shared/methods/state.jsx
new file mode 100644
index 000000000..a09a528d3
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/state.jsx
@@ -0,0 +1,148 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+import {
+ createPortal,
+} from '../../_helpers/react-compat';
+
+export default function describeState({
+ Wrap,
+ WrapperName,
+ isShallow,
+ makeDOMElement,
+}) {
+ describe('.state([name])', () => {
+ class HasFooState extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { foo: 'foo' };
+ }
+
+ render() {
+ const { foo } = this.state;
+ return {foo}
;
+ }
+ }
+
+ it('returns the state object', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.state()).to.eql({ foo: 'foo' });
+ });
+
+ it('returns the current state after state transitions', () => {
+ const wrapper = Wrap( );
+ wrapper.setState({ foo: 'bar' });
+ expect(wrapper.state()).to.eql({ foo: 'bar' });
+ });
+
+ it('allows a state property name be passed in as an argument', () => {
+ const wrapper = Wrap( );
+ expect(wrapper.state('foo')).to.equal('foo');
+ });
+
+ it('throws on host nodes', () => {
+ const wrapper = Wrap(
);
+
+ expect(() => wrapper.state()).to.throw(Error, `${WrapperName}::state() can only be called on class components`);
+ });
+
+ itIf(is('>= 16'), 'throws on Portals', () => {
+ const containerDiv = makeDOMElement();
+ const portal = createPortal(
+
,
+ containerDiv,
+ );
+
+ const wrapper = Wrap({portal}
);
+ expect(() => wrapper.state()).to.throw(Error, `${WrapperName}::state() can only be called on class components`);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('throws on SFCs', () => {
+ function FooSFC() {
+ return
;
+ }
+
+ const wrapper = Wrap( );
+ expect(() => wrapper.state()).to.throw(Error, `${WrapperName}::state() can only be called on class components`);
+ });
+ });
+
+ describe('child components', () => {
+ class Child extends React.Component {
+ constructor(...args) {
+ super(...args);
+ this.state = { state: 'a' };
+ }
+
+ render() {
+ const { prop } = this.props;
+ const { state } = this.state;
+ return (
+
+ {prop} - {state}
+
+ );
+ }
+ }
+
+ class Parent extends React.Component {
+ constructor(...args) {
+ super(...args);
+ this.state = { childProp: 1 };
+ }
+
+ render() {
+ const { childProp } = this.state;
+ return ;
+ }
+ }
+
+ it('gets the state of a stateful parent', () => {
+ const wrapper = Wrap( );
+
+ expect(wrapper.state()).to.eql({ childProp: 1 });
+ });
+
+ itIf(isShallow, 'can not get the state of the stateful child of a stateful root', () => {
+ const wrapper = Wrap( );
+
+ const child = wrapper.find(Child);
+ expect(() => child.state()).to.throw(Error, `${WrapperName}::state() can only be called on the root`);
+ });
+
+ itIf(!isShallow, 'gets the state of the stateful child of a stateful root', () => {
+ const wrapper = Wrap( );
+
+ const child = wrapper.find(Child);
+ expect(child.state()).to.eql({ state: 'a' });
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ function StatelessParent(props) {
+ return ;
+ }
+
+ itIf(isShallow, 'can not get the state of the stateful child of a stateless root', () => {
+ const wrapper = Wrap( );
+
+ const child = wrapper.find(Child);
+ expect(() => child.state()).to.throw(Error, `${WrapperName}::state() can only be called on the root`);
+ });
+
+ itIf(!isShallow, 'gets the state of the stateful child of a stateless root', () => {
+ const wrapper = Wrap( );
+
+ const child = wrapper.find(Child);
+ expect(child.state()).to.eql({ state: 'a' });
+ });
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/tap.jsx b/packages/enzyme-test-suite/test/shared/methods/tap.jsx
new file mode 100644
index 000000000..551553193
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/tap.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeTap({
+ Wrap,
+}) {
+ describe('.tap()', () => {
+ it('calls the passed function with current Wrapper and returns itself', () => {
+ const spy = sinon.spy();
+ const wrapper = Wrap((
+
+ )).find('li');
+ const result = wrapper.tap(spy);
+ expect(spy.calledWith(wrapper)).to.equal(true);
+ expect(result).to.equal(wrapper);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/text.jsx b/packages/enzyme-test-suite/test/shared/methods/text.jsx
new file mode 100644
index 000000000..4695bdc63
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/text.jsx
@@ -0,0 +1,260 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import { render } from 'enzyme';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+import { is } from '../../_helpers/version';
+
+import {
+ Fragment,
+} from '../../_helpers/react-compat';
+
+export default function describeText({
+ Wrap,
+ isShallow,
+}) {
+ describe('.text()', () => {
+ const matchesRender = function matchesRender(node) {
+ const actual = Wrap(node).text();
+ const expected = render(node).text();
+ expect(expected).to.equal(actual);
+ };
+
+ it('handles simple text nodes', () => {
+ const wrapper = Wrap((
+ some text
+ ));
+ expect(wrapper.text()).to.equal('some text');
+ });
+
+ it('handles nodes with mapped children', () => {
+ class Foo extends React.Component {
+ render() {
+ const { items } = this.props;
+ return (
+
+ {items.map(x => x)}
+
+ );
+ }
+ }
+ matchesRender( );
+ matchesRender((
+ abc,
+ def ,
+ hij ,
+ ]}
+ />
+ ));
+ });
+
+ context('composite components', () => {
+ class Foo extends React.Component {
+ render() { return foo
; }
+ }
+
+ itIf(isShallow, 'renders dumbly', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.text()).to.equal(' test');
+ });
+
+ itIf(!isShallow, 'renders smartly', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.text()).to.equal('footest');
+ });
+ });
+
+ it('handles html entities', () => {
+ matchesRender(>
);
+ });
+
+ it('handles spaces the same between shallow and mount', () => {
+ const Space = (
+
+
test
+
Hello
+
+
+ World
+
+
Hello World
+
Hello
+ World
+
+
Hello World
+
+
+
+ );
+
+ const wrapper = Wrap(Space);
+
+ expect(wrapper.text()).to.equal(' test Hello WorldHello WorldHello WorldHello World ');
+ });
+
+ it('handles non-breaking spaces correctly', () => {
+ class Foo extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+ const wrapper = Wrap( );
+ const charCodes = wrapper.text().split('').map(x => x.charCodeAt(0));
+ expect(charCodes).to.eql([
+ 0x00a0, // non-breaking space
+ 0x20, // normal space
+ 0x00a0, // non-breaking space
+ ]);
+ });
+
+ describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => {
+ it('handles nodes with mapped children', () => {
+ const Foo = ({ items }) => (
+
+ {items.map(x => x)}
+
+ );
+ matchesRender( );
+ matchesRender((
+ abc,
+ def ,
+ hij ,
+ ]}
+ />
+ ));
+ });
+
+ const FooSFC = () => (
+ foo
+ );
+
+ itIf(isShallow, 'renders composite components dumbly', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.text()).to.equal(' test');
+ });
+
+ itIf(!isShallow, 'renders composite components smartly', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.text()).to.equal('footest');
+ });
+ });
+
+ it('renders falsy numbers', () => {
+ [0, -0, '0', NaN].forEach((x) => {
+ const wrapper = Wrap({x}
);
+ expect(wrapper.text()).to.equal(String(x));
+ });
+ });
+
+ describe('text content with curly braces', () => {
+ it('handles literal strings', () => {
+ const wrapper = Wrap();
+ expect(wrapper.text()).to.equal('{}');
+ });
+
+ // FIXME: fix for shallow
+ itIf(!isShallow, 'handles innerHTML', () => {
+ const wrapper = Wrap((
+
+ ));
+ expect(wrapper.text()).to.equal('{ some text }');
+ });
+ });
+
+ describeIf(is('> 16.2'), 'fragments', () => {
+ class FragmentClassExample extends React.Component {
+ render() {
+ return (
+
+ Foo
+ Bar
+
+ );
+ }
+ }
+
+ const FragmentConstExample = () => (
+
+ Foo
+ Bar
+
+ );
+
+ it('correctly gets text for both children for class', () => {
+ const classWrapper = Wrap( );
+ expect(classWrapper.text()).to.include('Foo');
+ expect(classWrapper.text()).to.include('Bar');
+ });
+
+ it('correctly gets text for both children for const', () => {
+ const constWrapper = Wrap( );
+ expect(constWrapper.text()).to.include('Foo');
+ expect(constWrapper.text()).to.include('Bar');
+ });
+
+ it('works with a nested component', () => {
+ const Title = ({ children }) => {children} ;
+ const Foobar = () => (
+
+ Foo
+ Bar
+
+ );
+
+ const wrapper = Wrap( );
+ const text = wrapper.text();
+ const expectedDebug = isShallow
+ ? `
+
+ Foo
+
+ Bar
+ `
+ : `
+
+
+ Foo
+
+
+ Bar
+ `;
+ expect(wrapper.debug()).to.equal(expectedDebug);
+ expect(text).to.equal(isShallow ? ' Bar' : 'FooBar');
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/unmount.jsx b/packages/enzyme-test-suite/test/shared/methods/unmount.jsx
new file mode 100644
index 000000000..bcc65f92a
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/unmount.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+export default function describeUnmount({
+ Wrap,
+}) {
+ describe('.unmount()', () => {
+ class WillUnmount extends React.Component {
+ componentWillUnmount() {}
+
+ render() {
+ const { id } = this.props;
+ return (
+
+ {id}
+
+ );
+ }
+ }
+
+ it('calls componentWillUnmount()', () => {
+ const spy = sinon.spy(WillUnmount.prototype, 'componentWillUnmount');
+ const wrapper = Wrap( );
+ expect(spy).to.have.property('callCount', 0);
+
+ wrapper.unmount();
+
+ expect(spy).to.have.property('callCount', 1);
+ const [args] = spy.args;
+ expect(args).to.eql([]);
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/methods/wrap.jsx b/packages/enzyme-test-suite/test/shared/methods/wrap.jsx
new file mode 100644
index 000000000..befc3a880
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/methods/wrap.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ itIf,
+} from '../../_helpers';
+
+export default function describeWrap({
+ Wrap,
+ Wrapper,
+ isShallow,
+ isMount,
+}) {
+ describe('.wrap()', () => {
+ class Foo extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ it('returns itself when it is already a Wrapper', () => {
+ const wrapperDiv = Wrap(
);
+ const wrapperFoo = Wrap( );
+
+ expect(wrapperDiv.wrap(wrapperFoo)).to.equal(wrapperFoo);
+ expect(wrapperFoo.wrap(wrapperDiv)).to.equal(wrapperDiv);
+ });
+
+ itIf(isShallow, 'wraps when it is not already a Wrapper', () => {
+ const wrapper = Wrap( );
+ const el = wrapper.find('a').at(1);
+ const wrappedEl = wrapper.wrap(el.getElement());
+ expect(wrappedEl).to.be.instanceOf(Wrapper);
+ expect(wrappedEl.props()).to.eql(el.props());
+
+ expect(wrappedEl.shallow().debug()).to.equal(el.debug());
+ });
+
+ itIf(isMount, 'wraps when it is not already a Wrapper', () => {
+ const wrapper = Wrap( );
+ const el = wrapper.find('a').at(1);
+ const wrappedEl = wrapper.wrap(el.getElement());
+ expect(wrappedEl).to.be.instanceOf(Wrapper);
+ expect(wrappedEl.props()).to.eql(el.props());
+
+ // FIXME: enable this instead of that:
+ // expect(wrappedEl.mount().debug()).to.equal(el.debug());
+ expect(wrappedEl.debug()).to.equal(' ');
+ });
+ });
+}
diff --git a/packages/enzyme/src/ReactWrapper.js b/packages/enzyme/src/ReactWrapper.js
index 87c048713..64ed81e16 100644
--- a/packages/enzyme/src/ReactWrapper.js
+++ b/packages/enzyme/src/ReactWrapper.js
@@ -794,15 +794,15 @@ class ReactWrapper {
throw new TypeError('ReactWrapper::renderProp() can only be called on custom components');
}
if (typeof propName !== 'string') {
- throw new TypeError('`propName` must be a string');
+ throw new TypeError('ReactWrapper::renderProp(): `propName` must be a string');
}
const props = this.props();
if (!has(props, propName)) {
- throw new Error(`no prop called “${propName}“ found`);
+ throw new Error(`ReactWrapper::renderProp(): no prop called “${propName}“ found`);
}
const propValue = props[propName];
if (typeof propValue !== 'function') {
- throw new TypeError(`expected prop “${propName}“ to contain a function, but it holds “${typeof prop}“`);
+ throw new TypeError(`ReactWrapper::renderProp(): expected prop “${propName}“ to contain a function, but it holds “${typeof propValue}“`);
}
return (...args) => {
diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js
index b72b414db..7c2e79f6d 100644
--- a/packages/enzyme/src/ShallowWrapper.js
+++ b/packages/enzyme/src/ShallowWrapper.js
@@ -1191,15 +1191,15 @@ class ShallowWrapper {
throw new TypeError('ShallowWrapper::renderProp() can only be called on custom components');
}
if (typeof propName !== 'string') {
- throw new TypeError('`propName` must be a string');
+ throw new TypeError('ShallowWrapper::renderProp(): `propName` must be a string');
}
const props = this.props();
if (!has(props, propName)) {
- throw new Error(`no prop called “${propName}“ found`);
+ throw new Error(`ShallowWrapper::renderProp(): no prop called “${propName}“ found`);
}
const propValue = props[propName];
if (typeof propValue !== 'function') {
- throw new TypeError(`expected prop “${propName}“ to contain a function, but it holds “${typeof prop}“`);
+ throw new TypeError(`ShallowWrapper::renderProp(): expected prop “${propName}“ to contain a function, but it holds “${typeof propValue}“`);
}
return (...args) => {