Add tests for Press responder event module (#15290)
* Add Press responder event tests Behavior being tested takes cues from React Native's Pressability. A couple of these tests fail and require the Press implementation to be patched.
Nicolas Gallagher committed
Apr 2, 2019 at 08:49 UTC
f243deab8286b9c51f44e47926c1ba0ccb53449a
2 files changed
+339
-80
packages/react-events/src/Press.js
+1
-9
@@ -86,7 +86,7 @@ function dispatchPressStartEvents(
86
if ((props.onLongPress || props.onLongPressChange) && !state.isLongPressed) {
87
const delayLongPress = calculateDelayMS(
88
props.delayLongPress,
89
- 0,
89
+ 10,
90
DEFAULT_LONG_PRESS_DELAY_MS,
91
);
92
@@ -94,14 +94,6 @@ function dispatchPressStartEvents(
94
state.isLongPressed = true;
95
state.longPressTimeout = null;
96
97
- if (
98
- props.onPressChange &&
99
- props.onLongPressShouldCancelPress &&
100
- props.onLongPressShouldCancelPress()
101
- ) {
102
- dispatchPressChangeEvent(false);
103
- }
104
-
97
if (props.onLongPress) {
98
const longPressEventListener = e => {
99
props.onLongPress(e);
packages/react-events/src/__tests__/Press-test.internal.js
+338
-71
@@ -14,7 +14,23 @@ let ReactFeatureFlags;
14
let ReactDOM;
15
let Press;
16
17
-describe('Press event responder', () => {
17
+const DEFAULT_LONG_PRESS_DELAY = 1000;
18
+
19
+const createPointerEvent = type => {
20
+ const event = document.createEvent('Event');
21
+ event.initEvent(type, true, true);
22
+ return event;
23
+};
24
+
25
+const createKeyboardEvent = (type, data) => {
26
+ return new KeyboardEvent(type, {
27
+ bubbles: true,
28
+ cancelable: true,
29
+ ...data,
30
+ });
31
+};
32
+
33
+describe('Event responder: Press', () => {
34
let container;
35
36
beforeEach(() => {
@@ -34,95 +50,346 @@ describe('Press event responder', () => {
50
container = null;
51
});
52
37
- it('should support onPress', () => {
38
- let buttonRef = React.createRef();
39
- let events = [];
40
-
41
- function handleOnPress1() {
42
- events.push('press 1');
43
- }
44
-
45
- function handleOnPress2() {
46
- events.push('press 2');
47
- }
48
-
49
- function handleOnMouseDown() {
50
- events.push('mousedown');
51
- }
52
-
53
- function handleKeyDown() {
54
- events.push('keydown');
55
- }
56
-
57
- function Component() {
58
- return (
59
- <Press onPress={handleOnPress1}>
60
- <Press onPress={handleOnPress2}>
61
- <button
62
- ref={buttonRef}
63
- onMouseDown={handleOnMouseDown}
64
- onKeyDown={handleKeyDown}>
65
- Press me!
66
- </button>
67
- </Press>
53
+ describe('onPressStart', () => {
54
+ let onPressStart, ref;
55
+
56
+ beforeEach(() => {
57
+ onPressStart = jest.fn();
58
+ ref = React.createRef();
59
+ const element = (
60
+ <Press onPressStart={onPressStart}>
61
+ <div ref={ref} />
62
</Press>
63
);
70
- }
64
+ ReactDOM.render(element, container);
65
+ });
66
72
- ReactDOM.render(<Component />, container);
67
+ it('is called after "pointerdown" event', () => {
68
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
69
+ expect(onPressStart).toHaveBeenCalledTimes(1);
70
+ });
71
74
- const mouseDownEvent = document.createEvent('Event');
75
- mouseDownEvent.initEvent('mousedown', true, true);
76
- buttonRef.current.dispatchEvent(mouseDownEvent);
72
+ it('ignores emulated "mousedown" event', () => {
73
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
74
+ ref.current.dispatchEvent(createPointerEvent('mousedown'));
75
+ expect(onPressStart).toHaveBeenCalledTimes(1);
76
+ });
77
78
- const mouseUpEvent = document.createEvent('Event');
79
- mouseUpEvent.initEvent('mouseup', true, true);
80
- buttonRef.current.dispatchEvent(mouseUpEvent);
78
+ // No PointerEvent fallbacks
79
+ it('is called after "mousedown" event', () => {
80
+ ref.current.dispatchEvent(createPointerEvent('mousedown'));
81
+ expect(onPressStart).toHaveBeenCalledTimes(1);
82
+ });
83
+ it('is called after "touchstart" event', () => {
84
+ ref.current.dispatchEvent(createPointerEvent('touchstart'));
85
+ expect(onPressStart).toHaveBeenCalledTimes(1);
86
+ });
87
+
88
+ // TODO: complete delayPressStart tests
89
+ // describe('delayPressStart', () => {});
90
+ });
91
82
- expect(events).toEqual(['mousedown', 'press 2', 'press 1']);
92
+ describe('onPressEnd', () => {
93
+ let onPressEnd, ref;
94
+
95
+ beforeEach(() => {
96
+ onPressEnd = jest.fn();
97
+ ref = React.createRef();
98
+ const element = (
99
+ <Press onPressEnd={onPressEnd}>
100
+ <div ref={ref} />
101
+ </Press>
102
+ );
103
+ ReactDOM.render(element, container);
104
+ });
105
+
106
+ it('is called after "pointerup" event', () => {
107
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
108
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
109
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
110
+ });
111
84
- events = [];
85
- const keyDownEvent = new KeyboardEvent('keydown', {
86
- key: 'Enter',
87
- bubbles: true,
88
- cancelable: true,
112
+ it('ignores emulated "mouseup" event', () => {
113
+ ref.current.dispatchEvent(createPointerEvent('touchstart'));
114
+ ref.current.dispatchEvent(createPointerEvent('touchend'));
115
+ ref.current.dispatchEvent(createPointerEvent('mouseup'));
116
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
117
+ });
118
+
119
+ // No PointerEvent fallbacks
120
+ it('is called after "mouseup" event', () => {
121
+ ref.current.dispatchEvent(createPointerEvent('mousedown'));
122
+ ref.current.dispatchEvent(createPointerEvent('mouseup'));
123
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
124
+ });
125
+
126
+ it('is called after "touchend" event', () => {
127
+ ref.current.dispatchEvent(createPointerEvent('touchstart'));
128
+ ref.current.dispatchEvent(createPointerEvent('touchend'));
129
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
130
+ });
131
+
132
+ // TODO: complete delayPressStart tests
133
+ // describe('delayPressStart', () => {});
134
+ });
135
+
136
+ describe('onPressChange', () => {
137
+ let onPressChange, ref;
138
+
139
+ beforeEach(() => {
140
+ onPressChange = jest.fn();
141
+ ref = React.createRef();
142
+ const element = (
143
+ <Press onPressChange={onPressChange}>
144
+ <div ref={ref} />
145
+ </Press>
146
+ );
147
+ ReactDOM.render(element, container);
148
+ });
149
+
150
+ it('is called after "pointerdown" and "pointerup" events', () => {
151
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
152
+ expect(onPressChange).toHaveBeenCalledTimes(1);
153
+ expect(onPressChange).toHaveBeenCalledWith(true);
154
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
155
+ expect(onPressChange).toHaveBeenCalledTimes(2);
156
+ expect(onPressChange).toHaveBeenCalledWith(false);
157
+ });
158
+ });
159
+
160
+ describe('onPress', () => {
161
+ let onPress, ref;
162
+
163
+ beforeEach(() => {
164
+ onPress = jest.fn();
165
+ ref = React.createRef();
166
+ const element = (
167
+ <Press onPress={onPress}>
168
+ <div ref={ref} />
169
+ </Press>
170
+ );
171
+ ReactDOM.render(element, container);
172
});
90
- buttonRef.current.dispatchEvent(keyDownEvent);
173
92
- // press 1 should not occur as press 2 will preventDefault
93
- expect(events).toEqual(['keydown', 'press 2']);
174
+ it('is called after "pointerup" event', () => {
175
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
176
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
177
+ expect(onPress).toHaveBeenCalledTimes(1);
178
+ });
179
});
180
96
- it('should support onPressStart and onPressEnd', () => {
97
- let divRef = React.createRef();
98
- let events = [];
181
+ describe('onLongPress', () => {
182
+ let onLongPress, ref;
183
+
184
+ beforeEach(() => {
185
+ onLongPress = jest.fn();
186
+ ref = React.createRef();
187
+ const element = (
188
+ <Press onLongPress={onLongPress}>
189
+ <div ref={ref} />
190
+ </Press>
191
+ );
192
+ ReactDOM.render(element, container);
193
+ });
194
+
195
+ it('is called if press lasts default delay', () => {
196
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
197
+ jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1);
198
+ expect(onLongPress).not.toBeCalled();
199
+ jest.advanceTimersByTime(1);
200
+ expect(onLongPress).toHaveBeenCalledTimes(1);
201
+ });
202
100
- function handleOnPressStart() {
101
- events.push('onPressStart');
102
- }
203
+ it('is not called if press is released before delay', () => {
204
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
205
+ jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1);
206
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
207
+ jest.advanceTimersByTime(1);
208
+ expect(onLongPress).not.toBeCalled();
209
+ });
210
104
- function handleOnPressEnd() {
105
- events.push('onPressEnd');
106
- }
211
+ describe('delayLongPress', () => {
212
+ it('can be configured', () => {
213
+ const element = (
214
+ <Press delayLongPress={2000} onLongPress={onLongPress}>
215
+ <div ref={ref} />
216
+ </Press>
217
+ );
218
+ ReactDOM.render(element, container);
219
+
220
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
221
+ jest.advanceTimersByTime(1999);
222
+ expect(onLongPress).not.toBeCalled();
223
+ jest.advanceTimersByTime(1);
224
+ expect(onLongPress).toHaveBeenCalledTimes(1);
225
+ });
226
+
227
+ it('uses 10ms minimum delay length', () => {
228
+ const element = (
229
+ <Press delayLongPress={0} onLongPress={onLongPress}>
230
+ <div ref={ref} />
231
+ </Press>
232
+ );
233
+ ReactDOM.render(element, container);
234
+
235
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
236
+ jest.advanceTimersByTime(9);
237
+ expect(onLongPress).not.toBeCalled();
238
+ jest.advanceTimersByTime(1);
239
+ expect(onLongPress).toHaveBeenCalledTimes(1);
240
+ });
241
+
242
+ /*
243
+ it('compounds with "delayPressStart"', () => {
244
+ const delayPressStart = 100;
245
+ const element = (
246
+ <Press delayPressStart={delayPressStart} onLongPress={onLongPress}>
247
+ <div ref={ref} />
248
+ </Press>
249
+ );
250
+ ReactDOM.render(element, container);
251
+
252
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
253
+ jest.advanceTimersByTime(delayPressStart + DEFAULT_LONG_PRESS_DELAY - 1);
254
+ expect(onLongPress).not.toBeCalled();
255
+ jest.advanceTimersByTime(1);
256
+ expect(onLongPress).toHaveBeenCalledTimes(1);
257
+ });
258
+ */
259
+ });
260
+ });
261
108
- function Component() {
109
- return (
110
- <Press onPressStart={handleOnPressStart} onPressEnd={handleOnPressEnd}>
111
- <div ref={divRef}>Press me!</div>
262
+ describe('onLongPressChange', () => {
263
+ it('is called when long press state changes', () => {
264
+ const onLongPressChange = jest.fn();
265
+ const ref = React.createRef();
266
+ const element = (
267
+ <Press onLongPressChange={onLongPressChange}>
268
+ <div ref={ref} />
269
</Press>
270
);
114
- }
271
+ ReactDOM.render(element, container);
272
116
- ReactDOM.render(<Component />, container);
273
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
274
+ jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY);
275
+ expect(onLongPressChange).toHaveBeenCalledTimes(1);
276
+ expect(onLongPressChange).toHaveBeenCalledWith(true);
277
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
278
+ expect(onLongPressChange).toHaveBeenCalledTimes(2);
279
+ expect(onLongPressChange).toHaveBeenCalledWith(false);
280
+ });
281
+ });
282
+
283
+ describe('onLongPressShouldCancelPress', () => {
284
+ it('if true it cancels "onPress"', () => {
285
+ const onPress = jest.fn();
286
+ const onPressChange = jest.fn();
287
+ const ref = React.createRef();
288
+ const element = (
289
+ <Press
290
+ onLongPress={() => {}}
291
+ onLongPressShouldCancelPress={() => true}
292
+ onPressChange={onPressChange}
293
+ onPress={onPress}>
294
+ <div ref={ref} />
295
+ </Press>
296
+ );
297
+ ReactDOM.render(element, container);
298
+
299
+ // NOTE: onPressChange behavior should not be affected
300
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
301
+ expect(onPressChange).toHaveBeenCalledTimes(1);
302
+ jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY);
303
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
304
+ expect(onPress).not.toBeCalled();
305
+ expect(onPressChange).toHaveBeenCalledTimes(2);
306
+ });
307
+ });
308
+
309
+ // TODO
310
+ //describe('`onPress*` with movement', () => {
311
+ //describe('within bounds of hit rect', () => {
312
+ /** ┌──────────────────┐
313
+ * │ ┌────────────┐ │
314
+ * │ │ VisualRect │ │
315
+ * │ └────────────┘ │
316
+ * │ HitRect X │ <= Move to X and release
317
+ * └──────────────────┘
318
+ */
319
118
- const pointerEnterEvent = document.createEvent('Event');
119
- pointerEnterEvent.initEvent('pointerdown', true, true);
120
- divRef.current.dispatchEvent(pointerEnterEvent);
320
+ //it('"onPress*" events are called when no delay', () => {});
321
+ //it('"onPress*" events are called after a delay', () => {});
322
+ //});
323
122
- const pointerLeaveEvent = document.createEvent('Event');
123
- pointerLeaveEvent.initEvent('pointerup', true, true);
124
- divRef.current.dispatchEvent(pointerLeaveEvent);
324
+ //describe('beyond bounds of hit rect', () => {
325
+ /** ┌──────────────────┐
326
+ * │ ┌────────────┐ │
327
+ * │ │ VisualRect │ │
328
+ * │ └────────────┘ │
329
+ * │ HitRect │
330
+ * └──────────────────┘
331
+ * X <= Move to X and release
332
+ */
333
126
- expect(events).toEqual(['onPressStart', 'onPressEnd']);
334
+ //it('"onPress" only is not called when no delay', () => {});
335
+ //it('"onPress*" events are not called after a delay', () => {});
336
+ //it('"onPress*" events are called when press is released before measure completes', () => {});
337
+ //});
338
+ //});
339
+
340
+ describe('nested responders', () => {
341
+ it('dispatch events in the correct order', () => {
342
+ let events = [];
343
+ const ref = React.createRef();
344
+ const createEventHandler = msg => () => {
345
+ events.push(msg);
346
+ };
347
+
348
+ const element = (
349
+ <Press
350
+ onPress={createEventHandler('outer: onPress')}
351
+ onPressChange={createEventHandler('outer: onPressChange')}
352
+ onPressStart={createEventHandler('outer: onPressStart')}
353
+ onPressEnd={createEventHandler('outer: onPressEnd')}>
354
+ <Press
355
+ onPress={createEventHandler('inner: onPress')}
356
+ onPressChange={createEventHandler('inner: onPressChange')}
357
+ onPressStart={createEventHandler('inner: onPressStart')}
358
+ onPressEnd={createEventHandler('inner: onPressEnd')}>
359
+ <div
360
+ ref={ref}
361
+ onPointerDown={createEventHandler('pointerdown')}
362
+ onPointerUp={createEventHandler('pointerup')}
363
+ onKeyDown={createEventHandler('keydown')}
364
+ onKeyUp={createEventHandler('keyup')}
365
+ />
366
+ </Press>
367
+ </Press>
368
+ );
369
+
370
+ ReactDOM.render(element, container);
371
+
372
+ ref.current.dispatchEvent(createPointerEvent('pointerdown'));
373
+ ref.current.dispatchEvent(createPointerEvent('pointerup'));
374
+ expect(events).toEqual([
375
+ 'pointerdown',
376
+ 'inner: onPressStart',
377
+ 'inner: onPressChange',
378
+ 'outer: onPressStart',
379
+ 'outer: onPressChange',
380
+ 'pointerup',
381
+ 'inner: onPressEnd',
382
+ 'inner: onPressChange',
383
+ 'inner: onPress',
384
+ 'outer: onPressEnd',
385
+ 'outer: onPressChange',
386
+ 'outer: onPress',
387
+ ]);
388
+
389
+ events = [];
390
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
391
+ // Outer press should not occur as inner press will preventDefault
392
+ expect(events).toEqual(['keydown', 'inner: onPress']);
393
+ });
394
});
395
});