Skip to content

Commit 93b51b5

Browse files
vincentriemerfacebook-github-bot
authored andcommitted
Add MoveAcross test for pointer events
Summary: Changelog: [RNTester][Internal] - Add "move across" test for pointer events This diff adds a new platform test ported from the wpt's [mousemove-across test](https://github.com/web-platform-tests/wpt/blob/master/uievents/order-of-events/mouse-events/mousemove-across.html) along with a rough port of the wpt's event recorder class which is made to work in a react component environment. Reviewed By: lunaleaps Differential Revision: D39221252 fbshipit-source-id: 16b2e03dbc71a2e83cc43af1e950803feaf6657b
1 parent 7be829f commit 93b51b5

File tree

3 files changed

+348
-0
lines changed

3 files changed

+348
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
* @format
8+
* @flow
9+
*/
10+
11+
import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes';
12+
13+
import {useMemo} from 'react';
14+
15+
type EventRecorderOptions = $ReadOnly<{
16+
mergeEventTypes: Array<string>,
17+
relevantEvents: Array<string>,
18+
}>;
19+
20+
type EventRecord = {
21+
chronologicalOrder: number,
22+
sequentialOccurrences: number,
23+
nestedEvents: ?Array<EventRecord>,
24+
target: string,
25+
type: string,
26+
event: Object,
27+
};
28+
29+
class RNTesterPlatformTestEventRecorder {
30+
allRecords: Array<EventRecord> = [];
31+
relevantEvents: Array<string> = [];
32+
rawOrder: number = 1;
33+
eventsInScope: Array<EventRecord> = []; // Tracks syncronous event dispatches
34+
recording: boolean = true;
35+
36+
mergeTypesTruthMap: {[string]: boolean} = {};
37+
38+
constructor(options: EventRecorderOptions) {
39+
if (options.mergeEventTypes && Array.isArray(options.mergeEventTypes)) {
40+
options.mergeEventTypes.forEach(eventType => {
41+
this.mergeTypesTruthMap[eventType] = true;
42+
});
43+
}
44+
if (options.relevantEvents && Array.isArray(options.relevantEvents)) {
45+
this.relevantEvents = options.relevantEvents;
46+
}
47+
}
48+
49+
_createEventRecord(
50+
rawEvent: Object,
51+
target: string,
52+
type: string,
53+
): EventRecord {
54+
return {
55+
chronologicalOrder: this.rawOrder++,
56+
sequentialOccurrences: 1,
57+
nestedEvents: undefined,
58+
target,
59+
type,
60+
event: rawEvent,
61+
};
62+
}
63+
64+
_recordEvent(e: Object, targetName: string, eventType: string): ?EventRecord {
65+
const record = this._createEventRecord(e, targetName, eventType);
66+
let recordList = this.allRecords;
67+
// Adjust which sequential list to use depending on scope
68+
if (this.eventsInScope.length > 0) {
69+
let newRecordList =
70+
this.eventsInScope[this.eventsInScope.length - 1].nestedEvents;
71+
if (newRecordList == null) {
72+
newRecordList = this.eventsInScope[
73+
this.eventsInScope.length - 1
74+
].nestedEvents = [];
75+
}
76+
recordList = newRecordList;
77+
}
78+
if (this.mergeTypesTruthMap[eventType] && recordList.length > 0) {
79+
const tail = recordList[recordList.length - 1];
80+
// Same type and target?
81+
if (tail.type === eventType && tail.target === targetName) {
82+
tail.sequentialOccurrences++;
83+
return;
84+
}
85+
}
86+
recordList.push(record);
87+
return record;
88+
}
89+
90+
_generateRecordedEventHandlerWithCallback(
91+
targetName: string,
92+
callback?: (event: Object, eventType: string) => void,
93+
): (Object, string) => void {
94+
return (e: Object, eventType: string) => {
95+
if (this.recording) {
96+
this._recordEvent(e, targetName, eventType);
97+
if (callback) {
98+
callback(e, eventType);
99+
}
100+
}
101+
};
102+
}
103+
104+
useRecorderTestEventHandlers(
105+
targetNames: $ReadOnlyArray<string>,
106+
callback?: (event: Object, eventType: string, targetName: string) => void,
107+
): $ReadOnly<{[targetName: string]: ViewProps}> {
108+
// Yes this method exists as a class's prototype method but it will still only be used
109+
// in functional components
110+
// eslint-disable-next-line react-hooks/rules-of-hooks
111+
return useMemo(() => {
112+
const result: {[targetName: string]: ViewProps} = {};
113+
for (const targetName of targetNames) {
114+
const recordedEventHandler =
115+
this._generateRecordedEventHandlerWithCallback(
116+
targetName,
117+
(event, eventType) =>
118+
callback && callback(event, eventType, targetName),
119+
);
120+
const eventListenerProps = this.relevantEvents.reduce(
121+
(acc, eventName) => {
122+
const eventPropName =
123+
'on' + eventName[0].toUpperCase() + eventName.slice(1);
124+
return {
125+
...acc,
126+
[eventPropName]: e => {
127+
recordedEventHandler(e, eventName);
128+
},
129+
};
130+
},
131+
{},
132+
);
133+
result[targetName] = eventListenerProps;
134+
}
135+
return result;
136+
}, [callback, targetNames]);
137+
}
138+
139+
checkRecords(
140+
expected: Array<{
141+
type: string,
142+
target: string,
143+
optional?: boolean,
144+
}>,
145+
): boolean {
146+
if (expected.length < this.allRecords.length) {
147+
return false;
148+
}
149+
let j = 0;
150+
for (let i = 0; i < expected.length; ++i) {
151+
if (j >= this.allRecords.length) {
152+
if (expected[i].optional === true) {
153+
continue;
154+
}
155+
return false;
156+
}
157+
if (
158+
expected[i].type === this.allRecords[j].type &&
159+
expected[i].target === this.allRecords[j].target
160+
) {
161+
j++;
162+
continue;
163+
}
164+
if (expected[i].optional === true) {
165+
continue;
166+
}
167+
return false;
168+
}
169+
return true;
170+
}
171+
}
172+
173+
export default RNTesterPlatformTestEventRecorder;
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
* @format
8+
* @flow
9+
*/
10+
11+
import type {PlatformTestComponentBaseProps} from '../PlatformTest/RNTesterPlatformTestTypes';
12+
13+
import RNTesterPlatformTest from '../PlatformTest/RNTesterPlatformTest';
14+
import RNTesterPlatformTestEventRecorder from '../PlatformTest/RNTesterPlatformTestEventRecorder';
15+
import * as React from 'react';
16+
import {useCallback, useState} from 'react';
17+
import {View, StyleSheet} from 'react-native';
18+
19+
const styles = StyleSheet.create({
20+
a: {
21+
backgroundColor: 'red',
22+
height: 120,
23+
width: 200,
24+
},
25+
b: {
26+
marginLeft: 100,
27+
height: 120,
28+
width: 200,
29+
backgroundColor: 'green',
30+
},
31+
c: {
32+
height: 120,
33+
width: 200,
34+
backgroundColor: 'yellow',
35+
marginVertical: 100,
36+
marginLeft: 100,
37+
},
38+
a1: {
39+
backgroundColor: 'blue',
40+
height: 120,
41+
width: 200,
42+
},
43+
b1: {
44+
padding: 1,
45+
marginLeft: 100,
46+
height: 120,
47+
width: 200,
48+
backgroundColor: 'green',
49+
},
50+
c1: {
51+
height: 120,
52+
width: 200,
53+
backgroundColor: 'black',
54+
marginLeft: 100,
55+
},
56+
});
57+
58+
const relevantEvents = [
59+
'pointerMove',
60+
'pointerOver',
61+
'pointerEnter',
62+
'pointerOut',
63+
'pointerLeave',
64+
];
65+
66+
const expected = [
67+
{type: 'pointerOver', target: 'a'},
68+
{type: 'pointerEnter', target: 'c'},
69+
{type: 'pointerEnter', target: 'b'},
70+
{type: 'pointerEnter', target: 'a'},
71+
{type: 'pointerMove', target: 'a', optional: true},
72+
{type: 'pointerOut', target: 'a'},
73+
{type: 'pointerLeave', target: 'a'},
74+
{type: 'pointerLeave', target: 'b'},
75+
{type: 'pointerOver', target: 'c'},
76+
{type: 'pointerMove', target: 'c', optional: true},
77+
{type: 'pointerOut', target: 'c'},
78+
{type: 'pointerLeave', target: 'c'},
79+
{type: 'pointerOver', target: 'a1'},
80+
{type: 'pointerEnter', target: 'c1'},
81+
{type: 'pointerEnter', target: 'b1'},
82+
{type: 'pointerEnter', target: 'a1'},
83+
{type: 'pointerMove', target: 'a1', optional: true},
84+
{type: 'pointerOut', target: 'a1'},
85+
{type: 'pointerLeave', target: 'a1'},
86+
{type: 'pointerLeave', target: 'b1'},
87+
{type: 'pointerOver', target: 'c1'},
88+
{type: 'pointerMove', target: 'c1', optional: true},
89+
{type: 'pointerOut', target: 'c1'},
90+
{type: 'pointerLeave', target: 'c1'},
91+
];
92+
93+
const targetNames = ['a', 'b', 'c', 'a1', 'b1', 'c1'];
94+
95+
// adapted from https://github.com/web-platform-tests/wpt/blob/master/uievents/order-of-events/mouse-events/mousemove-across.html
96+
function PointerEventPointerMoveAcrossTestCase(
97+
props: PlatformTestComponentBaseProps,
98+
) {
99+
const {harness} = props;
100+
101+
const pointermove_across = harness.useAsyncTest(
102+
'Pointermove events across elements should fire in the correct order.',
103+
);
104+
105+
const [eventRecorder] = useState(
106+
() =>
107+
new RNTesterPlatformTestEventRecorder({
108+
mergeEventTypes: ['pointerMove'],
109+
relevantEvents,
110+
}),
111+
);
112+
113+
const eventHandler = useCallback(
114+
(event: PointerEvent, eventType: string, eventTarget: string) => {
115+
event.stopPropagation();
116+
if (eventTarget === 'c1' && eventType === 'pointerLeave') {
117+
pointermove_across.step(({assert_true}) => {
118+
assert_true(
119+
eventRecorder.checkRecords(expected),
120+
'Expected events to occur in the correct order',
121+
);
122+
});
123+
pointermove_across.done();
124+
}
125+
},
126+
[eventRecorder, pointermove_across],
127+
);
128+
129+
const eventProps = eventRecorder.useRecorderTestEventHandlers(
130+
targetNames,
131+
eventHandler,
132+
);
133+
134+
return (
135+
<>
136+
<View {...eventProps.c} style={styles.c}>
137+
<View {...eventProps.b} style={styles.b}>
138+
<View {...eventProps.a} style={styles.a} />
139+
</View>
140+
</View>
141+
<View {...eventProps.c1} style={styles.c1}>
142+
<View {...eventProps.b1} style={styles.b1}>
143+
<View {...eventProps.a1} style={styles.a1} />
144+
</View>
145+
</View>
146+
</>
147+
);
148+
}
149+
150+
type Props = $ReadOnly<{}>;
151+
export default function PointerEventPointerMoveAcross(
152+
props: Props,
153+
): React.MixedElement {
154+
return (
155+
<RNTesterPlatformTest
156+
component={PointerEventPointerMoveAcrossTestCase}
157+
description=""
158+
instructions={[
159+
'Move your mouse across the yellow/red <div> element quickly from right to left',
160+
'Move your mouse across the black/blue <div> element quickly from right to left',
161+
'If the test fails, redo it again and move faster on the black/blue <div> element next time.',
162+
]}
163+
title="Pointermove handling across elements"
164+
/>
165+
);
166+
}

packages/rn-tester/js/examples/Experimental/W3CPointerEventsExample.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import CompatibilityNativeGestureHandling from './Compatibility/CompatibilityNat
1818
import PointerEventPrimaryTouchPointer from './W3CPointerEventPlatformTests/PointerEventPrimaryTouchPointer';
1919
import PointerEventAttributesNoHoverPointers from './W3CPointerEventPlatformTests/PointerEventAttributesNoHoverPointers';
2020
import PointerEventPointerMoveOnChordedMouseButton from './W3CPointerEventPlatformTests/PointerEventPointerMoveOnChordedMouseButton';
21+
import PointerEventPointerMoveAcross from './W3CPointerEventPlatformTests/PointerEventPointerMoveAcross';
2122
import EventfulView from './W3CPointerEventsEventfulView';
2223

2324
function AbsoluteChildExample({log}: {log: string => void}) {
@@ -202,6 +203,14 @@ export default {
202203
return <PointerEventPointerMoveOnChordedMouseButton />;
203204
},
204205
},
206+
{
207+
name: 'pointerevent_pointermove_across',
208+
description: '',
209+
title: 'Pointermove handling across elements',
210+
render(): React.Node {
211+
return <PointerEventPointerMoveAcross />;
212+
},
213+
},
205214
CompatibilityAnimatedPointerMove,
206215
CompatibilityNativeGestureHandling,
207216
],

0 commit comments

Comments
 (0)