diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29..69f4e23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +v2.1.0 - Mon, 28 Sep 2015 13:45:13 GMT +-------------------------------------- + +- [df40903](../../commit/df40903) [added] shallow rendering utils + + + diff --git a/README.md b/README.md index 83f14c1..b4728b3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ A simple jquery like api wrapper for the React TestUtils to make them a bit frie 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') @@ -55,5 +57,50 @@ $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 => + +let DivList = ()=> ( +
+ +
  • hi 1
  • +
  • hi 2
  • +
  • hi 3
  • +
    +
    +) + + +let $root = $( 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 ``` diff --git a/karma.conf.js b/karma.conf.js index 8a4c3ab..04de6c5 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,7 +9,7 @@ module.exports = function (config) { reporters: ['mocha'], files: [ - 'test.js' + './test/*.js' ], port: 9876, @@ -19,14 +19,13 @@ module.exports = function (config) { logLevel: config.LOG_INFO, - browsers: ['Chrome'], + browsers: ['Chrome'], preprocessors: { - 'test.js': ['webpack'] + 'test/*.js': ['webpack'] }, webpack: { - entry: './test.js', module: { loaders: [{ test: /\.js$/, loader: 'babel', exclude: /node_modules/ }] } diff --git a/package.json b/package.json index 5c3f81b..5815e47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-testutil-query", - "version": "2.0.0", + "version": "2.1.0", "description": "small wrapper around react test utils so I don't have to write long method names", "main": "lib/index.js", "scripts": { @@ -49,10 +49,12 @@ "webpack": "^1.12.2" }, "dependencies": { + "bill": "^1.0.4", "dom-helpers": "^2.4.0", "react-addons-test-utils": "^0.14.0-rc1" }, "release-script": { - "defaultDryRun": "false" + "defaultDryRun": "false", + "altPkgRootFolder": "lib" } } diff --git a/src/shallow.js b/src/shallow.js new file mode 100644 index 0000000..0888373 --- /dev/null +++ b/src/shallow.js @@ -0,0 +1,126 @@ +import React, { cloneElement, isValidElement } from 'react'; +import TestUtils from 'react-addons-test-utils'; +import { match as _match, selector as s } from 'bill'; + +let isRtq = item => item && item.__isRTQ + +rtq.s = rtq.selector = s; + +export default rtq; + +function match(selector, tree, includeSelf){ + if (typeof selector === 'function') + selector = s`${selector}` + + return _match(selector, tree, includeSelf) +} + +function render(element){ + let root = element; + + if (!(typeof root.type === 'string' && root.type.toLowerCase() === root.type)){ + let renderer = TestUtils.createRenderer() + renderer.render(element) + root = renderer.getRenderOutput(); + } + + return { + root, + setProps(props){ + return render(cloneElement(element, props)) + } + } +} + +function rtq(element) { + var context, rerender; + + if (TestUtils.isElement(element)) { + let { root, setProps } = render(element) + element = context = root + rerender = setProps + } + else if (isRtq(element)) { + context = element.root + element = element.get(); + //rerender = element._rerender + } + + return new ShallowCollection(element, context, rerender) +} + +class ShallowCollection { + constructor(elements, root, rerender){ + elements = [].concat(elements).filter(el => isValidElement(el)) + + var idx = -1; + + while( ++idx < elements.length) + this[idx] = elements[idx] + + this._rerender = rerender + this.length = elements.length + this.root = root + } + + setProps(props){ + this._rerender && this._rerender(props) + return this + } + + each(cb) { + var idx = -1, len = this.length; + while (++idx < len) cb(this[idx], idx, this) + return this + } + + get() { + var result = [] + this.each(el => result.push(el)) + return result + } + + reduce(cb, initial){ + return new ShallowCollection( + [].reduce.call(this, cb, initial) + , this.root + ) + } + + map(cb) { + var result = [] + this.each((v, i, l) => result.push(cb(v, i, l))) + + return new ShallowCollection(result, this.root) + } + + find(selector) { + return this.reduce((result, element) => { + return result.concat(match(selector, element)) + }, []) + } + + children(selector) { + return this + .reduce((result, element) => result.concat(element.props.children || []), []) + .filter(selector) + } + + filter(selector) { + if (!selector) + return this + + if (typeof selector === 'function') + return new ShallowCollection([].filter.call(this, selector), this.root) + + let matches = match(selector, this.root); + + return new ShallowCollection([].filter.call(this, el => { + return matches.indexOf(el) !== -1 + }), this.root) + } + + is(selector) { + return this.filter(selector).length === this.length + } +} diff --git a/test.js b/test/dom.js similarity index 99% rename from test.js rename to test/dom.js index 120dced..7e63779 100644 --- a/test.js +++ b/test/dom.js @@ -1,6 +1,6 @@ import React from 'react'; import { unmountComponentAtNode, render } from 'react-dom'; -import $ from './src/index'; +import $ from '../src/index'; chai.use(require('sinon-chai')) diff --git a/test/shallow.js b/test/shallow.js new file mode 100644 index 0000000..1752fb4 --- /dev/null +++ b/test/shallow.js @@ -0,0 +1,110 @@ +import React, { cloneElement } from 'react'; +import $ from '../src/shallow'; +import { selector as sel } from 'bill'; + +chai.use(require('sinon-chai')) + +describe('Shallow rendering', ()=> { + let Stateless = props =>
    {props.children}
    + let List = class extends React.Component { + render(){ + return ( +
    + + + + +
    + ) + } + } + + it('create rtq object', ()=>{ + let instance = $(
    ) + + instance.root.type.should.equal('div') + instance.length.should.equal(1) + }) + + it('should not try to render primitives', ()=>{ + let el =
    + , instance = $(el) + + instance.root.should.equal(el) + }) + + it('should render Composite Components', ()=>{ + let el =
    + , Element = ()=> el + , instance = $() + + instance.root.should.equal(el) + }) + + it('should filter out invalid Elements', ()=>{ + let instance = $( +
      + { false } + { null} + {'text'} +
    • hi 1
    • +
    • hi 2
    • +
    • hi 3
    • +
    + ) + + instance.children().length.should.equal(3) + }) + + describe('querying', ()=> { + let FancyList = props =>
      {props.children}
    + let List = ()=> ( +
    + +
  • hi 1
  • +
  • hi 2
  • +
  • hi 3
  • +
    +
    + ) + + it('should: find()', ()=>{ + $().find('li').length.should.equal(3) + }) + + it('should: children()', ()=> { + $().find(FancyList).children().length.should.equal(3) + + $().find(FancyList).children('.foo').length.should.equal(2) + }) + + it('should: filter()', ()=>{ + let items = $().find('li') + + items.length.should.equal(3) + items.filter('.foo').length.should.equal(2) + + items.filter((el, idx) => idx === 0).length.should.equal(1) + }) + + it('an empty filter should be a noop', ()=>{ + let instance = $() + instance.filter().should.equal(instance) + }) + + it('should: is()', ()=>{ + $().find('.foo') + .is('li').should.equal(true) + + $().find('.foo') + .is(sel`${FancyList} > li`).should.equal(true) + + $().find(FancyList) + .is('div').should.equal(false) + }) + }) +})