-
Notifications
You must be signed in to change notification settings - Fork 272
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs(cookbook): network requests recipes (#1655)
* squash prev commits * remove unneeded axios mock * set maxWorkers=2 * run with a slow test reporter * revert: run with a slow test reporter * Add url check to mock and further reading and alternatives * use MSW for all API calls in cookbook test suits * Comments with implem. explanation * Arrange docs initially to reflect new scenario and remove jest.setTimeout * updating docs (1) * updating docs (2) * updating docs with global guarding and conclusion --------- Co-authored-by: stevegalili <[email protected]>
- Loading branch information
1 parent
60dd2e6
commit bd98be0
Showing
17 changed files
with
1,091 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import { Text } from 'react-native'; | ||
import { User } from './types'; | ||
import ContactsList from './components/ContactsList'; | ||
import FavoritesList from './components/FavoritesList'; | ||
import getAllContacts from './api/getAllContacts'; | ||
import getAllFavorites from './api/getAllFavorites'; | ||
|
||
export default () => { | ||
const [usersData, setUsersData] = useState<User[]>([]); | ||
const [favoritesData, setFavoritesData] = useState<User[]>([]); | ||
const [error, setError] = useState<string | null>(null); | ||
|
||
useEffect(() => { | ||
const _getAllContacts = async () => { | ||
const _data = await getAllContacts(); | ||
setUsersData(_data); | ||
}; | ||
const _getAllFavorites = async () => { | ||
const _data = await getAllFavorites(); | ||
setFavoritesData(_data); | ||
}; | ||
|
||
const run = async () => { | ||
try { | ||
await Promise.all([_getAllContacts(), _getAllFavorites()]); | ||
} catch (e) { | ||
const message = isErrorWithMessage(e) ? e.message : 'Something went wrong'; | ||
setError(message); | ||
} | ||
}; | ||
|
||
void run(); | ||
}, []); | ||
|
||
if (error) { | ||
return <Text>An error occurred: {error}</Text>; | ||
} | ||
|
||
return ( | ||
<> | ||
<FavoritesList users={favoritesData} /> | ||
<ContactsList users={usersData} /> | ||
</> | ||
); | ||
}; | ||
|
||
const isErrorWithMessage = ( | ||
e: unknown, | ||
): e is { | ||
message: string; | ||
} => typeof e === 'object' && e !== null && 'message' in e; |
40 changes: 40 additions & 0 deletions
40
examples/cookbook/app/network-requests/__tests__/PhoneBook.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native'; | ||
import React from 'react'; | ||
import PhoneBook from '../PhoneBook'; | ||
import { | ||
mockServerFailureForGetAllContacts, | ||
mockServerFailureForGetAllFavorites, | ||
} from './test-utils'; | ||
|
||
jest.setTimeout(10000); | ||
|
||
describe('PhoneBook', () => { | ||
it('fetches all contacts and favorites successfully and renders lists in sections correctly', async () => { | ||
render(<PhoneBook />); | ||
|
||
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i)); | ||
expect(await screen.findByText('Name: Mrs Ida Kristensen')).toBeOnTheScreen(); | ||
expect(await screen.findByText('Email: [email protected]')).toBeOnTheScreen(); | ||
expect(await screen.findAllByText(/name/i)).toHaveLength(3); | ||
expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen(); | ||
expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3); | ||
}); | ||
|
||
it('fails to fetch all contacts and renders error message', async () => { | ||
mockServerFailureForGetAllContacts(); | ||
render(<PhoneBook />); | ||
|
||
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i)); | ||
expect( | ||
await screen.findByText(/an error occurred: error fetching contacts/i), | ||
).toBeOnTheScreen(); | ||
}); | ||
|
||
it('fails to fetch favorites and renders error message', async () => { | ||
mockServerFailureForGetAllFavorites(); | ||
render(<PhoneBook />); | ||
|
||
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i)); | ||
expect(await screen.findByText(/error fetching favorites/i)).toBeOnTheScreen(); | ||
}); | ||
}); |
109 changes: 109 additions & 0 deletions
109
examples/cookbook/app/network-requests/__tests__/test-utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { User } from '../types'; | ||
import {http, HttpResponse} from "msw"; | ||
import {setupServer} from "msw/node"; | ||
|
||
// Define request handlers and response resolvers for random user API. | ||
// By default, we always return the happy path response. | ||
const handlers = [ | ||
http.get('https://randomuser.me/api/*', () => { | ||
return HttpResponse.json(DATA); | ||
}), | ||
]; | ||
|
||
export const server = setupServer(...handlers); | ||
|
||
export const mockServerFailureForGetAllContacts = () => { | ||
server.use( | ||
http.get('https://randomuser.me/api/', ({ request }) => { | ||
// Construct a URL instance out of the intercepted request. | ||
const url = new URL(request.url); | ||
// Read the "results" URL query parameter using the "URLSearchParams" API. | ||
const resultsLength = url.searchParams.get('results'); | ||
// Simulate a server error for the get all contacts request. | ||
// We check if the "results" query parameter is set to "25" | ||
// to know it's the correct request to mock, in our case get all contacts. | ||
if (resultsLength === '25') { | ||
return new HttpResponse(null, { status: 500 }); | ||
} | ||
|
||
return HttpResponse.json(DATA); | ||
}), | ||
); | ||
}; | ||
|
||
export const mockServerFailureForGetAllFavorites = () => { | ||
server.use( | ||
http.get('https://randomuser.me/api/', ({ request }) => { | ||
// Construct a URL instance out of the intercepted request. | ||
const url = new URL(request.url); | ||
// Read the "results" URL query parameter using the "URLSearchParams" API. | ||
const resultsLength = url.searchParams.get('results'); | ||
// Simulate a server error for the get all favorites request. | ||
// We check if the "results" query parameter is set to "10" | ||
// to know it's the correct request to mock, in our case get all favorites. | ||
if (resultsLength === '10') { | ||
return new HttpResponse(null, { status: 500 }); | ||
} | ||
|
||
return HttpResponse.json(DATA); | ||
}), | ||
); | ||
}; | ||
export const DATA: { results: User[] } = { | ||
results: [ | ||
{ | ||
name: { | ||
title: 'Mrs', | ||
first: 'Ida', | ||
last: 'Kristensen', | ||
}, | ||
email: '[email protected]', | ||
id: { | ||
name: 'CPR', | ||
value: '250562-5730', | ||
}, | ||
picture: { | ||
large: 'https://randomuser.me/api/portraits/women/26.jpg', | ||
medium: 'https://randomuser.me/api/portraits/med/women/26.jpg', | ||
thumbnail: 'https://randomuser.me/api/portraits/thumb/women/26.jpg', | ||
}, | ||
cell: '123-4567-890', | ||
}, | ||
{ | ||
name: { | ||
title: 'Mr', | ||
first: 'Elijah', | ||
last: 'Ellis', | ||
}, | ||
email: '[email protected]', | ||
id: { | ||
name: 'TFN', | ||
value: '138117486', | ||
}, | ||
picture: { | ||
large: 'https://randomuser.me/api/portraits/men/53.jpg', | ||
medium: 'https://randomuser.me/api/portraits/med/men/53.jpg', | ||
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/53.jpg', | ||
}, | ||
cell: '123-4567-890', | ||
}, | ||
{ | ||
name: { | ||
title: 'Mr', | ||
first: 'Miro', | ||
last: 'Halko', | ||
}, | ||
email: '[email protected]', | ||
id: { | ||
name: 'HETU', | ||
value: 'NaNNA945undefined', | ||
}, | ||
picture: { | ||
large: 'https://randomuser.me/api/portraits/men/17.jpg', | ||
medium: 'https://randomuser.me/api/portraits/med/men/17.jpg', | ||
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/17.jpg', | ||
}, | ||
cell: '123-4567-890', | ||
}, | ||
], | ||
}; |
10 changes: 10 additions & 0 deletions
10
examples/cookbook/app/network-requests/api/getAllContacts.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { User } from '../types'; | ||
|
||
export default async (): Promise<User[]> => { | ||
const res = await fetch('https://randomuser.me/api/?results=25'); | ||
if (!res.ok) { | ||
throw new Error(`Error fetching contacts`); | ||
} | ||
const json = await res.json(); | ||
return json.results; | ||
}; |
10 changes: 10 additions & 0 deletions
10
examples/cookbook/app/network-requests/api/getAllFavorites.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { User } from '../types'; | ||
|
||
export default async (): Promise<User[]> => { | ||
const res = await fetch('https://randomuser.me/api/?results=10'); | ||
if (!res.ok) { | ||
throw new Error(`Error fetching favorites`); | ||
} | ||
const json = await res.json(); | ||
return json.results; | ||
}; |
60 changes: 60 additions & 0 deletions
60
examples/cookbook/app/network-requests/components/ContactsList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { FlatList, Image, StyleSheet, Text, View } from 'react-native'; | ||
import React, { useCallback } from 'react'; | ||
import type { ListRenderItem } from '@react-native/virtualized-lists'; | ||
import { User } from '../types'; | ||
|
||
export default ({ users }: { users: User[] }) => { | ||
const renderItem: ListRenderItem<User> = useCallback( | ||
({ item: { name, email, picture, cell }, index }) => { | ||
const { title, first, last } = name; | ||
const backgroundColor = index % 2 === 0 ? '#f9f9f9' : '#fff'; | ||
return ( | ||
<View style={[{ backgroundColor }, styles.userContainer]}> | ||
<Image source={{ uri: picture.thumbnail }} style={styles.userImage} /> | ||
<View> | ||
<Text> | ||
Name: {title} {first} {last} | ||
</Text> | ||
<Text>Email: {email}</Text> | ||
<Text>Mobile: {cell}</Text> | ||
</View> | ||
</View> | ||
); | ||
}, | ||
[], | ||
); | ||
|
||
if (users.length === 0) return <FullScreenLoader />; | ||
|
||
return ( | ||
<View> | ||
<FlatList<User> | ||
data={users} | ||
renderItem={renderItem} | ||
keyExtractor={(item, index) => `${index}-${item.id.value}`} | ||
/> | ||
</View> | ||
); | ||
}; | ||
const FullScreenLoader = () => { | ||
return ( | ||
<View style={styles.loaderContainer}> | ||
<Text>Users data not quite there yet...</Text> | ||
</View> | ||
); | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
userContainer: { | ||
padding: 16, | ||
flexDirection: 'row', | ||
alignItems: 'center', | ||
}, | ||
userImage: { | ||
width: 50, | ||
height: 50, | ||
borderRadius: 24, | ||
marginRight: 16, | ||
}, | ||
loaderContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' }, | ||
}); |
59 changes: 59 additions & 0 deletions
59
examples/cookbook/app/network-requests/components/FavoritesList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { FlatList, Image, StyleSheet, Text, View } from 'react-native'; | ||
import React, { useCallback } from 'react'; | ||
import type { ListRenderItem } from '@react-native/virtualized-lists'; | ||
import { User } from '../types'; | ||
|
||
export default ({ users }: { users: User[] }) => { | ||
const renderItem: ListRenderItem<User> = useCallback(({ item: { picture } }) => { | ||
return ( | ||
<View style={styles.userContainer}> | ||
<Image | ||
source={{ uri: picture.thumbnail }} | ||
style={styles.userImage} | ||
accessibilityLabel={'favorite-contact-avatar'} | ||
/> | ||
</View> | ||
); | ||
}, []); | ||
|
||
if (users.length === 0) return <FullScreenLoader />; | ||
|
||
return ( | ||
<View style={styles.outerContainer}> | ||
<Text>⭐My Favorites</Text> | ||
<FlatList<User> | ||
horizontal | ||
showsHorizontalScrollIndicator={false} | ||
data={users} | ||
renderItem={renderItem} | ||
keyExtractor={(item, index) => `${index}-${item.id.value}`} | ||
/> | ||
</View> | ||
); | ||
}; | ||
const FullScreenLoader = () => { | ||
return ( | ||
<View style={styles.loaderContainer}> | ||
<Text>Figuring out your favorites...</Text> | ||
</View> | ||
); | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
outerContainer: { | ||
padding: 8, | ||
}, | ||
userContainer: { | ||
padding: 8, | ||
flexDirection: 'row', | ||
alignItems: 'center', | ||
}, | ||
userImage: { | ||
width: 52, | ||
height: 52, | ||
borderRadius: 36, | ||
borderColor: '#9b6dff', | ||
borderWidth: 2, | ||
}, | ||
loaderContainer: { height: 52, justifyContent: 'center', alignItems: 'center' }, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import * as React from 'react'; | ||
import PhoneBook from './PhoneBook'; | ||
|
||
export default function Example() { | ||
return <PhoneBook />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export type User = { | ||
name: { | ||
title: string; | ||
first: string; | ||
last: string; | ||
}; | ||
email: string; | ||
id: { | ||
name: string; | ||
value: string; | ||
}; | ||
picture: { | ||
large: string; | ||
medium: string; | ||
thumbnail: string; | ||
}; | ||
cell: string; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.