@samitouri / QOS-React / commits / f243deab82

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 });