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 Property Selector #70

Merged
merged 1 commit into from
Jan 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/api/ReactWrapper/find.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ expect(wrapper.find('div.some-class')).to.have.length(3);

// CSS id selector
expect(wrapper.find('#foo')).to.have.length(1);

// property selector
expect(wrapper.find('[htmlFor="checkbox"]')).to.have.length(1);
```

Component Constructors:
Expand Down
19 changes: 19 additions & 0 deletions docs/api/selector.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,32 @@ follows:
- class syntax (`.foo`, `.foo-bar`, etc.)
- tag syntax (`input`, `div`, `span`, etc.)
- id syntax (`#foo`, `#foo-bar`, etc.)
- prop syntax (`[htmlFor="foo"]`, `[bar]`, `[baz=1]`, etc.);

**Note -- Prop selector**
Strings, numeric literals and boolean property values are supported for prop syntax
in combination of the expected string syntax. For example, the following
is supported:

```js
const wrapper = mount(
<div>
<span foo={3} bar={false} title="baz" />
</div>
)

wrapper.find('[foo=3]')
wrapper.find('[bar=false]')
wrapper.find('[title="baz"]')
```

Further, enzyme supports combining any of those supported syntaxes together to uniquely identify a
single node. For instance:

```css
div.foo.bar
input#input-name
label[foo=true]
```

Are all valid selectors in enzyme. At this time, however, any contextual CSS selector syntax that
Expand Down
47 changes: 39 additions & 8 deletions src/MountedTraversal.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import {
coercePropValue,
nodeEqual,
propsOfNode,
isSimpleSelector,
splitSelector,
selectorError,
selectorType,
isCompoundSelector,
AND,
SELECTOR,
} from './Utils';
import {
isDOMComponent,
Expand Down Expand Up @@ -65,6 +69,26 @@ export function instHasType(inst, type) {
}
}

export function instHasProperty(inst, propKey, stringifiedPropValue) {
if (!isDOMComponent(inst)) return false;
const node = getNode(inst);
const nodeProps = propsOfNode(node);
const nodePropValue = nodeProps[propKey];

const propValue = coercePropValue(stringifiedPropValue);

// intentionally not matching node props that are undefined
if (nodePropValue === undefined) {
return false;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

there should be a propsOfNode() function that abstracts this away... it's also important to check for falsy nodes as well where this would probably throw.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍


if (propValue) {
return nodePropValue === propValue;
}

return nodeProps.hasOwnProperty(propKey);
}

// called with private inst
export function renderedChildrenOfInst(inst) {
return REACT013
Expand Down Expand Up @@ -166,15 +190,22 @@ export function buildInstPredicate(selector) {
if (isCompoundSelector.test(selector)) {
return AND(splitSelector(selector).map(buildInstPredicate));
}
if (selector[0] === '.') {
// selector is a class name
return inst => instHasClassName(inst, selector.substr(1));
} else if (selector[0] === '#') {
// selector is an id name
return inst => instHasId(inst, selector.substr(1));

switch (selectorType(selector)) {
case SELECTOR.CLASS_TYPE:
return inst => instHasClassName(inst, selector.substr(1));
case SELECTOR.ID_TYPE:
return inst => instHasId(inst, selector.substr(1));
case SELECTOR.PROP_TYPE:
const propKey = selector.split(/\[([a-zA-Z\-\:]*?)(=|\])/)[1];
const propValue = selector.split(/=(.*?)]/)[1];

return node => instHasProperty(node, propKey, propValue);
default:
// selector is a string. match to DOM tag or constructor displayName
return inst => instHasType(inst, selector);
}
// selector is a string. match to DOM tag or constructor displayName
return inst => instHasType(inst, selector);
break;

default:
throw new TypeError('Expecting a string or Component Constructor');
Expand Down
49 changes: 41 additions & 8 deletions src/ShallowTraversal.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import React from 'react';
import {
coercePropValue,
propsOfNode,
isSimpleSelector,
splitSelector,
selectorError,
isCompoundSelector,
selectorType,
AND,
SELECTOR,
} from './Utils';


export function childrenOfNode(node) {
if (!node) return [];
const maybeArray = propsOfNode(node).children;
Expand Down Expand Up @@ -66,6 +70,24 @@ export function nodeHasId(node, id) {
return propsOfNode(node).id === id;
}


export function nodeHasProperty(node, propKey, stringifiedPropValue) {
const nodeProps = propsOfNode(node);
const propValue = coercePropValue(stringifiedPropValue);
const nodePropValue = nodeProps[propKey];

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

if (propValue) {
return nodePropValue === propValue;
}

return nodeProps.hasOwnProperty(propKey);
Copy link
Collaborator

Choose a reason for hiding this comment

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

and, should this be nodeProps.hasOwnProperty(propKey) or !!nodeProps[propKey]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't ran into a situation where hasOwnProperty didn't work. Can you think of a test case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In fact, I just tested this with the new changes and it doesn't work in this situation.

wrapper = <div foo={false} />

wrapper.find('[foo=false]')

In that situation that turns into

!!wrapper['foo'] // false

which then says it doesn't have the prop, though it does. So hasOwnProperty seems to be the way to go.

Copy link
Collaborator

Choose a reason for hiding this comment

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

this is basically the foo={false} question. If it has a foo property but it's set to something falsy, do we still want to return true?

}


export function nodeHasType(node, type) {
if (!type || !node) return false;
if (!node.type) return false;
Expand All @@ -86,21 +108,32 @@ export function buildPredicate(selector) {
if (isCompoundSelector.test(selector)) {
return AND(splitSelector(selector).map(buildPredicate));
}
if (selector[0] === '.') {
// selector is a class name
return node => hasClassName(node, selector.substr(1));
} else if (selector[0] === '#') {
// selector is an id name
return node => nodeHasId(node, selector.substr(1));

switch (selectorType(selector)) {
case SELECTOR.CLASS_TYPE:
return node => hasClassName(node, selector.substr(1));

case SELECTOR.ID_TYPE:
return node => nodeHasId(node, selector.substr(1));

case SELECTOR.PROP_TYPE:
const propKey = selector.split(/\[([a-zA-Z\-]*?)(=|\])/)[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);
}
// selector is a string. match to DOM tag or constructor displayName
return node => nodeHasType(node, selector);
break;


default:
throw new TypeError('Expecting a string or Component Constructor');
}
}


export function getTextFromNode(node) {
if (node === null || node === undefined) {
return '';
Expand Down
46 changes: 43 additions & 3 deletions src/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ export function withSetStateAllowed(fn) {
}

export function splitSelector(selector) {
return selector.split(/(?=\.)/);
return selector.split(/(?=\.|\[.*\])/);
}

export function isSimpleSelector(selector) {
// any of these characters pretty much guarantee it's a complex selector
return !/[~\s\[\]:>]/.test(selector);
return !/[~\s:>]/.test(selector);
}

export function selectorError(selector) {
Expand All @@ -116,8 +116,25 @@ export function selectorError(selector) {
);
}

export const isCompoundSelector = /[a-z]\.[a-z]/i;
export const isCompoundSelector = /([a-z]\.[a-z]|[a-z]\[.*\])/i;
Copy link
Collaborator

Choose a reason for hiding this comment

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

will this always work? what about [foo][bar]?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does, I tested just tested that exact scenario, but also this test effectively proves it also, https://github.com/airbnb/enzyme/pull/70/files#diff-e08e795cdfa46e03ea97df132b57d75aR167

Copy link
Collaborator

Choose a reason for hiding this comment

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

My testing shows that it doesnt? div[foo][bar] matches but [foo][bar] does not

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe test again? I literally just tested both those ideas and they both matched.


const isPropSelector = /^\[.*\]$/;

export const SELECTOR = {
CLASS_TYPE: 0,
ID_TYPE: 1,
PROP_TYPE: 2,
};

export function selectorType(selector) {
if (selector[0] === '.') {
return SELECTOR.CLASS_TYPE;
} else if (selector[0] === '#') {
return SELECTOR.ID_TYPE;
} else if (isPropSelector.test(selector)) {
return SELECTOR.PROP_TYPE;
}
}

export function AND(fns) {
return x => {
Expand All @@ -128,3 +145,26 @@ export function AND(fns) {
return true;
};
}

export function coercePropValue(propValue) {
// can be undefined
if (propValue === undefined) {
return propValue;
}

// if propValue includes quotes, it should be
// treated as a string
if (propValue.search(/"/) !== -1) {
return propValue.replace(/"/g, '');
}

const numericPropValue = parseInt(propValue, 10);

// if parseInt is not NaN, then we've wanted a number
if (!isNaN(numericPropValue)) {
return numericPropValue;
}

// coerce to boolean
return propValue === 'true' ? true : false;
}
94 changes: 94 additions & 0 deletions src/__tests__/ReactWrapper-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,100 @@ describeWithDOM('mount', () => {
expect(wrapper.find(Foo).type()).to.equal(Foo);
});

it('should find component based on a react prop', () => {
const wrapper = mount(
<div>
<span htmlFor="foo" />
</div>
);

expect(wrapper.find('[htmlFor="foo"]')).to.have.length(1);
expect(wrapper.find('[htmlFor]')).to.have.length(1);
});

it('should compound tag and prop selector', () => {
const wrapper = mount(
<div>
<span htmlFor="foo" />
</div>
);

expect(wrapper.find('span[htmlFor="foo"]')).to.have.length(1);
expect(wrapper.find('span[htmlFor]')).to.have.length(1);

});

it('should support data prop selectors', () => {
const wrapper = mount(
<div>
<span data-foo="bar" />
</div>
);

expect(wrapper.find('[data-foo="bar"]')).to.have.length(1);
expect(wrapper.find('[data-foo]')).to.have.length(1);
});

it('should find components with multiple matching props', () => {
const onChange = () => {};
const wrapper = mount(
<div>
<span htmlFor="foo" onChange={onChange} preserveAspectRatio="xMaxYMax" />
</div>
);

expect(wrapper.find('span[htmlFor="foo"][onChange]')).to.have.length(1);
expect(wrapper.find('span[htmlFor="foo"][preserveAspectRatio="xMaxYMax"]')).to.have.length(1);
});


it('should not find property when undefined', () => {
const wrapper = mount(
<div>
<span data-foo={undefined} />
</div>
);

expect(wrapper.find('[data-foo]')).to.have.length(0);
});

it('should support boolean and numeric values for matching props', () => {
const wrapper = mount(
<div>
<span value={1} />
<a value={false} />
</div>
);

expect(wrapper.find('span[value=1]')).to.have.length(1);
expect(wrapper.find('span[value=2]')).to.have.length(0);
expect(wrapper.find('a[value=false]')).to.have.length(1);
expect(wrapper.find('a[value=true]')).to.have.length(0);
});

it('should not find key or ref via property selector', () => {
class Foo extends React.Component {
render() {
const arrayOfComponents = [<div key="1" />, <div key="2" />];

return (
<div>
<div ref="foo" />
{arrayOfComponents}
</div>
);
}
}

const wrapper = mount(<Foo />);

expect(wrapper.find('div[ref="foo"]')).to.have.length(0);
expect(wrapper.find('div[key="1"]')).to.have.length(0);
expect(wrapper.find('[ref]')).to.have.length(0);
expect(wrapper.find('[key]')).to.have.length(0);
});


it('should find multiple elements based on a class name', () => {
const wrapper = mount(
<div>
Expand Down
Loading