From ecfc1de6301aa3594bc42dbc6d9785b7cd8c6f21 Mon Sep 17 00:00:00 2001 From: jquense Date: Fri, 9 Oct 2015 20:32:58 -0400 Subject: [PATCH] [added] instance selector --- README.md | 61 ++++++++- karma.conf.js | 6 +- lib/compiler.js | 55 ++++++-- lib/package.json | 2 +- lib/select.js | 73 ++-------- package.json | 5 +- src/{select.js => element-selector.js} | 37 ++--- src/index.js | 14 +- src/instance-selector.js | 78 +++++++++++ src/utils.js | 21 +++ test.js | 183 ------------------------- test/compiler.js | 132 ++++++++++++++++++ test/index.js | 6 + test/selection.js | 132 ++++++++++++++++++ 14 files changed, 512 insertions(+), 293 deletions(-) rename src/{select.js => element-selector.js} (60%) create mode 100644 src/instance-selector.js create mode 100644 src/utils.js delete mode 100644 test.js create mode 100644 test/compiler.js create mode 100644 test/index.js create mode 100644 test/selection.js diff --git a/README.md b/README.md index 0faa4ff..f2777ef 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ bill ======= -A set of tools for matching React Elements against CSS selectors. +A set of tools for matching React Elements against CSS selectors, or easily creating new ways to match react components. +against css selectors. + +`bill` is meant to be a substrate library for building more interesting and user friendly testing utilities. +It probably shouldn't be used as a standalone tool. ```js import { match } from 'bill'; @@ -16,7 +20,7 @@ let matches = match('div li.foo' ) matches.length // 1 -matches[0] // { type: 'li', props: { className: 'foo' } } +matches[0] // ReactElement{ type: 'li', props: { className: 'foo' } } ``` For selecting non string values, like custom Component types, we can use a [tagged template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings) @@ -51,3 +55,56 @@ matches[0] // { type: List, props } - sibling selectors - pseudo selectors (except for has) - non string interpolations for anything other than "tag" or prop values + + +## API + +### `match(selector, elementOrInstance) -> array` + +`bill` will match against either a plain old ReactElement in which case it will walk `props.children`, +or if you provide it with a component instance, it will match against the entire rendered tree. + +__note:__ matching instances returns __private__ component instances not the normal instances you are used to +working with. This is because DOM and Stateless components do not have public instances that can be further traversed. +To get the normal instances you are used to call `.getPubliceInstance()` on each match. + +```js +let matches = match('div li.foo' +
+ +
  • John
  • +
  • Betty
  • +
    +
    +) +``` + +Or with a rendered instance + +```js +let root = ReactDOM.render(
    + +
  • John
  • +
  • Betty
  • +
    +
    , document.body) + +let matches = match('div li.foo', root) + +``` + +### `selector() -> Selector` + +A function used for tagged template strings, + +```js +selector`div > .foo` +``` + +You really only need to use the `selector` function when you want to write a selector matching exact prop values or a +composite type. + + +```js +selector`div > ${List}[length=${5}]` +``` diff --git a/karma.conf.js b/karma.conf.js index b06f738..e48e233 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,7 +9,7 @@ module.exports = function (config) { reporters: ['mocha'], files: [ - 'test.js' + 'test/index.js' ], port: 9876, @@ -22,11 +22,11 @@ module.exports = function (config) { browsers: ['Chrome'], preprocessors: { - 'test.js': ['webpack'] + 'test/index.js': ['webpack'] }, webpack: { - entry: './test.js', + entry: 'test/index.js', module: { loaders: [{ test: /\.js$/, loader: 'babel', exclude: /node_modules/ }] } diff --git a/lib/compiler.js b/lib/compiler.js index 6ac118b..8339234 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -24,14 +24,8 @@ var _lodashUtilityUniqueId2 = _interopRequireDefault(_lodashUtilityUniqueId); var _cssSelectorParser = require('css-selector-parser'); -var PREFIX = 'sub_____'; - var parser = new _cssSelectorParser.CssSelectorParser(); -parser.registerSelectorPseudos('has'); -parser.registerNestingOperators('>'); -parser.enableSubstitutes(); - var prim = function prim(value) { var typ = typeof value; return value === null || ['string', 'number'].indexOf(typ) !== -1; @@ -59,19 +53,27 @@ function parse(selector) { } } -function create(options) { +function create() { + var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + var NESTING = Object.create(null); var PSEUDOS = Object.create(null); + var PREFIX = options.prefix || 'sub_____'; var traverse = options.traverse; return { compile: compile, compileRule: compileRule, + selector: selector, + registerNesting: function registerNesting(name, fn) { + if (name !== 'any') parser.registerNestingOperators(name); NESTING[name] = fn; }, + registerPseudo: function registerPseudo(name, fn) { + parser.registerSelectorPseudos(name); PSEUDOS[name] = fn; } }; @@ -79,6 +81,11 @@ function create(options) { function compile(selector) { var values = arguments.length <= 1 || arguments[1] === undefined ? Object.create(null) : arguments[1]; + if (selector.selector) { + values = selector.valueMap; + selector = selector.selector; + } + var _parse = parse(selector); var rules = _parse.rules; @@ -114,8 +121,11 @@ function create(options) { if (rule.pseudos) { fns = fns.concat(rule.pseudos.map(function (pseudo) { - if (!PSEUDOS[pseudo.name]) throw new Error('psuedo element: ' + psuedo.name + ' is not supported'); - return PSEUDOS[pseudo.name](pseudo, values, options); + if (!PSEUDOS[pseudo.name]) throw new Error('psuedo element: ' + pseudo.name + ' is not supported'); + + var pseudoCompiled = pseudo.valueType === 'selector' ? compile(pseudo.value, values) : pseudo; + + return PSEUDOS[pseudo.name](pseudoCompiled, values, options); })); } @@ -133,11 +143,34 @@ function create(options) { return true; } : arguments[1]; - return function (root, parent) { - return next(root, parent) && current(root, parent); + return function () { + return next.apply(undefined, arguments) && current.apply(undefined, arguments); }; }); } + + function selector(strings) { + for (var _len = arguments.length, values = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + values[_key - 1] = arguments[_key]; + } + + var valueMap = Object.create(null); + + var selector = strings.reduce(function (rslt, string, idx) { + var noValue = idx >= values.length, + value = values[idx], + strValue = '' + value; + + if (!noValue && !prim(value)) valueMap[strValue = PREFIX + _lodashUtilityUniqueId2['default']()] = value; + + return rslt + string + (noValue ? '' : strValue); + }, ''); + + return { + selector: selector, + valueMap: valueMap + }; + } } function getTagComparer(rule, values) { diff --git a/lib/package.json b/lib/package.json index 9c9d507..71ad8dc 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,6 +1,6 @@ { "name": "bill", - "version": "1.0.5", + "version": "1.1.0", "description": "css selectors for React Elements", "main": "index.js", "repository": { diff --git a/lib/select.js b/lib/select.js index cb398eb..6f691d3 100644 --- a/lib/select.js +++ b/lib/select.js @@ -1,7 +1,6 @@ 'use strict'; exports.__esModule = true; -exports.selector = selector; exports.match = match; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } @@ -18,34 +17,13 @@ var _lodashObjectHas = require('lodash/object/has'); var _lodashObjectHas2 = _interopRequireDefault(_lodashObjectHas); -var _lodashUtilityUniqueId = require('lodash/utility/uniqueId'); - -var _lodashUtilityUniqueId2 = _interopRequireDefault(_lodashUtilityUniqueId); - -var _cssSelectorParser = require('css-selector-parser'); - var _compiler = require('./compiler'); -var PREFIX = 'sub_____'; - -var parser = new _cssSelectorParser.CssSelectorParser(); - -parser.registerSelectorPseudos('has'); -parser.registerNestingOperators('>'); -parser.enableSubstitutes(); - -var prim = function prim(value) { - var typ = typeof value; - return value === null || ['string', 'number'].indexOf(typ) !== -1; -}; - -var compiler = _compiler.create({}); - -compiler.registerPseudo('has', function (rule, valueMap) { - var compiled = compiler.compile(rule.value, valueMap); +var compiler = _compiler.create(); +compiler.registerPseudo('has', function (compiledSelector) { return function (root) { - var matches = findAll(root, compiled); + var matches = findAll(root, compiledSelector); return !!matches.length; }; }); @@ -53,48 +31,14 @@ compiler.registerPseudo('has', function (rule, valueMap) { compiler.registerNesting('any', function (test) { return anyParent.bind(null, test); }); - compiler.registerNesting('>', function (test) { return directParent.bind(null, test); }); -function selector(strings) { - for (var _len = arguments.length, values = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - values[_key - 1] = arguments[_key]; - } - - var valueMap = Object.create(null); - - var selector = strings.reduce(function (rslt, string, idx) { - var noValue = idx >= values.length, - value = values[idx], - strValue = '' + value; - - if (!noValue && !prim(value)) valueMap[strValue = PREFIX + _lodashUtilityUniqueId2['default']()] = value; - - return rslt + string + (noValue ? '' : strValue); - }, ''); - - return { - selector: selector, - valueMap: valueMap - }; -} - function match(selector, tree) { var includeSelf = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2]; - var valueMap = Object.create(null); - - if (selector.selector) { - valueMap = selector.valueMap; - selector = selector.selector; - } - - var compiled = compiler.compile(selector, valueMap); - var matches = findAll(tree, compiled, undefined, includeSelf); - - return matches; + return findAll(tree, compiler.compile(selector), undefined, includeSelf); } function findAll(root, test, getParent, includeSelf) { @@ -128,10 +72,7 @@ function findAll(root, test, getParent, includeSelf) { } function anyParent(test, node, parentNode) { - var i = 0; do { - i++; - var _parentNode = parentNode(); var getParent = _parentNode.getParent; @@ -139,7 +80,7 @@ function anyParent(test, node, parentNode) { node = parent; parentNode = getParent; - } while (i < 100 && node && !test(node, test, getParent)); + } while (node && !test(node, test, getParent)); return !!node; } @@ -151,5 +92,7 @@ function directParent(test, node, parentNode) { var compile = compiler.compile; var compileRule = compiler.compileRule; +var selector = compiler.selector; exports.compile = compile; -exports.compileRule = compileRule; \ No newline at end of file +exports.compileRule = compileRule; +exports.selector = selector; \ No newline at end of file diff --git a/package.json b/package.json index e2445c4..fa05f08 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "homepage": "https://github.com/jquense/bill", "peerDependencies": { - "react": ">=0.13.0 ||^0.14.0-alpha1" + "react": ">=0.13.0 || ^0.14.0-alpha1" }, "devDependencies": { "babel-core": "^5.8.25", @@ -42,7 +42,8 @@ "karma-webpack": "^1.7.0", "mocha": "^2.3.3", "mt-changelog": "^0.6.2", - "react-dom": "^0.14.0-rc1", + "react": "^0.14.0", + "react-dom": "^0.14.0", "release-script": "^0.5.3", "sinon": "^1.17.1", "sinon-chai": "^2.8.0", diff --git a/src/select.js b/src/element-selector.js similarity index 60% rename from src/select.js rename to src/element-selector.js index e990a60..56a1c8f 100644 --- a/src/select.js +++ b/src/element-selector.js @@ -2,8 +2,9 @@ import React from 'react'; import transform from 'lodash/object/transform'; import has from 'lodash/object/has'; import { create as createCompiler, parse } from './compiler'; +import { anyParent, directParent, isDomElement, isCompositeElement } from './utils'; -let compiler = createCompiler() +export let compiler = createCompiler() compiler.registerPseudo('has', function(compiledSelector) { return root => { @@ -12,14 +13,19 @@ compiler.registerPseudo('has', function(compiledSelector) { } }) +compiler.registerPseudo('dom', isDomElement) +compiler.registerPseudo('composite', isCompositeElement) + + compiler.registerNesting('any', test => anyParent.bind(null, test)) + compiler.registerNesting('>', test => directParent.bind(null, test)) export function match(selector, tree, includeSelf = true){ - return findAll(tree, compiler.compile(selector), undefined, includeSelf) + return findAll(tree, compiler.compile(selector), includeSelf) } -function findAll(root, test, getParent = ()=> ({ parent: null }), includeSelf){ +function findAll(root, test, includeSelf, getParent = ()=> ({ parent: null })) { let found = []; if (!React.isValidElement(root)) @@ -27,12 +33,12 @@ function findAll(root, test, getParent = ()=> ({ parent: null }), includeSelf){ let children = root.props.children - if (React.Children.count(children) === 0) - return found - if (includeSelf && test(root, getParent)) found.push(root); + if (React.Children.count(children) === 0) + return found + React.Children.forEach(children, child => { let parent = ()=> ({ parent: root, getParent }); @@ -40,26 +46,9 @@ function findAll(root, test, getParent = ()=> ({ parent: null }), includeSelf){ if (test(child, parent)) found.push(child); - found = found.concat(findAll(child, test, parent, false)) + found = found.concat(findAll(child, test, false, parent)) } }) return found } - -function anyParent(test, node, parentNode){ - do { - var { getParent, parent } = parentNode(); - node = parent - parentNode = getParent - } while(node && !test(node, test, getParent)) - - return !!node -} - -function directParent(test, node, parentNode) { - node = parentNode().parent - return !!(node && test(node, parentNode().getParent)) -} - -export let { compile, compileRule, selector } = compiler diff --git a/src/index.js b/src/index.js index 2f6e4a9..ef3ca32 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,15 @@ -import { match, selector } from './select'; +import { isValidElement } from 'react'; +import * as elements from './element-selector'; +import * as instance from './instance-selector'; + +function match(selector, element){ + if (isValidElement(element)) + return elements.match(selector, element) + + return instance.match(selector, element) +} module.exports = { - match, selector + match, + selector: elements.compiler.selector } diff --git a/src/instance-selector.js b/src/instance-selector.js new file mode 100644 index 0000000..3eebf04 --- /dev/null +++ b/src/instance-selector.js @@ -0,0 +1,78 @@ +import ReactInstanceMap from 'react/lib/ReactInstanceMap'; +import { create as createCompiler, parse } from './compiler'; +import { + anyParent, directParent + , isDomElement, isCompositeElement } from './utils'; + + +let isDOMComponent = inst => !!(inst && inst.nodeType === 1 && inst.tagName); + +let isCompositeComponent = inst => !isDOMComponent(inst) || inst === null + || typeof inst.render === 'function' && typeof inst.setState === 'function'; + + +export let compiler = createCompiler() + +compiler.registerPseudo('has', function(compiledSelector) { + return (_, inst) => { + let matches = findAll(inst, compiledSelector) + return !!matches.length + } +}) + +compiler.registerPseudo('dom', isDomElement) +compiler.registerPseudo('composite', isCompositeElement) + +compiler.registerNesting('any', test => + (element, inst, parent) => anyParent(test, element, parent)) + +compiler.registerNesting('>', test => + (element, inst, parent) => directParent(test, element, parent)) + + +function findAll(inst, test, getParent = ()=> ({ parent: null }), excludeSelf = true) { + let found = []; + + if (!inst || !inst.getPublicInstance) + return found; + + let publicInst = inst.getPublicInstance() + , element = inst._currentElement + , parent = ()=> ({ parent: element, getParent }); + + if (!excludeSelf && test(element, inst, getParent)) + found = found.concat(inst) + + if (isDOMComponent(publicInst)) { + let renderedChildren = inst._renderedChildren || {}; + + Object.keys(renderedChildren).forEach(key => { + found = found.concat( + findAll(renderedChildren[key], test, parent, false) + ); + }) + } + else if (isCompositeComponent(publicInst)) { + found = found.concat( + findAll(inst._renderedComponent, test, parent, false) + ); + } + + return found; +} + +/** + * The matcher actually works on internal instances, not public ones + * since DOM and stateless components don't have helpful public instances + */ +export function match(selector, inst, includeSelf = true) { + let tree = inst.getPublicInstance + ? inst //already a private instance + : inst._reactInternalComponent //is a DOM node + ? inst._reactInternalComponent + : ReactInstanceMap.get(inst) + + return findAll(tree, compiler.compile(selector), undefined, !includeSelf) +} + +export let { compile, compileRule, selector } = compiler diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..2acb648 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,21 @@ + +export let isDomElement = + element => typeof element.type === 'string' && element.type.toLowerCase() === element.type + +export let isCompositeElement = + element => typeof element.type === 'function' + +export function anyParent(test, element, parentNode){ + do { + var { getParent, parent } = parentNode(); + element = parent + parentNode = getParent + } while(element && !test(element, test, getParent)) + + return !!element +} + +export function directParent(test, element, parentNode) { + element = parentNode().parent + return !!(element && test(element, parentNode().getParent)) +} diff --git a/test.js b/test.js deleted file mode 100644 index e728af2..0000000 --- a/test.js +++ /dev/null @@ -1,183 +0,0 @@ -import React from 'react'; -import { _parser, compile, match, selector as s } from './src/select'; - -chai.use(require('sinon-chai')) - -describe('Element Selecting', ()=> { - - describe('compiling selectors', ()=> { - - it('should return a function', ()=>{ - let result = compile('a.foo') - - result.should.be.a('function') - }) - - it('should match object', ()=>{ - let result = compile('a.foo') - - result({ - type: 'a', - props: { - className: 'foo bar' - } - }).should.equal(true) - }) - - it('should fail when not a match', ()=>{ - let result = compile('a.foo') - - result({ - type: 'div', - props: { - className: 'foo bar' - } - }).should.equal(false) - }) - - it('should match props', ()=>{ - let result = compile('[foo="5"]') - - result({ - props: { - foo: 5 - } - }).should.equal(true) - }) - - it('should match bool props', ()=>{ - let result = compile('[foo]') - - result({ - props: { - foo: true - } - }).should.equal(true) - }) - - it('should match multiple selectors', ()=>{ - let result = compile('[foo], div') - - result({ - type: 'div', - props: { - foo: false - } - }).should.equal(true) - }) - - it('should fail when multiple selectors do not match', ()=>{ - let result = compile('[foo], div') - - result({ - type: 'a', - props: { - foo: false - } - }).should.equal(false) - }) - - it('should match nested', ()=>{ - match('div a.foo', -
    - - - - -
    - ).length.should.equal(2) - }) - - it('should work with :has()', ()=> { - match('div:has(a.foo, a[show])', -
    - - - - -
    - ).length.should.equal(1) - }) - - it('should match nested attributes', ()=>{ - match('div a.foo[show]', -
    - - - - -
    - ).length.should.equal(1) - }) - - it('should match direct descendents', ()=>{ - match('div > a.foo', -
    - - - - -
    - ).length.should.equal(1) - }) - - it('should create valid selector with substitutions', ()=>{ - let List = ()=>{}; - let { selector, valueMap } = s`div ${List}.foo`; - - ;(() => compile(selector)).should.not.throw() - - selector.match(/^div sub_____\d\.foo$/).should.be.ok - }) - - it('should use primitive value instead of placeholder', ()=>{ - let List = 'span'; - let { selector, valueMap } = s`div ${List}.foo`; - - selector.should.equal('div span.foo') - expect(valueMap.span).to.not.exist - - ;({ selector, valueMap } = s`div ${5}.foo`); - - selector.should.equal('div 5.foo') - expect(valueMap['5']).to.not.exist - }) - - it('should match with tag substitions', ()=>{ - let List = ()=>
    ; - - match(s`div ${List}.foo`, -
    - - - - -
    - ).length.should.equal(1) - }) - - it('should match with nested tag substitutions', ()=>{ - let List = ()=>
    ; - - match(s`${List}.foo > span`, -
    - - - -
    - ).length.should.equal(1) - }) - - it('should match with prop value substitutions', ()=>{ - let date = new Date(); - - match(s`div[date=${date}].foo`, -
    - - - -
    - ).length.should.equal(1) - }) - }) -}) diff --git a/test/compiler.js b/test/compiler.js new file mode 100644 index 0000000..695e530 --- /dev/null +++ b/test/compiler.js @@ -0,0 +1,132 @@ +import React from 'react'; +import { create } from '../src/compiler'; + +let { compile, selector: s } = create() + +chai.use(require('sinon-chai')) + +describe('create compiler', ()=> { + + it('should return a function', ()=>{ + let result = compile('a.foo') + + result.should.be.a('function') + }) + + it('should match object', ()=>{ + let result = compile('a.foo') + + result({ + type: 'a', + props: { + className: 'foo bar' + } + }).should.equal(true) + }) + + it('should fail when not a match', ()=>{ + let result = compile('a.foo') + + result({ + type: 'div', + props: { + className: 'foo bar' + } + }).should.equal(false) + }) + + it('should match props', ()=>{ + let result = compile('[foo="5"]') + + result({ + props: { + foo: 5 + } + }).should.equal(true) + }) + + it('should match bool props', ()=>{ + let result = compile('[foo]') + + result({ + props: { + foo: true + } + }).should.equal(true) + }) + + it('should match multiple selectors', ()=>{ + let result = compile('[foo], div') + + result({ + type: 'div', + props: { + foo: false + } + }).should.equal(true) + }) + + it('should fail when multiple selectors do not match', ()=>{ + let result = compile('[foo], div') + + result({ + type: 'a', + props: { + foo: false + } + }).should.equal(false) + }) + + it('should return a selector function', ()=>{ + s.should.be.a('function') + }) + + it('should accept a selector template result', ()=>{ + let result = compile(s`a[foo=${false}]`) + + result({ + type: 'a', + props: { + foo: false + } + }).should.equal(true) + }) + + it('should create valid selector with substitutions', ()=>{ + let List = ()=>{}; + let { selector, valueMap } = s`${List}.foo`; + + ;(() => compile(selector)).should.not.throw() + + selector.match(/^sub_____\d\.foo$/).should.be.ok + }) + + it('should use == on non interpolated values', ()=>{ + let result = compile(s`a[foo=false]`) + + result({ + type: 'a', + props: { + foo: 'false' + } + }).should.equal(true) + }) + + it('should use === on interpolated values', ()=>{ + let result = compile(s`a[foo=${false}]`) + + result({ type: 'a', props: { foo: 'false'} }).should.equal(false) + + result({ type: 'a', props: { foo: false } }).should.equal(true) + }) + + it('should match interpolated tagName', ()=>{ + let Klass = ()=>{} + let result = compile(s`${Klass}.foo`) + + result({ + type: Klass, + props: { className: 'foo' } + }).should.equal(true) + }) +}) diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..8126d32 --- /dev/null +++ b/test/index.js @@ -0,0 +1,6 @@ + +chai.use(require('sinon-chai')) + +const testsContext = require.context('.', true, /^((?!index).)*$/); + +testsContext.keys().forEach(testsContext); diff --git a/test/selection.js b/test/selection.js new file mode 100644 index 0000000..cf8586e --- /dev/null +++ b/test/selection.js @@ -0,0 +1,132 @@ +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom' +import bill from '../src/index'; +import each from 'lodash/collection/each'; + +describe('Selecting', ()=> { + let mountPoint; + + let types = { + element: bill, + instance: { + ...bill, + match(selector, root){ + return bill.match(selector, render(root, mountPoint)) + }, + + beforeEach(){ + mountPoint = document.createElement('div') + } + } + }; + + each(types, (details, key) => { + let { match, selector: s } = details; + + describe(key, ()=> { + + details.beforeEach && + beforeEach(details.beforeEach) + + details.afterEach && + beforeEach(details.afterEach) + + it('should match nested', ()=>{ + match('div a.foo', +
    + + + + +
    + ).length.should.equal(2) + }) + + it('should work with :has()', ()=> { + match('div:has(a.foo, a[show])', +
    + + + + +
    + ).length.should.equal(1) + }) + + it('should match nested attributes', ()=>{ + match('div a.foo[show]', +
    + + + + +
    + ).length.should.equal(1) + }) + + it('should match direct descendents', ()=>{ + match('div > a.foo', +
    + + + + +
    + ).length.should.equal(1) + }) + + + it('should use primitive value instead of placeholder', ()=>{ + let List = 'span'; + let { selector, valueMap } = s`div ${List}.foo`; + + selector.should.equal('div span.foo') + expect(valueMap.span).to.not.exist + + ;({ selector, valueMap } = s`div ${5}.foo`); + + selector.should.equal('div 5.foo') + expect(valueMap['5']).to.not.exist + }) + + it('should match with tag substitions', ()=>{ + let List = ()=>
    ; + + match(s`div ${List}.foo`, + + ).length.should.equal(1) + }) + + it('should match with nested tag substitutions', ()=>{ + let List = ({ children })=> children; + + match(s`${List}.foo > span`, +
    + + + +
    + ).length.should.equal(1) + }) + + it('should match with prop value substitutions', ()=>{ + let date = new Date(); + + match(s`div[date=${date}].foo`, +
    + ).length.should.equal(1) + }) + }) + }) + + +})