@samitouri / QOS-React / commits / 3661616c28

Improve test harness of submit events (#13463)

This PR includes a test that I've enabled in #13358 and another test that we've discussed in #13462 as well as some random cleanup while I'm at it.

Philipp Spieß committed Aug 22, 2018 at 22:46 UTC 3661616c282d50d3f90cfd1fb9f8ac1bf43eaab8
3 files changed +210 -147
fixtures/dom/yarn.lock
+1 -13
@@ -2153,14 +2153,6 @@ dotenv@4.0.0:
2153 version "4.0.0"
2154 resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
2155
2156 -draft-js@^0.10.5:
2157 - version "0.10.5"
2158 - resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.10.5.tgz#bfa9beb018fe0533dbb08d6675c371a6b08fa742"
2159 - dependencies:
2160 - fbjs "^0.8.15"
2161 - immutable "~3.7.4"
2162 - object-assign "^4.1.0"
2163 -
2156 duplexer2@^0.1.4:
2157 version "0.1.4"
2158 resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@@ -2680,7 +2672,7 @@ fbjs@^0.8.1, fbjs@^0.8.4:
2672 setimmediate "^1.0.5"
2673 ua-parser-js "^0.7.9"
2674
2683 -fbjs@^0.8.15, fbjs@^0.8.16:
2675 +fbjs@^0.8.16:
2676 version "0.8.16"
2677 resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
2678 dependencies:
@@ -3310,10 +3302,6 @@ ignore@^3.3.3:
3302 version "3.3.3"
3303 resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
3304
3313 -immutable@~3.7.4:
3314 - version "3.7.6"
3315 - resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"
3316 -
3305 imurmurhash@^0.1.4:
3306 version "0.1.4"
3307 resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
packages/react-dom/src/__tests__/ReactDOM-test.js
+33 -26
@@ -23,34 +23,41 @@ describe('ReactDOM', () => {
23 ReactTestUtils = require('react-dom/test-utils');
24 });
25
26 - // TODO: uncomment this test once we can run in phantom, which
27 - // supports real submit events.
28 - /*
26 it('should bubble onSubmit', function() {
30 - const count = 0;
31 - const form;
32 - const Parent = React.createClass({
33 - handleSubmit: function() {
34 - count++;
35 - return false;
36 - },
37 - render: function() {
38 - return <Child />;
39 - }
40 - });
41 - const Child = React.createClass({
42 - render: function() {
43 - return <form><input type="submit" value="Submit" /></form>;
44 - },
45 - componentDidMount: function() {
46 - form = ReactDOM.findDOMNode(this);
47 - }
48 - });
49 - const instance = ReactTestUtils.renderIntoDocument(<Parent />);
50 - form.submit();
51 - expect(count).toEqual(1);
27 + const container = document.createElement('div');
28 +
29 + let count = 0;
30 + let buttonRef;
31 +
32 + function Parent() {
33 + return (
34 + <div
35 + onSubmit={event => {
36 + event.preventDefault();
37 + count++;
38 + }}>
39 + <Child />
40 + </div>
41 + );
42 + }
43 +
44 + function Child() {
45 + return (
46 + <form>
47 + <input type="submit" ref={button => (buttonRef = button)} />
48 + </form>
49 + );
50 + }
51 +
52 + document.body.appendChild(container);
53 + try {
54 + ReactDOM.render(<Parent />, container);
55 + buttonRef.click();
56 + expect(count).toBe(1);
57 + } finally {
58 + document.body.removeChild(container);
59 + }
60 });
53 - */
61
62 it('allows a DOM element to be used with a string', () => {
63 const element = React.createElement('div', {className: 'foo'});
packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
+176 -108
@@ -28,14 +28,19 @@ describe('ReactDOMEventListener', () => {
28 document.body.appendChild(container);
29 document.body.appendChild(otherNode);
30
31 - otherNode.dispatchEvent(
32 - new MouseEvent('mouseout', {
33 - bubbles: true,
34 - cancelable: true,
35 - relatedTarget: node,
36 - }),
37 - );
38 - expect(mock).toBeCalled();
31 + try {
32 + otherNode.dispatchEvent(
33 + new MouseEvent('mouseout', {
34 + bubbles: true,
35 + cancelable: true,
36 + relatedTarget: node,
37 + }),
38 + );
39 + expect(mock).toBeCalled();
40 + } finally {
41 + document.body.removeChild(container);
42 + document.body.removeChild(otherNode);
43 + }
44 });
45
46 describe('Propagation', () => {
@@ -56,16 +61,18 @@ describe('ReactDOMEventListener', () => {
61 parentNode.appendChild(childContainer);
62 document.body.appendChild(parentContainer);
63
59 - const nativeEvent = document.createEvent('Event');
60 - nativeEvent.initEvent('mouseout', true, true);
61 - childNode.dispatchEvent(nativeEvent);
62 -
63 - expect(mouseOut).toBeCalled();
64 - expect(mouseOut).toHaveBeenCalledTimes(2);
65 - expect(mouseOut.mock.calls[0][0]).toEqual(childNode);
66 - expect(mouseOut.mock.calls[1][0]).toEqual(parentNode);
67 -
68 - document.body.removeChild(parentContainer);
64 + try {
65 + const nativeEvent = document.createEvent('Event');
66 + nativeEvent.initEvent('mouseout', true, true);
67 + childNode.dispatchEvent(nativeEvent);
68 +
69 + expect(mouseOut).toBeCalled();
70 + expect(mouseOut).toHaveBeenCalledTimes(2);
71 + expect(mouseOut.mock.calls[0][0]).toEqual(childNode);
72 + expect(mouseOut.mock.calls[1][0]).toEqual(parentNode);
73 + } finally {
74 + document.body.removeChild(parentContainer);
75 + }
76 });
77
78 it('should propagate events two levels down', () => {
@@ -92,50 +99,58 @@ describe('ReactDOMEventListener', () => {
99
100 document.body.appendChild(grandParentContainer);
101
95 - const nativeEvent = document.createEvent('Event');
96 - nativeEvent.initEvent('mouseout', true, true);
97 - childNode.dispatchEvent(nativeEvent);
98 -
99 - expect(mouseOut).toBeCalled();
100 - expect(mouseOut).toHaveBeenCalledTimes(3);
101 - expect(mouseOut.mock.calls[0][0]).toEqual(childNode);
102 - expect(mouseOut.mock.calls[1][0]).toEqual(parentNode);
103 - expect(mouseOut.mock.calls[2][0]).toEqual(grandParentNode);
104 -
105 - document.body.removeChild(grandParentContainer);
102 + try {
103 + const nativeEvent = document.createEvent('Event');
104 + nativeEvent.initEvent('mouseout', true, true);
105 + childNode.dispatchEvent(nativeEvent);
106 +
107 + expect(mouseOut).toBeCalled();
108 + expect(mouseOut).toHaveBeenCalledTimes(3);
109 + expect(mouseOut.mock.calls[0][0]).toEqual(childNode);
110 + expect(mouseOut.mock.calls[1][0]).toEqual(parentNode);
111 + expect(mouseOut.mock.calls[2][0]).toEqual(grandParentNode);
112 + } finally {
113 + document.body.removeChild(grandParentContainer);
114 + }
115 });
116
117 // Regression test for https://github.com/facebook/react/issues/1105
118 it('should not get confused by disappearing elements', () => {
119 const container = document.createElement('div');
120 document.body.appendChild(container);
112 - class MyComponent extends React.Component {
113 - state = {clicked: false};
114 - handleClick = () => {
115 - this.setState({clicked: true});
116 - };
117 - componentDidMount() {
118 - expect(ReactDOM.findDOMNode(this)).toBe(container.firstChild);
119 - }
120 - componentDidUpdate() {
121 - expect(ReactDOM.findDOMNode(this)).toBe(container.firstChild);
122 - }
123 - render() {
124 - if (this.state.clicked) {
125 - return <span>clicked!</span>;
126 - } else {
127 - return <button onClick={this.handleClick}>not yet clicked</button>;
121 +
122 + try {
123 + class MyComponent extends React.Component {
124 + state = {clicked: false};
125 + handleClick = () => {
126 + this.setState({clicked: true});
127 + };
128 + componentDidMount() {
129 + expect(ReactDOM.findDOMNode(this)).toBe(container.firstChild);
130 + }
131 + componentDidUpdate() {
132 + expect(ReactDOM.findDOMNode(this)).toBe(container.firstChild);
133 + }
134 + render() {
135 + if (this.state.clicked) {
136 + return <span>clicked!</span>;
137 + } else {
138 + return (
139 + <button onClick={this.handleClick}>not yet clicked</button>
140 + );
141 + }
142 }
143 }
144 + ReactDOM.render(<MyComponent />, container);
145 + container.firstChild.dispatchEvent(
146 + new MouseEvent('click', {
147 + bubbles: true,
148 + }),
149 + );
150 + expect(container.firstChild.textContent).toBe('clicked!');
151 + } finally {
152 + document.body.removeChild(container);
153 }
131 - ReactDOM.render(<MyComponent />, container);
132 - container.firstChild.dispatchEvent(
133 - new MouseEvent('click', {
134 - bubbles: true,
135 - }),
136 - );
137 - expect(container.firstChild.textContent).toBe('clicked!');
138 - document.body.removeChild(container);
154 });
155
156 it('should batch between handlers from different roots', () => {
@@ -164,21 +179,24 @@ describe('ReactDOMEventListener', () => {
179 parentNode.appendChild(childContainer);
180 document.body.appendChild(parentContainer);
181
167 - const nativeEvent = document.createEvent('Event');
168 - nativeEvent.initEvent('mouseout', true, true);
169 - childNode.dispatchEvent(nativeEvent);
170 -
171 - // Child and parent should both call from event handlers.
172 - expect(mock).toHaveBeenCalledTimes(2);
173 - // The first call schedules a render of '1' into the 'Child'.
174 - // However, we're batching so it isn't flushed yet.
175 - expect(mock.mock.calls[0][0]).toBe('Child');
176 - // The first call schedules a render of '2' into the 'Child'.
177 - // We're still batching so it isn't flushed yet either.
178 - expect(mock.mock.calls[1][0]).toBe('Child');
179 - // By the time we leave the handler, the second update is flushed.
180 - expect(childNode.textContent).toBe('2');
181 - document.body.removeChild(parentContainer);
182 + try {
183 + const nativeEvent = document.createEvent('Event');
184 + nativeEvent.initEvent('mouseout', true, true);
185 + childNode.dispatchEvent(nativeEvent);
186 +
187 + // Child and parent should both call from event handlers.
188 + expect(mock).toHaveBeenCalledTimes(2);
189 + // The first call schedules a render of '1' into the 'Child'.
190 + // However, we're batching so it isn't flushed yet.
191 + expect(mock.mock.calls[0][0]).toBe('Child');
192 + // The first call schedules a render of '2' into the 'Child'.
193 + // We're still batching so it isn't flushed yet either.
194 + expect(mock.mock.calls[1][0]).toBe('Child');
195 + // By the time we leave the handler, the second update is flushed.
196 + expect(childNode.textContent).toBe('2');
197 + } finally {
198 + document.body.removeChild(parentContainer);
199 + }
200 });
201 });
202
@@ -208,14 +226,17 @@ describe('ReactDOMEventListener', () => {
226
227 document.body.appendChild(container);
228
211 - const nativeEvent = document.createEvent('Event');
212 - nativeEvent.initEvent('mouseout', true, true);
213 - instance.getInner().dispatchEvent(nativeEvent);
229 + try {
230 + const nativeEvent = document.createEvent('Event');
231 + nativeEvent.initEvent('mouseout', true, true);
232 + instance.getInner().dispatchEvent(nativeEvent);
233
215 - expect(mouseOut).toBeCalled();
216 - expect(mouseOut).toHaveBeenCalledTimes(1);
217 - expect(mouseOut.mock.calls[0][0]).toEqual(instance.getInner());
218 - document.body.removeChild(container);
234 + expect(mouseOut).toBeCalled();
235 + expect(mouseOut).toHaveBeenCalledTimes(1);
236 + expect(mouseOut.mock.calls[0][0]).toEqual(instance.getInner());
237 + } finally {
238 + document.body.removeChild(container);
239 + }
240 });
241
242 // Regression test for https://github.com/facebook/react/pull/12877
@@ -271,46 +292,93 @@ describe('ReactDOMEventListener', () => {
292 document.body.removeChild(container);
293 });
294
295 + // This is a special case for submit and reset events as they are listened on
296 + // at the element level and not the document.
297 + // @see https://github.com/facebook/react/pull/13462
298 + it('should not receive submit events if native, interim DOM handler prevents it', () => {
299 + const container = document.createElement('div');
300 + document.body.appendChild(container);
301 +
302 + try {
303 + const formRef = React.createRef();
304 + const interimRef = React.createRef();
305 +
306 + const handleSubmit = jest.fn();
307 + const handleReset = jest.fn();
308 + ReactDOM.render(
309 + <div ref={interimRef}>
310 + <form ref={formRef} onSubmit={handleSubmit} onReset={handleReset} />
311 + </div>,
312 + container,
313 + );
314 +
315 + interimRef.current.onsubmit = nativeEvent =>
316 + nativeEvent.stopPropagation();
317 + interimRef.current.onreset = nativeEvent => nativeEvent.stopPropagation();
318 +
319 + formRef.current.dispatchEvent(
320 + new Event('submit', {
321 + // https://developer.mozilla.org/en-US/docs/Web/Events/submit
322 + bubbles: true,
323 + }),
324 + );
325 +
326 + formRef.current.dispatchEvent(
327 + new Event('reset', {
328 + // https://developer.mozilla.org/en-US/docs/Web/Events/reset
329 + bubbles: true,
330 + }),
331 + );
332 +
333 + expect(handleSubmit).toHaveBeenCalled();
334 + expect(handleReset).toHaveBeenCalled();
335 + } finally {
336 + document.body.removeChild(container);
337 + }
338 + });
339 +
340 it('should dispatch loadstart only for media elements', () => {
341 const container = document.createElement('div');
342 document.body.appendChild(container);
343
278 - const imgRef = React.createRef();
279 - const videoRef = React.createRef();
344 + try {
345 + const imgRef = React.createRef();
346 + const videoRef = React.createRef();
347
281 - const handleImgLoadStart = jest.fn();
282 - const handleVideoLoadStart = jest.fn();
283 - ReactDOM.render(
284 - <div>
285 - <img ref={imgRef} onLoadStart={handleImgLoadStart} />
286 - <video ref={videoRef} onLoadStart={handleVideoLoadStart} />
287 - </div>,
288 - container,
289 - );
348 + const handleImgLoadStart = jest.fn();
349 + const handleVideoLoadStart = jest.fn();
350 + ReactDOM.render(
351 + <div>
352 + <img ref={imgRef} onLoadStart={handleImgLoadStart} />
353 + <video ref={videoRef} onLoadStart={handleVideoLoadStart} />
354 + </div>,
355 + container,
356 + );
357
291 - // Note for debugging: loadstart currently doesn't fire in Chrome.
292 - // https://bugs.chromium.org/p/chromium/issues/detail?id=458851
293 - imgRef.current.dispatchEvent(
294 - new ProgressEvent('loadstart', {
295 - bubbles: false,
296 - }),
297 - );
298 - // Historically, we happened to not support onLoadStart
299 - // on <img>, and this test documents that lack of support.
300 - // If we decide to support it in the future, we should change
301 - // this line to expect 1 call. Note that fixing this would
302 - // be simple but would require attaching a handler to each
303 - // <img>. So far nobody asked us for it.
304 - expect(handleImgLoadStart).toHaveBeenCalledTimes(0);
305 -
306 - videoRef.current.dispatchEvent(
307 - new ProgressEvent('loadstart', {
308 - bubbles: false,
309 - }),
310 - );
311 - expect(handleVideoLoadStart).toHaveBeenCalledTimes(1);
358 + // Note for debugging: loadstart currently doesn't fire in Chrome.
359 + // https://bugs.chromium.org/p/chromium/issues/detail?id=458851
360 + imgRef.current.dispatchEvent(
361 + new ProgressEvent('loadstart', {
362 + bubbles: false,
363 + }),
364 + );
365 + // Historically, we happened to not support onLoadStart
366 + // on <img>, and this test documents that lack of support.
367 + // If we decide to support it in the future, we should change
368 + // this line to expect 1 call. Note that fixing this would
369 + // be simple but would require attaching a handler to each
370 + // <img>. So far nobody asked us for it.
371 + expect(handleImgLoadStart).toHaveBeenCalledTimes(0);
372
313 - document.body.removeChild(container);
373 + videoRef.current.dispatchEvent(
374 + new ProgressEvent('loadstart', {
375 + bubbles: false,
376 + }),
377 + );
378 + expect(handleVideoLoadStart).toHaveBeenCalledTimes(1);
379 + } finally {
380 + document.body.removeChild(container);
381 + }
382 });
383
384 it('should not attempt to listen to unnecessary events on the top level', () => {