在 React Native 中测试 Animated(View) 时,如何在 act(...) 中正确包装状态更新?

问题描述 投票:0回答:1

我正在写组件测试代码,我不知道去哪里问,所以我问一个问题。

我正在使用@testing-libray/react-native 和玩笑。

当我按下第一次渲染的背景时。 我编写了一些代码来测试何时按下照片按钮以及何时按下相机按钮。

我无法确认下面的代码是否可以编写,或者是否有错误的编写方式,所以我要求使用一个特定的组件作为示例。

下面的代码写法是否正确?或者有更好的方向吗?

When testing with the current code, 
Warning: An update to Animated(View) inside a test was not wrapped in act(...).
      
When testing, code that causes React state updates should be wrapped into act(...):
      
      act(() => {
        /* fire events that update state */
      });
      /* assert on the output */

出现警告文本。 我需要帮助。

    import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
import Modal from 'react-native-modal';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { lightTheme } from '@/constants/Colors';
import { fontScale, scale, verticalScale } from '@/constants/Metrics';
import dayjs from 'dayjs';

import WheelPicker from './WheelPicker';

/**
 * 예약 시간 설정 모달
 *  - 오늘 예약 시간 설정 시 현재 시간부터 24시간 이내 설정
 *  - 날짜 선택 시 현재 시간부터 24시간 이내 설정
 *  - 시간 선택 시 현재 분부터 59분 이내 설정
 *  - 분 선택 시 현재 분부터 59분 이내 설정
 */

/**
 * 버튼 정의하고 모달형식으로 사용(초기화)관련하여 아래와 같이 정의
 * isVisible && <DateTimePicker isVisible={isVisible} onChangeVisibility={onChangeVisibility} onChange={onChange} />
 */

type DateTimePickerProps = {
    isVisible: boolean;
    onChangeVisibility: (isVisible: boolean) => void;
    onChange: (date: dayjs.Dayjs) => void;
};

const DateTimePicker = ({ isVisible, onChangeVisibility, onChange }: DateTimePickerProps) => {
    const { top } = useSafeAreaInsets();

    const [selectedDateIndex, setSelectedDateIndex] = useState(0);
    const [selectedHourIndex, setSelectedHourIndex] = useState(dayjs().hour());
    const [selectedMinuteIndex, setSelectedMinuteIndex] = useState(Math.floor(dayjs().minute() / 10));

    const dateOptions = ['오늘', dayjs().add(1, 'day').format('M월 D일(dd)')];
    const hourOptions = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
    const minuteOptions = Array.from({ length: 6 }, (_, i) => (i * 10).toString().padStart(2, '0'));

    const closeModal = () => {
        onChangeVisibility(false);
    };

    const getDisabledHours = (dateIndex: number) => {
        const currentHour = dayjs().hour();
        if (dateIndex === 0) {
            return hourOptions.slice(0, currentHour);
        }
        if (dateIndex === 1) {
            return hourOptions.slice((currentHour + 1) % 24, 24);
        }
        return [];
    };

    const getDisabledMinutes = (dateIndex: number, hourIndex: number) => {
        const currentHour = dayjs().hour();
        const currentMinute = dayjs().minute();
        if (dateIndex === 0 && Number(hourOptions[hourIndex]) === currentHour) {
            return minuteOptions.filter(min => Number(min) < currentMinute);
        }
        if (dateIndex === 1) {
            const maxHour = (currentHour + 24) % 24;
            if (Number(hourOptions[hourIndex]) === maxHour) {
                return minuteOptions.filter(min => Number(min) > currentMinute);
            }
        }
        return [];
    };

    const findNextEnabledHourIndex = (dateIndex: number) => {
        const disabledHours = getDisabledHours(dateIndex);
        for (let i = 0; i < hourOptions.length; i++) {
            if (!disabledHours.includes(hourOptions[i])) {
                return i;
            }
        }
        return 0;
    };

    const findNextEnabledMinuteIndex = (dateIndex: number, hourIndex: number) => {
        const disabledMinutes = getDisabledMinutes(dateIndex, hourIndex);
        for (let i = 0; i < minuteOptions.length; i++) {
            if (!disabledMinutes.includes(minuteOptions[i])) {
                return i;
            }
        }
        return 0;
    };

    const findLastEnabledMinuteIndex = (dateIndex: number, hourIndex: number) => {
        const disabledMinutes = getDisabledMinutes(dateIndex, hourIndex);
        for (let i = minuteOptions.length - 1; i >= 0; i--) {
            if (!disabledMinutes.includes(minuteOptions[i])) {
                return i;
            }
        }
        return minuteOptions.length - 1;
    };

    const onChangeDate = (index: number) => {
        setSelectedDateIndex(index);
        if (index === 1) {
            // 다음날 선택 시 현재 시간으로부터 24시간 이내 설정
            const currentHour = dayjs().hour();
            const nextHour = currentHour % 24;
            setSelectedHourIndex(nextHour);
            const lastEnabledMinuteIndex = findLastEnabledMinuteIndex(index, nextHour);
            setSelectedMinuteIndex(lastEnabledMinuteIndex);
        } else {
            const nextEnabledHourIndex = findNextEnabledHourIndex(index);
            setSelectedHourIndex(nextEnabledHourIndex);
            const nextEnabledMinuteIndex = findNextEnabledMinuteIndex(index, nextEnabledHourIndex);
            setSelectedMinuteIndex(nextEnabledMinuteIndex);
        }
    };

    const onChangeHour = (index: number) => {
        setSelectedHourIndex(index);
        const nextEnabledMinuteIndex = findNextEnabledMinuteIndex(selectedDateIndex, index);
        setSelectedMinuteIndex(nextEnabledMinuteIndex);
    };

    const onChangeMinute = (index: number) => {
        setSelectedMinuteIndex(index);
    };

    const onConfirm = () => {
        const selectedDateTime = dayjs().hour(selectedHourIndex).minute(selectedMinuteIndex);
        onChange(selectedDateTime);
        closeModal();
    };

    return (
        <Modal
            testID="date-time-modal"
            statusBarTranslucent
            deviceHeight={Dimensions.get('window').height + top}
            isVisible={isVisible}
            onBackdropPress={closeModal}
            onBackButtonPress={closeModal}
            animationIn="fadeIn"
            animationOut="fadeOut"
            backdropTransitionOutTiming={0}
            backdropTransitionInTiming={0}
        >
            <View testID="date-time-modal-container" style={styles.modalContainer}>
                <Text testID="date-time-picker-title" style={styles.title}>
                    예약시간 설정
                </Text>
                <View style={styles.dateTimeContainer}>
                    <WheelPicker
                        testID="date-time-picker-date"
                        containerStyle={{
                            flex: 1,
                            alignSelf: 'flex-end',
                            justifyContent: 'flex-end',
                        }}
                        itemStyle={{
                            alignItems: 'flex-end',
                            justifyContent: 'center',
                            borderRadius: 0,
                        }}
                        selectedIndicatorStyle={{
                            borderRadius: 0,
                            borderTopLeftRadius: scale(10),
                            borderBottomLeftRadius: scale(10),
                        }}
                        itemTextStyle={styles.itemText}
                        selectedIndex={selectedDateIndex}
                        options={dateOptions}
                        onChange={onChangeDate}
                        decelerationRate="fast"
                    />
                    <WheelPicker
                        testID="date-time-picker-hour"
                        selectedIndicatorStyle={{
                            borderRadius: 0,
                        }}
                        itemTextStyle={styles.itemText}
                        infiniteScroll
                        selectedIndex={selectedHourIndex}
                        options={hourOptions}
                        onChange={onChangeHour}
                        decelerationRate="fast"
                        indexType={selectedDateIndex === 1 ? 'last' : 'first'}
                        disableData={getDisabledHours(selectedDateIndex)}
                    />
                    <WheelPicker
                        testID="date-time-picker-minute"
                        containerStyle={{
                            flex: 0.5,
                        }}
                        itemStyle={{
                            alignItems: 'flex-start',
                            justifyContent: 'center',
                            borderRadius: 0,
                        }}
                        selectedIndicatorStyle={{
                            borderRadius: 0,
                            borderTopRightRadius: scale(10),
                            borderBottomRightRadius: scale(10),
                        }}
                        itemTextStyle={styles.itemText}
                        infiniteScroll
                        selectedIndex={selectedMinuteIndex}
                        options={minuteOptions}
                        onChange={onChangeMinute}
                        decelerationRate="fast"
                        indexType={selectedDateIndex === 1 ? 'last' : 'first'}
                        disableData={getDisabledMinutes(selectedDateIndex, selectedHourIndex)}
                    />
                </View>
                <View style={styles.buttonContainer}>
                    <TouchableOpacity testID="cancel-button" onPress={closeModal} style={[styles.modalButton, { borderRightWidth: 0.5 }]}>
                        <Text style={styles.modalButtonText}>취소</Text>
                    </TouchableOpacity>
                    <TouchableOpacity testID="confirm-button" onPress={onConfirm} style={[styles.modalButton, { borderLeftWidth: 0.5 }]}>
                        <Text style={styles.modalButtonText}>예약(변경)</Text>
                    </TouchableOpacity>
                </View>
            </View>
        </Modal>
    );
};
export default ImagePickerComponent;

/* eslint-disable no-promise-executor-return */
import React from 'react';

import { act, fireEvent, render, waitFor } from '@testing-library/react-native';

import DateTimePicker from '../DateTimePicker';

describe('DateTimePicker', () => {
    const onChangeVisibility = jest.fn();
    const onChange = jest.fn();

    const renderComponent = (isVisible: boolean) => {
        return render(<DateTimePicker isVisible={isVisible} onChangeVisibility={onChangeVisibility} onChange={onChange} />);
    };

    test('모달이 보이는지 확인', async () => {
        const { getByTestId } = renderComponent(true);
        expect(getByTestId('date-time-modal')).toBeTruthy();
    });

    test('모달이 보이지 않는지 확인', async () => {
        const { queryByTestId } = renderComponent(false);
        expect(queryByTestId('date-time-modal')).toBeFalsy();
    });

    test('취소 버튼 클릭 시 모달 닫힘', async () => {
        const { getByTestId, rerender } = renderComponent(true);
        await act(async () => {
            fireEvent.press(getByTestId('cancel-button'));
            expect(onChangeVisibility).toHaveBeenCalledWith(false);
        });
        await waitFor(
            () => {
                rerender(<DateTimePicker isVisible={false} onChangeVisibility={onChangeVisibility} onChange={onChange} />);
            },
            { timeout: 1000 },
        );
        expect(getByTestId('date-time-modal')).toBeTruthy();
    });

    test('예약(변경) 버튼 클릭 시 onChange 호출', async () => {
        const { getByTestId } = renderComponent(true);
        await act(async () => {
            fireEvent.press(getByTestId('confirm-button'));
        });
        await waitFor(() => {
            expect(onChange).toHaveBeenCalled();
        });
    });
    test('날짜 선택 시 onChangeDate 호출', async () => {
        const { getByTestId } = renderComponent(true);
        const datePicker = getByTestId('date-time-picker-date');

        await act(async () => {
            fireEvent(datePicker, 'momentumScrollEnd', { nativeEvent: { contentOffset: { y: 10 } } });
        });
        await waitFor(() => {
            expect(onChange).toHaveBeenCalled();
        });
    });

    test('시간 선택 시 onChangeHour 호출', async () => {
        const { getByTestId } = renderComponent(true);
        const datePicker = getByTestId('date-time-picker-hour');
        await act(async () => {
            fireEvent(datePicker, 'momentumScrollEnd', { nativeEvent: { contentOffset: { y: 10 } } });
        });
        expect(onChange).toHaveBeenCalled();
    });

    test('분 선택 시 onChangeMinute 호출', async () => {
        const { getByTestId } = renderComponent(true);
        const datePicker = getByTestId('date-time-picker-minute');
        await act(async () => {
            fireEvent(datePicker, 'momentumScrollEnd', { nativeEvent: { contentOffset: { y: 10 } } });
        });
        expect(onChange).toHaveBeenCalled();
    });
});

react-native expo react-native-testing-library expo-image-picker
1个回答
0
投票

逻辑(当更改某些内容并更新渲染时)放入

act
中。

还有里面的 expect

waitFor

import { render, fireEvent, waitFor, act } from '@testing-library/react-native'
import { AddProduct } from '@/components/Product/AddProduct'
import { useCreateProduct } from '@/database/api/products'

  it('should create a fake product', async () => {
    const { findByText } = render(<AddProduct />)

    await act(async () => {
      fireEvent.press(await findByText('Adicionar produto'))
      fireEvent.press(await findByText('Criar Fake'))

      expect(mutateAsync).toHaveBeenCalledWith({
        name: expect.any(String),
        image: expect.any(String),
        purchasePrice: expect.any(Number),
        salesPrice: expect.any(Number),
      })
    })

    await waitFor(async () => {
      expect(mutateAsync).toHaveBeenCalledTimes(1)
    })
  })
© www.soinside.com 2019 - 2024. All rights reserved.