diff --git a/.babelrc b/.babelrc index 189a3f6..997044e 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,5 @@ { "stage": 0, - "loose": ["all"] + "loose": ["all"], + "plugins": ["object-assign"] } diff --git a/README.md b/README.md index f006cd2..2c79aa6 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,204 @@ A simple jquery like api wrapper for the React TestUtils to make them a bit frie Updated for react 0.14; works seamlessly with Stateless Components and you can find and filter on DOM components as well. +## API + +Like jQuery the exported function creates a collection of nodes, except in this case you select ReactElements instead +of DOM nodes. + +```js +import $ from 'react-testutil-query'; + +let $div = $(
); + +$div.length // 1 +$div[0] // ReactElement{ type: 'div', props: {} ... } +``` + +Since there is no globally accessible "document" of ReactElements like there is of DOM nodes, you need +to start by selecting a tree. Once you have a tree you can query it with css selectors and jQuery-like methods. + +```js +let elements = ( + + + +
+ +); + +var $elements = $(elements); + +$elements.find('div.fun-div').length // 1 +$elements.find(MyInput).length // 2 +``` + +`react-testutil-query` actually supports _two_ types of collections, we've already seen Element Collections, +but you can also work with Component _instance_ collections as well for querying rendered components. + +```js +let instance = ReactDOM.render(, mountNode) + +let $instance = $(instance); + +$instance.dom() // HTMLElemen +``` + +There is even a quick way to switch between them. + +```js +let elements = ( + + + +
+ +); + +var $elements = $(elements).render(); // renders `` into the DOM and returns an InstanceCollection + +$elements.find(MyInput).dom() // HTMLElement{ tagName: 'input' ... } + +$elements.unmount() // removes the mounted component and returns an ElementCollection +``` + +### Common Collection methods + +The methods are shared by both Element and Instance Collections. + +#### $.selector -> selector + +Selector creation function. + +#### $.fn.find(selector) + +Search all descendants of the current collection, matching against +the provided selector. + +```js +$(
  • item 1
).find('ul > li') +``` + +#### $.fn.filter(selector) + +Filter the current collection matching against the provided +selector. + +#### $.fn.is(selector) -> Bool + +Test if each item in the collection matches the provided +selector. + +#### $.fn.first([selector]) + +return the first item in a collection, alternatively search all +collection descendants matching the provided selector and return +the first match. + +#### $.fn.last([selector]) + +return the last item in a collection, alternatively search all +collection descendants matching the provided selector and return +the last match. + +#### $.fn.only() + +Assert that the current collection as only one item. + +#### $.fn.single(selector) + +Find and assert that only item matches the provided selector. + +#### $.fn.text() + +Return the text content of the matched Collection. + +```js +$(
Hello John ElementCollection + +Create an ElementCollection from an Element or array of Elements. + +#### $.fn.render([Bool renderIntoDocument, HTMLElement mountPoint ]) -> InstanceCollection + +Renders the first element of the ElementCollection into the DOM using `ReactDom.render`. By default +the component won't be added to the page `document`, you can pass `true` as the first parameter to render into the +document.body. Additional you can provide your own DOM node to mount the component into. + +`render()` returns a new _InstanceCollection_ + +```js +let elements = ( + +
+ +); + +var $elements = $(elements).render(); + +$elements = $(elements).render(true); //accessible by document.querySelectorAll + +$elements = $(elements).render(true, document.createElement('span')); //mounts the component to the +``` + +#### $.fn.shallowRender(props) -> ElementCollection + +Use the React shallow renderer utilities to _shallowly_ render the first element of the collection. + +```js +let MyComponent ()=>
Hi there!
+ +$().find('div').length // 0 + +$().shallowRender().is('div') // true +``` + +### $.fn.children([selector]) + +Return the children of the current selection, optionally filtered by those matching a provided selector. + +```js +let $list = $( +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +
+); + +$list.children().length // 3 + +$list.children('.foo').length // 1 +``` + +### InstanceCollection + +InstanceCollections are created when selecting Component instances, such as +the result of a `ReactDOM.render()` call. + +The public "instances" for components differ. DOM component instances +are the DOM nodes themselves, and Stateless Components technically don't have any +(we use the DOM node though). One key advantage to over the normal React +test utils is that here you can continue to chain `find` and `filter` on +DOM and Stateless components. + +#### $.fn.dom -> HTMLElement + +Returns the DOM nodes for each item in the Collection, if the exist + +#### $.fn.unmount -> HTMLElement + +Unmount the current tree and remove it from the DOM. `unmount()` returns an +ElementCollection of the _root_ component element. + + ### using selectors The selector syntax is subset of normal css selectors. You can query by tag: `'div > li'` or diff --git a/karma.conf.js b/karma.conf.js index 04de6c5..726d954 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,7 +9,7 @@ module.exports = function (config) { reporters: ['mocha'], files: [ - './test/*.js' + 'webpack.tests.js' ], port: 9876, @@ -22,7 +22,7 @@ module.exports = function (config) { browsers: ['Chrome'], preprocessors: { - 'test/*.js': ['webpack'] + 'webpack.tests.js': ['webpack'] }, webpack: { @@ -32,6 +32,7 @@ module.exports = function (config) { }, webpackServer: { + stats: { progress: true, modules: false }, noInfo: true } diff --git a/lib/shallow.js b/lib/shallow.js index 896162e..f04e5f0 100644 --- a/lib/shallow.js +++ b/lib/shallow.js @@ -142,6 +142,10 @@ var ShallowCollection = (function () { }), this.root); }; + ShallowCollection.prototype.is = function is(selector) { + return this.filter(selector).length === this.length; + }; + ShallowCollection.prototype.first = function first(selector) { return selector ? this.find(selector).first() : new ShallowCollection(this[0], this.root); }; @@ -150,11 +154,25 @@ var ShallowCollection = (function () { return selector ? this.find(selector).last() : new ComponentCollection(this[this.length - 1], this.root); }; - ShallowCollection.prototype.is = function is(selector) { - return this.filter(selector).length === this.length; + ShallowCollection.prototype.text = function text() { + var str = ''; + + this.each(function (element) { + return traverse(element, function (el) { + return typeof el === 'string' && (str += el); + }); + }); + return str; }; return ShallowCollection; })(); +function traverse(element, cb) { + cb(element); + + if (element && element.props) _react2['default'].Children.forEach(element.props.children, function (child) { + traverse(child, cb); + }); +} module.exports = exports['default']; \ No newline at end of file diff --git a/package.json b/package.json index f39212d..302c915 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "devDependencies": { "babel-core": "^5.8.25", "babel-loader": "^5.3.2", + "babel-plugin-object-assign": "^1.2.1", "chai": "^3.3.0", "cpy": "^3.4.1", "karma": "^0.13.10", @@ -50,8 +51,10 @@ "webpack": "^1.12.2" }, "dependencies": { - "bill": "^1.1.0", + "bill": "^1.3.2", "dom-helpers": "^2.4.0", + "gulp-babel-helpers": "^2.2.1", + "lodash": "^3.10.1", "react-addons-test-utils": "^0.14.0-rc1" }, "release-script": { diff --git a/src/QueryCollection.js b/src/QueryCollection.js new file mode 100644 index 0000000..fc1e510 --- /dev/null +++ b/src/QueryCollection.js @@ -0,0 +1,54 @@ +import common from './common'; + + +export default function(match, selector, init){ + + function QueryCollection(...args){ + + return new QueryCollection.fn.init(...args) + } + + Object.assign(QueryCollection, { + match, + selector, + s: selector, + isQueryCollection(inst){ + return !!inst._isQueryCollection + } + }) + + QueryCollection.fn = + QueryCollection.prototype = { + constructor: QueryCollection, + } + + createInit(QueryCollection) + common(QueryCollection) + + return QueryCollection + + function createInit($){ + + $.fn.init = function $init(element, context, ...args){ + let elements = element == null ? [] : [].concat(element); + + if ($.isQueryCollection(element)) { + return new element.constructor(element.get(), element.context) + } + else { + this.context = (context && context.context) || context || element; + elements = init.call(this, elements, context, ...args); + } + + if ($.isQueryCollection(elements)) + return elements + + elements.forEach((el, idx)=> this[idx] = el) + + this._isQueryCollection = true + this.length = elements.length; + } + + $.fn.init.prototype = $.fn + } +} diff --git a/src/shallow.js b/src/_shallow.js similarity index 80% rename from src/shallow.js rename to src/_shallow.js index adaee26..76027d1 100644 --- a/src/shallow.js +++ b/src/_shallow.js @@ -15,40 +15,39 @@ function match(selector, tree, includeSelf){ 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 + element = context = element } else if (isRtq(element)) { context = element.root element = element.get(); } - return new ShallowCollection(element, context, rerender) + return new ShallowCollection(element, context) +} + +rtq.render = function render(element, props) { + let isDomElement = element + && typeof element.type === 'string' + && element.type.toLowerCase() === element.type; + + if (props) + element = cloneElement(element, props) + + if (isDomElement) + return rtq(element) + + let renderer = TestUtils.createRenderer() + renderer.render(element) + return rtq(renderer.getRenderOutput()); } class ShallowCollection { + constructor(elements, root, rerender){ elements = [].concat(elements).filter(el => isValidElement(el)) @@ -57,7 +56,6 @@ class ShallowCollection { while( ++idx < elements.length) this[idx] = elements[idx] - this._rerender = rerender this.length = elements.length this.root = root } @@ -95,7 +93,7 @@ class ShallowCollection { find(selector) { return this.reduce((result, element) => { - return result.concat(match(selector, element)) + return result.concat(match(selector, element, false)) }, []) } @@ -109,7 +107,7 @@ class ShallowCollection { if (!selector) return this - let matches = match(selector, this.root); + let matches = match(selector, this.root, true); return new ShallowCollection([].filter.call(this, el => { return matches.indexOf(el) !== -1 @@ -118,6 +116,7 @@ class ShallowCollection { is(selector) { + return this.filter(selector).length === this.length } diff --git a/src/common.js b/src/common.js new file mode 100644 index 0000000..4cd6094 --- /dev/null +++ b/src/common.js @@ -0,0 +1,99 @@ +import * as utils from './utils'; + +export default function($){ + + Object.assign($, { + dom(component){ + return utils.findDOMNode(component) + } + }) + + Object.assign($.fn, { + _subjects: $.fn.get, + + _reduce: $.fn.reduce, + + _map(cb){ + var result = [] + this.each((...args) => result.push(cb(...args))) + return result + }, + + each(cb, thisArg) { + var idx = -1, len = this.length; + while (++idx < len) cb.call(thisArg, this[idx], idx, this) + return this + }, + + get() { + var result = [] + this.each(el => result.push(el)) + return result + }, + + reduce(cb, initial){ + return $([].reduce.call(this, cb, initial), this) + }, + + map(cb) { + return this.reduce((result, ...args) => { + result.push(cb(...args)) + return result + }, []) + }, + + find(selector) { + return this._reduce((result, element) => { + return result.concat($.match(selector, element, false)) + }, []) + }, + + traverse(test){ + return this._reduce((result, element) => { + return result.concat(utils.traverse(element, test)) + }, []) + }, + + filter(selector) { + if (!selector) return this + + let matches = $.match(selector, this.context, true); + + return this._reduce((result, element) => { + if (matches.indexOf(element) !== -1) + result.push(element); + + return result + }, []) + }, + + is(selector) { + return this.filter(selector).length === this.length + }, + + first(selector){ + return selector + ? this.find(selector).first() + : $(this[0], this) + }, + + last(selector){ + return selector + ? this.find(selector).last() + : $(this[this.length - 1], this) + }, + + only(){ + if (this.length !== 1) + throw new Error('The query found: ' + this.length + ' items not 1 ') + + return this.first() + }, + + single(selector) { + return selector + ? this.find(selector).only() + : this.only() + } + }) +} diff --git a/src/element.js b/src/element.js new file mode 100644 index 0000000..b393636 --- /dev/null +++ b/src/element.js @@ -0,0 +1,71 @@ +import React, { isValidElement, cloneElement } from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from'react-addons-test-utils'; +import createQueryCollection from './QueryCollection'; +import iQuery from './instance' +import * as utils from './utils'; +import { selector } from 'bill'; + +let isComponent = el => utils.isDOMComponent(el) || utils.isCompositeComponent(el) + +let eQuery = createQueryCollection(utils.match, selector, function init(elements, context){ + let first = elements.filter(e => !!e)[0]; + if (first && isComponent(first)) + return iQuery(elements); + + return elements.filter(el => isValidElement(el)) +}) + +Object.assign(eQuery.fn, { + + _reduce: eQuery.fn.reduce, + + render(intoDocument, mountPoint){ + var mount = mountPoint || document.createElement('div') + , element = this[0]; + + if (intoDocument) + document.body.appendChild(mount) + + let instance = ReactDOM.render(element, mount); + + if (instance === null) + instance = ReactDOM.render(utils.wrapStateless(element), mount) + + return iQuery(instance, utils.getInternalInstance(instance), mount); + }, + + shallowRender(props) { + if (!this.length) return this + + let element = this[0]; + let isDomElement = typeof element.type === 'string' && element.type.toLowerCase() === element.type; + + if (props) + element = cloneElement(element, props) + + if (isDomElement) + return eQuery(element) + + let renderer = ReactTestUtils.createRenderer() + renderer.render(element) + return eQuery(renderer.getRenderOutput()); + }, + + children(selector) { + return this + .reduce((result, element) => result.concat(element.props.children || []), []) + .filter(selector) + }, + + text(){ + let isText = el => typeof el === 'string'; + + return this.get().reduce((str, element)=> { + return str + utils.traverse(element, isText).join('') + }, '') + } + +}) + +export default eQuery; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index ae076f9..0000000 --- a/src/index.js +++ /dev/null @@ -1,252 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactInstanceMap from 'react/lib/ReactInstanceMap'; -import utils from 'react-addons-test-utils'; -import { match as _match, selector as sel } from './instance-selector'; -import hasClass from 'dom-helpers/class/hasClass'; - -var $r = module.exports = rtq - -let isRtq = item => item && item._isRTQ - -rtq.react = React; -rtq.s = rtq.selector = sel; - -function rtq(element, mount, renderIntoDocument = (mount === true)) { - var context; - - if (!mount || mount === true) - mount = document.createElement('div') - - if (utils.isElement(element)) - context = element = render(element, mount, renderIntoDocument) - else if (utils.isDOMComponent(element) || utils.isCompositeComponent(element)){ - context = element - mount = rtq.dom(element).parentNode - } - else if (isRtq(element)) { - mount = element._mountPoint - context = element.context - element = element.get(); - } - else - throw new TypeError('Wrong type: must either be ReactElement or a Component Instance') - - return new ComponentCollection(element, context, mount) -} - -function render(element, mountPoint, renderIntoDocument = false) { - var mount = mountPoint; - - if (renderIntoDocument) - document.body.appendChild(mount) - - let instance = ReactDOM.render(element, mount); - - if (instance === null) { - instance = ReactDOM.render(wrapStateless(element), mount) - } - - if (!instance.renderWithProps) { - instance.renderWithProps = newProps => render( - React.cloneElement(element, newProps) - , mount - , renderIntoDocument - ) - } - - return instance; -} - -function match(selector, tree, includeSelf){ - if (typeof selector === 'function') - selector = sel`${selector}` - - return _match(selector, tree, includeSelf) -} - -rtq.dom = function(component){ - return component instanceof HTMLElement ? component : ReactDOM.findDOMNode(component) -} - - -class ComponentCollection { - - constructor(_components, context, mountPoint, selector){ - var components = _components == null ? [] : [].concat(_components) - , idx = -1, len = components.length - - this._privateInstances = Object.create(null) - - while (++idx < len) { - var component = components[idx] - - if (component.getPublicInstance) { - this._privateInstances[idx] = component - component = component.getPublicInstance(); - - //stateless - if (component === null) - component = ReactDOM.findDOMNode(this._privateInstances[idx]._instance) - } - // if this a root Stateless component - else if (component && component.__isRTQstatelessWrapper){ - let wrapperInstance = ReactInstanceMap.get(component); - this._privateInstances[idx] = wrapperInstance._renderedComponent; - component = ReactDOM.findDOMNode(component) - } - else { - this._privateInstances[idx] = ReactInstanceMap.get(component) || component._reactInternalComponent - } - - this[idx] = component - } - - this.length = len - this.context = context - this.selector = selector - this._mountPoint = mountPoint - this._isRTQ = true - } - - _root(){ - return this.context._reactInternalComponent || this.context - } - - unmount(){ - let inBody = !!this.context.parentNode; - ReactDOM.unmountComponentAtNode(this._mountPoint) - - if (inBody) - document.body.removeChild(this._mountPoint) - - this.context = null - } - - setProps(newProps){ - return this.mapInPlace(element => element.renderWithProps(newProps)) - } - - each(cb, thisArg) { - var idx = -1, len = this.length; - while( ++idx < len ) cb.call(thisArg, this[idx], idx, this) - return this - } - - mapInPlace(cb, thisArg) { - return this.each((el, idx, list)=> this[idx] = cb(el, idx, list)) - } - - map(cb, thisArg) { - var idx = -1, len = this.length, result = [] - while (++idx < len) result.push(cb.call(thisArg, this[idx], idx, this)) - return result - } - - _reduce(cb, initial){ - return new ComponentCollection( - this._getInstances().reduce(cb, initial) - , this.context - , this._mountPount - , this.selector - ) - } - - reduce(cb, initial){ - return new ComponentCollection( - [].reduce.call(this, cb, initial) - , this.context - , this._mountPount - , this.selector - ) - } - - _getInstances(){ - return this.map((_, idx) => { - return this._privateInstances[idx] - }) - } - - get(){ - return unwrap(this.map(component => component)) - } - - dom() { - return unwrap(this.map(rtq.dom)) - } - - find(selector){ - return this._reduce((result, instance) => { - return result.concat(match(selector, instance, false)) - }, []) - } - - filter(selector) { - if (!selector) return this - - let matches = match(selector, this._root()); - - return this._reduce((result, el) => { - return matches.indexOf(el) !== -1 ? result.concat(el) : result - }, []) - } - - only(){ - if (this.length !== 1) throw Error('`' + this.selector +'` found: ' + this.length + ' not 1 ') - return this.first() - } - - single(selector) { - return selector - ? this.find(selector).only() - : this.only() - } - - first(selector){ - return selector - ? this.find(selector).first() - : new ComponentCollection(this[0], this.context, this._mountPount, this.selector) - } - - last(selector){ - return selector - ? this.find(selector).last() - : new ComponentCollection(this[this.length - 1], this.context, this._mountPount, this.selector) - } - - is(selector) { - return this.filter(selector).length === this.length - } - - trigger(event, data){ - data = data || {} - - if (event.substr(0, 2) === 'on' ) - event = event.substr(2, 1).toLowerCase() + event.substr(3) - - if (!(event in utils.Simulate)) - throw new TypeError( '"' + event + '" is not a supported DOM event') - - return this.each(component => - utils.Simulate[event]($r.dom(component), data)) - } -} - - -function unwrap(arr){ - return arr && arr.length === 1 ? arr[0] : arr -} - -function wrapStateless(Element){ - class StatelessWrapper extends React.Component { - constructor(){ - super() - this.__isRTQstatelessWrapper = true - } - render(){ - return Element - } - } - - return -} diff --git a/src/instance-selector.js b/src/instance-selector.js deleted file mode 100644 index 9bf5ecd..0000000 --- a/src/instance-selector.js +++ /dev/null @@ -1,85 +0,0 @@ -var React = require('react'); -var ReactDOM = require('react-dom') -var ReactInstanceMap = require('react/lib/ReactInstanceMap'); -var ReactTestUtils = require('react-addons-test-utils') - -import { create as createCompiler, parse } from 'bill/compiler'; -import { isCompositeComponent } from './utils'; - -let compiler = createCompiler() - -compiler.registerPseudo('has', function(compiledSelector) { - return (root, inst) => { - let matches = findAll(inst, compiledSelector) - return !!matches.length - } -}) - -compiler.registerPseudo('dom', function() { - return (root, inst) => { - return typeof root.type === 'string' && root.type.toLowerCase() === root.type - } -}) - -compiler.registerPseudo('composite', function() { - return (root, inst) => { - return typeof root.type === 'function' - } -}) - -compiler.registerNesting('any', test => anyParent.bind(null, test)) - -compiler.registerNesting('>', test => directParent.bind(null, test)) - - -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 (ReactTestUtils.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; -} - -function anyParent(test, element, inst, parentNode){ - do { - var { getParent, parent } = parentNode(); - element = parent - parentNode = getParent - } while(element && !test(element, test, getParent)) - - return !!element -} - -function directParent(test, element, inst, parentNode) { - element = parentNode().parent - return !!(element && test(element, parentNode().getParent)) -} - -export function match(selector, inst, includeSelf = true) { - let tree = inst.getPublicInstance ? inst : ReactInstanceMap.get(inst) - - return findAll(tree, compiler.compile(selector), undefined, !includeSelf) -} - -export let { compile, compileRule, selector } = compiler diff --git a/src/instance.js b/src/instance.js new file mode 100644 index 0000000..933888a --- /dev/null +++ b/src/instance.js @@ -0,0 +1,96 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactInstanceMap from 'react/lib/ReactInstanceMap'; +import ReactTestUtils from'react-addons-test-utils'; + +import closest from 'dom-helpers/query/closest'; +import createQueryCollection from './QueryCollection'; +import * as utils from './utils'; +import { match } from './utils'; +import selector from 'bill'; + +let $ = createQueryCollection(match, selector, function init(components, context, mount){ + let first = components[0] + + mount = mount || (context && context._mountPoint) || utils.getMountPoint(first); + + this.context = (context && context.context) + || context + || utils.getInternalInstance(utils.getRootInstance(mount)) + + this._mountPoint = mount; + this._privateInstances = Object.create(null) + + return components.map((component, idx) => { + let instances = utils.getInstances(component); + this._privateInstances[idx] = instances.private + return instances.public + }) +}) + +Object.assign($, { + dom(component){ + return utils.findDOMNode(component) + } +}) + +Object.assign($.fn, { + + _subjects(){ + return [].map.call(this, + (_, idx) => this._privateInstances[idx]) + }, + + _reduce(cb, initial){ + return $(this._subjects().reduce(cb, initial), this) + }, + + unmount(){ + let inBody = this._mountPoint.parentNode + , nextContext = this.context._currentElement; + + ReactDOM.unmountComponentAtNode(this._mountPoint) + + if (inBody) + document.body.removeChild(this._mountPoint) + + this.context = null + + return eQuery(nextContext) + }, + + dom(){ + return unwrap(this._map($.dom)) + }, + + text(){ + let isText = el => typeof el === 'string'; + + return this._subjects().reduce((str, element)=> { + return str + utils.traverse(element, isText) + .map(inst => inst._currentElement || inst) + .join('') + }, '') + }, + + trigger(event, data){ + data = data || {} + + if (event.substr(0, 2) === 'on' ) + event = event.substr(2, 1).toLowerCase() + event.substr(3) + + if (!(event in ReactTestUtils.Simulate)) + throw new TypeError( '"' + event + '" is not a supported DOM event') + + return this.each(component => + ReactTestUtils.Simulate[event]($.dom(component), data)) + } +}) + +function unwrap(arr){ + return arr && arr.length === 1 ? arr[0] : arr +} + +export default $; + +import eQuery from './element'; diff --git a/src/utils.js b/src/utils.js index a6a25da..21b4fd0 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,18 @@ -var ReactTestUtils = require('react-addons-test-utils') +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactInstanceMap from 'react/lib/ReactInstanceMap'; +import { + getID, getNode, findReactContainerForID + , getReactRootID, _instancesByReactRootID } from 'react/lib/ReactMount'; +import ReactTestUtils from'react-addons-test-utils'; + +import closest from 'dom-helpers/query/closest'; +import { match as _match, selector as s } from 'bill'; + +import { findAll as instanceTraverse } from 'bill/instance-selector'; +import { findAll as elementTraverse } from 'bill/element-selector'; + +export let isDOMComponent = ReactTestUtils.isDOMComponent; export function isCompositeComponent(inst) { if (ReactTestUtils.isDOMComponent(inst)) { @@ -8,3 +22,81 @@ export function isCompositeComponent(inst) { } return inst === null || typeof inst.render === 'function' && typeof inst.setState === 'function'; } + +export function getInstances(component){ + let _public = component + , _private = getInternalInstance(component); + + if (component.getPublicInstance) { + _public = component.getPublicInstance(); + + //stateless + if (_public === null) + _public = ReactDOM.findDOMNode(_private._instance) + } + // if this a root Stateless component + else if (component.__isStatelessWrapper) + _public = ReactDOM.findDOMNode(component) + + return { private: _private, public: _public } +} + +export function getInternalInstance(component){ + if (!component) return + + if (component.getPublicInstance) + return component + + if (component.__isStatelessWrapper) + return ReactInstanceMap.get(component)._renderedComponent + + if (component._reactInternalComponent) + return component._reactInternalComponent + + return ReactInstanceMap.get(component) +} + +export function wrapStateless(Element){ + class StatelessWrapper extends React.Component { + constructor(){ + super() + this.__isStatelessWrapper = true + } + render(){ + return Element + } + } + + return +} + +export function getMountPoint(instance){ + var id = getID(findDOMNode(instance)); + return findReactContainerForID(id); +} + +export function getRootInstance(mountPoint){ + return _instancesByReactRootID[getReactRootID(mountPoint)]; +} + +export function findDOMNode(component){ + return component instanceof HTMLElement + ? component + : component && component._rootID + ? getNode(component._rootID) + : ReactDOM.findDOMNode(component) +} + +export function match(selector, tree, includeSelf){ + if (typeof selector === 'function') + selector = s`${selector}` + + return _match(selector, tree, includeSelf) +} + +export function traverse(tree, test, includeSelf = true){ + if (React.isValidElement(tree)) + return elementTraverse(tree, test, includeSelf) + + return instanceTraverse(tree, test, includeSelf) +} diff --git a/test/common.js b/test/common.js new file mode 100644 index 0000000..767fed2 --- /dev/null +++ b/test/common.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { unmountComponentAtNode, render } from 'react-dom'; +import $ from '../src/element'; + +describe('common utils', ()=> { + let Stateless = props =>
{props.children}
+ + class List extends React.Component { + render(){ + return ( +
+ + + +
+ ) + } + } + + it('should create collection', ()=>{ + $(
).length.should.equal(1) + $(
)[0].type.should.equal('div') + }) + + it('should get', ()=>{ + Array.isArray($(
).get()).should.equal(true) + }) + + it('should render element', ()=> { + let instance = $(
).render() + + instance._mountPoint.querySelectorAll('.test').length.should.equal(1) + expect(instance._mountPoint.parentNode).to.not.exist + }) + + it('should render element at mountPoint', ()=> { + let mount = document.createElement('div') + let instance = $(
).render(false, mount) + + mount.children[0].classList.contains('test').should.equal(true) + instance._mountPoint.should.equal(mount) + }) + + it('should render into document', ()=> { + let instance = $(
).render(true) + + document.querySelectorAll('.test').length.should.equal(1) + + unmountComponentAtNode(instance._mountPoint) + }) + + it('should render mount into document', ()=> { + let mount = document.createElement('div') + let instance = $(
).render(true, mount) + + document.querySelectorAll('.test').length.should.equal(1) + instance._mountPoint.should.equal(mount) + + unmountComponentAtNode(instance._mountPoint) + }) + + it('should work with Stateless components as root', ()=>{ + let instance = $().render() + instance.length.should.equal(1) + }) + + it('should shallow render', ()=>{ + let instance = $().shallowRender() + + instance.length.should.equal(1) + instance[0].type.should.equal('div') + }) +}) diff --git a/test/dom.js b/test/dom.js index c8dd125..72b9c92 100644 --- a/test/dom.js +++ b/test/dom.js @@ -1,10 +1,7 @@ import React from 'react'; import { unmountComponentAtNode, render } from 'react-dom'; -import $ from '../src/index'; -import { match, selector as sel } from '../src/instance-selector'; - -chai.use(require('sinon-chai')) - +import $ from '../src/element'; +import * as utils from '../src/utils'; describe('DOM rendering', ()=> { let Stateless = props =>
{props.children}
@@ -12,6 +9,7 @@ describe('DOM rendering', ()=> { render(){ return (
+ Hello there @@ -38,34 +36,18 @@ describe('DOM rendering', ()=> { } } - describe('css selector parsing', ()=>{ - - it('should match nested', ()=>{ - let inst = $().get(); - - match('.list-wrapper', inst).length.should.equal(1) - - match(sel`div.list-wrapper > ${List}`, inst).length.should.equal(1) - - match(sel`${Stateless}`, inst).length.should.equal(1) - - match(sel`.list-wrapper:has(${List})`, inst).length.should.equal(1) - - match(sel`span:has(${List})`, inst).length.should.equal(0) - }) - - }) - it('should wrap existing mounted component', ()=> { - let instance = render(
, document.createElement('div')) + let mount = document.createElement('div') + , instance = render(
, mount) let $inst = $(instance); expect($inst[0]).to.equal(instance); - expect($inst.context).to.equal(instance) + //expect($inst.context).to.equal(utils.getInternalInstance(instance)) + expect($inst._mountPoint).to.equal(mount) }) - it('should recreate rtq object', ()=> { + it('should recreate $ object', ()=> { let instance = $(
) let instanceB = $(instance); @@ -74,60 +56,23 @@ describe('DOM rendering', ()=> { expect(instance[0]).to.equal(instanceB[0]) }) - it('should render element', ()=> { - let instance = $(
) - - instance.context.tagName.should.equal('DIV') - expect(instance._mountPoint.parentNode).to.not.exist - }) - - it('should render element at mountPoint', ()=> { - let mount = document.createElement('div') - let instance = $(
, mount) - - mount.children[0].classList.contains('test').should.equal(true) - instance._mountPoint.should.equal(mount) - }) - - it('should render into document', ()=> { - let instance = $(
, true) - - document.querySelectorAll('.test').length.should.equal(1) - - unmountComponentAtNode(instance._mountPoint) - }) - - it('should render mount into document', ()=> { - let mount = document.createElement('div') - let instance = $(
, mount, true) - - document.querySelectorAll('.test').length.should.equal(1) - instance._mountPoint.should.equal(mount) - - unmountComponentAtNode(instance._mountPoint) - }) - - it('should work with Stateless components as root', ()=>{ - let instance = $() - - instance.length.should.equal(1) - }) - it('should unmount', ()=> { let mount = document.createElement('div') - let instance = $(
, mount, true) + let instance = $(
).render(true, mount) document.querySelectorAll('.test').length.should.equal(1) - instance.unmount() + let next = instance.unmount() document.querySelectorAll('.test').length.should.equal(0) expect(instance.context).to.not.exist expect(mount.parentNode).to.not.exist + + expect(next[0].type).to.equal('div') }) it('should return DOM node from Component', ()=> { - let instance = $(
) + let instance = $(
).render() instance.dom().should.be.an.instanceof(HTMLElement) }) @@ -140,66 +85,66 @@ describe('DOM rendering', ()=> { it('should `get()` underlying element', ()=> { let instance = $() - instance.get().should.equal(instance[0]) + instance.get()[0].should.equal(instance[0]) }) - - it('should set props', ()=> { - let instance = $() - - instance.setProps({ min: 5 }) - - instance[0].props.min.should.equal(5) - }) - +// +// it('should set props', ()=> { +// let instance = $() +// +// instance.setProps({ min: 5 }) +// +// instance[0].props.min.should.equal(5) +// }) +// describe('querying', ()=> { describe('find', ()=> { it('should find by Component Types', ()=>{ - let instance = $() + let instance = $().render() - instance.find(List).get().should.be.an.instanceof(List); + instance.find(List)[0].should.be.an.instanceof(List); }) it('should find by Component Stateless Types', ()=>{ - let instance = $() + let instance = $().render() instance.find(Stateless).length.should.equal(1); }) it('should find by :composite', ()=>{ - let instance = $() + let instance = $().render() let result = instance.find(':composite') - result.length.should.equal(2); + result.length.should.equal(3); }) it('should find by :dom', ()=>{ - let instance = $() + let instance = $().render() let result = instance.find(':dom') result.length.should.equal(10); }) - it('should return stateless component dom nodes', ()=>{ - let instance = $() + it('should return stateless component DOM nodes', ()=>{ + let instance = $().render() - instance.find(Stateless).get().should.be.an.instanceof(HTMLElement); + instance.find(Stateless)[0].should.be.an.instanceof(HTMLElement); }) it('should find by className', ()=>{ - let instance = $() + let instance = $().render() - instance.find('.list-wrapper').get().tagName.should.equal('DIV'); + instance.find('.list-wrapper')[0].tagName.should.equal('DIV'); }) it('should find by tag', ()=>{ - let instance = $() + let instance = $().render() instance.find('li').length.should.equal(3); }) it('should allow find chaining', ()=>{ - let instance = $() + let instance = $().render() let items = instance .find('.list-wrapper') @@ -222,55 +167,55 @@ describe('DOM rendering', ()=> { describe('is', ()=> { it('should recognize Component Types', ()=>{ - let instance = $() + let instance = $().render() instance.is(Component).should.equal(true); }) it('should recognize Stateless Types', ()=>{ - let instance = $() + let instance = $().render() instance.is(Stateless).should.equal(true); }) it('should recognize className', ()=>{ - let instance = $() + let instance = $().render() instance.is('.test').should.equal(true); }) it('should recognize tags', ()=>{ - let instance = $() + let instance = $().render() instance.is('span').should.equal(true); }) it('should recognize :composite', ()=>{ - let instance = $() + let instance = $().render() instance.is(':composite').should.equal(true); }) it('should recognize :dom', ()=>{ - let instance = $() + let instance = $().render() instance.is(':dom').should.equal(true); }) it('should work with find', ()=>{ - let instance = $() + let instance = $().render() instance.find(Stateless).is(Stateless).should.equal(true); }) it('should work with multiple matches', ()=>{ - let instance = $() + let instance = $().render() instance.find('li').is('li').should.equal(true); }) it('should work with chaining', ()=>{ - let instance = $() + let instance = $().render() instance.find('li').is('.item').should.equal(false); @@ -279,60 +224,66 @@ describe('DOM rendering', ()=> { }) it('should: filter()', ()=>{ - let items = $().find('li') + let items = $().render().find('li') items.length.should.equal(3) items.filter('.item').length.should.equal(1) - $().find('div > *').filter(List).length.should.equal(1) + $().render().find('div > *').filter(List).length.should.equal(1) }) it('an empty filter should be a noop', ()=>{ - let instance = $() + let instance = $().render() instance.filter().should.equal(instance) }) + it('should get first', ()=> { + let instance = $().render() + + instance.first('li')[0].textContent.should.equal('hi 1') + + instance.find('li').first()[0].textContent.should.equal('hi 1') + }) + + it('should get last', ()=> { + let instance = $().render() + + instance.last('li')[0].textContent.should.equal('hi 3'); + instance.find('li').last()[0].textContent.should.equal('hi 3') + }) + it('should find single', ()=> { - let instance = $() + let instance = $().render() instance.single(Stateless).length.should.equal(1) }) it('should throw when single returns more than one', ()=> { - let instance = $() + let instance = $().render() ;(()=> instance.single('li')).should.throw() }) it('should throw when single returns none', ()=> { - let instance = $() + let instance = $().render() ;(()=> instance.single('article')).should.throw() }) - it('should get first', ()=> { - let instance = $() - - instance.first('li')[0].textContent.should.equal('hi 1') - - instance.find('li').first()[0].textContent.should.equal('hi 1') - }) - - it('should get last', ()=> { - let instance = $() + it('text content', ()=>{ + $().render().text().should.equal('Hello therehi 1hi 2hi 3') - instance.last('li')[0].textContent.should.equal('hi 3'); - instance.find('li').last()[0].textContent.should.equal('hi 3') + $(
hi {'john'}
).render().text().should.equal('hi john') }) it('should trigger event', ()=> { let clickSpy = sinon.spy(); - let instance = $() + let instance = $().render() instance.find(List).trigger('click', { clickedYo: true }) clickSpy.should.have.been.calledOnce }) - }) + }) }) diff --git a/test/shallow.js b/test/shallow.js index dbd43dd..b0ba6f0 100644 --- a/test/shallow.js +++ b/test/shallow.js @@ -1,10 +1,8 @@ import React, { cloneElement } from 'react'; -import $ from '../src/shallow'; -import { selector as sel } from 'bill'; +import $ from '../src/element'; -chai.use(require('sinon-chai')) -describe('Shallow rendering', ()=> { +describe.only('Shallow rendering', ()=> { let Stateless = props =>
{props.children}
let List = class extends React.Component { render(){ @@ -23,10 +21,10 @@ describe('Shallow rendering', ()=> { } } - it('create rtq object', ()=>{ + it('create element collection', ()=>{ let instance = $(
) - instance.root.type.should.equal('div') + instance.context.type.should.equal('div') instance.length.should.equal(1) }) @@ -34,15 +32,24 @@ describe('Shallow rendering', ()=> { let el =
, instance = $(el) - instance.root.should.equal(el) + instance.context.should.equal(el) }) it('should render Composite Components', ()=>{ let el =
, Element = ()=> el - , instance = $() + , instance = $().shallowRender() - instance.root.should.equal(el) + instance.context.should.equal(el) + }) + + it.only('should query Composite Components', ()=>{ + $() + .is(Element).should.equal(true) + + $(
) + .find('div > span') + .length.should.equal(1) }) it('should filter out invalid Elements', ()=>{ @@ -73,40 +80,41 @@ describe('Shallow rendering', ()=> { ) it('should: find()', ()=>{ - $().find('li').length.should.equal(3) + $().shallowRender().find('li').length.should.equal(3) }) it('should: children()', ()=> { - $().find(FancyList).children().length.should.equal(3) + $().shallowRender().find(FancyList).children().length.should.equal(3) - $().find(FancyList).children('.foo').length.should.equal(2) + $().shallowRender().find(FancyList).children('.foo').length.should.equal(2) }) it('should: filter()', ()=>{ - let items = $().find('li') + let items = $().shallowRender().find('li') items.length.should.equal(3) items.filter('.foo').length.should.equal(2) }) it('an empty filter should be a noop', ()=>{ - let instance = $() + let instance = $().shallowRender() instance.filter().should.equal(instance) }) it.only('text content', ()=>{ - let instance = $() - instance.text().should.equal('hi 1hi 2hi 3') + $().shallowRender().text().should.equal('hi 1hi 2hi 3') + + $(
hi {'john'}
).text().should.equal('hi john') }) it('should: is()', ()=>{ - $().find('.foo') + $().shallowRender().find('.foo') .is('li').should.equal(true) - $().find('.foo') - .is(sel`${FancyList} > li`).should.equal(true) + $().shallowRender().find('.foo') + .is($.s`${FancyList} > li`).should.equal(true) - $().find(FancyList) + $().shallowRender().find(FancyList) .is('div').should.equal(false) }) }) diff --git a/webpack.tests.js b/webpack.tests.js new file mode 100644 index 0000000..ada098b --- /dev/null +++ b/webpack.tests.js @@ -0,0 +1,6 @@ + +chai.use(require('sinon-chai')) + +const testsContext = require.context('./test', true, /.+/) + +testsContext.keys().forEach(testsContext);