enzyme
Version:
JavaScript Testing utilities for React
1,102 lines (1,024 loc) • 32.3 kB
JavaScript
import React from 'react';
import flatten from 'lodash/flatten';
import unique from 'lodash/uniq';
import compact from 'lodash/compact';
import cheerio from 'cheerio';
import ComplexSelector from './ComplexSelector';
import {
nodeEqual,
nodeMatches,
containsChildrenSubArray,
propFromEvent,
withSetStateAllowed,
propsOfNode,
typeOfNode,
isReactElementAlike,
displayNameOfNode,
isFunctionalComponent,
isCustomComponentElement,
ITERATOR_SYMBOL,
} from './Utils';
import {
debugNodes,
} from './Debug';
import {
getTextFromNode,
hasClassName,
childrenOfNode,
parentsOfNode,
treeFilter,
buildPredicate,
} from './ShallowTraversal';
import {
createShallowRenderer,
renderToStaticMarkup,
batchedUpdates,
isDOMComponentElement,
} from './react-compat';
import { REACT155 } from './version';
/**
* Finds all nodes in the current wrapper nodes' render trees that match the provided predicate
* function.
*
* @param {ShallowWrapper} wrapper
* @param {Function} predicate
* @param {Function} filter
* @returns {ShallowWrapper}
*/
function findWhereUnwrapped(wrapper, predicate, filter = treeFilter) {
return wrapper.flatMap(n => filter(n.getNode(), predicate));
}
/**
* Returns a new wrapper instance with only the nodes of the current wrapper instance that match
* the provided predicate function.
*
* @param {ShallowWrapper} wrapper
* @param {Function} predicate
* @returns {ShallowWrapper}
*/
function filterWhereUnwrapped(wrapper, predicate) {
return wrapper.wrap(compact(wrapper.getNodes().filter(predicate)));
}
/**
* Ensure options passed to ShallowWrapper are valid. Throws otherwise.
* @param {Object} options
*/
function validateOptions(options) {
const { lifecycleExperimental, disableLifecycleMethods } = options;
if (
typeof lifecycleExperimental !== 'undefined' &&
typeof lifecycleExperimental !== 'boolean'
) {
throw new Error(
'lifecycleExperimental must be either true or false if provided',
);
}
if (
typeof disableLifecycleMethods !== 'undefined' &&
typeof disableLifecycleMethods !== 'boolean'
) {
throw new Error(
'disableLifecycleMethods must be either true or false if provided',
);
}
if (
lifecycleExperimental != null &&
disableLifecycleMethods != null &&
lifecycleExperimental === disableLifecycleMethods
) {
throw new Error(
'lifecycleExperimental and disableLifecycleMethods cannot be set to the same value',
);
}
}
function performBatchedUpdates(wrapper, fn) {
const renderer = wrapper.root.renderer;
if (REACT155 && renderer.unstable_batchedUpdates) {
// React 15.5+ exposes batching on shallow renderer itself
return renderer.unstable_batchedUpdates(fn);
}
// React <15.5: Fallback to ReactDOM
return batchedUpdates(fn);
}
/**
* @class ShallowWrapper
*/
class ShallowWrapper {
constructor(nodes, root, options = {}) {
validateOptions(options);
if (!root) {
this.root = this;
this.unrendered = nodes;
this.renderer = createShallowRenderer();
withSetStateAllowed(() => {
performBatchedUpdates(this, () => {
this.renderer.render(nodes, options.context);
const instance = this.instance();
if (
options.lifecycleExperimental &&
instance &&
typeof instance.componentDidMount === 'function'
) {
instance.componentDidMount();
}
});
});
this.node = this.renderer.getRenderOutput();
this.nodes = [this.node];
this.length = 1;
} else {
this.root = root;
this.unrendered = null;
this.renderer = null;
if (!Array.isArray(nodes)) {
this.node = nodes;
this.nodes = [nodes];
} else {
this.node = nodes[0];
this.nodes = nodes;
}
this.length = this.nodes.length;
}
this.options = options;
this.complexSelector = new ComplexSelector(buildPredicate, findWhereUnwrapped, childrenOfNode);
}
/**
* Returns the wrapped ReactElement.
*
* @return {ReactElement}
*/
getNode() {
if (this.length !== 1) {
throw new Error(
'ShallowWrapper::getNode() can only be called when wrapping one node',
);
}
return this.root === this ? this.renderer.getRenderOutput() : this.node;
}
/**
* Returns the wrapped ReactElements.
*
* @return {Array<ReactElement>}
*/
getNodes() {
return this.root === this ? [this.renderer.getRenderOutput()] : this.nodes;
}
/**
* Gets the instance of the component being rendered as the root node passed into `shallow()`.
*
* NOTE: can only be called on a wrapper instance that is also the root instance.
*
* Example:
* ```
* const wrapper = shallow(<MyComponent />);
* const inst = wrapper.instance();
* expect(inst).to.be.instanceOf(MyComponent);
* ```
* @returns {ReactComponent}
*/
instance() {
if (this.root !== this) {
throw new Error('ShallowWrapper::instance() can only be called on the root');
}
return this.renderer._instance ? this.renderer._instance._instance : null;
}
/**
* Forces a re-render. Useful to run before checking the render output if something external
* may be updating the state of the component somewhere.
*
* NOTE: can only be called on a wrapper instance that is also the root instance.
*
* @returns {ShallowWrapper}
*/
update() {
if (this.root !== this) {
throw new Error('ShallowWrapper::update() can only be called on the root');
}
this.single('update', () => {
this.node = this.renderer.getRenderOutput();
this.nodes = [this.node];
});
return this;
}
/**
* A method is for re-render with new props and context.
* This calls componentDidUpdate method if lifecycleExperimental is enabled.
*
* NOTE: can only be called on a wrapper instance that is also the root instance.
*
* @param {Object} props
* @param {Object} context
* @returns {ShallowWrapper}
*/
rerender(props, context) {
this.single('rerender', () => {
withSetStateAllowed(() => {
const instance = this.instance();
const state = instance.state;
const prevProps = instance.props;
const prevContext = instance.context;
const nextProps = props || prevProps;
const nextContext = context || prevContext;
performBatchedUpdates(this, () => {
let shouldRender = true;
// dirty hack:
// make sure that componentWillReceiveProps is called before shouldComponentUpdate
let originalComponentWillReceiveProps;
if (
this.options.lifecycleExperimental &&
instance &&
typeof instance.componentWillReceiveProps === 'function'
) {
instance.componentWillReceiveProps(nextProps, nextContext);
originalComponentWillReceiveProps = instance.componentWillReceiveProps;
instance.componentWillReceiveProps = () => {};
}
// dirty hack: avoid calling shouldComponentUpdate twice
let originalShouldComponentUpdate;
if (
this.options.lifecycleExperimental &&
instance &&
typeof instance.shouldComponentUpdate === 'function'
) {
shouldRender = instance.shouldComponentUpdate(nextProps, state, nextContext);
originalShouldComponentUpdate = instance.shouldComponentUpdate;
}
if (shouldRender) {
if (props) this.unrendered = React.cloneElement(this.unrendered, props);
if (originalShouldComponentUpdate) {
instance.shouldComponentUpdate = () => true;
}
this.renderer.render(this.unrendered, nextContext);
if (originalShouldComponentUpdate) {
instance.shouldComponentUpdate = originalShouldComponentUpdate;
}
if (
this.options.lifecycleExperimental &&
instance &&
typeof instance.componentDidUpdate === 'function'
) {
instance.componentDidUpdate(prevProps, state, prevContext);
}
this.update();
// If it doesn't need to rerender, update only its props.
} else if (props) {
instance.props = props;
}
if (originalComponentWillReceiveProps) {
instance.componentWillReceiveProps = originalComponentWillReceiveProps;
}
});
});
});
return this;
}
/**
* A method that sets the props of the root component, and re-renders. Useful for when you are
* wanting to test how the component behaves over time with changing props. Calling this, for
* instance, will call the `componentWillReceiveProps` lifecycle method.
*
* Similar to `setState`, this method accepts a props object and will merge it in with the already
* existing props.
*
* NOTE: can only be called on a wrapper instance that is also the root instance.
*
* @param {Object} props object
* @returns {ShallowWrapper}
*/
setProps(props) {
if (this.root !== this) {
throw new Error('ShallowWrapper::setProps() can only be called on the root');
}
return this.rerender(props);
}
/**
* A method to invoke `setState` on the root component instance similar to how you might in the
* definition of the component, and re-renders. This method is useful for testing your component
* in hard to achieve states, however should be used sparingly. If possible, you should utilize
* your component's external API in order to get it into whatever state you want to test, in order
* to be as accurate of a test as possible. This is not always practical, however.
*
* NOTE: can only be called on a wrapper instance that is also the root instance.
*
* @param {Object} state to merge
* @param {Function} cb - callback function
* @returns {ShallowWrapper}
*/
setState(state, callback = undefined) {
if (this.root !== this) {
throw new Error('ShallowWrapper::setState() can only be called on the root');
}
if (isFunctionalComponent(this.instance())) {
throw new Error('ShallowWrapper::setState() can only be called on class components');
}
this.single('setState', () => {
withSetStateAllowed(() => {
this.instance().setState(state, callback);
this.update();
});
});
return this;
}
/**
* A method that sets the context of the root component, and re-renders. Useful for when you are
* wanting to test how the component behaves over time with changing contexts.
*
* NOTE: can only be called on a wrapper instance that is also the root instance.
*
* @param {Object} context object
* @returns {ShallowWrapper}
*/
setContext(context) {
if (this.root !== this) {
throw new Error('ShallowWrapper::setContext() can only be called on the root');
}
if (!this.options.context) {
throw new Error(
'ShallowWrapper::setContext() can only be called on a wrapper that was originally passed ' +
'a context option',
);
}
return this.rerender(null, context);
}
/**
* Whether or not a given react element exists in the shallow render tree.
*
* Example:
* ```
* const wrapper = shallow(<MyComponent />);
* expect(wrapper.contains(<div className="foo bar" />)).to.equal(true);
* ```
*
* @param {ReactElement|Array<ReactElement>} nodeOrNodes
* @returns {Boolean}
*/
contains(nodeOrNodes) {
if (!isReactElementAlike(nodeOrNodes)) {
throw new Error(
'ShallowWrapper::contains() can only be called with ReactElement (or array of them), ' +
'string or number as argument.',
);
}
const predicate = Array.isArray(nodeOrNodes)
? other => containsChildrenSubArray(nodeEqual, other, nodeOrNodes)
: other => nodeEqual(nodeOrNodes, other);
return findWhereUnwrapped(this, predicate).length > 0;
}
/**
* Whether or not a given react element exists in the shallow render tree.
* Match is based on the expected element and not on wrappers element.
* It will determine if one of the wrappers element "looks like" the expected
* element by checking if all props of the expected element are present
* on the wrappers element and equals to each other.
*
* Example:
* ```
* // MyComponent outputs <div><div class="foo">Hello</div></div>
* const wrapper = shallow(<MyComponent />);
* expect(wrapper.containsMatchingElement(<div>Hello</div>)).to.equal(true);
* ```
*
* @param {ReactElement} node
* @returns {Boolean}
*/
containsMatchingElement(node) {
const predicate = other => nodeMatches(node, other, (a, b) => a <= b);
return findWhereUnwrapped(this, predicate).length > 0;
}
/**
* Whether or not all the given react elements exists in the shallow render tree.
* Match is based on the expected element and not on wrappers element.
* It will determine if one of the wrappers element "looks like" the expected
* element by checking if all props of the expected element are present
* on the wrappers element and equals to each other.
*
* Example:
* ```
* const wrapper = shallow(<MyComponent />);
* expect(wrapper.containsAllMatchingElements([
* <div>Hello</div>,
* <div>Goodbye</div>,
* ])).to.equal(true);
* ```
*
* @param {Array<ReactElement>} nodes
* @returns {Boolean}
*/
containsAllMatchingElements(nodes) {
const invertedEquals = (n1, n2) => nodeMatches(n2, n1, (a, b) => a <= b);
const predicate = other => containsChildrenSubArray(invertedEquals, other, nodes);
return findWhereUnwrapped(this, predicate).length > 0;
}
/**
* Whether or not one of the given react elements exists in the shallow render tree.
* Match is based on the expected element and not on wrappers element.
* It will determine if one of the wrappers element "looks like" the expected
* element by checking if all props of the expected element are present
* on the wrappers element and equals to each other.
*
* Example:
* ```
* const wrapper = shallow(<MyComponent />);
* expect(wrapper.containsAnyMatchingElements([
* <div>Hello</div>,
* <div>Goodbye</div>,
* ])).to.equal(true);
* ```
*
* @param {Array<ReactElement>} nodes
* @returns {Boolean}
*/
containsAnyMatchingElements(nodes) {
return Array.isArray(nodes) && nodes.some(node => this.containsMatchingElement(node));
}
/**
* Whether or not a given react element exists in the shallow render tree.
*
* Example:
* ```
* const wrapper = shallow(<MyComponent />);
* expect(wrapper.contains(<div className="foo bar" />)).to.equal(true);
* ```
*
* @param {ReactElement} node
* @returns {Boolean}
*/
equals(node) {
return this.single('equals', () => nodeEqual(this.getNode(), node));
}
/**
* Whether or not a given react element matches the shallow render tree.
* Match is based on the expected element and not on wrapper root node.
* It will determine if the wrapper root node "looks like" the expected
* element by checking if all props of the expected element are present
* on the wrapper root node and equals to each other.
*
* Example:
* ```
* // MyComponent outputs <div class="foo">Hello</div>
* const wrapper = shallow(<MyComponent />);
* expect(wrapper.matchesElement(<div>Hello</div>)).to.equal(true);
* ```
*
* @param {ReactElement} node
* @returns {Boolean}
*/
matchesElement(node) {
return this.single('matchesElement', () => nodeMatches(node, this.getNode(), (a, b) => a <= b));
}
/**
* Finds every node in the render tree of the current wrapper that matches the provided selector.
*
* @param {String|Function} selector
* @returns {ShallowWrapper}
*/
find(selector) {
return this.complexSelector.find(selector, this);
}
/**
* Returns whether or not current node matches a provided selector.
*
* NOTE: can only be called on a wrapper of a single node.
*
* @param {String|Function} selector
* @returns {boolean}
*/
is(selector) {
const predicate = buildPredicate(selector);
return this.single('is', n => predicate(n));
}
/**
* Returns true if the component rendered nothing, i.e., null or false.
*
* @returns {boolean}
*/
isEmptyRender() {
return this.type() === null;
}
/**
* Returns a new wrapper instance with only the nodes of the current wrapper instance that match
* the provided predicate function. The predicate should receive a wrapped node as its first
* argument.
*
* @param {Function} predicate
* @returns {ShallowWrapper}
*/
filterWhere(predicate) {
return filterWhereUnwrapped(this, n => predicate(this.wrap(n)));
}
/**
* Returns a new wrapper instance with only the nodes of the current wrapper instance that match
* the provided selector.
*
* @param {String|Function} selector
* @returns {ShallowWrapper}
*/
filter(selector) {
const predicate = buildPredicate(selector);
return filterWhereUnwrapped(this, predicate);
}
/**
* Returns a new wrapper instance with only the nodes of the current wrapper that did not match
* the provided selector. Essentially the inverse of `filter`.
*
* @param {String|Function} selector
* @returns {ShallowWrapper}
*/
not(selector) {
const predicate = buildPredicate(selector);
return filterWhereUnwrapped(this, n => !predicate(n));
}
/**
* Returns a string of the rendered text of the current render tree. This function should be
* looked at with skepticism if being used to test what the actual HTML output of the component
* will be. If that is what you would like to test, use enzyme's `render` function instead.
*
* NOTE: can only be called on a wrapper of a single node.
*
* @returns {String}
*/
text() {
return this.single('text', getTextFromNode);
}
/**
* Returns the HTML of the node.
*
* NOTE: can only be called on a wrapper of a single node.
*
* @returns {String}
*/
html() {
return this.single('html', n => (this.type() === null ? null : renderToStaticMarkup(n)));
}
/**
* Returns the current node rendered to HTML and wrapped in a CheerioWrapper.
*
* NOTE: can only be called on a wrapper of a single node.
*
* @returns {CheerioWrapper}
*/
render() {
return this.type() === null ? cheerio() : cheerio.load(this.html()).root();
}
/**
* A method that unmounts the component. This can be used to simulate a component going through
* and unmount/mount lifecycle.
* @returns {ShallowWrapper}
*/
unmount() {
this.renderer.unmount();
return this;
}
/**
* Used to simulate events. Pass an eventname and (optionally) event arguments. This method of
* testing events should be met with some skepticism.
*
* @param {String} event
* @param {Array} args
* @returns {ShallowWrapper}
*/
simulate(event, ...args) {
const handler = this.prop(propFromEvent(event));
if (handler) {
withSetStateAllowed(() => {
// TODO(lmr): create/use synthetic events
// TODO(lmr): emulate React's event propagation
performBatchedUpdates(this, () => {
handler(...args);
});
this.root.update();
});
}
return this;
}
/**
* Returns the props hash for the current node of the wrapper.
*
* NOTE: can only be called on a wrapper of a single node.
*
* @returns {Object}
*/
props() {
return this.single('props', propsOfNode);
}
/**
* Returns the state hash for the root node of the wrapper. Optionally pass in a prop name and it
* will return just that value.
*
* NOTE: can only be called on a wrapper of a single node.
*
* @param {String} name (optional)
* @returns {*}
*/
state(name) {
if (this.root !== this) {
throw new Error('ShallowWrapper::state() can only be called on the root');
}
if (isFunctionalComponent(this.instance())) {
throw new Error('ShallowWrapper::state() can only be called on class components');
}
const _state = this.single('state', () => this.instance().state);
if (name !== undefined) {
return _state[name];
}
return _state;
}
/**
* Returns the context hash for the root node of the wrapper.
* Optionally pass in a prop name and it will return just that value.
*
* NOTE: can only be called on a wrapper of a single node.
*
* @param {String} name (optional)
* @returns {*}
*/
context(name) {
if (this.root !== this) {
throw new Error('ShallowWrapper::context() can only be called on the root');
}
if (!this.options.context) {
throw new Error(
'ShallowWrapper::context() can only be called on a wrapper that was originally passed ' +
'a context option',
);
}
const _context = this.single('context', () => this.instance().context);
if (name) {
return _context[name];
}
return _context;
}
/**
* Returns a new wrapper with all of the children of the current wrapper.
*
* @param {String|Function} [selector]
* @returns {ShallowWrapper}
*/
children(selector) {
const allChildren = this.flatMap(n => childrenOfNode(n.getNode()));
return selector ? allChildren.filter(selector) : allChildren;
}
/**
* Returns a new wrapper with a specific child
*
* @param {Number} [index]
* @returns {ShallowWrapper}
*/
childAt(index) {
return this.single('childAt', () => this.children().at(index));
}
/**
* Returns a wrapper around all of the parents/ancestors of the wrapper. Does not include the node
* in the current wrapper.
*
* NOTE: can only be called on a wrapper of a single node.
*
* @param {String|Function} [selector]
* @returns {ShallowWrapper}
*/
parents(selector) {
const allParents = this.wrap(
this.single('parents', n => parentsOfNode(n, this.root.getNode())),
);
return selector ? allParents.filter(selector) : allParents;
}
/**
* Returns a wrapper around the immediate parent of the current node.
*
* @returns {ShallowWrapper}
*/
parent() {
return this.flatMap(n => [n.parents().get(0)]);
}
/**
*
* @param {String|Function} selector
* @returns {ShallowWrapper}
*/
closest(selector) {
return this.is(selector) ? this : this.parents().filter(selector).first();
}
/**
* Shallow renders the current node and returns a shallow wrapper around it.
*
* NOTE: can only be called on wrapper of a single node.
*
* @param {Object} options
* @returns {ShallowWrapper}
*/
shallow(options) {
return this.single('shallow', n => this.wrap(n, null, options));
}
/**
* Returns the value of prop with the given name of the current node.
*
* @param propName
* @returns {*}
*/
prop(propName) {
return this.props()[propName];
}
/**
* Returns the key assigned to the current node.
*
* @returns {String}
*/
key() {
return this.single('key', n => n.key);
}
/**
* Returns the type of the current node of this wrapper. If it's a composite component, this will
* be the component constructor. If it's a native DOM node, it will be a string.
*
* @returns {String|Function}
*/
type() {
return this.single('type', typeOfNode);
}
/**
* Returns the name of the current node of this wrapper.
*
* In order of precedence => type.displayName -> type.name -> type.
*
* @returns {String}
*/
name() {
return this.single('name', displayNameOfNode);
}
/**
* Returns whether or not the current node has the given class name or not.
*
* NOTE: can only be called on a wrapper of a single node.
*
* @param className
* @returns {Boolean}
*/
hasClass(className) {
if (className && className.indexOf('.') !== -1) {
// eslint-disable-next-line no-console
console.warn(
'It looks like you\'re calling `ShallowWrapper::hasClass()` with a CSS selector. ' +
'hasClass() expects a class name, not a CSS selector.',
);
}
return this.single('hasClass', n => hasClassName(n, className));
}
/**
* Iterates through each node of the current wrapper and executes the provided function with a
* wrapper around the corresponding node passed in as the first argument.
*
* @param {Function} fn
* @returns {ShallowWrapper}
*/
forEach(fn) {
this.getNodes().forEach((n, i) => fn.call(this, this.wrap(n), i));
return this;
}
/**
* Maps the current array of nodes to another array. Each node is passed in as a `ShallowWrapper`
* to the map function.
*
* @param {Function} fn
* @returns {Array}
*/
map(fn) {
return this.getNodes().map((n, i) => fn.call(this, this.wrap(n), i));
}
/**
* Reduces the current array of nodes to a value. Each node is passed in as a `ShallowWrapper`
* to the reducer function.
*
* @param {Function} fn - the reducer function
* @param {*} initialValue - the initial value
* @returns {*}
*/
reduce(fn, initialValue) {
return this.getNodes().reduce(
(accum, n, i) => fn.call(this, accum, this.wrap(n), i),
initialValue,
);
}
/**
* Reduces the current array of nodes to another array, from right to left. Each node is passed
* in as a `ShallowWrapper` to the reducer function.
*
* @param {Function} fn - the reducer function
* @param {*} initialValue - the initial value
* @returns {*}
*/
reduceRight(fn, initialValue) {
return this.getNodes().reduceRight(
(accum, n, i) => fn.call(this, accum, this.wrap(n), i),
initialValue,
);
}
/**
* Returns a new wrapper with a subset of the nodes of the original wrapper, according to the
* rules of `Array#slice`.
*
* @param {Number} begin
* @param {Number} end
* @returns {ShallowWrapper}
*/
slice(begin, end) {
return this.wrap(this.getNodes().slice(begin, end));
}
/**
* Returns whether or not any of the nodes in the wrapper match the provided selector.
*
* @param {Function|String} selector
* @returns {Boolean}
*/
some(selector) {
if (this.root === this) {
throw new Error('ShallowWrapper::some() can not be called on the root');
}
const predicate = buildPredicate(selector);
return this.getNodes().some(predicate);
}
/**
* Returns whether or not any of the nodes in the wrapper pass the provided predicate function.
*
* @param {Function} predicate
* @returns {Boolean}
*/
someWhere(predicate) {
return this.getNodes().some((n, i) => predicate.call(this, this.wrap(n), i));
}
/**
* Returns whether or not all of the nodes in the wrapper match the provided selector.
*
* @param {Function|String} selector
* @returns {Boolean}
*/
every(selector) {
const predicate = buildPredicate(selector);
return this.getNodes().every(predicate);
}
/**
* Returns whether or not any of the nodes in the wrapper pass the provided predicate function.
*
* @param {Function} predicate
* @returns {Boolean}
*/
everyWhere(predicate) {
return this.getNodes().every((n, i) => predicate.call(this, this.wrap(n), i));
}
/**
* Utility method used to create new wrappers with a mapping function that returns an array of
* nodes in response to a single node wrapper. The returned wrapper is a single wrapper around
* all of the mapped nodes flattened (and de-duplicated).
*
* @param {Function} fn
* @returns {ShallowWrapper}
*/
flatMap(fn) {
const nodes = this.getNodes().map((n, i) => fn.call(this, this.wrap(n), i));
const flattened = flatten(nodes, true);
const uniques = unique(flattened);
const compacted = compact(uniques);
return this.wrap(compacted);
}
/**
* Finds all nodes in the current wrapper nodes' render trees that match the provided predicate
* function. The predicate function will receive the nodes inside a ShallowWrapper as its
* first argument.
*
* @param {Function} predicate
* @returns {ShallowWrapper}
*/
findWhere(predicate) {
return findWhereUnwrapped(this, n => predicate(this.wrap(n)));
}
/**
* Returns the node at a given index of the current wrapper.
*
* @param index
* @returns {ReactElement}
*/
get(index) {
return this.getNodes()[index];
}
/**
* Returns a wrapper around the node at a given index of the current wrapper.
*
* @param index
* @returns {ShallowWrapper}
*/
at(index) {
return this.wrap(this.getNodes()[index]);
}
/**
* Returns a wrapper around the first node of the current wrapper.
*
* @returns {ShallowWrapper}
*/
first() {
return this.at(0);
}
/**
* Returns a wrapper around the last node of the current wrapper.
*
* @returns {ShallowWrapper}
*/
last() {
return this.at(this.length - 1);
}
/**
* Delegates to exists()
*
* @returns {boolean}
*/
isEmpty() {
// eslint-disable-next-line no-console
console.warn('Enzyme::Deprecated method isEmpty() called, use exists() instead.');
return !this.exists();
}
/**
* Returns true if the current wrapper has nodes. False otherwise.
*
* @returns {boolean}
*/
exists() {
return this.length > 0;
}
/**
* Utility method that throws an error if the current instance has a length other than one.
* This is primarily used to enforce that certain methods are only run on a wrapper when it is
* wrapping a single node.
*
* @param fn
* @returns {*}
*/
single(name, fn) {
const fnName = typeof name === 'string' ? name : 'unknown';
const callback = typeof fn === 'function' ? fn : name;
if (this.length !== 1) {
throw new Error(
`Method “${fnName}” is only meant to be run on a single node. ${this.length} found instead.`,
);
}
return callback.call(this, this.getNode());
}
/**
* Helpful utility method to create a new wrapper with the same root as the current wrapper, with
* any nodes passed in as the first parameter automatically wrapped.
*
* @param node
* @returns {ShallowWrapper}
*/
wrap(node, root = this.root, ...args) {
if (node instanceof ShallowWrapper) {
return node;
}
return new ShallowWrapper(node, root, ...args);
}
/**
* Returns an HTML-like string of the shallow render for debugging purposes.
*
* @param {Object} options - (Optional) Property bag of additional options.
* options.ignoreProps - if true, props are omitted from the string.
* @returns {String}
*/
debug(options = {}) {
return debugNodes(this.getNodes(), options);
}
/**
* Invokes intercepter and returns itself. intercepter is called with itself.
* This is helpful when debugging nodes in method chains.
* @param fn
* @returns {ShallowWrapper}
*/
tap(intercepter) {
intercepter(this);
return this;
}
/**
* Primarily useful for HOCs (higher-order components), this method may only be
* run on a single, non-DOM node, and will return the node, shallow-rendered.
*
* @param {Object} options
* @returns {ShallowWrapper}
*/
dive(options = {}) {
const name = 'dive';
return this.single(name, (n) => {
if (isDOMComponentElement(n)) {
throw new TypeError(`ShallowWrapper::${name}() can not be called on DOM components`);
}
if (!isCustomComponentElement(n)) {
throw new TypeError(`ShallowWrapper::${name}() can only be called on components`);
}
return this.wrap(n, null, { ...this.options, ...options });
});
}
}
if (ITERATOR_SYMBOL) {
Object.defineProperty(ShallowWrapper.prototype, ITERATOR_SYMBOL, {
configurable: true,
value: function iterator() {
return this.nodes[ITERATOR_SYMBOL]();
},
});
}
export default ShallowWrapper;