Skip to content

Commit 19dd3bc

Browse files
author
stevegalili
committed
squash prev commits
1 parent 239e046 commit 19dd3bc

File tree

18 files changed

+882
-8
lines changed

18 files changed

+882
-8
lines changed

examples/cookbook/__mocks__/axios.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const chuckNorrisError = () => {
2+
throw Error(
3+
"Please ensure you mock 'Axios' - Only Chuck Norris is allowed to make API requests when testing ;)",
4+
);
5+
};
6+
7+
export default {
8+
get: jest.fn(chuckNorrisError),
9+
post: jest.fn(chuckNorrisError),
10+
put: jest.fn(chuckNorrisError),
11+
delete: jest.fn(chuckNorrisError),
12+
request: jest.fn(chuckNorrisError),
13+
};

examples/cookbook/app/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type Recipe = {
8282
};
8383

8484
const recipes: Recipe[] = [
85-
{ id: 2, title: 'Welcome Screen with Custom Render', path: 'custom-render/' },
86-
{ id: 1, title: 'Task List with Jotai', path: 'jotai/' },
85+
{ id: 1, title: 'Welcome Screen with Custom Render', path: 'custom-render/' },
86+
{ id: 2, title: 'Task List with Jotai', path: 'state-management/jotai/' },
87+
{ id: 3, title: 'Phone book with\na Variety of Net. Req. Methods', path: 'network-requests/' },
8788
];
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { Text } from 'react-native';
3+
import { User } from './types';
4+
import ContactsList from './components/ContactsList';
5+
import FavoritesList from './components/FavoritesList';
6+
import getAllContacts from './api/getAllContacts';
7+
import getAllFavorites from './api/getAllFavorites';
8+
9+
export default () => {
10+
const [usersData, setUsersData] = useState<User[]>([]);
11+
const [favoritesData, setFavoritesData] = useState<User[]>([]);
12+
const [error, setError] = useState<string | null>(null);
13+
14+
useEffect(() => {
15+
const _getAllContacts = async () => {
16+
const _data = await getAllContacts();
17+
setUsersData(_data);
18+
};
19+
const _getAllFavorites = async () => {
20+
const _data = await getAllFavorites();
21+
setFavoritesData(_data);
22+
};
23+
24+
const run = async () => {
25+
try {
26+
await Promise.all([_getAllContacts(), _getAllFavorites()]);
27+
} catch (e) {
28+
const message = isErrorWithMessage(e) ? e.message : 'Something went wrong';
29+
setError(message);
30+
}
31+
};
32+
33+
void run();
34+
}, []);
35+
36+
if (error) {
37+
return <Text>An error occurred: {error}</Text>;
38+
}
39+
40+
return (
41+
<>
42+
<FavoritesList users={favoritesData} />
43+
<ContactsList users={usersData} />
44+
</>
45+
);
46+
};
47+
48+
const isErrorWithMessage = (
49+
e: unknown,
50+
): e is {
51+
message: string;
52+
} => typeof e === 'object' && e !== null && 'message' in e;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
2+
import React from 'react';
3+
import axios from 'axios';
4+
import PhoneBook from '../PhoneBook';
5+
import { User } from '../types';
6+
7+
jest.mock('axios');
8+
9+
jest.setTimeout(10000);
10+
describe('PhoneBook', () => {
11+
it('fetches contacts successfully and renders in list', async () => {
12+
(global.fetch as jest.Mock).mockResolvedValueOnce({
13+
ok: true,
14+
json: jest.fn().mockResolvedValueOnce(DATA),
15+
});
16+
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
17+
render(<PhoneBook />);
18+
19+
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
20+
expect(await screen.findByText('Name: Mrs Ida Kristensen')).toBeOnTheScreen();
21+
expect(await screen.findByText('Email: [email protected]')).toBeOnTheScreen();
22+
expect(await screen.findAllByText(/name/i)).toHaveLength(3);
23+
});
24+
25+
it('fails to fetch contacts and renders error message', async () => {
26+
(global.fetch as jest.Mock).mockResolvedValueOnce({
27+
ok: false,
28+
});
29+
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
30+
render(<PhoneBook />);
31+
32+
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
33+
expect(await screen.findByText(/error fetching contacts/i)).toBeOnTheScreen();
34+
});
35+
36+
it('fetches favorites successfully and renders all users avatars', async () => {
37+
(global.fetch as jest.Mock).mockResolvedValueOnce({
38+
ok: true,
39+
json: jest.fn().mockResolvedValueOnce(DATA),
40+
});
41+
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
42+
render(<PhoneBook />);
43+
44+
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));
45+
expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen();
46+
expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3);
47+
});
48+
49+
it('fails to fetch favorites and renders error message', async () => {
50+
(global.fetch as jest.Mock).mockResolvedValueOnce({
51+
ok: true,
52+
json: jest.fn().mockResolvedValueOnce(DATA),
53+
});
54+
(axios.get as jest.Mock).mockRejectedValueOnce({ message: 'Error fetching favorites' });
55+
render(<PhoneBook />);
56+
57+
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));
58+
expect(await screen.findByText(/error fetching favorites/i)).toBeOnTheScreen();
59+
});
60+
});
61+
62+
const DATA: { results: User[] } = {
63+
results: [
64+
{
65+
name: {
66+
title: 'Mrs',
67+
first: 'Ida',
68+
last: 'Kristensen',
69+
},
70+
71+
id: {
72+
name: 'CPR',
73+
value: '250562-5730',
74+
},
75+
picture: {
76+
large: 'https://randomuser.me/api/portraits/women/26.jpg',
77+
medium: 'https://randomuser.me/api/portraits/med/women/26.jpg',
78+
thumbnail: 'https://randomuser.me/api/portraits/thumb/women/26.jpg',
79+
},
80+
cell: '123-4567-890',
81+
},
82+
{
83+
name: {
84+
title: 'Mr',
85+
first: 'Elijah',
86+
last: 'Ellis',
87+
},
88+
89+
id: {
90+
name: 'TFN',
91+
value: '138117486',
92+
},
93+
picture: {
94+
large: 'https://randomuser.me/api/portraits/men/53.jpg',
95+
medium: 'https://randomuser.me/api/portraits/med/men/53.jpg',
96+
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/53.jpg',
97+
},
98+
cell: '123-4567-890',
99+
},
100+
{
101+
name: {
102+
title: 'Mr',
103+
first: 'Miro',
104+
last: 'Halko',
105+
},
106+
107+
id: {
108+
name: 'HETU',
109+
value: 'NaNNA945undefined',
110+
},
111+
picture: {
112+
large: 'https://randomuser.me/api/portraits/men/17.jpg',
113+
medium: 'https://randomuser.me/api/portraits/med/men/17.jpg',
114+
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/17.jpg',
115+
},
116+
cell: '123-4567-890',
117+
},
118+
],
119+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { User } from '../types';
2+
3+
export default async (): Promise<User[]> => {
4+
const res = await fetch('https://randomuser.me/api/?results=25');
5+
if (!res.ok) {
6+
throw new Error(`Error fetching contacts`);
7+
}
8+
const json = await res.json();
9+
return json.results;
10+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import axios from 'axios';
2+
import { User } from '../types';
3+
4+
export default async (): Promise<User[]> => {
5+
const res = await axios.get('https://randomuser.me/api/?results=10');
6+
return res.data.results;
7+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { FlatList, Image, StyleSheet, Text, View } from 'react-native';
2+
import React, { useCallback } from 'react';
3+
import type { ListRenderItem } from '@react-native/virtualized-lists';
4+
import { User } from '../types';
5+
6+
export default ({ users }: { users: User[] }) => {
7+
const renderItem: ListRenderItem<User> = useCallback(
8+
({ item: { name, email, picture, cell }, index }) => {
9+
const { title, first, last } = name;
10+
const backgroundColor = index % 2 === 0 ? '#f9f9f9' : '#fff';
11+
return (
12+
<View style={[{ backgroundColor }, styles.userContainer]}>
13+
<Image source={{ uri: picture.thumbnail }} style={styles.userImage} />
14+
<View>
15+
<Text>
16+
Name: {title} {first} {last}
17+
</Text>
18+
<Text>Email: {email}</Text>
19+
<Text>Mobile: {cell}</Text>
20+
</View>
21+
</View>
22+
);
23+
},
24+
[],
25+
);
26+
27+
if (users.length === 0) return <FullScreenLoader />;
28+
29+
return (
30+
<View>
31+
<FlatList<User>
32+
data={users}
33+
renderItem={renderItem}
34+
keyExtractor={(item, index) => `${index}-${item.id.value}`}
35+
/>
36+
</View>
37+
);
38+
};
39+
const FullScreenLoader = () => {
40+
return (
41+
<View style={styles.loaderContainer}>
42+
<Text>Users data not quite there yet...</Text>
43+
</View>
44+
);
45+
};
46+
47+
const styles = StyleSheet.create({
48+
userContainer: {
49+
padding: 16,
50+
flexDirection: 'row',
51+
alignItems: 'center',
52+
},
53+
userImage: {
54+
width: 50,
55+
height: 50,
56+
borderRadius: 24,
57+
marginRight: 16,
58+
},
59+
loaderContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
60+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { FlatList, Image, StyleSheet, Text, View } from 'react-native';
2+
import React, { useCallback } from 'react';
3+
import type { ListRenderItem } from '@react-native/virtualized-lists';
4+
import { User } from '../types';
5+
6+
export default ({ users }: { users: User[] }) => {
7+
const renderItem: ListRenderItem<User> = useCallback(({ item: { picture } }) => {
8+
return (
9+
<View style={styles.userContainer}>
10+
<Image
11+
source={{ uri: picture.thumbnail }}
12+
style={styles.userImage}
13+
accessibilityLabel={'favorite-contact-avatar'}
14+
/>
15+
</View>
16+
);
17+
}, []);
18+
19+
if (users.length === 0) return <FullScreenLoader />;
20+
21+
return (
22+
<View style={styles.outerContainer}>
23+
<Text>⭐My Favorites</Text>
24+
<FlatList<User>
25+
horizontal
26+
showsHorizontalScrollIndicator={false}
27+
data={users}
28+
renderItem={renderItem}
29+
keyExtractor={(item, index) => `${index}-${item.id.value}`}
30+
/>
31+
</View>
32+
);
33+
};
34+
const FullScreenLoader = () => {
35+
return (
36+
<View style={styles.loaderContainer}>
37+
<Text>Figuring out your favorites...</Text>
38+
</View>
39+
);
40+
};
41+
42+
const styles = StyleSheet.create({
43+
outerContainer: {
44+
padding: 8,
45+
},
46+
userContainer: {
47+
padding: 8,
48+
flexDirection: 'row',
49+
alignItems: 'center',
50+
},
51+
userImage: {
52+
width: 52,
53+
height: 52,
54+
borderRadius: 36,
55+
borderColor: '#9b6dff',
56+
borderWidth: 2,
57+
},
58+
loaderContainer: { height: 52, justifyContent: 'center', alignItems: 'center' },
59+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as React from 'react';
2+
import PhoneBook from './PhoneBook';
3+
4+
export default function Example() {
5+
return <PhoneBook />;
6+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export type User = {
2+
name: {
3+
title: string;
4+
first: string;
5+
last: string;
6+
};
7+
email: string;
8+
id: {
9+
name: string;
10+
value: string;
11+
};
12+
picture: {
13+
large: string;
14+
medium: string;
15+
thumbnail: string;
16+
};
17+
cell: string;
18+
};

0 commit comments

Comments
 (0)