10
'use strict';
11
12
describe('ReactDOMComponentTree', () => {
13
- var React;
14
- var ReactDOM;
15
- var ReactDOMComponentTree;
16
- var ReactDOMServer;
17
-
18
- function renderMarkupIntoDocument(elt) {
19
- var container = document.createElement('div');
20
- // Force server-rendering path:
21
- container.innerHTML = ReactDOMServer.renderToString(elt);
22
- return ReactDOM.hydrate(elt, container);
23
- }
24
-
25
- function getTypeOf(instance) {
26
- return instance.type;
27
- }
28
-
29
- function getTextOf(instance) {
30
- return instance.memoizedProps;
31
- }
13
+ let React;
14
+ let ReactDOM;
15
+ let container;
16
17
beforeEach(() => {
18
React = require('react');
19
ReactDOM = require('react-dom');
36
- // TODO: can we express this test with only public API?
37
- ReactDOMComponentTree = require('../client/ReactDOMComponentTree');
38
- ReactDOMServer = require('react-dom/server');
20
+ container = document.createElement('div');
21
+ document.body.appendChild(container);
22
});
23
41
- it('finds nodes for instances', () => {
42
- // This is a little hard to test directly. But refs rely on it -- so we
43
- // check that we can find a ref at arbitrary points in the tree, even if
44
- // other nodes don't have a ref.
24
+ afterEach(() => {
25
+ document.body.removeChild(container);
26
+ container = null;
27
+ });
28
+
29
+ it('finds nodes for instances on events', () => {
30
+ const mouseOverID = 'mouseOverID';
31
+ const clickID = 'clickID';
32
+ let currentTargetID = null;
33
+ // the current target of an event is set to result of getNodeFromInstance
34
+ // when an event is dispatched so we can test behavior by invoking
35
+ // events on elements in the tree and confirming the expected node is
36
+ // set as the current target
37
class Component extends React.Component {
38
+ handler = e => {
39
+ currentTargetID = e.currentTarget.id;
40
+ };
41
render() {
47
- var toRef = this.props.toRef;
42
return (
49
- <div ref={toRef === 'div' ? 'target' : null}>
50
- <h1 ref={toRef === 'h1' ? 'target' : null}>hello</h1>
51
- <p ref={toRef === 'p' ? 'target' : null}>
52
- <input ref={toRef === 'input' ? 'target' : null} />
53
- </p>
54
- goodbye.
43
+ <div id={mouseOverID} onMouseOver={this.handler}>
44
+ <div id={clickID} onClick={this.handler} />
45
</div>
46
);
47
}
48
}
49
60
- function renderAndGetRef(toRef) {
61
- var inst = renderMarkupIntoDocument(<Component toRef={toRef} />);
62
- return inst.refs.target.nodeName;
50
+ function simulateMouseEvent(elem, type) {
51
+ const event = new MouseEvent(type, {
52
+ bubbles: true,
53
+ });
54
+ elem.dispatchEvent(event);
55
}
56
65
- expect(renderAndGetRef('div')).toBe('DIV');
66
- expect(renderAndGetRef('h1')).toBe('H1');
67
- expect(renderAndGetRef('p')).toBe('P');
68
- expect(renderAndGetRef('input')).toBe('INPUT');
57
+ const component = <Component />;
58
+ ReactDOM.render(component, container);
59
+ expect(currentTargetID).toBe(null);
60
+ simulateMouseEvent(document.getElementById(mouseOverID), 'mouseover');
61
+ expect(currentTargetID).toBe(mouseOverID);
62
+ simulateMouseEvent(document.getElementById(clickID), 'click');
63
+ expect(currentTargetID).toBe(clickID);
64
});
65
71
- it('finds instances for nodes', () => {
72
- class Component extends React.Component {
66
+ it('finds closest instance for node when an event happens', () => {
67
+ const nonReactElemID = 'aID';
68
+ const innerHTML = {__html: `<div id="${nonReactElemID}"></div>`};
69
+ const closestInstanceID = 'closestInstance';
70
+ let currentTargetID = null;
71
+
72
+ class ClosestInstance extends React.Component {
73
+ _onClick = e => {
74
+ currentTargetID = e.currentTarget.id;
75
+ };
76
render() {
77
return (
75
- <div>
76
- <h1>hello</h1>
77
- <p>
78
- <input />
79
- </p>
80
- goodbye.
81
- <main dangerouslySetInnerHTML={{__html: '<b><img></b>'}} />
82
- </div>
78
+ <div
79
+ id={closestInstanceID}
80
+ onClick={this._onClick}
81
+ dangerouslySetInnerHTML={innerHTML}
82
+ />
83
);
84
}
85
}
86
87
- function renderAndQuery(sel) {
88
- var root = renderMarkupIntoDocument(
89
- <section>
90
- <Component />
91
- </section>,
92
- );
93
- return sel ? root.querySelector(sel) : root;
87
+ function simulateClick(elem) {
88
+ const event = new MouseEvent('click', {
89
+ bubbles: true,
90
+ });
91
+ elem.dispatchEvent(event);
92
+ }
93
+
94
+ const component = <ClosestInstance />;
95
+ ReactDOM.render(<section>{component}</section>, container);
96
+ expect(currentTargetID).toBe(null);
97
+ simulateClick(document.getElementById(nonReactElemID));
98
+ expect(currentTargetID).toBe(closestInstanceID);
99
+ });
100
+
101
+ it('updates event handlers from fiber props', () => {
102
+ let action = '';
103
+ let instance;
104
+ const handlerA = () => (action = 'A');
105
+ const handlerB = () => (action = 'B');
106
+
107
+ function simulateMouseOver(target) {
108
+ const event = new MouseEvent('mouseover', {
109
+ bubbles: true,
110
+ });
111
+ target.dispatchEvent(event);
112
+ }
113
+
114
+ class HandlerFlipper extends React.Component {
115
+ state = {flip: false};
116
+ flip() {
117
+ this.setState({flip: true});
118
+ }
119
+ render() {
120
+ return (
121
+ <div
122
+ id="update"
123
+ onMouseOver={this.state.flip ? handlerB : handlerA}
124
+ />
125
+ );
126
+ }
127
}
128
96
- function renderAndGetInstance(sel) {
97
- return ReactDOMComponentTree.getInstanceFromNode(renderAndQuery(sel));
129
+ ReactDOM.render(
130
+ <HandlerFlipper key="1" ref={n => (instance = n)} />,
131
+ container,
132
+ );
133
+ const node = container.firstChild;
134
+ simulateMouseOver(node);
135
+ expect(action).toEqual('A');
136
+ action = '';
137
+ // Render with the other event handler.
138
+ instance.flip();
139
+ simulateMouseOver(node);
140
+ expect(action).toEqual('B');
141
+ });
142
+
143
+ it('finds a controlled instance from node and gets its current fiber props', () => {
144
+ const inputID = 'inputID';
145
+ const startValue = undefined;
146
+ const finishValue = 'finish';
147
+
148
+ class Controlled extends React.Component {
149
+ state = {value: startValue};
150
+ a = null;
151
+ _onChange = e => this.setState({value: e.currentTarget.value});
152
+ render() {
153
+ return (
154
+ <input
155
+ id={inputID}
156
+ type="text"
157
+ ref={n => (this.a = n)}
158
+ value={this.state.value}
159
+ onChange={this._onChange}
160
+ />
161
+ );
162
+ }
163
}
164
100
- function renderAndGetClosest(sel) {
101
- return ReactDOMComponentTree.getClosestInstanceFromNode(
102
- renderAndQuery(sel),
103
- );
165
+ const setUntrackedInputValue = Object.getOwnPropertyDescriptor(
166
+ HTMLInputElement.prototype,
167
+ 'value',
168
+ ).set;
169
+
170
+ function simulateInput(elem, value) {
171
+ const inputEvent = new Event('input', {
172
+ bubbles: true,
173
+ });
174
+ setUntrackedInputValue.call(elem, value);
175
+ elem.dispatchEvent(inputEvent);
176
}
177
106
- expect(getTypeOf(renderAndGetInstance(null))).toBe('section');
107
- expect(getTypeOf(renderAndGetInstance('div'))).toBe('div');
108
- expect(getTypeOf(renderAndGetInstance('h1'))).toBe('h1');
109
- expect(getTypeOf(renderAndGetInstance('p'))).toBe('p');
110
- expect(getTypeOf(renderAndGetInstance('input'))).toBe('input');
111
- expect(getTypeOf(renderAndGetInstance('main'))).toBe('main');
112
-
113
- // This one's a text component!
114
- var root = renderAndQuery(null);
115
- var inst = ReactDOMComponentTree.getInstanceFromNode(
116
- root.children[0].childNodes[2],
178
+ const component = <Controlled />;
179
+ const instance = ReactDOM.render(component, container);
180
+ spyOn(console, 'error');
181
+ expectDev(console.error.calls.count()).toBe(0);
182
+ simulateInput(instance.a, finishValue);
183
+ expectDev(console.error.calls.count()).toBe(1);
184
+ expectDev(console.error.calls.argsFor(0)[0]).toContain(
185
+ 'Warning: A component is changing an uncontrolled input of ' +
186
+ 'type text to be controlled. Input elements should not ' +
187
+ 'switch from uncontrolled to controlled (or vice versa). ' +
188
+ 'Decide between using a controlled or uncontrolled input ' +
189
+ 'element for the lifetime of the component. More info: ' +
190
+ 'https://fb.me/react-controlled-components',
191
);
118
- expect(getTextOf(inst)).toBe('goodbye.');
192
+ });
193
120
- expect(getTypeOf(renderAndGetClosest('b'))).toBe('main');
121
- expect(getTypeOf(renderAndGetClosest('img'))).toBe('main');
194
+ it('finds instance of node that is attempted to be unmounted', () => {
195
+ spyOn(console, 'error');
196
+ const component = <div />;
197
+ const node = ReactDOM.render(<div>{component}</div>, container);
198
+ ReactDOM.unmountComponentAtNode(node);
199
+ expectDev(console.error.calls.count()).toBe(1);
200
+ expectDev(console.error.calls.argsFor(0)[0]).toContain(
201
+ "unmountComponentAtNode(): The node you're attempting to unmount " +
202
+ 'was rendered by React and is not a top-level container. You may ' +
203
+ 'have accidentally passed in a React root node instead of its ' +
204
+ 'container.',
205
+ );
206
+ });
207
+
208
+ it('finds instance from node to stop rendering over other react rendered components', () => {
209
+ spyOn(console, 'error');
210
+ const component = (
211
+ <div>
212
+ <span>Hello</span>
213
+ </div>
214
+ );
215
+ const anotherComponent = <div />;
216
+ const instance = ReactDOM.render(component, container);
217
+ ReactDOM.render(anotherComponent, instance);
218
+ expectDev(console.error.calls.count()).toBe(1);
219
+ expectDev(console.error.calls.argsFor(0)[0]).toContain(
220
+ 'render(...): Replacing React-rendered children with a new root ' +
221
+ 'component. If you intended to update the children of this node, ' +
222
+ 'you should instead have the existing children update their state ' +
223
+ 'and render the new components instead of calling ReactDOM.render.',
224
+ );
225
});
226
});