diff --git a/docs/README.md b/docs/README.md index 5c6a1721a..bac5a2d17 100644 --- a/docs/README.md +++ b/docs/README.md @@ -80,6 +80,7 @@ * [unmount()](/docs/api/ReactWrapper/unmount.md) * [mount()](/docs/api/ReactWrapper/mount.md) * [update()](/docs/api/ReactWrapper/update.md) + * [debug()](/docs/api/ReactWrapper/debug.md) * [type()](/docs/api/ReactWrapper/type.md) * [forEach(fn)](/docs/api/ReactWrapper/forEach.md) * [map(fn)](/docs/api/ReactWrapper/map.md) diff --git a/docs/api/ReactWrapper/debug.md b/docs/api/ReactWrapper/debug.md new file mode 100644 index 000000000..5ae11b14c --- /dev/null +++ b/docs/api/ReactWrapper/debug.md @@ -0,0 +1,76 @@ +# `.debug() => String` + +Returns an HTML-like string of the wrapper for debugging purposes. Useful to print out to the +console when tests are not passing when you expect them to. + + +#### Returns + +`String`: The resulting string. + + + +#### Examples + +Say we have the following components: +```jsx +class Foo extends React.Component { + render() { + return ( +
+ Foo +
+ ); + } +} + +class Bar extends React.Component { + render() { + return ( +
+ Non-Foo + +
+ ); + } +} +``` + +In this case, running: +```jsx +console.log(mount().debug()); +``` + +Would output the following to the console: +```jsx + +
+ + Non-Foo + + +
+ + Foo + +
+
+
+
+``` + +Likewise, running: + +```jsx +console.log(mount().find(Foo).debug(); +``` +Would output the following to the console: +```jsx + +
+ + Foo + +
+
+``` diff --git a/docs/api/ShallowWrapper/debug.md b/docs/api/ShallowWrapper/debug.md index b97a7efa4..2ceb54bcb 100644 --- a/docs/api/ShallowWrapper/debug.md +++ b/docs/api/ShallowWrapper/debug.md @@ -1,6 +1,6 @@ # `.debug() => String` -Returns an html-like string of the wrapper for debugging purposes. Useful to print out to the +Returns an HTML-like string of the wrapper for debugging purposes. Useful to print out to the console when tests are not passing when you expect them to. diff --git a/docs/api/mount.md b/docs/api/mount.md index 12ed68b22..ee1484b66 100644 --- a/docs/api/mount.md +++ b/docs/api/mount.md @@ -145,6 +145,9 @@ A method that re-mounts the component. #### [`.update() => ReactWrapper`](ReactWrapper/update.md) Calls `.forceUpdate()` on the root component instance. +#### [`.debug() => String`](ReactWrapper/debug.md) +Returns a string representation of the current render tree for debugging purposes. + #### [`.type() => String|Function`](ReactWrapper/type.md) Returns the type of the current node of the wrapper. diff --git a/package.json b/package.json index 8986859e9..68def65a4 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,12 @@ "version": "npm run build", "clean": "rimraf build", "lint": "eslint src/**", - "test": "npm run lint && npm run tests-only", - "tests-only": "mocha --compilers js:babel-core/register --recursive withDom.js src/**/__tests__/*.js", "check": "npm run lint && npm run test:all", "build": "babel src --out-dir build", - "test:only": "mocha --compilers js:babel-core/register --watch withDom.js", - "test:watch": "mocha --compilers js:babel-core/register --recursive withDom.js src/**/__tests__/*.js --watch", + "test": "npm run lint && npm run test:only", + "test:only": "mocha --require withDom.js --compilers js:babel-core/register --recursive src/**/__tests__/*.js", + "test:single": "mocha --require withDom.js --compilers js:babel-core/register --watch", + "test:watch": "mocha --require withDom.js --compilers js:babel-core/register --recursive src/**/__tests__/*.js --watch", "test:describeWithDOMOnly": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/describeWithDOM/describeWithDOMOnly-spec.js", "test:describeWithDOMSkip": "mocha --compilers js:babel-core/register --recursive src/**/__tests__/describeWithDOM/describeWithDOMSkip-spec.js", "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 @@ "cheerio": "^0.20.0", "is-subset": "^0.1.1", "object.assign": "^4.0.3", + "object.values": "^1.0.3", "sinon": "^1.17.3", "underscore": "^1.8.3" }, diff --git a/src/Debug.js b/src/Debug.js index 829296ead..2d16d03b9 100644 --- a/src/Debug.js +++ b/src/Debug.js @@ -1,10 +1,21 @@ import { childrenOfNode, } from './ShallowTraversal'; +import { + internalInstance, + renderedChildrenOfInst, +} from './MountedTraversal'; +import { + isDOMComponent, + isCompositeComponent, + isElement, +} from './react-compat'; import { propsOfNode, } from './Utils'; import { without, escape, compact } from 'underscore'; +import { REACT013, REACT014 } from './version'; +import objectValues from 'object.values'; export function typeName(node) { return typeof node.type === 'function' @@ -63,3 +74,59 @@ export function debugNode(node, indentLength = 2) { export function debugNodes(nodes) { return nodes.map(debugNode).join('\n\n\n'); } + +export function debugInst(inst, indentLength = 2) { + if (typeof inst === 'string' || typeof inst === 'number') return escape(inst); + if (!inst) return ''; + + if (!inst.getPublicInstance) { + const internal = internalInstance(inst); + return debugInst(internal, indentLength); + } + + const publicInst = inst.getPublicInstance(); + + if (typeof publicInst === 'string' || typeof publicInst === 'number') return escape(publicInst); + if (!publicInst) return ''; + + // do stuff with publicInst + const currentElement = inst._currentElement; + const type = typeName(currentElement); + const props = propsString(currentElement); + const children = []; + if (isDOMComponent(publicInst)) { + const renderedChildren = renderedChildrenOfInst(inst); + if (!renderedChildren) { + children.push(...childrenOfNode(currentElement)); + } else { + children.push(...objectValues(renderedChildren)); + } + } else if ( + REACT014 && + isElement(currentElement) && + typeof currentElement.type === 'function' + ) { + children.push(inst._renderedComponent); + } else if ( + REACT013 && + isCompositeComponent(publicInst) + ) { + children.push(inst._renderedComponent); + } + + const childrenStrs = compact(children.map(n => debugInst(n, indentLength))); + + const beforeProps = props ? ' ' : ''; + const nodeClose = childrenStrs.length ? `` : '/>'; + const afterProps = childrenStrs.length + ? '>' + : ' '; + const childrenIndented = childrenStrs.length + ? `\n${childrenStrs.map(x => indent(indentLength + 2, x)).join('\n')}\n` + : ''; + return `<${type}${beforeProps}${props}${afterProps}${childrenIndented}${nodeClose}`; +} + +export function debugInsts(insts) { + return insts.map(debugInst).join('\n\n\n'); +} diff --git a/src/ReactWrapper.js b/src/ReactWrapper.js index 9af154404..61dae3b76 100644 --- a/src/ReactWrapper.js +++ b/src/ReactWrapper.js @@ -20,6 +20,9 @@ import { mapNativeEventNames, containsChildrenSubArray, } from './Utils'; +import { + debugInsts, +} from './Debug'; /** * Finds all nodes in the current wrapper nodes' render trees that match the provided predicate @@ -699,4 +702,13 @@ export default class ReactWrapper { } return new ReactWrapper(node, this.root); } + + /** + * Returns an HTML-like string of the shallow render for debugging purposes. + * + * @returns {String} + */ + debug() { + return debugInsts(this.nodes); + } } diff --git a/src/ShallowWrapper.js b/src/ShallowWrapper.js index 424355e55..9616db946 100644 --- a/src/ShallowWrapper.js +++ b/src/ShallowWrapper.js @@ -686,7 +686,7 @@ export default class ShallowWrapper { } /** - * Returns an html-like string of the shallow render for debugging purposes. + * Returns an HTML-like string of the shallow render for debugging purposes. * * @returns {String} */ diff --git a/src/Utils.js b/src/Utils.js index 0f9021c1c..7d123001e 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -14,6 +14,9 @@ export function propsOfNode(node) { if (REACT013 && node && node._store) { return (node._store.props) || {}; } + if (node && node._reactInternalComponent && node._reactInternalComponent._currentElement) { + return (node._reactInternalComponent._currentElement.props) || {}; + } return (node && node.props) || {}; } diff --git a/src/__tests__/Debug-spec.js b/src/__tests__/Debug-spec.js index cc14aa438..fcfc56d19 100644 --- a/src/__tests__/Debug-spec.js +++ b/src/__tests__/Debug-spec.js @@ -5,7 +5,8 @@ import { indent, debugNode, } from '../Debug'; -import { itIf } from './_helpers'; +import { mount } from '../'; +import { describeWithDOM, itIf } from './_helpers'; import { REACT013 } from '../version'; describe('debug', () => { @@ -188,4 +189,133 @@ describe('debug', () => { }); + describeWithDOM('debugInst(inst)', () => { + it('renders basic debug of mounted components', () => { + class Foo extends React.Component { + render() { + return ( +
+ Foo +
+ ); + } + } + expect(mount().debug()).to.eql( +` +
+ + Foo + +
+
`); + }); + + it('renders debug of compositional components', () => { + class Foo extends React.Component { + render() { + return ( +
+ Foo +
+ ); + } + } + class Bar extends React.Component { + render() { + return ( +
+ Non-Foo + +
+ ); + } + } + expect(mount().debug()).to.eql( + ` +
+ + Non-Foo + + +
+ + Foo + +
+
+
+
`); + }); + + it('renders a subtree of a mounted tree', () => { + class Foo extends React.Component { + render() { + return ( +
+ Foo +
+ ); + } + } + class Bar extends React.Component { + render() { + return ( +
+ Non-Foo + +
+ ); + } + } + expect(mount().find(Foo).debug()).to.eql( + ` +
+ + Foo + +
+
`); + }); + + it('renders passed children properly', () => { + class Foo extends React.Component { + render() { + return ( +
+ From Foo + {this.props.children} +
+ ); + } + } + class Bar extends React.Component { + render() { + return ( +
+ + From Bar + +
+ ); + } + } + + expect(mount().debug()).to.eql( +` +
+ +
+ + From Foo + + + From Bar + +
+
+
+
`); + + }); + }); });