Add basic platform testing framework for RNTester

Summary: Changelog: [RNTester][Internal] - Add experimental platform testing framework for RNTester

Reviewed By: lunaleaps, kacieb

Differential Revision: D36633708

fbshipit-source-id: 0f2f7642bc6db31e148a2a214c17fe39656612fd
This commit is contained in:
Vincent Riemer
2022-05-24 19:14:11 -07:00
committed by Facebook GitHub Bot
parent 004b8609d9
commit b2aa41578e
8 changed files with 1016 additions and 0 deletions
@@ -0,0 +1,74 @@
/**
* 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 './RNTesterPlatformTestTypes';
import * as React from 'react';
import {StyleSheet, View, Text, ScrollView} from 'react-native';
import RNTesterPlatformTestInstructions from './RNTesterPlatformTestInstructions';
import usePlatformTestHarness from './usePlatformTestHarness';
import RNTesterPlatformTestResultView from './RNTesterPlatformTestResultView';
type Props = $ReadOnly<{|
title: string,
description: string,
instructions?: $ReadOnlyArray<string>,
component: React.ComponentType<PlatformTestComponentBaseProps>,
|}>;
export default function RNTesterPlatformTest(props: Props): React.MixedElement {
const {
title,
description,
instructions,
component: UnderTestComponent,
} = props;
const {harness, reset, results, testKey} = usePlatformTestHarness();
return (
<ScrollView style={styles.root}>
<Text style={[styles.textBlock, styles.title]}>{title}</Text>
<Text style={[styles.textBlock, styles.description]}>{description}</Text>
<RNTesterPlatformTestInstructions
instructions={instructions}
style={styles.block}
/>
<View style={styles.block}>
<UnderTestComponent key={testKey} harness={harness} />
</View>
<RNTesterPlatformTestResultView
reset={reset}
results={results}
style={styles.block}
/>
</ScrollView>
);
}
const styles = StyleSheet.create({
block: {
marginBottom: 8,
},
description: {
fontSize: 16,
},
textBlock: {
marginBottom: 8,
},
root: {
padding: 8,
},
title: {
fontSize: 32,
fontWeight: '700',
},
});
@@ -0,0 +1,44 @@
/**
* 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 * as React from 'react';
import {View, Text, StyleSheet} from 'react-native';
import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet';
type Props = $ReadOnly<{|
instructions?: $ReadOnlyArray<string>,
style?: ?ViewStyleProp,
|}>;
export default function RNTesterPlatformTestInstructions({
instructions,
style,
}: Props): React.MixedElement | null {
if (instructions == null) {
return null;
}
return (
<View style={style}>
{instructions.map((instruction, idx) => {
return (
<Text key={idx} style={styles.instructionText}>
{idx + 1}. {instruction}
</Text>
);
})}
</View>
);
}
const styles = StyleSheet.create({
instructionText: {
fontSize: 16,
},
});
@@ -0,0 +1,184 @@
/**
* 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 {
ViewStyleProp,
TextStyle,
} from 'react-native/Libraries/StyleSheet/StyleSheet';
import type {
PlatformTestResult,
PlatformTestResultStatus,
} from './RNTesterPlatformTestTypes';
import * as React from 'react';
import {useMemo} from 'react';
import {Button, View, Text, StyleSheet} from 'react-native';
const DISPLAY_STATUS_MAPPING: {[PlatformTestResultStatus]: string} = {
PASS: 'Pass',
FAIL: 'Fail',
ERROR: 'Error',
};
type Props = $ReadOnly<{|
reset: () => void,
results: $ReadOnlyArray<PlatformTestResult>,
style?: ?ViewStyleProp,
|}>;
export default function RNTesterPlatformTestResultView(
props: Props,
): React.MixedElement {
const {reset, results, style} = props;
const {numPass, numFail, numError} = useMemo(
() =>
results.reduce(
(acc, result) => {
switch (result.status) {
case 'PASS':
return {...acc, numPass: acc.numPass + 1};
case 'FAIL':
return {...acc, numFail: acc.numFail + 1};
case 'ERROR':
return {...acc, numError: acc.numError + 1};
}
},
{
numPass: 0,
numFail: 0,
numError: 0,
},
),
[results],
);
return (
<View style={style}>
<View style={styles.titleContainer}>
<Text style={styles.title}>Results</Text>
<Button title="Reset" onPress={reset} />
</View>
<Text style={styles.summaryContainer}>
<Text>
{numPass} <Text style={styles.passText}>Pass</Text>
</Text>
{' '}
<Text>
{numFail} <Text style={styles.failText}>Fail</Text>
</Text>
{' '}
<Text>
{numError} <Text style={styles.errorText}>Error</Text>
</Text>
</Text>
<View style={styles.table}>
{/* Table Heading Row */}
<View style={styles.tableRow}>
<View style={[styles.tableHeaderColumn, styles.tableResultColumn]}>
<Text style={styles.tableHeader}>Result</Text>
</View>
<View style={[styles.tableHeaderColumn, styles.tableTestNameColumn]}>
<Text style={styles.tableHeader}>Test Name</Text>
</View>
<View style={[styles.tableHeaderColumn, styles.tableMessageColumn]}>
<Text style={styles.tableHeader}>Message</Text>
</View>
</View>
{/* Table Contents */}
{results.map((testResult, resultIdx) => {
return (
<View key={resultIdx} style={styles.tableRow}>
<View style={styles.tableResultColumn}>
<Text style={STATUS_TEXT_STYLE_MAPPING[testResult.status]}>
{DISPLAY_STATUS_MAPPING[testResult.status]}
</Text>
</View>
<View style={styles.tableTestNameColumn}>
<Text>{testResult.name}</Text>
</View>
<View style={styles.tableMessageColumn}>
{testResult.assertions.map((assertion, assertionIdx) => {
if (assertion.passing) {
return null;
}
return (
<Text key={assertionIdx}>
{assertion.name}: {assertion.description}{' '}
{assertion.failureMessage}
</Text>
);
})}
</View>
</View>
);
})}
</View>
</View>
);
}
const styles = StyleSheet.create({
errorText: {
color: 'orange',
},
failText: {
color: 'red',
},
passText: {
color: 'green',
},
table: {},
tableHeader: {
fontSize: 16,
fontWeight: '700',
},
tableHeaderColumn: {
alignItems: 'center',
},
tableMessageColumn: {
flex: 2.5,
justifyContent: 'center',
},
tableRow: {
flexDirection: 'row',
borderBottomWidth: StyleSheet.hairlineWidth,
paddingVertical: 8,
},
tableResultColumn: {
flex: 0.5,
justifyContent: 'center',
},
tableTestNameColumn: {
flex: 2,
justifyContent: 'center',
},
summaryContainer: {
flexDirection: 'row',
},
title: {
fontSize: 32,
fontWeight: '700',
marginBottom: 8,
},
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
});
const STATUS_TEXT_STYLE_MAPPING: {[PlatformTestResultStatus]: TextStyle} = {
PASS: styles.passText,
FAIL: styles.failText,
ERROR: styles.errorText,
};
@@ -0,0 +1,56 @@
/**
* 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
*/
type BasePlatformTestAssertionResult = $ReadOnly<{|
name: string,
description: string,
|}>;
export type PassingPlatformTestAssertionResult = $ReadOnly<{|
...BasePlatformTestAssertionResult,
passing: true,
|}>;
export type FailingPlatformTestAssertionResult = $ReadOnly<{|
...BasePlatformTestAssertionResult,
passing: false,
failureMessage: string,
|}>;
export type PlatformTestAssertionResult =
| PassingPlatformTestAssertionResult
| FailingPlatformTestAssertionResult;
export type PlatformTestResultStatus = 'PASS' | 'FAIL' | 'ERROR';
export type PlatformTestResult = $ReadOnly<{|
name: string,
status: PlatformTestResultStatus,
assertions: $ReadOnlyArray<PlatformTestAssertionResult>,
error: mixed | null, // null is technically unecessary but is kept to ensure the error is described as nullable
|}>;
export type PlatformTestContext = $ReadOnly<{
assert_true(a: boolean, description: string): void,
assert_equals(a: any, b: any, description: string): void,
assert_greater_than_equal(a: number, b: number, description: string): void,
assert_less_than_equal(a: number, b: number, description: string): void,
}>;
export type PlatformTestCase = (context: PlatformTestContext) => void;
export type PlatformTestHarness = $ReadOnly<{|
test(testcase: PlatformTestCase, name: string): void,
|}>;
export type PlatformTestComponentBaseProps = {
+harness: PlatformTestHarness,
...
};
@@ -0,0 +1,144 @@
/**
* 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 {useState, useCallback, useMemo} from 'react';
import type {
PlatformTestResult,
PlatformTestHarness,
PlatformTestCase,
PlatformTestAssertionResult,
PlatformTestContext,
} from './RNTesterPlatformTestTypes';
function didAllAssertionsPass(
assertions: Array<PlatformTestAssertionResult>,
): boolean {
const hasFailingAssertion = assertions.some(assertion => !assertion.passing);
return !hasFailingAssertion;
}
export type PlatformTestHarnessHookResult = $ReadOnly<{|
testKey: number,
harness: PlatformTestHarness,
reset: () => void,
results: $ReadOnlyArray<PlatformTestResult>,
|}>;
export default function usePlatformTestHarness(): PlatformTestHarnessHookResult {
const [testResults, updateTestResults] = useState<
$ReadOnlyArray<PlatformTestResult>,
>([]);
// When reseting the test results we should also re-mount the
// so we apply a key to that component which we can increment
// to ensure it re-mounts
const [testElementKey, setTestElementKey] = useState<number>(0);
const reset = useCallback(() => {
updateTestResults([]);
setTestElementKey(k => k + 1);
}, []);
const addTestResult = useCallback((newResult: PlatformTestResult) => {
updateTestResults(prev => [...prev, newResult]);
}, []);
const testFunction: PlatformTestHarness['test'] = useCallback(
(testCase: PlatformTestCase, name: string): void => {
const assertionResults: Array<PlatformTestAssertionResult> = [];
const baseAssert = (
assertionName: string,
testConditionResult: boolean,
description: string,
failureMessage: string,
) => {
if (testConditionResult) {
assertionResults.push({
passing: true,
name: assertionName,
description,
});
} else {
assertionResults.push({
passing: false,
name: assertionName,
description,
failureMessage,
});
}
};
const context: PlatformTestContext = {
assert_true: (cond: boolean, desc: string) =>
baseAssert(
'assert_true',
cond,
desc,
"expected 'true' but recieved 'false'",
),
assert_equals: (a: any, b: any, desc: string) =>
baseAssert(
'assert_equal',
a === b,
desc,
`expected ${a} to equal ${b}`,
),
assert_greater_than_equal: (a: number, b: number, desc: string) =>
baseAssert(
'assert_greater_than_equal',
a >= b,
desc,
`expected ${a} to be greater than or equal to ${b}`,
),
assert_less_than_equal: (a: number, b: number, desc: string) =>
baseAssert(
'assert_less_than_equal',
a <= b,
desc,
`expected ${a} to be less than or equal to ${b}`,
),
};
try {
testCase(context);
addTestResult({
name,
status: didAllAssertionsPass(assertionResults) ? 'PASS' : 'FAIL',
assertions: assertionResults,
error: null,
});
} catch (error) {
addTestResult({
name,
status: 'ERROR',
assertions: assertionResults,
error,
});
}
},
[addTestResult],
);
const harness: PlatformTestHarness = useMemo(
() => ({
test: testFunction,
}),
[testFunction],
);
return {
harness,
reset,
results: testResults,
testKey: testElementKey,
};
}
@@ -0,0 +1,302 @@
/**
* 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 {
Layout,
PointerEvent,
} from 'react-native/Libraries/Types/CoreEventTypes';
import type {PlatformTestComponentBaseProps} from '../PlatformTest/RNTesterPlatformTestTypes';
import * as React from 'react';
import {useCallback, useRef, useState} from 'react';
import {View, StyleSheet} from 'react-native';
import RNTesterPlatformTest from '../PlatformTest/RNTesterPlatformTest';
import {check_PointerEvent, useTestEventHandler} from './PointerEventSupport';
const UNINITIALIZED_LAYOUT: Layout = {
x: NaN,
y: NaN,
width: NaN,
height: NaN,
};
// TODO: remove number suffixes
const eventList = [
'pointerOver',
'pointerEnter2',
'pointerMove2',
'pointerDown',
'pointerUp',
'pointerOut',
'pointerLeave2',
];
function PointerEventAttributesHoverablePointersTestCase(
props: PlatformTestComponentBaseProps,
) {
const {harness} = props;
const detected_pointertypesRef = useRef({});
const detected_eventTypesRef = useRef({});
const expectedPointerIdRef = useRef(NaN);
const rectSquare1Ref = useRef<Layout>({...UNINITIALIZED_LAYOUT});
const rectSquare2Ref = useRef<Layout>({...UNINITIALIZED_LAYOUT});
const [square1Visible, setSquare1Visible] = useState(true);
const [square2Visible, setSquare2Visible] = useState(false);
// Adapted from https://github.com/web-platform-tests/wpt/blob/6c26371ea1c144dd612864a278e88b6ba2f3d883/pointerevents/pointerevent_attributes_hoverable_pointers.html#L29
const checkPointerEventAttributes = useCallback(
(
event: PointerEvent,
eventType: string,
targetLayout: Layout,
testNamePrefix: string,
expectedPointerType: string,
) => {
const detected_pointertypes = detected_pointertypesRef.current;
const detected_eventTypes = detected_eventTypesRef.current;
const expectedPointerId = expectedPointerIdRef.current;
if (detected_eventTypes[eventType]) {
return;
}
const expectedEventType =
eventList[Object.keys(detected_eventTypes).length].toLowerCase();
detected_eventTypes[eventType] = true;
const pointerTestName =
testNamePrefix + ' ' + expectedPointerType + ' ' + expectedEventType;
detected_pointertypes[event.nativeEvent.pointerType] = true;
harness.test(({assert_equals}) => {
assert_equals(
eventType,
expectedEventType,
'Event.type should be ' + expectedEventType,
);
}, pointerTestName + "'s type should be " + expectedEventType);
// Test button and buttons
if (eventType === 'pointerdown') {
harness.test(({assert_equals}) => {
assert_equals(event.nativeEvent.button, 0, 'Button attribute is 0');
}, pointerTestName + "'s button attribute is 0 when left mouse button is pressed.");
harness.test(({assert_equals}) => {
assert_equals(event.nativeEvent.buttons, 1, 'Buttons attribute is 1');
}, pointerTestName + "'s buttons attribute is 1 when left mouse button is pressed.");
} else if (eventType === 'pointerup') {
harness.test(({assert_equals}) => {
assert_equals(event.nativeEvent.button, 0, 'Button attribute is 0');
}, pointerTestName + "'s button attribute is 0 when left mouse button is just released.");
harness.test(({assert_equals}) => {
assert_equals(event.nativeEvent.buttons, 0, 'Buttons attribute is 0');
}, pointerTestName + "'s buttons attribute is 0 when left mouse button is just released.");
} else {
harness.test(({assert_equals}) => {
assert_equals(event.nativeEvent.button, -1, 'Button attribute is -1');
}, pointerTestName + "'s button is -1 when mouse buttons are in released state.");
harness.test(({assert_equals}) => {
assert_equals(event.nativeEvent.buttons, 0, 'Buttons attribute is 0');
}, pointerTestName + "'s buttons is 0 when mouse buttons are in released state.");
}
const left = targetLayout.x;
const top = targetLayout.y;
const right = targetLayout.x + targetLayout.width;
const bottom = targetLayout.y + targetLayout.height;
// Test clientX and clientY
if (eventType !== 'pointerout' && eventType !== 'pointerleave') {
harness.test(({assert_greater_than_equal, assert_less_than_equal}) => {
assert_greater_than_equal(
event.nativeEvent.clientX,
left,
'clientX should be greater or equal than left of the box',
);
assert_greater_than_equal(
event.nativeEvent.clientY,
top,
'clientY should be greater or equal than top of the box',
);
assert_less_than_equal(
event.nativeEvent.clientX,
right,
'clientX should be less or equal than right of the box',
);
assert_less_than_equal(
event.nativeEvent.clientY,
bottom,
'clientY should be less or equal than bottom of the box',
);
}, pointerTestName + "'s ClientX and ClientY attributes are correct.");
} else {
harness.test(({assert_true}) => {
assert_true(
event.nativeEvent.clientX < left ||
event.nativeEvent.clientX >= right ||
event.nativeEvent.clientY < top ||
event.nativeEvent.clientY >= bottom,
'ClientX/Y should be out of the boundaries of the box',
);
}, pointerTestName + "'s ClientX and ClientY attributes are correct.");
}
// TODO: check_PointerEvent
check_PointerEvent(harness, event, eventType, {
testNamePrefix,
});
// Test isPrimary value
harness.test(({assert_equals}) => {
assert_equals(
event.nativeEvent.isPrimary,
true,
'isPrimary should be true',
);
}, pointerTestName + '.isPrimary attribute is correct.');
// Test pointerId value
if (isNaN(expectedPointerId)) {
expectedPointerIdRef.current = event.nativeEvent.pointerId;
} else {
harness.test(({assert_equals}) => {
assert_equals(
event.nativeEvent.pointerId,
expectedPointerId,
'pointerId should remain the same for the same active pointer',
);
}, pointerTestName + '.pointerId should be the same as previous pointer events for this active pointer.');
}
},
[harness],
);
const square1Handlers = useTestEventHandler(eventList, (event, eventType) => {
if (!square1Visible) {
return;
}
checkPointerEventAttributes(
event,
eventType,
rectSquare1Ref.current,
'',
'mouse',
);
if (
Object.keys(detected_eventTypesRef.current).length === eventList.length
) {
setSquare1Visible(false);
detected_eventTypesRef.current = {};
setSquare2Visible(true);
expectedPointerIdRef.current = NaN;
}
});
const square2Handlers = useTestEventHandler(eventList, (event, eventType) => {
checkPointerEventAttributes(
event,
eventType,
rectSquare2Ref.current,
'Inner frame ',
'mouse',
);
if (
Object.keys(detected_eventTypesRef.current).length === eventList.length
) {
setSquare2Visible(false);
// TODO: Mark test as done
}
});
const updateSquare1Layout = useCallback(evt => {
const elem = evt.target;
if (typeof elem !== 'number' && elem != null) {
elem.measureInWindow((x, y, width, height) => {
rectSquare1Ref.current = {x, y, width, height};
});
}
}, []);
const updateSquare2Layout = useCallback(evt => {
const elem = evt.target;
if (typeof elem !== 'number' && elem != null) {
elem.measureInWindow((x, y, width, height) => {
rectSquare2Ref.current = {x, y, width, height};
});
}
}, []);
return (
<View style={styles.root}>
<View style={styles.squareContainer}>
{square1Visible && (
<View
onLayout={updateSquare1Layout}
style={styles.square1}
{...square1Handlers}
/>
)}
</View>
<View style={styles.squareContainer}>
{square2Visible && (
<View
onLayout={updateSquare2Layout}
style={styles.square2}
{...square2Handlers}
/>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
root: {
height: 150,
flexDirection: 'row',
alignItems: 'center',
},
square1: {
width: 40,
height: 40,
backgroundColor: 'black',
},
square2: {
width: 40,
height: 40,
backgroundColor: 'red',
},
squareContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
type Props = $ReadOnly<{}>;
export default function PointerEventAttributesHoverablePointers(
props: Props,
): React.MixedElement {
return (
<RNTesterPlatformTest
component={PointerEventAttributesHoverablePointersTestCase}
description="This test checks the properties of hoverable pointer events. If you are using hoverable pen don't leave the range of digitizer while doing the instructions."
instructions={[
'Move your pointer over the black square and click on it.',
'Then move it off the black square so that it disappears.',
'When red square appears move your pointer over the red square and click on it.',
'Then move it off the red square.',
]}
title="Pointer Events hoverable pointer attributes test"
/>
);
}
@@ -0,0 +1,202 @@
/**
* 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 {PlatformTestHarness} from '../PlatformTest/RNTesterPlatformTestTypes';
import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes';
import type {PointerEvent} from 'react-native/Libraries/Types/CoreEventTypes';
import {useMemo} from 'react';
// Check for conformance to PointerEvent interface
// TA: 1.1, 1.2, 1.6, 1.7, 1.8, 1.9, 1.10, 1.11, 1.12, 1.13
// Adapted from https://github.com/web-platform-tests/wpt/blob/6c26371ea1c144dd612864a278e88b6ba2f3d883/pointerevents/pointerevent_support.js#L15
export function check_PointerEvent(
harness: PlatformTestHarness,
event: PointerEvent,
eventType: string,
{
expectedPointerType,
testNamePrefix,
}: {expectedPointerType?: string, testNamePrefix?: string},
) {
const {nativeEvent} = event;
if (testNamePrefix == null) {
testNamePrefix = '';
}
// Use expectedPointerType if set otherwise just use the incoming event pointerType in the test name.
var pointerTestName =
testNamePrefix +
' ' +
(expectedPointerType == null
? nativeEvent.pointerType
: expectedPointerType) +
' ' +
eventType;
if (expectedPointerType != null) {
harness.test(({assert_equals}) => {
assert_equals(
nativeEvent.pointerType,
expectedPointerType,
'pointerType should be the one specified in the test page.',
);
}, pointerTestName + ' event pointerType is correct.');
}
// TODO: Ensure event is a pointer event
// Check attributes for conformance to WebIDL:
// * attribute exists
// * has proper type
// * if the attribute is "readonly", it cannot be changed
// TA: 1.1, 1.2
const idl_type_check = {
long: function (v) {
return typeof v === 'number' && Math.round(v) === v;
},
float: function (v) {
return typeof v === 'number';
},
string: function (v) {
return typeof v === 'string';
},
boolean: function (v) {
return typeof v === 'boolean';
},
object: function (v) {
return typeof v === 'object';
},
};
[
['readonly', 'long', 'pointerId'],
['readonly', 'float', 'width'],
['readonly', 'float', 'height'],
['readonly', 'float', 'pressure'],
['readonly', 'long', 'tiltX'],
['readonly', 'long', 'tiltY'],
['readonly', 'string', 'pointerType'],
['readonly', 'boolean', 'isPrimary'],
['readonly', 'long', 'detail', 0],
['readonly', 'object', 'fromElement', null],
['readonly', 'object', 'toElement', null],
].forEach(attr => {
// const readonly = attr[0];
const type = attr[1];
const name = attr[2];
const value = attr[3];
// existence check
harness.test(({assert_true}) => {
assert_true(
name in nativeEvent,
name + ' attribute in ' + eventType + ' event',
);
}, pointerTestName + '.' + name + ' attribute exists');
// readonly check
// TODO
// type check
harness.test(({assert_true}) => {
assert_true(
// $FlowFixMe
idl_type_check[type](nativeEvent[name]),
name + ' attribute of type ' + type,
);
// $FlowFixMe
}, pointerTestName + '.' + name + ' IDL type ' + type + ' (JS type was ' + typeof nativeEvent[name] + ')');
// value check if defined
if (value !== undefined) {
harness.test(({assert_equals}) => {
// $FlowFixMe
assert_equals(nativeEvent[name], value, name + ' attribute value');
}, pointerTestName + '.' + name + ' value is ' + String(value) + '.');
}
});
// Check the pressure value
// TA: 1.6, 1.7, 1.8
harness.test(
({assert_greater_than_equal, assert_less_than_equal, assert_equals}) => {
// TA: 1.6
assert_greater_than_equal(
nativeEvent.pressure,
0,
'pressure is greater than or equal to 0',
);
assert_less_than_equal(
nativeEvent.pressure,
1,
'pressure is less than or equal to 1',
);
if (nativeEvent.buttons === 0) {
assert_equals(
nativeEvent.pressure,
0,
'pressure is 0 for mouse with no buttons pressed',
);
}
// TA: 1.7, 1.8
if (nativeEvent.pointerType === 'mouse') {
if (nativeEvent.buttons !== 0) {
assert_equals(
nativeEvent.pressure,
0.5,
'pressure is 0.5 for mouse with a button pressed',
);
}
}
},
pointerTestName + '.pressure value is valid',
);
// Check mouse-specific properties
if (nativeEvent.pointerType === 'mouse') {
// TA: 1.9, 1.10, 1.13
harness.test(({assert_equals, assert_true}) => {
assert_equals(nativeEvent.width, 1, 'width of mouse should be 1');
assert_equals(nativeEvent.height, 1, 'height of mouse should be 1');
assert_equals(nativeEvent.tiltX, 0, eventType + '.tiltX is 0 for mouse');
assert_equals(nativeEvent.tiltY, 0, eventType + '.tiltY is 0 for mouse');
assert_true(
nativeEvent.isPrimary,
eventType + '.isPrimary is true for mouse',
);
}, pointerTestName + ' properties for pointerType = mouse');
// Check properties for pointers other than mouse
}
}
/**
* Helper hook to allow you to easily listen to multiple
* view events with the same handler
*/
export function useTestEventHandler(
eventNames: $ReadOnlyArray<string>,
handler: (event: any, eventName: string) => void,
): ViewProps {
const eventProps: any = useMemo(() => {
const handlerFactory = eventName => event => handler(event, eventName);
const props = {};
for (const eventName of eventNames) {
const eventPropName =
'on' + eventName[0].toUpperCase() + eventName.slice(1);
props[eventPropName] = handlerFactory(eventName.toLowerCase());
}
return props;
}, [eventNames, handler]);
return eventProps;
}
@@ -12,6 +12,8 @@ import {Button, StyleSheet, ScrollView, View, Text} from 'react-native';
import * as React from 'react';
import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes';
import PointerEventAttributesHoverablePointers from './W3CPointerEventPlatformTests/PointerEventAttributesHoverablePointers';
function EventfulView(props: {|
name: string,
emitByDefault?: boolean,
@@ -223,5 +225,13 @@ export default {
return <PointerEventScaffolding Example={AbsoluteChildExample} />;
},
},
{
name: 'pointerevent_attributes_hoverable_pointers',
description: '',
title: 'Pointer Events hoverable pointer attributes test',
render(): React.Node {
return <PointerEventAttributesHoverablePointers />;
},
},
],
};