Skip to content

Commit

Permalink
Support all attribute value selector operators
Browse files Browse the repository at this point in the history
  • Loading branch information
Brandon Dail authored and ljharb committed Sep 26, 2017
1 parent b88114b commit 4c19481
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 11 deletions.
10 changes: 2 additions & 8 deletions packages/enzyme/src/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,23 +219,17 @@ export function AND(fns) {
return x => fnsReversed.every(fn => fn(x));
}

export function nodeHasProperty(node, propKey, propValue) {
export function nodeHasMatchingProperty(node, propKey, matcher) {
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);
return matcher(nodePropValue, nodeProps);
}

export function displayNameOfNode(node) {
Expand Down
79 changes: 76 additions & 3 deletions packages/enzyme/src/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import values from 'object.values';
import isEmpty from 'lodash/isEmpty';
import flatten from 'lodash/flatten';
import unique from 'lodash/uniq';
import is from 'object-is';
import {
treeFilter,
nodeHasId,
Expand All @@ -11,7 +12,7 @@ import {
childrenOfNode,
hasClassName,
} from './RSTTraversal';
import { nodeHasType, nodeHasProperty } from './Utils';
import { nodeHasType, nodeHasMatchingProperty } from './Utils';
// our CSS selector parser instance
const parser = createParser();

Expand All @@ -32,6 +33,13 @@ const ATTRIBUTE_VALUE = 'attributeValueSelector';
const PSEUDO_CLASS = 'pseudoClassSelector';
const PSEUDO_ELEMENT = 'pseudoElementSelector';

const EXACT_ATTRIBUTE_OPERATOR = '=';
const WHITELIST_ATTRIBUTE_OPERATOR = '~=';
const HYPEN_ATTRIBUTE_OPERATOR = '|=';
const PREFIX_ATTRIBUTE_OPERATOR = '^=';
const SUFFIX_ATTRIBUTE_OPERATOR = '$=';
const SUBSTRING_ATTRIBUTE_OPERATOR = '*=';

/**
* Calls reduce on a array of nodes with the passed
* function, returning only unique results.
Expand All @@ -55,6 +63,71 @@ function safelyGenerateTokens(selector) {
}
}

function matchAttributeSelector(node, token) {
return nodeHasMatchingProperty(node, token.name, (nodePropValue, nodeProps) => {
const { operator, value } = token;
if (token.type === ATTRIBUTE_PRESENCE) {
return Object.prototype.hasOwnProperty.call(nodeProps, token.name);
}
// Only the exact value operator ("=") can match non-strings
if (typeof nodePropValue !== 'string' && operator !== EXACT_ATTRIBUTE_OPERATOR) {
return false;
}
switch (operator) {
/**
* Represents an element with the att attribute whose value is exactly "val".
* @example
* [attr="val"] matches attr="val"
*/
case EXACT_ATTRIBUTE_OPERATOR:
return is(nodePropValue, value);
/**
* Represents an element with the att attribute whose value is a whitespace-separated
* list of words, one of which is exactly
* @example
* [rel~="copyright"] matches rel="copyright other"
*/
case WHITELIST_ATTRIBUTE_OPERATOR:
return nodePropValue.split(' ').indexOf(value) !== -1;
/**
* Represents an element with the att attribute, its value either being exactly the
* value or beginning with the value immediately followed by "-"
* @example
* [hreflang|="en"] matches hreflang="en-US"
*/
case HYPEN_ATTRIBUTE_OPERATOR:
return nodePropValue === value || nodePropValue.startsWith(`${value}-`);
/**
* Represents an element with the att attribute whose value begins with the prefix value.
* If the value is the empty string then the selector does not represent anything.
* @example
* [type^="image"] matches type="imageobject"
*/
case PREFIX_ATTRIBUTE_OPERATOR:
return value === '' ? false : nodePropValue.substr(0, value.length) === value;
/**
* Represents an element with the att attribute whose value ends with the suffix value.
* If the value is the empty string then the selector does not represent anything.
* @example
* [type^="image"] matches type="imageobject"
*/
case SUFFIX_ATTRIBUTE_OPERATOR:
return value === '' ? false : nodePropValue.substr(0, -value.length) === value;
/**
* Represents an element with the att attribute whose value contains at least one
* instance of the value. If value is the empty string then the
* selector does not represent anything.
* @example
* [title*="hello"] matches title="well hello there"
*/
case SUBSTRING_ATTRIBUTE_OPERATOR:
return value === '' ? false : nodePropValue.indexOf(value) !== -1;
default:
throw new Error(`Enzyme::Selector: Unknown attribute selector operator "${operator}"`);
}
});
}

/**
* Takes a node and a token and determines if the node
* matches the predicate defined by the token.
Expand Down Expand Up @@ -90,14 +163,14 @@ function nodeMatchesToken(node, token) {
* @example '[disabled]' matches <a disabled />
*/
case ATTRIBUTE_PRESENCE:
return nodeHasProperty(node, token.name);
return matchAttributeSelector(node, token);
/**
* Matches if an attribute is present with the
* provided value
* @example '[data-foo=foo]' matches <div data-foo="foo" />
*/
case ATTRIBUTE_VALUE:
return nodeHasProperty(node, token.name, token.value);
return matchAttributeSelector(node, token);
case PSEUDO_ELEMENT:
case PSEUDO_CLASS:
throw new Error('Enzyme::Selector does not support psuedo-element or psuedo-class selectors.');
Expand Down

0 comments on commit 4c19481

Please sign in to comment.