25
loading...
This website collects cookies to deliver better user experience
yarn add @testing-library/react-native
jest
configuration:"transformIgnorePatterns": [
"node_modules/(?!@react-native|react-native)"
],
"setupFiles": [
"./node_modules/react-native-gesture-handler/jestSetup.js"
],
testID
property to each RectButton
at src/common/components/CharacterCard.tsx:import React from 'react';
import {Image, StyleSheet, Text, View} from 'react-native';
import {RectButton} from 'react-native-gesture-handler';
import Icon from 'react-native-vector-icons/Entypo';
import {useUpdateChosenQuantity} from '../hooks/use-update-chosen-quantity';
interface Props {
data: {
id?: string | null;
image?: string | null;
name?: string | null;
unitPrice?: number;
chosenQuantity?: number;
};
}
const CharacterCard: React.FC<Props> = ({data}) => {
const {onIncreaseChosenQuantity, onDecreaseChosenQuantity} =
useUpdateChosenQuantity();
return (
<View style={styles.container}>
{data.image && <Image source={{uri: data.image}} style={styles.image} />}
<View style={styles.details}>
<Text style={styles.text}>{data.name}</Text>
<Text style={styles.text}>{`U$ ${data.unitPrice}`}</Text>
</View>
<View style={styles.choseQuantityContainer}>
<RectButton
testID="charactter-remove-btn"
onPress={onDecreaseChosenQuantity.bind(null, data.id as string)}>
<Icon name="minus" size={24} color="#3D7199" />
</RectButton>
<Text style={styles.choseQuantityText}>{data.chosenQuantity}</Text>
<RectButton
testID="charactter-add-btn"
onPress={onIncreaseChosenQuantity.bind(null, data.id as string)}>
<Icon name="plus" size={24} color="#3D7199" />
</RectButton>
</View>
</View>
);
};
export default CharacterCard;
const styles = StyleSheet.create({
container: {
width: '100%',
borderRadius: 20,
marginVertical: 8,
paddingHorizontal: 8,
paddingVertical: 24,
backgroundColor: '#F0F0F0',
flexDirection: 'row',
},
image: {width: 70, height: 70},
details: {
marginLeft: 8,
justifyContent: 'space-between',
flex: 1,
},
text: {
fontSize: 16,
fontWeight: 'bold',
},
choseQuantityContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: 'row',
},
choseQuantityText: {
padding: 8,
borderRadius: 8,
backgroundColor: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});
import {fireEvent, render} from '@testing-library/react-native';
import React from 'react';
import * as useUpdateChosenQuantityModule from '../src/common/hooks/use-update-chosen-quantity';
import CharacterCard from '../src/common/components/CharacterCard';
describe('CharacterCard component', () => {
beforeEach(() => {
jest
.spyOn(useUpdateChosenQuantityModule, 'useUpdateChosenQuantity')
.mockReturnValue({
onIncreaseChosenQuantity: jest.fn(),
onDecreaseChosenQuantity: jest.fn(),
});
});
it('should render', () => {
const wrapper = render(<CharacterCard data={mockData} />);
expect(wrapper).toBeTruthy();
});
it('should call onIncreaseChosenQuantity and onDecreaseChosenQuantity on press', () => {
const mockOnIncreaseChosenQuantity = jest.fn();
const mockOnDecreaseChosenQuantity = jest.fn();
jest
.spyOn(useUpdateChosenQuantityModule, 'useUpdateChosenQuantity')
.mockReturnValue({
onIncreaseChosenQuantity: mockOnIncreaseChosenQuantity,
onDecreaseChosenQuantity: mockOnDecreaseChosenQuantity,
});
const wrapper = render(<CharacterCard data={mockData} />);
fireEvent.press(wrapper.getByTestId('charactter-remove-btn'));
fireEvent.press(wrapper.getByTestId('charactter-add-btn'));
expect(mockOnIncreaseChosenQuantity).toBeCalled();
expect(mockOnDecreaseChosenQuantity).toBeCalled();
});
});
const mockData = {
id: 'any_id',
image: 'any_image',
name: 'any_name',
unitPrice: 10,
chosenQuantity: 0,
};
testID
at scr/screens/Home.tsx to help us:import React from 'react';
import { ActivityIndicator, FlatList, StyleSheet, View } from 'react-native';
import { Character, useGetCharactersQuery } from '../common/generated/graphql';
import CharacterCard from '../common/components/CharacterCard';
const Home = () => {
const { data, loading } = useGetCharactersQuery();
if (loading) {
return (
<View testID="progress" style={styles.container}>
<ActivityIndicator color="#32B768" size="large" />
</View>
);
}
return (
<View style={styles.container} testID="container">
<FlatList
data={data?.characters?.results}
renderItem={({ item }) => <CharacterCard data={item as Character} />}
contentContainerStyle={styles.characterList}
/>
</View>
);
};
export default Home;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
characterList: {
padding: 16,
},
});
import {render, waitFor} from '@testing-library/react-native';
import React from 'react';
import {MockedProvider} from '@apollo/client/testing';
import {GetCharactersDocument} from '../src/common/generated/graphql';
import Home from '../src/screens/Home';
describe('Home component', () => {
it('should render and show progress on loading', () => {
const wrapper = render(
<MockedProvider addTypename={false} mocks={[mock]}>
<Home />
</MockedProvider>,
);
expect(wrapper.queryByTestId('progress')).toBeTruthy();
});
it('should render the flatlist when whe loading is false', async () => {
const wrapper = render(
<MockedProvider addTypename={false} mocks={[mock]}>
<Home />
</MockedProvider>,
);
await waitFor(() => [
expect(wrapper.queryByTestId('progress')).toBeFalsy(),
]);
expect(wrapper.queryByTestId('container')?.children.length).toBe(1);
});
});
const mock = {
request: {
query: GetCharactersDocument,
},
result: {
data: {
characters: {
__typename: 'Characters',
results: [
{
id: '1',
__typename: 'Character',
name: 'Rick Sanchez',
image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg',
species: 'Human',
unitPrice: 1,
chosenQuantity: 10,
origin: {
id: '1',
__typename: 'Location',
name: 'Earth (C-137)',
},
location: {
id: '20',
__typename: 'Location',
name: 'Earth (Replacement Dimension)',
},
},
],
},
},
},
};
MockedProvider
to, first, ensure that at loading we render the ActivityIndicator
. Then, we ensure that when the data is available, we render the view properly with only one character (since the data array has only one entry). testID
at scr/screens/Cart.tsx:import React, { useCallback } from 'react';
import { useNavigation } from '@react-navigation/native';
import { StyleSheet, Text, View, SafeAreaView, Button } from 'react-native';
import { useGetShoppingCartQuery } from '../common/generated/graphql';
const Cart = () => {
const navigation = useNavigation();
const { data } = useGetShoppingCartQuery();
const handleNavigation = useCallback(() => {
navigation.navigate('Home');
}, [navigation]);
return (
<SafeAreaView style={styles.container}>
{data?.shoppingCart?.numActionFigures ? (
<>
<View style={styles.content} testID="fulfilled-cart">
<Text style={styles.emoji}>🤗</Text>
<Text
style={
styles.subtitle
}>{`Total number of items: ${data?.shoppingCart.numActionFigures}`}</Text>
<Text
style={
styles.subtitle
}>{`Total price: U$ ${data?.shoppingCart.totalPrice}`}</Text>
</View>
</>
) : (
<>
<View style={styles.content} testID="empty-cart">
<Text style={styles.emoji}>😢</Text>
<Text style={styles.title}>Empty cart!</Text>
<View style={styles.footer}>
<Button title="Go back to shop" onPress={handleNavigation} />
</View>
</View>
</>
)}
</SafeAreaView>
);
};
export default Cart;
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
content: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
width: '100%',
},
title: {
fontSize: 24,
marginTop: 15,
lineHeight: 32,
textAlign: 'center',
},
subtitle: {
fontSize: 16,
lineHeight: 32,
marginTop: 8,
textAlign: 'center',
paddingHorizontal: 20,
},
emoji: {
fontSize: 44,
textAlign: 'center',
},
footer: {
width: '100%',
paddingHorizontal: 20,
},
});
import {fireEvent, render, waitFor} from '@testing-library/react-native';
import React from 'react';
import {MockedProvider} from '@apollo/client/testing';
import Cart from '../src/screens/Cart';
const mockedNavigate = jest.fn();
jest.mock('@react-navigation/native', () => ({
useNavigation: () => ({navigate: mockedNavigate}),
}));
describe('Cart component', () => {
it('should render with empty cart and navigate on click', async () => {
const resolvers = {
Query: {
shoppingCart: jest.fn().mockReturnValue(null),
},
};
const wrapper = render(
<MockedProvider resolvers={resolvers} addTypename={false} mocks={[]}>
<Cart />
</MockedProvider>,
);
await waitFor(() => [
expect(wrapper.queryByTestId('empty-cart')).toBeTruthy(),
expect(wrapper.queryByTestId('fulfilled-cart')).toBeFalsy(),
]);
fireEvent.press(wrapper.getByRole('button'));
expect(mockedNavigate).toHaveBeenCalledWith('Home');
});
it('should render when the shoppingCart cart is fulfilled', async () => {
const resolvers = {
Query: {
shoppingCart: jest.fn().mockReturnValue({
id: 'any_id',
totalPrice: 10,
numActionFigures: 10,
}),
},
};
const wrapper = render(
<MockedProvider resolvers={resolvers} addTypename={false} mocks={[]}>
<Cart />
</MockedProvider>,
);
await waitFor(() => [
expect(wrapper.queryByTestId('empty-cart')).toBeFalsy(),
expect(wrapper.queryByTestId('fulfilled-cart')).toBeTruthy(),
]);
});
});
use-update-chosen-quantity.ts
we write:import {fireEvent, render} from '@testing-library/react-native';
import React from 'react';
import {MockedProvider} from '@apollo/client/testing';
import {ApolloClient, gql, InMemoryCache} from '@apollo/client';
import {useUpdateChosenQuantity} from '../src/common/hooks/use-update-chosen-quantity';
import {Button} from 'react-native';
import {
CharacterDataFragment,
CharacterDataFragmentDoc,
GetShoppingCartDocument,
GetShoppingCartQuery,
} from '../src/common/generated/graphql';
describe('useUpdateChosenQuantity hook', () => {
let cache: InMemoryCache;
let client: ApolloClient<any>;
beforeEach(() => {
cache = new InMemoryCache();
cache.writeQuery({
query: mockCharactersQuery,
data: mockCharactersData,
});
client = new ApolloClient({
cache,
});
});
it('should increase the quantity correctly', async () => {
const MockComponent = () => {
const {onIncreaseChosenQuantity} = useUpdateChosenQuantity();
return (
<Button
title="any_title"
onPress={() => onIncreaseChosenQuantity('1')}
/>
);
};
const wrapper = render(
<MockedProvider cache={cache}>
<MockComponent />
</MockedProvider>,
);
fireEvent.press(wrapper!.getByRole('button')!);
fireEvent.press(wrapper!.getByRole('button')!);
const shoopingCart = client.readQuery<GetShoppingCartQuery>({
query: GetShoppingCartDocument,
});
const character = client.readFragment<CharacterDataFragment>({
fragment: CharacterDataFragmentDoc,
id: 'Character:1',
});
expect(shoopingCart?.shoppingCart?.numActionFigures).toBe(2);
expect(shoopingCart?.shoppingCart?.totalPrice).toBe(20);
expect(character?.chosenQuantity).toBe(2);
});
it('should decrease the quantity correctly', async () => {
cache.writeQuery({
query: mockShoppingCartQuery,
data: mockShoppinData,
});
client = new ApolloClient({
cache,
});
const MockComponent = () => {
const {onDecreaseChosenQuantity} = useUpdateChosenQuantity();
return (
<Button
title="any_title"
onPress={() => onDecreaseChosenQuantity('2')}
/>
);
};
const wrapper = render(
<MockedProvider cache={cache}>
<MockComponent />
</MockedProvider>,
);
fireEvent.press(wrapper!.getByRole('button')!);
fireEvent.press(wrapper!.getByRole('button')!);
fireEvent.press(wrapper!.getByRole('button')!);
fireEvent.press(wrapper!.getByRole('button')!);
fireEvent.press(wrapper!.getByRole('button')!);
const shoopingCart = client.readQuery<GetShoppingCartQuery>({
query: GetShoppingCartDocument,
});
const character = client.readFragment<CharacterDataFragment>({
fragment: CharacterDataFragmentDoc,
id: 'Character:2',
});
expect(shoopingCart?.shoppingCart?.numActionFigures).toBe(0);
expect(shoopingCart?.shoppingCart?.totalPrice).toBe(0);
expect(character?.chosenQuantity).toBe(0);
});
});
const mockCharactersQuery = gql`
fragment characterData on Character {
id
__typename
name
unitPrice @client
chosenQuantity @client
}
query GetCharacters {
characters {
__typename
results {
...characterData
image
species
origin {
id
__typename
name
}
location {
id
__typename
name
}
}
}
}
`;
const mockCharactersData = {
characters: {
__typename: 'Characters',
results: [
{
id: '1',
__typename: 'Character',
name: 'Rick Sanchez',
image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg',
species: 'Human',
unitPrice: 10,
chosenQuantity: 0,
origin: {
id: '1',
__typename: 'Location',
name: 'Earth (C-137)',
},
location: {
id: '20',
__typename: 'Location',
name: 'Earth (Replacement Dimension)',
},
},
{
id: '2',
__typename: 'Character',
name: 'Rick Sanchez',
image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg',
species: 'Human',
unitPrice: 10,
chosenQuantity: 1,
origin: {
id: '1',
__typename: 'Location',
name: 'Earth (C-137)',
},
location: {
id: '20',
__typename: 'Location',
name: 'Earth (Replacement Dimension)',
},
},
],
},
};
const mockShoppingCartQuery = gql`
query GetShoppingCart {
shoppingCart @client {
id
totalPrice
numActionFigures
}
}
`;
const mockShoppinData = {
shoppingCart: {
id: 'ShoppingCart:1',
totalPrice: 10,
numActionFigures: 1,
},
};
yarn test
all tests should pass!. Again, I'll be happy if you could provide me any feedback about the code, structure, doubt or anything that could make me a better developer!