我正在写组件测试代码,我不知道去哪里问,所以我问一个问题。
我正在使用@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();
});
});
将逻辑(当更改某些内容并更新渲染时)放入
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)
})
})