├── .gitignore ├── .gitmodules ├── babel-require.js ├── .npmignore ├── .babelrc ├── src ├── .babelrc ├── componentMap.js ├── testHelpers │ └── emulateDom.js ├── globalHook.js └── tests │ ├── componentMap.spec.js │ └── hook.spec.js ├── CHANGELOG.md ├── package.json ├── wallaby.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/react-devtools"] 2 | path = src/react-devtools 3 | url = https://github.com/facebook/react-devtools.git 4 | -------------------------------------------------------------------------------- /babel-require.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | ignore: ['node_modules/'], 3 | babelrcRoots: [ 4 | '.', 5 | './src/react-dev-tool' 6 | ] 7 | }); 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/react-devtools/ 3 | !lib/react-devtools/backend/ 4 | wallaby.js 5 | lib/testHelpers/ 6 | lib/tests/ 7 | src/testHelpers 8 | src/react-devtools/ 9 | !src/react-devtools/backend/ 10 | src/tests/ 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@babel/plugin-transform-flow-strip-types"], 4 | ["@babel/plugin-proposal-class-properties", { "loose": false }] 5 | ], 6 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"] 7 | } 8 | -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@babel/plugin-transform-flow-strip-types"], 4 | ["@babel/plugin-proposal-class-properties", { "loose": false }] 5 | ], 6 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"] 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.1.1 4 | Initial release 5 | 6 | ## v0.1.2 7 | Update devtools to fix issue with rendering numerical 0 8 | 9 | ## v0.1.3 10 | Remove react peer dependencies (thanks Christian Hoffmeister @choffmeister) 11 | 12 | ## v0.1.4 13 | Updates and further information in package.json (thanks @valscion) 14 | 15 | ## v1.0.0 16 | Update to support fiber (React v16) 17 | Converted to use ES6 Maps with objects as keys (this must be supported by the runtime - Node v4 onwards is fine) 18 | 19 | ## v1.0.1 20 | Update devDependencies 21 | 22 | ## v1.1.0 23 | React 16.5.0 compatibility by updating devtools. Thanks @albertfdp for tracking this down! 24 | 25 | ## v1.1.1 26 | Add fbjs as a dependency needed by devtools in some circumstances (the tests in this package weren't affected, not sure why yet) 27 | 28 | ## v2.0.0 29 | Update react-devtool to support react 16.9 onwards 30 | -------------------------------------------------------------------------------- /src/componentMap.js: -------------------------------------------------------------------------------- 1 | 2 | const publicInstanceToData = new Map(); 3 | const privateInstanceToData = new Map(); 4 | 5 | exports.mount = function (component) { 6 | if (component.internalInstance.stateNode) { 7 | publicInstanceToData.set(component.internalInstance.stateNode, component) 8 | } 9 | privateInstanceToData.set(component.internalInstance, component); 10 | }; 11 | 12 | exports.update = function (component) { 13 | const existing = exports.findInternalComponent(component.internalInstance); 14 | if (existing) { 15 | existing.data = component.data; 16 | } 17 | }; 18 | 19 | exports.findComponent = function (component) { 20 | return publicInstanceToData.get(component) || null; 21 | }; 22 | 23 | exports.findInternalComponent = function (internalComponent) { 24 | return privateInstanceToData.get(internalComponent) || null; 25 | }; 26 | 27 | exports.clearAll = function () { 28 | publicInstanceToData.clear(); 29 | }; 30 | -------------------------------------------------------------------------------- /src/testHelpers/emulateDom.js: -------------------------------------------------------------------------------- 1 | 2 | if (typeof document === 'undefined') { 3 | 4 | require('jsdom-global')(); 5 | 6 | // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 7 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 8 | 9 | // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel 10 | 11 | // MIT license 12 | 13 | (function() { 14 | var lastTime = 0; 15 | var vendors = ['ms', 'moz', 'webkit', 'o']; 16 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 17 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 18 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 19 | || window[vendors[x]+'CancelRequestAnimationFrame']; 20 | } 21 | 22 | if (!window.requestAnimationFrame) 23 | window.requestAnimationFrame = function(callback, element) { 24 | var currTime = new Date().getTime(); 25 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 26 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 27 | timeToCall); 28 | lastTime = currTime + timeToCall; 29 | return id; 30 | }; 31 | 32 | if (!window.cancelAnimationFrame) 33 | window.cancelAnimationFrame = function(id) { 34 | clearTimeout(id); 35 | }; 36 | global.requestAnimationFrame = window.requestAnimationFrame; 37 | global.cancelAnimationFrame = window.cancelAnimationFrame; 38 | }()); 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-render-hook", 3 | "version": "2.0.0", 4 | "description": "Hooks to React.js rendering, to enable access to the virtual DOM (as shown in React devtools)", 5 | "scripts": { 6 | "build": "babel src -d lib --source-maps true --config-file ./.babelrc", 7 | "prepare": "npm run build", 8 | "test": "mocha --require babel-require.js src/tests/**/*.spec.js" 9 | }, 10 | "main": "lib/globalHook.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/bruderstein/react-render-hook.git" 14 | }, 15 | "author": "Dave Brotherstone ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/bruderstein/react-render-hook/issues" 19 | }, 20 | "homepage": "https://github.com/bruderstein/react-render-hook", 21 | "devDependencies": { 22 | "@babel/cli": "^7.7.4", 23 | "@babel/core": "^7.7.4", 24 | "@babel/plugin-proposal-class-properties": "^7.7.4", 25 | "@babel/plugin-transform-flow-strip-types": "^7.7.4", 26 | "@babel/preset-env": "^7.7.4", 27 | "@babel/preset-flow": "^7.7.4", 28 | "@babel/preset-react": "^7.7.4", 29 | "@babel/register": "^7.7.4", 30 | "create-react-class": "^15.6.0", 31 | "jsdom": "^11.1.0", 32 | "jsdom-global": "^3.0.2", 33 | "magicpen": "^5.4.0", 34 | "mocha": "^6.2.2", 35 | "object-assign": "^4.0.1", 36 | "react": "^16.12.0", 37 | "react-dom": "^16.12.0", 38 | "unexpected": "^10.0.2" 39 | }, 40 | "files": [ 41 | "lib/*.js", 42 | "lib/react-devtools/backend/*.js", 43 | "README.md", 44 | "src/*.js", 45 | "src/react-devtools/backend/*.js" 46 | ], 47 | "dependencies": { 48 | "fbjs": "^1.0.0", 49 | "semver": "^5.5.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var Babel = require('babel-core'); 4 | 5 | module.exports = function (wallaby) { 6 | 7 | return { 8 | files: [ 9 | 'src/**/*.js', 10 | { 11 | pattern: 'src/**/tests/*.spec.js', 12 | ignore: true 13 | }, 14 | { 15 | pattern: 'src/react-devtools/**/*.js', 16 | instrument: false 17 | }, 18 | { 19 | pattern: 'src/testHelpers/**/*', 20 | instrument: false 21 | }, 22 | { 23 | pattern: 'src/react-devtools/frontend/**/*.js', 24 | ignore: true 25 | }, 26 | { 27 | pattern: 'src/react-devtools/agent/**/*.js', 28 | ignore: true 29 | }, 30 | { 31 | pattern: 'src/react-devtools/plugins/**/*.js', 32 | ignore: true 33 | }], 34 | 35 | tests: ['src/**/*.spec.js'], 36 | env: { 37 | type: 'node', 38 | runner: 'node' 39 | }, 40 | 41 | debug: true, 42 | 43 | workers: { 44 | /** 45 | * We need to recycle the node processes in order to get the React injection 46 | * The 'react' module is not removed from the cache, but the local file 'globalHook.js' is 47 | * This means that the event handler for 'renderer-attached' is recreated, but the `inject` call 48 | * (which fires the renderer-attached event) in react is only performed when react is require()d 49 | * for the first time. 50 | */ 51 | 52 | recycle: true 53 | }, 54 | 55 | compilers: { 56 | '**/*.js': wallaby.compilers.babel({ 57 | babel: Babel 58 | }), 59 | 'src/**/*.jsx': wallaby.compilers.babel({ 60 | babel: Babel 61 | }) 62 | } 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /src/globalHook.js: -------------------------------------------------------------------------------- 1 | const Backend = require('./react-devtools/backend/backend'); 2 | const GlobalHook = require('./react-devtools/backend/installGlobalHook'); 3 | 4 | const ComponentMap = require('./componentMap'); 5 | 6 | if (typeof global.__REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined') { 7 | delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__; 8 | } 9 | if (typeof window !== 'undefined' && typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined') { 10 | delete window.__REACT_DEVTOOLS_GLOBAL_HOOK__; 11 | } 12 | 13 | 14 | // Install the global hook 15 | const tempGlobal = {}; 16 | 17 | GlobalHook(tempGlobal); 18 | const hook = tempGlobal.__REACT_DEVTOOLS_GLOBAL_HOOK__; 19 | 20 | 21 | if (typeof global !== 'undefined') { 22 | global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook; 23 | } 24 | 25 | if (typeof window !== 'undefined') { 26 | window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook; 27 | } 28 | 29 | let globalWindowTempDefined = false; 30 | if (typeof window === 'undefined' && typeof global !== 'undefined') { 31 | // If we've got no DOM emulation, and we're in a node environment 32 | // we'll emulate window just while we install the GlobalHook, as that expects window to be there 33 | global.window = {}; 34 | globalWindowTempDefined = true; 35 | } 36 | 37 | // Inject the backend 38 | Backend(hook); 39 | 40 | if (globalWindowTempDefined) { 41 | // We can remove our temporary object, now that Backend has done its thing 42 | delete global.window; 43 | } 44 | 45 | const exported = { 46 | hook, 47 | cleanup() { 48 | ComponentMap.clearAll(); 49 | } 50 | }; 51 | 52 | hook.sub('renderer-attached', function (args) { 53 | exported.rendererId = args.renderer; 54 | exported.helpers = args.helpers; 55 | exported.isAttached = true; 56 | }); 57 | 58 | 59 | hook.sub('mount', component => { 60 | ComponentMap.mount(component); 61 | }); 62 | 63 | hook.sub('update', component => { 64 | ComponentMap.update(component); 65 | }); 66 | 67 | exported.findComponent = function (component) { 68 | return ComponentMap.findComponent(component); 69 | }; 70 | 71 | exported.findInternalComponent = function (internalComponent) { 72 | return ComponentMap.findInternalComponent(internalComponent); 73 | }; 74 | 75 | 76 | function isRawType(value) { 77 | var type = typeof value; 78 | return type === 'string' || 79 | type === 'number' || 80 | type === 'boolean' || 81 | type === 'undefined' || 82 | value === null; 83 | } 84 | 85 | exported.findChildren = function (component) { 86 | 87 | let internalComponent; 88 | if (component && component.data && component.data.publicInstance) { 89 | internalComponent = component; 90 | } else { 91 | internalComponent = exported.findComponent(component); 92 | } 93 | 94 | if (internalComponent && internalComponent.data.children) { 95 | if (isRawType(internalComponent.data.children)) { 96 | return [internalComponent.data.children]; 97 | } 98 | 99 | return internalComponent.data.children.map(child => { 100 | const renderedChild = exported.findInternalComponent(child); 101 | if (renderedChild.data.nodeType === 'NativeWrapper') { 102 | return exported.findInternalComponent(renderedChild.data.children[0]); 103 | } 104 | return renderedChild; 105 | }); 106 | } 107 | }; 108 | 109 | exported.clearAll = ComponentMap.clearAll; 110 | 111 | module.exports = exported; 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-render-hook 2 | 3 | `react-render-hook` uses the [React-devtools](https://github.com/facebook/react-devtools) hook to watch for rendering React components, 4 | and provide an interface to get to the virtual DOM tree and find rendered elements and get the metadata about them. 5 | 6 | This is meant to be used from libraries that need access to the whole virtual DOM tree, such as testing/assertion libraries. 7 | 8 | It is used from [unexpected-react](https://github.com/bruderstein/unexpected-react) as a plugin for [unexpected](http://unexpected.js.org) 9 | to allow assertions over the DOM tree. This package is extracted as a separate package, to allow for other implementations for other assertion 10 | libraries. 11 | 12 | 13 | This package uses [React-devtools](https://github.com/facebook/react-devtools) as a git submodule, such that it can keep up-to-date with it. However, 14 | only a small part of the devtools is actually used (specifically, the `backend` directory), so it is not anticipated that there will be many updates to this. 15 | 16 | # Usage 17 | As React only checks for the global devtools hook when it is first 'required', `react-render-hook` *must* be included *before* `react` is included. 18 | You can validate that this is the case by checking the `isAttached` property. 19 | 20 | # API 21 | 22 | ## isAttached (boolean) 23 | 24 | This is set to true when React attaches to the devtools. This should happen immediately after `react` is `require`d. 25 | 26 | e.g. 27 | ```js 28 | var RenderHook = require('react-render-hook'); 29 | 30 | assert(RenderHook.isAttached === false); 31 | 32 | var React = require('react'); 33 | 34 | assert(RenderHook.isAttached === true); 35 | ``` 36 | 37 | As `react` is often required as part of components etc, it is recommended to check this property 38 | before calling the other APIs, such that a useful error message can be given to the user. 39 | 40 | ## findComponent(component) 41 | 42 | Returns the `devtools` representation of the `component`. `component` is the value returned from `React.render()`, or any 43 | valid instance from the children (e.g. `ref`s, etc.). 44 | 45 | The `devtools` representation currently has the following properties (this is currently more-or-less worked out via guess work, 46 | improvements / links to better documentation greatly appreciated) 47 | * `element` - the element instance 48 | * `data` - metadata about the instance. Contains the following important properties 49 | ** `nodeType` - the type of node, either `'Native'`, `'Text'` or `'Composite'` (`'NativeWrapper'` exists too, but that is skipped over and 50 | the `'Native'` child is returned) 51 | ** `type` - the type of the element. This corresponds to the `React.createClass` or `class` that extends `React.Component` that 52 | created the element, or is a string value of the native node. 53 | ** `name` - tag name of the node 54 | ** `props` - props passed to the render method 55 | 56 | ## findChildren(component) 57 | 58 | Returns an array of the `devtools` representation of the `component`'s rendered children. Note that this is not the children passed in to the 59 | render method as the `children` prop, rather the result of the render method of the given component. The `component` parameter can be either a 60 | component instance (like that returned from `React.render()`) or an `devtools` representation of the element, such as that returned from the 61 | `findComponent` method. 62 | 63 | ## clearAll() 64 | 65 | `react-render-hook` maintains an index of mounted components. In a testing scenario, it may be helpful to occassionally clear this index out, as there can be 66 | no way for `react-render-hook` to know that the test is completed and the data can be thrown away. Memory will continue to be used whenever React renders a 67 | component. 68 | 69 | # Contributing 70 | 71 | Contributions are very welcome. This project was made without really any knowledge of the devtools internals, so it is not unlikely that some 72 | things are sub-optimal, not best practice, or just plain wrong. The tests show that the rendered content is caught and located by the API. 73 | 74 | 75 | # License 76 | 77 | MIT. 78 | -------------------------------------------------------------------------------- /src/tests/componentMap.spec.js: -------------------------------------------------------------------------------- 1 | import Unexpected from 'unexpected'; 2 | 3 | import ComponentMap from '../componentMap'; 4 | 5 | const expect = Unexpected.clone(); 6 | 7 | function TestElement(name) { 8 | this.props = {}; 9 | // Just some property for identification later - doesn't matter what it is 10 | this.name = name; 11 | } 12 | 13 | function getInternalInstance(publicInstance) { 14 | return { 15 | stateNode: publicInstance 16 | }; 17 | } 18 | 19 | describe('componentMap', () => { 20 | 21 | 22 | let testInstance1, testInternalInstance1; 23 | let testInstance2, testInternalInstance2; 24 | let testInstance2b, testInternalInstance2b; 25 | beforeEach(function () { 26 | 27 | testInstance1 = new TestElement('one'); 28 | testInternalInstance1 = getInternalInstance(testInstance1); 29 | testInstance2 = new TestElement('two'); 30 | testInternalInstance2 = getInternalInstance(testInstance2); 31 | testInstance2b = new TestElement('two -second'); 32 | testInternalInstance2b = getInternalInstance(testInstance2b); 33 | }); 34 | 35 | it('finds a single added component', () => { 36 | 37 | ComponentMap.mount({ 38 | internalInstance: testInternalInstance1, 39 | data: { test: 123, publicInstance: testInstance1 } 40 | }); 41 | 42 | const located = ComponentMap.findComponent(testInstance1); 43 | expect(located, 'to satisfy', { 44 | data: { test: 123 } 45 | }); 46 | 47 | }); 48 | 49 | it('finds a component when two different roots are added', () => { 50 | 51 | ComponentMap.mount({ 52 | internalInstance: testInternalInstance1, 53 | data: { test: 123, publicInstance: testInstance1 } 54 | }); 55 | 56 | ComponentMap.mount({ 57 | internalInstance: testInternalInstance2, 58 | data: { test: 123, publicInstance: testInstance2 } 59 | }); 60 | 61 | const located = ComponentMap.findComponent(testInstance1); 62 | expect(located, 'to satisfy', { 63 | data: { test: 123 } 64 | }); 65 | }); 66 | 67 | it('finds a component when two different components in the same root', () => { 68 | 69 | ComponentMap.mount({ 70 | internalInstance: testInternalInstance2, 71 | data: { test: 123, publicInstance: testInstance2 } 72 | }); 73 | 74 | ComponentMap.mount({ 75 | internalInstance: testInternalInstance2b, 76 | data: { test: 234, publicInstance: testInstance2b } 77 | }); 78 | 79 | const located = ComponentMap.findComponent(testInstance2); 80 | expect(located, 'to satisfy', { 81 | data: { test: 123 } 82 | }); 83 | 84 | const second = ComponentMap.findComponent(testInstance2b); 85 | expect(second, 'to satisfy', { 86 | data: { test: 234 } 87 | }); 88 | }); 89 | 90 | it('does not find a component after clearAll()', () => { 91 | 92 | ComponentMap.mount({ 93 | internalInstance: testInternalInstance2, 94 | data: { test: 123, publicInstance: testInstance2 } 95 | }); 96 | 97 | ComponentMap.clearAll(); 98 | 99 | const located = ComponentMap.findComponent(testInstance2); 100 | expect(located, 'to be null'); 101 | }); 102 | 103 | it('allows updating an instance', () => { 104 | 105 | ComponentMap.mount({ 106 | internalInstance: testInternalInstance2, 107 | data: { test: 123, publicInstance: testInstance2 } 108 | }); 109 | 110 | ComponentMap.update({ 111 | internalInstance: testInternalInstance2, 112 | data: { test: 234, publicInstance: testInstance2 } 113 | }); 114 | const located = ComponentMap.findComponent(testInstance2); 115 | expect(located, 'to satisfy', { data: { test: 234 } }); 116 | }); 117 | 118 | it('ignores updates to unknown instances', () => { 119 | 120 | // This appears to happen when the shallow renderer is used 121 | ComponentMap.clearAll(); 122 | 123 | ComponentMap.update({ 124 | internalInstance: testInternalInstance2, 125 | data: { test: 234, publicInstance: testInstance2 } 126 | }); 127 | const located = ComponentMap.findComponent(testInstance2); 128 | expect(located, 'to be null'); 129 | }); 130 | 131 | }); 132 | -------------------------------------------------------------------------------- /src/tests/hook.spec.js: -------------------------------------------------------------------------------- 1 | 2 | import EmulateDom from '../testHelpers/emulateDom'; 3 | 4 | import GlobalHook from '../globalHook'; 5 | 6 | import React from 'react'; 7 | import TestUtils from 'react-dom/test-utils'; 8 | import createClass from 'create-react-class'; 9 | import Unexpected from 'unexpected'; 10 | 11 | const expect = Unexpected.clone(); 12 | 13 | 14 | const versionParts = React.version.split('.'); 15 | const isReact014 = (parseFloat(versionParts[0] + '.' + versionParts[1]) >= 0.14); 16 | 17 | const TestComponent = createClass({ 18 | 19 | displayName: 'TestComponent', 20 | render() { 21 | return
Test Component
; 22 | } 23 | }); 24 | 25 | class ClassComponent extends React.Component { 26 | 27 | constructor() { 28 | super(); 29 | this.state = { }; 30 | 31 | this.onClick = this.onClick.bind(this); 32 | 33 | } 34 | 35 | onClick() { 36 | this.setState({ 37 | id: 'clicked' 38 | }); 39 | } 40 | 41 | render() { 42 | return
{this.props.injectChildren}
; 43 | } 44 | } 45 | 46 | function StatelessComponent(props) { 47 | return
Stateless content
; 48 | } 49 | 50 | class StatelessWrapper extends React.Component { 51 | render() { 52 | return React.Children.only(this.props.children); 53 | } 54 | } 55 | 56 | 57 | describe('react-render-hook', () => { 58 | 59 | it('identifies as attached', () => { 60 | 61 | expect(GlobalHook.isAttached, 'to be true'); 62 | }); 63 | 64 | describe('findComponent', () => { 65 | 66 | 67 | it('finds a component using renderIntoDocument', () => { 68 | 69 | const component = TestUtils.renderIntoDocument(); 70 | 71 | const locatedComponent = GlobalHook.findComponent(component); 72 | 73 | expect(locatedComponent, 'to satisfy', { 74 | data: { 75 | nodeType: 'Composite', 76 | type: expect.it('to be', TestComponent), // Unexpected calls functions that are in 'to satisfy' 77 | name: 'TestComponent', // Hence the extra `expect.it()` 78 | publicInstance: { 79 | props: { className: 'foo' } 80 | } 81 | } 82 | }); 83 | 84 | }); 85 | 86 | it('finds the correct component when rendering more than one', () => { 87 | 88 | const component = TestUtils.renderIntoDocument(); 89 | const component2 = TestUtils.renderIntoDocument(); 90 | 91 | const locatedComponent = GlobalHook.findComponent(component); 92 | const locatedComponent2 = GlobalHook.findComponent(component2); 93 | 94 | expect(locatedComponent, 'to satisfy', { 95 | data: { 96 | nodeType: 'Composite', 97 | type: expect.it('to be', TestComponent), // Unexpected calls functions that are in 'to satisfy' 98 | name: 'TestComponent', // Hence the extra `expect.it()` 99 | publicInstance: { 100 | props: { className: 'foo' } 101 | } 102 | } 103 | }); 104 | 105 | expect(locatedComponent2, 'to satisfy', { 106 | data: { 107 | nodeType: 'Composite', 108 | type: expect.it('to be', TestComponent), // Unexpected calls functions that are in 'to satisfy' 109 | name: 'TestComponent', // Hence the extra `expect.it()` 110 | publicInstance: { 111 | props: { className: 'bar' } 112 | } 113 | } 114 | }); 115 | }); 116 | 117 | it('finds a class component', () => { 118 | 119 | const component = TestUtils.renderIntoDocument(); 120 | 121 | const locatedComponent = GlobalHook.findComponent(component); 122 | 123 | expect(locatedComponent, 'to satisfy', { 124 | data: { 125 | nodeType: 'Composite', 126 | type: expect.it('to be', ClassComponent), 127 | name: 'ClassComponent', 128 | publicInstance: expect.it('to be an', 'object') 129 | } 130 | }); 131 | }); 132 | 133 | it('clears the collection on clearAll()', () => { 134 | 135 | const component = TestUtils.renderIntoDocument(); 136 | 137 | let locatedComponent = GlobalHook.findComponent(component); 138 | 139 | expect(locatedComponent, 'not to be null'); 140 | 141 | GlobalHook.clearAll(); 142 | 143 | locatedComponent = GlobalHook.findComponent(component); 144 | 145 | expect(locatedComponent, 'to be null'); 146 | }); 147 | }); 148 | 149 | describe('findChildren', () => { 150 | 151 | describe('with a native->string child', () => { 152 | 153 | let component; 154 | 155 | beforeEach(() => { 156 | 157 | component = TestUtils.renderIntoDocument(); 158 | // renders
foo
159 | }); 160 | 161 | it('returns a native node', () => { 162 | 163 | const locatedComponent = GlobalHook.findChildren(component); 164 | 165 | expect(locatedComponent, 'to satisfy', [{ 166 | data: { 167 | nodeType: 'Native', 168 | type: 'div', 169 | props: { 170 | className: 'class-component' 171 | }, 172 | publicInstance: expect.it('to be an', 'object'), 173 | } 174 | }]); 175 | }); 176 | 177 | it('returns the string child of the first child', () => { 178 | 179 | const theDiv = GlobalHook.findChildren(component); 180 | const divContent = GlobalHook.findChildren(theDiv[0]); 181 | 182 | expect(divContent, 'to equal', ['foo']); 183 | }); 184 | 185 | }); 186 | 187 | describe('with a native->Composite child', () => { 188 | 189 | let component; 190 | 191 | beforeEach(() => { 192 | 193 | component = TestUtils.renderIntoDocument(} />); 194 | /* renders 195 | 196 |
197 | 198 |
199 |
200 | 201 | */ 202 | }); 203 | 204 | it('returns the native component', () => { 205 | 206 | const theDiv = GlobalHook.findChildren(component); 207 | expect(theDiv, 'to satisfy', [{ 208 | data: { 209 | nodeType: 'Native', 210 | type: 'div', 211 | publicInstance: expect.it('to be an', 'object'), 212 | } 213 | }]); 214 | 215 | }); 216 | 217 | it('returns the CompositeComponent as a child of the div', () => { 218 | 219 | const theDiv = GlobalHook.findChildren(component); 220 | const divChildren = GlobalHook.findChildren(theDiv[0]); 221 | expect(divChildren, 'to satisfy', [{ 222 | data: { 223 | nodeType: 'Composite', 224 | type: expect.it('to be', TestComponent), 225 | publicInstance: expect.it('to be an', 'object'), 226 | } 227 | }]); 228 | }); 229 | }); 230 | 231 | describe('after an update', () => { 232 | 233 | let component; 234 | beforeEach(() => { 235 | component = TestUtils.renderIntoDocument(} />); 236 | }); 237 | 238 | it('returns the new children', () => { 239 | let classComp = GlobalHook.findChildren(component); 240 | 241 | 242 | expect(classComp, 'to satisfy', [{ 243 | data: { 244 | nodeType: 'Native', 245 | type: 'div', 246 | props: { 247 | id: undefined 248 | } 249 | } 250 | }]); 251 | 252 | const [div] = TestUtils.scryRenderedDOMComponentsWithTag(component, 'div'); 253 | TestUtils.Simulate.click(div); 254 | classComp = GlobalHook.findChildren(component); 255 | 256 | expect(classComp, 'to satisfy', [{ 257 | data: { 258 | nodeType: 'Native', 259 | type: 'div', 260 | props: { 261 | id: 'clicked' 262 | } 263 | } 264 | }]); 265 | }); 266 | }); 267 | }); 268 | 269 | describe('findChildren with stateless child', function () { 270 | let component; 271 | 272 | beforeEach(() => { 273 | component = TestUtils.renderIntoDocument(); 274 | }); 275 | 276 | it('finds the stateless component', function () { 277 | const children = GlobalHook.findChildren(component); 278 | expect(children[0].data, 'to satisfy', { 279 | nodeType: 'Composite', 280 | type: StatelessComponent, 281 | props: { 282 | className: 'foo' 283 | } 284 | }); 285 | }); 286 | }); 287 | }); 288 | --------------------------------------------------------------------------------