mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
004b8609d9
commit
b2aa41578e
@@ -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',
|
||||
},
|
||||
});
|
||||
+44
@@ -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,
|
||||
},
|
||||
});
|
||||
+184
@@ -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,
|
||||
};
|
||||
}
|
||||
+302
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
+202
@@ -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 />;
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user