Skip to content

Commit

Permalink
Add MoveAcross test for pointer events
Browse files Browse the repository at this point in the history
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
  • Loading branch information
vincentriemer authored and facebook-github-bot committed Sep 12, 2022
1 parent 7be829f commit 93b51b5
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/

import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes';

import {useMemo} from 'react';

type EventRecorderOptions = $ReadOnly<{
mergeEventTypes: Array<string>,
relevantEvents: Array<string>,
}>;

type EventRecord = {
chronologicalOrder: number,
sequentialOccurrences: number,
nestedEvents: ?Array<EventRecord>,
target: string,
type: string,
event: Object,
};

class RNTesterPlatformTestEventRecorder {
allRecords: Array<EventRecord> = [];
relevantEvents: Array<string> = [];
rawOrder: number = 1;
eventsInScope: Array<EventRecord> = []; // Tracks syncronous event dispatches
recording: boolean = true;

mergeTypesTruthMap: {[string]: boolean} = {};

constructor(options: EventRecorderOptions) {
if (options.mergeEventTypes && Array.isArray(options.mergeEventTypes)) {
options.mergeEventTypes.forEach(eventType => {
this.mergeTypesTruthMap[eventType] = true;
});
}
if (options.relevantEvents && Array.isArray(options.relevantEvents)) {
this.relevantEvents = options.relevantEvents;
}
}

_createEventRecord(
rawEvent: Object,
target: string,
type: string,
): EventRecord {
return {
chronologicalOrder: this.rawOrder++,
sequentialOccurrences: 1,
nestedEvents: undefined,
target,
type,
event: rawEvent,
};
}

_recordEvent(e: Object, targetName: string, eventType: string): ?EventRecord {
const record = this._createEventRecord(e, targetName, eventType);
let recordList = this.allRecords;
// Adjust which sequential list to use depending on scope
if (this.eventsInScope.length > 0) {
let newRecordList =
this.eventsInScope[this.eventsInScope.length - 1].nestedEvents;
if (newRecordList == null) {
newRecordList = this.eventsInScope[
this.eventsInScope.length - 1
].nestedEvents = [];
}
recordList = newRecordList;
}
if (this.mergeTypesTruthMap[eventType] && recordList.length > 0) {
const tail = recordList[recordList.length - 1];
// Same type and target?
if (tail.type === eventType && tail.target === targetName) {
tail.sequentialOccurrences++;
return;
}
}
recordList.push(record);
return record;
}

_generateRecordedEventHandlerWithCallback(
targetName: string,
callback?: (event: Object, eventType: string) => void,
): (Object, string) => void {
return (e: Object, eventType: string) => {
if (this.recording) {
this._recordEvent(e, targetName, eventType);
if (callback) {
callback(e, eventType);
}
}
};
}

useRecorderTestEventHandlers(
targetNames: $ReadOnlyArray<string>,
callback?: (event: Object, eventType: string, targetName: string) => void,
): $ReadOnly<{[targetName: string]: ViewProps}> {
// Yes this method exists as a class's prototype method but it will still only be used
// in functional components
// eslint-disable-next-line react-hooks/rules-of-hooks
return useMemo(() => {
const result: {[targetName: string]: ViewProps} = {};
for (const targetName of targetNames) {
const recordedEventHandler =
this._generateRecordedEventHandlerWithCallback(
targetName,
(event, eventType) =>
callback && callback(event, eventType, targetName),
);
const eventListenerProps = this.relevantEvents.reduce(
(acc, eventName) => {
const eventPropName =
'on' + eventName[0].toUpperCase() + eventName.slice(1);
return {
...acc,
[eventPropName]: e => {
recordedEventHandler(e, eventName);
},
};
},
{},
);
result[targetName] = eventListenerProps;
}
return result;
}, [callback, targetNames]);
}

checkRecords(
expected: Array<{
type: string,
target: string,
optional?: boolean,
}>,
): boolean {
if (expected.length < this.allRecords.length) {
return false;
}
let j = 0;
for (let i = 0; i < expected.length; ++i) {
if (j >= this.allRecords.length) {
if (expected[i].optional === true) {
continue;
}
return false;
}
if (
expected[i].type === this.allRecords[j].type &&
expected[i].target === this.allRecords[j].target
) {
j++;
continue;
}
if (expected[i].optional === true) {
continue;
}
return false;
}
return true;
}
}

export default RNTesterPlatformTestEventRecorder;
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/

import type {PlatformTestComponentBaseProps} from '../PlatformTest/RNTesterPlatformTestTypes';

import RNTesterPlatformTest from '../PlatformTest/RNTesterPlatformTest';
import RNTesterPlatformTestEventRecorder from '../PlatformTest/RNTesterPlatformTestEventRecorder';
import * as React from 'react';
import {useCallback, useState} from 'react';
import {View, StyleSheet} from 'react-native';

const styles = StyleSheet.create({
a: {
backgroundColor: 'red',
height: 120,
width: 200,
},
b: {
marginLeft: 100,
height: 120,
width: 200,
backgroundColor: 'green',
},
c: {
height: 120,
width: 200,
backgroundColor: 'yellow',
marginVertical: 100,
marginLeft: 100,
},
a1: {
backgroundColor: 'blue',
height: 120,
width: 200,
},
b1: {
padding: 1,
marginLeft: 100,
height: 120,
width: 200,
backgroundColor: 'green',
},
c1: {
height: 120,
width: 200,
backgroundColor: 'black',
marginLeft: 100,
},
});

const relevantEvents = [
'pointerMove',
'pointerOver',
'pointerEnter',
'pointerOut',
'pointerLeave',
];

const expected = [
{type: 'pointerOver', target: 'a'},
{type: 'pointerEnter', target: 'c'},
{type: 'pointerEnter', target: 'b'},
{type: 'pointerEnter', target: 'a'},
{type: 'pointerMove', target: 'a', optional: true},
{type: 'pointerOut', target: 'a'},
{type: 'pointerLeave', target: 'a'},
{type: 'pointerLeave', target: 'b'},
{type: 'pointerOver', target: 'c'},
{type: 'pointerMove', target: 'c', optional: true},
{type: 'pointerOut', target: 'c'},
{type: 'pointerLeave', target: 'c'},
{type: 'pointerOver', target: 'a1'},
{type: 'pointerEnter', target: 'c1'},
{type: 'pointerEnter', target: 'b1'},
{type: 'pointerEnter', target: 'a1'},
{type: 'pointerMove', target: 'a1', optional: true},
{type: 'pointerOut', target: 'a1'},
{type: 'pointerLeave', target: 'a1'},
{type: 'pointerLeave', target: 'b1'},
{type: 'pointerOver', target: 'c1'},
{type: 'pointerMove', target: 'c1', optional: true},
{type: 'pointerOut', target: 'c1'},
{type: 'pointerLeave', target: 'c1'},
];

const targetNames = ['a', 'b', 'c', 'a1', 'b1', 'c1'];

// adapted from https://github.com/web-platform-tests/wpt/blob/master/uievents/order-of-events/mouse-events/mousemove-across.html
function PointerEventPointerMoveAcrossTestCase(
props: PlatformTestComponentBaseProps,
) {
const {harness} = props;

const pointermove_across = harness.useAsyncTest(
'Pointermove events across elements should fire in the correct order.',
);

const [eventRecorder] = useState(
() =>
new RNTesterPlatformTestEventRecorder({
mergeEventTypes: ['pointerMove'],
relevantEvents,
}),
);

const eventHandler = useCallback(
(event: PointerEvent, eventType: string, eventTarget: string) => {
event.stopPropagation();
if (eventTarget === 'c1' && eventType === 'pointerLeave') {
pointermove_across.step(({assert_true}) => {
assert_true(
eventRecorder.checkRecords(expected),
'Expected events to occur in the correct order',
);
});
pointermove_across.done();
}
},
[eventRecorder, pointermove_across],
);

const eventProps = eventRecorder.useRecorderTestEventHandlers(
targetNames,
eventHandler,
);

return (
<>
<View {...eventProps.c} style={styles.c}>
<View {...eventProps.b} style={styles.b}>
<View {...eventProps.a} style={styles.a} />
</View>
</View>
<View {...eventProps.c1} style={styles.c1}>
<View {...eventProps.b1} style={styles.b1}>
<View {...eventProps.a1} style={styles.a1} />
</View>
</View>
</>
);
}

type Props = $ReadOnly<{}>;
export default function PointerEventPointerMoveAcross(
props: Props,
): React.MixedElement {
return (
<RNTesterPlatformTest
component={PointerEventPointerMoveAcrossTestCase}
description=""
instructions={[
'Move your mouse across the yellow/red <div> element quickly from right to left',
'Move your mouse across the black/blue <div> element quickly from right to left',
'If the test fails, redo it again and move faster on the black/blue <div> element next time.',
]}
title="Pointermove handling across elements"
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import CompatibilityNativeGestureHandling from './Compatibility/CompatibilityNat
import PointerEventPrimaryTouchPointer from './W3CPointerEventPlatformTests/PointerEventPrimaryTouchPointer';
import PointerEventAttributesNoHoverPointers from './W3CPointerEventPlatformTests/PointerEventAttributesNoHoverPointers';
import PointerEventPointerMoveOnChordedMouseButton from './W3CPointerEventPlatformTests/PointerEventPointerMoveOnChordedMouseButton';
import PointerEventPointerMoveAcross from './W3CPointerEventPlatformTests/PointerEventPointerMoveAcross';
import EventfulView from './W3CPointerEventsEventfulView';

function AbsoluteChildExample({log}: {log: string => void}) {
Expand Down Expand Up @@ -202,6 +203,14 @@ export default {
return <PointerEventPointerMoveOnChordedMouseButton />;
},
},
{
name: 'pointerevent_pointermove_across',
description: '',
title: 'Pointermove handling across elements',
render(): React.Node {
return <PointerEventPointerMoveAcross />;
},
},
CompatibilityAnimatedPointerMove,
CompatibilityNativeGestureHandling,
],
Expand Down

0 comments on commit 93b51b5

Please sign in to comment.