Skip to content

Commit 6226b11

Browse files
Merge pull request #158 from airbnb/lmr--reactwrapper-debug
Add .debug() method to ReactWrapper
2 parents 2593b9f + ea4afc9 commit 6226b11

File tree

10 files changed

+300
-7
lines changed

10 files changed

+300
-7
lines changed

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
* [unmount()](/docs/api/ReactWrapper/unmount.md)
8181
* [mount()](/docs/api/ReactWrapper/mount.md)
8282
* [update()](/docs/api/ReactWrapper/update.md)
83+
* [debug()](/docs/api/ReactWrapper/debug.md)
8384
* [type()](/docs/api/ReactWrapper/type.md)
8485
* [forEach(fn)](/docs/api/ReactWrapper/forEach.md)
8586
* [map(fn)](/docs/api/ReactWrapper/map.md)

docs/api/ReactWrapper/debug.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# `.debug() => String`
2+
3+
Returns an HTML-like string of the wrapper for debugging purposes. Useful to print out to the
4+
console when tests are not passing when you expect them to.
5+
6+
7+
#### Returns
8+
9+
`String`: The resulting string.
10+
11+
12+
13+
#### Examples
14+
15+
Say we have the following components:
16+
```jsx
17+
class Foo extends React.Component {
18+
render() {
19+
return (
20+
<div className="foo">
21+
<span>Foo</span>
22+
</div>
23+
);
24+
}
25+
}
26+
27+
class Bar extends React.Component {
28+
render() {
29+
return (
30+
<div className="bar">
31+
<span>Non-Foo</span>
32+
<Foo baz="bax" />
33+
</div>
34+
);
35+
}
36+
}
37+
```
38+
39+
In this case, running:
40+
```jsx
41+
console.log(mount(<Bar id="2" />).debug());
42+
```
43+
44+
Would output the following to the console:
45+
```jsx
46+
<Bar id="2">
47+
<div className="bar">
48+
<span>
49+
Non-Foo
50+
</span>
51+
<Foo baz="bax">
52+
<div className="foo">
53+
<span>
54+
Foo
55+
</span>
56+
</div>
57+
</Foo>
58+
</div>
59+
</Bar>
60+
```
61+
62+
Likewise, running:
63+
64+
```jsx
65+
console.log(mount(<Bar id="2" />).find(Foo).debug();
66+
```
67+
Would output the following to the console:
68+
```jsx
69+
<Foo baz="bax">
70+
<div className="foo">
71+
<span>
72+
Foo
73+
</span>
74+
</div>
75+
</Foo>
76+
```

docs/api/ShallowWrapper/debug.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# `.debug() => String`
22

3-
Returns an html-like string of the wrapper for debugging purposes. Useful to print out to the
3+
Returns an HTML-like string of the wrapper for debugging purposes. Useful to print out to the
44
console when tests are not passing when you expect them to.
55

66

docs/api/mount.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ A method that re-mounts the component.
145145
#### [`.update() => ReactWrapper`](ReactWrapper/update.md)
146146
Calls `.forceUpdate()` on the root component instance.
147147

148+
#### [`.debug() => String`](ReactWrapper/debug.md)
149+
Returns a string representation of the current render tree for debugging purposes.
150+
148151
#### [`.type() => String|Function`](ReactWrapper/type.md)
149152
Returns the type of the current node of the wrapper.
150153

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
"version": "npm run build",
1111
"clean": "rimraf build",
1212
"lint": "eslint src/**",
13-
"test": "npm run lint && npm run tests-only",
14-
"tests-only": "mocha --compilers js:babel-core/register --recursive withDom.js src/**/__tests__/*.js",
1513
"check": "npm run lint && npm run test:all",
1614
"build": "babel src --out-dir build",
17-
"test:only": "mocha --compilers js:babel-core/register --watch withDom.js",
18-
"test:watch": "mocha --compilers js:babel-core/register --recursive withDom.js src/**/__tests__/*.js --watch",
15+
"test": "npm run lint && npm run test:only",
16+
"test:only": "mocha --require withDom.js --compilers js:babel-core/register --recursive src/**/__tests__/*.js",
17+
"test:single": "mocha --require withDom.js --compilers js:babel-core/register --watch",
18+
"test:watch": "mocha --require withDom.js --compilers js:babel-core/register --recursive src/**/__tests__/*.js --watch",
1919
"test:describeWithDOMOnly": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/describeWithDOM/describeWithDOMOnly-spec.js",
2020
"test:describeWithDOMSkip": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/describeWithDOM/describeWithDOMSkip-spec.js",
2121
"test:all": "npm run react:13 && npm test && npm run test:describeWithDOMOnly && npm run test:describeWithDOMSkip && npm run react:14 && npm test && npm run test:describeWithDOMOnly && npm run test:describeWithDOMSkip",
@@ -53,6 +53,7 @@
5353
"cheerio": "^0.20.0",
5454
"is-subset": "^0.1.1",
5555
"object.assign": "^4.0.3",
56+
"object.values": "^1.0.3",
5657
"sinon": "^1.17.3",
5758
"underscore": "^1.8.3"
5859
},

src/Debug.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import {
22
childrenOfNode,
33
} from './ShallowTraversal';
4+
import {
5+
internalInstance,
6+
renderedChildrenOfInst,
7+
} from './MountedTraversal';
8+
import {
9+
isDOMComponent,
10+
isCompositeComponent,
11+
isElement,
12+
} from './react-compat';
413
import {
514
propsOfNode,
615
} from './Utils';
716
import { without, escape, compact } from 'underscore';
17+
import { REACT013, REACT014 } from './version';
18+
import objectValues from 'object.values';
819

920
export function typeName(node) {
1021
return typeof node.type === 'function'
@@ -63,3 +74,59 @@ export function debugNode(node, indentLength = 2) {
6374
export function debugNodes(nodes) {
6475
return nodes.map(debugNode).join('\n\n\n');
6576
}
77+
78+
export function debugInst(inst, indentLength = 2) {
79+
if (typeof inst === 'string' || typeof inst === 'number') return escape(inst);
80+
if (!inst) return '';
81+
82+
if (!inst.getPublicInstance) {
83+
const internal = internalInstance(inst);
84+
return debugInst(internal, indentLength);
85+
}
86+
87+
const publicInst = inst.getPublicInstance();
88+
89+
if (typeof publicInst === 'string' || typeof publicInst === 'number') return escape(publicInst);
90+
if (!publicInst) return '';
91+
92+
// do stuff with publicInst
93+
const currentElement = inst._currentElement;
94+
const type = typeName(currentElement);
95+
const props = propsString(currentElement);
96+
const children = [];
97+
if (isDOMComponent(publicInst)) {
98+
const renderedChildren = renderedChildrenOfInst(inst);
99+
if (!renderedChildren) {
100+
children.push(...childrenOfNode(currentElement));
101+
} else {
102+
children.push(...objectValues(renderedChildren));
103+
}
104+
} else if (
105+
REACT014 &&
106+
isElement(currentElement) &&
107+
typeof currentElement.type === 'function'
108+
) {
109+
children.push(inst._renderedComponent);
110+
} else if (
111+
REACT013 &&
112+
isCompositeComponent(publicInst)
113+
) {
114+
children.push(inst._renderedComponent);
115+
}
116+
117+
const childrenStrs = compact(children.map(n => debugInst(n, indentLength)));
118+
119+
const beforeProps = props ? ' ' : '';
120+
const nodeClose = childrenStrs.length ? `</${type}>` : '/>';
121+
const afterProps = childrenStrs.length
122+
? '>'
123+
: ' ';
124+
const childrenIndented = childrenStrs.length
125+
? `\n${childrenStrs.map(x => indent(indentLength + 2, x)).join('\n')}\n`
126+
: '';
127+
return `<${type}${beforeProps}${props}${afterProps}${childrenIndented}${nodeClose}`;
128+
}
129+
130+
export function debugInsts(insts) {
131+
return insts.map(debugInst).join('\n\n\n');
132+
}

src/ReactWrapper.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import {
2020
mapNativeEventNames,
2121
containsChildrenSubArray,
2222
} from './Utils';
23+
import {
24+
debugInsts,
25+
} from './Debug';
2326

2427
/**
2528
* Finds all nodes in the current wrapper nodes' render trees that match the provided predicate
@@ -699,4 +702,13 @@ export default class ReactWrapper {
699702
}
700703
return new ReactWrapper(node, this.root);
701704
}
705+
706+
/**
707+
* Returns an HTML-like string of the shallow render for debugging purposes.
708+
*
709+
* @returns {String}
710+
*/
711+
debug() {
712+
return debugInsts(this.nodes);
713+
}
702714
}

src/ShallowWrapper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ export default class ShallowWrapper {
686686
}
687687

688688
/**
689-
* Returns an html-like string of the shallow render for debugging purposes.
689+
* Returns an HTML-like string of the shallow render for debugging purposes.
690690
*
691691
* @returns {String}
692692
*/

src/Utils.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export function propsOfNode(node) {
1414
if (REACT013 && node && node._store) {
1515
return (node._store.props) || {};
1616
}
17+
if (node && node._reactInternalComponent && node._reactInternalComponent._currentElement) {
18+
return (node._reactInternalComponent._currentElement.props) || {};
19+
}
1720
return (node && node.props) || {};
1821
}
1922

src/__tests__/Debug-spec.js

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
indent,
66
debugNode,
77
} from '../Debug';
8-
import { itIf } from './_helpers';
8+
import { mount } from '../';
9+
import { describeWithDOM, itIf } from './_helpers';
910
import { REACT013 } from '../version';
1011

1112
describe('debug', () => {
@@ -188,4 +189,133 @@ describe('debug', () => {
188189

189190
});
190191

192+
describeWithDOM('debugInst(inst)', () => {
193+
it('renders basic debug of mounted components', () => {
194+
class Foo extends React.Component {
195+
render() {
196+
return (
197+
<div className="foo">
198+
<span>Foo</span>
199+
</div>
200+
);
201+
}
202+
}
203+
expect(mount(<Foo id="2" />).debug()).to.eql(
204+
`<Foo id="2">
205+
<div className="foo">
206+
<span>
207+
Foo
208+
</span>
209+
</div>
210+
</Foo>`);
211+
});
212+
213+
it('renders debug of compositional components', () => {
214+
class Foo extends React.Component {
215+
render() {
216+
return (
217+
<div className="foo">
218+
<span>Foo</span>
219+
</div>
220+
);
221+
}
222+
}
223+
class Bar extends React.Component {
224+
render() {
225+
return (
226+
<div className="bar">
227+
<span>Non-Foo</span>
228+
<Foo baz="bax" />
229+
</div>
230+
);
231+
}
232+
}
233+
expect(mount(<Bar id="2" />).debug()).to.eql(
234+
`<Bar id="2">
235+
<div className="bar">
236+
<span>
237+
Non-Foo
238+
</span>
239+
<Foo baz="bax">
240+
<div className="foo">
241+
<span>
242+
Foo
243+
</span>
244+
</div>
245+
</Foo>
246+
</div>
247+
</Bar>`);
248+
});
249+
250+
it('renders a subtree of a mounted tree', () => {
251+
class Foo extends React.Component {
252+
render() {
253+
return (
254+
<div className="foo">
255+
<span>Foo</span>
256+
</div>
257+
);
258+
}
259+
}
260+
class Bar extends React.Component {
261+
render() {
262+
return (
263+
<div className="bar">
264+
<span>Non-Foo</span>
265+
<Foo baz="bax" />
266+
</div>
267+
);
268+
}
269+
}
270+
expect(mount(<Bar id="2" />).find(Foo).debug()).to.eql(
271+
`<Foo baz="bax">
272+
<div className="foo">
273+
<span>
274+
Foo
275+
</span>
276+
</div>
277+
</Foo>`);
278+
});
279+
280+
it('renders passed children properly', () => {
281+
class Foo extends React.Component {
282+
render() {
283+
return (
284+
<div className="foo">
285+
<span>From Foo</span>
286+
{this.props.children}
287+
</div>
288+
);
289+
}
290+
}
291+
class Bar extends React.Component {
292+
render() {
293+
return (
294+
<div className="bar">
295+
<Foo baz="bax">
296+
<span>From Bar</span>
297+
</Foo>
298+
</div>
299+
);
300+
}
301+
}
302+
303+
expect(mount(<Bar id="2" />).debug()).to.eql(
304+
`<Bar id="2">
305+
<div className="bar">
306+
<Foo baz="bax">
307+
<div className="foo">
308+
<span>
309+
From Foo
310+
</span>
311+
<span>
312+
From Bar
313+
</span>
314+
</div>
315+
</Foo>
316+
</div>
317+
</Bar>`);
318+
319+
});
320+
});
191321
});

0 commit comments

Comments
 (0)