React Native Jest - 如何测试具有多个挂钩的功能组件?无法存根 AccessiblityInfo 模块
React Native Jest - How to test Functional component with multiple hooks? Unable to stub AccessiblityInfo module
我正在尝试为我最近编写的功能组件编写单元测试。该组件使用多个挂钩,包括 useState
、useEffect
和 useSelector
。我发现为所述组件编写测试非常困难,因为我读到改变状态但只测试结果不是好的做法。
现在我一直在编写非常简单的单元测试,但我似乎无法开始工作。我的第一个测试目标是将 AccessibilityInfo isScreenReaderEnabled
设为 return true,这样我就可以验证在启用屏幕 reader 时应该出现的组件是否存在。我正在使用 sinon
存根 AccessibilityInfo
但是当我安装我的组件时,我正在寻找的子组件不存在并且测试失败。我不明白为什么它会失败,因为我认为我已经正确地存根了所有内容,但看起来我做错了什么。
我将在下面添加我的组件和测试文件。两者都被精简为最相关的代码。
家庭区域组件:
const MAP_MARKER_LIMIT = 3;
const MAP_DELTA = 0.002;
const ACCESSIBILITY_MAP_DELTA = 0.0002;
type HomeAreaProps = {
onDismiss: () => void;
onBack: () => void;
onCompleted: (region: Region) => void;
getHomeFence: (deviceId: string) => void;
setHomeFence: (deviceId: string, location: LatLng) => void;
initialRegion: LatLng | undefined;
deviceId: string;
};
const HomeArea = (props: HomeAreaProps) => {
// reference to map view
const mapRef = useRef<MapView | null>(null);
// current app state
let previousAppState = useRef(RNAppState.currentState).current;
const initialRegion = {
latitude: parseFloat((props.initialRegion?.latitude ?? 0).toFixed(6)),
longitude: parseFloat((props.initialRegion?.longitude ?? 0).toFixed(6)),
latitudeDelta: MAP_DELTA,
longitudeDelta: MAP_DELTA,
};
// modified region of senior
const [region, setRegion] = useState(initialRegion);
// is accessibility screen reader enabled
const [isScreenReaderEnabled, setIsScreenReaderEnabled] = useState(false);
// state for floating modal
const [showFloatingModal, setShowFloatingModal] = useState(false);
// state for center the zone alert screen
const [showAlertScreen, setShowAlertScreen] = useState(false);
// state for center the zone error screen
const [showErrorScreen, setShowErrorScreen] = useState(false);
// To query error status after a request is made, default to false incase
// error cannot be queried from store
const requestError = useSelector<AppState, boolean>((state) => {
if (state.homeFence[props.deviceId]) {
return state.homeZoneFence[props.deviceId].error;
} else {
return false;
}
});
// To access device data from redux store, same as above if device data
// can't be queried then set to null
const deviceData = useSelector<AppState, HomeDeviceData | null | undefined>(
(state) => {
if (state.homeFence[props.deviceId]) {
return state.homeFence[props.deviceId].deviceData;
} else {
return null;
}
}
);
const [initialHomeData] = useState<HomeDeviceData | null | undefined>(
deviceData
);
// didTap on [x] button
const onDismiss = () => {
setShowFloatingModal(true);
};
// didTap on 'save' button
const onSave = () => {
if (
didHomeLocationMovePastLimit(
region.latitude,
region.longitude,
MAP_MARKER_LIMIT
)
) {
setShowAlertScreen(true);
} else {
updateHomeFence();
}
};
const onDismissFloatingModal = () => {
setShowFloatingModal(false);
props.getHomeFence(props.deviceId);
props.onDismiss();
};
const onSaveFloatingModal = () => {
setShowFloatingModal(false);
if (
didHomeLocationMovePastLimit(
region.latitude,
region.longitude,
MAP_MARKER_LIMIT
)
) {
setShowFloatingModal(false);
setShowAlertScreen(true);
} else {
updateHomeFence();
}
};
const onDismissModal = () => {
setShowFloatingModal(false);
};
// Center the Zone Alert Screen
const onBackAlert = () => {
// Go back to center the zone screen
setShowAlertScreen(false);
};
const onNextAlert = () => {
updateHomeFence();
setShowAlertScreen(false);
};
// Center the Zone Error Screen
const onBackError = () => {
setShowErrorScreen(false);
};
const onNextError = () => {
updateHomeFence();
};
const didHomeLocationMovePastLimit = (
lat: number,
lon: number,
limit: number
) => {
if (
lat !== undefined &&
lat !== null &&
lon !== undefined &&
lon !== null
) {
const haversineDistance = haversineFormula(
lat,
lon,
initialRegion.latitude,
initialRegion.longitude,
"M"
);
return haversineDistance > limit;
}
return false;
};
// didTap on 'reset' button
const onReset = () => {
// animate to initial region
if (initialRegion && mapRef) {
mapRef.current?.animateToRegion(initialRegion, 1000);
}
};
// did update region by manually moving map
const onRegionChange = (region: Region) => {
setRegion({
...initialRegion,
latitude: parseFloat(region.latitude.toFixed(6)),
longitude: parseFloat(region.longitude.toFixed(6)),
});
};
// didTap 'left' map control
const onLeft = () => {
let adjustedRegion: Region = {
...region,
longitude: region.longitude - ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
// didTap 'right' map control
const onRight = () => {
let adjustedRegion: Region = {
...region,
longitude: region.longitude + ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
// didTap 'up' map control
const onUp = () => {
let adjustedRegion: Region = {
...region,
latitude: region.latitude + ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
// didTap 'down' map control
const onDown = () => {
let adjustedRegion: Region = {
...region,
latitude: region.latitude - ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
const updateHomeFence = () => {
const lat = region.latitude;
const lon = region.longitude;
const location: LatLng = {
latitude: lat,
longitude: lon,
};
props.setHomeFence(props.deviceId, location);
};
// gets accessibility status info
const getAccessibilityStatus = () => {
AccessibilityInfo.isScreenReaderEnabled()
.then((isEnabled) => setIsScreenReaderEnabled(isEnabled))
.catch((error) => console.log(error));
};
// listener for when the app changes app state
const onAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === "active" && previousAppState === "background") {
// when we come to the foreground from the background we should
// check the accessibility status again
getAccessibilityStatus();
}
previousAppState = nextAppState;
};
useEffect(() => {
getAccessibilityStatus();
RNAppState.addEventListener("change", onAppStateChange);
return () => RNAppState.removeEventListener("change", onAppStateChange);
}, []);
useEffect(() => {
// exit screen if real update has occurred, i.e. data changed on backend
// AND if there is no request error
if (initialHomeData !== deviceData && initialHomeData && deviceData) {
if (!requestError) {
props.onCompleted(region);
}
}
setShowErrorScreen(requestError);
}, [requestError, deviceData]);
return (
<DualPane>
<TopPane>
<View style={styles.mapContainer}>
<MapView
accessible={false}
importantForAccessibility={"no-hide-descendants"}
style={styles.mapView}
provider={PROVIDER_GOOGLE}
showsUserLocation={false}
zoomControlEnabled={!isScreenReaderEnabled}
pitchEnabled={false}
zoomEnabled={!isScreenReaderEnabled}
scrollEnabled={!isScreenReaderEnabled}
rotateEnabled={!isScreenReaderEnabled}
showsPointsOfInterest={false}
initialRegion={initialRegion}
ref={mapRef}
onRegionChange={onRegionChange}
/>
<ScrollingHand />
{isScreenReaderEnabled && (
<MapControls
onLeft={onLeft}
onRight={onRight}
onUp={onUp}
onDown={onDown}
/>
)}
{region && <PulsingMarker />}
{JSON.stringify(region) !== JSON.stringify(initialRegion) && (
<Button
style={[btn, overrideButtonStyle]}
label={i18n.t("homeZone.homeZoneArea.buttonTitle.reset")}
icon={reset}
onTap={onReset}
accessibilityLabel={i18n.t(
"homeZone.homeZoneArea.buttonTitle.reset"
)}
/>
)}
</View>
</TopPane>
<OneButtonBottomPane
onPress={onSave}
buttonLabel={i18n.t("homeZone.homeZoneArea.buttonTitle.save")}
>
<View style={styles.bottomPaneContainer}>
<BottomPaneText
title={i18n.t("homeZone.homeZoneArea.title")}
content={i18n.t("homeZone.homeZoneArea.description")}
/>
</View>
</OneButtonBottomPane>
<TouchableOpacity
style={styles.closeIconContainer}
onPress={onDismiss}
accessibilityLabel={i18n.t("homeZone.homeZoneArea.buttonTitle.close")}
accessibilityRole={"button"}
>
<Image
style={styles.cancelIcon}
source={require("../../../assets/home-zone/close.png")}
/>
</TouchableOpacity>
<HomeFloatingModal
showFloatingModal={showFloatingModal}
onDismiss={onDismissModal}
onDiscard={onDismissFloatingModal}
onSave={onSaveFloatingModal}
/>
<HomeAlert
isVisible={showAlertScreen}
modalTitle={i18n.t("home.feedbackCenter.title.confirmZoneCenter")}
modalDescription={i18n.t(
"home.feedbackCenter.description.confirmZoneCenter"
)}
onBackButtonTitle={i18n.t("home.feedback.buttonTitle.back")}
onNextButtonTitle={i18n.t("home.feedback.buttonTitle.okay")}
onBack={onBackAlert}
onNext={onNextAlert}
/>
<HomeAlert
isVisible={showErrorScreen}
sentimentType={SentimentType.alert}
showWarningIcon={false}
modalTitle={i18n.t("home.errorScreen.title")}
modalDescription={i18n.t("home.errorScreen.description")}
onBackButtonTitle={i18n.t("home.errorScreen.buttonTitle.cancel")}
onNextButtonTitle={i18n.t("home.errorScreen.buttonTitle.tryAgain")}
onBack={onBackError}
onNext={onNextError}
/>
</DualPane>
);
};
export default HomeArea;
家庭区域测试:
import "jsdom-global/register";
import React from "react";
import { AccessibilityInfo } from "react-native";
import HomeArea from "../../../src/home/components/home-area";
import HomeAlert from "../../../src/home/components/home-alert";
import MapControls from "../../../src/home/components/map-controls";
import { mount } from "enzyme";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import sinon from "sinon";
jest.useFakeTimers();
const mockStore = configureStore();
const initialState = {
homeFence: {
"c9035f03-b562-4670-86c6-748b56f02aef": {
deviceData: {
eTag: "964665368A4BD68CF86B525385BA507A3D7F5335",
fences: [
{
pointsOfInterest: [
{
latitude: 32.8463898,
longitude: -117.2776381,
radius: 100,
uncertainty: 0,
poiSource: 2,
},
],
id: "5e1e0bc0-880d-4b0c-a0fa-268975f3046b",
timeZoneId: "America/Los_Angeles",
type: 7,
name: "Children's Pool",
},
{
pointsOfInterest: [
{
latitude: 32.9148887,
longitude: -117.228307,
radius: 100,
uncertainty: 0,
poiSource: 2,
},
],
id: "782d8fcd-242d-47c0-872b-f669e7ca81c7",
timeZoneId: "America/Los_Angeles",
type: 1,
name: "Home",
},
],
},
error: false,
},
},
};
const initialStateWithError = {
homeFence: {
"c9035f03-b562-4670-86c6-748b56f02aef": {
deviceData: {
eTag: "964665368A4BD68CF86B525385BA507A3D7F5335",
fences: [],
},
error: true,
},
},
};
const store = mockStore(initialState);
const props = {
onDismiss: jest.fn(),
onBack: jest.fn(),
onCompleted: jest.fn(),
getHomeZoneFence: jest.fn(),
setHomeZoneFence: jest.fn(),
initialRegion: { latitude: 47.6299, longitude: -122.3537 },
deviceId: "c9035f03-b562-4670-86c6-748b56f02aef",
};
// https://github.com/react-native-maps/react-native-maps/issues/2918#issuecomment-510795210
jest.mock("react-native-maps", () => {
const { View } = require("react-native");
const MockMapView = (props: any) => {
return <View>{props.children}</View>;
};
const MockMarker = (props: any) => {
return <View>{props.children}</View>;
};
return {
__esModule: true,
default: MockMapView,
Marker: MockMarker,
};
});
describe("<HomeArea />", () => {
describe("accessibility", () => {
it("should return true and we should have map control present", async () => {
sinon.stub(AccessibilityInfo, "isScreenReaderEnabled").callsFake(() => {
return new Promise((res, _) => {
res(true);
});
});
const wrapper = mount(
<Provider store={store}>
<HomeArea {...props} />
</Provider>
);
expect(wrapper).not.toBeUndefined(){jest.fn()} onRight={jest.fn()} onUp={jest.fn()} onDown={jest.fn()} />).instance()).not.toBeUndefined();
expect(wrapper.find(MapControls).length).toEqual(1);
});
});
describe("requestError modal", () => {
it("should render requestErrorModal", async () => {
const store = mockStore(initialStateWithError);
const wrapper = mount(
<Provider store={store}>
<HomeArea {...props} />
</Provider>
);
expect(wrapper).not.toBeUndefined();
expect(
wrapper.contains(
<HomeAlert
isVisible={false}
modalTitle={""}
modalDescription={""}
onBackButtonTitle={""}
onNextButtonTitle={""}
onBack={jest.fn()}
onNext={jest.fn()}
/>
)
).toBe(true);
});
});
});
有人认为我曾经在我的组件中存根 getAccessibilityStatus
但没有任何运气这样做。我一直在阅读在线功能组件有点像“黑匣子”,存根功能似乎不可能,这是真的吗?我开始想知道如果多个挂钩和它是一个功能组件的事实使我很难成功地测试我的组件。
非常感谢任何帮助。
这可能是因为在您检查组件是否存在之前承诺没有解决。您可以在此处阅读更多相关信息 https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/
像这样尝试
const runAllPromises = () => new Promise(setImmediate)
...
describe("accessibility", () => {
it("should return true and we should have map control present", async () => {
sinon.stub(AccessibilityInfo, "isScreenReaderEnabled").callsFake(() => {
return new Promise((res, _) => {
res(true);
});
});
const wrapper = mount(
<Provider store={store}>
<HomeArea {...props} />
</Provider>
);
await runAllPromises()
// after waiting for all the promises to be exhausted
// we can do our UI check
component.update()
expect(wrapper).not.toBeUndefined();
expect(wrapper.find(MapControls).length).toEqual(1);
});
});
...
我正在尝试为我最近编写的功能组件编写单元测试。该组件使用多个挂钩,包括 useState
、useEffect
和 useSelector
。我发现为所述组件编写测试非常困难,因为我读到改变状态但只测试结果不是好的做法。
现在我一直在编写非常简单的单元测试,但我似乎无法开始工作。我的第一个测试目标是将 AccessibilityInfo isScreenReaderEnabled
设为 return true,这样我就可以验证在启用屏幕 reader 时应该出现的组件是否存在。我正在使用 sinon
存根 AccessibilityInfo
但是当我安装我的组件时,我正在寻找的子组件不存在并且测试失败。我不明白为什么它会失败,因为我认为我已经正确地存根了所有内容,但看起来我做错了什么。
我将在下面添加我的组件和测试文件。两者都被精简为最相关的代码。
家庭区域组件:
const MAP_MARKER_LIMIT = 3;
const MAP_DELTA = 0.002;
const ACCESSIBILITY_MAP_DELTA = 0.0002;
type HomeAreaProps = {
onDismiss: () => void;
onBack: () => void;
onCompleted: (region: Region) => void;
getHomeFence: (deviceId: string) => void;
setHomeFence: (deviceId: string, location: LatLng) => void;
initialRegion: LatLng | undefined;
deviceId: string;
};
const HomeArea = (props: HomeAreaProps) => {
// reference to map view
const mapRef = useRef<MapView | null>(null);
// current app state
let previousAppState = useRef(RNAppState.currentState).current;
const initialRegion = {
latitude: parseFloat((props.initialRegion?.latitude ?? 0).toFixed(6)),
longitude: parseFloat((props.initialRegion?.longitude ?? 0).toFixed(6)),
latitudeDelta: MAP_DELTA,
longitudeDelta: MAP_DELTA,
};
// modified region of senior
const [region, setRegion] = useState(initialRegion);
// is accessibility screen reader enabled
const [isScreenReaderEnabled, setIsScreenReaderEnabled] = useState(false);
// state for floating modal
const [showFloatingModal, setShowFloatingModal] = useState(false);
// state for center the zone alert screen
const [showAlertScreen, setShowAlertScreen] = useState(false);
// state for center the zone error screen
const [showErrorScreen, setShowErrorScreen] = useState(false);
// To query error status after a request is made, default to false incase
// error cannot be queried from store
const requestError = useSelector<AppState, boolean>((state) => {
if (state.homeFence[props.deviceId]) {
return state.homeZoneFence[props.deviceId].error;
} else {
return false;
}
});
// To access device data from redux store, same as above if device data
// can't be queried then set to null
const deviceData = useSelector<AppState, HomeDeviceData | null | undefined>(
(state) => {
if (state.homeFence[props.deviceId]) {
return state.homeFence[props.deviceId].deviceData;
} else {
return null;
}
}
);
const [initialHomeData] = useState<HomeDeviceData | null | undefined>(
deviceData
);
// didTap on [x] button
const onDismiss = () => {
setShowFloatingModal(true);
};
// didTap on 'save' button
const onSave = () => {
if (
didHomeLocationMovePastLimit(
region.latitude,
region.longitude,
MAP_MARKER_LIMIT
)
) {
setShowAlertScreen(true);
} else {
updateHomeFence();
}
};
const onDismissFloatingModal = () => {
setShowFloatingModal(false);
props.getHomeFence(props.deviceId);
props.onDismiss();
};
const onSaveFloatingModal = () => {
setShowFloatingModal(false);
if (
didHomeLocationMovePastLimit(
region.latitude,
region.longitude,
MAP_MARKER_LIMIT
)
) {
setShowFloatingModal(false);
setShowAlertScreen(true);
} else {
updateHomeFence();
}
};
const onDismissModal = () => {
setShowFloatingModal(false);
};
// Center the Zone Alert Screen
const onBackAlert = () => {
// Go back to center the zone screen
setShowAlertScreen(false);
};
const onNextAlert = () => {
updateHomeFence();
setShowAlertScreen(false);
};
// Center the Zone Error Screen
const onBackError = () => {
setShowErrorScreen(false);
};
const onNextError = () => {
updateHomeFence();
};
const didHomeLocationMovePastLimit = (
lat: number,
lon: number,
limit: number
) => {
if (
lat !== undefined &&
lat !== null &&
lon !== undefined &&
lon !== null
) {
const haversineDistance = haversineFormula(
lat,
lon,
initialRegion.latitude,
initialRegion.longitude,
"M"
);
return haversineDistance > limit;
}
return false;
};
// didTap on 'reset' button
const onReset = () => {
// animate to initial region
if (initialRegion && mapRef) {
mapRef.current?.animateToRegion(initialRegion, 1000);
}
};
// did update region by manually moving map
const onRegionChange = (region: Region) => {
setRegion({
...initialRegion,
latitude: parseFloat(region.latitude.toFixed(6)),
longitude: parseFloat(region.longitude.toFixed(6)),
});
};
// didTap 'left' map control
const onLeft = () => {
let adjustedRegion: Region = {
...region,
longitude: region.longitude - ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
// didTap 'right' map control
const onRight = () => {
let adjustedRegion: Region = {
...region,
longitude: region.longitude + ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
// didTap 'up' map control
const onUp = () => {
let adjustedRegion: Region = {
...region,
latitude: region.latitude + ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
// didTap 'down' map control
const onDown = () => {
let adjustedRegion: Region = {
...region,
latitude: region.latitude - ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
const updateHomeFence = () => {
const lat = region.latitude;
const lon = region.longitude;
const location: LatLng = {
latitude: lat,
longitude: lon,
};
props.setHomeFence(props.deviceId, location);
};
// gets accessibility status info
const getAccessibilityStatus = () => {
AccessibilityInfo.isScreenReaderEnabled()
.then((isEnabled) => setIsScreenReaderEnabled(isEnabled))
.catch((error) => console.log(error));
};
// listener for when the app changes app state
const onAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === "active" && previousAppState === "background") {
// when we come to the foreground from the background we should
// check the accessibility status again
getAccessibilityStatus();
}
previousAppState = nextAppState;
};
useEffect(() => {
getAccessibilityStatus();
RNAppState.addEventListener("change", onAppStateChange);
return () => RNAppState.removeEventListener("change", onAppStateChange);
}, []);
useEffect(() => {
// exit screen if real update has occurred, i.e. data changed on backend
// AND if there is no request error
if (initialHomeData !== deviceData && initialHomeData && deviceData) {
if (!requestError) {
props.onCompleted(region);
}
}
setShowErrorScreen(requestError);
}, [requestError, deviceData]);
return (
<DualPane>
<TopPane>
<View style={styles.mapContainer}>
<MapView
accessible={false}
importantForAccessibility={"no-hide-descendants"}
style={styles.mapView}
provider={PROVIDER_GOOGLE}
showsUserLocation={false}
zoomControlEnabled={!isScreenReaderEnabled}
pitchEnabled={false}
zoomEnabled={!isScreenReaderEnabled}
scrollEnabled={!isScreenReaderEnabled}
rotateEnabled={!isScreenReaderEnabled}
showsPointsOfInterest={false}
initialRegion={initialRegion}
ref={mapRef}
onRegionChange={onRegionChange}
/>
<ScrollingHand />
{isScreenReaderEnabled && (
<MapControls
onLeft={onLeft}
onRight={onRight}
onUp={onUp}
onDown={onDown}
/>
)}
{region && <PulsingMarker />}
{JSON.stringify(region) !== JSON.stringify(initialRegion) && (
<Button
style={[btn, overrideButtonStyle]}
label={i18n.t("homeZone.homeZoneArea.buttonTitle.reset")}
icon={reset}
onTap={onReset}
accessibilityLabel={i18n.t(
"homeZone.homeZoneArea.buttonTitle.reset"
)}
/>
)}
</View>
</TopPane>
<OneButtonBottomPane
onPress={onSave}
buttonLabel={i18n.t("homeZone.homeZoneArea.buttonTitle.save")}
>
<View style={styles.bottomPaneContainer}>
<BottomPaneText
title={i18n.t("homeZone.homeZoneArea.title")}
content={i18n.t("homeZone.homeZoneArea.description")}
/>
</View>
</OneButtonBottomPane>
<TouchableOpacity
style={styles.closeIconContainer}
onPress={onDismiss}
accessibilityLabel={i18n.t("homeZone.homeZoneArea.buttonTitle.close")}
accessibilityRole={"button"}
>
<Image
style={styles.cancelIcon}
source={require("../../../assets/home-zone/close.png")}
/>
</TouchableOpacity>
<HomeFloatingModal
showFloatingModal={showFloatingModal}
onDismiss={onDismissModal}
onDiscard={onDismissFloatingModal}
onSave={onSaveFloatingModal}
/>
<HomeAlert
isVisible={showAlertScreen}
modalTitle={i18n.t("home.feedbackCenter.title.confirmZoneCenter")}
modalDescription={i18n.t(
"home.feedbackCenter.description.confirmZoneCenter"
)}
onBackButtonTitle={i18n.t("home.feedback.buttonTitle.back")}
onNextButtonTitle={i18n.t("home.feedback.buttonTitle.okay")}
onBack={onBackAlert}
onNext={onNextAlert}
/>
<HomeAlert
isVisible={showErrorScreen}
sentimentType={SentimentType.alert}
showWarningIcon={false}
modalTitle={i18n.t("home.errorScreen.title")}
modalDescription={i18n.t("home.errorScreen.description")}
onBackButtonTitle={i18n.t("home.errorScreen.buttonTitle.cancel")}
onNextButtonTitle={i18n.t("home.errorScreen.buttonTitle.tryAgain")}
onBack={onBackError}
onNext={onNextError}
/>
</DualPane>
);
};
export default HomeArea;
家庭区域测试:
import "jsdom-global/register";
import React from "react";
import { AccessibilityInfo } from "react-native";
import HomeArea from "../../../src/home/components/home-area";
import HomeAlert from "../../../src/home/components/home-alert";
import MapControls from "../../../src/home/components/map-controls";
import { mount } from "enzyme";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import sinon from "sinon";
jest.useFakeTimers();
const mockStore = configureStore();
const initialState = {
homeFence: {
"c9035f03-b562-4670-86c6-748b56f02aef": {
deviceData: {
eTag: "964665368A4BD68CF86B525385BA507A3D7F5335",
fences: [
{
pointsOfInterest: [
{
latitude: 32.8463898,
longitude: -117.2776381,
radius: 100,
uncertainty: 0,
poiSource: 2,
},
],
id: "5e1e0bc0-880d-4b0c-a0fa-268975f3046b",
timeZoneId: "America/Los_Angeles",
type: 7,
name: "Children's Pool",
},
{
pointsOfInterest: [
{
latitude: 32.9148887,
longitude: -117.228307,
radius: 100,
uncertainty: 0,
poiSource: 2,
},
],
id: "782d8fcd-242d-47c0-872b-f669e7ca81c7",
timeZoneId: "America/Los_Angeles",
type: 1,
name: "Home",
},
],
},
error: false,
},
},
};
const initialStateWithError = {
homeFence: {
"c9035f03-b562-4670-86c6-748b56f02aef": {
deviceData: {
eTag: "964665368A4BD68CF86B525385BA507A3D7F5335",
fences: [],
},
error: true,
},
},
};
const store = mockStore(initialState);
const props = {
onDismiss: jest.fn(),
onBack: jest.fn(),
onCompleted: jest.fn(),
getHomeZoneFence: jest.fn(),
setHomeZoneFence: jest.fn(),
initialRegion: { latitude: 47.6299, longitude: -122.3537 },
deviceId: "c9035f03-b562-4670-86c6-748b56f02aef",
};
// https://github.com/react-native-maps/react-native-maps/issues/2918#issuecomment-510795210
jest.mock("react-native-maps", () => {
const { View } = require("react-native");
const MockMapView = (props: any) => {
return <View>{props.children}</View>;
};
const MockMarker = (props: any) => {
return <View>{props.children}</View>;
};
return {
__esModule: true,
default: MockMapView,
Marker: MockMarker,
};
});
describe("<HomeArea />", () => {
describe("accessibility", () => {
it("should return true and we should have map control present", async () => {
sinon.stub(AccessibilityInfo, "isScreenReaderEnabled").callsFake(() => {
return new Promise((res, _) => {
res(true);
});
});
const wrapper = mount(
<Provider store={store}>
<HomeArea {...props} />
</Provider>
);
expect(wrapper).not.toBeUndefined(){jest.fn()} onRight={jest.fn()} onUp={jest.fn()} onDown={jest.fn()} />).instance()).not.toBeUndefined();
expect(wrapper.find(MapControls).length).toEqual(1);
});
});
describe("requestError modal", () => {
it("should render requestErrorModal", async () => {
const store = mockStore(initialStateWithError);
const wrapper = mount(
<Provider store={store}>
<HomeArea {...props} />
</Provider>
);
expect(wrapper).not.toBeUndefined();
expect(
wrapper.contains(
<HomeAlert
isVisible={false}
modalTitle={""}
modalDescription={""}
onBackButtonTitle={""}
onNextButtonTitle={""}
onBack={jest.fn()}
onNext={jest.fn()}
/>
)
).toBe(true);
});
});
});
有人认为我曾经在我的组件中存根 getAccessibilityStatus
但没有任何运气这样做。我一直在阅读在线功能组件有点像“黑匣子”,存根功能似乎不可能,这是真的吗?我开始想知道如果多个挂钩和它是一个功能组件的事实使我很难成功地测试我的组件。
非常感谢任何帮助。
这可能是因为在您检查组件是否存在之前承诺没有解决。您可以在此处阅读更多相关信息 https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/
像这样尝试
const runAllPromises = () => new Promise(setImmediate)
...
describe("accessibility", () => {
it("should return true and we should have map control present", async () => {
sinon.stub(AccessibilityInfo, "isScreenReaderEnabled").callsFake(() => {
return new Promise((res, _) => {
res(true);
});
});
const wrapper = mount(
<Provider store={store}>
<HomeArea {...props} />
</Provider>
);
await runAllPromises()
// after waiting for all the promises to be exhausted
// we can do our UI check
component.update()
expect(wrapper).not.toBeUndefined();
expect(wrapper.find(MapControls).length).toEqual(1);
});
});
...