Related
I'm trying to write unit tests for a functional component I've recently written. This component makes use of multiple hooks, including, useState, useEffect and useSelector. I'm finding it very difficult to write tests for said component since I've read that it's not good practice to alter the state but only test for outcomes.
Right now I'm stuck writing pretty simple unit tests that I just can't seem to get working. My goal for the first test is to stub AccessibilityInfo isScreenReaderEnabled to return true so that I can verify the existence of a component that should appear when we have screen reader enabled. I'm using sinon to stub AccessibilityInfo but when I mount my component the child component I'm looking for doesn't exist and the test fails. I don't understand why it's failing because I thought I had stubbed everything properly, but it looks like I'm doing something wrong.
I'll add both my component and test files below. Both have been stripped down to the most relevant code.
Home-Area Component:
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;
Home-Area-Tests:
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);
});
});
});
One thought I had was to stub getAccessibilityStatus in my component but haven't had any luck doing so. I've been reading online functional components are a bit of a "black box" and stubbing functions doesn't seem possible, is this true? I'm starting to wonder how I can successfully test my component if the multiple hooks and the fact that it's a functional component make it very difficult to do so.
Any help is greatly appreciated.
It probably is because the promise is not resolving before you check that the component exists. You can read more about it here https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/
try it like this
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);
});
});
...
I am trying to add dynamically components to View with hooks in React Native this way:
const AssetDetailScreen = (props) => {
const [details, setDetails] = React.useState('');
React.useEffect(() => {
getAssetDetailData()
});
getAssetDetailData = () => {
assetDetailPromise().then((data) => {
setDetails(data)
}).catch((error) => {
...
});
}
assetItems = details.map((item) => {
return(
<Text>{item.label}</Text>
)
})
return (
<View>
{assetItems}
</View>
)
}
But I get this error:
TypeError: undefined is not a function (near '... details.map...')
How can I solve this issue?
Is there any workaround?
I solved this issue by replacing this line:
const [details, setDetails] = React.useState('');
with this:
const [details, setDetails] = React.useState([]);
I have a wrapper for Flatlist called FlatListShadow but for this post FlatListShadow and FlatList is the same thing
I need to test the renderItem function in FlatListShadow which looks like this
renderItem={({ item }) => (
<Device
title={item.deviceName}
platform={item.platform}
updatedAt={item.updatedAt}
status={item.status}
selectDevice={() => selectDevice(item.deviceId)}
isSelected={selectedDeviceIdList.includes(item.deviceId)}
/>
)}
Unfortuanately in the snapshot it only gives this information
renderItem={[Function]}
If you're using enzyme you can achieve it like this
// prepare a mock item to render the renderItem with
const mockItem = {
deviceName: 'mock device name',
platform: 'mock platform',
updatedAt: 123,
status: 'mock status',
deviceId: '1-2-3-4',
}
describe('YourComponent', () => {
let shallowWrapper
beforeAll(() => {
shallowWraper = shallow(<YourComponent />);
});
it('should match the snapshot', () => {
// will generate snapshot for your component
expect(shallowWrapper).toMatchSnapshot();
});
describe('.renderItem', () => {
let renderItemShallowWrapper;
beforeAll(() => {
// find the component whose property is rendered as renderItem={[Function]}
// if we presume it's imported as ComponentWithRenderItemProp
// find it and get it's renderItem property
RenderItem = shallowWraper.find(ComponentWithRenderItemProp).prop('renderItem');
// and since it's a component render it as such
// with mockItem
renderItemShallowWrapper = shallow(<RenderItem item={mockItem} />);
});
it('should match the snapshot', () => {
// generate snapshot for the renderItem
expect(renderItemShallowWrapper).toMatchSnapshot();
});
});
});
If you are using jest :
describe('EasyToUseSection', () => {
it.only('flatlist should return renderItem correctly', () => {
const itemData = {
name: 'Name',
desc: 'Description',
};
const { getByTestId } = renderComponent();
expect(getByTestId('flatlist')).toBeDefined();
const element = getByTestId('flatlist').props.renderItem(itemData);
expect(element.props.data).toStrictEqual(itemData);
expect(element.type).toBe(Device);
});
});
This way the data that is sent is checked and also the component rendered type can be checked
I'm testing the content of a <Text /> tag in React Native using Enzyme and Jest. My problem is the that the test is failing (even though everything empirically works and even though I feel like I wrote the test correctly). Here is the test:
describe("when less than minimum mandatory chosen", () => {
it("should render a label saying choose at least X items", () => {
console.log(wrapper);
expect(
wrapper
.find(Text)
.at(1)
.contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST} ${props.minChoices}.`)
).toBe(true);
});
});
I would like to check what the actual string within the <Text /> tag is. How could I achieve this?
As per request by Brian, here is the full test code:
import React from "react";
import { shallow } from "enzyme";
import { Text, View } from "react-native";
import {
SCREEN_TEXT_MENU_ITEM_DETAIL_MANDATORY,
SCREEN_TEXT_MENU_ITEM_DETAIL_OPTIONAL,
SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO,
SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST
} from "../../config/constants/screenTexts";
import { AccordionList } from "./AccordionList";
import styles from "./styles";
const createTestProps = props => ({
header: "Kekse",
items: [
{
uuid: "1057e751-8ef1-4524-a743-1b4ba7b33d7b",
name: "Haferkeks",
price: "2.00",
priceCurrency: "EUR"
},
{
uuid: "f41f8e1a-b526-490e-ba4a-3d6acb3f3c16",
name: "Schokosojakeks",
price: "1.50",
priceCurrency: "EUR"
}
],
chosenItems: [],
onItemPressed: jest.fn(),
...props
});
describe("AccordionList", () => {
describe("rendering", () => {
let wrapper;
let props;
beforeEach(() => {
props = createTestProps();
wrapper = shallow(<AccordionList {...props} />);
});
it("should render container", () => {
expect(
wrapper
.find(View)
.at(0)
.prop("style")
).toContain(styles.container);
});
it("should render a <Collapsible />", () => {
expect(wrapper.find("Collapsible")).toHaveLength(1);
});
it("should give the header's <TouchableOpacity /> the headerbutton style", () => {
expect(
wrapper
.find("TouchableOpacity")
.at(0)
.prop("style")
).toEqual(styles.headerButton);
});
it("should render a header", () => {
expect(
wrapper
.find(Text)
.at(0)
.contains(props.header)
).toBe(true);
});
it("should give the header the header style", () => {
expect(
wrapper
.find(Text)
.at(0)
.prop("style")
).toEqual(styles.header);
});
it("should render a subheader", () => {
expect(
wrapper
.find(Text)
.at(1)
.prop("style")
).toContain(styles.subHeader);
});
it("should render a <TouchableOpacity /> for each of it's items", () => {
expect(wrapper.find("TouchableOpacity")).toHaveLength(props.items.length + 1);
});
describe("folded", () => {
it("should render an arrow pointing to the right", () => {
expect(wrapper.find("Image").prop("source")).toEqual(
require("../../assets/icons/rightArrow.png")
);
});
it("should render the folded arrow with the default style", () => {
expect(wrapper.find("Image").prop("style")).toEqual([styles.arrowIcon, styles.inActive]);
});
describe("mandatory", () => {
beforeEach(() => {
props = createTestProps({ minChoices: 1 });
wrapper = shallow(<AccordionList {...props} />);
});
it("should render a mandatory label with the minimum number of mandatory items", () => {
expect(
wrapper
.find(Text)
.at(1)
.contains(`(${SCREEN_TEXT_MENU_ITEM_DETAIL_MANDATORY}, ${props.minChoices})`)
).toBe(true);
});
});
describe("optional", () => {
it("should render an optional label", () => {
expect(
wrapper
.find(Text)
.at(1)
.contains(`(${SCREEN_TEXT_MENU_ITEM_DETAIL_OPTIONAL})`)
).toBe(true);
});
});
});
describe("expanded", () => {
beforeEach(() => {
wrapper.setState({ collapsed: false });
});
it("should render an arrow pointing down", () => {
expect(wrapper.find("Image").prop("source")).toEqual(
require("../../assets/icons/downArrow.png")
);
});
it("should render the expanded arrow with the default style", () => {
expect(wrapper.find("Image").prop("style")).toEqual([styles.arrowIcon, styles.inActive]);
});
// FIXME: These tests should also work but don't for some reason.
describe("mandatory", () => {
beforeEach(() => {
props = createTestProps({ minChoices: 1 });
wrapper = shallow(<AccordionList {...props} />);
wrapper.setState({ collapsed: false });
});
describe("when less than minimum mandatory chosen", () => {
it("should render a label saying choose at least X items", () => {
expect(
wrapper
.find(Text)
.at(1)
.contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST} ${props.minChoices}.`)
).toBe(true);
});
});
describe("when more than minimum mandatory chosen", () => {
beforeEach(() => {
props = createTestProps({
minChoices: 1,
chosenItems: ["1057e751-8ef1-4524-a743-1b4ba7b33d7b"]
});
wrapper = shallow(<AccordionList {...props} />);
wrapper.setState({ collapsed: false });
});
it("should render a label saying choose up to X items", () => {
expect(
wrapper
.find(Text)
.at(1)
.contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${props.maxChoices}.`)
).toBe(true);
});
});
});
describe("optional", () => {
it("should render a label saying choose up to X items", () => {
expect(
wrapper
.find(Text)
.at(1)
.contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${props.maxChoices}.`)
).toBe(true);
});
});
});
describe("item chosen", () => {
beforeEach(() => {
props = createTestProps({ chosenItems: ["1057e751-8ef1-4524-a743-1b4ba7b33d7b"] });
wrapper = shallow(<AccordionList {...props} />);
wrapper.setState({ collapsed: false });
});
it("should render a checkmark for the item", () => {
expect(
wrapper
.find("Image")
.at(1)
.prop("source")
).toEqual(require("../../assets/icons/checkmark.png"));
});
it("should render the checkmark with the checkmarkIcon and active style", () => {
expect(
wrapper
.find("Image")
.at(1)
.prop("style")
).toEqual([styles.checkmarkIcon, styles.active]);
});
it("should render the folded arrow with the primary style", () => {
expect(
wrapper
.find("Image")
.at(0)
.prop("style")
).toContain(styles.active);
});
it("should render the expanded arrow with the primary style", () => {
wrapper.setState({ collapsed: false });
expect(
wrapper
.find("Image")
.at(0)
.prop("style")
).toContain(styles.active);
});
});
describe("max items chosen", () => {
beforeEach(() => {
props = createTestProps({
maxChoices: 1,
chosenItems: ["1057e751-8ef1-4524-a743-1b4ba7b33d7b"]
});
wrapper = shallow(<AccordionList {...props} />);
wrapper.setState({ collapsed: false });
});
it("should disable all items but the chosen", () => {
expect(
wrapper
.find("TouchableOpacity")
.at(2)
.prop("disabled")
).toEqual(true);
});
});
});
describe("interaction", () => {
let wrapper;
let props;
beforeEach(() => {
props = createTestProps();
wrapper = shallow(<AccordionList {...props} />);
});
// FIXME: This test does not work for some reason...
// describe("pressing the header", () => {
// beforeEach(() => {
// wrapper.instance().toggleExpanded = jest.fn();
// wrapper
// .find("TouchableOpacity")
// .first()
// .prop("onPress")();
// });
//
// it("should call the toggleExpanded() instance function", () => {
// expect(wrapper.instance().toggleExpanded).toHaveBeenCalledTimes(1);
// });
// });
describe("pressing an item", () => {
beforeEach(() => {
wrapper
.find("TouchableOpacity")
.at(1)
.prop("onPress")();
});
it("should call the onItemPressed callback", () => {
expect(props.onItemPressed).toHaveBeenCalledTimes(1);
});
});
});
describe("component methods", () => {
describe("toggleExpanded", () => {
let wrapper;
let props;
beforeEach(() => {
props = createTestProps();
wrapper = shallow(<AccordionList {...props} />);
wrapper.instance().toggleExpanded();
});
it("should change the state of the component to collapsed=false", () => {
expect(wrapper.instance().state.collapsed).toBe(false);
});
});
});
});
And here is the full code of the component:
import React, { PureComponent } from "react";
import { Image, Text, TouchableOpacity, View } from "react-native";
import Collapsible from "react-native-collapsible";
import PropTypes from "prop-types";
import {
SCREEN_TEXT_MENU_ITEM_DETAIL_MANDATORY,
SCREEN_TEXT_MENU_ITEM_DETAIL_OPTIONAL,
SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO,
SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST
} from "../../config/constants/screenTexts";
import styles from "./styles";
export class AccordionList extends PureComponent {
static propTypes = {
header: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
chosenItems: PropTypes.array.isRequired,
onItemPressed: PropTypes.func.isRequired,
minChoices: PropTypes.number,
maxChoices: PropTypes.number,
borderTop: PropTypes.bool,
borderBottom: PropTypes.bool
};
static defaultProps = {
minChoices: 0,
maxChoices: 1,
borderTop: false,
borderBottom: false
};
state = { collapsed: true };
toggleExpanded = () => {
this.setState(state => ({ collapsed: !state.collapsed }));
};
renderContent = () => {
const { items, onItemPressed, chosenItems, maxChoices } = this.props;
return (
<View>
{items.map(item => {
const disabled =
!chosenItems.includes(item.uuid) &&
chosenItems.filter(item => items.map(item => item.uuid).includes(item)).length ===
maxChoices;
return (
<View style={styles.itemContainer} key={item.uuid}>
<TouchableOpacity
onPress={() => onItemPressed(item.uuid)}
style={[styles.itemButton, disabled ? styles.opaque : null]}
disabled={disabled}
>
{item.name && <Text style={styles.itemText}>{item.name}</Text>}
{item.price && (
<View style={styles.priceContainer}>
<Text style={styles.sizeText}>{item.label ? `${item.label} ` : ""}</Text>
<Text style={styles.sizeText}>
{item.size ? `${item.size.size}${item.size.unit}: ` : ""}
</Text>
<Text style={styles.priceText}>{item.price} €</Text>
</View>
)}
<View style={styles.checkMarkContainer}>
{chosenItems.includes(item.uuid) ? (
<Image
source={require("../../assets/icons/checkmark.png")}
resizeMode="contain"
style={[styles.checkmarkIcon, styles.active]}
/>
) : null}
</View>
</TouchableOpacity>
</View>
);
})}
</View>
);
};
render() {
const {
header,
items,
maxChoices,
minChoices,
chosenItems,
borderTop,
borderBottom
} = this.props;
const { collapsed } = this.state;
return (
<View
style={[
styles.container,
borderTop ? styles.borderTop : null,
borderBottom ? styles.borderBottom : null
]}
>
<TouchableOpacity onPress={this.toggleExpanded} style={styles.headerButton}>
<Text style={styles.header}>{header}</Text>
<Text style={[styles.subHeader, minChoices > 0 ? styles.mandatory : styles.optional]}>
{minChoices > 0
? collapsed
? `(${SCREEN_TEXT_MENU_ITEM_DETAIL_MANDATORY}, ${minChoices})`
: chosenItems.filter(item => items.map(item => item.uuid).includes(item)).length >=
maxChoices
? `${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${maxChoices}.`
: `${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST} ${minChoices}`
: collapsed
? `(${SCREEN_TEXT_MENU_ITEM_DETAIL_OPTIONAL})`
: `${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${maxChoices}.`}
</Text>
<Image
source={
collapsed
? require("../../assets/icons/rightArrow.png")
: require("../../assets/icons/downArrow.png")
}
resizeMode="contain"
style={[
styles.arrowIcon,
chosenItems.length > 0 &&
chosenItems.some(item => items.map(item => item.uuid).includes(item))
? styles.active
: styles.inActive
]}
/>
</TouchableOpacity>
<Collapsible collapsed={collapsed}>{this.renderContent()}</Collapsible>
</View>
);
}
}
export default AccordionList;
Looks like it's just a typo, you're missing a . in the component code.
Change this line:
: `${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST} ${minChoices}`
...to this:
: `${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST} ${minChoices}.`
One other thing I noticed, your component uses a default for maxChoices of 1, but in your test there are two spots where you are referencing props.maxChoices where it hasn't been set. You'll probably want to change the two lines like this:
.contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${props.maxChoices}.`)
..to this:
.contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${props.maxChoices || 1}.`)
to reflect the default assigned by the component.
I have a component in react-native
export default class SearchResultsScreen extends Component {
constructor(props){
super(props);
this.state = {
centres: [],
};
};
componentDidMount() {
let searchUrl =`${hostname}centres_search/`;
let lat = this.props.origin_lat;
let long = this.props.origin_long;
let distance = this.props.distance;
let url = `${searchUrl}origin_lat=${lat}&origin_long=${long}&distance=${distance}`
console.log('before fetch called');
fetch(url)
.then((response) => response.json())
.then((responseJson) => {
console.log('before state updated '+responseJson[0].name);
this.setState({
centres: responseJson,
});
console.log('after fetch called '+responseJson[0].name);
})
.catch((error)=>{
console.log('error '+error);
this.setState({
centres: [],
});
});
}
static navigationOptions = {
title: 'Centers',
headerTitleStyle: navigationHeaderStyle
};
render() {
let centresList;
if(typeof this.state.centres == 'undefined' ||
this.state.centres.length == 0) {
centresList = <Text>No results found</Text>
} else {
centresList = <FlatList
data={this.state.centres}
keyExtractor={(item, index) => item._centre_id}
renderItem={({item}) => <CentreComponent Centre={item}/>}
ItemSeparatorComponent={() => <ListSeperator />}
/>
}
return(
<View>
{centresList}
<ListSeperator />
</View>
);
};
}
So basically it loads CentreComponent(s) based on the response from fetch (i.e the number of results it gets).
In my test with Jest I am trying to assert two Components of type CentreComponent exist. My test looks like this:
jest.disableAutomock();
import 'react-native';
import React from 'react';
import {shallow, mount, render} from 'enzyme';
import SearchResultsScreen from '../../src/SearchResultsScreen/index';
import CentreComponent from
'../../src/SearchResultsScreen/CentreComponent';
import renderer from 'react-test-renderer';
describe('SearchResultsScreen', () => {
test.only('renders more than one CentreComponent', () => {
jest.useFakeTimers();
global.fetch = jest.fn().mockImplementation(() => {
let kc = [{
_centre_id: 1,
name: 'test',
address_1: 'abc',
address_2: 'def',
city: 'so',
postcode: 'tt',
}, {
_centre_id: 2,
name: 'testee',
address_1: 'abc',
address_2: 'def',
city: 'so',
postcode: 'tt',
}]
console.log('mock fetch called');
return new Promise((resolve, reject) => {
process.nextTick(
() => resolve({ok: true, json: function(){return kc}})
);
});
});
const checkbox = mount(<SearchResultsScreen origin_long='5' origin_lat='2' distance='5'/>);
jest.runAllTicks();
checkbox.update();
expect(checkbox.find(KarCentreComponent)).toHaveLength(2);
});
});
When I run the test
jest.js --env=jsdom
I see the following console.log being printed in order:
console.log('before fetch called');
console.log('mock fetch called');
console.log('before state updated '+responseJson[0].name);
Error at line:
console.log('error '+error);
The error which I get is:
{ Invariant Violation: Element type is invalid: expected a string (for
built-in components) or a class/function (for composite components)
but got: undefined. You likely forgot to export your component from
the file it's defined in. Check the render method of
`SearchResultsScreen`
So my suspicion is that, when componentDidUpdate is calling
this.setState({
centres: responseJson,
});
It is throwing an exception which is being caught by the "catch".