Skip to content

Commit

Permalink
Add support for the prop selector
Browse files Browse the repository at this point in the history
cleanup

mounted attr api

Add documentation for attr api

the rest of the attribute mapping and test for colons

react 13/14 compatibility

clean up code by utilizing utils method

fix lint error

Change syntax for react property lookups

fix lint errors

Fix key lookup bug, and simplify regex

Change back to [] syntax for props only

fix documentation

split out test coverage

more test coverage

fix lint issue

support non-string values for prop querying

Add tests for not finding ref or key

string based selector only

deny undefined values

fix lint issues

Revert to 80b14d3

support multiple literal types for property lookups

WIP: Early push for discussion

cleanup

Add support for prop selector

clean up code by utilizing utils method

fix lint error

Change syntax for react property lookups

Change back to [] syntax for props only

split out test coverage

fix lint issue

support non-string values for prop querying

string based selector only

fix lint issues

Revert to 80b14d3

support multiple literal types for property lookups

Revert "support multiple literal types for property lookups"

This reverts commit 72872fa.

undo revert

Trigger new build

add support for prop selector

fix lint errors
  • Loading branch information
blainekasten committed Jan 10, 2016
1 parent ba911a0 commit 3b2af8c
Show file tree
Hide file tree
Showing 9 changed files with 395 additions and 21 deletions.
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;
}

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);
}


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;

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

0 comments on commit 3b2af8c

Please sign in to comment.