Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support all attribute selector operators #1157

Merged
merged 12 commits into from
Nov 11, 2017
91 changes: 0 additions & 91 deletions packages/enzyme-test-suite/test/RSTTraversal-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { expect } from 'chai';
import { elementToTree } from 'enzyme-adapter-utils';
import {
hasClassName,
nodeHasProperty,
treeForEach,
treeFilter,
pathToNode,
Expand Down Expand Up @@ -46,96 +45,6 @@ describe('RSTTraversal', () => {
});
});

describe('nodeHasProperty', () => {
it('should find properties', () => {
function noop() {}
const node = $(<div onChange={noop} title="foo" />);

expect(nodeHasProperty(node, 'onChange')).to.equal(true);
expect(nodeHasProperty(node, 'title', 'foo')).to.equal(true);
});

it('should not match on html attributes', () => {
const node = $(<div htmlFor="foo" />);

expect(nodeHasProperty(node, 'for', 'foo')).to.equal(false);
});

it('should not find undefined properties', () => {
const node = $(<div title={undefined} />);

expect(nodeHasProperty(node, 'title')).to.equal(false);
});

it('should parse booleans', () => {
expect(nodeHasProperty(<div foo />, 'foo', true)).to.equal(true);
expect(nodeHasProperty(<div foo />, 'foo', false)).to.equal(false);
expect(nodeHasProperty(<div foo />, 'foo', 'true')).to.equal(false);
expect(nodeHasProperty(<div foo={false} />, 'foo', false)).to.equal(true);
expect(nodeHasProperty(<div foo={false} />, 'foo', true)).to.equal(false);
expect(nodeHasProperty(<div foo={false} />, 'foo', 'false')).to.equal(false);
});

it('should parse numeric literals', () => {
expect(nodeHasProperty(<div foo={2.3} />, 'foo', 2.3)).to.equal(true);
expect(nodeHasProperty(<div foo={2} />, 'foo', 2)).to.equal(true);
expect(nodeHasProperty(<div foo={2} />, 'foo', '2abc')).to.equal(false);
expect(nodeHasProperty(<div foo={2} />, 'foo', 'abc2')).to.equal(false);
expect(nodeHasProperty(<div foo={-2} />, 'foo', -2)).to.equal(true);
expect(nodeHasProperty(<div foo={2e8} />, 'foo', 2e8)).to.equal(true);
expect(nodeHasProperty(<div foo={Infinity} />, 'foo', Infinity)).to.equal(true);
expect(nodeHasProperty(<div foo={-Infinity} />, 'foo', -Infinity)).to.equal(true);
});

it('should parse zeroes properly', () => {
expect(nodeHasProperty(<div foo={0} />, 'foo', 0)).to.equal(true);
expect(nodeHasProperty(<div foo={0} />, 'foo', +0)).to.equal(true);
expect(nodeHasProperty(<div foo={-0} />, 'foo', -0)).to.equal(true);
expect(nodeHasProperty(<div foo={-0} />, 'foo', 0)).to.equal(false);
expect(nodeHasProperty(<div foo={0} />, 'foo', -0)).to.equal(false);
expect(nodeHasProperty(<div foo={1} />, 'foo', 0)).to.equal(false);
expect(nodeHasProperty(<div foo={2} />, 'foo', -0)).to.equal(false);
});

it('should work with empty strings', () => {
expect(nodeHasProperty(<div foo="" />, 'foo', '')).to.equal(true);
expect(nodeHasProperty(<div foo={''} />, 'foo', '')).to.equal(true);
expect(nodeHasProperty(<div foo={'bar'} />, 'foo', '')).to.equal(false);
});

it('should work with NaN', () => {
expect(nodeHasProperty(<div foo={NaN} />, 'foo', NaN)).to.equal(true);
expect(nodeHasProperty(<div foo={0} />, 'foo', NaN)).to.equal(false);
});

it('should work with null', () => {
expect(nodeHasProperty(<div foo={null} />, 'foo', null)).to.equal(true);
expect(nodeHasProperty(<div foo={0} />, 'foo', null)).to.equal(false);
});

it('should work with false', () => {
expect(nodeHasProperty(<div foo={false} />, 'foo', false)).to.equal(true);
expect(nodeHasProperty(<div foo={0} />, 'foo', false)).to.equal(false);
});

it('should work with ±Infinity', () => {
expect(nodeHasProperty(<div foo={Infinity} />, 'foo', Infinity)).to.equal(true);
expect(nodeHasProperty(<div foo={Infinity} />, 'foo', +Infinity)).to.equal(true);
expect(nodeHasProperty(<div foo={Infinity} />, 'foo', -Infinity)).to.equal(false);
expect(nodeHasProperty(<div foo={Infinity} />, 'foo', 'Infinity')).to.equal(false);
expect(nodeHasProperty(<div foo={Infinity} />, 'foo', NaN)).to.equal(false);
expect(nodeHasProperty(<div foo={0} />, 'foo', Infinity)).to.equal(false);
expect(nodeHasProperty(<div foo={-Infinity} />, 'foo', -Infinity)).to.equal(true);
expect(nodeHasProperty(<div foo={-Infinity} />, 'foo', Infinity)).to.equal(false);
expect(nodeHasProperty(<div foo={-Infinity} />, 'foo', Infinity)).to.equal(false);
expect(nodeHasProperty(<div foo={-Infinity} />, 'foo', '-Infinity')).to.equal(false);
expect(nodeHasProperty(<div foo={-Infinity} />, 'foo', NaN)).to.equal(false);
expect(nodeHasProperty(<div foo={NaN} />, 'foo', Infinity)).to.equal(false);
expect(nodeHasProperty(<div foo={NaN} />, 'foo', -Infinity)).to.equal(false);
expect(nodeHasProperty(<div foo={0} />, 'foo', -Infinity)).to.equal(false);
});
});

describe('treeForEach', () => {
it('should be called once for a leaf node', () => {
const spy = sinon.spy();
Expand Down
126 changes: 126 additions & 0 deletions packages/enzyme-test-suite/test/selector-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ const tests = [
},
];

let expectAttributeMatch;

describe('selectors', () => {
tests.forEach(({ describeMethod, name, renderMethod }) => {
before(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is "before all"; could we use beforeEach instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but "before all" is the intention here, since expectAttributeMatch only needs to be initialized once per renderMethod

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair; beforeAll tends to cause test flakiness in my experience

expectAttributeMatch = (element, selector, expected) => {
const wrapper = renderMethod(element);
expect(wrapper.is(selector)).to.equal(expected);
};
});
describeMethod(name, () => {
it('simple descendent', () => {
const wrapper = renderMethod((
Expand Down Expand Up @@ -350,6 +358,124 @@ describe('selectors', () => {
expect(wrapper.find('Wrapped(Foo)')).to.have.lengthOf(1);
expect(wrapper.find('Wrapped(Twice(Bar))')).to.have.lengthOf(1);
});

it('should parse booleans', () => {
expectAttributeMatch(<div hidden />, '[hidden=true]', true);
expectAttributeMatch(<div hidden />, '[hidden=false]', false);
expectAttributeMatch(<div hidden />, '[hidden="true"]', false);
expectAttributeMatch(<div hidden={false} />, '[hidden=false]', true);
expectAttributeMatch(<div hidden={false} />, '[hidden=true]', false);
expectAttributeMatch(<div hidden={false} />, '[hidden="false"]', false);
});

it('should parse numeric literals', () => {
expectAttributeMatch(<div data-foo={2.3} />, '[data-foo=2.3]', true);
expectAttributeMatch(<div data-foo={2} />, '[data-foo=2]', true);
expectAttributeMatch(<div data-foo={2} />, '[data-foo="2abc"]', false);
expectAttributeMatch(<div data-foo={2} />, '[data-foo="abc2"]', false);
expectAttributeMatch(<div data-foo={-2} />, '[data-foo=-2]', true);
// @TODO this is failing due to a parser issue
// expectAttributeMatch(<div data-foo={2e8} />, '[data-foo=2e8]', true);
expectAttributeMatch(<div data-foo={Infinity} />, '[data-foo=Infinity]', true);
expectAttributeMatch(<div data-foo={Infinity} />, '[data-foo=-Infinity]', false);
expectAttributeMatch(<div data-foo={-Infinity} />, '[data-foo=-Infinity]', true);
expectAttributeMatch(<div data-foo={-Infinity} />, '[data-foo=Infinity]', false);
});

it('should parse zeroes properly', () => {
expectAttributeMatch(<div data-foo={0} />, '[data-foo=0]', true);
expectAttributeMatch(<div data-foo={0} />, '[data-foo=+0]', true);
expectAttributeMatch(<div data-foo={-0} />, '[data-foo=-0]', true);
expectAttributeMatch(<div data-foo={-0} />, '[data-foo=0]', false);
expectAttributeMatch(<div data-foo={0} />, '[data-foo=-0]', false);
expectAttributeMatch(<div data-foo={1} />, '[data-foo=0]', false);
expectAttributeMatch(<div data-foo={2} />, '[data-foo=-0]', false);
});

it('should work with empty strings', () => {
expectAttributeMatch(<div className="" />, '[className=""]', true);
expectAttributeMatch(<div className={''} />, '[className=""]', true);
expectAttributeMatch(<div className={'bar'} />, '[className=""]', false);
});

it('should work with NaN', () => {
expectAttributeMatch(<div data-foo={NaN} />, '[data-foo=NaN]', true);
expectAttributeMatch(<div data-foo={0} />, '[data-foo=NaN]', false);
});

it('should work with null', () => {
expectAttributeMatch(<div data-foo={null} />, '[data-foo=null]', true);
expectAttributeMatch(<div data-foo={0} />, '[data-foo=null]', false);
});

it('should work with false', () => {
expectAttributeMatch(<div data-foo={false} />, '[data-foo=false]', true);
expectAttributeMatch(<div data-foo={0} />, '[data-foo=false]', false);
});
it('should work with ±Infinity', () => {
expectAttributeMatch(<div data-foo={Infinity} />, '[data-foo=Infinity]', true);
expectAttributeMatch(<div data-foo={Infinity} />, '[data-foo=+Infinity]', true);
expectAttributeMatch(<div data-foo={Infinity} />, '[data-foo=-Infinity]', false);
expectAttributeMatch(<div data-foo={Infinity} />, '[data-foo=NaN]', false);
expectAttributeMatch(<div data-foo={0} />, '[data-foo=Infinity]', false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this test is duplicated at the bottom of the it block - should one of them be testing -Infinity, as per the old tests?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, thanks!

expectAttributeMatch(<div data-foo={0} />, '[data-foo=-Infinity]', false);
expectAttributeMatch(<div data-foo={-Infinity} />, '[data-foo=-Infinity]', true);
expectAttributeMatch(<div data-foo={-Infinity} />, '[data-foo=Infinity]', false);
expectAttributeMatch(<div data-foo={-Infinity} />, '[data-foo="-Infinity"]', false);
expectAttributeMatch(<div data-foo={-Infinity} />, '[data-foo=NaN]', false);
expectAttributeMatch(<div data-foo={NaN} />, '[data-foo=Infinity]', false);
expectAttributeMatch(<div data-foo={NaN} />, '[data-foo=-Infinity]', false);
});

it('whitespace list attribute selector', () => {
expectAttributeMatch(<div rel="foo bar baz" />, '[rel~="bar"]', true);
expectAttributeMatch(<div rel="foo bar baz" />, '[rel~="baz"]', true);
expectAttributeMatch(<div rel="foo bar baz" />, '[rel~="foo"]', true);
expectAttributeMatch(<div rel="foo bar baz" />, '[rel~="foo bar"]', false);
expectAttributeMatch(<div rel={1} />, '[rel~=1]', false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong - shouldn't this return true? I think ~= should still match if it's exactly equal, with no whitespace.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jack-lewin this implementation only supports matching strings with these partial match operators, so all other types will return false. If you want to match exactly equal values you should use =

expectAttributeMatch(<div rel="1" />, '[rel~=1]', false);
});

it('hypen attribute selector', () => {
expectAttributeMatch(<a hrefLang="en-US" />, '[hrefLang|="en"]', true);
expectAttributeMatch(<a hrefLang="en-US" />, '[hrefLang|="en-US"]', true);
expectAttributeMatch(<a hrefLang="en-US" />, '[hrefLang|="US"]', false);
expectAttributeMatch(<a hrefLang="en-US" />, '[hrefLang|="enUS"]', false);
expectAttributeMatch(<a hrefLang={1} />, '[hrefLang|=1]', false);
expectAttributeMatch(<a hrefLang="1" />, '[hrefLang|=1]', false);
});

it('prefix attribute operator', () => {
expectAttributeMatch(<img alt="" src="foo-bar.jpg" />, '[src^="foo"]', true);
expectAttributeMatch(<img alt="" src="foo-bar.jpg" />, '[src^="foo-bar"]', true);
expectAttributeMatch(<img alt="" src="foo-bar.jpg" />, '[src^="foo-bar.jpg"]', true);
expectAttributeMatch(<img alt="" src="foo-bar.jpg" />, '[src^="bar"]', false);
expectAttributeMatch(<img alt="" src="foo-bar.jpg" />, '[src^=""]', false);
expectAttributeMatch(<img alt="" src={1} />, '[src^=1]', false);
expectAttributeMatch(<img alt="" src="1" />, '[src^=1]', false);
});

it('suffix attribute operator', () => {
expectAttributeMatch(<img alt="" src="foo-bar.jpg" />, '[src$=".jpg"]', true);
expectAttributeMatch(<img alt="" src="foo-bar.jpg" />, '[src$="bar.jpg"]', true);
expectAttributeMatch(<img alt="" src="foo-bar.jpg" />, '[src$="foo-bar.jpg"]', true);
expectAttributeMatch(<img alt="" src="foo-bar.jpg" />, '[src$="foo"]', false);
expectAttributeMatch(<img alt="" src="foo-bar.jpg" />, '[src$=""]', false);
expectAttributeMatch(<img alt="" src={1} />, '[src$=1]', false);
expectAttributeMatch(<img alt="" src="1" />, '[src$=1]', false);
});

it('substring attribute operator', () => {
expectAttributeMatch(<div id="foo bar baz" />, '[id*="foo"]', true);
expectAttributeMatch(<div id="foo bar baz" />, '[id*="foo bar"]', true);
expectAttributeMatch(<div id="foo bar baz" />, '[id*="foo bar baz"]', true);
expectAttributeMatch(<div id="foo bar baz" />, '[id*="foo "]', true);
expectAttributeMatch(<div id="foo bar baz" />, '[id*="fo"]', true);
expectAttributeMatch(<div id="foo bar baz" />, '[id*="foz"]', false);
expectAttributeMatch(<div id="foo bar baz" />, '[id*=1]', false);
expectAttributeMatch(<div id={1} />, '[id*=1]', false);
expectAttributeMatch(<div id="1" />, '[id*=1]', false);
});
});
});
});
1 change: 1 addition & 0 deletions packages/enzyme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"dependencies": {
"cheerio": "^1.0.0-rc.2",
"function.prototype.name": "^1.0.3",
"has": "^1.0.1",
"is-subset": "^0.1.1",
"lodash": "^4.17.4",
"object-is": "^1.0.1",
Expand Down
21 changes: 1 addition & 20 deletions packages/enzyme/src/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function isCustomComponentElement(inst, adapter) {
return !!inst && adapter.isValidElement(inst) && typeof inst.type === 'function';
}

function propsOfNode(node) {
export function propsOfNode(node) {
return entries((node && node.props) || {})
.filter(([, value]) => typeof value !== 'undefined')
.reduce((acc, [key, value]) => Object.assign(acc, { [key]: value }), {});
Expand Down Expand Up @@ -219,25 +219,6 @@ export function AND(fns) {
return x => fnsReversed.every(fn => fn(x));
}

export function nodeHasProperty(node, propKey, propValue) {
const nodeProps = propsOfNode(node);
const descriptor = Object.getOwnPropertyDescriptor(nodeProps, propKey);
if (descriptor && descriptor.get) {
return false;
}
const nodePropValue = nodeProps[propKey];

if (typeof nodePropValue === 'undefined') {
return false;
}

if (typeof propValue !== 'undefined') {
return is(nodePropValue, propValue);
}

return Object.prototype.hasOwnProperty.call(nodeProps, propKey);
}

export function displayNameOfNode(node) {
if (!node) return null;

Expand Down
Loading