diff --git a/packages/react-dom/src/__tests__/ReactDOMComponentTree-test.js b/packages/react-dom/src/__tests__/ReactDOMComponentTree-test.js index f1873882f5c7d..93c081d8d48c6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponentTree-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponentTree-test.js @@ -10,114 +10,217 @@ 'use strict'; describe('ReactDOMComponentTree', () => { - var React; - var ReactDOM; - var ReactDOMComponentTree; - var ReactDOMServer; - - function renderMarkupIntoDocument(elt) { - var container = document.createElement('div'); - // Force server-rendering path: - container.innerHTML = ReactDOMServer.renderToString(elt); - return ReactDOM.hydrate(elt, container); - } - - function getTypeOf(instance) { - return instance.type; - } - - function getTextOf(instance) { - return instance.memoizedProps; - } + let React; + let ReactDOM; + let container; beforeEach(() => { React = require('react'); ReactDOM = require('react-dom'); - // TODO: can we express this test with only public API? - ReactDOMComponentTree = require('../client/ReactDOMComponentTree'); - ReactDOMServer = require('react-dom/server'); + container = document.createElement('div'); + document.body.appendChild(container); }); - it('finds nodes for instances', () => { - // This is a little hard to test directly. But refs rely on it -- so we - // check that we can find a ref at arbitrary points in the tree, even if - // other nodes don't have a ref. + afterEach(() => { + document.body.removeChild(container); + container = null; + }); + + it('finds nodes for instances on events', () => { + const mouseOverID = 'mouseOverID'; + const clickID = 'clickID'; + let currentTargetID = null; + // the current target of an event is set to result of getNodeFromInstance + // when an event is dispatched so we can test behavior by invoking + // events on elements in the tree and confirming the expected node is + // set as the current target class Component extends React.Component { + handler = e => { + currentTargetID = e.currentTarget.id; + }; render() { - var toRef = this.props.toRef; return ( -
-

hello

-

- -

- goodbye. +
+
); } } - function renderAndGetRef(toRef) { - var inst = renderMarkupIntoDocument(); - return inst.refs.target.nodeName; + function simulateMouseEvent(elem, type) { + const event = new MouseEvent(type, { + bubbles: true, + }); + elem.dispatchEvent(event); } - expect(renderAndGetRef('div')).toBe('DIV'); - expect(renderAndGetRef('h1')).toBe('H1'); - expect(renderAndGetRef('p')).toBe('P'); - expect(renderAndGetRef('input')).toBe('INPUT'); + const component = ; + ReactDOM.render(component, container); + expect(currentTargetID).toBe(null); + simulateMouseEvent(document.getElementById(mouseOverID), 'mouseover'); + expect(currentTargetID).toBe(mouseOverID); + simulateMouseEvent(document.getElementById(clickID), 'click'); + expect(currentTargetID).toBe(clickID); }); - it('finds instances for nodes', () => { - class Component extends React.Component { + it('finds closest instance for node when an event happens', () => { + const nonReactElemID = 'aID'; + const innerHTML = {__html: `
`}; + const closestInstanceID = 'closestInstance'; + let currentTargetID = null; + + class ClosestInstance extends React.Component { + _onClick = e => { + currentTargetID = e.currentTarget.id; + }; render() { return ( -
-

hello

-

- -

- goodbye. -
'}} /> -
+
); } } - function renderAndQuery(sel) { - var root = renderMarkupIntoDocument( -
- -
, - ); - return sel ? root.querySelector(sel) : root; + function simulateClick(elem) { + const event = new MouseEvent('click', { + bubbles: true, + }); + elem.dispatchEvent(event); + } + + const component = ; + ReactDOM.render(
{component}
, container); + expect(currentTargetID).toBe(null); + simulateClick(document.getElementById(nonReactElemID)); + expect(currentTargetID).toBe(closestInstanceID); + }); + + it('updates event handlers from fiber props', () => { + let action = ''; + let instance; + const handlerA = () => (action = 'A'); + const handlerB = () => (action = 'B'); + + function simulateMouseOver(target) { + const event = new MouseEvent('mouseover', { + bubbles: true, + }); + target.dispatchEvent(event); + } + + class HandlerFlipper extends React.Component { + state = {flip: false}; + flip() { + this.setState({flip: true}); + } + render() { + return ( +
+ ); + } } - function renderAndGetInstance(sel) { - return ReactDOMComponentTree.getInstanceFromNode(renderAndQuery(sel)); + ReactDOM.render( + (instance = n)} />, + container, + ); + const node = container.firstChild; + simulateMouseOver(node); + expect(action).toEqual('A'); + action = ''; + // Render with the other event handler. + instance.flip(); + simulateMouseOver(node); + expect(action).toEqual('B'); + }); + + it('finds a controlled instance from node and gets its current fiber props', () => { + const inputID = 'inputID'; + const startValue = undefined; + const finishValue = 'finish'; + + class Controlled extends React.Component { + state = {value: startValue}; + a = null; + _onChange = e => this.setState({value: e.currentTarget.value}); + render() { + return ( + (this.a = n)} + value={this.state.value} + onChange={this._onChange} + /> + ); + } } - function renderAndGetClosest(sel) { - return ReactDOMComponentTree.getClosestInstanceFromNode( - renderAndQuery(sel), - ); + const setUntrackedInputValue = Object.getOwnPropertyDescriptor( + HTMLInputElement.prototype, + 'value', + ).set; + + function simulateInput(elem, value) { + const inputEvent = new Event('input', { + bubbles: true, + }); + setUntrackedInputValue.call(elem, value); + elem.dispatchEvent(inputEvent); } - expect(getTypeOf(renderAndGetInstance(null))).toBe('section'); - expect(getTypeOf(renderAndGetInstance('div'))).toBe('div'); - expect(getTypeOf(renderAndGetInstance('h1'))).toBe('h1'); - expect(getTypeOf(renderAndGetInstance('p'))).toBe('p'); - expect(getTypeOf(renderAndGetInstance('input'))).toBe('input'); - expect(getTypeOf(renderAndGetInstance('main'))).toBe('main'); - - // This one's a text component! - var root = renderAndQuery(null); - var inst = ReactDOMComponentTree.getInstanceFromNode( - root.children[0].childNodes[2], + const component = ; + const instance = ReactDOM.render(component, container); + spyOn(console, 'error'); + expectDev(console.error.calls.count()).toBe(0); + simulateInput(instance.a, finishValue); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: A component is changing an uncontrolled input of ' + + 'type text to be controlled. Input elements should not ' + + 'switch from uncontrolled to controlled (or vice versa). ' + + 'Decide between using a controlled or uncontrolled input ' + + 'element for the lifetime of the component. More info: ' + + 'https://fb.me/react-controlled-components', ); - expect(getTextOf(inst)).toBe('goodbye.'); + }); - expect(getTypeOf(renderAndGetClosest('b'))).toBe('main'); - expect(getTypeOf(renderAndGetClosest('img'))).toBe('main'); + it('finds instance of node that is attempted to be unmounted', () => { + spyOn(console, 'error'); + const component =
; + const node = ReactDOM.render(
{component}
, container); + ReactDOM.unmountComponentAtNode(node); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + "unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by React and is not a top-level container. You may ' + + 'have accidentally passed in a React root node instead of its ' + + 'container.', + ); + }); + + it('finds instance from node to stop rendering over other react rendered components', () => { + spyOn(console, 'error'); + const component = ( +
+ Hello +
+ ); + const anotherComponent =
; + const instance = ReactDOM.render(component, container); + ReactDOM.render(anotherComponent, instance); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'render(...): Replacing React-rendered children with a new root ' + + 'component. If you intended to update the children of this node, ' + + 'you should instead have the existing children update their state ' + + 'and render the new components instead of calling ReactDOM.render.', + ); }); });