main
js 331 lines 8.36 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 let React;
13 let ReactDOM;
14 let ReactDOMClient;
15 let act;
16
17 describe('EnterLeaveEventPlugin', () => {
18 let container;
19
20 beforeEach(() => {
21 jest.resetModules();
22
23 React = require('react');
24 ReactDOM = require('react-dom');
25 ReactDOMClient = require('react-dom/client');
26 act = require('internal-test-utils').act;
27
28 // The container has to be attached for events to fire.
29 container = document.createElement('div');
30 document.body.appendChild(container);
31 });
32
33 afterEach(() => {
34 document.body.removeChild(container);
35 container = null;
36 });
37
38 it('should set onMouseLeave relatedTarget properly in iframe', async () => {
39 const iframe = document.createElement('iframe');
40 container.appendChild(iframe);
41 const iframeDocument = iframe.contentDocument;
42 iframeDocument.write(
43 '<!DOCTYPE html><html><head></head><body><div></div></body></html>',
44 );
45 iframeDocument.close();
46
47 const leaveEvents = [];
48 const root = ReactDOMClient.createRoot(
49 iframeDocument.body.getElementsByTagName('div')[0],
50 );
51 await act(() => {
52 root.render(
53 <div
54 onMouseLeave={e => {
55 e.persist();
56 leaveEvents.push(e);
57 }}
58 />,
59 );
60 });
61 const node = iframeDocument.body.getElementsByTagName('div')[0].firstChild;
62 await act(() => {
63 node.dispatchEvent(
64 new MouseEvent('mouseout', {
65 bubbles: true,
66 cancelable: true,
67 relatedTarget: iframe.contentWindow,
68 }),
69 );
70 });
71
72 expect(leaveEvents.length).toBe(1);
73 expect(leaveEvents[0].target).toBe(node);
74 expect(leaveEvents[0].relatedTarget).toBe(iframe.contentWindow);
75 });
76
77 it('should set onMouseEnter relatedTarget properly in iframe', async () => {
78 const iframe = document.createElement('iframe');
79 container.appendChild(iframe);
80 const iframeDocument = iframe.contentDocument;
81 iframeDocument.write(
82 '<!DOCTYPE html><html><head></head><body><div></div></body></html>',
83 );
84 iframeDocument.close();
85
86 const enterEvents = [];
87 const root = ReactDOMClient.createRoot(
88 iframeDocument.body.getElementsByTagName('div')[0],
89 );
90 await act(() => {
91 root.render(
92 <div
93 onMouseEnter={e => {
94 e.persist();
95 enterEvents.push(e);
96 }}
97 />,
98 );
99 });
100 const node = iframeDocument.body.getElementsByTagName('div')[0].firstChild;
101 await act(() => {
102 node.dispatchEvent(
103 new MouseEvent('mouseover', {
104 bubbles: true,
105 cancelable: true,
106 relatedTarget: null,
107 }),
108 );
109 });
110
111 expect(enterEvents.length).toBe(1);
112 expect(enterEvents[0].target).toBe(node);
113 expect(enterEvents[0].relatedTarget).toBe(iframe.contentWindow);
114 });
115
116 // Regression test for https://github.com/facebook/react/issues/10906.
117 it('should find the common parent after updates', async () => {
118 let parentEnterCalls = 0;
119 let childEnterCalls = 0;
120 let parent = null;
121
122 class Parent extends React.Component {
123 render() {
124 return (
125 <div
126 onMouseEnter={() => parentEnterCalls++}
127 ref={node => (parent = node)}>
128 {this.props.showChild && (
129 <div onMouseEnter={() => childEnterCalls++} />
130 )}
131 </div>
132 );
133 }
134 }
135
136 const root = ReactDOMClient.createRoot(container);
137 await act(() => {
138 root.render(<Parent />);
139 });
140 await act(() => {
141 root.render(<Parent showChild={true} />);
142 });
143
144 // Enter from parent into the child.
145 await act(() => {
146 parent.dispatchEvent(
147 new MouseEvent('mouseout', {
148 bubbles: true,
149 cancelable: true,
150 relatedTarget: parent.firstChild,
151 }),
152 );
153 });
154
155 // Entering a child should fire on the child, not on the parent.
156 expect(childEnterCalls).toBe(1);
157 expect(parentEnterCalls).toBe(0);
158 });
159
160 // Test for https://github.com/facebook/react/issues/16763.
161 // @gate !disableLegacyMode
162 it('should call mouseEnter once from sibling rendered inside a rendered component in legacy roots', async () => {
163 const mockFn = jest.fn();
164
165 class Parent extends React.Component {
166 constructor(props) {
167 super(props);
168 this.parentEl = React.createRef();
169 }
170
171 componentDidMount() {
172 ReactDOM.render(<MouseEnterDetect />, this.parentEl.current);
173 }
174
175 render() {
176 return <div ref={this.parentEl} />;
177 }
178 }
179
180 class MouseEnterDetect extends React.Component {
181 constructor(props) {
182 super(props);
183 this.firstEl = React.createRef();
184 this.siblingEl = React.createRef();
185 }
186
187 componentDidMount() {
188 this.siblingEl.current.dispatchEvent(
189 new MouseEvent('mouseout', {
190 bubbles: true,
191 cancelable: true,
192 relatedTarget: this.firstEl.current,
193 }),
194 );
195 }
196
197 render() {
198 return (
199 <React.Fragment>
200 <div ref={this.firstEl} onMouseEnter={mockFn} />
201 <div ref={this.siblingEl} />
202 </React.Fragment>
203 );
204 }
205 }
206
207 await act(() => {
208 ReactDOM.render(<Parent />, container);
209 });
210 expect(mockFn.mock.calls.length).toBe(1);
211 });
212
213 // @gate !disableLegacyMode
214 it('should call mouseEnter when pressing a non tracked React node in legacy root', async () => {
215 const mockFn = jest.fn();
216
217 class Parent extends React.Component {
218 constructor(props) {
219 super(props);
220 this.parentEl = React.createRef();
221 }
222
223 componentDidMount() {
224 ReactDOM.render(<MouseEnterDetect />, this.parentEl.current);
225 }
226
227 render() {
228 return <div ref={this.parentEl} />;
229 }
230 }
231
232 class MouseEnterDetect extends React.Component {
233 constructor(props) {
234 super(props);
235 this.divRef = React.createRef();
236 this.siblingEl = React.createRef();
237 }
238
239 componentDidMount() {
240 const attachedNode = document.createElement('div');
241 this.divRef.current.appendChild(attachedNode);
242 attachedNode.dispatchEvent(
243 new MouseEvent('mouseout', {
244 bubbles: true,
245 cancelable: true,
246 relatedTarget: this.siblingEl.current,
247 }),
248 );
249 }
250
251 render() {
252 return (
253 <div ref={this.divRef}>
254 <div ref={this.siblingEl} onMouseEnter={mockFn} />
255 </div>
256 );
257 }
258 }
259
260 await act(() => {
261 ReactDOM.render(<Parent />, container);
262 });
263 expect(mockFn.mock.calls.length).toBe(1);
264 });
265
266 it('should work with portals outside of the root that has onMouseLeave', async () => {
267 const divRef = React.createRef();
268 const onMouseLeave = jest.fn();
269
270 function Component() {
271 return (
272 <div onMouseLeave={onMouseLeave}>
273 {ReactDOM.createPortal(<div ref={divRef} />, document.body)}
274 </div>
275 );
276 }
277
278 const root = ReactDOMClient.createRoot(container);
279
280 await act(() => {
281 root.render(<Component />);
282 });
283
284 // Leave from the portal div
285 await act(() => {
286 divRef.current.dispatchEvent(
287 new MouseEvent('mouseout', {
288 bubbles: true,
289 cancelable: true,
290 relatedTarget: document.body,
291 }),
292 );
293 });
294
295 expect(onMouseLeave).toHaveBeenCalledTimes(1);
296 });
297
298 it('should work with portals that have onMouseEnter outside of the root', async () => {
299 const divRef = React.createRef();
300 const otherDivRef = React.createRef();
301 const onMouseEnter = jest.fn();
302
303 function Component() {
304 return (
305 <div ref={divRef}>
306 {ReactDOM.createPortal(
307 <div ref={otherDivRef} onMouseEnter={onMouseEnter} />,
308 document.body,
309 )}
310 </div>
311 );
312 }
313
314 const root = ReactDOMClient.createRoot(container);
315
316 await act(() => {
317 root.render(<Component />);
318 });
319
320 // Leave from the portal div
321 divRef.current.dispatchEvent(
322 new MouseEvent('mouseout', {
323 bubbles: true,
324 cancelable: true,
325 relatedTarget: otherDivRef.current,
326 }),
327 );
328
329 expect(onMouseEnter).toHaveBeenCalledTimes(1);
330 });
331 });