diff --git a/.travis.yml b/.travis.yml index 8eaf0f057..24ed1735c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,61 +8,39 @@ node_js: before_install: - 'if [ "${TRAVIS_NODE_VERSION}" != "0.9" ]; then case "$(npm --version)" in 1.*) npm install -g npm@1.4.28 ;; 2.*) npm install -g npm@2 ;; esac ; fi' - 'if [ "${TRAVIS_NODE_VERSION}" != "0.6" ] && [ "${TRAVIS_NODE_VERSION}" != "0.9" ]; then npm install -g npm; fi' -before_script: "sh install-relevant-react.sh" +before_script: + - "sh install-relevant-react.sh" + - if [ -n "${KARMA-}" ]; then export CHROME_BIN=chromium-browser; export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start; fi script: - - 'if [ -z "$REACT" ] && [ "${TRAVIS_NODE_VERSION}" = "4" ]; then npm run test:env -- "${EXAMPLE}" ; elif [ -z "$REACT" ]; then echo "Test Skipped" ; elif [ "${TRAVIS_NODE_VERSION}" = "4" ]; then npm run lint && npm run travis ; elif [ "${TRAVIS_NODE_VERSION}" = "0.12" ] || [ "${TRAVIS_NODE_VERSION}" = "0.10" ]; then npm run travis ; else npm test ; fi' + - 'if [ -n "${EXAMPLE-}" ]; then npm run test:env -- "${EXAMPLE}" ; elif [ -n "${LINT-}" ]; then npm run lint; elif [ -n "${KARMA-}" ]; then npm run test:karma -- --single-run; elif [ -n "${REACT-}" ]; then npm run travis; else false ; fi' after_script: - 'if [ "${TRAVIS_NODE_VERSION}" = "4" ] || [ "${TRAVIS_NODE_VERSION}" = "0.12" ]; then cat ./coverage/lcov.info | ./node_modules/.bin/coveralls ; fi' +sudo: false +matrix: + fast_finish: true + include: + - node_js: "node" + env: LINT=true + - node_js: "node" + env: EXAMPLE=mocha + - node_js: "node" + env: EXAMPLE=karma + - node_js: "node" + env: EXAMPLE=react-native + - node_js: "node" + env: EXAMPLE=karma-webpack + - node_js: "node" + env: EXAMPLE=jest + - node_js: "node" + env: KARMA=true REACT=0.13 + - node_js: "node" + env: KARMA=true REACT=0.14 + - node_js: "node" + env: KARMA=true REACT=15 + allow_failures: + - node_js: "node" + env: EXAMPLE=react-native env: - REACT=0.13 - REACT=0.14 - REACT=15 - - EXAMPLE=mocha - - EXAMPLE=karma - - EXAMPLE=react-native - - EXAMPLE=karma-webpack - - EXAMPLE=jest -sudo: false -matrix: - fast_finish: true - exclude: - - node_js: "5" - env: EXAMPLE=mocha - - node_js: "5" - env: EXAMPLE=karma - - node_js: "5" - env: EXAMPLE=react-native - - node_js: "5" - env: EXAMPLE=karma-webpack - - node_js: "5" - env: EXAMPLE=jest - - node_js: "4" - env: EXAMPLE=mocha - - node_js: "4" - env: EXAMPLE=karma - - node_js: "4" - env: EXAMPLE=react-native - - node_js: "4" - env: EXAMPLE=karma-webpack - - node_js: "4" - env: EXAMPLE=jest - - node_js: "0.12" - env: EXAMPLE=mocha - - node_js: "0.12" - env: EXAMPLE=karma - - node_js: "0.12" - env: EXAMPLE=react-native - - node_js: "0.12" - env: EXAMPLE=karma-webpack - - node_js: "0.12" - env: EXAMPLE=jest - - node_js: "0.10" - env: EXAMPLE=mocha - - node_js: "0.10" - env: EXAMPLE=karma - - node_js: "0.10" - env: EXAMPLE=react-native - - node_js: "0.10" - env: EXAMPLE=karma-webpack - - node_js: "0.10" - env: EXAMPLE=jest diff --git a/INTHEWILD.md b/INTHEWILD.md index 53e0ba21f..e24a84fab 100644 --- a/INTHEWILD.md +++ b/INTHEWILD.md @@ -10,6 +10,9 @@ Organizations - [Hudl](http://hudl.github.io/) - [NET-A-PORTER](https://github.com/NET-A-PORTER) - [Rangle.io](https://github.com/rangle) + - [GoDaddy](https://github.com/godaddy) + - [Airware](https://github.com/airware) + - [Flatiron School](https://github.com/flatiron-labs) Projects ---------- @@ -18,3 +21,4 @@ Projects - [Reactstrap](https://github.com/reactstrap/reactstrap) - [Recompose](https://github.com/acdlite/recompose) - [Reapop](https://github.com/LouisBarranqueiro/reapop) + - [React Dates](https://github.com/airbnb/react-dates) diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index e7fc582dd..844287a88 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -2,7 +2,7 @@ A Selector in enzyme is similar to a CSS selector, but can be a number of other things as well in order to easily specify a criteria by which you want to find nodes in a enzyme wrapper. See the -Selector page for more information. +[Selector page](/docs/api/selector.md) for more information. # wrapper diff --git a/docs/api/ReactWrapper/html.md b/docs/api/ReactWrapper/html.md index 6fb610ba4..4fe3fe66b 100644 --- a/docs/api/ReactWrapper/html.md +++ b/docs/api/ReactWrapper/html.md @@ -1,6 +1,6 @@ # `.html() => String` -Returns a string of the rendered HTML markup of the current render tree. +Returns a string of the rendered HTML markup of the current render tree. See also [.debug()](debug.md) Note: can only be called on a wrapper of a single node. diff --git a/docs/api/ShallowWrapper/dive.md b/docs/api/ShallowWrapper/dive.md new file mode 100644 index 000000000..d56674b13 --- /dev/null +++ b/docs/api/ShallowWrapper/dive.md @@ -0,0 +1,52 @@ +# `.dive([options]) => ShallowWrapper` + +Shallow render the one non-DOM child of the current wrapper, and return a wrapper around the result. + +NOTE: can only be called on wrapper of a single non-DOM component element node. + + +#### Arguments + +1. `options` (`Object` [optional]): +- `options.context`: (`Object` [optional]): Context to be passed into the component + + + +#### Returns + +`ShallowWrapper`: A new wrapper that wraps the current node after it's been shallow rendered. + + + +#### Examples + +```jsx +class Bar extends React.Component { + render() { + return ( +
+
+
+ ); + } +} +``` + +```jsx +class Foo extends React.Component { + render() { + return ( +
+ +
+ ); + } +} +``` + +```jsx +const wrapper = shallow(); +expect(wrapper.find('.in-bar')).to.have.length(0); +expect(wrapper.find(Bar)).to.have.length(1); +expect(wrapper.find(Bar).dive().find('.in-bar')).to.have.length(1); +``` diff --git a/docs/api/ShallowWrapper/hasClass.md b/docs/api/ShallowWrapper/hasClass.md index 38f979c65..baf6cbc4d 100644 --- a/docs/api/ShallowWrapper/hasClass.md +++ b/docs/api/ShallowWrapper/hasClass.md @@ -11,7 +11,7 @@ Returns whether or not the current node has a `className` prop including the pas #### Returns -`Boolean`: whether or not the current node has the class or note. +`Boolean`: whether or not the current node has the class or not. diff --git a/docs/api/ShallowWrapper/parents.md b/docs/api/ShallowWrapper/parents.md index f5d23e620..ee24b195e 100644 --- a/docs/api/ShallowWrapper/parents.md +++ b/docs/api/ShallowWrapper/parents.md @@ -2,7 +2,7 @@ Returns a wrapper around all of the parents/ancestors of the wrapper. Does not include the node in the current wrapper. Optionally, a selector can be provided and it will filter the parents by -this selector +this selector. Note: can only be called on a wrapper of a single node. diff --git a/docs/api/ShallowWrapper/prop.md b/docs/api/ShallowWrapper/prop.md index d19f6fe85..9ab4c6c87 100644 --- a/docs/api/ShallowWrapper/prop.md +++ b/docs/api/ShallowWrapper/prop.md @@ -1,13 +1,17 @@ # `.prop(key) => Any` -Returns the prop value for the node of the current wrapper with the provided key. +Returns the prop value for the root node of the wrapper with the provided key. +`.prop(key)` can only be called on a wrapper of a single node. -NOTE: can only be called on a wrapper of a single node. +NOTE: When called on a shallow wrapper, `.prop(key)` will return values for +props on the root node that the component *renders*, not the component itself. +To return the props for the entire React component, use `wrapper.instance().props`. +See [`.instance() => ReactComponent`](instance.md) #### Arguments 1. `key` (`String`): The prop name such that this will return value will be the `this.props[key]` -of the component instance. +of the root node of the component. @@ -15,8 +19,27 @@ of the component instance. ```jsx -const wrapper = shallow(); -expect(wrapper.prop('foo')).to.equal(10); +const MyComponent = React.createClass({ + render() { + return ( +
Hello
+ ) + } +}) +const wrapper = shallow(); +expect(wrapper.prop('includedProp')).to.equal("Success!"); + +// Warning: .prop(key) only returns values for props that exist in the root node. +// See the note above about wrapper.instance().props to return all props in the React component. + +wrapper.prop('includedProp'); +// "Success!" + +wrapper.prop('excludedProp'); +// undefined + +wrapper.instance().props.excludedProp; +// "I'm not included" ``` diff --git a/docs/api/ShallowWrapper/props.md b/docs/api/ShallowWrapper/props.md index 6d480d8a0..6fa070932 100644 --- a/docs/api/ShallowWrapper/props.md +++ b/docs/api/ShallowWrapper/props.md @@ -1,16 +1,38 @@ # `.props() => Object` -Returns the props hash for the current node of the wrapper. +Returns the props hash for the root node of the wrapper. `.props()` can only be +called on a wrapper of a single node. -NOTE: can only be called on a wrapper of a single node. +NOTE: When called on a shallow wrapper, `.props()` will return values for +props on the root node that the component *renders*, not the component itself. +To return the props for the entire React component, use `wrapper.instance().props`. +See [`.instance() => ReactComponent`](instance.md) #### Example ```jsx -const wrapper = shallow(); -expect(wrapper.props().foo).to.equal(10); +const MyComponent = React.createClass({ + render() { + return ( +
Hello
+ ) + } +}) +const wrapper = shallow(); +expect(wrapper.props().includedProp).to.equal("Success!"); + +// Warning: .props() only returns props that are passed to the root node, +// which does not include excludedProp in this example. +// See the note above about wrapper.instance().props. + +wrapper.props(); +// {children: "Hello", className: "foo bar", includedProp="Success!"} + +wrapper.instance().props; +// {children: "Hello", className: "foo bar", includedProp:"Success!", excludedProp: "I'm not included"} + ``` diff --git a/docs/api/mount.md b/docs/api/mount.md index 3986881fb..503a5afb9 100644 --- a/docs/api/mount.md +++ b/docs/api/mount.md @@ -1,7 +1,7 @@ # Full Rendering API (`mount(...)`) -Full DOM rendering is ideal for use cases where you have components that may interact with DOM apis, -or may require the full lifecycle in order to fully test the component (ie, `componentDidMount` +Full DOM rendering is ideal for use cases where you have components that may interact with DOM APIs, +or may require the full lifecycle in order to fully test the component (i.e., `componentDidMount` etc.) Full DOM rendering requires that a full DOM API be available at the global scope. This means that @@ -12,11 +12,13 @@ implemented completely in JS. ```jsx import { mount } from 'enzyme'; +import sinon from 'sinon'; +import Foo from './Foo'; describe('', () => { it('calls componentDidMount', () => { - spy(Foo.prototype, 'componentDidMount'); + sinon.spy(Foo.prototype, 'componentDidMount'); const wrapper = mount(); expect(Foo.prototype.componentDidMount.calledOnce).to.equal(true); }); @@ -29,7 +31,7 @@ describe('', () => { }); it('simulates click events', () => { - const onButtonClick = spy(); + const onButtonClick = sinon.spy(); const wrapper = mount( ); @@ -186,28 +188,28 @@ Maps the current array of nodes to another array. #### [`.matchesElement(node) => Boolean`](ReactWrapper/matchesElement.md) Returns whether or not a given react element matches the current render tree. -#### [`.reduce(fn[, initialValue]) => Any`](/docs/api/ReactWrapper/reduce.md) +#### [`.reduce(fn[, initialValue]) => Any`](ReactWrapper/reduce.md) Reduces the current array of nodes to a value -#### [`.reduceRight(fn[, initialValue]) => Any`](/docs/api/ReactWrapper/reduceRight.md) +#### [`.reduceRight(fn[, initialValue]) => Any`](ReactWrapper/reduceRight.md) Reduces the current array of nodes to a value, from right to left. #### [`.tap(intercepter) => Self`](ReactWrapper/tap.md) Taps into the wrapper method chain. Helpful for debugging. -#### [`.some(selector) => Boolean`](/docs/api/ReactWrapper/some.md) +#### [`.some(selector) => Boolean`](ReactWrapper/some.md) Returns whether or not any of the nodes in the wrapper match the provided selector. -#### [`.someWhere(predicate) => Boolean`](/docs/api/ReactWrapper/someWHere.md) +#### [`.someWhere(predicate) => Boolean`](ReactWrapper/someWhere.md) Returns whether or not any of the nodes in the wrapper pass the provided predicate function. -#### [`.every(selector) => Boolean`](/docs/api/ReactWrapper/every.md) +#### [`.every(selector) => Boolean`](ReactWrapper/every.md) Returns whether or not all of the nodes in the wrapper match the provided selector. -#### [`.everyWhere(predicate) => Boolean`](/docs/api/ReactWrapper/everyWhere.md) +#### [`.everyWhere(predicate) => Boolean`](ReactWrapper/everyWhere.md) Returns whether or not all of the nodes in the wrapper pass the provided predicate function. -#### [`.ref(refName) => ReactWrapper`](/docs/api/ReactWrapper/ref.md) +#### [`.ref(refName) => ReactWrapper`](ReactWrapper/ref.md) Returns a wrapper of the node that matches the provided reference name. #### [`.detach() => void`](ReactWrapper/detach.md) diff --git a/docs/api/shallow.md b/docs/api/shallow.md index a67d2694d..2e31a3b31 100644 --- a/docs/api/shallow.md +++ b/docs/api/shallow.md @@ -187,23 +187,26 @@ Iterates through each node of the current wrapper and executes the provided func #### [`.map(fn) => Array`](ShallowWrapper/map.md) Maps the current array of nodes to another array. -#### [`.reduce(fn[, initialValue]) => Any`](/docs/api/ShallowWrapper/reduce.md) +#### [`.reduce(fn[, initialValue]) => Any`](ShallowWrapper/reduce.md) Reduces the current array of nodes to a value -#### [`.reduceRight(fn[, initialValue]) => Any`](/docs/api/ShallowWrapper/reduceRight.md) +#### [`.reduceRight(fn[, initialValue]) => Any`](ShallowWrapper/reduceRight.md) Reduces the current array of nodes to a value, from right to left. #### [`.tap(intercepter) => Self`](ShallowWrapper/tap.md) Taps into the wrapper method chain. Helpful for debugging. -#### [`.some(selector) => Boolean`](/docs/api/ShallowWrapper/some.md) +#### [`.some(selector) => Boolean`](ShallowWrapper/some.md) Returns whether or not any of the nodes in the wrapper match the provided selector. -#### [`.someWhere(predicate) => Boolean`](/docs/api/ShallowWrapper/someWhere.md) +#### [`.someWhere(predicate) => Boolean`](ShallowWrapper/someWhere.md) Returns whether or not any of the nodes in the wrapper pass the provided predicate function. -#### [`.every(selector) => Boolean`](/docs/api/ShallowWrapper/every.md) +#### [`.every(selector) => Boolean`](ShallowWrapper/every.md) Returns whether or not all of the nodes in the wrapper match the provided selector. -#### [`.everyWhere(predicate) => Boolean`](/docs/api/ShallowWrapper/everyWhere.md) +#### [`.everyWhere(predicate) => Boolean`](ShallowWrapper/everyWhere.md) Returns whether or not all of the nodes in the wrapper pass the provided predicate function. + +#### [`.dive([options]) => ShallowWrapper`](ShallowWrapper/dive.md) +Shallow render the one non-DOM child of the current wrapper, and return a wrapper around the result. diff --git a/docs/guides/jsdom.md b/docs/guides/jsdom.md index a5b36fe18..5ffbd4f75 100644 --- a/docs/guides/jsdom.md +++ b/docs/guides/jsdom.md @@ -16,13 +16,10 @@ As a result, a standalone script like the one below is generally a good approach var jsdom = require('jsdom').jsdom; -var exposedProperties = ['window', 'navigator', 'document']; - global.document = jsdom(''); global.window = document.defaultView; Object.keys(document.defaultView).forEach((property) => { if (typeof global[property] === 'undefined') { - exposedProperties.push(property); global[property] = document.defaultView[property]; } }); diff --git a/docs/guides/mocha.md b/docs/guides/mocha.md index 30fe94f9e..f0eabf8d9 100644 --- a/docs/guides/mocha.md +++ b/docs/guides/mocha.md @@ -9,6 +9,7 @@ npm i --save-dev enzyme ```jsx import React from 'react'; +import { expect } from 'chai'; import { mount, shallow } from 'enzyme'; describe('', () => { diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 000000000..8bbdc4da0 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,92 @@ +/* eslint-disable no-var,prefer-arrow-callback,vars-on-top */ + +require('babel-register'); + +var IgnorePlugin = require('webpack').IgnorePlugin; +var REACT013 = require('./src/version').REACT013; + +module.exports = function karma(config) { + config.set({ + basePath: '.', + + plugins: [ + 'karma-chrome-launcher', + 'karma-firefox-launcher', + 'karma-mocha', + 'karma-webpack', + 'karma-sourcemap-loader', + ], + + customLaunchers: { + Chrome_travis: { + base: 'Chrome', + flags: ['--no-sandbox'], + }, + }, + + frameworks: ['mocha'], + + reporters: ['dots'], + + files: [ + 'test/*.{jsx,js}', + ], + + exclude: [ + 'test/_*.{jsx,js}', + ], + + browsers: [ + process.env.TRAVIS ? 'Chrome_travis' : 'Chrome', + 'Firefox', + ], + + preprocessors: { + 'test/*.{jsx,js}': ['webpack', 'sourcemap'], + }, + + webpack: { + devtool: 'inline-source-map', + resolve: { + extensions: ['', '.js', '.jsx', '.json'], + alias: { + // dynamic require calls in sinon confuse webpack so we ignore it + sinon: 'sinon/pkg/sinon', + }, + }, + module: { + noParse: [ + // dynamic require calls in sinon confuse webpack so we ignore it + /node_modules\/sinon\//, + ], + loaders: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: 'babel-loader', + }, + { + test: /\.json$/, + loader: 'json-loader', + }, + ], + }, + plugins: [ + /* + this list of conditional IgnorePlugins mirrors the conditional + requires in src/react-compat.js and exists to avoid error + output from the webpack compilation + */ + !REACT013 && new IgnorePlugin(/react\/lib\/ExecutionEnvironment/), + !REACT013 && new IgnorePlugin(/react\/lib\/ReactContext/), + !REACT013 && new IgnorePlugin(/react\/addons/), + REACT013 && new IgnorePlugin(/react-dom/), + REACT013 && new IgnorePlugin(/react-addons-test-utils/), + ].filter(function filterPlugins(plugin) { return plugin !== false; }), + }, + + webpackServer: { + noInfo: true, + }, + }); +}; diff --git a/package.json b/package.json index c75c52346..060687cb3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test:only": "mocha --recursive test --reporter dot", "test:single": "mocha --watch --reporter dot", "test:watch": "mocha --recursive test --watch --reporter dot", + "test:karma": "karma start", "test:env": "sh ./example-test.sh", "test:all": "npm run react:13 && npm run test:only && npm run react:14 && npm run test:only && npm run react:15 && npm run test:only", "react:clean": "rimraf node_modules/react node_modules/react-dom node_modules/react-addons-test-utils", @@ -51,39 +52,48 @@ "author": "Leland Richardson ", "license": "MIT", "dependencies": { - "cheerio": "^0.20.0", + "cheerio": "^0.22.0", "is-subset": "^0.1.1", - "lodash": "^4.14.1", + "lodash": "^4.15.0", "object-is": "^1.0.1", "object.assign": "^4.0.4", "object.values": "^1.0.3" }, "devDependencies": { - "babel-cli": "^6.11.4", - "babel-core": "^6.13.2", + "babel-cli": "^6.14.0", + "babel-core": "^6.14.0", "babel-eslint": "^6.1.2", - "babel-preset-airbnb": "^2.0.0", - "babel-register": "^6.11.6", + "babel-loader": "^6.2.5", + "babel-preset-airbnb": "^2.1.0", + "babel-register": "^6.14.0", "chai": "^3.5.0", - "coveralls": "^2.11.12", + "coveralls": "^2.11.14", "enzyme-example-jest": "^0.1.0", "enzyme-example-karma": "^0.1.1", "enzyme-example-karma-webpack": "^0.1.4", "enzyme-example-mocha": "^0.1.0", "enzyme-example-react-native": "^0.1.0", - "eslint": "^3.2.2", - "eslint-config-airbnb": "^10.0.0", - "eslint-plugin-import": "^1.12.0", - "eslint-plugin-jsx-a11y": "^2.0.1", - "eslint-plugin-react": "^6.0.0", + "eslint": "^3.6.1", + "eslint-config-airbnb": "^12.0.0", + "eslint-plugin-import": "^1.16.0", + "eslint-plugin-jsx-a11y": "^2.2.2", + "eslint-plugin-react": "^6.3.0", "gitbook-cli": "^1.0.1", "istanbul": "^1.0.0-alpha.2", "jsdom": "^6.1.0", - "mocha": "^3.0.1", + "json-loader": "^0.5.4", + "karma": "^1.3.0", + "karma-chrome-launcher": "^1.0.1", + "karma-firefox-launcher": "^1.0.0", + "karma-mocha": "^1.1.1", + "karma-sourcemap-loader": "^0.3.7", + "karma-webpack": "^1.7.0", + "mocha": "^3.1.0", "rimraf": "^2.5.4", - "sinon": "^1.17.5" + "sinon": "^1.17.6", + "webpack": "^1.13.2" }, "peerDependencies": { - "react": "0.13.x || 0.14.x || ^15.0.0-0" + "react": "0.13.x || 0.14.x || ^15.0.0-0 || 15.x" } } diff --git a/src/ComplexSelector.js b/src/ComplexSelector.js index 1a5722d63..27346b823 100644 --- a/src/ComplexSelector.js +++ b/src/ComplexSelector.js @@ -7,17 +7,15 @@ export default class ComplexSelector { this.childrenOfNode = childrenOfNode; } - getSelectors(selector) { + getSelectors(selector) { // eslint-disable-line class-methods-use-this const selectors = split(selector, / (?=(?:(?:[^"]*"){2})*[^"]*$)/); return selectors.reduce((list, sel) => { if (sel === '+' || sel === '~') { const temp = list.pop(); - list.push(sel, temp); - return list; + return list.concat(sel, temp); } - list.push(sel); - return list; + return list.concat(sel); }, []); } @@ -35,7 +33,7 @@ export default class ComplexSelector { return (child) => { if (firstPredicate(child)) { - return (sibling) => secondPredicate(sibling); + return sibling => secondPredicate(sibling); } return false; @@ -85,16 +83,7 @@ export default class ComplexSelector { } treeFilterDirect() { - return (tree, fn) => { - const results = []; - this.childrenOfNode(tree).forEach(child => { - if (fn(child)) { - results.push(child); - } - }); - - return results; - }; + return (tree, fn) => this.childrenOfNode(tree).filter(child => fn(child)); } treeFindSiblings(selectSiblings) { @@ -102,16 +91,15 @@ export default class ComplexSelector { const results = []; const list = [this.childrenOfNode(tree)]; - const traverseChildren = (children) => - children.forEach((child, i) => { - const secondPredicate = fn(child); + const traverseChildren = children => children.forEach((child, i) => { + const secondPredicate = fn(child); - list.push(this.childrenOfNode(child)); + list.push(this.childrenOfNode(child)); - if (secondPredicate) { - selectSiblings(children, secondPredicate, results, i); - } - }); + if (secondPredicate) { + selectSiblings(children, secondPredicate, results, i); + } + }); while (list.length) { traverseChildren(list.shift()); diff --git a/src/MountedTraversal.js b/src/MountedTraversal.js index 7b1f9da95..d6e8685e9 100644 --- a/src/MountedTraversal.js +++ b/src/MountedTraversal.js @@ -2,7 +2,6 @@ import isEmpty from 'lodash/isEmpty'; import isSubset from 'is-subset'; import { internalInstance, - coercePropValue, nodeEqual, propsOfNode, isFunctionalComponent, @@ -12,6 +11,7 @@ import { AND, SELECTOR, nodeHasType, + nodeHasProperty, } from './Utils'; import { isDOMComponent, @@ -85,26 +85,10 @@ export function instHasType(inst, type) { export function instHasProperty(inst, propKey, stringifiedPropValue) { if (!isDOMComponent(inst)) return false; - const node = getNode(inst); - const nodeProps = propsOfNode(node); - const descriptor = Object.getOwnPropertyDescriptor(nodeProps, propKey); - if (descriptor && descriptor.get) { - return false; - } - const nodePropValue = nodeProps[propKey]; - const propValue = coercePropValue(propKey, stringifiedPropValue); - - // intentionally not matching node props that are undefined - if (nodePropValue === undefined) { - return false; - } - - if (propValue) { - return nodePropValue === propValue; - } + const node = getNode(inst); - return Object.prototype.hasOwnProperty.call(nodeProps, propKey); + return nodeHasProperty(node, propKey, stringifiedPropValue); } // called with private inst @@ -133,7 +117,7 @@ export function childrenOfInstInternal(inst) { return false; } return true; - }).map(key => { + }).map((key) => { if (!REACT013 && typeof renderedChildren[key]._currentElement.type === 'function') { return renderedChildren[key]._instance; } diff --git a/src/ReactWrapper.jsx b/src/ReactWrapper.jsx index 131ba0aaa..f6ba502b4 100644 --- a/src/ReactWrapper.jsx +++ b/src/ReactWrapper.jsx @@ -87,7 +87,9 @@ export default class ReactWrapper { } else { this.component = null; this.root = root; - if (!Array.isArray(nodes)) { + if (!nodes) { + this.nodes = []; + } else if (!Array.isArray(nodes)) { this.node = nodes; this.nodes = [nodes]; } else { @@ -358,14 +360,7 @@ export default class ReactWrapper { * @returns {Boolean} */ containsAnyMatchingElements(nodes) { - if (!Array.isArray(nodes)) return false; - if (nodes.length <= 0) return false; - for (let i = 0; i < nodes.length; i++) { - if (this.containsMatchingElement(nodes[i])) { - return true; - } - } - return false; + return Array.isArray(nodes) && nodes.some(node => this.containsMatchingElement(node)); } /** @@ -465,7 +460,7 @@ export default class ReactWrapper { * @returns {String} */ html() { - return this.single('html', n => { + return this.single('html', (n) => { const node = findDOMNode(n); return node === null ? null : node.outerHTML.replace(/\sdata-(reactid|reactroot)+="([^"]*)+"/g, ''); @@ -493,7 +488,7 @@ export default class ReactWrapper { * @returns {ReactWrapper} */ simulate(event, mock = {}) { - this.single('simulate', n => { + this.single('simulate', (n) => { const mappedEvent = mapNativeEventNames(event); const eventFn = Simulate[mappedEvent]; if (!eventFn) { diff --git a/src/ReactWrapperComponent.jsx b/src/ReactWrapperComponent.jsx index 3cb191557..b5cc35bb2 100644 --- a/src/ReactWrapperComponent.jsx +++ b/src/ReactWrapperComponent.jsx @@ -1,6 +1,8 @@ import React, { PropTypes } from 'react'; import objectAssign from 'object.assign'; +/* eslint react/forbid-prop-types: 0 */ + /** * This is a utility component to wrap around the nodes we are * passing in to `mount()`. Theoretically, you could do everything diff --git a/src/ShallowTraversal.js b/src/ShallowTraversal.js index e12608428..c561fe073 100644 --- a/src/ShallowTraversal.js +++ b/src/ShallowTraversal.js @@ -2,7 +2,6 @@ import React from 'react'; import isEmpty from 'lodash/isEmpty'; import isSubset from 'is-subset'; import { - coercePropValue, propsOfNode, splitSelector, isCompoundSelector, @@ -10,6 +9,7 @@ import { AND, SELECTOR, nodeHasType, + nodeHasProperty, } from './Utils'; @@ -17,7 +17,7 @@ export function childrenOfNode(node) { if (!node) return []; const maybeArray = propsOfNode(node).children; const result = []; - React.Children.forEach(maybeArray, child => { + React.Children.forEach(maybeArray, (child) => { if (child !== null && child !== false && typeof child !== 'undefined') { result.push(child); } @@ -40,7 +40,7 @@ export function treeForEach(tree, fn) { export function treeFilter(tree, fn) { const results = []; - treeForEach(tree, node => { + treeForEach(tree, (node) => { if (fn(node)) { results.push(node); } @@ -56,7 +56,7 @@ export function pathToNode(node, root) { const queue = [root]; const path = []; - const hasNode = (testNode) => node === testNode; + const hasNode = testNode => node === testNode; while (queue.length) { const current = queue.pop(); @@ -84,25 +84,7 @@ export function nodeHasId(node, id) { } -export function nodeHasProperty(node, propKey, stringifiedPropValue) { - const nodeProps = propsOfNode(node); - const propValue = coercePropValue(propKey, stringifiedPropValue); - const descriptor = Object.getOwnPropertyDescriptor(nodeProps, propKey); - if (descriptor && descriptor.get) { - return false; - } - const nodePropValue = nodeProps[propKey]; - - if (nodePropValue === undefined) { - return false; - } - - if (propValue) { - return nodePropValue === propValue; - } - - return Object.prototype.hasOwnProperty.call(nodeProps, propKey); -} +export { nodeHasProperty }; export function nodeMatchesObjectProps(node, props) { return isSubset(propsOfNode(node), props); diff --git a/src/ShallowWrapper.js b/src/ShallowWrapper.js index e1551b0d0..13e49d95d 100644 --- a/src/ShallowWrapper.js +++ b/src/ShallowWrapper.js @@ -15,6 +15,7 @@ import { isReactElementAlike, displayNameOfNode, isFunctionalComponent, + isCustomComponentElement, } from './Utils'; import { debugNodes, @@ -31,6 +32,7 @@ import { createShallowRenderer, renderToStaticMarkup, batchedUpdates, + isDOMComponentElement, } from './react-compat'; /** @@ -376,14 +378,7 @@ export default class ShallowWrapper { * @returns {Boolean} */ containsAnyMatchingElements(nodes) { - if (!Array.isArray(nodes)) return false; - if (nodes.length <= 0) return false; - for (let i = 0; i < nodes.length; i++) { - if (this.containsMatchingElement(nodes[i])) { - return true; - } - } - return false; + return Array.isArray(nodes) && nodes.some(node => this.containsMatchingElement(node)); } /** @@ -512,11 +507,7 @@ export default class ShallowWrapper { * @returns {String} */ html() { - return this.single('html', n => { - // NOTE: splitting this into two statements is required to make the linter happy. - const isNull = this.type() === null; - return isNull ? null : renderToStaticMarkup(n); - }); + return this.single('html', n => (this.type() === null ? null : renderToStaticMarkup(n))); } /** @@ -709,7 +700,7 @@ export default class ShallowWrapper { /** * 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 native DOM node, it will be a string. + * be the component constructor. If it's a native DOM node, it will be a string. * * @returns {String|Function} */ @@ -970,4 +961,24 @@ export default class ShallowWrapper { 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 options object + * @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 new ShallowWrapper(n, null, options); + }); + } } diff --git a/src/Utils.js b/src/Utils.js index 473a432e3..606d52af8 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -22,7 +22,11 @@ export function internalInstance(inst) { } export function isFunctionalComponent(inst) { - return inst && inst.constructor && inst.constructor.name === 'StatelessComponent'; + return !!inst && !!inst.constructor && inst.constructor.name === 'StatelessComponent'; +} + +export function isCustomComponentElement(inst) { + return !!inst && React.isValidElement(inst) && typeof inst.type === 'function'; } export function propsOfNode(node) { @@ -67,7 +71,7 @@ export function childrenEqual(a, b, lenComp) { if (!a && !b) return true; if (a.length !== b.length) return false; if (a.length === 0 && b.length === 0) return true; - for (let i = 0; i < a.length; i++) { + for (let i = 0; i < a.length; i += 1) { if (!nodeEqual(a[i], b[i], lenComp)) return false; } return true; @@ -80,7 +84,7 @@ export function nodeEqual(a, b, lenComp = is) { const left = propsOfNode(a); const leftKeys = Object.keys(left); const right = propsOfNode(b); - for (let i = 0; i < leftKeys.length; i++) { + for (let i = 0; i < leftKeys.length; i += 1) { const prop = leftKeys[i]; // we will check children later if (prop === 'children') { @@ -186,14 +190,27 @@ export function splitSelector(selector) { return selector.split(/(?=\.|\[.*\])|(?=#|\[#.*\])/); } -export function isSimpleSelector(selector) { - // any of these characters pretty much guarantee it's a complex selector - return !/[~\s:>]/.test(selector); + +const containsQuotes = /'|"/; +const containsColon = /:/; + + +export function isPseudoClassSelector(selector) { + if (containsColon.test(selector)) { + if (!containsQuotes.test(selector)) { + return true; + } + const tokens = selector.split(containsQuotes); + return tokens.some((token, i) => + containsColon.test(token) && i % 2 === 0 + ); + } + return false; } -export function selectorError(selector) { +export function selectorError(selector, type = '') { return new TypeError( - `Enzyme received a complex CSS selector ('${selector}') that it does not currently support` + `Enzyme received a ${type} CSS selector ('${selector}') that it does not currently support` ); } @@ -208,6 +225,9 @@ export const SELECTOR = { }; export function selectorType(selector) { + if (isPseudoClassSelector(selector)) { + throw selectorError(selector, 'pseudo-class'); + } if (selector[0] === '.') { return SELECTOR.CLASS_TYPE; } else if (selector[0] === '#') { @@ -219,13 +239,8 @@ export function selectorType(selector) { } export function AND(fns) { - return x => { - let i = fns.length; - while (i--) { - if (!fns[i](x)) return false; - } - return true; - }; + const fnsReversed = fns.slice().reverse(); + return x => fnsReversed.every(fn => fn(x)); } export function coercePropValue(propName, propValue) { @@ -234,6 +249,19 @@ export function coercePropValue(propName, propValue) { return propValue; } + // can be the empty string + if (propValue === '') { + return propValue; + } + + if (propValue === 'NaN') { + return NaN; + } + + if (propValue === 'null') { + return null; + } + const trimmedValue = propValue.trim(); // if propValue includes quotes, it should be @@ -245,7 +273,7 @@ export function coercePropValue(propName, propValue) { const numericPropValue = +trimmedValue; // if parseInt is not NaN, then we've wanted a number - if (!isNaN(numericPropValue)) { + if (!is(NaN, numericPropValue)) { return numericPropValue; } @@ -260,6 +288,27 @@ export function coercePropValue(propName, propValue) { ); } +export function nodeHasProperty(node, propKey, stringifiedPropValue) { + const nodeProps = propsOfNode(node); + const descriptor = Object.getOwnPropertyDescriptor(nodeProps, propKey); + if (descriptor && descriptor.get) { + return false; + } + const nodePropValue = nodeProps[propKey]; + + const propValue = coercePropValue(propKey, stringifiedPropValue); + + if (nodePropValue === undefined) { + return false; + } + + if (propValue !== undefined) { + return is(nodePropValue, propValue); + } + + return Object.prototype.hasOwnProperty.call(nodeProps, propKey); +} + export function mapNativeEventNames(event) { const nativeToReactEventMap = { compositionend: 'compositionEnd', diff --git a/src/react-compat.js b/src/react-compat.js index a35e0dc63..56cbaae36 100644 --- a/src/react-compat.js +++ b/src/react-compat.js @@ -150,6 +150,10 @@ if (REACT013) { }; } +function isDOMComponentElement(inst) { + return React.isValidElement(inst) && typeof inst.type === 'string'; +} + const { mockComponent, isElement, @@ -170,6 +174,7 @@ export { isElement, isElementOfType, isDOMComponent, + isDOMComponentElement, isCompositeComponent, isCompositeComponentWithType, isCompositeComponentElement, diff --git a/test/.eslintrc b/test/.eslintrc index 916f89a9f..890a258d6 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -11,5 +11,7 @@ "import/no-extraneous-dependencies": [2, { "devDependencies": true }], + "jsx-a11y/no-static-element-interactions": 0, + "jsx-a11y/anchor-has-content": 0 } } diff --git a/test/Debug-spec.jsx b/test/Debug-spec.jsx index 93fea1fc8..4f6aed922 100644 --- a/test/Debug-spec.jsx +++ b/test/Debug-spec.jsx @@ -417,7 +417,7 @@ describe('debug', () => { }); it('renders passed children properly', () => { - const Foo = (props) => ( + const Foo = props => (
From Foo {props.children} diff --git a/test/ReactWrapper-spec.jsx b/test/ReactWrapper-spec.jsx index 503eee358..914509d4c 100644 --- a/test/ReactWrapper-spec.jsx +++ b/test/ReactWrapper-spec.jsx @@ -16,10 +16,9 @@ import { render, ReactWrapper, } from '../src'; -import { REACT013, REACT15 } from '../src/version'; +import { REACT013, REACT014, REACT15 } from '../src/version'; describeWithDOM('mount', () => { - describe('context', () => { it('can pass in context', () => { const SimpleComponent = React.createClass({ @@ -140,7 +139,6 @@ describeWithDOM('mount', () => { expect(wrapper.context('name')).to.equal(context.name); }); - it('works with stateless components', () => { const Foo = ({ foo }) => (
@@ -207,7 +205,6 @@ describeWithDOM('mount', () => { }); describe('.contains(node)', () => { - it('should allow matches on the root node', () => { const a =
; const b =
; @@ -267,7 +264,6 @@ describeWithDOM('mount', () => { }); describeIf(!REACT013, 'stateless components', () => { - it('should match composite components', () => { const Foo = () =>
; const wrapper = mount( @@ -282,7 +278,6 @@ describeWithDOM('mount', () => { }); describe('.find(selector)', () => { - it('should find an element based on a class name', () => { const wrapper = mount(
@@ -384,19 +379,22 @@ describeWithDOM('mount', () => { }); - it('should not find components with invalid attributes', () => { - // Invalid attributes aren't valid JSX, so manual instantiation is necessary - const wrapper = mount( - React.createElement('div', null, React.createElement('span', { - '123-foo': 'bar', - '-foo': 'bar', - ':foo': 'bar', - })) - ); + // React 15.2 warns when setting a non valid prop to an DOM element + describeIf(REACT013 || REACT014, 'unauthorized dom props', () => { + it('should not find components with invalid attributes', () => { + // Invalid attributes aren't valid JSX, so manual instantiation is necessary + const wrapper = mount( + React.createElement('div', null, React.createElement('span', { + '123-foo': 'bar', + '-foo': 'bar', + '+foo': 'bar', + })) + ); - expect(wrapper.find('[-foo]')).to.have.length(0, '-foo'); - expect(wrapper.find('[:foo]')).to.have.length(0, ':foo'); - expect(wrapper.find('[123-foo]')).to.have.length(0, '123-foo'); + expect(wrapper.find('[-foo]')).to.have.length(0, '-foo'); + expect(wrapper.find('[+foo]')).to.have.length(0, '+foo'); + expect(wrapper.find('[123-foo]')).to.have.length(0, '123-foo'); + }); }); it('should support data prop selectors', () => { @@ -519,40 +517,45 @@ describeWithDOM('mount', () => {
-