Skip to content
This repository has been archived by the owner on Sep 20, 2018. It is now read-only.

Commit

Permalink
[added] much better selector support
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Sep 28, 2015
1 parent 113797b commit 386ef82
Show file tree
Hide file tree
Showing 11 changed files with 577 additions and 226 deletions.
42 changes: 34 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,42 @@ React TestUtils utils

A simple jquery like api wrapper for the React TestUtils to make them a bit friendlier to use.

Updates for react 0.14, works with Stateless Components and you can scry and filter on DOM components
Updated for react 0.14; works seamlessly with Stateless Components and you can find and filter on DOM components
as well.

### using selectors

The selector syntax is subset of normal css selectors. You can query by tag: `'div > li'` or
by `className` with `'.my-class'`. Attribute selectors work on props: `'[show=true]'` or `'[name="my-input"]'`.
You can even use the `has()` pseudo selector for selecting parents.

Unlike normal css selectors though, React Elements often have prop values, and element types that are not serializable
to a nice string. What if you needed to select a `MyList` component by its "tag" or wanted to get all elements with
a `date` prop equal to today?

To write selectors for these values we use an es6 tagged template string! Both the DOM and shallow rendering
imports expose a `$.selector` (also aliased as `$.s`) for writing complex selectors like so:

```
//select all `<MyList/>`s that are children of divs
$.s`div > ${List}`
//select components with `start` props equal to `min`
let min = 10
$.s`[start=${10}]`
```

### Traditional DOM rendering

```js
var $r = require('react-testutil-query')

var elements = (
<MyComponent>
<MyInput/>
<div className='fun-div'>
<MyInput/>
</MyComponent>
<MyComponent>
<MyInput/>
<div className='fun-div'>
<MyInput/>
</MyComponent>
)

var $root = $r(elements) // renders and returns a wrapped instance
Expand All @@ -27,8 +49,13 @@ $r($root[0]) // |
//-- simple selector syntax --
$root.find('.fun-div') //class
$root.find('div') // tag name

$root.find(MyInput) // component type

// complex selectors
$root.find('div.foo > span:has(div.bar)')
$root.find($.s`${MyList} > li.foo`)

$root.find(':dom') // all dom nodes
$root.find(':composite') // all non DOM components

Expand Down Expand Up @@ -61,7 +88,7 @@ $root.find(MyInput).trigger('change', { target: { value: 6 }}) // triggers onCha

### Shallow rendering

We can use an even more powerful selector syntax will shallow rendering
To query shallow rendered Components use the `'react-testutil-query/shallow'` import

```js
var $ = require('react-testutil-query/shallow');
Expand Down Expand Up @@ -89,7 +116,6 @@ $root.find('.my-list').children('.foo').length // 2

$root.find('div li[aria-label="list item"]').length // 1

// you can even use es6 template strings to write
// selectors for your custom components
$root.find($.s`${BasicList} > li.foo`).length // 2

Expand Down
106 changes: 106 additions & 0 deletions lib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
React TestUtils utils
========

A simple jquery like api wrapper for the React TestUtils to make them a bit friendlier to use.

Updates for react 0.14, works with Stateless Components and you can scry and filter on DOM components
as well.

### Traditional DOM rendering

```js
var $r = require('react-testutil-query')

var elements = (
<MyComponent>
<MyInput/>
<div className='fun-div'>
<MyInput/>
</MyComponent>
)

var $root = $r(elements) // renders and returns a wrapped instance

$r($root) // | calling it again won't rerender or rewrap
$r($root[0]) // |

//-- simple selector syntax --
$root.find('.fun-div') //class
$root.find('div') // tag name
$root.find(MyInput) // component type

$root.find(':dom') // all dom nodes
$root.find(':composite') // all non DOM components

$root.find() // everything! all descendents

//-- like jquery you get an arraylike thing
$root.find(MyInput).length // 2

$root.find(MyInput).each( (component, idx) => /*do something */)

// use the index or `get()` to unwrap the collection into a single component or real array
$root.find('.fun-div')[0]


$root.find(MyInput).first()
$root.find(MyInput).last()

// you can still get the implicit asserts for finding single components
$root.find('.fun-div').only() // throws a TypeError .length === 0
$root.single('.fun-div') // is the same thing


// -- getting DOM nodes
$root.single('.fun-div').dom() // returns the single DOM node
$root.find(MyInput).dom() //returns an array of DOM nodes

// -- events
$root.find(MyInput).trigger('change', { target: { value: 6 }}) // triggers onChange for all of them
```

### Shallow rendering

We can use an even more powerful selector syntax will shallow rendering

```js
var $ = require('react-testutil-query/shallow');

let label = 'list item';

let BasicList = props => <ul>{props.children}</ul>

let DivList = ()=> (
<div>
<BasicList className='my-list'>
<li className='foo'>hi 1</li>
<li className='foo'>hi 2</li>
<li aria-label={label}>hi 3</li>
</BasicList>
</div>
)


let $root = $(<DivList);

$root.find('.my-list > li.foo').length // 2

$root.find('.my-list').children('.foo').length // 2

$root.find('div li[aria-label="list item"]').length // 1

// you can even use es6 template strings to write
// selectors for your custom components
$root.find($.s`${BasicList} > li.foo`).length // 2

//or for prop values
$root.find($.s`li[aria-label=${label}]`).length // 1

$root.find(BasicList)
.children()
.filter(element => element.props.className === 'foo')
.length // 2

$root.find(BasicList).is('.my-list').length // true

```
30 changes: 30 additions & 0 deletions lib/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "react-testutil-query",
"version": "2.2.0",
"description": "small wrapper around react test utils so I don't have to write long method names",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/jquense/react-testutil-query"
},
"keywords": [
"react",
"test",
"query"
],
"author": "jquense",
"license": "MIT",
"bugs": {
"url": "https://github.com/jquense/react-testutil-query/issues"
},
"homepage": "https://github.com/jquense/react-testutil-query",
"peerDependencies": {
"react": ">=0.14.0-rc1",
"react-dom": ">=0.14.0-rc1"
},
"dependencies": {
"bill": "^1.0.4",
"dom-helpers": "^2.4.0",
"react-addons-test-utils": "^0.14.0-rc1"
}
}
155 changes: 155 additions & 0 deletions lib/shallow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
'use strict';

exports.__esModule = true;

var _templateObject = _taggedTemplateLiteralLoose(['', ''], ['', '']);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

function _taggedTemplateLiteralLoose(strings, raw) { strings.raw = raw; return strings; }

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

var _reactAddonsTestUtils = require('react-addons-test-utils');

var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils);

var _bill = require('bill');

var isRtq = function isRtq(item) {
return item && item.__isRTQ;
};

rtq.s = rtq.selector = _bill.selector;

exports['default'] = rtq;

function match(selector, tree, includeSelf) {
if (typeof selector === 'function') selector = _bill.selector(_templateObject, selector);

return _bill.match(selector, tree, includeSelf);
}

function render(element) {
var root = element;

if (!(typeof root.type === 'string' && root.type.toLowerCase() === root.type)) {
var renderer = _reactAddonsTestUtils2['default'].createRenderer();
renderer.render(element);
root = renderer.getRenderOutput();
}

return {
root: root,
setProps: function setProps(props) {
return render(_react.cloneElement(element, props));
}
};
}

function rtq(element) {
var context, rerender;

if (_reactAddonsTestUtils2['default'].isElement(element)) {
var _render = render(element);

var root = _render.root;
var setProps = _render.setProps;

element = context = root;
rerender = setProps;
} else if (isRtq(element)) {
context = element.root;
element = element.get();
//rerender = element._rerender
}

return new ShallowCollection(element, context, rerender);
}

var ShallowCollection = (function () {
function ShallowCollection(elements, root, rerender) {
_classCallCheck(this, ShallowCollection);

elements = [].concat(elements).filter(function (el) {
return _react.isValidElement(el);
});

var idx = -1;

while (++idx < elements.length) this[idx] = elements[idx];

this._rerender = rerender;
this.length = elements.length;
this.root = root;
}

ShallowCollection.prototype.setProps = function setProps(props) {
this._rerender && this._rerender(props);
return this;
};

ShallowCollection.prototype.each = function each(cb) {
var idx = -1,
len = this.length;
while (++idx < len) cb(this[idx], idx, this);
return this;
};

ShallowCollection.prototype.get = function get() {
var result = [];
this.each(function (el) {
return result.push(el);
});
return result;
};

ShallowCollection.prototype.reduce = function reduce(cb, initial) {
return new ShallowCollection([].reduce.call(this, cb, initial), this.root);
};

ShallowCollection.prototype.map = function map(cb) {
var result = [];
this.each(function (v, i, l) {
return result.push(cb(v, i, l));
});

return new ShallowCollection(result, this.root);
};

ShallowCollection.prototype.find = function find(selector) {
return this.reduce(function (result, element) {
return result.concat(match(selector, element));
}, []);
};

ShallowCollection.prototype.children = function children(selector) {
return this.reduce(function (result, element) {
return result.concat(element.props.children || []);
}, []).filter(selector);
};

ShallowCollection.prototype.filter = function filter(selector) {
if (!selector) return this;

if (typeof selector === 'function') return new ShallowCollection([].filter.call(this, selector), this.root);

var matches = match(selector, this.root);

return new ShallowCollection([].filter.call(this, function (el) {
return matches.indexOf(el) !== -1;
}), this.root);
};

ShallowCollection.prototype.is = function is(selector) {
return this.filter(selector).length === this.length;
};

return ShallowCollection;
})();

module.exports = exports['default'];
Loading

0 comments on commit 386ef82

Please sign in to comment.