├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── package.json ├── src ├── dom-asserts.js ├── domnode.js ├── domnodes.js ├── index.js ├── reactcomponent.js └── renderer.js └── test ├── asserts ├── dom-node-with-attr-and-value-spec.js └── dom-node-with-textcontent-spec.js ├── find-dom-nodes-spec.js └── find-reactcomponent-spec.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.2.0 - 30. July 2015 2 | 3 | * fix `rendersDomNode*` which failed when there was textContent in the node 4 | 5 | # 1.1.0 - 13. July 2015 6 | 7 | * add `rendersDomNodeWithTextContent` and 8 | * `rendersNoDomNodeWithTextContent` 9 | * export `rendersNoDomNodeWithAttrAndValue` 10 | 11 | # 1.0.0 - 12. July 2015 12 | 13 | * add `rendersDomNodeWithAttrAndValue` -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 uxebu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-components-asserts 2 | 3 | Asserts for react.js components using the shallow renderer. 4 | 5 | # Early stage 6 | 7 | This is just a very early draft and evolves with the features needed where this project is used, 8 | initially for the es6katas.org site. Will see where it goes from here. 9 | 10 | # What is it? 11 | 12 | In order to do TDD with unit tests, without the need to interact with the DOM 13 | this package will provide some assert functions that help verifying certain 14 | conditions when building components. 15 | It is NOT meant for HTML structure validation. The main intention is to verify that 16 | certain properties and components are used and receive the correct data. 17 | Using this may lead to better components design and allows for refactoring components. 18 | 19 | Assert functions like `rendersDomNodeWithTextContent(component, textContent)` will ensure that some DOM node 20 | inside a component has the expected `textContent` where in the HTML structure it is located is not 21 | scope of this project. 22 | 23 | # Example 24 | 25 | ```jsx 26 | class Article extends React.Component { 27 | render() { 28 | const price = 42; 29 | return ( 30 |
31 |
32 | {price} 33 |
34 |
35 | ); 36 | } 37 | } 38 | class OtherComp extends React.Component { 39 | render() { 40 | const price = 42; 41 | return ( 42 | {price} 43 | ); 44 | } 45 | } 46 | ``` 47 | 48 | a test could now validate that the price gets rendered at all, as an innerText, like so: 49 | 50 | ```js 51 | import { 52 | rendersDomNodeWithAttrAndValue, 53 | rendersDomNodeWithTextContent 54 | } from 'react-components-asserts'; 55 | 56 | it('has an `href=#some`', function() { 57 | rendersDomNodeWithAttrAndValue(
, 'href', '#some'); 58 | }); 59 | 60 | it('also has an `className=#some`', function() { 61 | rendersDomNodeWithAttrAndValue(, 'className', '#some'); 62 | }); 63 | 64 | describe('renders the price', function() { 65 | it('in
', function() { 66 | rendersDomNodeWithTextContent(
, '42'); 67 | }); 68 | it('in ', function() { 69 | rendersDomNodeWithTextContent(, '42'); 70 | }); 71 | }); 72 | ``` 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-components-asserts", 3 | "version": "1.2.0", 4 | "description": "Asserts for react.js components using the shallow renderer.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "mocha --compilers js:babel/register" 8 | }, 9 | "keywords": [ 10 | "react", 11 | "assert", 12 | "component" 13 | ], 14 | "author": "Wolfram Kriesing, uxebu", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/uxebu/react-components-asserts.git" 19 | }, 20 | "devDependencies": { 21 | "mocha": "^2.2.5", 22 | "babel": "^5.6.14", 23 | "react": "^0.13.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/dom-asserts.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {fromComponent} from './domnodes.js'; 3 | import DomNode from './domnode.js'; 4 | 5 | export function rendersDomNodeWithAttrAndValue(component, attributeName, expectedValue) { 6 | const found = _rendersDomNodeWithAttrAndValue(component, attributeName, expectedValue); 7 | const message = `Expected \`${component.type.name || component.type}\` to render a DOM node with the attribute \`${attributeName}\` with value \`${expectedValue}\``; 8 | assert.equal(found, true, message); 9 | } 10 | export function rendersNoDomNodeWithAttrAndValue(component, attributeName, expectedValue) { 11 | const anyFound = _rendersDomNodeWithAttrAndValue(component, attributeName, expectedValue); 12 | assert.equal(anyFound, false); 13 | } 14 | export function rendersDomNodeWithTextContent(component, textContent) { 15 | const found = _findsOneWithTextContent(component, textContent); 16 | const message = `Expected \`${component.type.name || component.type}\` to contain text content \`${textContent}\`.`; 17 | assert.equal(found, true, message); 18 | } 19 | export function rendersNoDomNodeWithTextContent(component, textContent) { 20 | const found = _findsOneWithTextContent(component, textContent); 21 | const message = `Did NOT expect \`${component.type.name || component.type}\` to contain text content \`${textContent}\`.`; 22 | assert.equal(found, false, message); 23 | } 24 | 25 | 26 | function domNodesFromComponent(component) { 27 | return fromComponent(component).domNodes; 28 | } 29 | function _findsOneWithTextContent(component, textContent) { 30 | const domNodes = domNodesFromComponent(component); 31 | return domNodes.some(domNode => domNode.hasTextContent(textContent)); 32 | } 33 | function _rendersDomNodeWithAttrAndValue(component, attributeName, expectedValue) { 34 | const domNodes = domNodesFromComponent(component); 35 | return domNodes 36 | .some(domNode => domNode.hasAttributeWithValue(attributeName, expectedValue)) 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/domnode.js: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | 3 | export default class DomNode { 4 | static fromRenderedNode(renderedNode) { 5 | let domNode = new DomNode(); 6 | domNode._renderedNode = renderedNode; 7 | return domNode; 8 | } 9 | static isDomNode(node) { 10 | return node && node.type in React.DOM; 11 | } 12 | 13 | get type() { 14 | return this._renderedNode.type; 15 | } 16 | 17 | hasAttribute(attributeName) { 18 | return attributeName in this._renderedNode.props; 19 | } 20 | hasTextContent(textContent) { 21 | const children = this._renderedNode.props.children; 22 | if (Array.isArray(children)) { 23 | if (children.map(child => ''+child).join('') === textContent) { 24 | // e.g. ['(', '42', ')'] is checked as '(42)' 25 | return true; 26 | } 27 | return children.some(child => child === textContent); 28 | } 29 | return children === textContent; 30 | } 31 | getAttributeValue(attributeName) { 32 | return this._renderedNode.props[attributeName]; 33 | } 34 | hasAttributeWithValue(attributeName, value) { 35 | return this.hasAttribute(attributeName) && 36 | this.getAttributeValue(attributeName) === value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/domnodes.js: -------------------------------------------------------------------------------- 1 | import DomNode from './domnode.js'; 2 | import ReactComponent from './reactcomponent.js'; 3 | import Renderer from './renderer.js'; 4 | 5 | export default class DomNodes { 6 | 7 | static fromComponent(component) { 8 | const renderedTree = Renderer.withComponent(component).renderedTree(); 9 | return DomNodes.fromRenderedTree(renderedTree); 10 | } 11 | 12 | static fromRenderedTree(renderedTree) { 13 | let instance = new DomNodes(); 14 | instance.domNodes = flattenAllNodes(renderedTree) 15 | .filter(DomNode.isDomNode) 16 | .map(DomNode.fromRenderedNode); 17 | return instance; 18 | } 19 | 20 | } 21 | 22 | const ensureToBeArray = (mayBeArray) => Array.isArray(mayBeArray) ? mayBeArray : [mayBeArray]; 23 | const flatten = (arr, merged) => [...arr, ...merged]; 24 | 25 | function allChildren({props = {}} = {}) { 26 | if (!props.children) { 27 | return []; 28 | } 29 | let children = ensureToBeArray(props.children); 30 | let all = []; 31 | for (let i=0, l=children.length; i; 13 | rendersNoDomNodeWithAttrAndValue(component, 'className', 'x'); 14 | }); 15 | it('finds a `className` in one DOM node', function() { 16 | const component = ; 17 | rendersDomNodeWithAttrAndValue(component, 'className', 'x'); 18 | }); 19 | it('finds a `className` in one DOM node of many', function() { 20 | const component =
; 21 | rendersDomNodeWithAttrAndValue(component, 'className', 'x'); 22 | }); 23 | }); 24 | 25 | describe('assert function', function() { 26 | it('is silent when test passes', function() { 27 | const component = ; 28 | const fn = () => { 29 | rendersDomNodeWithAttrAndValue(component, 'className', 'x'); 30 | }; 31 | assert.doesNotThrow(fn); 32 | }); 33 | describe('when it fails', function() { 34 | it('throws', function() { 35 | const component = ; 36 | const fn = () => { 37 | rendersDomNodeWithAttrAndValue(component, 'className', 'x'); 38 | }; 39 | assert.throws(fn); 40 | }); 41 | it('throws right message', function() { 42 | const component = ; 43 | try { 44 | rendersDomNodeWithAttrAndValue(component, 'className', 'x'); 45 | } catch (error) { 46 | assert.ok(error.message.startsWith('Expected')); 47 | } 48 | }); 49 | }); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /test/asserts/dom-node-with-textcontent-spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | import assert from 'assert'; 3 | import { 4 | rendersDomNodeWithTextContent, 5 | rendersNoDomNodeWithTextContent 6 | } from '../../src/dom-asserts.js'; 7 | 8 | describe('renders(No)DomNodeWithTextContent', function() { 9 | 10 | describe('finds none', function() { 11 | it('there is none', function() { 12 | const component = ; 13 | rendersNoDomNodeWithTextContent(component, 'bold'); 14 | }); 15 | it('there is one, which should fail', function() { 16 | const component = bold; 17 | assert.throws(() => { 18 | rendersNoDomNodeWithTextContent(component, 'bold'); 19 | }); 20 | }); 21 | 22 | describe('error message', function() { 23 | let errorMessage; 24 | const component = bold; 25 | beforeEach(function() { 26 | try { 27 | rendersNoDomNodeWithTextContent(component, 'bold'); 28 | } catch (error) { 29 | errorMessage = error.message; 30 | } 31 | }); 32 | it('starts with `Did NOT expect`', function() { 33 | assert.ok(errorMessage.startsWith('Did NOT expect'), 'Doesnt start with `Did NOT expect`.'); 34 | }); 35 | it('contains the component name in backticks', function() { 36 | const name = component.type; 37 | assert.ok(errorMessage.includes(`\`${name}\``), 'Doesn`t contain ``.'); 38 | }); 39 | it('contains `bold` in backticks', function() { 40 | assert.ok(errorMessage.includes('`bold`'), 'Doesn`t contain `\`bold\``.'); 41 | }); 42 | }); 43 | 44 | }); 45 | 46 | describe('finds some', function() { 47 | it('when its the only node', function() { 48 | const component = bold; 49 | rendersDomNodeWithTextContent(component, 'bold'); 50 | }); 51 | it('and has another sibling', function() { 52 | const component = bold; 53 | rendersDomNodeWithTextContent(component, 'bold'); 54 | }); 55 | 56 | describe('error message', function() { 57 | let errorMessage; 58 | const component = ; 59 | beforeEach(function() { 60 | try { 61 | rendersDomNodeWithTextContent(component, 'bold'); 62 | } catch (error) { 63 | errorMessage = error.message; 64 | } 65 | }); 66 | it('starts with `Expected`', function() { 67 | assert.ok(errorMessage.startsWith('Expected'), 'Doesnt start with `Expected`.'); 68 | }); 69 | it('contains the component name in backticks', function() { 70 | const name = component.type; 71 | assert.ok(errorMessage.includes(`\`${name}\``), 'Doesn`t contain ``.'); 72 | }); 73 | it('contains `bold` in backticks', function() { 74 | assert.ok(errorMessage.includes('`bold`'), 'Doesn`t contain `\`bold\``.'); 75 | }); 76 | }); 77 | }); 78 | 79 | describe('finds combined content', function() { 80 | it('e.g. ({one})', function() { 81 | const number = 42; 82 | const component = ({number}); 83 | rendersDomNodeWithTextContent(component, '(42)'); 84 | }); 85 | }); 86 | 87 | it('finds rendered subcomponents', function() { 88 | const numbers = [23, 42]; 89 | const NumberComponent = class extends React.Component { 90 | render() {return {this.props.number};} 91 | }; 92 | const component = ( 93 | 94 | 95 | 96 | 97 | ); 98 | rendersDomNodeWithTextContent(component, numbers[1]); 99 | }); 100 | 101 | it('finds multile textContents combined in a component', function() { 102 | const kata = {name:'fourty-two'}; 103 | const NumberComponent = class extends React.Component { 104 | render() {return #{kata.id} {kata.name};} 105 | }; 106 | const component = ; 107 | rendersDomNodeWithTextContent(component, `#undefined ${kata.name}`); 108 | }); 109 | 110 | }); 111 | -------------------------------------------------------------------------------- /test/find-dom-nodes-spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import React from 'react/addons'; 3 | const TestUtils = React.addons.TestUtils; 4 | import {fromComponent} from '../src/domnodes.js'; 5 | 6 | function domNodesFromComponent(component) { 7 | return fromComponent(component).domNodes; 8 | } 9 | 10 | describe('find dom nodes', function() { 11 | 12 | describe('finds the right number of nodes', function() { 13 | it('one', function() { 14 | assert.equal(domNodesFromComponent().length, 1); 15 | }); 16 | it('two nodes, one nesting level deep', function() { 17 | assert.equal(domNodesFromComponent(
).length, 2); 18 | }); 19 | it('if inner node is NOT a DOM node, it does not count', function() { 20 | class NotDomNode extends React.Component { render() { return null; } } 21 | let component =
; 22 | assert.equal(domNodesFromComponent(component).length, 1); 23 | }); 24 | it('if inner node is textContent, it does not count', function() { 25 | class ContentNode extends React.Component { render() { return 1; } } 26 | let component =
; 27 | assert.equal(domNodesFromComponent(component).length, 2); 28 | }); 29 | describe('if a child is undefined', function() { 30 | // important is the space after `{void 0}`!!! 31 | const comp = {void 0} ; 32 | it('wont bail', function() { 33 | assert.doesNotThrow(() => {domNodesFromComponent(comp)}); 34 | }); 35 | it('finds the right number of children', function() { 36 | assert.equal(domNodesFromComponent(comp).length, 1); 37 | }); 38 | }); 39 | }); 40 | 41 | describe('returns all nodes', function() { 42 | it('for one node', function() { 43 | assert.equal(domNodesFromComponent()[0].type, 'b'); 44 | }); 45 | describe('nested nodes', function() { 46 | describe('two nodes, one level nesting', function() { 47 | let domNodes; 48 | beforeEach(function() { 49 | domNodes = domNodesFromComponent(
); 50 | }); 51 | it('first node is the outer node', () => { assert.equal(domNodes[0].type, 'div'); }); 52 | it('second node is the inner node', () => { assert.equal(domNodes[1].type, 'b'); }); 53 | }); 54 | describe('three nodes, two levels nesting', function() { 55 | let domNodes; 56 | beforeEach(function() { 57 | let component =
; 58 | domNodes = domNodesFromComponent(component); 59 | }); 60 | it('first node is the outer node', () => { assert.equal(domNodes[0].type, 'div'); }); 61 | it('second node is the node on the first level', () => { assert.equal(domNodes[1].type, 'b'); }); 62 | it('third node is the inner node', () => { assert.equal(domNodes[2].type, 'span'); }); 63 | }); 64 | describe('three nodes, one level nesting', function() { 65 | let domNodes; 66 | beforeEach(function() { 67 | let renderedTree =
; 68 | domNodes = domNodesFromComponent(renderedTree); 69 | }); 70 | it('first node is the outer node', () => { assert.equal(domNodes[0].type, 'div'); }); 71 | it('second node is the 1st node on the first level', () => { assert.equal(domNodes[1].type, 'b'); }); 72 | it('third node is the 2nd node on the first level', () => { assert.equal(domNodes[2].type, 'span'); }); 73 | }); 74 | describe('many DOM nodes, various nestings', function() { 75 | let domNodes; 76 | beforeEach(function() { 77 | let renderedTree = ( 78 | 85 | ); 86 | domNodes = domNodesFromComponent(renderedTree); 87 | }); 88 | describe('the order should be depth first', function() { 89 | it('the count is correct', () => { assert.equal(domNodes.length, 8); }); 90 | it('first node is the outer node', () => { assert.equal(domNodes[0].type, 'div'); }); 91 | it('2nd node is `p`', () => { assert.equal(domNodes[1].type, 'p'); }); 92 | it('3rd node is `a`', () => { assert.equal(domNodes[2].type, 'a'); }); 93 | it('4th node is `b`', () => { assert.equal(domNodes[3].type, 'b'); }); 94 | it('5th node is `span`', () => { assert.equal(domNodes[4].type, 'span'); }); 95 | it('6th node is `blockquote`', () => { assert.equal(domNodes[5].type, 'blockquote'); }); 96 | it('7th node is `form`', () => { assert.equal(domNodes[6].type, 'form'); }); 97 | it('8th node is `button`', () => { assert.equal(domNodes[7].type, 'button'); }); 98 | }); 99 | }); 100 | 101 | describe('node that has an innerText', function() { 102 | it('1st node is `b`', () => { 103 | let domNodes = domNodesFromComponent(bold); 104 | assert.equal(domNodes[0].type, 'b'); 105 | }); 106 | }); 107 | }); 108 | }); 109 | 110 | describe('finds in nested components', function() { 111 | class InnerComponent extends React.Component { render() { return ; } } 112 | describe('one nesting level deep', function() { 113 | it('inside is a react component', function() { 114 | assert.equal(domNodesFromComponent().length, 2); 115 | }); 116 | it('different children', function() { 117 | assert.equal(domNodesFromComponent().length, 3); 118 | }); 119 | it('multiple different children', function() { 120 | assert.equal(domNodesFromComponent().length, 5); 121 | }); 122 | }); 123 | 124 | describe('multiple nesting levels', function() { 125 | it('two levels', function() { 126 | assert.equal(domNodesFromComponent().length, 3); 127 | }); 128 | 129 | it('multiple nesting of components', function() { 130 | class MultiComponent extends React.Component { render() { return ; } } 131 | assert.equal(domNodesFromComponent().length, 5); 132 | }); 133 | }); 134 | 135 | describe('ensure order in rendered tree', function() { 136 | class FirstLevel extends React.Component { render() { return (

); } } 137 | class SecondLevel extends React.Component { render() { return

; } } 138 | let domNodes; 139 | beforeEach(function() { 140 | let renderedTree = ( 141 |
146 | ); 147 | domNodes = domNodesFromComponent(renderedTree); 148 | }); 149 | it('the count is correct', () => { assert.equal(domNodes.length, 12); }); 150 | it('first node is the outer node', () => { assert.equal(domNodes[0].type, 'div'); }); 151 | it('3rd node is `span`', () => { assert.equal(domNodes[1].type, 'span'); }); 152 | it('4th node is `h1`', () => { assert.equal(domNodes[2].type, 'h1'); }); 153 | it('5th node is `a`', () => { assert.equal(domNodes[3].type, 'a'); }); 154 | it('6th node is `p`', () => { assert.equal(domNodes[4].type, 'p'); }); 155 | it('7th node is `h1`', () => { assert.equal(domNodes[5].type, 'h1'); }); 156 | it('8th node is `b`', () => { assert.equal(domNodes[6].type, 'b'); }); 157 | it('9th node is `blockquote`', () => { assert.equal(domNodes[7].type, 'blockquote'); }); 158 | }); 159 | 160 | }); 161 | 162 | describe('inherited components', function() { 163 | it('over two levels', function() { 164 | // ??? 165 | }); 166 | }); 167 | 168 | }); 169 | -------------------------------------------------------------------------------- /test/find-reactcomponent-spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import React from 'react/addons'; 3 | import Renderer from '../src/renderer.js'; 4 | 5 | function findReactComponent(component) { 6 | let renderer = Renderer.withComponent(component); 7 | return renderer.allReactComponents(); 8 | } 9 | 10 | describe('find react components', function() { 11 | 12 | describe('the right amount', function() { 13 | it('one', function() { 14 | class Comp extends React.Component { render() { return ; } } 15 | const component = ; 16 | 17 | assert.equal(findReactComponent(component).length, 1); 18 | }); 19 | describe('two', function() { 20 | class Comp extends React.Component { render() { return ; } } 21 | it('as siblings', function() { 22 | const component = ; 23 | 24 | assert.equal(findReactComponent(component).length, 2); 25 | }); 26 | it('nested inside each other', function() { 27 | class Comp extends React.Component { render() { 28 | return {this.props.children}; 29 | } } 30 | class Inner extends React.Component { render() { return ; } } 31 | const component = ; 32 | 33 | assert.equal(findReactComponent(component).length, 2); 34 | }); 35 | }); 36 | }); 37 | }); 38 | --------------------------------------------------------------------------------