* Copyright (c) 2025 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
type ReactElement,
type ReactNode,
FC,
useContext,
Children,
useState,
useEffect,
isValidElement,
} from 'react';
import {StyleSheet, Text, View, Button} from 'react-native';
import {TestSuiteContext} from './TestingContext';
import {TestingContext} from './TestingContext';
import {TestCaseResultType} from '../core';
import {getTestCaseTypeFromProps} from './TestCase';
type TestCaseElement = ReactElement<Record<string, any>>;
const isTestCaseElement = (
child: ReactNode,
): child is ReactElement<Record<string, any>> =>
isValidElement(child) &&
typeof child.props === 'object' &&
child.props !== null;
function shouldChangeCurrentChild(
result: TestCaseResultType,
child: TestCaseElement | undefined,
): boolean {
if (!child) {
return false;
}
if (!getTestCaseTypeFromProps(child.props)) {
return false;
}
if (
result !== 'skipped' &&
result !== 'pass' &&
result !== 'fail' &&
result !== 'broken'
) {
return false;
}
return true;
}
export const TestSuite: FC<{name: string; children: ReactNode}> = ({
name,
children,
}) => {
const [currentChildIndex, setCurrentChildIndex] = useState(0);
const childElements = Children.toArray(children).filter(isTestCaseElement);
const testingContext = useContext(TestingContext)!;
const testSuiteContext = useContext(TestSuiteContext);
const parentTestSuiteId = testSuiteContext?.testSuiteId;
const depth = testSuiteContext?.depth ?? 0;
const [showNextTestButton, setShowNextTestButton] = useState(false);
const pauseOnFailure =
typeof testingContext?.isSequential === 'object' &&
testingContext.isSequential.pauseOnFailure;
const filteredChildren = childElements.filter(child => {
const testCaseType = getTestCaseTypeFromProps(child.props);
if (testCaseType === null) {
return true;
}
const shouldRender = testingContext.filter({
testCaseType,
tags: child.props.tags ?? [],
});
return shouldRender;
});
const currentChild = filteredChildren[currentChildIndex];
const changeRenderedTestsIfSequential = () => {
if (!testingContext.isSequential) {
return;
}
if (currentChildIndex === filteredChildren.length - 1) {
testingContext?.onTestSuiteComplete();
} else {
setShowNextTestButton(false);
setCurrentChildIndex(currentChildIndex + 1);
}
};
const shouldPause = (result: TestCaseResultType) => {
return pauseOnFailure && result !== 'skipped' && result !== 'pass';
};
useEffect(() => {
if (testingContext.isSequential && filteredChildren.length === 0) {
testingContext.onTestSuiteComplete();
}
if (
currentChild &&
getTestCaseTypeFromProps(currentChild.props) === 'example'
) {
setShowNextTestButton(true);
}
}, [currentChildIndex]);
if (!testingContext) {
return null;
}
return (
<TestSuiteContext.Provider
value={{
testSuiteName: name,
testSuiteId: `${
parentTestSuiteId ? `${parentTestSuiteId}::` : ''
}${name}`,
depth: depth + 1,
onTestCaseIgnored: () => {
changeRenderedTestsIfSequential();
},
}}>
<View style={styles.testSuiteContainer}>
<Text
style={[
styles.testSuiteHeader,
{
fontSize: depth > 0 ? 12 : 16,
},
]}>
{name}
</Text>
<TestingContext.Provider
value={{
...testingContext,
reportTestCaseResult: (testCaseId, result) => {
const shouldChange = shouldChangeCurrentChild(
result,
currentChild,
);
if (shouldChange && shouldPause(result)) {
setShowNextTestButton(true);
} else if (shouldChange) {
changeRenderedTestsIfSequential();
}
testingContext?.reportTestCaseResult(testCaseId, result);
},
onTestSuiteComplete: () => {
changeRenderedTestsIfSequential();
},
}}>
{testingContext.isSequential && (
<>
{currentChild}
{showNextTestButton && (
<Button
title="Next Test"
onPress={changeRenderedTestsIfSequential}
/>
)}
</>
)}
{!testingContext.isSequential && filteredChildren}
</TestingContext.Provider>
</View>
</TestSuiteContext.Provider>
);
};
const styles = StyleSheet.create({
testSuiteContainer: {
padding: 8,
},
testSuiteHeader: {
width: '100%',
fontWeight: 'bold',
color: '#EEE',
},
});