9
10
'use strict';
11
12
+import {
13
+ dispatchPointerCancel,
14
+ dispatchPointerHoverEnter,
15
+ dispatchPointerHoverExit,
16
+ dispatchPointerHoverMove,
17
+ dispatchTouchTap,
18
+ setPointerEvent,
19
+} from '../test-utils';
20
+
21
let React;
22
let ReactFeatureFlags;
23
let ReactDOM;
15
-let TestUtils;
16
-let Scheduler;
24
let HoverResponder;
25
let useHoverResponder;
26
20
-const createEvent = (type, data) => {
21
- const event = document.createEvent('CustomEvent');
22
- event.initCustomEvent(type, true, true);
23
- if (data != null) {
24
- Object.entries(data).forEach(([key, value]) => {
25
- event[key] = value;
26
- });
27
- }
28
- return event;
29
-};
30
-
31
-function createTouchEvent(type, id, data) {
32
- return createEvent(type, {
33
- changedTouches: [
34
- {
35
- ...data,
36
- identifier: id,
37
- },
38
- ],
39
- });
27
+function initializeModules(hasPointerEvents) {
28
+ jest.resetModules();
29
+ setPointerEvent(hasPointerEvents);
30
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
31
+ ReactFeatureFlags.enableFlareAPI = true;
32
+ ReactFeatureFlags.enableUserBlockingEvents = true;
33
+ React = require('react');
34
+ ReactDOM = require('react-dom');
35
+ HoverResponder = require('react-events/hover').HoverResponder;
36
+ useHoverResponder = require('react-events/hover').useHoverResponder;
37
}
38
42
-describe('Hover event responder', () => {
39
+const forcePointerEvents = true;
40
+const table = [[forcePointerEvents], [!forcePointerEvents]];
41
+
42
+describe.each(table)('Hover responder', hasPointerEvents => {
43
let container;
44
45
beforeEach(() => {
46
- jest.resetModules();
47
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
48
- ReactFeatureFlags.enableFlareAPI = true;
49
- ReactFeatureFlags.enableUserBlockingEvents = true;
50
- React = require('react');
51
- ReactDOM = require('react-dom');
52
- TestUtils = require('react-dom/test-utils');
53
- Scheduler = require('scheduler');
54
- HoverResponder = require('react-events/hover').HoverResponder;
55
- useHoverResponder = require('react-events/hover').useHoverResponder;
46
+ initializeModules(hasPointerEvents);
47
container = document.createElement('div');
48
document.body.appendChild(container);
49
});
55
});
56
57
describe('disabled', () => {
67
- let onHoverStart, onHoverEnd, ref;
58
+ let onHoverChange, onHoverStart, onHoverMove, onHoverEnd, ref;
59
60
beforeEach(() => {
61
+ onHoverChange = jest.fn();
62
onHoverStart = jest.fn();
63
+ onHoverMove = jest.fn();
64
onHoverEnd = jest.fn();
65
ref = React.createRef();
66
const Component = () => {
67
const listener = useHoverResponder({
68
disabled: true,
76
- onHoverStart: onHoverStart,
77
- onHoverEnd: onHoverEnd,
69
+ onHoverChange,
70
+ onHoverStart,
71
+ onHoverMove,
72
+ onHoverEnd,
73
});
74
return <div ref={ref} listeners={listener} />;
75
};
77
});
78
79
it('prevents custom events being dispatched', () => {
85
- ref.current.dispatchEvent(createEvent('pointerover'));
86
- ref.current.dispatchEvent(createEvent('pointerout'));
80
+ dispatchPointerHoverEnter(ref);
81
+ dispatchPointerHoverExit(ref);
82
+ expect(onHoverChange).not.toBeCalled();
83
expect(onHoverStart).not.toBeCalled();
84
+ expect(onHoverMove).not.toBeCalled();
85
expect(onHoverEnd).not.toBeCalled();
86
});
87
});
101
ReactDOM.render(<Component />, container);
102
});
103
107
- it('is called after "pointerover" event', () => {
108
- ref.current.dispatchEvent(createEvent('pointerover'));
104
+ it('is called for mouse pointers', () => {
105
+ dispatchPointerHoverEnter(ref);
106
expect(onHoverStart).toHaveBeenCalledTimes(1);
107
});
108
112
- it('is not called if "pointerover" pointerType is touch', () => {
113
- const event = createEvent('pointerover', {pointerType: 'touch'});
114
- ref.current.dispatchEvent(event);
109
+ it('is not called for touch pointers', () => {
110
+ dispatchTouchTap(ref);
111
expect(onHoverStart).not.toBeCalled();
112
});
113
118
- it('is called if valid "pointerover" follows touch', () => {
119
- ref.current.dispatchEvent(
120
- createEvent('pointerover', {pointerType: 'touch'}),
121
- );
122
- ref.current.dispatchEvent(
123
- createEvent('pointerout', {pointerType: 'touch'}),
124
- );
125
- ref.current.dispatchEvent(
126
- createEvent('pointerover', {pointerType: 'mouse'}),
127
- );
128
- expect(onHoverStart).toHaveBeenCalledTimes(1);
129
- });
130
-
131
- it('ignores browser emulated "mouseover" event', () => {
132
- ref.current.dispatchEvent(createEvent('pointerover'));
133
- ref.current.dispatchEvent(
134
- createEvent('mouseover', {
135
- button: 0,
136
- }),
137
- );
114
+ it('is called if a mouse pointer is used after a touch pointer', () => {
115
+ dispatchTouchTap(ref);
116
+ dispatchPointerHoverEnter(ref);
117
expect(onHoverStart).toHaveBeenCalledTimes(1);
118
});
140
-
141
- // No PointerEvent fallbacks
142
- it('is called after "mouseover" event', () => {
143
- ref.current.dispatchEvent(
144
- createEvent('mouseover', {
145
- button: 0,
146
- }),
147
- );
148
- expect(onHoverStart).toHaveBeenCalledTimes(1);
149
- });
150
-
151
- it('is not called after "touchstart"', () => {
152
- ref.current.dispatchEvent(
153
- createTouchEvent('touchstart', 0, {
154
- target: ref.current,
155
- }),
156
- );
157
- ref.current.dispatchEvent(
158
- createTouchEvent('touchend', 0, {
159
- target: ref.current,
160
- }),
161
- );
162
- ref.current.dispatchEvent(
163
- createEvent('mouseover', {
164
- button: 0,
165
- }),
166
- );
167
- expect(onHoverStart).not.toBeCalled();
168
- });
119
});
120
121
describe('onHoverChange', () => {
133
ReactDOM.render(<Component />, container);
134
});
135
186
- it('is called after "pointerover" and "pointerout" events', () => {
187
- ref.current.dispatchEvent(createEvent('pointerover'));
188
- expect(onHoverChange).toHaveBeenCalledTimes(1);
189
- expect(onHoverChange).toHaveBeenCalledWith(true);
190
- ref.current.dispatchEvent(createEvent('pointerout'));
191
- expect(onHoverChange).toHaveBeenCalledTimes(2);
192
- expect(onHoverChange).toHaveBeenCalledWith(false);
193
- });
194
-
195
- // No PointerEvent fallbacks
196
- it('is called after "mouseover" and "mouseout" events', () => {
197
- ref.current.dispatchEvent(createEvent('mouseover'));
136
+ it('is called for mouse pointers', () => {
137
+ dispatchPointerHoverEnter(ref);
138
expect(onHoverChange).toHaveBeenCalledTimes(1);
139
expect(onHoverChange).toHaveBeenCalledWith(true);
200
- ref.current.dispatchEvent(createEvent('mouseout'));
140
+ dispatchPointerHoverExit(ref);
141
expect(onHoverChange).toHaveBeenCalledTimes(2);
142
expect(onHoverChange).toHaveBeenCalledWith(false);
143
});
144
205
- it('should be user-blocking but not discrete', async () => {
206
- const {act} = TestUtils;
207
- const {useState} = React;
208
-
209
- const newContainer = document.createElement('div');
210
- document.body.appendChild(newContainer);
211
- const root = ReactDOM.unstable_createRoot(newContainer);
212
-
213
- const target = React.createRef(null);
214
- function Foo() {
215
- const [isHover, setHover] = useState(false);
216
- const listener = useHoverResponder({
217
- onHoverChange: setHover,
218
- });
219
- return (
220
- <div ref={target} listeners={listener}>
221
- {isHover ? 'hovered' : 'not hovered'}
222
- </div>
223
- );
224
- }
225
-
226
- await act(async () => {
227
- root.render(<Foo />);
228
- });
229
- expect(newContainer.textContent).toEqual('not hovered');
230
-
231
- await act(async () => {
232
- target.current.dispatchEvent(createEvent('mouseover'));
233
-
234
- // 3s should be enough to expire the updates
235
- Scheduler.unstable_advanceTime(3000);
236
- expect(newContainer.textContent).toEqual('hovered');
237
- });
145
+ it('is not called for touch pointers', () => {
146
+ dispatchTouchTap(ref);
147
+ expect(onHoverChange).not.toBeCalled();
148
});
149
});
150
163
ReactDOM.render(<Component />, container);
164
});
165
256
- it('is called after "pointerout" event', () => {
257
- ref.current.dispatchEvent(createEvent('pointerover'));
258
- ref.current.dispatchEvent(createEvent('pointerout'));
166
+ it('is called for mouse pointers', () => {
167
+ dispatchPointerHoverEnter(ref);
168
+ dispatchPointerHoverExit(ref);
169
expect(onHoverEnd).toHaveBeenCalledTimes(1);
170
});
171
262
- it('is not called if "pointerover" pointerType is touch', () => {
263
- const event = createEvent('pointerover');
264
- event.pointerType = 'touch';
265
- ref.current.dispatchEvent(event);
266
- ref.current.dispatchEvent(createEvent('pointerout'));
267
- expect(onHoverEnd).not.toBeCalled();
268
- });
269
-
270
- it('ignores browser emulated "mouseout" event', () => {
271
- ref.current.dispatchEvent(createEvent('pointerover'));
272
- ref.current.dispatchEvent(createEvent('pointerout'));
273
- ref.current.dispatchEvent(createEvent('mouseout'));
274
- expect(onHoverEnd).toHaveBeenCalledTimes(1);
275
- });
276
-
277
- it('is called after "pointercancel" event', () => {
278
- ref.current.dispatchEvent(createEvent('pointerover'));
279
- ref.current.dispatchEvent(createEvent('pointercancel'));
280
- expect(onHoverEnd).toHaveBeenCalledTimes(1);
281
- });
282
-
283
- it('is not called again after "pointercancel" event if it follows "pointerout"', () => {
284
- ref.current.dispatchEvent(createEvent('pointerover'));
285
- ref.current.dispatchEvent(createEvent('pointerout'));
286
- ref.current.dispatchEvent(createEvent('pointercancel'));
287
- expect(onHoverEnd).toHaveBeenCalledTimes(1);
288
- });
172
+ if (hasPointerEvents) {
173
+ it('is called once for cancelled mouse pointers', () => {
174
+ dispatchPointerHoverEnter(ref);
175
+ dispatchPointerCancel(ref);
176
+ expect(onHoverEnd).toHaveBeenCalledTimes(1);
177
+
178
+ // only called once if cancel follows exit
179
+ onHoverEnd.mockReset();
180
+ dispatchPointerHoverEnter(ref);
181
+ dispatchPointerHoverExit(ref);
182
+ dispatchPointerCancel(ref);
183
+ expect(onHoverEnd).toHaveBeenCalledTimes(1);
184
+ });
185
+ }
186
290
- // No PointerEvent fallbacks
291
- it('is called after "mouseout" event', () => {
292
- ref.current.dispatchEvent(createEvent('mouseover'));
293
- ref.current.dispatchEvent(createEvent('mouseout'));
294
- expect(onHoverEnd).toHaveBeenCalledTimes(1);
295
- });
296
- it('is not called after "touchend"', () => {
297
- ref.current.dispatchEvent(
298
- createTouchEvent('touchstart', 0, {
299
- target: ref.current,
300
- }),
301
- );
302
- ref.current.dispatchEvent(
303
- createTouchEvent('touchend', 0, {
304
- target: ref.current,
305
- }),
306
- );
307
- ref.current.dispatchEvent(createEvent('mouseout'));
187
+ it('is not called for touch pointers', () => {
188
+ dispatchTouchTap(ref);
189
expect(onHoverEnd).not.toBeCalled();
190
});
191
});
192
193
describe('onHoverMove', () => {
313
- it('is called after "pointermove"', () => {
194
+ it('is called after the active pointer moves"', () => {
195
const onHoverMove = jest.fn();
196
const ref = React.createRef();
197
const Component = () => {
201
return <div ref={ref} listeners={listener} />;
202
};
203
ReactDOM.render(<Component />, container);
323
-
324
- ref.current.getBoundingClientRect = () => ({
325
- top: 50,
326
- left: 50,
327
- bottom: 500,
328
- right: 500,
329
- });
330
- ref.current.dispatchEvent(createEvent('pointerover'));
331
- ref.current.dispatchEvent(
332
- createEvent('pointermove', {pointerType: 'mouse'}),
333
- );
334
- ref.current.dispatchEvent(createEvent('touchmove'));
335
- ref.current.dispatchEvent(createEvent('mousemove'));
336
- expect(onHoverMove).toHaveBeenCalledTimes(1);
204
+ dispatchPointerHoverEnter(ref);
205
+ dispatchPointerHoverMove(ref, {from: {x: 0, y: 0}, to: {x: 1, y: 1}});
206
+ expect(onHoverMove).toHaveBeenCalledTimes(2);
207
expect(onHoverMove).toHaveBeenCalledWith(
208
expect.objectContaining({type: 'hovermove'}),
209
);
242
};
243
ReactDOM.render(<Outer />, container);
244
375
- outerRef.current.dispatchEvent(createEvent('pointerover'));
376
- outerRef.current.dispatchEvent(
377
- createEvent('pointerout', {relatedTarget: innerRef.current}),
378
- );
379
- innerRef.current.dispatchEvent(createEvent('pointerover'));
380
- innerRef.current.dispatchEvent(
381
- createEvent('pointerout', {relatedTarget: outerRef.current}),
382
- );
383
- outerRef.current.dispatchEvent(
384
- createEvent('pointerover', {relatedTarget: innerRef.current}),
385
- );
386
- outerRef.current.dispatchEvent(createEvent('pointerout'));
245
+ dispatchPointerHoverEnter(outerRef, {relatedTarget: container});
246
+ dispatchPointerHoverExit(outerRef, {relatedTarget: innerRef.current});
247
+ dispatchPointerHoverEnter(innerRef, {relatedTarget: outerRef.current});
248
+ dispatchPointerHoverExit(innerRef, {relatedTarget: outerRef.current});
249
+ dispatchPointerHoverEnter(outerRef, {relatedTarget: innerRef.current});
250
+ dispatchPointerHoverExit(outerRef, {relatedTarget: container});
251
+
252
expect(events).toEqual([
253
'outer: onHoverStart',
254
'outer: onHoverChange',
276
const eventLog = [];
277
const logEvent = event => {
278
const propertiesWeCareAbout = {
279
+ x: event.x,
280
+ y: event.y,
281
pageX: event.pageX,
282
pageY: event.pageY,
416
- screenX: event.screenX,
417
- screenY: event.screenY,
283
clientX: event.clientX,
284
clientY: event.clientY,
285
pointerType: event.pointerType,
300
};
301
ReactDOM.render(<Component />, container);
302
438
- ref.current.getBoundingClientRect = () => ({
439
- top: 10,
440
- left: 10,
441
- bottom: 20,
442
- right: 20,
443
- });
303
+ dispatchPointerHoverEnter(ref, {x: 10, y: 10});
304
+ dispatchPointerHoverMove(ref, {from: {x: 10, y: 10}, to: {x: 20, y: 20}});
305
+ dispatchPointerHoverExit(ref, {x: 20, y: 20});
306
445
- ref.current.dispatchEvent(
446
- createEvent('pointerover', {
447
- pointerType: 'mouse',
448
- pageX: 15,
449
- pageY: 16,
450
- screenX: 20,
451
- screenY: 21,
452
- clientX: 30,
453
- clientY: 31,
454
- }),
455
- );
456
- ref.current.dispatchEvent(
457
- createEvent('pointermove', {
458
- pointerType: 'mouse',
459
- pageX: 16,
460
- pageY: 17,
461
- screenX: 21,
462
- screenY: 22,
463
- clientX: 31,
464
- clientY: 32,
465
- }),
466
- );
467
- ref.current.dispatchEvent(
468
- createEvent('pointerout', {
469
- pointerType: 'mouse',
470
- pageX: 17,
471
- pageY: 18,
472
- screenX: 22,
473
- screenY: 23,
474
- clientX: 32,
475
- clientY: 33,
476
- }),
477
- );
307
expect(eventLog).toEqual([
308
{
480
- pageX: 15,
481
- pageY: 16,
482
- screenX: 20,
483
- screenY: 21,
484
- clientX: 30,
485
- clientY: 31,
309
+ x: 10,
310
+ y: 10,
311
+ pageX: 10,
312
+ pageY: 10,
313
+ clientX: 10,
314
+ clientY: 10,
315
target: ref.current,
316
timeStamp: timeStamps[0],
317
type: 'hoverstart',
318
+ pointerType: 'mouse',
319
},
320
{
491
- pageX: 16,
492
- pageY: 17,
493
- screenX: 21,
494
- screenY: 22,
495
- clientX: 31,
496
- clientY: 32,
321
+ x: 10,
322
+ y: 10,
323
+ pageX: 10,
324
+ pageY: 10,
325
+ clientX: 10,
326
+ clientY: 10,
327
target: ref.current,
328
timeStamp: timeStamps[1],
329
type: 'hovermove',
330
+ pointerType: 'mouse',
331
},
332
{
502
- pageX: 17,
503
- pageY: 18,
504
- screenX: 22,
505
- screenY: 23,
506
- clientX: 32,
507
- clientY: 33,
333
+ x: 20,
334
+ y: 20,
335
+ pageX: 20,
336
+ pageY: 20,
337
+ clientX: 20,
338
+ clientY: 20,
339
target: ref.current,
340
timeStamp: timeStamps[2],
341
+ type: 'hovermove',
342
+ pointerType: 'mouse',
343
+ },
344
+ {
345
+ x: 20,
346
+ y: 20,
347
+ pageX: 20,
348
+ pageY: 20,
349
+ clientX: 20,
350
+ clientY: 20,
351
+ target: ref.current,
352
+ timeStamp: timeStamps[3],
353
type: 'hoverend',
354
+ pointerType: 'mouse',
355
},
356
]);
357
});