├── 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 |
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 |
79 |
80 |
81 |
82 |
83 |
84 |
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 |
--------------------------------------------------------------------------------