-
Notifications
You must be signed in to change notification settings - Fork 24k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
7be829f
commit 93b51b5
Showing
3 changed files
with
348 additions
and
0 deletions.
There are no files selected for viewing
173 changes: 173 additions & 0 deletions
173
...ages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestEventRecorder.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
166 changes: 166 additions & 0 deletions
166
...er/js/examples/Experimental/W3CPointerEventPlatformTests/PointerEventPointerMoveAcross.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters