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); diff --git a/packages/enzyme-test-suite/test/ComplexSelector-spec.jsx b/packages/enzyme-test-suite/test/ComplexSelector-spec.jsx deleted file mode 100644 index 16fdad3d2..000000000 --- a/packages/enzyme-test-suite/test/ComplexSelector-spec.jsx +++ /dev/null @@ -1,193 +0,0 @@ -import './_helpers/setupAdapters'; -import React from 'react'; -import { expect } from 'chai'; - -import { describeWithDOM } from './_helpers'; -import { - mount, - shallow, -} from 'enzyme'; - -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 -
- -
- -
, - ); - - expect(wrapper.find('span').length).to.equal(2); - expect(wrapper.find('.top-div span').length).to.equal(1); - }); - - it('nested descendent', () => { - const wrapper = renderMethod( -
-
-

heading

-
-
-

heading

-
-
-
-

heading

-
, - ); - - 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); - }); - - 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); - }); - }); - }); -}); diff --git a/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx b/packages/enzyme-test-suite/test/RSTTraversal-spec.jsx index 7248705ce..81aa4429f 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', () => { @@ -82,13 +53,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', () => { @@ -97,71 +68,73 @@ describe('RSTTraversal', () => { expect(nodeHasProperty(node, 'title')).to.equal(false); }); - it('should parse false as a literal', () => { - 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 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); - expect(() => nodeHasProperty(
, 'foo', '2abc')).to.throw(); - expect(() => nodeHasProperty(
, 'foo', 'abc2')).to.throw(); - 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 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); }); 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); + 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(true); expect(nodeHasProperty(
, 'foo', '')).to.equal(false); - expect(nodeHasProperty(
, 'foo', '""')).to.equal(false); }); 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); - }); - - it('should throw when un unquoted string is passed in', () => { - const node = $(
); - - expect(() => nodeHasProperty(node, 'title', 'foo')).to.throw(); + 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', 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); }); - }); describe('treeForEach', () => { @@ -367,12 +340,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/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index f2a2ebef6..0c09ca602 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,11 +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', () => { @@ -476,22 +478,26 @@ describeWithDOM('mount', () => { expect(wrapper.find('.row + .row')).to.have.lengthOf(1); }); - // 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 throw for non-numeric attribute values without quotes', () => { + const wrapper = mount( +
+ + + +
, + ); + expect(() => wrapper.find('[type=text]')).to.throw( + Error, + 'Failed to parse selector: [type=text]', + ); + expect(() => wrapper.find('[type=hidden]')).to.throw( + Error, + 'Failed to parse selector: [type=hidden]', + ); + expect(() => wrapper.find('[type="text"]')).to.not.throw( + Error, + 'Failed to parse selector: [type="text"]', + ); }); it('should support data prop selectors', () => { @@ -544,6 +550,10 @@ describeWithDOM('mount', () => { , ); diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index e518954cf..7dfee2367 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -561,17 +561,25 @@ describe('shallow', () => { expect(wrapper.find('.row + .row')).to.have.lengthOf(1); }); - it('should error sensibly if prop selector without quotes', () => { + it('should throw for non-numeric attribute values without quotes', () => { 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 + 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"]', ); }); diff --git a/packages/enzyme-test-suite/test/Utils-spec.jsx b/packages/enzyme-test-suite/test/Utils-spec.jsx index ac58d381d..25ebf2f22 100644 --- a/packages/enzyme-test-suite/test/Utils-spec.jsx +++ b/packages/enzyme-test-suite/test/Utils-spec.jsx @@ -4,13 +4,9 @@ import { expect } from 'chai'; import { describeIf } from './_helpers'; import { - coercePropValue, childrenToSimplifiedArray, nodeEqual, nodeMatches, - isPseudoClassSelector, - SELECTOR, - selectorType, displayNameOfNode, } from 'enzyme/build/Utils'; import { @@ -439,85 +435,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', () => { - 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-test-suite/test/selector-spec.jsx b/packages/enzyme-test-suite/test/selector-spec.jsx new file mode 100644 index 000000000..6967c80e6 --- /dev/null +++ b/packages/enzyme-test-suite/test/selector-spec.jsx @@ -0,0 +1,336 @@ +import './_helpers/setupAdapters'; +import React from 'react'; +import { expect } from 'chai'; + +import { describeWithDOM } from './_helpers'; +import { + mount, + shallow, +} from 'enzyme'; + +const tests = [ + { + name: 'mount', + renderMethod: mount, + describeMethod: describeWithDOM, + }, + { + name: 'shallow', + renderMethod: shallow, + describeMethod: describe, + }, +]; + +describe('selectors', () => { + tests.forEach(({ describeMethod, name, renderMethod }) => { + describeMethod(name, () => { + it('simple descendent', () => { + const wrapper = renderMethod( +
+
+ inside top div +
+ +
+ +
, + ); + + expect(wrapper.find('span')).to.have.lengthOf(2); + expect(wrapper.find('.top-div span')).to.have.lengthOf(1); + }); + + it('nested descendent', () => { + const wrapper = renderMethod( +
+
+

heading

+
+
+

heading

+
+
+
+

heading

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

heading

+
+
+
+
+

heading

+
, + ); + + expect(wrapper.find('h1')).to.have.lengthOf(2); + expect(wrapper.find('div .inner span .way-inner h1')).to.have.lengthOf(1); + }); + + it('direct descendent', () => { + const wrapper = renderMethod( +
+
+
Direct
+
+
Nested
+
+
+
Outside
+
, + ); + + expect(wrapper.find('.to-find')).to.have.lengthOf(3); + const descendent = wrapper.find('.container > .to-find'); + expect(descendent).to.have.lengthOf(1); + expect(descendent.text()).to.equal('Direct'); + }); + + it('simple adjacent', () => { + const wrapper = renderMethod( +
+
+
Adjacent
+
Not Adjacent
+
, + ); + + expect(wrapper.find('.sibling')).to.have.lengthOf(2); + const toFind = wrapper.find('.to-find + .sibling'); + expect(toFind).to.have.lengthOf(1); + 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( +
+
+
Adjacent
+
+
Not Adjacent
+
+
+
Adjacent
+
+
Not Adjacent
+
+
, + ); + + expect(wrapper.find('.to-find')).to.have.lengthOf(3); + const toFind = wrapper.find('.to-find + .sibling'); + expect(toFind).to.have.lengthOf(2); + toFind.map(found => expect(found.text()).to.equal('Adjacent')); + }); + + it('simple general siblings', () => { + const wrapper = renderMethod( +
+ + + + +
+ +
+
, + ); + + expect(wrapper.find('.to-find ~ span')).to.have.lengthOf(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('throws for complex selectors in simple selector methods', () => { + const wrapper = renderMethod(
); + ['is', 'filter', 'not', 'every'].forEach((method) => { + expect(() => wrapper[method]('.foo + div')).to.throw( + TypeError, + 'This method does not support complex CSS 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( +
+
+
+ +
+
+ +
+
, + ); + + expect(wrapper.find('.foo + div > span')).to.have.lengthOf(1); + }); + + it('.foo + .foo + .foo', () => { + const wrapper = renderMethod( +
+
foo1
+
foo2
+
foo3
+
, + ); + 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')).to.have.lengthOf(1); + }); + + it('attribute names with numbers', () => { + const wrapper = renderMethod( +
+
+
+
+
+
, + ); + 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', () => { + const wrapper = renderMethod( +
+
+
+
+ +
, + ); + 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', () => { + const wrapper = renderMethod( +
+
+
+
+ +
, + ); + 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', () => { + const wrapper = renderMethod( +
+
+
+
+
, + ); + expect(wrapper.find('[type="foo bar"]')).to.have.lengthOf(1); + }); + + it('dots in attribute values', () => { + const wrapper = renderMethod( +
+
+
+
+
, + ); + expect(wrapper.find('[type="foo.bar"]')).to.have.lengthOf(1); + }); + + it('brackets in attribute values', () => { + const wrapper = renderMethod( +
+
+
, + ); + expect(wrapper.find('[type="foo[1]"]')).to.have.lengthOf(1); + }); + + it('URLs in attribute values', () => { + const wrapper = renderMethod( + , + ); + expect(wrapper.find('a[href="https://www.foo.com"]')).to.have.lengthOf(1); + expect(wrapper.find('a[href="foo.com"]')).to.have.lengthOf(1); + }); + }); + }); +}); diff --git a/packages/enzyme/package.json b/packages/enzyme/package.json index 7f7b444b3..f1b519151 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", - "uuid": "^3.1.0" + "rst-selector-parser": "^2.2.0" }, "devDependencies": { "babel-cli": "^6.24.1", 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; - }; - } -} diff --git a/packages/enzyme/src/RSTTraversal.js b/packages/enzyme/src/RSTTraversal.js index 730be555c..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) || {}; @@ -44,6 +35,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); } @@ -86,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/ReactWrapper.js b/packages/enzyme/src/ReactWrapper.js index b4fd2fb53..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 ComplexSelector from './ComplexSelector'; import { containsChildrenSubArray, typeOfNode, @@ -25,9 +24,10 @@ import { childrenOfNode, parentsOfNode, treeFilter, - buildPredicate, } from './RSTTraversal'; +import { buildPredicate, reduceTreeBySelector } from './selectors'; + const noop = () => {}; const NODE = sym('__node__'); @@ -36,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 @@ -101,11 +100,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 +458,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..af1c3e068 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__'); @@ -38,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. @@ -145,11 +143,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 +534,7 @@ class ShallowWrapper { * @returns {ShallowWrapper} */ find(selector) { - return this[COMPLEX_SELECTOR].find(selector, this); + return reduceTreeBySelector(selector, this); } /** diff --git a/packages/enzyme/src/Utils.js b/packages/enzyme/src/Utils.js index 12f36de51..84c9de011 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,135 +205,12 @@ 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)); } -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(); - - // 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 - if (!is(NaN, numericPropValue)) { - return numericPropValue; - } - - // coerce to boolean - 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.`, - ); -} - -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) { @@ -342,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 new file mode 100644 index 000000000..fb096f741 --- /dev/null +++ b/packages/enzyme/src/selectors.js @@ -0,0 +1,315 @@ +import { createParser } from 'rst-selector-parser'; +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'; +// @TODO we dont support these, throw if they are used +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 + * by scalpel. + * @param {String} selector + */ +function safelyGenerateTokens(selector) { + try { + return parser.parse(selector); + } catch (err) { + throw new Error(`Failed to parse selector: ${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) { + /** + * 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