diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 711ea4c33..8cc872c6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,6 +85,19 @@ npm run build:watch npm run test:watch ``` +### Tests for functionality shared between `shallow` and `mount` + +Tests for a method "foo" are stored in `packages/enzyme-test-suite/test/shared/methods/foo`. The file default exports a function that receives an injected object argument, containing the following properties: + - `Wrap`: e.g. `shallow`, `mount` + - `WrapRendered`: this abstracts around the differences between `shallow` and `mount` - e.g., that the root of a shallow wrapper around `Foo` is what `Foo` *renders*, where the root of a mount wrapper around `Foo` is `Foo` itself. Thus, this function produces a wrapper around what `Foo` renders, regardless of the `Wrap` method used. + - `Wrapper`: e.g. `ShallowWrapper`, `ReactWrapper` + - `WrapperName`: e.g. `"ShallowWrapper"`, `"ReactWrapper"` + - `isShallow`: true if `shallow`. note: needing to use this is a code smell, please avoid. + - `isMount`: true if `mount`. note: needing to use this is a code smell, please avoid. + - `makeDOMElement`: in `mount`, makes a real DOM element; in `shallow`, makes a mock object. + + These tests are ran via an explicit list in a `describeMethods` call in the ReactWrapper and ShallowWrapper test files. If you add a new test file for a shared method, you'll need to add its name to both calls. + ### Style & Linting This codebase adheres to the [Airbnb Styleguide](https://github.com/airbnb/javascript) and is diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index 020f1cced..e7f2b25a1 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -5,30 +5,22 @@ import { expect } from 'chai'; import sinon from 'sinon-sandbox'; import wrap from 'mocha-wrap'; import isEqual from 'lodash.isequal'; -import getData from 'html-element-map/getData'; import { mount, - render, ReactWrapper, } from 'enzyme'; import mountEntry from 'enzyme/mount'; import ReactWrapperEntry from 'enzyme/ReactWrapper'; import { - ITERATOR_SYMBOL, withSetStateAllowed, - sym, } from 'enzyme/build/Utils'; import getAdapter from 'enzyme/build/getAdapter'; -import { - Portal, -} from 'react-is'; import './_helpers/setupAdapters'; import { createClass, createContext, createPortal, - createRef, Fragment, forwardRef, memo, @@ -40,21 +32,11 @@ import { describeWithDOM, describeIf, itIf, - itWithData, - generateEmptyRenderData, } from './_helpers'; +import describeMethods from './_helpers/describeMethods'; import { - REACT16, is, } from './_helpers/version'; -import realArrowFunction from './_helpers/realArrowFunction'; -import sloppyReturnThis from './_helpers/untranspiledSloppyReturnThis'; - -const getElementPropSelector = prop => x => x.props[prop]; -const getWrapperPropSelector = prop => x => x.prop(prop); - -// some React versions pass undefined as an argument of setState callback. -const CALLING_SETSTATE_CALLBACK_WITH_UNDEFINED = is('^15.5'); describeWithDOM('mount', () => { describe('top level entry points', () => { @@ -673,7783 +655,2383 @@ describeWithDOM('mount', () => { }); }); - describe('.contains(node)', () => { - it('allows matches on the root node', () => { - const a =
; - const b =
; - const c =
; - expect(mount(a).contains(b)).to.equal(true); - expect(mount(a).contains(c)).to.equal(false); - }); + itIf(is('>= 16.2'), 'supports fragments', () => { + const wrapper = mount(( + +

hello

+ boo +
+ )); + + expect(wrapper).to.have.lengthOf(2); + }); + + const Wrap = mount; + const Wrapper = ReactWrapper; + describeMethods( + { Wrap, Wrapper }, + '@@iterator', + 'at', + 'childAt', + 'children', + 'closest', + 'contains', + 'containsAllMatchingElements', + 'containsAnyMatchingElements', + 'containsMatchingElement', + 'debug', + 'equals', + 'every', + 'everyWhere', + 'exists', + 'filter', + 'filterWhere', + 'find', + 'findWhere', + 'first', + 'flatMap', + 'forEach', + 'get', + 'getElement', + 'getElements', + 'getNode', + 'getNodes', + 'hasClass', + 'hostNodes', + 'html', + 'instance', + 'is', + 'isEmpty', + 'isEmptyRender', + 'key', + 'last', + 'map', + 'matchesElement', + 'name', + 'not', + 'parent', + 'parents', + 'prop', + 'props', + 'reduce', + 'reduceRight', + 'render', + 'renderProp', + 'root', + 'setContext', + 'setProps', + 'setState', + 'simulate', + 'simulateError', + 'single', + 'slice', + 'some', + 'someWhere', + 'state', + 'tap', + 'text', + 'unmount', + 'wrap', + ); - it('allows matches on a nested node', () => { - const wrapper = mount(( -
-
-
- )); - const b =
; - expect(wrapper.contains(b)).to.equal(true); - }); + describe('.mount()', () => { + it('calls componentWillUnmount()', () => { + const willMount = sinon.spy(); + const didMount = sinon.spy(); + const willUnmount = sinon.spy(); - it('matches composite components', () => { class Foo extends React.Component { - render() { return
; } - } - const wrapper = mount(( -
- -
- )); - const b = ; - expect(wrapper.contains(b)).to.equal(true); - }); - - it('does something with arrays of nodes', () => { - const wrapper = mount(( -
- 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); - }); - - describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { - it('matches composite components', () => { - function Foo() { - return
; + constructor(props) { + super(props); + this.componentWillUnmount = willUnmount; + this.componentWillMount = willMount; + this.componentDidMount = didMount; } - const wrapper = mount(( -
- -
- )); - const b = ; - expect(wrapper.contains(b)).to.equal(true); - }); - it('matches composite components if rendered by function', () => { - function Foo() { - return
; + render() { + return ( +
+ {this.props.id} +
+ ); } - const renderStatelessComponent = () => ; - const wrapper = mount(( -
- {renderStatelessComponent()} -
- )); - const b = ; - expect(wrapper.contains(b)).to.equal(true); - }); + } + const wrapper = mount(); + expect(willMount).to.have.property('callCount', 1); + expect(didMount).to.have.property('callCount', 1); + expect(willUnmount).to.have.property('callCount', 0); + wrapper.unmount(); + expect(willMount).to.have.property('callCount', 1); + expect(didMount).to.have.property('callCount', 1); + expect(willUnmount).to.have.property('callCount', 1); + wrapper.mount(); + expect(willMount).to.have.property('callCount', 2); + expect(didMount).to.have.property('callCount', 2); + expect(willUnmount).to.have.property('callCount', 1); }); }); - describe('.equals(node)', () => { - it('allows matches on the root node', () => { - const a =
; - const b =
; - const c =
; + describe('.getDOMNode()', () => { + class Test extends React.Component { + render() { + return ( +
+
+ + +
+
+ ); + } + } + class TestZero extends React.Component { + render() { + return
; + } + } - expect(mount(a).equals(b)).to.equal(true); - expect(mount(a).equals(c)).to.equal(false); + it('returns the outermost DOMComponent of the root wrapper', () => { + const wrapper = mount(); + expect(wrapper.getDOMNode()).to.have.property('className', 'outer'); }); - it('does NOT allow matches on a nested node', () => { - const wrapper = mount(( -
-
-
- )); - const b =
; - expect(wrapper.equals(b)).to.equal(false); + it('returns the outermost DOMComponent of the inner div wrapper', () => { + const wrapper = mount(); + expect(wrapper.find('.inner').getDOMNode()).to.have.property('className', 'inner'); }); - it('matches composite components', () => { - class Foo extends React.Component { - render() { return
; } - } - const wrapper = mount(( -
- -
- )); - const b =
; - expect(wrapper.equals(b)).to.equal(true); + it('throws when wrapping multiple elements', () => { + const wrapper = mount().find('span'); + expect(() => wrapper.getDOMNode()).to.throw( + Error, + 'Method “getDOMNode” is meant to be run on 1 node. 2 found instead.', + ); }); - it('does not expand `node` content', () => { - class Bar extends React.Component { - render() { return
; } - } - - class Foo extends React.Component { - render() { return ; } - } + it('throws when wrapping zero elements', () => { + const wrapper = mount().find('span'); + expect(() => wrapper.getDOMNode()).to.throw( + Error, + 'Method “getDOMNode” is meant to be run on 1 node. 0 found instead.', + ); + }); - const wrapper = mount().children(); - expect(wrapper.equals()).to.equal(true); - expect(wrapper.equals()).to.equal(false); + it('throws when wrapping zero elements', () => { + const wrapper = mount().find('span'); + expect(() => wrapper.getDOMNode()).to.throw( + Error, + 'Method “getDOMNode” is meant to be run on 1 node. 0 found instead.', + ); }); describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { - it('matches composite SFCs', () => { - const Foo = () => ( -
- ); - - const wrapper = mount(( -
- + const SFC = () => ( +
+
+ +
- )); - const b =
; - expect(wrapper.equals(b)).to.equal(true); +
+ ); + + it('returns the outermost DOMComponent of the root wrapper', () => { + const wrapper = mount(); + expect(wrapper.getDOMNode()).to.have.property('className', 'outer'); }); - it('does not expand `node` content', () => { - const Bar = () => ( -
- ); + it('returns the outermost DOMComponent of the inner div wrapper', () => { + const wrapper = mount(); + expect(wrapper.find('.inner').getDOMNode()).to.have.property('className', 'inner'); + }); - const Foo = () => ( - + it('throws when wrapping multiple elements', () => { + const wrapper = mount().find('span'); + expect(() => wrapper.getDOMNode()).to.throw( + Error, + 'Method “getDOMNode” is meant to be run on 1 node. 2 found instead.', ); - - const wrapper = mount().children(); - 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 ( -
-
-
-
- ); - } + it('lets you read the value of an input', () => { + const wrapper = mount(
); + const inputNode = wrapper.find('input').getDOMNode(); + expect(inputNode.value).to.equal('0'); + }); + }); + + describe('.ref(refName)', () => { + it('unavailable ref should return undefined', () => { + class WithoutRef extends React.Component { + render() { return
; } } + const wrapper = mount(); + const ref = wrapper.ref('not-a-ref'); + + expect(ref).to.equal(undefined); + }); + + it('gets a wrapper of the node matching the provided refName', () => { - class TwoChildrenOneArrayed extends React.Component { + class Foo extends React.Component { render() { return ( -
-
- {[
]} +
+ First + Second + Third
); } } - const twoChildren = mount().children(); - const twoChildrenOneArrayed = mount().children(); - - expect(twoChildren.equals(twoChildrenOneArrayed.getElement())).to.equal(true); - expect(twoChildrenOneArrayed.equals(twoChildren.getElement())).to.equal(true); + const wrapper = mount(); + // React 13 and 14 return instances whereas 15+ returns actual DOM nodes. In this case, + // the public API of enzyme is to just return what `this.refs[refName]` would be expected + // to return for the version of react you're using. + if (is('< 15')) { + expect(wrapper.ref('secondRef').getDOMNode().getAttribute('data-amount')).to.equal('4'); + expect(wrapper.ref('secondRef').getDOMNode().textContent).to.equal('Second'); + } else { + expect(wrapper.ref('secondRef').getAttribute('data-amount')).to.equal('4'); + expect(wrapper.ref('secondRef').textContent).to.equal('Second'); + } }); }); - describe('.hostNodes()', () => { - it('strips out any non-hostNode', () => { + describe('attachTo option', () => { + it('attaches and stuff', () => { class Foo extends React.Component { render() { - return
; + return (
); } } - const wrapper = mount(( -
- - -
- )); - - const foos = wrapper.find('.foo'); - expect(foos).to.have.lengthOf(3); - - const hostNodes = foos.hostNodes(); - expect(hostNodes).to.have.lengthOf(2); - expect(hostNodes.filter('.foo')).to.have.lengthOf(2); - - expect(hostNodes.filter('div')).to.have.lengthOf(1); - expect(hostNodes.filter('span')).to.have.lengthOf(1); - }); - - it('does NOT allow matches on a nested node', () => { - const wrapper = mount(( -
-
-
- )); - const b =
; - expect(wrapper.equals(b)).to.equal(false); - }); - - it('matches composite components', () => { - class Foo extends React.Component { - render() { return
; } - } - const wrapper = mount(( -
- -
- )); - const b =
; - expect(wrapper.equals(b)).to.equal(true); - }); + const div = global.document.createElement('div'); - it.skip('does not expand `node` content', () => { - class Bar extends React.Component { - render() { return
; } - } + const initialBodyChildren = document.body.childNodes.length; + global.document.body.appendChild(div); - class Foo extends React.Component { - render() { return ; } - } + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(0); - expect(mount().equals()).to.equal(true); - expect(mount().equals()).to.equal(false); - }); + const wrapper = mount(, { attachTo: div }); - describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { - it('matches composite SFCs', () => { - const Foo = () => ( -
- ); + expect(wrapper.find('.in-foo')).to.have.lengthOf(1); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(1); - const wrapper = mount(( -
- -
- )); - const b =
; - expect(wrapper.equals(b)).to.equal(true); - }); + wrapper.detach(); - it.skip('does not expand `node` content', () => { - const Bar = () => ( -
- ); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(0); - const Foo = () => ( - - ); + global.document.body.removeChild(div); - expect(mount().equals()).to.equal(true); - expect(mount().equals()).to.equal(false); - }); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren); + expect(div.childNodes).to.have.lengthOf(0); }); - it.skip('flattens arrays of children to compare', () => { - class TwoChildren extends React.Component { + it('allows for multiple attaches/detaches on same node', () => { + class Foo extends React.Component { render() { - return ( -
-
-
-
- ); + return (
); } } - - class TwoChildrenOneArrayed extends React.Component { + class Bar extends React.Component { render() { - return ( -
-
- {[
]} -
- ); + return (
); } } - const twoChildren = mount(); - const twoChildrenOneArrayed = mount(); + let wrapper; + const div = global.document.createElement('div'); - expect(twoChildren.equals(twoChildrenOneArrayed.getElement())).to.equal(true); - expect(twoChildrenOneArrayed.equals(twoChildren.getElement())).to.equal(true); - }); - }); + const initialBodyChildren = document.body.childNodes.length; + global.document.body.appendChild(div); - describe('.find(selector)', () => { - it('finds an element based on a class name', () => { - const wrapper = mount(( -
- -
- )); - expect(wrapper.find('.foo').type()).to.equal('input'); - }); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(0); - it('finds an SVG element based on a class name', () => { - const wrapper = mount(( -
- -
- )); - expect(wrapper.find('.foo').type()).to.equal('svg'); - }); + wrapper = mount(, { attachTo: div }); - it('finds an element based on a tag name', () => { - const wrapper = mount(( -
- -
- )); - expect(wrapper.find('input').props().className).to.equal('foo'); - }); + expect(wrapper.find('.in-foo')).to.have.lengthOf(1); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(1); - it('finds an element based on a tag name and class name', () => { - const wrapper = mount(( -
- -
-
- )); - expect(wrapper.find('input.foo')).to.have.lengthOf(1); - }); + wrapper.detach(); - it('works on non-single nodes', () => { - const wrapper = mount(( -
-
-
Text
-
Text
-
Text
-
-
-
Text
-
Text
-
Text
-
-
- )); - 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); - }); + wrapper = mount(, { attachTo: div }); + expect(wrapper.find('.in-bar')).to.have.lengthOf(1); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(1); - it('finds an element based on a tag name and id', () => { - const wrapper = mount(( -
- -
- )); - expect(wrapper.find('input#foo')).to.have.lengthOf(1); - }); + wrapper.detach(); - it('finds an element based on a tag name, id, and class name', () => { - const wrapper = mount(( -
- -
- )); - expect(wrapper.find('input#foo.bar')).to.have.lengthOf(1); - }); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(0); - it('finds a component based on a constructor', () => { - class Foo extends React.Component { - render() { return
; } - } - const wrapper = mount(( -
- -
- )); - expect(wrapper.find(Foo).type()).to.equal(Foo); + global.document.body.removeChild(div); + + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren); + expect(div.childNodes).to.have.lengthOf(0); }); - wrap() - .withOverride(() => getAdapter(), 'isValidElementType', () => () => false) - .it('throws when an adapter’s `isValidElementType` lies', () => { - class Foo extends React.Component { - render() { return
; } + it('will attach to the body successfully', () => { + class Bar extends React.Component { + render() { + return (
); } - const wrapper = mount(( -
- -
- )); + } + const wrapper = mount(, { attachTo: document.body }); - expect(() => wrapper.find(Foo)).to.throw( - TypeError, - 'Enzyme::Selector expects a string, object, or valid element type (Component Constructor)', - ); - }); + expect(wrapper.find('.in-bar')).to.have.lengthOf(1); + expect(document.body.childNodes).to.have.lengthOf(1); - it('finds a component based on a component displayName', () => { - class Foo extends React.Component { - render() { return
; } - } - const wrapper = mount(( -
- -
- )); - expect(wrapper.find('Foo').type()).to.equal(Foo); + wrapper.detach(); + + expect(document.body.childNodes).to.have.lengthOf(0); }); describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { - it('finds a stateless component based on a component displayName', () => { - const Foo = () =>
; - const wrapper = mount(( -
- -
- )); - expect(wrapper.find('Foo').type()).to.equal(Foo); - }); + it('attaches and stuff', () => { + const Foo = () =>
; - it('finds a stateless component based on a component displayName if rendered by function', () => { - const Foo = () =>
; - const renderStatelessComponent = () => ; - const wrapper = mount(( -
- {renderStatelessComponent()} -
- )); - expect(wrapper.find('Foo').type()).to.equal(Foo); - }); - }); + const div = global.document.createElement('div'); + const initialBodyChildren = document.body.childNodes.length; + global.document.body.appendChild(div); - it('finds component based on a react prop', () => { - const wrapper = mount(( -
- -
-
- )); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(0); - expect(wrapper.find('[htmlFor="foo"]')).to.have.lengthOf(1); - expect(wrapper.find('[htmlFor]')).to.have.lengthOf(2); - }); + const wrapper = mount(, { attachTo: div }); - it('errors sensibly if any of the search props are undefined', () => { - const wrapper = mount(( -
- -
- )); + expect(wrapper.find('.in-foo')).to.have.lengthOf(1); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(1); - expect(() => wrapper.find({ type: undefined })).to.throw( - TypeError, - 'Enzyme::Props can’t have `undefined` values. Try using ‘findWhere()’ instead.', - ); - }); + wrapper.detach(); - it('compounds tag and prop selector', () => { - const wrapper = mount(( -
- -
- )); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(0); - expect(wrapper.find('span[htmlFor="foo"]')).to.have.lengthOf(1); - expect(wrapper.find('span[htmlFor]')).to.have.lengthOf(1); - }); + global.document.body.removeChild(div); - it('works with an adjacent sibling selector', () => { - const a = 'some'; - const b = 'text'; - const wrapper = mount(( -
-
- {a} - {b} -
-
- {a} - {b} -
-
- )); - expect(wrapper.find('.row')).to.have.lengthOf(2); - expect(wrapper.find('.row + .row')).to.have.lengthOf(1); - }); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren); + expect(div.childNodes).to.have.lengthOf(0); + }); - it('throws for non-numeric attribute values without quotes', () => { - const wrapper = mount(( -
- - - -
- )); - 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('allows for multiple attaches/detaches on same node', () => { + const Foo = () =>
; + const Bar = () =>
; - it('supports data prop selectors', () => { - const wrapper = mount(( -
- - - - -
- )); + let wrapper; + const div = global.document.createElement('div'); + const initialBodyChildren = document.body.childNodes.length; + global.document.body.appendChild(div); - expect(wrapper.find('[data-foo="bar"]')).to.have.lengthOf(1); - expect(wrapper.find('[data-foo]')).to.have.lengthOf(1); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(0); - expect(wrapper.find('[data-foo-123]')).to.have.lengthOf(1); - expect(wrapper.find('[data-foo-123="bar2"]')).to.have.lengthOf(1); + wrapper = mount(, { attachTo: div }); - expect(wrapper.find('[data-123-foo]')).to.have.lengthOf(1); - expect(wrapper.find('[data-123-foo="bar3"]')).to.have.lengthOf(1); + expect(wrapper.find('.in-foo')).to.have.lengthOf(1); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).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); - }); + wrapper.detach(); - it('finds components with multiple matching props', () => { - const onChange = () => ({}); - const wrapper = mount(( -
- -
- )); + wrapper = mount(, { attachTo: div }); - 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('.in-bar')).to.have.lengthOf(1); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(1); - it('does not find property when undefined', () => { - const wrapper = mount(( -
- -
- )); + wrapper.detach(); - expect(wrapper.find('[data-foo]')).to.have.lengthOf(0); - }); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); + expect(div.childNodes).to.have.lengthOf(0); - it('supports boolean and numeric values for matching props', () => { - const wrapper = mount(( - - )); + global.document.body.removeChild(div); - 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); - }); + expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 0); + expect(div.childNodes).to.have.lengthOf(0); + }); - it('does not find key or ref via property selector', () => { - class Foo extends React.Component { - render() { - const arrayOfComponents = [
,
]; + it('will attach to the body successfully', () => { + const Bar = () =>
; - return ( -
-
- {arrayOfComponents} -
- ); - } - } + const wrapper = mount(, { attachTo: document.body }); - const wrapper = mount(); + expect(wrapper.find('.in-bar')).to.have.lengthOf(1); + expect(document.body.childNodes).to.have.lengthOf(1); - 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); - }); + wrapper.detach(); - it('finds multiple elements based on a class name', () => { - const wrapper = mount(( -
- -
- )); - expect(wrapper.find('.foo')).to.have.lengthOf(2); + expect(document.body.childNodes).to.have.lengthOf(0); + }); }); + }); - it('finds multiple elements based on a tag name', () => { - const wrapper = mount(( -
- - -
- )); - expect(wrapper.find('input')).to.have.lengthOf(2); - expect(wrapper.find('button')).to.have.lengthOf(1); - }); + describe('lifecycle methods', () => { + describeIf(is('>= 16.3'), 'getDerivedStateFromProps', () => { + let spy; - it('finds multiple elements based on a constructor', () => { - const wrapper = mount(( -
- - -
- )); - expect(wrapper.find('input')).to.have.lengthOf(2); - expect(wrapper.find('button')).to.have.lengthOf(1); - }); + beforeEach(() => { + spy = sinon.spy(); + }); - itIf(is('>= 16.2'), 'supports fragments', () => { - const wrapper = mount(( - -

hello

- boo -
- )); + class Spy extends React.Component { + constructor(...args) { + super(...args); + this.state = { state: true }; // eslint-disable-line react/no-unused-state + spy('constructor'); + } - expect(wrapper).to.have.lengthOf(2); - }); + shouldComponentUpdate(nextProps, nextState, nextContext) { + spy('shouldComponentUpdate', { + prevProps: this.props, + nextProps, + prevState: this.state, + nextState, + prevContext: this.context, + nextContext, + }); + return true; + } - it('supports object property selectors', () => { - const wrapper = mount(( -
- - - -
- ); - } - } - const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); - - const wrapper = mount(); - 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 = mount(); - expect(spy).to.have.property('callCount', 1); - expect(wrapper.state('foo')).to.equal('update'); - }); - - it('does not call `componentDidMount` twice when a child component is created', () => { - class Foo extends React.Component { - constructor(props) { - super(props); - this.state = { - foo: 'init', - }; - } - - componentDidMount() {} - - render() { - return ( -
- - {this.state.foo} -
- ); - } - } - const spy = sinon.spy(Foo.prototype, 'componentDidMount'); - - const wrapper = mount(); - expect(spy).to.have.property('callCount', 1); - wrapper.find('button').prop('onClick')(); - expect(spy).to.have.property('callCount', 1); - }); - }); - - 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', - }; - } - - componentDidUpdate() {} - - render() { - return ( -
- {this.state.foo} -
- ); - } - } - const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); - const wrapper = mount(); - 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 = mount(); - 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 = mount(); - 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} -

- ); - } - } - - 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 = mount(); - - expect(cDU).to.have.property('callCount', 0); - 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 = mount(); - - 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 }); - }); - }); - }); - }); - - 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', - }; - } - - shouldComponentUpdate(nextProps, nextState) { - return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState); - } - - componentDidUpdate() {} - - render() { - return ( -
- {this.state.foo} -
- ); - } - } - const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); - const wrapper = mount(); - 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); - }); - }); - - 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', - }; - } - - componentDidUpdate(prevProps, prevState, snapshot) { - spy('componentDidUpdate', prevProps, this.props, prevState, this.state, snapshot); - } - - getSnapshotBeforeUpdate(prevProps, prevState) { - spy('getSnapshotBeforeUpdate', prevProps, this.props, prevState, this.state); - return { snapshot: 'ok' }; - } - - render() { - spy('render'); - return
foo
; - } - } - const wrapper = mount(); - 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('attachTo option', () => { - it('attaches and stuff', () => { - class Foo extends React.Component { - render() { - return (
); - } - } - const div = global.document.createElement('div'); - - const initialBodyChildren = document.body.childNodes.length; - global.document.body.appendChild(div); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(0); - - const wrapper = mount(, { attachTo: div }); - - expect(wrapper.find('.in-foo')).to.have.lengthOf(1); - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(1); - - wrapper.detach(); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(0); - - global.document.body.removeChild(div); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren); - expect(div.childNodes).to.have.lengthOf(0); - }); - - it('allows for multiple attaches/detaches on same node', () => { - class Foo extends React.Component { - render() { - return (
); - } - } - class Bar extends React.Component { - render() { - return (
); - } - } - let wrapper; - const div = global.document.createElement('div'); - - const initialBodyChildren = document.body.childNodes.length; - global.document.body.appendChild(div); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(0); - - wrapper = mount(, { attachTo: div }); - - expect(wrapper.find('.in-foo')).to.have.lengthOf(1); - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(1); - - wrapper.detach(); - - wrapper = mount(, { attachTo: div }); - - expect(wrapper.find('.in-bar')).to.have.lengthOf(1); - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(1); - - wrapper.detach(); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(0); - - global.document.body.removeChild(div); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren); - expect(div.childNodes).to.have.lengthOf(0); - }); - - it('will attach to the body successfully', () => { - class Bar extends React.Component { - render() { - return (
); - } - } - const wrapper = mount(, { attachTo: document.body }); - - expect(wrapper.find('.in-bar')).to.have.lengthOf(1); - expect(document.body.childNodes).to.have.lengthOf(1); - - wrapper.detach(); - - expect(document.body.childNodes).to.have.lengthOf(0); - }); - - describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { - it('attaches and stuff', () => { - const Foo = () =>
; - - const div = global.document.createElement('div'); - const initialBodyChildren = document.body.childNodes.length; - global.document.body.appendChild(div); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(0); - - const wrapper = mount(, { attachTo: div }); - - expect(wrapper.find('.in-foo')).to.have.lengthOf(1); - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(1); - - wrapper.detach(); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(0); - - global.document.body.removeChild(div); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren); - expect(div.childNodes).to.have.lengthOf(0); - }); - - it('allows for multiple attaches/detaches on same node', () => { - const Foo = () =>
; - const Bar = () =>
; - - let wrapper; - const div = global.document.createElement('div'); - const initialBodyChildren = document.body.childNodes.length; - global.document.body.appendChild(div); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(0); - - wrapper = mount(, { attachTo: div }); - - expect(wrapper.find('.in-foo')).to.have.lengthOf(1); - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(1); - - wrapper.detach(); - - wrapper = mount(, { attachTo: div }); - - expect(wrapper.find('.in-bar')).to.have.lengthOf(1); - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(1); - - wrapper.detach(); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 1); - expect(div.childNodes).to.have.lengthOf(0); - - global.document.body.removeChild(div); - - expect(document.body.childNodes).to.have.lengthOf(initialBodyChildren + 0); - expect(div.childNodes).to.have.lengthOf(0); - }); - - it('will attach to the body successfully', () => { - const Bar = () =>
; - - const wrapper = mount(, { attachTo: document.body }); - - expect(wrapper.find('.in-bar')).to.have.lengthOf(1); - expect(document.body.childNodes).to.have.lengthOf(1); - - wrapper.detach(); - - expect(document.body.childNodes).to.have.lengthOf(0); - }); - }); - }); - - it('works with class components that return null', () => { - class Foo extends React.Component { - render() { - return null; - } - } - const wrapper = mount(); - expect(wrapper).to.have.lengthOf(1); - expect(wrapper.type()).to.equal(Foo); - expect(wrapper.html()).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 = mount(); - expect(wrapper).to.have.lengthOf(1); - expect(wrapper.type()).to.equal(Foo); - expect(wrapper.children()).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 = mount(); - expect(wrapper).to.have.lengthOf(1); - expect(wrapper.type()).to.equal(Foo); - expect(wrapper.html()).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 ReactWrapper and returns itself', () => { - const spy = sinon.spy(); - const wrapper = mount(( -
    -
  • xxx
  • -
  • yyy
  • -
  • zzz
  • -
- )).find('li'); - const result = wrapper.tap(spy); - expect(spy.calledWith(wrapper)).to.equal(true); - expect(result).to.equal(wrapper); - }); - }); + const result = mount( + , + { + 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); + }); - describe('.key()', () => { - it('returns the key of the node', () => { - const wrapper = mount(( -
    - {['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('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, + }; + } - it('returns null when no key is specified', () => { - const wrapper = mount(( -
    -
  • foo
  • -
- )).find('li'); - expect(wrapper.key()).to.equal(null); - }); - }); + 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 */ + } + } - describe('.matchesElement(node)', () => { - it('matches on a root node that looks like the rendered one', () => { - const spy = sinon.spy(); - const wrapper = mount(( -
-
Hello World
-
- )).first(); - expect(wrapper.matchesElement(
Hello World
)).to.equal(true); - expect(wrapper.matchesElement(( -
-
Hello World
-
- ))).to.equal(true); - expect(wrapper.matchesElement(( -
-
Hello World
-
- ))).to.equal(true); - expect(wrapper.matchesElement(( -
-
Hello World
-
- ))).to.equal(true); - expect(spy).to.have.property('callCount', 0); + render() { + spy(); + return
{this.state.name}
; + } + } + const result = mount( + , + { + 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); + }); }); - 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 = mount(( -
-
Hello World
-
- )).first(); - expect(wrapper.matchesElement(
Bonjour le monde
)).to.equal(false); - expect(wrapper.matchesElement(( -
-
Hello World
-
- ))).to.equal(false); - expect(wrapper.matchesElement(( -
-
Hello World
-
- ))).to.equal(false); - expect(wrapper.matchesElement(( -
-
Hello World
-
- ))).to.equal(false); - expect(spy).to.have.property('callCount', 0); - expect(spy2).to.have.property('callCount', 0); - }); + context('unmounting phase', () => { + it('calls componentWillUnmount', () => { + const spy = sinon.spy(); + class Foo extends React.Component { + componentWillUnmount() { + spy(); + } - it('matches a simple node', () => { - class Test extends React.Component { - render() { - return

test

; + render() { + return
foo
; + } } - } - const wrapper = mount(); - expect(wrapper.children().matchesElement(

test

)).to.equal(true); + const wrapper = mount(); + wrapper.unmount(); + expect(spy).to.have.property('callCount', 1); + }); }); - }); - describe('.containsMatchingElement(node)', () => { - it('matches a root node that looks like the rendered one', () => { - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); - const wrapper = mount(( -
-
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); - }); + 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', + }; + } - it('matches on a single node that looks like a rendered one', () => { - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); - const wrapper = mount(( -
-
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); - }); + componentDidUpdate() {} - 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 = mount(( -
-
Hello World
-
Goodbye World
-
- )); - expect(wrapper.containsMatchingElement(
Bonjour le monde
)).to.equal(false); - expect(wrapper.containsMatchingElement(( -
Au revoir le monde
- ))).to.equal(false); - }); + onChange() { + this.setState({ foo: 'onChange update' }); + } - it('does not differentiate between absence, null, or undefined', () => { - const wrapper = mount(( -
-
-
-
-
- )); + render() { + return
{this.state.foo}
; + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); - expect(wrapper.containsMatchingElement(
)).to.equal(true); + const wrapper = mount(); + 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); + }); - expect(wrapper.containsMatchingElement(
)).to.equal(true); - expect(wrapper.containsMatchingElement(
)).to.equal(true); - expect(wrapper.containsMatchingElement(
)).to.equal(true); + 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); + } - expect(wrapper.containsMatchingElement(
)).to.equal(true); - expect(wrapper.containsMatchingElement(
)).to.equal(true); - expect(wrapper.containsMatchingElement(
)).to.equal(true); + componentDidUpdate() {} - expect(wrapper.containsMatchingElement(
)).to.equal(true); - expect(wrapper.containsMatchingElement(
)).to.equal(true); - expect(wrapper.containsMatchingElement(
)).to.equal(true); - }); + onChange() { + this.setState({ foo: 'onChange update' }); + } - it('works with leading and trailing spaces', () => { - const wrapper = mount(( -
  • - All Operations -
  • - )); + render() { + return ( +
    + {this.state.foo} + +
    + ); + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); - expect(wrapper.containsMatchingElement( All Operations )).to.equal(true); - }); + const wrapper = mount(); + wrapper.find('button').prop('onClick')(); + expect(wrapper.state('foo')).to.equal('onChange update'); + expect(spy).to.have.property('callCount', 1); + }); - it('works with leading and trailing newlines', () => { - const wrapper = mount(( -
  • - - All Operations - -
  • - )); + 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.containsMatchingElement( All Operations )).to.equal(true); - }); - }); + componentDidMount() { + this.update(); + } - describe('.containsAllMatchingElements(nodes)', () => { - it('throws TypeError if non-array passed in', () => { - const wrapper = mount(( -
    - Hello -
    - )); + componentDidUpdate() {} - expect(() => wrapper.containsAllMatchingElements(( -
    - Hello -
    - ))).to.throw(TypeError, 'nodes should be an Array'); - }); + render() { + return
    {this.state.foo}
    ; + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); - it('matches on array of nodes that each look like rendered nodes, with nested elements', () => { - const wrapper = mount(( -
    -
    -

    Hello

    -
    -
    -

    Goodbye

    -
    -
    - )); + const wrapper = mount(); + expect(spy).to.have.property('callCount', 1); + expect(wrapper.state('foo')).to.equal('update'); + }); - expect(wrapper.containsAllMatchingElements([ -

    Hello

    , -

    Goodbye

    , - ])).to.equal(true); - }); + 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('matches on an array of nodes that all looks like one of rendered nodes', () => { - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); - const wrapper = mount(( -
    -
    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 = mount(( -
    -
    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); - }); - }); + componentDidMount() {} - 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 = mount(( -
    -
    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 = mount(( -
    -
    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); + render() { + return ( +
    + + {this.state.foo} +
    + ); + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidMount'); + + const wrapper = mount(); + expect(spy).to.have.property('callCount', 1); + wrapper.find('button').prop('onClick')(); + expect(spy).to.have.property('callCount', 1); + }); }); - }); - 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
    ; } + 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', + }; } - Foo.displayName = 'CustomWrapper'; + componentDidUpdate() {} - const wrapper = mount(); - expect(wrapper.name()).to.equal('CustomWrapper'); - }); + render() { + return ( +
    + {this.state.foo} +
    + ); + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); + const wrapper = mount(); + wrapper.setState({ foo: 'update' }); + expect(spy).to.have.property('callCount', 1); + wrapper.setState({ foo: 'update' }); + expect(spy).to.have.property('callCount', 1); - describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { - it('returns the name of the node', () => { - function SFC() { - return
    ; - } + wrapper.setProps({ id: 2 }); + expect(spy).to.have.property('callCount', 2); + wrapper.setProps({ id: 2 }); + expect(spy).to.have.property('callCount', 2); + }); - SFC.displayName = 'CustomWrapper'; + class Test extends PureComponent { + constructor(...args) { + super(...args); - const wrapper = mount(); - expect(wrapper.name()).to.equal('CustomWrapper'); - }); - }); + this.state = { a: { b: { c: 1 } } }; + } - describe('createClass', () => { - it('returns the name of the node', () => { - const Foo = createClass({ - displayName: 'CustomWrapper', - render() { - return
    ; - }, - }); + componentDidUpdate() { + const { onUpdate } = this.props; + onUpdate(); + } - const wrapper = mount(); - expect(wrapper.name()).to.equal('CustomWrapper'); - }); - }); + setDeepEqualState() { + this.setState({ a: { b: { c: 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); + setDeepDifferentState() { + this.setState({ a: { b: { c: 2 } } }); + } - const wrapper = mount(); + render() { + const { a: { b: { c } } } = this.state; + return
    {c}
    ; + } + } - expect(wrapper.name()).to.equal(sentinel); + it('rerenders on setState when new state is !==, but deeply equal to existing state', () => { + const updateSpy = sinon.spy(); + const wrapper = mount(); + wrapper.instance().setDeepEqualState(); + expect(updateSpy).to.have.property('callCount', 1); + }); - expect(stub).to.have.property('callCount', 1); - const { args } = stub.firstCall; - expect(args).to.eql([wrapper.getNodeInternal()]); - }); - }); + it('rerenders when setState is called with an object that doesnt have deep equality', () => { + const updateSpy = sinon.spy(); + const wrapper = mount(); + wrapper.instance().setDeepDifferentState(); + expect(updateSpy).to.have.property('callCount', 1); }); - describe('node without displayName', () => { - it('returns the name of the node', () => { - class Foo extends React.Component { - render() { return
    ; } + describeIf(is('>= 16.3'), 'setProps calls `componentDidUpdate` when `getDerivedStateFromProps` is defined', () => { + class DummyComp extends PureComponent { + constructor(...args) { + super(...args); + this.state = { state: -1 }; } - const wrapper = mount(); - 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 wrapper = mount(); - expect(wrapper.name()).to.equal('SFC'); - }); - }); - }); + static getDerivedStateFromProps({ changeState, counter }) { + return changeState ? { state: counter * 10 } : null; + } - describe('DOM node', () => { - it('returns the name of the node', () => { - const wrapper = mount(
    ); - expect(wrapper.name()).to.equal('div'); - }); - }); + componentDidUpdate() {} - describe('.ref()', () => { - it('unavailable ref should return undefined', () => { - class WithoutRef extends React.Component { - render() { return
    ; } + render() { + const { counter } = this.props; + const { state } = this.state; + return ( +

    + {counter} + {state} +

    + ); } - const wrapper = mount(); - const ref = wrapper.ref('not-a-ref'); + } - expect(ref).to.equal(undefined); + let cDU; + let gDSFP; + + beforeEach(() => { // eslint-disable-line mocha/no-sibling-hooks + cDU = sinon.spy(DummyComp.prototype, 'componentDidUpdate'); + gDSFP = sinon.spy(DummyComp, 'getDerivedStateFromProps'); }); - }); - }); - describeIf(!!ITERATOR_SYMBOL, '@@iterator', () => { - it('is iterable', () => { - class Foo extends React.Component { - render() { - return ( -
    - Hello - Hello - Hello - Hello -
    - ); - } - } - const wrapper = mount(); - 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('with no state changes, calls both methods with a sync and async setProps', () => { + const wrapper = mount(); - it('returns an iterable iterator', () => { - class Foo extends React.Component { - render() { - return ( -
    - Hello - Hello - Hello - Hello -
    - ); - } - } - const wrapper = mount(); + expect(cDU).to.have.property('callCount', 0); + 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 }); - const iter = wrapper[ITERATOR_SYMBOL](); - expect(iter).to.have.property(ITERATOR_SYMBOL).and.be.a('function'); - expect(iter[ITERATOR_SYMBOL]()).to.equal(iter); - }); - }); + wrapper.setProps({ counter: 1 }); - describe('.instance()', () => { - class Test extends React.Component { - render() { - return ( -
    - - -
    - ); - } - } + 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 }); - it('returns the wrapped component instance', () => { - const wrapper = mount(); - expect(wrapper.instance()).to.be.an.instanceof(Test); - }); + 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('throws when wrapping multiple elements', () => { - const wrapper = mount().find('span'); - expect(() => wrapper.instance()).to.throw(Error); - }); - }); + it('with a state changes, calls both methods with a sync and async setProps', () => { + const wrapper = mount(); - 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; - } + 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 }); - setRef(node) { - this.node = node; - } + wrapper.setProps({ counter: 1 }); - render() { - return ( -
    -
    -
    - ); - } - } - const wrapper = mount(); - const mockNode = { mock: true }; - wrapper.find('.foo').getElement().ref(mockNode); - expect(wrapper.instance().node).to.equal(mockNode); + 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 }); + }); + }); + }); }); - itIf(is('>= 16.3'), 'returns nodes with createRefs as well', () => { - class Foo extends React.Component { - constructor(props) { - super(props); - this.setRef = createRef(); - } + 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', + }; + } + + shouldComponentUpdate(nextProps, nextState) { + return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState); + } + + componentDidUpdate() {} - render() { - return ( -
    -
    -
    - ); + render() { + return ( +
    + {this.state.foo} +
    + ); + } } - } - const wrapper = mount(); - const element = wrapper.find('.foo').instance(); - expect(wrapper.instance().setRef).to.have.property('current', element); + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); + const wrapper = mount(); + 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); + }); }); - 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); - } + 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', + }; + } - setRef(node) { - this.node = node; - } + componentDidUpdate(prevProps, prevState, snapshot) { + spy('componentDidUpdate', prevProps, this.props, prevState, this.state, snapshot); + } - render() { - return ( -
    - ); - } - } - const wrapper = mount(); - expect(wrapper.getElement()).to.have.property('key', null); - }); + getSnapshotBeforeUpdate(prevProps, prevState) { + spy('getSnapshotBeforeUpdate', prevProps, this.props, prevState, this.state); + return { snapshot: 'ok' }; + } - 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() { + spy('render'); + return
    foo
    ; + } } + const wrapper = mount(); + 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' }], + ]); + }); + }); + }); - render() { - return ( -
    - ); - } + it('works with class components that return null', () => { + class Foo extends React.Component { + render() { + return null; } - const wrapper = mount(); - expect(wrapper.getElement()).to.have.property('key', null); - }); + } + const wrapper = mount(); + expect(wrapper).to.have.lengthOf(1); + expect(wrapper.type()).to.equal(Foo); + expect(wrapper.html()).to.equal(null); + const rendered = wrapper.render(); + expect(rendered).to.have.lengthOf(0); + expect(rendered.html()).to.equal(null); }); - describe('.getElements()', () => { - it('returns the wrapped elements', () => { - class Test extends React.Component { - render() { - return ( -
    - - -
    - ); - } + itIf(is('>= 16'), 'works with class components that return arrays', () => { + class Foo extends React.Component { + render() { + return [
    ,
    ]; } + } + const wrapper = mount(); + expect(wrapper).to.have.lengthOf(1); + expect(wrapper.type()).to.equal(Foo); + expect(wrapper.children()).to.have.lengthOf(2); + expect(wrapper.find('div')).to.have.lengthOf(2); + }); - const wrapper = mount(); - expect(wrapper.find('span').getElements()).to.have.lengthOf(2); - }); + itIf(is('>=15 || ^16.0.0-alpha'), 'works with SFCs that return null', () => { + const Foo = () => null; + + const wrapper = mount(); + expect(wrapper).to.have.lengthOf(1); + expect(wrapper.type()).to.equal(Foo); + expect(wrapper.html()).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', () => { @@ -8512,120 +3094,6 @@ describeWithDOM('mount', () => { }); }); - describe('.getDOMNode()', () => { - class Test extends React.Component { - render() { - return ( -
    -
    - - -
    -
    - ); - } - } - class TestZero extends React.Component { - render() { - return
    ; - } - } - - it('returns the outermost DOMComponent of the root wrapper', () => { - const wrapper = mount(); - expect(wrapper.getDOMNode()).to.have.property('className', 'outer'); - }); - - it('returns the outermost DOMComponent of the inner div wrapper', () => { - const wrapper = mount(); - expect(wrapper.find('.inner').getDOMNode()).to.have.property('className', 'inner'); - }); - - it('throws when wrapping multiple elements', () => { - const wrapper = mount().find('span'); - expect(() => wrapper.getDOMNode()).to.throw( - Error, - 'Method “getDOMNode” is meant to be run on 1 node. 2 found instead.', - ); - }); - - it('throws when wrapping zero elements', () => { - const wrapper = mount().find('span'); - expect(() => wrapper.getDOMNode()).to.throw( - Error, - 'Method “getDOMNode” is meant to be run on 1 node. 0 found instead.', - ); - }); - - it('throws when wrapping zero elements', () => { - const wrapper = mount().find('span'); - expect(() => wrapper.getDOMNode()).to.throw( - Error, - 'Method “getDOMNode” is meant to be run on 1 node. 0 found instead.', - ); - }); - - describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { - const SFC = () => ( -
    -
    - - -
    -
    - ); - - it('returns the outermost DOMComponent of the root wrapper', () => { - const wrapper = mount(); - expect(wrapper.getDOMNode()).to.have.property('className', 'outer'); - }); - - it('returns the outermost DOMComponent of the inner div wrapper', () => { - const wrapper = mount(); - expect(wrapper.find('.inner').getDOMNode()).to.have.property('className', 'inner'); - }); - - it('throws when wrapping multiple elements', () => { - const wrapper = mount().find('span'); - expect(() => wrapper.getDOMNode()).to.throw( - Error, - 'Method “getDOMNode” is meant to be run on 1 node. 2 found instead.', - ); - }); - }); - - it('lets you read the value of an input', () => { - const wrapper = mount(
    ); - const inputNode = wrapper.find('input').getDOMNode(); - expect(inputNode.value).to.equal('0'); - }); - }); - - describe('#single()', () => { - it('throws if run on multiple nodes', () => { - const wrapper = mount(
    ).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('works with a name', () => { - const wrapper = mount(
    ); - wrapper.single('foo', (node) => { - expect(node).to.equal(wrapper.getNodeInternal()); - }); - }); - - it('works without a name', () => { - const wrapper = mount(
    ); - wrapper.single((node) => { - expect(node).to.equal(wrapper.getNodeInternal()); - }); - }); - }); - describe('setState through a props method', () => { class Child extends React.Component { render() { @@ -8713,38 +3181,6 @@ describeWithDOM('mount', () => { }); }); - describe('#wrap()', () => { - class Foo extends React.Component { - render() { - return ( -
    - Hello - Hello -
    - ); - } - } - - it('returns itself when it is already a ReactWrapper', () => { - const wrapperDiv = mount(
    ); - const wrapperFoo = mount(); - expect(wrapperDiv.wrap(wrapperFoo)).to.equal(wrapperFoo); - expect(wrapperFoo.wrap(wrapperDiv)).to.equal(wrapperDiv); - }); - - it('wraps when it is not already a ReactWrapper', () => { - const wrapper = mount(); - const el = wrapper.find('a').at(1); - const wrappedEl = wrapper.wrap(el.getElement()); - expect(wrappedEl).to.be.instanceOf(ReactWrapper); - expect(wrappedEl.props()).to.eql(el.props()); - - // TODO: enable this instead of that: - // expect(wrappedEl.mount().debug()).to.equal(el.debug()); - expect(wrappedEl.debug()).to.equal(''); - }); - }); - describe('cloning elements', () => { class Foo extends React.Component { render() { @@ -8800,20 +3236,6 @@ describeWithDOM('mount', () => { }); }); - describe('.root()', () => { - it('returns the root component instance', () => { - class Fixture extends React.Component { - render() { - return
    ; - } - } - const wrapper = mount(); - const root = wrapper.root(); - expect(root.is(Fixture)).to.equal(true); - expect(root.childAt(0).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/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index f80a3e8a8..7cdfb73a3 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -4,61 +4,42 @@ import { expect } from 'chai'; import sinon from 'sinon-sandbox'; import wrap from 'mocha-wrap'; import isEqual from 'lodash.isequal'; -import getData from 'html-element-map/getData'; import { shallow, - render, ShallowWrapper, } from 'enzyme'; import shallowEntry from 'enzyme/shallow'; import ShallowWrapperEntry from 'enzyme/ShallowWrapper'; import { - ITERATOR_SYMBOL, withSetStateAllowed, - sym, } from 'enzyme/build/Utils'; import getAdapter from 'enzyme/build/getAdapter'; -import { - Portal, -} from 'react-is'; import './_helpers/setupAdapters'; import { createClass, createContext, createPortal, - createRef, Fragment, forwardRef, - memo, PureComponent, useEffect, useState, } from './_helpers/react-compat'; import { describeIf, - describeWithDOM, itIf, - itWithData, - generateEmptyRenderData, } from './_helpers'; +import describeMethods from './_helpers/describeMethods'; import { REACT16, is, } from './_helpers/version'; -import realArrowFunction from './_helpers/realArrowFunction'; -import sloppyReturnThis from './_helpers/untranspiledSloppyReturnThis'; // 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. const BATCHING = !REACT16; -// some React versions pass undefined as an argument of setState callback. -const CALLING_SETSTATE_CALLBACK_WITH_UNDEFINED = is('^15.5'); - -const getElementPropSelector = prop => x => x.props[prop]; -const getWrapperPropSelector = prop => x => x.prop(prop); - describe('shallow', () => { describe('top level entry points', () => { expect(shallowEntry).to.equal(shallow); @@ -695,6675 +676,1815 @@ describe('shallow', () => { }); }); - describe('.contains(node)', () => { - it('allows matches on the root node', () => { - const a =
    ; - const b =
    ; - const c =
    ; - expect(shallow(a).contains(b)).to.equal(true); - expect(shallow(a).contains(c)).to.equal(false); - }); - - it('allows matches on a nested node', () => { - const wrapper = shallow(( -
    -
    -
    - )); - const b =
    ; - expect(wrapper.contains(b)).to.equal(true); - }); + itIf(is('>= 16.2'), 'does not support fragments', () => { + const wrapper = () => shallow(( + +

    hello

    + boo +
    + )); + + expect(wrapper).to.throw('ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was `symbol`.'); + }); + + const Wrap = shallow; + const Wrapper = ShallowWrapper; + describeMethods( + { Wrap, Wrapper }, + '@@iterator', + 'at', + 'childAt', + 'children', + 'closest', + 'contains', + 'containsAllMatchingElements', + 'containsAnyMatchingElements', + 'containsMatchingElement', + 'debug', + 'equals', + 'every', + 'everyWhere', + 'exists', + 'filter', + 'filterWhere', + 'find', + 'findWhere', + 'first', + 'flatMap', + 'forEach', + 'get', + 'getElement', + 'getElements', + 'getNode', + 'getNodes', + 'hasClass', + 'hostNodes', + 'html', + 'instance', + 'is', + 'isEmpty', + 'isEmptyRender', + 'key', + 'last', + 'map', + 'matchesElement', + 'name', + 'not', + 'parent', + 'parents', + 'prop', + 'props', + 'reduce', + 'reduceRight', + 'render', + 'renderProp', + 'root', + 'setContext', + 'setProps', + 'setState', + 'simulate', + 'simulateError', + 'single', + 'slice', + 'some', + 'someWhere', + 'state', + 'tap', + 'text', + 'unmount', + 'wrap', + ); - it('matches composite components', () => { + 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
    ; } + render() { + return ( +
    + +
    + ); + } } - const wrapper = shallow(( -
    - -
    - )); - const b = ; - expect(wrapper.contains(b)).to.equal(true); - }); - - it('works with strings', () => { - const wrapper = shallow(
    foo
    ); - - expect(wrapper.contains('foo')).to.equal(true); - expect(wrapper.contains('bar')).to.equal(false); - }); - - it('works with numbers', () => { - const wrapper = shallow(
    {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 = shallow(( -
    -
    -
    {5}
    -
    -
    foo
    -
    - )); - - 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 = shallow(( -
    - 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); - }); - - it('throws on invalid argument', () => { - const wrapper = shallow(
    ); - - expect(() => wrapper.contains({})).to.throw( - Error, - 'ShallowWrapper::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, - 'ShallowWrapper::contains() can only be called with a ReactElement (or an array of them), a string, or a number as an argument.', - ); + 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); }); - describeIf(is('> 0.13'), 'stateless function components (SFCs)', () => { - it('matches composite components', () => { - function Foo() { - return
    ; + 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 wrapper = shallow(( -
    - -
    - )); - const b = ; - expect(wrapper.contains(b)).to.equal(true); + 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('matches composite components if rendered by function', () => { - function Foo() { - return
    ; + 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 renderStatelessComponent = () => ; - const wrapper = shallow(( -
    - {renderStatelessComponent()} -
    - )); - const b = ; - expect(wrapper.contains(b)).to.equal(true); - }); - }); - }); - - describe('.equals(node)', () => { - it('allows matches on the root node', () => { - const a =
    ; - const b =
    ; - const c =
    ; - expect(shallow(a).equals(b)).to.equal(true); - expect(shallow(a).equals(c)).to.equal(false); - }); + const context = { name: 'foo' }; + const wrapper = shallow(); + expect(() => wrapper.find(Bar).shallow({ context })).to.not.throw(); + }); - it('does NOT allow matches on a nested node', () => { - const wrapper = shallow(( -
    -
    -
    - )); - const b =
    ; - expect(wrapper.equals(b)).to.equal(false); - }); + 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 ( +
    + +
    + ); + } + } - it('matches composite components', () => { - class Foo extends React.Component { - render() { return
    ; } - } - const wrapper = shallow(( -
    - -
    - )); - const b =
    ; - expect(wrapper.equals(b)).to.equal(true); - }); + const context = { name: 'foo' }; + const wrapper = shallow().find(Bar).shallow({ context }); - it('does not expand `node` content', () => { - class Bar extends React.Component { - render() { return
    ; } - } + expect(wrapper.context().name).to.equal(context.name); + expect(wrapper.context('name')).to.equal(context.name); + }); - class Foo extends React.Component { - render() { return ; } - } + 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 wrapper = shallow(); - expect(wrapper.equals()).to.equal(true); - expect(wrapper.equals()).to.equal(false); + 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('matches composite SFCs', () => { - const Foo = () => ( -
    - ); - - const wrapper = shallow(( + it('returns a shallow rendered instance of the current node', () => { + const Bar = () => (
    - +
    - )); - const b =
    ; - expect(wrapper.equals(b)).to.equal(true); - }); - - it('does not expand `node` content', () => { - const Bar = () => ( -
    ); - const Foo = () => ( - +
    + +
    ); const wrapper = shallow(); - expect(wrapper.equals()).to.equal(true); - expect(wrapper.equals()).to.equal(false); + 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); }); - }); - it('flattens arrays of children to compare', () => { - class TwoChildren extends React.Component { - render() { - return ( -
    -
    -
    + describe('context', () => { + it('can pass in context', () => { + const Bar = (props, context) => ( +
    {context.name}
    + ); + 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(( -
    - - -