如何用fireGestureHandler测试React Native评分组件的平移手势?
可滑动评分组件的手势测试问题及解决方法
问题背景
我开发了一个支持滑动选择的星级评分组件,核心逻辑依赖react-native-gesture-handler的Pan手势实现滑动评分。但在测试时,使用fireGestureHandler无法触发组件的平移手势逻辑,导致手势相关代码覆盖率低。
组件核心代码
import FontistiIcon from '@expo/vector-icons/Fontisto'; import React, { useCallback, useMemo } from 'react'; import { LayoutRectangle, View } from 'react-native'; import { Gesture, GestureDetector, TouchableOpacity, } from 'react-native-gesture-handler'; import { useTheme } from '@theme'; import { roundToStep } from '@utils'; import { useStyle } from './numerical-rating.styles'; import { RatingProps } from './numerical-rating.type'; const NumericalRating: React.FC<RatingProps> = props => { const { scale = 5, testID = 'TestID__component-NumericalRating', starTestID = 'TestID__component-NumericalRating-star', isFractional = false, onChange, value, } = props; const styles = useStyle(); const theme = useTheme(); const [ratingContainerLayout, setRatingContainerLayout] = React.useState<LayoutRectangle | null>(null); const scaleRounded = Math.round(scale); const scaleList = Array.from(Array(scaleRounded).keys(), x => x + 1); const fractionalScaleList = scaleList.reduce<number[][]>( (acc, curr) => [...acc, [curr - 0.5, curr]], [], ); const iconSize = theme.t.moderateScale(58); const iconWidth = Math.floor(iconSize / 2); const selectedRating = Math.max(0, Math.min(value, scale)); const [draggedRating, setDraggedRating] = React.useState(selectedRating); const onRatingTap = (rating: number) => () => { onChange(rating); setDraggedRating(rating); }; const getRatingByPosition = useCallback( (position: number) => { if (ratingContainerLayout?.width) { const ratingIconSize = ratingContainerLayout.width / scaleRounded; const calculatedRatingValue = position / ratingIconSize; return roundToStep(calculatedRatingValue, isFractional ? 0.5 : 1); } return 0; }, [isFractional, ratingContainerLayout, scaleRounded], ); const gesture = useMemo( () => Gesture.Pan() .runOnJS(true) .onUpdate(e => { setDraggedRating(getRatingByPosition(e.x)); }) .onEnd(e => { onChange(getRatingByPosition(e.x)); }), [getRatingByPosition, onChange], ); const renderScale = () => scaleList.map(rating => ( <TouchableOpacity key={rating} onPress={onRatingTap(rating)} testID={starTestID} accessibilityState={{ selected: draggedRating >= rating, }} > <View style={styles.scale}> <FontistiIcon name="star" size={iconSize} color={ draggedRating >= rating ? theme.t.palette.accents.color2 : theme.t.palette.accents.color4 } /> </View> </TouchableOpacity> )); const renderFractionalScale = () => fractionalScaleList.map((ratingPair: number[], index) => ( <View style={[styles.ratingPair, styles.scale]} key={index}> {ratingPair.map((rating: number) => ( <View key={rating} style={rating % 1 === 0 ? styles.rightHalf : {}}> <TouchableOpacity onPress={onRatingTap(rating)} testID={starTestID} accessibilityState={{ selected: draggedRating >= rating, }} > <FontistiIcon name="star-half" size={iconSize} style={{ width: iconWidth, }} color={ draggedRating >= rating ? theme.t.palette.accents.color2 : theme.t.palette.accents.color4 } /> </TouchableOpacity> </View> ))} </View> )); const renderRating = () => { if (isFractional) { return renderFractionalScale(); } return renderScale(); }; return ( <GestureDetector gesture={gesture}> <View> <View style={styles.container} testID={testID} onLayout={e => setRatingContainerLayout(e.nativeEvent.layout)} > {renderRating()} </View> </View> </GestureDetector> ); }; export default NumericalRating;
测试代码(无法触发手势)
it('should select the correct star on swipe', () => { const onChange = jest.fn(); render(<NumericalRating value={2} onChange={onChange} />); const starContainer = screen.getByTestId( 'TestID__component-NumericalRating', ); fireGestureHandler<PanGesture>(starContainer, [ { x: 5, y: 15 }, { x: 6, y: 16 }, { x: 7, y: 17 }, ]); expect(onChange).toHaveBeenCalledWith(1); });
解决方法
1. 模拟容器布局宽度
组件中getRatingByPosition依赖ratingContainerLayout的宽度计算评分,测试时容器的onLayout事件不会自动触发,导致ratingContainerLayout为null,直接返回0。需要手动模拟布局信息:
// 触发onLayout事件,设置容器宽度(对应5个星星的总宽度) fireEvent(starContainer, 'layout', { nativeEvent: { layout: { width: 290, height: 58, x: 0, y: 0 } } });
2. 正确构造手势事件参数
fireGestureHandler需要指定手势的阶段(BEGIN/UPDATE/END),并且事件参数需要符合Pan手势的结构。修改测试代码如下:
import { PanGesture } from 'react-native-gesture-handler'; it('should select the correct star on swipe', () => { const onChange = jest.fn(); render(<NumericalRating value={2} onChange={onChange} />); const starContainer = screen.getByTestId('TestID__component-NumericalRating'); // 先触发布局事件,设置容器宽度 fireEvent(starContainer, 'layout', { nativeEvent: { layout: { width: 290, height: 58, x: 0, y: 0 } } }); // 构造完整的手势事件流程:BEGIN -> UPDATE -> END fireGestureHandler<PanGesture>(starContainer, [ { state: 'BEGAN', x: 0, y: 0 }, { state: 'ACTIVE', x: 58, y: 0 }, // 滑动到第一个星星的位置(290/5=58) { state: 'END', x: 58, y: 0 } ]); expect(onChange).toHaveBeenCalledWith(1); });
3. 配置GestureHandler测试环境
在测试文件顶部添加以下配置,确保react-native-gesture-handler的测试工具正常工作:
import 'react-native-gesture-handler/jestSetup'; jest.mock('react-native-gesture-handler', () => { const ActualGestureHandler = jest.requireActual('react-native-gesture-handler'); return { ...ActualGestureHandler, GestureDetector: jest.fn(({ children }) => children), Gesture: { Pan: jest.fn(() => ({ runOnJS: jest.fn().mockReturnThis(), onUpdate: jest.fn().mockReturnThis(), onEnd: jest.fn().mockReturnThis(), })), }, }; });
4. 优化组件容错处理
为避免ratingContainerLayout为null时逻辑失效,可添加默认布局值:
// 在组件中初始化默认布局 const [ratingContainerLayout, setRatingContainerLayout] = React.useState<LayoutRectangle>({ width: theme.t.moderateScale(58) * 5, height: theme.t.moderateScale(58), x: 0, y: 0 });
提升覆盖率的额外技巧
- 单独测试
getRatingByPosition函数:传入不同的position和width参数,验证返回评分的正确性 - 分别测试手势的
onUpdate和onEnd阶段逻辑 - 测试 fractional 和 non-fractional 两种模式下的滑动行为
内容的提问来源于stack exchange,提问作者shet_tayyy
相关产品推荐
相关产品推荐

