From 8177e260ea707fdbeaf9af64b03b12340bfb6951 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 12:38:56 -0500 Subject: [PATCH 01/25] Implement new selector parsing module --- src/selectors.js | 207 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/selectors.js diff --git a/src/selectors.js b/src/selectors.js new file mode 100644 index 000000000..708bdd6be --- /dev/null +++ b/src/selectors.js @@ -0,0 +1,207 @@ +import { createParser } from 'scalpel'; +import isEmpty from 'lodash/isEmpty'; +import unique from 'lodash/uniq'; +import { + treeFilter, + nodeHasId, + findParentNode, + nodeMatchesObjectProps, + childrenOfNode, + hasClassName, +} from './RSTTraversal'; +import { nodeHasType, nodeHasProperty } from './Utils'; +// our CSS selector parser instance +const parser = createParser(); + +// Combinators that allow you to chance selectors +const CHILD = 'childCombinator'; +const ADJACENT_SIBLING = 'adjacentSiblingCombinator'; +const GENERAL_SIBLING = 'generalSiblingCombinator'; +const DESCENDANT = 'descendantCombinator'; + +// Selectors for targeting elements +const SELECTOR = 'selector'; +const TYPE_SELECTOR = 'typeSelector'; +const CLASS_SELECTOR = 'classSelector'; +const ID_SELECTOR = 'idSelector'; +const ATTRIBUTE_PRESENCE = 'attributePresenceSelector'; +const ATTRIBUTE_VALUE = 'attributeValueSelector'; +// @TODOD we dont support these, throw if they are used +const PSEUDO_CLASS = 'pseudoClassSelector'; +const PSEUDO_ELEMENT = 'pseudoElementSelector'; + +// psuedo class types +const PSUEDO_CLASS_NOT = 'not'; + +function safelyGeneratorTokens(selector) { + try { + return parser.parse(selector); + } catch (err) { + console.log(err); + throw new Error(`Failed to parse selector: ${selector}`); + } +} + +function nodeMatchesToken(node, token) { + if (node === null || typeof node === 'string') { + return false; + } + switch (token.type) { + // Parse .class queries + case CLASS_SELECTOR: + return hasClassName(node, token.name); + case TYPE_SELECTOR: + return nodeHasType(node, token.name); + case ID_SELECTOR: + return nodeHasId(node, token.name); + case ATTRIBUTE_VALUE: + return nodeHasProperty(node, token.name, token.value); + case ATTRIBUTE_PRESENCE: + return nodeHasProperty(node, token.name); + default: + throw new Error(`Unknown token type: ${token.type}`); + } +} + +function buildPredicateFromToken(token) { + return node => token.body.every( + bodyToken => nodeMatchesToken(node, bodyToken), + ); +} + +/** + * Returns whether a parsed selector is a complex selector, which + * is defined as a selector that contains combinators. + * @param {Array} tokens + */ +function isComplexSelector(tokens) { + return tokens.some(token => token.type !== SELECTOR); +} + + +export function buildPredicate(selector) { + switch (typeof selector) { + case 'function': + // constructor + return node => node && node.type === selector; + case 'object': + if (!Array.isArray(selector) && selector !== null && !isEmpty(selector)) { + return node => nodeMatchesObjectProps(node, selector); + } + throw new TypeError( + 'Enzyme::Selector does not support an array, null, or empty object as a selector', + ); + case 'string': { + const tokens = safelyGeneratorTokens(selector); + if (isComplexSelector(tokens)) { + // @TODO throw a helpful error. + } + // Simple selectors only have a single selector token + return buildPredicateFromToken(tokens[0]); + } + default: + throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor'); + } +} + +export function reduceTreeBySelector(selector, wrapper) { + const root = wrapper.getNodeInternal(); + let results = []; + switch (typeof selector) { + case 'function': + case 'object': + results = treeFilter(root, buildPredicate(selector)); + break; + case 'string': { + const tokens = safelyGeneratorTokens(selector); + let index = 0; + // let next = null; + let token = null; + while (index < tokens.length) { + token = tokens[index]; + if (token.type === SELECTOR) { + const predicate = buildPredicateFromToken(token); + results = results.concat(treeFilter(root, predicate)); + // eslint-disable-next-line no-loop-func + } else { + // Combinator tokens dictate the "direction" we should + // parse from the previously matched tokens. We can assume + // There always all previously matched tokens since selectors + // cannot start with combinators. + const type = token.type; + // We assume the next token is a selector, so move the index + // forward and build the predicate. + index += 1; + token = tokens[index]; + const predicate = buildPredicateFromToken(token); + // We match against only the nodes which have already been matched, + // since a combinator is meant to refine a previous selector. + switch (type) { + case ADJACENT_SIBLING: { + results = results.reduce((matches, node) => { + const parent = findParentNode(root, node); + // If there's no parent, there's no siblings + if (!parent) { + return matches; + } + const nodeIndex = parent.rendered.indexOf(node); + const adjacentSibling = parent.rendered[nodeIndex + 1]; + // No sibling + if (!adjacentSibling) { + return matches; + } + if (predicate(adjacentSibling)) { + matches.push(adjacentSibling); + } + return matches; + }, []); + break; + } + case DESCENDANT: { + const matched = results.reduce( + (matches, node) => matches.concat(treeFilter(node, predicate)), + [], + ); + results = unique(matched); + break; + } + case CHILD: { + const matched = results.reduce((matches, node) => { + const children = childrenOfNode(node); + children.forEach((child) => { + if (predicate(child)) { + matches.push(child); + } + }); + return matches; + }, []); + results = unique(matched); + break; + } + case GENERAL_SIBLING: { + const matched = results.reduce((matches, node) => { + const parent = findParentNode(root, node); + const nodeIndex = parent.rendered.indexOf(node); + parent.rendered.forEach((sibling, i) => { + if (i > nodeIndex && predicate(sibling)) { + matches.push(sibling); + } + }); + return matches; + }, []); + results = unique(matched); + break; + } + default: + throw new Error(`Unkown combinator selector: ${type}`); + } + } + index += 1; + } + break; + } + default: + throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor'); + } + return wrapper.wrap(results); +} From 0d4a7eb149e4bb5ece627781c4dc99d9dc8fa94e Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 12:39:20 -0500 Subject: [PATCH 02/25] Use new selector API in mount and shallow wrappers --- packages/enzyme/src/ReactWrapper.js | 12 ++++-------- packages/enzyme/src/ShallowWrapper.js | 10 ++-------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/enzyme/src/ReactWrapper.js b/packages/enzyme/src/ReactWrapper.js index b4fd2fb53..121e49232 100644 --- a/packages/enzyme/src/ReactWrapper.js +++ b/packages/enzyme/src/ReactWrapper.js @@ -3,7 +3,7 @@ import flatten from 'lodash/flatten'; import unique from 'lodash/uniq'; import compact from 'lodash/compact'; -import ComplexSelector from './ComplexSelector'; +import createWrapperComponent from './ReactWrapperComponent'; import { containsChildrenSubArray, typeOfNode, @@ -25,9 +25,10 @@ import { childrenOfNode, parentsOfNode, treeFilter, - buildPredicate, } from './RSTTraversal'; +import { buildPredicate, reduceTreeBySelector } from './selectors'; + const noop = () => {}; const NODE = sym('__node__'); @@ -101,11 +102,6 @@ class ReactWrapper { this.length = this[NODES].length; } privateSet(this, OPTIONS, root ? root[OPTIONS] : options); - privateSet(this, COMPLEX_SELECTOR, new ComplexSelector( - buildPredicate, - findWhereUnwrapped, - childrenOfNode, - )); } /** @@ -464,7 +460,7 @@ class ReactWrapper { * @returns {ReactWrapper} */ find(selector) { - return this[COMPLEX_SELECTOR].find(selector, this); + return reduceTreeBySelector(selector, this); } /** diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js index 3d89215af..85df05ef3 100644 --- a/packages/enzyme/src/ShallowWrapper.js +++ b/packages/enzyme/src/ShallowWrapper.js @@ -3,7 +3,6 @@ import unique from 'lodash/uniq'; import compact from 'lodash/compact'; import cheerio from 'cheerio'; -import ComplexSelector from './ComplexSelector'; import { nodeEqual, nodeMatches, @@ -29,8 +28,8 @@ import { childrenOfNode, parentsOfNode, treeFilter, - buildPredicate, } from './RSTTraversal'; +import { buildPredicate, reduceTreeBySelector } from './selectors'; const NODE = sym('__node__'); const NODES = sym('__nodes__'); @@ -145,11 +144,6 @@ class ShallowWrapper { this.length = this[NODES].length; } privateSet(this, OPTIONS, root ? root[OPTIONS] : options); - privateSet(this, COMPLEX_SELECTOR, new ComplexSelector( - buildPredicate, - findWhereUnwrapped, - childrenOfNode, - )); } getNodeInternal() { @@ -541,7 +535,7 @@ class ShallowWrapper { * @returns {ShallowWrapper} */ find(selector) { - return this[COMPLEX_SELECTOR].find(selector, this); + return reduceTreeBySelector(selector, this); } /** From 51ff0e7046dd7fe527059e81d673e82aa7339dcf Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 12:39:45 -0500 Subject: [PATCH 03/25] Implement findParentNode utility --- packages/enzyme/src/RSTTraversal.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/enzyme/src/RSTTraversal.js b/packages/enzyme/src/RSTTraversal.js index 730be555c..ad693935c 100644 --- a/packages/enzyme/src/RSTTraversal.js +++ b/packages/enzyme/src/RSTTraversal.js @@ -44,6 +44,31 @@ export function treeFilter(tree, fn) { return results; } +/** + * To support sibling selectors we need to be able to find + * the siblings of a node. The easiest way to do that is find + * the parent of the node and access its children. + * + * This would be unneeded if the RST spec included sibling pointers + * such as node.nextSibling and node.prevSibling + * @param {*} root + * @param {*} targetNode + */ +export function findParentNode(root, targetNode) { + const results = treeFilter( + root, + (node) => { + if (!node.rendered) { + return false; + } + return Array.isArray(node.rendered) + ? node.rendered.indexOf(targetNode) !== -1 + : node.rendered === targetNode; + }, + ); + return results[0] || null; +} + function pathFilter(path, fn) { return path.filter(tree => treeFilter(tree, fn).length !== 0); } From 26bf2002fd3c2a3f30c4e52f6eef3c76f53cdd62 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 12:40:23 -0500 Subject: [PATCH 04/25] Rename selector tests file --- test/selector-spec.jsx | 197 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 test/selector-spec.jsx diff --git a/test/selector-spec.jsx b/test/selector-spec.jsx new file mode 100644 index 000000000..78b2f7f15 --- /dev/null +++ b/test/selector-spec.jsx @@ -0,0 +1,197 @@ +import './_helpers/setupAdapters'; +import React from 'react'; +import { expect } from 'chai'; + +import { describeWithDOM } from './_helpers'; +import { + mount, + shallow, +} from '../src/'; + +const tests = [ + { + name: 'mount', + renderMethod: mount, + describeMethod: describeWithDOM, + }, + { + name: 'shallow', + renderMethod: shallow, + describeMethod: describe, + }, +]; + +describe('ComplexSelector', () => { + tests.forEach(({ describeMethod, name, renderMethod }) => { + describeMethod(name, () => { + it('simple descendent', () => { + const wrapper = renderMethod( +
+
+ inside top div +
+
+
div inside top div
+
+
+ +
, + ); + + expect(wrapper.find('span').length).to.equal(2); + expect(wrapper.find('.top-div span').length).to.equal(1); + expect(wrapper.find('.top-div span').text()).to.equal('inside top div'); + }); + + it('nested descendent', () => { + const wrapper = renderMethod( +
+
+

first

+
+
+

second

+
+
+
+

third

+
, + ); + + expect(wrapper.find('h1').length).to.equal(3); + expect(wrapper.find('.my-div h1').length).to.equal(2); + }); + + it('deep descendent', () => { + const wrapper = renderMethod( +
+
+
+ +
+

heading

+
+
+
+
+

heading

+
, + ); + + expect(wrapper.find('h1').length).to.equal(2); + expect(wrapper.find('div .inner span .way-inner h1').length).to.equal(1); + expect(wrapper.find('div .inner span .way-inner h1').text()).to.equal('heading'); + }); + + it('direct descendent', () => { + const wrapper = renderMethod( +
+
+
Direct
+
+
Nested
+
+
+
Outside
+
, + ); + + expect(wrapper.find('.to-find').length).to.equal(3); + const descendent = wrapper.find('.container > .to-find'); + expect(descendent.length).to.equal(1); + expect(descendent.text()).to.equal('Direct'); + }); + + it('simple adjacent', () => { + const wrapper = renderMethod( +
+
+
Adjacent
+
Not Adjacent
+
, + ); + + expect(wrapper.find('.sibling').length).to.equal(2); + const toFind = wrapper.find('.to-find + .sibling'); + expect(toFind.length).to.equal(1); + expect(toFind.text()).to.equal('Adjacent'); + }); + + it('nested adjacent', () => { + const wrapper = renderMethod( +
+
+
Adjacent
+
+
Not Adjacent
+
+
+
Adjacent
+
+
Not Adjacent
+
+
, + ); + + expect(wrapper.find('.to-find').length).to.equal(3); + const toFind = wrapper.find('.to-find + .sibling'); + expect(toFind.length).to.equal(2); + toFind.map(found => expect(found.text()).to.equal('Adjacent')); + }); + + it('simple general siblings', () => { + const wrapper = renderMethod( +
+ + + + +
+ +
+
, + ); + + expect(wrapper.find('.to-find ~ span').length).to.equal(3); + }); + + it('nested general siblings', () => { + const wrapper = renderMethod( +
+ Top + + +
+
+ Top + + +
+
+
, + ); + + const spans = wrapper.find('span'); + const siblings = wrapper.find('span ~ span'); + expect(spans.length - 2).to.equal(siblings.length); + siblings.map(sibling => expect(sibling.text()).to.not.equal('Top')); + }); + + it('.foo + div > span', () => { + const wrapper = renderMethod( +
+
+
+ +
+
+ +
+
, + ); + + expect(wrapper.find('.foo + div > span').length).to.equal(1); + }); + }); + }); +}); From fa80c0e78f7597164b2cee11ff3ccff3849c1da5 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 12:40:51 -0500 Subject: [PATCH 05/25] Stop treating unquoted strings as invalid --- packages/enzyme/src/Utils.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/enzyme/src/Utils.js b/packages/enzyme/src/Utils.js index 12f36de51..2299ad8a5 100644 --- a/packages/enzyme/src/Utils.js +++ b/packages/enzyme/src/Utils.js @@ -308,14 +308,6 @@ export function coercePropValue(propName, propValue) { const trimmedValue = propValue.trim(); - // if propValue includes quotes, it should be - // treated as a string - // eslint override pending https://github.com/eslint/eslint/issues/7472 - // eslint-disable-next-line no-useless-escape - if (/^(['"]).*\1$/.test(trimmedValue)) { - return trimmedValue.slice(1, -1); - } - const numericPropValue = +trimmedValue; // if parseInt is not NaN, then we've wanted a number @@ -327,11 +319,7 @@ export function coercePropValue(propName, propValue) { if (trimmedValue === 'true') return true; if (trimmedValue === 'false') return false; - // user provided an unquoted string value - throw new TypeError( - `Enzyme::Unable to parse selector '[${propName}=${propValue}]'. ` + - `Perhaps you forgot to escape a string? Try '[${propName}="${trimmedValue}"]' instead.`, - ); + return trimmedValue; } export function nodeHasProperty(node, propKey, stringifiedPropValue) { From b18cfe498f8655b848e87ad36653cb074c9dcd6e Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 12:41:12 -0500 Subject: [PATCH 06/25] Update tests --- .../test/RSTTraversal-spec.jsx | 17 +- .../test/ReactWrapper-spec.jsx | 13 ++ .../test/ShallowWrapper-spec.jsx | 11 +- ...lexSelector-spec.jsx => selector-spec.jsx} | 0 packages/enzyme/package.json | 1 + packages/enzyme/src/ReactWrapper.js | 2 - {src => packages/enzyme/src}/selectors.js | 1 - test/selector-spec.jsx | 197 ------------------ 8 files changed, 22 insertions(+), 220 deletions(-) rename packages/enzyme-test-suite/test/{ComplexSelector-spec.jsx => selector-spec.jsx} (100%) rename {src => packages/enzyme/src}/selectors.js (99%) delete mode 100644 test/selector-spec.jsx diff --git a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx index 7248705ce..44bb859e0 100644 --- a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx +++ b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx @@ -82,13 +82,13 @@ describe('RSTTraversal', () => { const node = $(
); expect(nodeHasProperty(node, 'onChange')).to.equal(true); - expect(nodeHasProperty(node, 'title', '"foo"')).to.equal(true); + expect(nodeHasProperty(node, 'title', 'foo')).to.equal(true); }); it('should not match on html attributes', () => { const node = $(
); - expect(nodeHasProperty(node, 'for', '"foo"')).to.equal(false); + expect(nodeHasProperty(node, 'for', 'foo')).to.equal(false); }); it('should not find undefined properties', () => { @@ -112,8 +112,8 @@ describe('RSTTraversal', () => { it('should parse numbers as numeric literals', () => { expect(nodeHasProperty(
, 'foo', '2.3')).to.equal(true); expect(nodeHasProperty(
, 'foo', '2')).to.equal(true); - expect(() => nodeHasProperty(
, 'foo', '2abc')).to.throw(); - expect(() => nodeHasProperty(
, 'foo', 'abc2')).to.throw(); + expect(nodeHasProperty(
, 'foo', '2abc')).to.equal(false); + expect(nodeHasProperty(
, 'foo', 'abc2')).to.equal(false); expect(nodeHasProperty(
, 'foo', '-2')).to.equal(true); expect(nodeHasProperty(
, 'foo', '2e8')).to.equal(true); expect(nodeHasProperty(
, 'foo', 'Infinity')).to.equal(true); @@ -129,9 +129,7 @@ describe('RSTTraversal', () => { it('should work with empty strings', () => { expect(nodeHasProperty(
, 'foo', '')).to.equal(true); - expect(nodeHasProperty(
, 'foo', '""')).to.equal(true); expect(nodeHasProperty(
, 'foo', '')).to.equal(false); - expect(nodeHasProperty(
, 'foo', '""')).to.equal(false); }); it('should work with NaN', () => { @@ -155,13 +153,6 @@ describe('RSTTraversal', () => { expect(nodeHasProperty(
, 'foo', '-Infinity')).to.equal(true); expect(nodeHasProperty(
, 'foo', '-Infinity')).to.equal(false); }); - - it('should throw when un unquoted string is passed in', () => { - const node = $(
); - - expect(() => nodeHasProperty(node, 'title', 'foo')).to.throw(); - }); - }); describe('treeForEach', () => { diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index f2a2ebef6..cf07910d3 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -388,6 +388,7 @@ describeWithDOM('mount', () => { const wrapper = mount(
+
, ); expect(wrapper.find('input.foo').length).to.equal(1); @@ -439,6 +440,7 @@ describeWithDOM('mount', () => { const wrapper = mount(
+
, ); @@ -476,6 +478,17 @@ describeWithDOM('mount', () => { expect(wrapper.find('.row + .row')).to.have.lengthOf(1); }); + it('should treat unquoted attribute values as strings', () => { + const wrapper = mount( +
+ + + +
+ ); + expect(wrapper.find('[type=text]')).to.have.lengthOf(2); + }); + // 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', () => { diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index e518954cf..5d255dd58 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -561,18 +561,15 @@ describe('shallow', () => { expect(wrapper.find('.row + .row')).to.have.lengthOf(1); }); - it('should error sensibly if prop selector without quotes', () => { + it('should treat unquoted attribute values as strings', () => { const wrapper = shallow(
-
, - ); - - expect(() => wrapper.find('[type=text]')).to.throw( - TypeError, - 'Enzyme::Unable to parse selector \'[type=text]\'. Perhaps you forgot to escape a string? Try \'[type="text"]\' instead.', // eslint-disable-line max-len + +
); + expect(wrapper.find('[type=text]')).to.have.lengthOf(2); }); it('should compound tag and prop selector', () => { diff --git a/packages/enzyme-test-suite/test/ComplexSelector-spec.jsx b/packages/enzyme-test-suite/test/selector-spec.jsx similarity index 100% rename from packages/enzyme-test-suite/test/ComplexSelector-spec.jsx rename to packages/enzyme-test-suite/test/selector-spec.jsx diff --git a/packages/enzyme/package.json b/packages/enzyme/package.json index 7f7b444b3..20ba19cab 100644 --- a/packages/enzyme/package.json +++ b/packages/enzyme/package.json @@ -39,6 +39,7 @@ "object.assign": "^4.0.4", "object.entries": "^1.0.4", "raf": "^3.3.2", + "scalpel": "^2.1.0", "uuid": "^3.1.0" }, "devDependencies": { diff --git a/packages/enzyme/src/ReactWrapper.js b/packages/enzyme/src/ReactWrapper.js index 121e49232..818b44e33 100644 --- a/packages/enzyme/src/ReactWrapper.js +++ b/packages/enzyme/src/ReactWrapper.js @@ -3,7 +3,6 @@ import flatten from 'lodash/flatten'; import unique from 'lodash/uniq'; import compact from 'lodash/compact'; -import createWrapperComponent from './ReactWrapperComponent'; import { containsChildrenSubArray, typeOfNode, @@ -37,7 +36,6 @@ const RENDERER = sym('__renderer__'); const UNRENDERED = sym('__unrendered__'); const ROOT = sym('__root__'); const OPTIONS = sym('__options__'); -const COMPLEX_SELECTOR = sym('__complexSelector__'); /** * Finds all nodes in the current wrapper nodes' render trees that match the provided predicate diff --git a/src/selectors.js b/packages/enzyme/src/selectors.js similarity index 99% rename from src/selectors.js rename to packages/enzyme/src/selectors.js index 708bdd6be..3bdb9c13b 100644 --- a/src/selectors.js +++ b/packages/enzyme/src/selectors.js @@ -37,7 +37,6 @@ function safelyGeneratorTokens(selector) { try { return parser.parse(selector); } catch (err) { - console.log(err); throw new Error(`Failed to parse selector: ${selector}`); } } diff --git a/test/selector-spec.jsx b/test/selector-spec.jsx deleted file mode 100644 index 78b2f7f15..000000000 --- a/test/selector-spec.jsx +++ /dev/null @@ -1,197 +0,0 @@ -import './_helpers/setupAdapters'; -import React from 'react'; -import { expect } from 'chai'; - -import { describeWithDOM } from './_helpers'; -import { - mount, - shallow, -} from '../src/'; - -const tests = [ - { - name: 'mount', - renderMethod: mount, - describeMethod: describeWithDOM, - }, - { - name: 'shallow', - renderMethod: shallow, - describeMethod: describe, - }, -]; - -describe('ComplexSelector', () => { - tests.forEach(({ describeMethod, name, renderMethod }) => { - describeMethod(name, () => { - it('simple descendent', () => { - const wrapper = renderMethod( -
-
- inside top div -
-
-
div inside top div
-
-
- -
, - ); - - expect(wrapper.find('span').length).to.equal(2); - expect(wrapper.find('.top-div span').length).to.equal(1); - expect(wrapper.find('.top-div span').text()).to.equal('inside top div'); - }); - - it('nested descendent', () => { - const wrapper = renderMethod( -
-
-

first

-
-
-

second

-
-
-
-

third

-
, - ); - - expect(wrapper.find('h1').length).to.equal(3); - expect(wrapper.find('.my-div h1').length).to.equal(2); - }); - - it('deep descendent', () => { - const wrapper = renderMethod( -
-
-
- -
-

heading

-
-
-
-
-

heading

-
, - ); - - expect(wrapper.find('h1').length).to.equal(2); - expect(wrapper.find('div .inner span .way-inner h1').length).to.equal(1); - expect(wrapper.find('div .inner span .way-inner h1').text()).to.equal('heading'); - }); - - it('direct descendent', () => { - const wrapper = renderMethod( -
-
-
Direct
-
-
Nested
-
-
-
Outside
-
, - ); - - expect(wrapper.find('.to-find').length).to.equal(3); - const descendent = wrapper.find('.container > .to-find'); - expect(descendent.length).to.equal(1); - expect(descendent.text()).to.equal('Direct'); - }); - - it('simple adjacent', () => { - const wrapper = renderMethod( -
-
-
Adjacent
-
Not Adjacent
-
, - ); - - expect(wrapper.find('.sibling').length).to.equal(2); - const toFind = wrapper.find('.to-find + .sibling'); - expect(toFind.length).to.equal(1); - expect(toFind.text()).to.equal('Adjacent'); - }); - - it('nested adjacent', () => { - const wrapper = renderMethod( -
-
-
Adjacent
-
-
Not Adjacent
-
-
-
Adjacent
-
-
Not Adjacent
-
-
, - ); - - expect(wrapper.find('.to-find').length).to.equal(3); - const toFind = wrapper.find('.to-find + .sibling'); - expect(toFind.length).to.equal(2); - toFind.map(found => expect(found.text()).to.equal('Adjacent')); - }); - - it('simple general siblings', () => { - const wrapper = renderMethod( -
- - - - -
- -
-
, - ); - - expect(wrapper.find('.to-find ~ span').length).to.equal(3); - }); - - it('nested general siblings', () => { - const wrapper = renderMethod( -
- Top - - -
-
- Top - - -
-
-
, - ); - - const spans = wrapper.find('span'); - const siblings = wrapper.find('span ~ span'); - expect(spans.length - 2).to.equal(siblings.length); - siblings.map(sibling => expect(sibling.text()).to.not.equal('Top')); - }); - - it('.foo + div > span', () => { - const wrapper = renderMethod( -
-
-
- -
-
- -
-
, - ); - - expect(wrapper.find('.foo + div > span').length).to.equal(1); - }); - }); - }); -}); From 5dde9c683f57803763235fd751eb38abbf83ca29 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 13:27:37 -0500 Subject: [PATCH 07/25] Add more comments to selectors.js --- packages/enzyme/src/selectors.js | 128 +++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 32 deletions(-) diff --git a/packages/enzyme/src/selectors.js b/packages/enzyme/src/selectors.js index 3bdb9c13b..bd0ef1348 100644 --- a/packages/enzyme/src/selectors.js +++ b/packages/enzyme/src/selectors.js @@ -26,14 +26,17 @@ const CLASS_SELECTOR = 'classSelector'; const ID_SELECTOR = 'idSelector'; const ATTRIBUTE_PRESENCE = 'attributePresenceSelector'; const ATTRIBUTE_VALUE = 'attributeValueSelector'; -// @TODOD we dont support these, throw if they are used -const PSEUDO_CLASS = 'pseudoClassSelector'; -const PSEUDO_ELEMENT = 'pseudoElementSelector'; +// @TODO we dont support these, throw if they are used +// const PSEUDO_CLASS = 'pseudoClassSelector'; +// const PSEUDO_ELEMENT = 'pseudoElementSelector'; -// psuedo class types -const PSUEDO_CLASS_NOT = 'not'; -function safelyGeneratorTokens(selector) { +/** + * Takes a CSS selector and returns a set of tokens parsed + * by scalpel. + * @param {String} selector + */ +function safelyGenerateTokens(selector) { try { return parser.parse(selector); } catch (err) { @@ -41,27 +44,60 @@ function safelyGeneratorTokens(selector) { } } +/** + * Takes a node and a token and determines if the node + * matches the predicate defined by the token. + * @param {Node} node + * @param {Token} token + */ function nodeMatchesToken(node, token) { if (node === null || typeof node === 'string') { return false; } switch (token.type) { - // Parse .class queries + /** + * Match against the className prop + * @example '.active' matches
+ */ case CLASS_SELECTOR: return hasClassName(node, token.name); + /** + * Simple type matching + * @example 'div' matches
+ */ case TYPE_SELECTOR: return nodeHasType(node, token.name); + /** + * Match against the `id` prop + * @example '#nav' matches
, ); expect(wrapper.find('[type=text]')).to.have.lengthOf(2); }); diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index 5d255dd58..19faa6631 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -567,7 +567,7 @@ describe('shallow', () => { -
+
, ); expect(wrapper.find('[type=text]')).to.have.lengthOf(2); }); diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js index 85df05ef3..af1c3e068 100644 --- a/packages/enzyme/src/ShallowWrapper.js +++ b/packages/enzyme/src/ShallowWrapper.js @@ -37,7 +37,6 @@ const RENDERER = sym('__renderer__'); const UNRENDERED = sym('__unrendered__'); const ROOT = sym('__root__'); const OPTIONS = sym('__options__'); -const COMPLEX_SELECTOR = sym('__complexSelector__'); /** * Finds all nodes in the current wrapper nodes' render trees that match the provided predicate * function. diff --git a/packages/enzyme/src/selectors.js b/packages/enzyme/src/selectors.js index bd0ef1348..fab97a3c5 100644 --- a/packages/enzyme/src/selectors.js +++ b/packages/enzyme/src/selectors.js @@ -55,7 +55,7 @@ function nodeMatchesToken(node, token) { return false; } switch (token.type) { - /** + /** * Match against the className prop * @example '.active' matches
*/ @@ -79,7 +79,7 @@ function nodeMatchesToken(node, token) { * @example '[disabled]' matches */ case ATTRIBUTE_PRESENCE: - return nodeHasProperty(node, token.name); + return nodeHasProperty(node, token.name); /** * Matches if an attribute is present with the * provided value From d7abfbeb7f18b423b5a13909ac31deb18134e02d Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 13:44:45 -0500 Subject: [PATCH 09/25] Remove ComplexSelector --- packages/enzyme/src/ComplexSelector.js | 111 ------------------------- 1 file changed, 111 deletions(-) delete mode 100644 packages/enzyme/src/ComplexSelector.js diff --git a/packages/enzyme/src/ComplexSelector.js b/packages/enzyme/src/ComplexSelector.js deleted file mode 100644 index 9bd31e55f..000000000 --- a/packages/enzyme/src/ComplexSelector.js +++ /dev/null @@ -1,111 +0,0 @@ -import split from 'lodash/split'; - -export default class ComplexSelector { - constructor(buildPredicate, findWhereUnwrapped, childrenOfNode) { - this.buildPredicate = buildPredicate; - this.findWhereUnwrapped = findWhereUnwrapped; - this.childrenOfNode = childrenOfNode; - } - - 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(); - return list.concat(sel, temp); - } - - return list.concat(sel); - }, []); - } - - handleSelectors(selectors, wrapper) { - const recurseSelector = (offset, fn, pre) => { - const predicate = pre || this.buildPredicate(selectors[offset]); - const nextWrapper = this.findWhereUnwrapped(wrapper, predicate, fn); - const nextSelectors = selectors.slice(offset + 1); - return this.handleSelectors(nextSelectors, nextWrapper); - }; - - const buildSiblingPredicate = (first, second) => { - const firstPredicate = this.buildPredicate(first); - const secondPredicate = this.buildPredicate(second); - - return (child) => { - if (firstPredicate(child)) { - return sibling => secondPredicate(sibling); - } - - return false; - }; - }; - - let predicate; - let selectSiblings; - - if (selectors.length) { - switch (selectors[0]) { - case '>': - return recurseSelector(1, this.treeFilterDirect()); - case '+': - predicate = buildSiblingPredicate(selectors[1], selectors[2]); - selectSiblings = (children, pre, results, idx) => { - const adjacent = children[idx + 1]; - if (pre(adjacent)) { results.push(adjacent); } - }; - - return recurseSelector(2, this.treeFindSiblings(selectSiblings), predicate); - case '~': - predicate = buildSiblingPredicate(selectors[1], selectors[2]); - selectSiblings = (children, pre, results, idx) => - children.slice(idx + 1).map(child => - (pre(child) ? results.push(child) : null), - ); - - return recurseSelector(2, this.treeFindSiblings(selectSiblings), predicate); - default: - return recurseSelector(0); - } - } - - return wrapper; - } - - find(selector, wrapper) { - if (typeof selector === 'string') { - const selectors = this.getSelectors(selector); - - return this.handleSelectors(selectors, wrapper); - } - - const predicate = this.buildPredicate(selector); - return this.findWhereUnwrapped(wrapper, predicate); - } - - treeFilterDirect() { - return (tree, fn) => this.childrenOfNode(tree).filter(child => fn(child)); - } - - treeFindSiblings(selectSiblings) { - return (tree, fn) => { - const results = []; - const list = [this.childrenOfNode(tree)]; - - const traverseChildren = children => children.forEach((child, i) => { - const secondPredicate = fn(child); - - list.push(this.childrenOfNode(child)); - - if (secondPredicate) { - selectSiblings(children, secondPredicate, results, i); - } - }); - - while (list.length) { - traverseChildren(list.shift()); - } - - return results; - }; - } -} From 8bffb8a2ea59bceb47a6cd5e7a4479db83f5971c Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 13:55:38 -0500 Subject: [PATCH 10/25] Remove old predicate/selector parsing utilities --- .../test/RSTTraversal-spec.jsx | 37 --------- .../enzyme-test-suite/test/Utils-spec.jsx | 65 ---------------- packages/enzyme/src/RSTTraversal.js | 54 +------------ packages/enzyme/src/Utils.js | 77 ------------------- 4 files changed, 1 insertion(+), 232 deletions(-) diff --git a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx index 44bb859e0..8e8edb78f 100644 --- a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx +++ b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx @@ -2,9 +2,6 @@ import './_helpers/setupAdapters'; import React from 'react'; import sinon from 'sinon'; import { expect } from 'chai'; -import { - splitSelector, -} from 'enzyme/build/Utils'; import { elementToTree } from 'enzyme-adapter-utils'; import { hasClassName, @@ -13,7 +10,6 @@ import { treeFilter, pathToNode, getTextFromNode, - buildPredicate, } from 'enzyme/build/RSTTraversal'; import { describeIf } from './_helpers'; import { REACT013 } from './_helpers/version'; @@ -21,31 +17,6 @@ import { REACT013 } from './_helpers/version'; const $ = elementToTree; describe('RSTTraversal', () => { - describe('splitSelector', () => { - const fn = splitSelector; - it('splits multiple class names', () => { - expect(fn('.foo.bar')).to.eql(['.foo', '.bar']); - expect(fn('.foo.bar.baz')).to.eql(['.foo', '.bar', '.baz']); - }); - - it('splits tag names and class names', () => { - expect(fn('input.bar')).to.eql(['input', '.bar']); - expect(fn('div.bar.baz')).to.eql(['div', '.bar', '.baz']); - expect(fn('Foo.bar')).to.eql(['Foo', '.bar']); - }); - - it('splits tag names and attributes', () => { - expect(fn('input[type="text"]')).to.eql(['input', '[type="text"]']); - expect( - fn('div[title="title"][data-value="foo"]'), - ).to.eql(['div', '[title="title"]', '[data-value="foo"]']); - }); - - it('throws for malformed selectors', () => { - expect(() => fn('div[data-name="xyz"')).to.throw(/Enzyme::Selector received what appears to be a malformed string selector/); - }); - }); - describe('hasClassName', () => { it('should work for standalone classNames', () => { @@ -358,12 +329,4 @@ describe('RSTTraversal', () => { }); }); }); - - describe('buildPredicate', () => { - it('should throw expected error', () => { - const intSelector = 10; - const func = buildPredicate.bind(this, intSelector); - expect(func).to.throw(TypeError, 'Enzyme::Selector expects a string, object, or Component Constructor'); - }); - }); }); diff --git a/packages/enzyme-test-suite/test/Utils-spec.jsx b/packages/enzyme-test-suite/test/Utils-spec.jsx index ac58d381d..af0a4e03d 100644 --- a/packages/enzyme-test-suite/test/Utils-spec.jsx +++ b/packages/enzyme-test-suite/test/Utils-spec.jsx @@ -8,9 +8,6 @@ import { childrenToSimplifiedArray, nodeEqual, nodeMatches, - isPseudoClassSelector, - SELECTOR, - selectorType, displayNameOfNode, } from 'enzyme/build/Utils'; import { @@ -439,68 +436,6 @@ describe('Utils', () => { }); }); - - describe('isPseudoClassSelector', () => { - describe('prohibited selectors', () => { - function isNotPseudo(selector) { - it(selector, () => { - expect(isPseudoClassSelector(selector)).to.equal(false); - }); - } - isNotPseudo('.foo'); - isNotPseudo('div'); - isNotPseudo('.foo .bar'); - isNotPseudo('[hover]'); - isNotPseudo('[checked=""]'); - isNotPseudo('[checked=":checked"]'); - isNotPseudo('[checked=\':checked\']'); - isNotPseudo('.foo>.bar'); - isNotPseudo('.foo > .bar'); - isNotPseudo('.foo~.bar'); - isNotPseudo('#foo'); - }); - - describe('allowed selectors', () => { - function isPseudo(selector) { - it(selector, () => { - expect(isPseudoClassSelector(selector)).to.equal(true); - }); - } - isPseudo(':checked'); - isPseudo(':focus'); - isPseudo(':hover'); - isPseudo(':disabled'); - isPseudo(':any'); - isPseudo(':last-child'); - isPseudo(':nth-child(1)'); - isPseudo('div:checked'); - isPseudo('[data-foo=":hover"]:hover'); - }); - }); - - describe('selectorType', () => { - it('returns CLASS_TYPE for a prefixed .', () => { - const type = selectorType('.foo'); - - expect(type).to.be.equal(SELECTOR.CLASS_TYPE); - }); - - it('returns ID_TYPE for a prefixed #', () => { - const type = selectorType('#foo'); - - expect(type).to.be.equal(SELECTOR.ID_TYPE); - }); - - it('returns PROP_TYPE for []', () => { - function isProp(selector) { - expect(selectorType(selector)).to.be.equal(SELECTOR.PROP_TYPE); - } - - isProp('[foo]'); - isProp('[foo="bar"]'); - }); - }); - describe('coercePropValue', () => { const key = 'foo'; it('returns undefined if passed undefined', () => { diff --git a/packages/enzyme/src/RSTTraversal.js b/packages/enzyme/src/RSTTraversal.js index ad693935c..9b55cc434 100644 --- a/packages/enzyme/src/RSTTraversal.js +++ b/packages/enzyme/src/RSTTraversal.js @@ -1,16 +1,7 @@ -import isEmpty from 'lodash/isEmpty'; import flatten from 'lodash/flatten'; import isSubset from 'is-subset'; import functionName from 'function.prototype.name'; -import { - splitSelector, - isCompoundSelector, - selectorType, - AND, - SELECTOR, - nodeHasType, - nodeHasProperty, -} from './Utils'; +import { nodeHasProperty } from './Utils'; export function propsOfNode(node) { return (node && node.props) || {}; @@ -111,49 +102,6 @@ export function nodeMatchesObjectProps(node, props) { return isSubset(propsOfNode(node), props); } -export function buildPredicate(selector) { - switch (typeof selector) { - case 'function': - // selector is a component constructor - return node => node && node.type === selector; - - case 'string': - if (isCompoundSelector.test(selector)) { - return AND(splitSelector(selector).map(buildPredicate)); - } - - switch (selectorType(selector)) { - case SELECTOR.CLASS_TYPE: - return node => hasClassName(node, selector.slice(1)); - - case SELECTOR.ID_TYPE: - return node => nodeHasId(node, selector.slice(1)); - - case SELECTOR.PROP_TYPE: { - const propKey = selector.split(/\[([a-zA-Z][a-zA-Z_\d\-:]*?)(=|])/)[1]; - const propValue = selector.split(/=(.*?)]/)[1]; - - return node => nodeHasProperty(node, propKey, propValue); - } - default: - // selector is a string. match to DOM tag or constructor displayName - return node => nodeHasType(node, selector); - } - - case 'object': - if (!Array.isArray(selector) && selector !== null && !isEmpty(selector)) { - return node => nodeMatchesObjectProps(node, selector); - } - throw new TypeError( - 'Enzyme::Selector does not support an array, null, or empty object as a selector', - ); - - default: - throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor'); - } -} - - export function getTextFromNode(node) { if (node === null || node === undefined) { return ''; diff --git a/packages/enzyme/src/Utils.js b/packages/enzyme/src/Utils.js index 2299ad8a5..da544760f 100644 --- a/packages/enzyme/src/Utils.js +++ b/packages/enzyme/src/Utils.js @@ -1,7 +1,6 @@ /* eslint no-use-before-define:0 */ import isEqual from 'lodash/isEqual'; import is from 'object-is'; -import uuidv4 from 'uuid/v4'; import entries from 'object.entries'; import functionName from 'function.prototype.name'; import configuration from './configuration'; @@ -206,82 +205,6 @@ export function withSetStateAllowed(fn) { } } -export function splitSelector(selector) { - // step 1: make a map of all quoted strings with a uuid - const quotedSegments = selector.split(/[^" ]+|("[^"]*")|.*/g) - .filter(Boolean) - .reduce((obj, match) => ({ ...obj, [match]: uuidv4() }), {}); - - const splits = selector - // step 2: replace all quoted strings with the uuid, so we don't have to properly parse them - .replace(/[^" ]+|("[^"]*")|.*/g, x => quotedSegments[x] || x) - // step 3: split as best we can without a proper parser - .split(/(?=\.|\[.*])|(?=#|\[#.*])/) - // step 4: restore the quoted strings by swapping back the uuid's for the original segments - .map((selectorSegment) => { - let restoredSegment = selectorSegment; - entries(quotedSegments).forEach(([k, v]) => { - restoredSegment = restoredSegment.replace(v, k); - }); - return restoredSegment; - }); - - if (splits.length === 1 && splits[0] === selector) { - // splitSelector expects selector to be "splittable" - throw new TypeError('Enzyme::Selector received what appears to be a malformed string selector'); - } - - return splits; -} - - -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; -} - -function selectorError(selector, type = '') { - return new TypeError( - `Enzyme received a ${type} CSS selector ('${selector}') that it does not currently support`, - ); -} - -export const isCompoundSelector = /^[.#]?-?[_a-z]+[_a-z0-9-]*[.[#]/i; - -const isPropSelector = /^\[.*]$/; - -export const SELECTOR = { - CLASS_TYPE: 0, - ID_TYPE: 1, - PROP_TYPE: 2, -}; - -export function selectorType(selector) { - if (isPseudoClassSelector(selector)) { - throw selectorError(selector, 'pseudo-class'); - } - if (selector[0] === '.') { - return SELECTOR.CLASS_TYPE; - } else if (selector[0] === '#') { - return SELECTOR.ID_TYPE; - } else if (isPropSelector.test(selector)) { - return SELECTOR.PROP_TYPE; - } - return undefined; -} - export function AND(fns) { const fnsReversed = fns.slice().reverse(); return x => fnsReversed.every(fn => fn(x)); From 10019da8caa5b0f6b0c05cc6038c1263fd00143a Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 15:39:07 -0500 Subject: [PATCH 11/25] Remove invalid attribute test --- .../test/ReactWrapper-spec.jsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index e3e64564d..fca22aaf9 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -489,24 +489,6 @@ describeWithDOM('mount', () => { expect(wrapper.find('[type=text]')).to.have.lengthOf(2); }); - // 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'); - }); - }); - it('should support data prop selectors', () => { const wrapper = mount(
From 5efaf08b89d4757f32e7a914cd2775ae48162727 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 16:34:32 -0500 Subject: [PATCH 12/25] Throw an error for complex selectors in buildPredicate --- packages/enzyme-test-suite/test/selector-spec.jsx | 11 ++++++++++- packages/enzyme/src/selectors.js | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/enzyme-test-suite/test/selector-spec.jsx b/packages/enzyme-test-suite/test/selector-spec.jsx index 16fdad3d2..97419f87d 100644 --- a/packages/enzyme-test-suite/test/selector-spec.jsx +++ b/packages/enzyme-test-suite/test/selector-spec.jsx @@ -21,7 +21,7 @@ const tests = [ }, ]; -describe('ComplexSelector', () => { +describe('selectors', () => { tests.forEach(({ describeMethod, name, renderMethod }) => { describeMethod(name, () => { it('simple descendent', () => { @@ -173,6 +173,15 @@ describe('ComplexSelector', () => { siblings.map(sibling => expect(sibling.text()).to.not.equal('Top')); }); + it('throws for complex selectors in simple selector methods', () => { + const wrapper = renderMethod(
); + ['is', 'filter', 'not', 'every'].forEach((method) => { + expect(() => wrapper[method]('.foo + div')).to.throw( + `This method does not support complex CSS selectors` + ); + }); + }); + it('.foo + div > span', () => { const wrapper = renderMethod(
diff --git a/packages/enzyme/src/selectors.js b/packages/enzyme/src/selectors.js index fab97a3c5..5e969592a 100644 --- a/packages/enzyme/src/selectors.js +++ b/packages/enzyme/src/selectors.js @@ -135,7 +135,7 @@ export function buildPredicate(selector) { case 'string': { const tokens = safelyGenerateTokens(selector); if (isComplexSelector(tokens)) { - // @TODO throw a helpful error. + throw new TypeError('This method does not support complex CSS selectors'); } // Simple selectors only have a single selector token return buildPredicateFromToken(tokens[0]); From 83c6f04c514abc9c3964ca9d62198b3d63b00f45 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Mon, 21 Aug 2017 16:52:23 -0500 Subject: [PATCH 13/25] Move combinator logic into discrete utility functions --- .../enzyme-test-suite/test/selector-spec.jsx | 2 +- packages/enzyme/src/selectors.js | 143 ++++++++++++------ 2 files changed, 95 insertions(+), 50 deletions(-) diff --git a/packages/enzyme-test-suite/test/selector-spec.jsx b/packages/enzyme-test-suite/test/selector-spec.jsx index 97419f87d..873b53e3b 100644 --- a/packages/enzyme-test-suite/test/selector-spec.jsx +++ b/packages/enzyme-test-suite/test/selector-spec.jsx @@ -177,7 +177,7 @@ describe('selectors', () => { const wrapper = renderMethod(
); ['is', 'filter', 'not', 'every'].forEach((method) => { expect(() => wrapper[method]('.foo + div')).to.throw( - `This method does not support complex CSS selectors` + 'This method does not support complex CSS selectors', ); }); }); diff --git a/packages/enzyme/src/selectors.js b/packages/enzyme/src/selectors.js index 5e969592a..fb50b8150 100644 --- a/packages/enzyme/src/selectors.js +++ b/packages/enzyme/src/selectors.js @@ -30,6 +30,15 @@ const ATTRIBUTE_VALUE = 'attributeValueSelector'; // const PSEUDO_CLASS = 'pseudoClassSelector'; // const PSEUDO_ELEMENT = 'pseudoElementSelector'; +/** + * Calls reduce on a array of nodes with the passed + * function, returning only unique results. + * @param {Function} fn + * @param {Array} nodes + */ +function uniqueReduce(fn, nodes) { + return unique(nodes.reduce(fn, [])); +} /** * Takes a CSS selector and returns a set of tokens parsed @@ -145,6 +154,84 @@ export function buildPredicate(selector) { } } +/** + * Matches only nodes which are adjacent siblings (direct next sibling) + * against a predicate, returning those that match. + * @param {Array} nodes + * @param {Function} predicate + * @param {Node} root + */ +function matchAdjacentSiblings(nodes, predicate, root) { + return nodes.reduce((matches, node) => { + const parent = findParentNode(root, node); + // If there's no parent, there's no siblings + if (!parent) { + return matches; + } + const nodeIndex = parent.rendered.indexOf(node); + const adjacentSibling = parent.rendered[nodeIndex + 1]; + // No sibling + if (!adjacentSibling) { + return matches; + } + if (predicate(adjacentSibling)) { + matches.push(adjacentSibling); + } + return matches; + }, []); +} + +/** + * Matches only nodes which are general siblings (any sibling *after*) + * against a predicate, returning those that match. + * @param {Array} nodes + * @param {Function} predicate + * @param {Node} root + */ +function matchGeneralSibling(nodes, predicate, root) { + return uniqueReduce((matches, node) => { + const parent = findParentNode(root, node); + const nodeIndex = parent.rendered.indexOf(node); + parent.rendered.forEach((sibling, i) => { + if (i > nodeIndex && predicate(sibling)) { + matches.push(sibling); + } + }); + return matches; + }, nodes); +} + +/** + * Matches only nodes which are direct children (not grandchildren, etc.) + * against a predicate, returning those that match. + * @param {Array} nodes + * @param {Function} predicate + */ +function matchDirectChild(nodes, predicate) { + return uniqueReduce((matches, node) => { + const children = childrenOfNode(node); + children.forEach((child) => { + if (predicate(child)) { + matches.push(child); + } + }); + return matches; + }, nodes); +} + +/** + * Matches all descendant nodes against a predicate, + * returning those that match. + * @param {Array} nodes + * @param {Function} predicate + */ +function matchDescendant(nodes, predicate) { + return uniqueReduce( + (matches, node) => matches.concat(treeFilter(node, predicate)), + nodes, + ); +} + /** * Takes an RST and reduces it to a set of nodes matching * the selector. The selector can be a simple selector, which @@ -197,62 +284,20 @@ export function reduceTreeBySelector(selector, wrapper) { // since a combinator is meant to refine a previous selector. switch (type) { // The + combinator - case ADJACENT_SIBLING: { - results = results.reduce((matches, node) => { - const parent = findParentNode(root, node); - // If there's no parent, there's no siblings - if (!parent) { - return matches; - } - const nodeIndex = parent.rendered.indexOf(node); - const adjacentSibling = parent.rendered[nodeIndex + 1]; - // No sibling - if (!adjacentSibling) { - return matches; - } - if (predicate(adjacentSibling)) { - matches.push(adjacentSibling); - } - return matches; - }, []); + case ADJACENT_SIBLING: + results = matchAdjacentSiblings(results, predicate, root); break; - } // The ~ combinator - case GENERAL_SIBLING: { - const matched = results.reduce((matches, node) => { - const parent = findParentNode(root, node); - const nodeIndex = parent.rendered.indexOf(node); - parent.rendered.forEach((sibling, i) => { - if (i > nodeIndex && predicate(sibling)) { - matches.push(sibling); - } - }); - return matches; - }, []); - results = unique(matched); + case GENERAL_SIBLING: + results = matchGeneralSibling(results, predicate, root); break; - } // The > combinator - case CHILD: { - const matched = results.reduce((matches, node) => { - const children = childrenOfNode(node); - children.forEach((child) => { - if (predicate(child)) { - matches.push(child); - } - }); - return matches; - }, []); - results = unique(matched); + case CHILD: + results = matchDirectChild(results, predicate); break; - } // The ' ' (whitespace) combinator case DESCENDANT: { - const matched = results.reduce( - (matches, node) => matches.concat(treeFilter(node, predicate)), - [], - ); - results = unique(matched); + results = matchDescendant(results, predicate); break; } default: From 8b3e00807e6fb8b60b5c206eca615f92206f041a Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Tue, 22 Aug 2017 10:39:51 -0500 Subject: [PATCH 14/25] Replace scalpel with new rst-selector-parser package --- .../test/RSTTraversal-spec.jsx | 55 +++++++++---------- .../test/ReactWrapper-spec.jsx | 10 +++- .../test/ShallowWrapper-spec.jsx | 6 +- .../enzyme-test-suite/test/Utils-spec.jsx | 18 ------ packages/enzyme/package.json | 2 +- packages/enzyme/src/Utils.js | 39 +------------ packages/enzyme/src/selectors.js | 2 +- 7 files changed, 41 insertions(+), 91 deletions(-) diff --git a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx index 8e8edb78f..06a90afc9 100644 --- a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx +++ b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx @@ -68,34 +68,29 @@ describe('RSTTraversal', () => { expect(nodeHasProperty(node, 'title')).to.equal(false); }); - it('should parse false as a literal', () => { + it('should parse false', () => { const node = $(
); - expect(nodeHasProperty(node, 'foo', 'false')).to.equal(true); + expect(nodeHasProperty(node, 'foo', false)).to.equal(true); }); - it('should parse false as a literal', () => { - const node = $(
); - - expect(nodeHasProperty(node, 'foo', 'true')).to.equal(true); - }); - - it('should parse numbers as numeric literals', () => { - expect(nodeHasProperty(
, 'foo', '2.3')).to.equal(true); - expect(nodeHasProperty(
, 'foo', '2')).to.equal(true); + it('should parse numeric literals', () => { + expect(nodeHasProperty(
, 'foo', 2.3)).to.equal(true); + expect(nodeHasProperty(
, 'foo', 2)).to.equal(true); expect(nodeHasProperty(
, 'foo', '2abc')).to.equal(false); expect(nodeHasProperty(
, 'foo', 'abc2')).to.equal(false); - expect(nodeHasProperty(
, 'foo', '-2')).to.equal(true); - expect(nodeHasProperty(
, 'foo', '2e8')).to.equal(true); - expect(nodeHasProperty(
, 'foo', 'Infinity')).to.equal(true); - expect(nodeHasProperty(
, 'foo', '-Infinity')).to.equal(true); + expect(nodeHasProperty(
, 'foo', -2)).to.equal(true); + expect(nodeHasProperty(
, 'foo', 2e8)).to.equal(true); + expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(true); + expect(nodeHasProperty(
, 'foo', -Infinity)).to.equal(true); }); it('should parse zeroes properly', () => { - expect(nodeHasProperty(
, 'foo', '0')).to.equal(true); - expect(nodeHasProperty(
, 'foo', '-0')).to.equal(true); - expect(nodeHasProperty(
, 'foo', '0')).to.equal(false); - expect(nodeHasProperty(
, 'foo', '-0')).to.equal(false); + expect(nodeHasProperty(
, 'foo', 0)).to.equal(true); + expect(nodeHasProperty(
, 'foo', +0)).to.equal(true); + expect(nodeHasProperty(
, 'foo', -0)).to.equal(true); + expect(nodeHasProperty(
, 'foo', 0)).to.equal(false); + expect(nodeHasProperty(
, 'foo', -0)).to.equal(false); }); it('should work with empty strings', () => { @@ -104,25 +99,27 @@ describe('RSTTraversal', () => { }); it('should work with NaN', () => { - expect(nodeHasProperty(
, 'foo', 'NaN')).to.equal(true); - expect(nodeHasProperty(
, 'foo', 'NaN')).to.equal(false); + expect(nodeHasProperty(
, 'foo', NaN)).to.equal(true); + expect(nodeHasProperty(
, 'foo', NaN)).to.equal(false); }); it('should work with null', () => { - expect(nodeHasProperty(
, 'foo', 'null')).to.equal(true); - expect(nodeHasProperty(
, 'foo', 'null')).to.equal(false); + expect(nodeHasProperty(
, 'foo', null)).to.equal(true); + expect(nodeHasProperty(
, 'foo', null)).to.equal(false); }); it('should work with false', () => { - expect(nodeHasProperty(
, 'foo', 'false')).to.equal(true); - expect(nodeHasProperty(
, 'foo', 'false')).to.equal(false); + expect(nodeHasProperty(
, 'foo', false)).to.equal(true); + expect(nodeHasProperty(
, 'foo', false)).to.equal(false); }); it('should work with ±Infinity', () => { - expect(nodeHasProperty(
, 'foo', 'Infinity')).to.equal(true); - expect(nodeHasProperty(
, 'foo', 'Infinity')).to.equal(false); - expect(nodeHasProperty(
, 'foo', '-Infinity')).to.equal(true); - expect(nodeHasProperty(
, 'foo', '-Infinity')).to.equal(false); + expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(true); + expect(nodeHasProperty(
, 'foo', +Infinity)).to.equal(true); + expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(false); + expect(nodeHasProperty(
, 'foo', -Infinity)).to.equal(true); + expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(false); + expect(nodeHasProperty(
, 'foo', -Infinity)).to.equal(false); }); }); diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index fca22aaf9..1d63120a8 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -478,7 +478,7 @@ describeWithDOM('mount', () => { expect(wrapper.find('.row + .row')).to.have.lengthOf(1); }); - it('should treat unquoted attribute values as strings', () => { + it('should throw for non-numeric attribute values without quotes', () => { const wrapper = mount(
@@ -486,7 +486,9 @@ describeWithDOM('mount', () => {
, ); - expect(wrapper.find('[type=text]')).to.have.lengthOf(2); + expect(() => wrapper.find('[type=text]')).to.throw(); + expect(() => wrapper.find('[type=hidden]')).to.throw(); + expect(() => wrapper.find('[type="text"]')).to.not.throw(); }); it('should support data prop selectors', () => { @@ -539,6 +541,10 @@ describeWithDOM('mount', () => { , ); diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index 19faa6631..6ab900402 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -561,7 +561,7 @@ describe('shallow', () => { expect(wrapper.find('.row + .row')).to.have.lengthOf(1); }); - it('should treat unquoted attribute values as strings', () => { + it('should throw for non-numeric attribute values without quotes', () => { const wrapper = shallow(
@@ -569,7 +569,9 @@ describe('shallow', () => {
, ); - expect(wrapper.find('[type=text]')).to.have.lengthOf(2); + expect(() => wrapper.find('[type=text]')).to.throw(); + expect(() => wrapper.find('[type=hidden]')).to.throw(); + expect(() => wrapper.find('[type="text"]')).to.not.throw(); }); it('should compound tag and prop selector', () => { diff --git a/packages/enzyme-test-suite/test/Utils-spec.jsx b/packages/enzyme-test-suite/test/Utils-spec.jsx index af0a4e03d..25ebf2f22 100644 --- a/packages/enzyme-test-suite/test/Utils-spec.jsx +++ b/packages/enzyme-test-suite/test/Utils-spec.jsx @@ -4,7 +4,6 @@ import { expect } from 'chai'; import { describeIf } from './_helpers'; import { - coercePropValue, childrenToSimplifiedArray, nodeEqual, nodeMatches, @@ -436,23 +435,6 @@ describe('Utils', () => { }); }); - describe('coercePropValue', () => { - const key = 'foo'; - it('returns undefined if passed undefined', () => { - expect(coercePropValue(key, undefined)).to.equal(undefined); - }); - - it('returns number if passed a stringified number', () => { - expect(coercePropValue(key, '1')).to.be.equal(1); - expect(coercePropValue(key, '0')).to.be.equal(0); - }); - - it('returns a boolean if passed a stringified bool', () => { - expect(coercePropValue(key, 'true')).to.equal(true); - expect(coercePropValue(key, 'false')).to.equal(false); - }); - }); - describe('mapNativeEventNames', () => { describe('given an event that isn\'t a mapped', () => { it('returns the original event', () => { diff --git a/packages/enzyme/package.json b/packages/enzyme/package.json index 20ba19cab..21c665c1a 100644 --- a/packages/enzyme/package.json +++ b/packages/enzyme/package.json @@ -39,7 +39,7 @@ "object.assign": "^4.0.4", "object.entries": "^1.0.4", "raf": "^3.3.2", - "scalpel": "^2.1.0", + "rst-selector-parser": "^2.2.0", "uuid": "^3.1.0" }, "devDependencies": { diff --git a/packages/enzyme/src/Utils.js b/packages/enzyme/src/Utils.js index da544760f..84c9de011 100644 --- a/packages/enzyme/src/Utils.js +++ b/packages/enzyme/src/Utils.js @@ -210,42 +210,7 @@ export function AND(fns) { return x => fnsReversed.every(fn => fn(x)); } -export function coercePropValue(propName, propValue) { - // can be undefined - if (propValue === undefined) { - 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(); - - const numericPropValue = +trimmedValue; - - // if parseInt is not NaN, then we've wanted a number - if (!is(NaN, numericPropValue)) { - return numericPropValue; - } - - // coerce to boolean - if (trimmedValue === 'true') return true; - if (trimmedValue === 'false') return false; - - return trimmedValue; -} - -export function nodeHasProperty(node, propKey, stringifiedPropValue) { +export function nodeHasProperty(node, propKey, propValue) { const nodeProps = propsOfNode(node); const descriptor = Object.getOwnPropertyDescriptor(nodeProps, propKey); if (descriptor && descriptor.get) { @@ -253,8 +218,6 @@ export function nodeHasProperty(node, propKey, stringifiedPropValue) { } const nodePropValue = nodeProps[propKey]; - const propValue = coercePropValue(propKey, stringifiedPropValue); - if (nodePropValue === undefined) { return false; } diff --git a/packages/enzyme/src/selectors.js b/packages/enzyme/src/selectors.js index fb50b8150..5af273070 100644 --- a/packages/enzyme/src/selectors.js +++ b/packages/enzyme/src/selectors.js @@ -1,4 +1,4 @@ -import { createParser } from 'scalpel'; +import { createParser } from 'rst-selector-parser'; import isEmpty from 'lodash/isEmpty'; import unique from 'lodash/uniq'; import { From 1a737246144bde03f9a279aff7b597ce81b269cf Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Wed, 23 Aug 2017 12:56:04 -0500 Subject: [PATCH 15/25] Throw for pseudo-class and psuedo-element selectors --- packages/enzyme-test-suite/test/selector-spec.jsx | 14 ++++++++++++++ packages/enzyme/src/selectors.js | 7 +++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/enzyme-test-suite/test/selector-spec.jsx b/packages/enzyme-test-suite/test/selector-spec.jsx index 873b53e3b..23f9d07f5 100644 --- a/packages/enzyme-test-suite/test/selector-spec.jsx +++ b/packages/enzyme-test-suite/test/selector-spec.jsx @@ -182,6 +182,20 @@ describe('selectors', () => { }); }); + it('throws for pseudo-element selectors', () => { + const wrapper = renderMethod(
); + expect(() => wrapper.find('div::after')).to.throw( + 'Enzyme::Selector does not support psuedo-element or psuedo-class selectors.', + ); + }); + + it('throws for pseudo-class selectors', () => { + const wrapper = renderMethod(
); + expect(() => wrapper.find('div:hover')).to.throw( + 'Enzyme::Selector does not support psuedo-element or psuedo-class selectors.', + ); + }); + it('.foo + div > span', () => { const wrapper = renderMethod(
diff --git a/packages/enzyme/src/selectors.js b/packages/enzyme/src/selectors.js index 5af273070..4cadf0f92 100644 --- a/packages/enzyme/src/selectors.js +++ b/packages/enzyme/src/selectors.js @@ -27,8 +27,8 @@ const ID_SELECTOR = 'idSelector'; const ATTRIBUTE_PRESENCE = 'attributePresenceSelector'; const ATTRIBUTE_VALUE = 'attributeValueSelector'; // @TODO we dont support these, throw if they are used -// const PSEUDO_CLASS = 'pseudoClassSelector'; -// const PSEUDO_ELEMENT = 'pseudoElementSelector'; +const PSEUDO_CLASS = 'pseudoClassSelector'; +const PSEUDO_ELEMENT = 'pseudoElementSelector'; /** * Calls reduce on a array of nodes with the passed @@ -96,6 +96,9 @@ function nodeMatchesToken(node, token) { */ case ATTRIBUTE_VALUE: return nodeHasProperty(node, token.name, token.value); + case PSEUDO_ELEMENT: + case PSEUDO_CLASS: + throw new Error('Enzyme::Selector does not support psuedo-element or psuedo-class selectors.'); default: throw new Error(`Unknown token type: ${token.type}`); } From 36e10d24e1c1d2aa223a6c0c51865cd38f8b23f5 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Wed, 23 Aug 2017 13:34:33 -0500 Subject: [PATCH 16/25] Add more tests for selectors --- .../enzyme-test-suite/test/selector-spec.jsx | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/packages/enzyme-test-suite/test/selector-spec.jsx b/packages/enzyme-test-suite/test/selector-spec.jsx index 23f9d07f5..5e7ddcef8 100644 --- a/packages/enzyme-test-suite/test/selector-spec.jsx +++ b/packages/enzyme-test-suite/test/selector-spec.jsx @@ -211,6 +211,102 @@ describe('selectors', () => { expect(wrapper.find('.foo + div > span').length).to.equal(1); }); + + it('.foo + .foo + .foo', () => { + const wrapper = renderMethod( +
+
foo1
+
foo2
+
foo3
+
, + ); + expect(wrapper.find('.foo + .foo').length).to.equal(2); + expect(wrapper.find('.foo + .foo').at(0).text()).to.equal('foo2'); + expect(wrapper.find('.foo + .foo').at(1).text()).to.equal('foo3'); + expect(wrapper.find('.foo + .foo + .foo').length).to.equal(1); + }); + + it('attribute names with numbers', () => { + const wrapper = renderMethod( +
+
+
+
+
+
, + ); + expect(wrapper.find('[data-foo-1=1]').length).to.equal(2); + expect(wrapper.find('[data-foo-1="1"]').length).to.equal(0); + expect(wrapper.find('[data-foo-2=2]').length).to.equal(1); + expect(wrapper.find('[data-foo-2="2"]').length).to.equal(1); + }); + + it('hyphens', () => { + const wrapper = renderMethod( +
+
+
+
+ +
, + ); + expect(wrapper.find('.-foo').length).to.equal(3); + expect(wrapper.find('.foo-').length).to.equal(1); + expect(wrapper.find('[type="foo"].foo-').length).to.equal(1); + expect(wrapper.find('.foo-.-bar-').length).to.equal(1); + expect(wrapper.find('div.foo-').length).to.equal(1); + expect(wrapper.find('div.-foo').length).to.equal(2); + expect(wrapper.find('#bar.-foo').length).to.equal(1); + }); + + it('hyphens', () => { + const wrapper = renderMethod( +
+
+
+
+ +
, + ); + expect(wrapper.find('.-foo').length).to.equal(3); + expect(wrapper.find('.foo-').length).to.equal(1); + expect(wrapper.find('[type="foo"].foo-').length).to.equal(1); + expect(wrapper.find('.foo-.-bar-').length).to.equal(1); + expect(wrapper.find('div.foo-').length).to.equal(1); + expect(wrapper.find('div.-foo').length).to.equal(2); + expect(wrapper.find('#bar.-foo').length).to.equal(1); + }); + + it('spaces in attribute values', () => { + const wrapper = renderMethod( +
+
+
+
+
, + ); + expect(wrapper.find('[type="foo bar"]').length).to.equal(1); + }); + + it('dots in attribute values', () => { + const wrapper = renderMethod( +
+
+
+
+
, + ); + expect(wrapper.find('[type="foo.bar"]').length).to.equal(1); + }); + + it('brackets in attribute values', () => { + const wrapper = renderMethod( +
+
+
, + ); + expect(wrapper.find('[type="foo[1]"]').length).to.equal(1); + }); }); }); }); From ae733eefaa83c4269310c36865b032b26d379db5 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Fri, 25 Aug 2017 11:25:13 -0500 Subject: [PATCH 17/25] Add test for URLS in attribute values --- packages/enzyme-test-suite/test/selector-spec.jsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/enzyme-test-suite/test/selector-spec.jsx b/packages/enzyme-test-suite/test/selector-spec.jsx index 5e7ddcef8..db548351f 100644 --- a/packages/enzyme-test-suite/test/selector-spec.jsx +++ b/packages/enzyme-test-suite/test/selector-spec.jsx @@ -307,6 +307,17 @@ describe('selectors', () => { ); expect(wrapper.find('[type="foo[1]"]').length).to.equal(1); }); + + it('URLs in attribute values', () => { + const wrapper = renderMethod( + , + ); + expect(wrapper.find('a[href="https://www.foo.com"]').length).to.equal(1); + expect(wrapper.find('a[href="foo.com"]').length).to.equal(1); + }); }); }); }); From 60b6a86eba01549b83f4ee1ac8d7ef5480b2fe14 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Fri, 1 Sep 2017 14:50:16 -0700 Subject: [PATCH 18/25] Improve test coverage, use correct APIs --- .../test/RSTTraversal-spec.jsx | 18 ++++- .../test/ReactWrapper-spec.jsx | 15 +++- .../test/ShallowWrapper-spec.jsx | 15 +++- .../enzyme-test-suite/test/selector-spec.jsx | 79 ++++++++++--------- 4 files changed, 78 insertions(+), 49 deletions(-) diff --git a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx index 06a90afc9..91deeffb6 100644 --- a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx +++ b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx @@ -68,10 +68,13 @@ describe('RSTTraversal', () => { expect(nodeHasProperty(node, 'title')).to.equal(false); }); - it('should parse false', () => { - const node = $(
); - - expect(nodeHasProperty(node, 'foo', false)).to.equal(true); + it('should parse booleans', () => { + expect(nodeHasProperty(
, 'foo', true)).to.equal(true); + expect(nodeHasProperty(
, 'foo', false)).to.equal(false); + expect(nodeHasProperty(
, 'foo', 'true')).to.equal(false); + expect(nodeHasProperty(
, 'foo', false)).to.equal(true); + expect(nodeHasProperty(
, 'foo', true)).to.equal(false); + expect(nodeHasProperty(
, 'foo', 'false')).to.equal(false); }); it('should parse numeric literals', () => { @@ -89,11 +92,14 @@ describe('RSTTraversal', () => { expect(nodeHasProperty(
, 'foo', 0)).to.equal(true); expect(nodeHasProperty(
, 'foo', +0)).to.equal(true); expect(nodeHasProperty(
, 'foo', -0)).to.equal(true); + expect(nodeHasProperty(
, 'foo', 0)).to.equal(false); + expect(nodeHasProperty(
, 'foo', -0)).to.equal(false); expect(nodeHasProperty(
, 'foo', 0)).to.equal(false); expect(nodeHasProperty(
, 'foo', -0)).to.equal(false); }); it('should work with empty strings', () => { + expect(nodeHasProperty(
, 'foo', '')).to.equal(true); expect(nodeHasProperty(
, 'foo', '')).to.equal(true); expect(nodeHasProperty(
, 'foo', '')).to.equal(false); }); @@ -116,9 +122,13 @@ describe('RSTTraversal', () => { it('should work with ±Infinity', () => { expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(true); expect(nodeHasProperty(
, 'foo', +Infinity)).to.equal(true); + expect(nodeHasProperty(
, 'foo', -Infinity)).to.equal(false); + expect(nodeHasProperty(
, 'foo', 'Infinity')).to.equal(false); expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(false); expect(nodeHasProperty(
, 'foo', -Infinity)).to.equal(true); expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(false); + expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(false); + expect(nodeHasProperty(
, 'foo', '-Infinity')).to.equal(false); expect(nodeHasProperty(
, 'foo', -Infinity)).to.equal(false); }); }); diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index 1d63120a8..73186d667 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -486,9 +486,18 @@ describeWithDOM('mount', () => {
, ); - expect(() => wrapper.find('[type=text]')).to.throw(); - expect(() => wrapper.find('[type=hidden]')).to.throw(); - expect(() => wrapper.find('[type="text"]')).to.not.throw(); + 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('should support data prop selectors', () => { diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index 6ab900402..7dfee2367 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -569,9 +569,18 @@ describe('shallow', () => {
, ); - expect(() => wrapper.find('[type=text]')).to.throw(); - expect(() => wrapper.find('[type=hidden]')).to.throw(); - expect(() => wrapper.find('[type="text"]')).to.not.throw(); + 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('should compound tag and prop selector', () => { diff --git a/packages/enzyme-test-suite/test/selector-spec.jsx b/packages/enzyme-test-suite/test/selector-spec.jsx index db548351f..21bf2d935 100644 --- a/packages/enzyme-test-suite/test/selector-spec.jsx +++ b/packages/enzyme-test-suite/test/selector-spec.jsx @@ -36,8 +36,8 @@ describe('selectors', () => {
, ); - expect(wrapper.find('span').length).to.equal(2); - expect(wrapper.find('.top-div span').length).to.equal(1); + expect(wrapper.find('span')).to.have.lengthOf(2); + expect(wrapper.find('.top-div span')).to.have.lengthOf(1); }); it('nested descendent', () => { @@ -55,8 +55,8 @@ describe('selectors', () => {
, ); - expect(wrapper.find('h1').length).to.equal(3); - expect(wrapper.find('.my-div h1').length).to.equal(2); + expect(wrapper.find('h1')).to.have.lengthOf(3); + expect(wrapper.find('.my-div h1')).to.have.lengthOf(2); }); it('deep descendent', () => { @@ -75,8 +75,8 @@ describe('selectors', () => {
, ); - expect(wrapper.find('h1').length).to.equal(2); - expect(wrapper.find('div .inner span .way-inner h1').length).to.equal(1); + expect(wrapper.find('h1')).to.have.lengthOf(2); + expect(wrapper.find('div .inner span .way-inner h1')).to.have.lengthOf(1); }); it('direct descendent', () => { @@ -92,9 +92,9 @@ describe('selectors', () => {
, ); - expect(wrapper.find('.to-find').length).to.equal(3); + expect(wrapper.find('.to-find')).to.have.lengthOf(3); const descendent = wrapper.find('.container > .to-find'); - expect(descendent.length).to.equal(1); + expect(descendent).to.have.lengthOf(1); expect(descendent.text()).to.equal('Direct'); }); @@ -107,9 +107,9 @@ describe('selectors', () => {
, ); - expect(wrapper.find('.sibling').length).to.equal(2); + expect(wrapper.find('.sibling')).to.have.lengthOf(2); const toFind = wrapper.find('.to-find + .sibling'); - expect(toFind.length).to.equal(1); + expect(toFind).to.have.lengthOf(1); expect(toFind.text()).to.equal('Adjacent'); }); @@ -129,9 +129,9 @@ describe('selectors', () => {
, ); - expect(wrapper.find('.to-find').length).to.equal(3); + expect(wrapper.find('.to-find')).to.have.lengthOf(3); const toFind = wrapper.find('.to-find + .sibling'); - expect(toFind.length).to.equal(2); + expect(toFind).to.have.lengthOf(2); toFind.map(found => expect(found.text()).to.equal('Adjacent')); }); @@ -148,7 +148,7 @@ describe('selectors', () => {
, ); - expect(wrapper.find('.to-find ~ span').length).to.equal(3); + expect(wrapper.find('.to-find ~ span')).to.have.lengthOf(3); }); it('nested general siblings', () => { @@ -177,6 +177,7 @@ describe('selectors', () => { const wrapper = renderMethod(
); ['is', 'filter', 'not', 'every'].forEach((method) => { expect(() => wrapper[method]('.foo + div')).to.throw( + Error, 'This method does not support complex CSS selectors', ); }); @@ -209,7 +210,7 @@ describe('selectors', () => {
, ); - expect(wrapper.find('.foo + div > span').length).to.equal(1); + expect(wrapper.find('.foo + div > span')).to.have.lengthOf(1); }); it('.foo + .foo + .foo', () => { @@ -220,10 +221,10 @@ describe('selectors', () => {
foo3
, ); - expect(wrapper.find('.foo + .foo').length).to.equal(2); + expect(wrapper.find('.foo + .foo')).to.have.lengthOf(2); expect(wrapper.find('.foo + .foo').at(0).text()).to.equal('foo2'); expect(wrapper.find('.foo + .foo').at(1).text()).to.equal('foo3'); - expect(wrapper.find('.foo + .foo + .foo').length).to.equal(1); + expect(wrapper.find('.foo + .foo + .foo')).to.have.lengthOf(1); }); it('attribute names with numbers', () => { @@ -235,10 +236,10 @@ describe('selectors', () => {
, ); - expect(wrapper.find('[data-foo-1=1]').length).to.equal(2); - expect(wrapper.find('[data-foo-1="1"]').length).to.equal(0); - expect(wrapper.find('[data-foo-2=2]').length).to.equal(1); - expect(wrapper.find('[data-foo-2="2"]').length).to.equal(1); + expect(wrapper.find('[data-foo-1=1]')).to.have.lengthOf(2); + expect(wrapper.find('[data-foo-1="1"]')).to.have.lengthOf(0); + expect(wrapper.find('[data-foo-2=2]')).to.have.lengthOf(1); + expect(wrapper.find('[data-foo-2="2"]')).to.have.lengthOf(1); }); it('hyphens', () => { @@ -250,13 +251,13 @@ describe('selectors', () => {
, ); - expect(wrapper.find('.-foo').length).to.equal(3); - expect(wrapper.find('.foo-').length).to.equal(1); - expect(wrapper.find('[type="foo"].foo-').length).to.equal(1); - expect(wrapper.find('.foo-.-bar-').length).to.equal(1); - expect(wrapper.find('div.foo-').length).to.equal(1); - expect(wrapper.find('div.-foo').length).to.equal(2); - expect(wrapper.find('#bar.-foo').length).to.equal(1); + expect(wrapper.find('.-foo')).to.have.lengthOf(3); + expect(wrapper.find('.foo-')).to.have.lengthOf(1); + expect(wrapper.find('[type="foo"].foo-')).to.have.lengthOf(1); + expect(wrapper.find('.foo-.-bar-')).to.have.lengthOf(1); + expect(wrapper.find('div.foo-')).to.have.lengthOf(1); + expect(wrapper.find('div.-foo')).to.have.lengthOf(2); + expect(wrapper.find('#bar.-foo')).to.have.lengthOf(1); }); it('hyphens', () => { @@ -268,13 +269,13 @@ describe('selectors', () => {
, ); - expect(wrapper.find('.-foo').length).to.equal(3); - expect(wrapper.find('.foo-').length).to.equal(1); - expect(wrapper.find('[type="foo"].foo-').length).to.equal(1); - expect(wrapper.find('.foo-.-bar-').length).to.equal(1); - expect(wrapper.find('div.foo-').length).to.equal(1); - expect(wrapper.find('div.-foo').length).to.equal(2); - expect(wrapper.find('#bar.-foo').length).to.equal(1); + expect(wrapper.find('.-foo')).to.have.lengthOf(3); + expect(wrapper.find('.foo-')).to.have.lengthOf(1); + expect(wrapper.find('[type="foo"].foo-')).to.have.lengthOf(1); + expect(wrapper.find('.foo-.-bar-')).to.have.lengthOf(1); + expect(wrapper.find('div.foo-')).to.have.lengthOf(1); + expect(wrapper.find('div.-foo')).to.have.lengthOf(2); + expect(wrapper.find('#bar.-foo')).to.have.lengthOf(1); }); it('spaces in attribute values', () => { @@ -285,7 +286,7 @@ describe('selectors', () => {
, ); - expect(wrapper.find('[type="foo bar"]').length).to.equal(1); + expect(wrapper.find('[type="foo bar"]')).to.have.lengthOf(1); }); it('dots in attribute values', () => { @@ -296,7 +297,7 @@ describe('selectors', () => {
, ); - expect(wrapper.find('[type="foo.bar"]').length).to.equal(1); + expect(wrapper.find('[type="foo.bar"]')).to.have.lengthOf(1); }); it('brackets in attribute values', () => { @@ -305,7 +306,7 @@ describe('selectors', () => {
, ); - expect(wrapper.find('[type="foo[1]"]').length).to.equal(1); + expect(wrapper.find('[type="foo[1]"]')).to.have.lengthOf(1); }); it('URLs in attribute values', () => { @@ -315,8 +316,8 @@ describe('selectors', () => {
, ); - expect(wrapper.find('a[href="https://www.foo.com"]').length).to.equal(1); - expect(wrapper.find('a[href="foo.com"]').length).to.equal(1); + expect(wrapper.find('a[href="https://www.foo.com"]')).to.have.lengthOf(1); + expect(wrapper.find('a[href="foo.com"]')).to.have.lengthOf(1); }); }); }); From 2c3227bd45dadb889bdce69a26ec02c48dd2d4fa Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Fri, 1 Sep 2017 14:50:55 -0700 Subject: [PATCH 19/25] Remove uuid from enzyme package.json --- packages/enzyme/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/enzyme/package.json b/packages/enzyme/package.json index 21c665c1a..f1b519151 100644 --- a/packages/enzyme/package.json +++ b/packages/enzyme/package.json @@ -39,8 +39,7 @@ "object.assign": "^4.0.4", "object.entries": "^1.0.4", "raf": "^3.3.2", - "rst-selector-parser": "^2.2.0", - "uuid": "^3.1.0" + "rst-selector-parser": "^2.2.0" }, "devDependencies": { "babel-cli": "^6.24.1", From d0d810ea01375486580d2d2e83b1a1e907049a09 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Fri, 1 Sep 2017 15:10:09 -0700 Subject: [PATCH 20/25] Fix error type in selector assertion --- packages/enzyme-test-suite/test/selector-spec.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/enzyme-test-suite/test/selector-spec.jsx b/packages/enzyme-test-suite/test/selector-spec.jsx index 21bf2d935..badba8201 100644 --- a/packages/enzyme-test-suite/test/selector-spec.jsx +++ b/packages/enzyme-test-suite/test/selector-spec.jsx @@ -177,7 +177,7 @@ describe('selectors', () => { const wrapper = renderMethod(
); ['is', 'filter', 'not', 'every'].forEach((method) => { expect(() => wrapper[method]('.foo + div')).to.throw( - Error, + TypeError, 'This method does not support complex CSS selectors', ); }); From 459108a9bea0057dfcc4af372b46f3ffd9dee29a Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Fri, 1 Sep 2017 15:33:47 -0700 Subject: [PATCH 21/25] Fix react prop test assertion --- packages/enzyme-test-suite/test/ReactWrapper-spec.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index 73186d667..0c09ca602 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -440,12 +440,12 @@ describeWithDOM('mount', () => { const wrapper = mount(
-
+
, ); expect(wrapper.find('[htmlFor="foo"]')).to.have.length(1); - expect(wrapper.find('[htmlFor]')).to.have.length(1); + expect(wrapper.find('[htmlFor]')).to.have.length(2); }); it('should compound tag and prop selector', () => { From 188ed1c9c263aab0c89c3db22741fd5c7c615d99 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Fri, 1 Sep 2017 16:58:10 -0700 Subject: [PATCH 22/25] Use if/else for branching in selector predicate functions --- packages/enzyme/src/selectors.js | 161 +++++++++++++++---------------- 1 file changed, 79 insertions(+), 82 deletions(-) diff --git a/packages/enzyme/src/selectors.js b/packages/enzyme/src/selectors.js index 4cadf0f92..fb096f741 100644 --- a/packages/enzyme/src/selectors.js +++ b/packages/enzyme/src/selectors.js @@ -133,28 +133,29 @@ function isComplexSelector(tokens) { * @param {Function|Object|String} selector */ export function buildPredicate(selector) { - switch (typeof selector) { - case 'function': - // constructor - return node => node && node.type === selector; - case 'object': - if (!Array.isArray(selector) && selector !== null && !isEmpty(selector)) { - return node => nodeMatchesObjectProps(node, selector); - } - throw new TypeError( - 'Enzyme::Selector does not support an array, null, or empty object as a selector', - ); - case 'string': { - const tokens = safelyGenerateTokens(selector); - if (isComplexSelector(tokens)) { - throw new TypeError('This method does not support complex CSS selectors'); - } - // Simple selectors only have a single selector token - return buildPredicateFromToken(tokens[0]); + // If the selector is a function, check if the node's constructor matches + if (typeof selector === 'function') { + return node => node && node.type === selector; + } + // If the selector is an non-empty object, treat the keys/values as props + if (typeof selector === 'object') { + if (!Array.isArray(selector) && selector !== null && !isEmpty(selector)) { + return node => nodeMatchesObjectProps(node, selector); } - default: - throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor'); + throw new TypeError( + 'Enzyme::Selector does not support an array, null, or empty object as a selector', + ); + } + // If the selector is a string, parse it as a simple CSS selector + if (typeof selector === 'string') { + const tokens = safelyGenerateTokens(selector); + if (isComplexSelector(tokens)) { + throw new TypeError('This method does not support complex CSS selectors'); + } + // Simple selectors only have a single selector token + return buildPredicateFromToken(tokens[0]); } + throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor'); } /** @@ -246,73 +247,69 @@ function matchDescendant(nodes, predicate) { export function reduceTreeBySelector(selector, wrapper) { const root = wrapper.getNodeInternal(); let results = []; - switch (typeof selector) { - case 'function': - case 'object': - results = treeFilter(root, buildPredicate(selector)); - break; - case 'string': { - const tokens = safelyGenerateTokens(selector); - let index = 0; - let token = null; - while (index < tokens.length) { + + if (typeof selector === 'function' || typeof selector === 'object') { + results = treeFilter(root, buildPredicate(selector)); + } else if (typeof selector === 'string') { + const tokens = safelyGenerateTokens(selector); + let index = 0; + let token = null; + while (index < tokens.length) { + token = tokens[index]; + /** + * There are two types of tokens in a CSS selector: + * + * 1. Selector tokens. These target nodes directly, like + * type or attribute selectors. These are easy to apply + * because we can travserse the tree and return only + * the nodes that match the predicate. + * + * 2. Combinator tokens. These tokens chain together + * selector nodes. For example > for children, or + + * for adjecent siblings. These are harder to match + * as we have to track where in the tree we are + * to determine if a selector node applies or not. + */ + if (token.type === SELECTOR) { + const predicate = buildPredicateFromToken(token); + results = results.concat(treeFilter(root, predicate)); + } else { + // We can assume there always all previously matched tokens since selectors + // cannot start with combinators. + const type = token.type; + // We assume the next token is a selector, so move the index + // forward and build the predicate. + index += 1; token = tokens[index]; - /** - * There are two types of tokens in a CSS selector: - * - * 1. Selector tokens. These target nodes directly, like - * type or attribute selectors. These are easy to apply - * because we can travserse the tree and return only - * the nodes that match the predicate. - * - * 2. Combinator tokens. These tokens chain together - * selector nodes. For example > for children, or + - * for adjecent siblings. These are harder to match - * as we have to track where in the tree we are - * to determine if a selector node applies or not. - */ - if (token.type === SELECTOR) { - const predicate = buildPredicateFromToken(token); - results = results.concat(treeFilter(root, predicate)); - } else { - // We can assume there always all previously matched tokens since selectors - // cannot start with combinators. - const type = token.type; - // We assume the next token is a selector, so move the index - // forward and build the predicate. - index += 1; - token = tokens[index]; - const predicate = buildPredicateFromToken(token); - // We match against only the nodes which have already been matched, - // since a combinator is meant to refine a previous selector. - switch (type) { - // The + combinator - case ADJACENT_SIBLING: - results = matchAdjacentSiblings(results, predicate, root); - break; - // The ~ combinator - case GENERAL_SIBLING: - results = matchGeneralSibling(results, predicate, root); - break; - // The > combinator - case CHILD: - results = matchDirectChild(results, predicate); - break; - // The ' ' (whitespace) combinator - case DESCENDANT: { - results = matchDescendant(results, predicate); - break; - } - default: - throw new Error(`Unkown combinator selector: ${type}`); + const predicate = buildPredicateFromToken(token); + // We match against only the nodes which have already been matched, + // since a combinator is meant to refine a previous selector. + switch (type) { + // The + combinator + case ADJACENT_SIBLING: + results = matchAdjacentSiblings(results, predicate, root); + break; + // The ~ combinator + case GENERAL_SIBLING: + results = matchGeneralSibling(results, predicate, root); + break; + // The > combinator + case CHILD: + results = matchDirectChild(results, predicate); + break; + // The ' ' (whitespace) combinator + case DESCENDANT: { + results = matchDescendant(results, predicate); + break; } + default: + throw new Error(`Unkown combinator selector: ${type}`); } - index += 1; } - break; + index += 1; } - default: - throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor'); + } else { + throw new TypeError('Enzyme::Selector expects a string, object, or Component Constructor'); } return wrapper.wrap(results); } From 17f366d176c23f08f2f61d7e50bec938a684ff18 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Tue, 12 Sep 2017 20:02:44 -0700 Subject: [PATCH 23/25] Add nodeHasProperty test for NaN and Infinity --- packages/enzyme-test-suite/test/RSTTraversal-spec.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx index 91deeffb6..81aa4429f 100644 --- a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx +++ b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx @@ -124,11 +124,15 @@ describe('RSTTraversal', () => { expect(nodeHasProperty(
, 'foo', +Infinity)).to.equal(true); expect(nodeHasProperty(
, 'foo', -Infinity)).to.equal(false); expect(nodeHasProperty(
, 'foo', 'Infinity')).to.equal(false); + expect(nodeHasProperty(
, 'foo', NaN)).to.equal(false); expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(false); expect(nodeHasProperty(
, 'foo', -Infinity)).to.equal(true); expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(false); expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(false); expect(nodeHasProperty(
, 'foo', '-Infinity')).to.equal(false); + expect(nodeHasProperty(
, 'foo', NaN)).to.equal(false); + expect(nodeHasProperty(
, 'foo', Infinity)).to.equal(false); + expect(nodeHasProperty(
, 'foo', -Infinity)).to.equal(false); expect(nodeHasProperty(
, 'foo', -Infinity)).to.equal(false); }); }); From aadbc4a657f264ab509d9acf217613e75377d62b Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Tue, 12 Sep 2017 20:02:57 -0700 Subject: [PATCH 24/25] Add test for adjacent siblings in arrays --- packages/enzyme-test-suite/test/selector-spec.jsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/enzyme-test-suite/test/selector-spec.jsx b/packages/enzyme-test-suite/test/selector-spec.jsx index badba8201..6967c80e6 100644 --- a/packages/enzyme-test-suite/test/selector-spec.jsx +++ b/packages/enzyme-test-suite/test/selector-spec.jsx @@ -113,6 +113,18 @@ describe('selectors', () => { expect(toFind.text()).to.equal('Adjacent'); }); + it('simple adjacent with arrays', () => { + const wrapper = renderMethod( +
+
+ {[
Adjacent
]} +
, + ); + const toFind = wrapper.find('.to-find + .sibling'); + expect(toFind).to.have.lengthOf(1); + expect(toFind.text()).to.equal('Adjacent'); + }); + it('nested adjacent', () => { const wrapper = renderMethod(
From a79e150df452fb30110eba49464feb6dfe905a06 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Tue, 12 Sep 2017 23:29:37 -0700 Subject: [PATCH 25/25] Use _reactInternalFiber for 16 adapater --- packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js index b2c8b2094..b054c0dc2 100644 --- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js +++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js @@ -171,7 +171,7 @@ class ReactSixteenAdapter extends EnzymeAdapter { instance = null; }, getNode() { - return instance ? toTree(instance._reactInternalInstance).rendered : null; + return instance ? toTree(instance._reactInternalFiber).rendered : null; }, simulateEvent(node, event, mock) { const mappedEvent = mapNativeEventNames(event);