import { createParser } from 'rst-selector-parser'; import values from 'object.values'; import flat from 'array.prototype.flat'; import is from 'object-is'; import has from 'has'; import elementsByConstructor from 'html-element-map/byConstructor'; import { treeFilter, nodeHasId, findParentNode, nodeMatchesObjectProps, childrenOfNode, hasClassName, } from './RSTTraversal'; import { nodeHasType, propsOfNode } from './Utils'; import getAdapter from './getAdapter'; // 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 UNIVERSAL_SELECTOR = 'universalSelector'; 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'; const EXACT_ATTRIBUTE_OPERATOR = '='; const WHITELIST_ATTRIBUTE_OPERATOR = '~='; const HYPHENATED_ATTRIBUTE_OPERATOR = '|='; const PREFIX_ATTRIBUTE_OPERATOR = '^='; const SUFFIX_ATTRIBUTE_OPERATOR = '$='; const SUBSTRING_ATTRIBUTE_OPERATOR = '*='; function unique(arr) { return [...new Set(arr)]; } /** * 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}`); } } function matchAttributeSelector(node, token) { const { operator, value, name } = token; const nodeProps = propsOfNode(node); const descriptor = Object.getOwnPropertyDescriptor(nodeProps, name); if (descriptor && descriptor.get) { return false; } const nodePropValue = nodeProps[name]; if (typeof nodePropValue === 'undefined') { return false; } if (token.type === ATTRIBUTE_PRESENCE) { return has(nodeProps, token.name); } // Only the exact value operator ("=") can match non-strings if (typeof nodePropValue !== 'string' || typeof value !== 'string') { if (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 HYPHENATED_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.slice(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.slice(-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}"`); } } function matchPseudoSelector(node, token, root) { const { name, parameters } = token; if (name === 'not') { // eslint-disable-next-line no-use-before-define return parameters.every((selector) => reduceTreeBySelector(selector, node).length === 0); } if (name === 'empty') { return treeFilter(node, (n) => n !== node).length === 0; } if (name === 'first-child') { const { rendered } = findParentNode(root, node); const [firstChild] = rendered; return firstChild === node; } if (name === 'last-child') { const { rendered } = findParentNode(root, node); return rendered[rendered.length - 1] === node; } if (name === 'focus') { if (typeof document === 'undefined') { throw new Error('Enzyme::Selector does not support the ":focus" pseudo-element without a global `document`.'); } const adapter = getAdapter(); /* eslint-env browser */ return document.activeElement && adapter.nodeToHostNode(node) === document.activeElement; } throw new TypeError(`Enzyme::Selector does not support the "${token.name}" pseudo-element or pseudo-class selectors.`); } /** * 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, root) { if (node === null || typeof node === 'string') { return false; } switch (token.type) { /** * Match every node * @example '*' matches every node */ case UNIVERSAL_SELECTOR: return true; /** * 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