@samitouri / QOS-React / commits / b2eaafaf3b

Revert "Check for event listener in props instead of bank (#8192)" (#8239)

This reverts commit a878c3056d2df0cf4d7f3b5e2db6031d598ef23f. The previous stuff that this builds on didn't successfully land fully. Since I want to measure this in isolation, I'll revert this for now.

Sebastian Markbåge committed Nov 8, 2016 at 11:32 UTC b2eaafaf3b19d3c74a30f0e6dc9d4a51bd91ba2f
15 files changed +491 -239
scripts/fiber/tests-failing.txt
+5 -3
@@ -147,6 +147,8 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
147 * should validate against invalid styles
148 * should track input values
149 * should track textarea values
150 +* should execute custom event plugin listening behavior
151 +* should handle null and missing properly with event hooks
152 * should warn for children on void elements
153 * should support custom elements which extend native elements
154 * should warn against children for void elements
@@ -155,6 +157,7 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
157 * should warn about contentEditable and children
158 * should validate against invalid styles
159 * should report component containing invalid styles
160 +* should clean up listeners
161 * should clean up input value tracking
162 * should clean up input textarea tracking
163 * should warn about the `onScroll` issue when unsupported (IE8)
@@ -379,11 +382,13 @@ src/renderers/dom/stack/server/__tests__/ReactServerRendering-test.js
382 * should generate simple markup for self-closing tags
383 * should generate simple markup for attribute with `>` symbol
384 * should generate comment markup for component returns null
385 +* should not register event listeners
386 * should render composite components
387 * should only execute certain lifecycle methods
388 * should have the correct mounting behavior
389 * should not put checksum and React ID on components
390 * should not put checksum and React ID on text components
391 +* should not register event listeners
392 * should only execute certain lifecycle methods
393 * allows setState in componentWillMount without using DOM
394 * renders components with different batching strategies
@@ -498,9 +503,6 @@ src/renderers/shared/shared/__tests__/ReactTreeTraversal-test.js
503 * should leave to the window
504 * should leave to the window from the shallowest
505
501 -src/renderers/shared/shared/event/__tests__/EventPluginHub-test.js
502 -* should prevent non-function listeners, at dispatch
503 -
506 src/renderers/shared/stack/reconciler/__tests__/ReactChildReconciler-test.js
507 * warns for duplicated keys
508 * warns for duplicated keys with component stack info
scripts/fiber/tests-passing.txt
+4
@@ -955,6 +955,9 @@ src/renderers/shared/shared/__tests__/ReactTreeTraversal-test.js
955 * should not traverse if enter/leave the same node
956 * should determine the first common ancestor correctly
957
958 +src/renderers/shared/shared/event/__tests__/EventPluginHub-test.js
959 +* should prevent non-function listeners
960 +
961 src/renderers/shared/shared/event/__tests__/EventPluginRegistry-test.js
962 * should be able to inject ordering before plugins
963 * should be able to inject plugins before and after ordering
@@ -1059,6 +1062,7 @@ src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js
1062 * propagates errors on retry on mounting
1063 * propagates errors inside boundary during componentWillMount
1064 * propagates errors inside boundary while rendering error state
1065 +* does not register event handlers for unmounted children
1066 * does not call componentWillUnmount when aborting initial mount
1067 * resets refs if mounting aborts
1068 * successfully mounts if no error occurs
src/renderers/dom/shared/ReactBrowserEventEmitter.js
+10
@@ -163,6 +163,16 @@ function getListeningForDocument(mountAt) {
163 return alreadyListeningTo[mountAt[topListenersIDKey]];
164 }
165
166 +/**
167 + * `ReactBrowserEventEmitter` is used to attach top-level event listeners. For
168 + * example:
169 + *
170 + * EventPluginHub.putListener('myID', 'onClick', myFunction);
171 + *
172 + * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
173 + *
174 + * @internal
175 + */
176 var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, {
177
178 /**
src/renderers/dom/shared/__tests__/ReactBrowserEventEmitter-test.js
+126 -175
@@ -15,9 +15,8 @@ var EventListener;
15 var EventPluginHub;
16 var EventPluginRegistry;
17 var React;
18 -var ReactDOM;
19 -var ReactDOMComponentTree;
18 var ReactBrowserEventEmitter;
19 +var ReactDOMComponentTree;
20 var ReactTestUtils;
21 var TapEventPlugin;
22
@@ -44,17 +43,18 @@ var GRANDPARENT;
43 var PARENT;
44 var CHILD;
45
47 -var getListener;
48 -var putListener;
49 -var deleteAllListeners;
50 -
46 function registerSimpleTestHandler() {
52 - putListener(CHILD, ON_CLICK_KEY, LISTENER);
53 - var listener = getListener(CHILD, ON_CLICK_KEY);
47 + EventPluginHub.putListener(getInternal(CHILD), ON_CLICK_KEY, LISTENER);
48 + var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY);
49 expect(listener).toEqual(LISTENER);
55 - return getListener(CHILD, ON_CLICK_KEY);
50 + return EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY);
51 +}
52 +
53 +function getInternal(node) {
54 + return ReactDOMComponentTree.getInstanceFromNode(node);
55 }
56
57 +
58 describe('ReactBrowserEventEmitter', () => {
59 beforeEach(() => {
60 jest.resetModuleRegistry();
@@ -63,67 +63,18 @@ describe('ReactBrowserEventEmitter', () => {
63 EventPluginHub = require('EventPluginHub');
64 EventPluginRegistry = require('EventPluginRegistry');
65 React = require('React');
66 - ReactDOM = require('ReactDOM');
67 - ReactDOMComponentTree = require('ReactDOMComponentTree');
66 ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
67 + ReactDOMComponentTree = require('ReactDOMComponentTree');
68 ReactTestUtils = require('ReactTestUtils');
69 TapEventPlugin = require('TapEventPlugin');
70
72 - var container = document.createElement('div');
73 -
74 - var GRANDPARENT_PROPS = {};
75 - var PARENT_PROPS = {};
76 - var CHILD_PROPS = {};
77 -
78 - function renderTree() {
79 - ReactDOM.render(
80 - <div ref={(c) => GRANDPARENT = c} {...GRANDPARENT_PROPS}>
81 - <div ref={(c) => PARENT = c} {...PARENT_PROPS}>
82 - <div ref={(c) => CHILD = c} {...CHILD_PROPS} />
83 - </div>
84 - </div>,
85 - container
86 - );
87 - }
88 -
89 - renderTree();
90 -
91 - getListener = function(node, eventName) {
92 - var inst = ReactDOMComponentTree.getInstanceFromNode(node);
93 - return EventPluginHub.getListener(
94 - inst,
95 - eventName
96 - );
97 - };
98 - putListener = function(node, eventName, listener) {
99 - switch (node) {
100 - case CHILD:
101 - CHILD_PROPS[eventName] = listener;
102 - break;
103 - case PARENT:
104 - PARENT_PROPS[eventName] = listener;
105 - break;
106 - case GRANDPARENT:
107 - GRANDPARENT_PROPS[eventName] = listener;
108 - break;
109 - }
110 - // Rerender with new event listeners
111 - renderTree();
112 - };
113 - deleteAllListeners = function(node) {
114 - switch (node) {
115 - case CHILD:
116 - CHILD_PROPS = {};
117 - break;
118 - case PARENT:
119 - PARENT_PROPS = {};
120 - break;
121 - case GRANDPARENT:
122 - GRANDPARENT_PROPS = {};
123 - break;
124 - }
125 - renderTree();
126 - };
71 + ReactTestUtils.renderIntoDocument(
72 + <div ref={(c) => GRANDPARENT = c}>
73 + <div ref={(c) => PARENT = c}>
74 + <div ref={(c) => CHILD = c} />
75 + </div>
76 + </div>
77 + );
78
79 idCallOrder = [];
80 tapMoveThreshold = TapEventPlugin.tapMoveThreshold;
@@ -134,20 +85,20 @@ describe('ReactBrowserEventEmitter', () => {
85
86 it('should store a listener correctly', () => {
87 registerSimpleTestHandler();
137 - var listener = getListener(CHILD, ON_CLICK_KEY);
88 + var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY);
89 expect(listener).toBe(LISTENER);
90 });
91
92 it('should retrieve a listener correctly', () => {
93 registerSimpleTestHandler();
143 - var listener = getListener(CHILD, ON_CLICK_KEY);
94 + var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY);
95 expect(listener).toEqual(LISTENER);
96 });
97
98 it('should clear all handlers when asked to', () => {
99 registerSimpleTestHandler();
149 - deleteAllListeners(CHILD);
150 - var listener = getListener(CHILD, ON_CLICK_KEY);
100 + EventPluginHub.deleteAllListeners(getInternal(CHILD));
101 + var listener = EventPluginHub.getListener(getInternal(CHILD), ON_CLICK_KEY);
102 expect(listener).toBe(undefined);
103 });
104
@@ -171,153 +122,153 @@ describe('ReactBrowserEventEmitter', () => {
122 );
123
124 it('should bubble simply', () => {
174 - putListener(
175 - CHILD,
125 + EventPluginHub.putListener(
126 + getInternal(CHILD),
127 ON_CLICK_KEY,
177 - recordID.bind(null, CHILD)
128 + recordID.bind(null, getInternal(CHILD))
129 );
179 - putListener(
180 - PARENT,
130 + EventPluginHub.putListener(
131 + getInternal(PARENT),
132 ON_CLICK_KEY,
182 - recordID.bind(null, PARENT)
133 + recordID.bind(null, getInternal(PARENT))
134 );
184 - putListener(
185 - GRANDPARENT,
135 + EventPluginHub.putListener(
136 + getInternal(GRANDPARENT),
137 ON_CLICK_KEY,
187 - recordID.bind(null, GRANDPARENT)
138 + recordID.bind(null, getInternal(GRANDPARENT))
139 );
140 ReactTestUtils.Simulate.click(CHILD);
141 expect(idCallOrder.length).toBe(3);
191 - expect(idCallOrder[0]).toBe(CHILD);
192 - expect(idCallOrder[1]).toBe(PARENT);
193 - expect(idCallOrder[2]).toBe(GRANDPARENT);
142 + expect(idCallOrder[0]).toBe(getInternal(CHILD));
143 + expect(idCallOrder[1]).toBe(getInternal(PARENT));
144 + expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT));
145 });
146
147 it('should continue bubbling if an error is thrown', () => {
197 - putListener(
198 - CHILD,
148 + EventPluginHub.putListener(
149 + getInternal(CHILD),
150 ON_CLICK_KEY,
200 - recordID.bind(null, CHILD)
151 + recordID.bind(null, getInternal(CHILD))
152 );
202 - putListener(
203 - PARENT,
153 + EventPluginHub.putListener(
154 + getInternal(PARENT),
155 ON_CLICK_KEY,
156 function() {
206 - recordID(PARENT);
157 + recordID(getInternal(PARENT));
158 throw new Error('Handler interrupted');
159 }
160 );
210 - putListener(
211 - GRANDPARENT,
161 + EventPluginHub.putListener(
162 + getInternal(GRANDPARENT),
163 ON_CLICK_KEY,
213 - recordID.bind(null, GRANDPARENT)
164 + recordID.bind(null, getInternal(GRANDPARENT))
165 );
166 expect(function() {
167 ReactTestUtils.Simulate.click(CHILD);
168 }).toThrow();
169 expect(idCallOrder.length).toBe(3);
219 - expect(idCallOrder[0]).toBe(CHILD);
220 - expect(idCallOrder[1]).toBe(PARENT);
221 - expect(idCallOrder[2]).toBe(GRANDPARENT);
170 + expect(idCallOrder[0]).toBe(getInternal(CHILD));
171 + expect(idCallOrder[1]).toBe(getInternal(PARENT));
172 + expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT));
173 });
174
175 it('should set currentTarget', () => {
225 - putListener(
226 - CHILD,
176 + EventPluginHub.putListener(
177 + getInternal(CHILD),
178 ON_CLICK_KEY,
179 function(event) {
229 - recordID(CHILD);
180 + recordID(getInternal(CHILD));
181 expect(event.currentTarget).toBe(CHILD);
182 }
183 );
233 - putListener(
234 - PARENT,
184 + EventPluginHub.putListener(
185 + getInternal(PARENT),
186 ON_CLICK_KEY,
187 function(event) {
237 - recordID(PARENT);
188 + recordID(getInternal(PARENT));
189 expect(event.currentTarget).toBe(PARENT);
190 }
191 );
241 - putListener(
242 - GRANDPARENT,
192 + EventPluginHub.putListener(
193 + getInternal(GRANDPARENT),
194 ON_CLICK_KEY,
195 function(event) {
245 - recordID(GRANDPARENT);
196 + recordID(getInternal(GRANDPARENT));
197 expect(event.currentTarget).toBe(GRANDPARENT);
198 }
199 );
200 ReactTestUtils.Simulate.click(CHILD);
201 expect(idCallOrder.length).toBe(3);
251 - expect(idCallOrder[0]).toBe(CHILD);
252 - expect(idCallOrder[1]).toBe(PARENT);
253 - expect(idCallOrder[2]).toBe(GRANDPARENT);
202 + expect(idCallOrder[0]).toBe(getInternal(CHILD));
203 + expect(idCallOrder[1]).toBe(getInternal(PARENT));
204 + expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT));
205 });
206
207 it('should support stopPropagation()', () => {
257 - putListener(
258 - CHILD,
208 + EventPluginHub.putListener(
209 + getInternal(CHILD),
210 ON_CLICK_KEY,
260 - recordID.bind(null, CHILD)
211 + recordID.bind(null, getInternal(CHILD))
212 );
262 - putListener(
263 - PARENT,
213 + EventPluginHub.putListener(
214 + getInternal(PARENT),
215 ON_CLICK_KEY,
265 - recordIDAndStopPropagation.bind(null, PARENT)
216 + recordIDAndStopPropagation.bind(null, getInternal(PARENT))
217 );
267 - putListener(
268 - GRANDPARENT,
218 + EventPluginHub.putListener(
219 + getInternal(GRANDPARENT),
220 ON_CLICK_KEY,
270 - recordID.bind(null, GRANDPARENT)
221 + recordID.bind(null, getInternal(GRANDPARENT))
222 );
223 ReactTestUtils.Simulate.click(CHILD);
224 expect(idCallOrder.length).toBe(2);
274 - expect(idCallOrder[0]).toBe(CHILD);
275 - expect(idCallOrder[1]).toBe(PARENT);
225 + expect(idCallOrder[0]).toBe(getInternal(CHILD));
226 + expect(idCallOrder[1]).toBe(getInternal(PARENT));
227 });
228
229 it('should stop after first dispatch if stopPropagation', () => {
279 - putListener(
280 - CHILD,
230 + EventPluginHub.putListener(
231 + getInternal(CHILD),
232 ON_CLICK_KEY,
282 - recordIDAndStopPropagation.bind(null, CHILD)
233 + recordIDAndStopPropagation.bind(null, getInternal(CHILD))
234 );
284 - putListener(
285 - PARENT,
235 + EventPluginHub.putListener(
236 + getInternal(PARENT),
237 ON_CLICK_KEY,
287 - recordID.bind(null, PARENT)
238 + recordID.bind(null, getInternal(PARENT))
239 );
289 - putListener(
290 - GRANDPARENT,
240 + EventPluginHub.putListener(
241 + getInternal(GRANDPARENT),
242 ON_CLICK_KEY,
292 - recordID.bind(null, GRANDPARENT)
243 + recordID.bind(null, getInternal(GRANDPARENT))
244 );
245 ReactTestUtils.Simulate.click(CHILD);
246 expect(idCallOrder.length).toBe(1);
296 - expect(idCallOrder[0]).toBe(CHILD);
247 + expect(idCallOrder[0]).toBe(getInternal(CHILD));
248 });
249
250 it('should not stopPropagation if false is returned', () => {
300 - putListener(
301 - CHILD,
251 + EventPluginHub.putListener(
252 + getInternal(CHILD),
253 ON_CLICK_KEY,
303 - recordIDAndReturnFalse.bind(null, CHILD)
254 + recordIDAndReturnFalse.bind(null, getInternal(CHILD))
255 );
305 - putListener(
306 - PARENT,
256 + EventPluginHub.putListener(
257 + getInternal(PARENT),
258 ON_CLICK_KEY,
308 - recordID.bind(null, PARENT)
259 + recordID.bind(null, getInternal(PARENT))
260 );
310 - putListener(
311 - GRANDPARENT,
261 + EventPluginHub.putListener(
262 + getInternal(GRANDPARENT),
263 ON_CLICK_KEY,
313 - recordID.bind(null, GRANDPARENT)
264 + recordID.bind(null, getInternal(GRANDPARENT))
265 );
266 spyOn(console, 'error');
267 ReactTestUtils.Simulate.click(CHILD);
268 expect(idCallOrder.length).toBe(3);
318 - expect(idCallOrder[0]).toBe(CHILD);
319 - expect(idCallOrder[1]).toBe(PARENT);
320 - expect(idCallOrder[2]).toBe(GRANDPARENT);
269 + expect(idCallOrder[0]).toBe(getInternal(CHILD));
270 + expect(idCallOrder[1]).toBe(getInternal(PARENT));
271 + expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT));
272 expect(console.error.calls.count()).toEqual(0);
273 });
274
@@ -333,15 +284,15 @@ describe('ReactBrowserEventEmitter', () => {
284 it('should invoke handlers that were removed while bubbling', () => {
285 var handleParentClick = jest.fn();
286 var handleChildClick = function(event) {
336 - deleteAllListeners(PARENT);
287 + EventPluginHub.deleteAllListeners(getInternal(PARENT));
288 };
338 - putListener(
339 - CHILD,
289 + EventPluginHub.putListener(
290 + getInternal(CHILD),
291 ON_CLICK_KEY,
292 handleChildClick
293 );
343 - putListener(
344 - PARENT,
294 + EventPluginHub.putListener(
295 + getInternal(PARENT),
296 ON_CLICK_KEY,
297 handleParentClick
298 );
@@ -352,14 +303,14 @@ describe('ReactBrowserEventEmitter', () => {
303 it('should not invoke newly inserted handlers while bubbling', () => {
304 var handleParentClick = jest.fn();
305 var handleChildClick = function(event) {
355 - putListener(
356 - PARENT,
306 + EventPluginHub.putListener(
307 + getInternal(PARENT),
308 ON_CLICK_KEY,
309 handleParentClick
310 );
311 };
361 - putListener(
362 - CHILD,
312 + EventPluginHub.putListener(
313 + getInternal(CHILD),
314 ON_CLICK_KEY,
315 handleChildClick
316 );
@@ -368,21 +319,21 @@ describe('ReactBrowserEventEmitter', () => {
319 });
320
321 it('should have mouse enter simulated by test utils', () => {
371 - putListener(
372 - CHILD,
322 + EventPluginHub.putListener(
323 + getInternal(CHILD),
324 ON_MOUSE_ENTER_KEY,
374 - recordID.bind(null, CHILD)
325 + recordID.bind(null, getInternal(CHILD))
326 );
327 ReactTestUtils.Simulate.mouseEnter(CHILD);
328 expect(idCallOrder.length).toBe(1);
378 - expect(idCallOrder[0]).toBe(CHILD);
329 + expect(idCallOrder[0]).toBe(getInternal(CHILD));
330 });
331
332 it('should infer onTouchTap from a touchStart/End', () => {
382 - putListener(
383 - CHILD,
333 + EventPluginHub.putListener(
334 + getInternal(CHILD),
335 ON_TOUCH_TAP_KEY,
385 - recordID.bind(null, CHILD)
336 + recordID.bind(null, getInternal(CHILD))
337 );
338 ReactTestUtils.SimulateNative.touchStart(
339 CHILD,
@@ -393,14 +344,14 @@ describe('ReactBrowserEventEmitter', () => {
344 ReactTestUtils.nativeTouchData(0, 0)
345 );
346 expect(idCallOrder.length).toBe(1);
396 - expect(idCallOrder[0]).toBe(CHILD);
347 + expect(idCallOrder[0]).toBe(getInternal(CHILD));
348 });
349
350 it('should infer onTouchTap from when dragging below threshold', () => {
400 - putListener(
401 - CHILD,
351 + EventPluginHub.putListener(
352 + getInternal(CHILD),
353 ON_TOUCH_TAP_KEY,
403 - recordID.bind(null, CHILD)
354 + recordID.bind(null, getInternal(CHILD))
355 );
356 ReactTestUtils.SimulateNative.touchStart(
357 CHILD,
@@ -411,14 +362,14 @@ describe('ReactBrowserEventEmitter', () => {
362 ReactTestUtils.nativeTouchData(0, tapMoveThreshold - 1)
363 );
364 expect(idCallOrder.length).toBe(1);
414 - expect(idCallOrder[0]).toBe(CHILD);
365 + expect(idCallOrder[0]).toBe(getInternal(CHILD));
366 });
367
368 it('should not onTouchTap from when dragging beyond threshold', () => {
418 - putListener(
419 - CHILD,
369 + EventPluginHub.putListener(
370 + getInternal(CHILD),
371 ON_TOUCH_TAP_KEY,
421 - recordID.bind(null, CHILD)
372 + recordID.bind(null, getInternal(CHILD))
373 );
374 ReactTestUtils.SimulateNative.touchStart(
375 CHILD,
@@ -472,20 +423,20 @@ describe('ReactBrowserEventEmitter', () => {
423 });
424
425 it('should bubble onTouchTap', () => {
475 - putListener(
476 - CHILD,
426 + EventPluginHub.putListener(
427 + getInternal(CHILD),
428 ON_TOUCH_TAP_KEY,
478 - recordID.bind(null, CHILD)
429 + recordID.bind(null, getInternal(CHILD))
430 );
480 - putListener(
481 - PARENT,
431 + EventPluginHub.putListener(
432 + getInternal(PARENT),
433 ON_TOUCH_TAP_KEY,
483 - recordID.bind(null, PARENT)
434 + recordID.bind(null, getInternal(PARENT))
435 );
485 - putListener(
486 - GRANDPARENT,
436 + EventPluginHub.putListener(
437 + getInternal(GRANDPARENT),
438 ON_TOUCH_TAP_KEY,
488 - recordID.bind(null, GRANDPARENT)
439 + recordID.bind(null, getInternal(GRANDPARENT))
440 );
441 ReactTestUtils.SimulateNative.touchStart(
442 CHILD,
@@ -496,9 +447,9 @@ describe('ReactBrowserEventEmitter', () => {
447 ReactTestUtils.nativeTouchData(0, 0)
448 );
449 expect(idCallOrder.length).toBe(3);
499 - expect(idCallOrder[0]).toBe(CHILD);
500 - expect(idCallOrder[1]).toBe(PARENT);
501 - expect(idCallOrder[2]).toBe(GRANDPARENT);
450 + expect(idCallOrder[0]).toBe(getInternal(CHILD));
451 + expect(idCallOrder[1]).toBe(getInternal(PARENT));
452 + expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT));
453 });
454
455 it('should not crash ensureScrollValueMonitoring when createEvent returns null', () => {
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+80
@@ -972,6 +972,61 @@ describe('ReactDOMComponent', () => {
972 expect(tracker.getValue()).toEqual('foo');
973 });
974
975 + it('should execute custom event plugin listening behavior', () => {
976 + var SimpleEventPlugin = require('SimpleEventPlugin');
977 +
978 + SimpleEventPlugin.didPutListener = jest.fn();
979 + SimpleEventPlugin.willDeleteListener = jest.fn();
980 +
981 + var container = document.createElement('div');
982 + ReactDOM.render(
983 + <div onClick={() => true} />,
984 + container
985 + );
986 +
987 + expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(1);
988 +
989 + ReactDOM.unmountComponentAtNode(container);
990 +
991 + expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
992 + });
993 +
994 + it('should handle null and missing properly with event hooks', () => {
995 + var SimpleEventPlugin = require('SimpleEventPlugin');
996 +
997 + SimpleEventPlugin.didPutListener = jest.fn();
998 + SimpleEventPlugin.willDeleteListener = jest.fn();
999 + var container = document.createElement('div');
1000 +
1001 + ReactDOM.render(<div onClick={false} />, container);
1002 + expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(0);
1003 + expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
1004 +
1005 + ReactDOM.render(<div onClick={null} />, container);
1006 + expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(0);
1007 + expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
1008 +
1009 + ReactDOM.render(<div onClick={() => 'apple'} />, container);
1010 + expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(1);
1011 + expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
1012 +
1013 + ReactDOM.render(<div onClick={() => 'banana'} />, container);
1014 + expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
1015 + expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(0);
1016 +
1017 + ReactDOM.render(<div onClick={null} />, container);
1018 + expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
1019 + expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
1020 +
1021 + ReactDOM.render(<div />, container);
1022 + expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
1023 + expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
1024 +
1025 + ReactDOM.unmountComponentAtNode(container);
1026 + expect(SimpleEventPlugin.didPutListener.mock.calls.length).toBe(2);
1027 + expect(SimpleEventPlugin.willDeleteListener.mock.calls.length).toBe(1);
1028 + });
1029 +
1030 it('should warn for children on void elements', () => {
1031 class X extends React.Component {
1032 render() {
@@ -1102,6 +1157,31 @@ describe('ReactDOMComponent', () => {
1157 });
1158
1159 describe('unmountComponent', () => {
1160 + it('should clean up listeners', () => {
1161 + var EventPluginHub = require('EventPluginHub');
1162 + var ReactDOMComponentTree = require('ReactDOMComponentTree');
1163 +
1164 + var container = document.createElement('div');
1165 + document.body.appendChild(container);
1166 +
1167 + var callback = function() {};
1168 + var instance = <div onClick={callback} />;
1169 + instance = ReactDOM.render(instance, container);
1170 +
1171 + var rootNode = ReactDOM.findDOMNode(instance);
1172 + var inst = ReactDOMComponentTree.getInstanceFromNode(rootNode);
1173 + expect(
1174 + EventPluginHub.getListener(inst, 'onClick')
1175 + ).toBe(callback);
1176 + expect(rootNode).toBe(ReactDOM.findDOMNode(instance));
1177 +
1178 + ReactDOM.unmountComponentAtNode(container);
1179 +
1180 + expect(
1181 + EventPluginHub.getListener(inst, 'onClick')
1182 + ).toBe(undefined);
1183 + });
1184 +
1185 it('should clean up input value tracking', () => {
1186 var container = document.createElement('div');
1187 var node = ReactDOM.render(<input type="text" defaultValue="foo"/>, container);
src/renderers/dom/stack/client/ReactDOMComponent.js
+46 -3
@@ -19,6 +19,7 @@ var DOMLazyTree = require('DOMLazyTree');
19 var DOMNamespaces = require('DOMNamespaces');
20 var DOMProperty = require('DOMProperty');
21 var DOMPropertyOperations = require('DOMPropertyOperations');
22 +var EventPluginHub = require('EventPluginHub');
23 var EventPluginRegistry = require('EventPluginRegistry');
24 var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
25 var ReactDOMComponentFlags = require('ReactDOMComponentFlags');
@@ -42,6 +43,7 @@ var warning = require('warning');
43 var didWarnShadyDOM = false;
44
45 var Flags = ReactDOMComponentFlags;
46 +var deleteListener = EventPluginHub.deleteListener;
47 var getNode = ReactDOMComponentTree.getNodeFromInstance;
48 var listenTo = ReactBrowserEventEmitter.listenTo;
49 var registrationNameModules = EventPluginRegistry.registrationNameModules;
@@ -203,6 +205,30 @@ function assertValidProps(component, props) {
205 );
206 }
207
208 +function enqueuePutListener(inst, registrationName, listener, transaction) {
209 + if (transaction instanceof ReactServerRenderingTransaction) {
210 + return;
211 + }
212 + if (__DEV__) {
213 + // IE8 has no API for event capturing and the `onScroll` event doesn't
214 + // bubble.
215 + warning(
216 + registrationName !== 'onScroll' || isEventSupported('scroll', true),
217 + 'This browser doesn\'t support the `onScroll` event'
218 + );
219 + }
220 + var containerInfo = inst._hostContainerInfo;
221 + var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
222 + var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
223 + listenTo(registrationName, doc);
224 + transaction.getReactMountReady().enqueue(putListener, {
225 + inst: inst,
226 + registrationName: registrationName,
227 + listener: listener,
228 + });
229 +}
230 +
231 +// TODO: This is coming from future #8192. Dedupe this and enqueuePutListener.
232 function ensureListeningTo(inst, registrationName, transaction) {
233 if (transaction instanceof ReactServerRenderingTransaction) {
234 return;
@@ -221,6 +247,15 @@ function ensureListeningTo(inst, registrationName, transaction) {
247 listenTo(registrationName, doc);
248 }
249
250 +function putListener() {
251 + var listenerToPut = this;
252 + EventPluginHub.putListener(
253 + listenerToPut.inst,
254 + listenerToPut.registrationName,
255 + listenerToPut.listener
256 + );
257 +}
258 +
259 function inputPostMount() {
260 var inst = this;
261 ReactDOMInput.postMountWrapper(inst);
@@ -757,7 +792,7 @@ ReactDOMComponent.Mixin = {
792 }
793 if (registrationNameModules.hasOwnProperty(propKey)) {
794 if (propValue) {
760 - ensureListeningTo(this, propKey, transaction);
795 + enqueuePutListener(this, propKey, propValue, transaction);
796 }
797 } else {
798 if (propKey === STYLE) {
@@ -1012,7 +1047,12 @@ ReactDOMComponent.Mixin = {
1047 }
1048 this._previousStyleCopy = null;
1049 } else if (registrationNameModules.hasOwnProperty(propKey)) {
1015 - // Do nothing for event names.
1050 + if (lastProps[propKey]) {
1051 + // Only call deleteListener if there was a listener previously or
1052 + // else willDeleteListener gets called when there wasn't actually a
1053 + // listener (e.g., onClick={null})
1054 + deleteListener(this, propKey);
1055 + }
1056 } else if (isCustomComponent(this._tag, lastProps)) {
1057 if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
1058 DOMPropertyOperations.deleteValueForAttribute(
@@ -1073,7 +1113,9 @@ ReactDOMComponent.Mixin = {
1113 }
1114 } else if (registrationNameModules.hasOwnProperty(propKey)) {
1115 if (nextProp) {
1076 - ensureListeningTo(this, propKey, transaction);
1116 + enqueuePutListener(this, propKey, nextProp, transaction);
1117 + } else if (lastProp) {
1118 + deleteListener(this, propKey);
1119 }
1120 } else if (isCustomComponentTag) {
1121 if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
@@ -1222,6 +1264,7 @@ ReactDOMComponent.Mixin = {
1264
1265 this.unmountChildren(safely, skipLifecycle);
1266 ReactDOMComponentTree.uncacheNode(this);
1267 + EventPluginHub.deleteAllListeners(this);
1268 this._rootNodeID = 0;
1269 this._domID = 0;
1270 this._wrapperState = null;
src/renderers/dom/stack/server/__tests__/ReactServerRendering-test.js
+19 -1
@@ -85,7 +85,15 @@ describe('ReactServerRendering', () => {
85 expect(response).toBe('<!-- react-empty: 1 -->');
86 });
87
88 - // TODO: Test that listeners are not registered onto any document/container.
88 + it('should not register event listeners', () => {
89 + var EventPluginHub = require('EventPluginHub');
90 + var cb = jest.fn();
91 +
92 + ReactServerRendering.renderToString(
93 + <span onClick={cb}>hello world</span>
94 + );
95 + expect(EventPluginHub.__getListenerBank()).toEqual({});
96 + });
97
98 it('should render composite components', () => {
99 class Parent extends React.Component {
@@ -314,6 +322,16 @@ describe('ReactServerRendering', () => {
322 expect(response).toBe('<span>hello world</span>');
323 });
324
325 + it('should not register event listeners', () => {
326 + var EventPluginHub = require('EventPluginHub');
327 + var cb = jest.fn();
328 +
329 + ReactServerRendering.renderToStaticMarkup(
330 + <span onClick={cb}>hello world</span>
331 + );
332 + expect(EventPluginHub.__getListenerBank()).toEqual({});
333 + });
334 +
335 it('should only execute certain lifecycle methods', () => {
336 function runTest() {
337 var lifecycle = [];
src/renderers/native/ReactNativeBaseComponent.js
+43
@@ -14,12 +14,18 @@
14 var NativeMethodsMixin = require('NativeMethodsMixin');
15 var ReactNativeAttributePayload = require('ReactNativeAttributePayload');
16 var ReactNativeComponentTree = require('ReactNativeComponentTree');
17 +var ReactNativeEventEmitter = require('ReactNativeEventEmitter');
18 var ReactNativeTagHandles = require('ReactNativeTagHandles');
19 var ReactMultiChild = require('ReactMultiChild');
20 var UIManager = require('UIManager');
21
22 var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev');
23
24 +var registrationNames = ReactNativeEventEmitter.registrationNames;
25 +var putListener = ReactNativeEventEmitter.putListener;
26 +var deleteListener = ReactNativeEventEmitter.deleteListener;
27 +var deleteAllListeners = ReactNativeEventEmitter.deleteAllListeners;
28 +
29 type ReactNativeBaseComponentViewConfig = {
30 validAttributes: Object;
31 uiViewClassName: string;
@@ -51,6 +57,7 @@ ReactNativeBaseComponent.Mixin = {
57
58 unmountComponent: function(safely, skipLifecycle) {
59 ReactNativeComponentTree.uncacheNode(this);
60 + deleteAllListeners(this);
61 this.unmountChildren(safely, skipLifecycle);
62 this._rootNodeID = 0;
63 },
@@ -116,9 +123,44 @@ ReactNativeBaseComponent.Mixin = {
123 );
124 }
125
126 + this._reconcileListenersUponUpdate(
127 + prevElement.props,
128 + nextElement.props
129 + );
130 this.updateChildren(nextElement.props.children, transaction, context);
131 },
132
133 + /**
134 + * @param {object} initialProps Native component props.
135 + */
136 + _registerListenersUponCreation: function(initialProps) {
137 + for (var key in initialProps) {
138 + // NOTE: The check for `!props[key]`, is only possible because this method
139 + // registers listeners the *first* time a component is created.
140 + if (registrationNames[key] && initialProps[key]) {
141 + var listener = initialProps[key];
142 + putListener(this, key, listener);
143 + }
144 + }
145 + },
146 +
147 + /**
148 + * Reconciles event listeners, adding or removing if necessary.
149 + * @param {object} prevProps Native component props including events.
150 + * @param {object} nextProps Next native component props including events.
151 + */
152 + _reconcileListenersUponUpdate: function(prevProps, nextProps) {
153 + for (var key in nextProps) {
154 + if (registrationNames[key] && (nextProps[key] !== prevProps[key])) {
155 + if (nextProps[key]) {
156 + putListener(this, key, nextProps[key]);
157 + } else {
158 + deleteListener(this, key);
159 + }
160 + }
161 + }
162 + },
163 +
164 /**
165 * Currently this still uses IDs for reconciliation so this can return null.
166 *
@@ -165,6 +207,7 @@ ReactNativeBaseComponent.Mixin = {
207
208 ReactNativeComponentTree.precacheNode(this, tag);
209
210 + this._registerListenersUponCreation(this._currentElement.props);
211 this.initializeChildren(
212 this._currentElement.props.children,
213 tag,
src/renderers/native/ReactNativeEventEmitter.js
+15
@@ -78,14 +78,29 @@ var removeTouchesAtIndices = function(
78 return rippedOut;
79 };
80
81 +/**
82 + * `ReactNativeEventEmitter` is used to attach top-level event listeners. For example:
83 + *
84 + * ReactNativeEventEmitter.putListener('myID', 'onClick', myFunction);
85 + *
86 + * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
87 + *
88 + * @internal
89 + */
90 var ReactNativeEventEmitter = {
91
92 ...ReactEventEmitterMixin,
93
94 registrationNames: EventPluginRegistry.registrationNameModules,
95
96 + putListener: EventPluginHub.putListener,
97 +
98 getListener: EventPluginHub.getListener,
99
100 + deleteListener: EventPluginHub.deleteListener,
101 +
102 + deleteAllListeners: EventPluginHub.deleteAllListeners,
103 +
104 /**
105 * Internal version of `receiveEvent` in terms of normalized (non-tag)
106 * `rootNodeID`.
src/renderers/shared/shared/event/EventPluginHub.js
+97 -9
@@ -19,6 +19,11 @@ var accumulateInto = require('accumulateInto');
19 var forEachAccumulated = require('forEachAccumulated');
20 var invariant = require('invariant');
21
22 +/**
23 + * Internal store for event listeners
24 + */
25 +var listenerBank = {};
26 +
27 /**
28 * Internal queue of events that have accumulated their dispatches and are
29 * waiting to have their dispatches executed.
@@ -48,6 +53,12 @@ var executeDispatchesAndReleaseTopLevel = function(e) {
53 return executeDispatchesAndRelease(e, false);
54 };
55
56 +var getDictionaryKey = function(inst) {
57 + // Prevents V8 performance issue:
58 + // https://github.com/facebook/react/pull/7232
59 + return '.' + inst._rootNodeID;
60 +};
61 +
62 /**
63 * This is a unified interface for event plugins to be installed and configured.
64 *
@@ -90,22 +101,88 @@ var EventPluginHub = {
101
102 },
103
104 + /**
105 + * Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent.
106 + *
107 + * @param {object} inst The instance, which is the source of events.
108 + * @param {string} registrationName Name of listener (e.g. `onClick`).
109 + * @param {function} listener The callback to store.
110 + */
111 + putListener: function(inst, registrationName, listener) {
112 + invariant(
113 + typeof listener === 'function',
114 + 'Expected %s listener to be a function, instead got type %s',
115 + registrationName, typeof listener
116 + );
117 +
118 + var key = getDictionaryKey(inst);
119 + var bankForRegistrationName =
120 + listenerBank[registrationName] || (listenerBank[registrationName] = {});
121 + bankForRegistrationName[key] = listener;
122 +
123 + var PluginModule =
124 + EventPluginRegistry.registrationNameModules[registrationName];
125 + if (PluginModule && PluginModule.didPutListener) {
126 + PluginModule.didPutListener(inst, registrationName, listener);
127 + }
128 + },
129 +
130 /**
131 * @param {object} inst The instance, which is the source of events.
132 * @param {string} registrationName Name of listener (e.g. `onClick`).
133 * @return {?function} The stored callback.
134 */
135 getListener: function(inst, registrationName) {
99 - var listener = inst._currentElement.props[registrationName];
100 - if (listener !== undefined) {
101 - invariant(
102 - typeof listener === 'function',
103 - 'Expected %s listener to be a function, instead got type %s',
104 - registrationName,
105 - typeof listener
106 - );
136 + var bankForRegistrationName = listenerBank[registrationName];
137 + var key = getDictionaryKey(inst);
138 + return bankForRegistrationName && bankForRegistrationName[key];
139 + },
140 +
141 + /**
142 + * Deletes a listener from the registration bank.
143 + *
144 + * @param {object} inst The instance, which is the source of events.
145 + * @param {string} registrationName Name of listener (e.g. `onClick`).
146 + */
147 + deleteListener: function(inst, registrationName) {
148 + var PluginModule =
149 + EventPluginRegistry.registrationNameModules[registrationName];
150 + if (PluginModule && PluginModule.willDeleteListener) {
151 + PluginModule.willDeleteListener(inst, registrationName);
152 + }
153 +
154 + var bankForRegistrationName = listenerBank[registrationName];
155 + // TODO: This should never be null -- when is it?
156 + if (bankForRegistrationName) {
157 + var key = getDictionaryKey(inst);
158 + delete bankForRegistrationName[key];
159 + }
160 + },
161 +
162 + /**
163 + * Deletes all listeners for the DOM element with the supplied ID.
164 + *
165 + * @param {object} inst The instance, which is the source of events.
166 + */
167 + deleteAllListeners: function(inst) {
168 + var key = getDictionaryKey(inst);
169 + for (var registrationName in listenerBank) {
170 + if (!listenerBank.hasOwnProperty(registrationName)) {
171 + continue;
172 + }
173 +
174 + if (!listenerBank[registrationName][key]) {
175 + continue;
176 + }
177 +
178 + var PluginModule =
179 + EventPluginRegistry.registrationNameModules[registrationName];
180 + if (PluginModule && PluginModule.willDeleteListener) {
181 + PluginModule.willDeleteListener(inst, registrationName);
182 + }
183 +
184 + delete listenerBank[registrationName][key];
185 }
108 - return listener;
186 },
187
188 /**
@@ -183,6 +260,17 @@ var EventPluginHub = {
260 ReactErrorUtils.rethrowCaughtError();
261 },
262
263 + /**
264 + * These are needed for tests only. Do not use!
265 + */
266 + __purge: function() {
267 + listenerBank = {};
268 + },
269 +
270 + __getListenerBank: function() {
271 + return listenerBank;
272 + },
273 +
274 };
275
276 module.exports = EventPluginHub;
src/renderers/shared/shared/event/EventPluginRegistry.js
+2 -1
@@ -130,7 +130,8 @@ function publishEventForPlugin(
130 }
131
132 /**
133 - * Publishes a registration name that is used to identify dispatched events.
133 + * Publishes a registration name that is used to identify dispatched events and
134 + * can be used with `EventPluginHub.putListener` to register listeners.
135 *
136 * @param {string} registrationName Registration name to add.
137 * @param {object} PluginModule Plugin publishing the event.
src/renderers/shared/shared/event/PluginModuleType.js
+9
@@ -36,5 +36,14 @@ export type PluginModule<NativeEvent> = {
36 nativeTarget: NativeEvent,
37 nativeEventTarget: EventTarget,
38 ) => null | ReactSyntheticEvent,
39 + didPutListener?: (
40 + inst: ReactInstance,
41 + registrationName: string,
42 + listener: () => void,
43 + ) => void,
44 + willDeleteListener?: (
45 + inst: ReactInstance,
46 + registrationName: string,
47 + ) => void,
48 tapMoveThreshold?: number,
49 };
src/renderers/shared/shared/event/__tests__/EventPluginHub-test.js
+7 -9
@@ -14,21 +14,19 @@
14 jest.mock('isEventSupported');
15
16 describe('EventPluginHub', () => {
17 - var React;
18 - var ReactTestUtils;
17 + var EventPluginHub;
18 + var isEventSupported;
19
20 beforeEach(() => {
21 jest.resetModuleRegistry();
22 - React = require('React');
23 - ReactTestUtils = require('ReactTestUtils');
22 + EventPluginHub = require('EventPluginHub');
23 + isEventSupported = require('isEventSupported');
24 + isEventSupported.mockReturnValueOnce(false);
25 });
26
26 - it('should prevent non-function listeners, at dispatch', () => {
27 - var node = ReactTestUtils.renderIntoDocument(
28 - <div onClick="not a function" />
29 - );
27 + it('should prevent non-function listeners', () => {
28 expect(function() {
31 - ReactTestUtils.SimulateNative.click(node);
29 + EventPluginHub.putListener(1, 'onClick', 'not a function');
30 }).toThrowError(
31 'Expected onClick listener to be a function, instead got type string'
32 );
src/renderers/shared/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js
+8 -38
@@ -233,7 +233,7 @@ var registerTestHandlers = function(eventTestConfig, readableIDToID) {
233 }
234 return config.returnVal;
235 }.bind(null, readableID, nodeConfig);
236 - putListener(getInstanceFromNode(id), registrationName, handler);
236 + EventPluginHub.putListener(getInstanceFromNode(id), registrationName, handler);
237 }
238 };
239 for (var eventName in eventTestConfig) {
@@ -258,6 +258,9 @@ var registerTestHandlers = function(eventTestConfig, readableIDToID) {
258 return runs;
259 };
260
261 +
262 +
263 +
264 var run = function(config, hierarchyConfig, nativeEventConfig) {
265 var max = NA;
266 var searchForMax = function(nodeConfig) {
@@ -306,30 +309,10 @@ var PARENT_HOST_NODE = { };
309 var CHILD_HOST_NODE = { };
310 var CHILD_HOST_NODE2 = { };
311
309 -var GRANDPARENT_INST = {
310 - _hostParent: null,
311 - _rootNodeID: '1',
312 - _hostNode: GRANDPARENT_HOST_NODE,
313 - _currentElement: { props: {} },
314 -};
315 -var PARENT_INST = {
316 - _hostParent: GRANDPARENT_INST,
317 - _rootNodeID: '2',
318 - _hostNode: PARENT_HOST_NODE,
319 - _currentElement: { props: {} },
320 -};
321 -var CHILD_INST = {
322 - _hostParent: PARENT_INST,
323 - _rootNodeID: '3',
324 - _hostNode: CHILD_HOST_NODE,
325 - _currentElement: { props: {} },
326 -};
327 -var CHILD_INST2 = {
328 - _hostParent: PARENT_INST,
329 - _rootNodeID: '4',
330 - _hostNode: CHILD_HOST_NODE2,
331 - _currentElement: { props: {} },
332 -};
312 +var GRANDPARENT_INST = { _hostParent: null, _rootNodeID: '1', _hostNode: GRANDPARENT_HOST_NODE };
313 +var PARENT_INST = { _hostParent: GRANDPARENT_INST, _rootNodeID: '2', _hostNode: PARENT_HOST_NODE };
314 +var CHILD_INST = { _hostParent: PARENT_INST, _rootNodeID: '3', _hostNode: CHILD_HOST_NODE };
315 +var CHILD_INST2 = { _hostParent: PARENT_INST, _rootNodeID: '4', _hostNode: CHILD_HOST_NODE2 };
316
317 GRANDPARENT_HOST_NODE._reactInstance = GRANDPARENT_INST;
318 PARENT_HOST_NODE._reactInstance = PARENT_INST;
@@ -356,14 +339,6 @@ function getNodeFromInstance(inst) {
339 return inst._hostNode;
340 }
341
359 -function putListener(node, registrationName, handler) {
360 - node._currentElement.props[registrationName] = handler;
361 -}
362 -
363 -function deleteAllListeners(node) {
364 - node._currentElement.props = {};
365 -}
366 -
342 describe('ResponderEventPlugin', () => {
343
344 beforeEach(() => {
@@ -373,11 +348,6 @@ describe('ResponderEventPlugin', () => {
348 EventPluginUtils = require('EventPluginUtils');
349 ResponderEventPlugin = require('ResponderEventPlugin');
350
376 - deleteAllListeners(GRANDPARENT_INST);
377 - deleteAllListeners(PARENT_INST);
378 - deleteAllListeners(CHILD_INST);
379 - deleteAllListeners(CHILD_INST2);
380 -
351 EventPluginUtils.injection.injectComponentTree({
352 getInstanceFromNode,
353 getNodeFromInstance,
src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js
+20
@@ -879,6 +879,26 @@ describe('ReactErrorBoundaries', () => {
879 ]);
880 });
881
882 + it('does not register event handlers for unmounted children', () => {
883 + var EventPluginHub = require('EventPluginHub');
884 + var container = document.createElement('div');
885 + EventPluginHub.putListener = jest.fn();
886 + ReactDOM.render(
887 + <ErrorBoundary>
888 + <button onClick={() => {}}>Click me</button>
889 + <BrokenRender />
890 + </ErrorBoundary>,
891 + container
892 + );
893 + expect(EventPluginHub.putListener).not.toBeCalled();
894 +
895 + log.length = 0;
896 + ReactDOM.unmountComponentAtNode(container);
897 + expect(log).toEqual([
898 + 'ErrorBoundary componentWillUnmount',
899 + ]);
900 + });
901 +
902 it('does not call componentWillUnmount when aborting initial mount', () => {
903 var container = document.createElement('div');
904 ReactDOM.render(