Skip to content

Commit

Permalink
Rewrite ReactDOMComponentTree-test to test behavior using Public API (f…
Browse files Browse the repository at this point in the history
…acebook#11383)

* Rewrite ReactDOMComponentTree-test to test behavior using Public API

 - Part of facebook#11299
 - I've tried to identify cases where code within ReactDOMComponentTree is exercised and have updated accordingly but I'm not entirely sure whether I'm on the right track. I thought I'd PR to get feedback from the community. Looking forward to comments.

* Prettier and lint changes

* Remove testing of internals and add test cases for testing behavior exhibited after use of getInstanceFromNode

* [RFC] Update testing approach to verify exhibited behavior dependent upon methods in ReactDOMComponentTree

* Remove tests from event handlers and use sync tests

* Prettier changes

* Rename variables to be more semantic

* Prettier updates

* Update test following review

 - Use beforeEach and afterEach to set up and tear down container element for use in each test
 - Move any functions specific to one test to within test body (improves readability imo)

* Add coverage for getNodeFromInstance and implementation of getFiberCurrentPropsFromNode
 - After researching usage of getNodeFromInstance we can test getNodeFromInstance dispatching some events and asserting the id of the currentTarget
 - After checking git blame for getFiberCurrentPropsFromNode and reading through facebook#8607 I found a test that we can simplify to assert behavior of the function by ensuring event handler props are updated from the fiber props. Swapping out the implementation of this function with `return node[internalInstanceKey].memoizedProps` results in a failure.
  • Loading branch information
GordyD authored and Ethan-Arrowood committed Dec 8, 2017
1 parent b870304 commit c79427e
Showing 1 changed file with 180 additions and 77 deletions.
257 changes: 180 additions & 77 deletions packages/react-dom/src/__tests__/ReactDOMComponentTree-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div ref={toRef === 'div' ? 'target' : null}>
<h1 ref={toRef === 'h1' ? 'target' : null}>hello</h1>
<p ref={toRef === 'p' ? 'target' : null}>
<input ref={toRef === 'input' ? 'target' : null} />
</p>
goodbye.
<div id={mouseOverID} onMouseOver={this.handler}>
<div id={clickID} onClick={this.handler} />
</div>
);
}
}

function renderAndGetRef(toRef) {
var inst = renderMarkupIntoDocument(<Component toRef={toRef} />);
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 = <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: `<div id="${nonReactElemID}"></div>`};
const closestInstanceID = 'closestInstance';
let currentTargetID = null;

class ClosestInstance extends React.Component {
_onClick = e => {
currentTargetID = e.currentTarget.id;
};
render() {
return (
<div>
<h1>hello</h1>
<p>
<input />
</p>
goodbye.
<main dangerouslySetInnerHTML={{__html: '<b><img></b>'}} />
</div>
<div
id={closestInstanceID}
onClick={this._onClick}
dangerouslySetInnerHTML={innerHTML}
/>
);
}
}

function renderAndQuery(sel) {
var root = renderMarkupIntoDocument(
<section>
<Component />
</section>,
);
return sel ? root.querySelector(sel) : root;
function simulateClick(elem) {
const event = new MouseEvent('click', {
bubbles: true,
});
elem.dispatchEvent(event);
}

const component = <ClosestInstance />;
ReactDOM.render(<section>{component}</section>, 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 (
<div
id="update"
onMouseOver={this.state.flip ? handlerB : handlerA}
/>
);
}
}

function renderAndGetInstance(sel) {
return ReactDOMComponentTree.getInstanceFromNode(renderAndQuery(sel));
ReactDOM.render(
<HandlerFlipper key="1" ref={n => (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 (
<input
id={inputID}
type="text"
ref={n => (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 = <Controlled />;
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 = <div />;
const node = ReactDOM.render(<div>{component}</div>, 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 = (
<div>
<span>Hello</span>
</div>
);
const anotherComponent = <div />;
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.',
);
});
});

0 comments on commit c79427e

Please sign in to comment.