Merge branch 'main' into poc/custom-renderer

# Conflicts:
#	package.json
#	src/__tests__/__snapshots__/render-debug.test.tsx.snap
#	yarn.lock
This commit is contained in:
Maciej Jastrzębski
2024-10-18 15:54:16 +02:00
30 changed files with 2414 additions and 1829 deletions
+3
View File
@@ -1,3 +1,6 @@
flow-typed/
build/
experiments-rtl/
website/
jest-setup.ts
-1
View File
@@ -1,7 +1,6 @@
{
"extends": "@callstack",
"rules": {
"flowtype/no-weak-types": 0,
"react-native/no-raw-text": 0,
"no-console": 1,
"react/no-multi-comp": 0,
+3 -3
View File
@@ -32,9 +32,9 @@ Our pre-commit hooks verify that your commit message matches this format when co
### Linting and tests
We use `flow` for type checking, `eslint` with `prettier` for linting and formatting the code, and `jest` for testing. Our pre-commit hooks verify that the linter and tests pass when committing. You can also run the following commands manually:
We use TypeScript for type checking, `eslint` with `prettier` for linting and formatting the code, and `jest` for testing. Our pre-commit hooks verify that the linter and tests pass when committing. You can also run the following commands manually:
- `yarn flow`: run flow on all files.
- `yarn typecheck`: run TypeScript compiler on all files.
- `yarn lint`: run eslint and prettier.
- `yarn test`: run tests.
@@ -43,7 +43,7 @@ We use `flow` for type checking, `eslint` with `prettier` for linting and format
When you're sending a pull request:
- Prefer small pull requests focused on one change.
- Verify that `flow`, `eslint` and tests are passing.
- Verify that `typecheck`, `eslint` and tests are passing.
- Preview the documentation to make sure it looks good.
- Follow the pull request template when opening a pull request.
+1 -2
View File
@@ -14,8 +14,7 @@ module.exports = {
],
env: {
test: {
// https://github.com/react-native-community/upgrade-support/issues/152
plugins: ['@babel/plugin-transform-flow-strip-types'],
presets: ['@react-native/babel-preset'],
},
},
};
+23 -3
View File
@@ -1,10 +1,30 @@
# RNTL Cookbook
<p align="center">
<img alt="banner" src="assets/readme/banner.png" />
</p>
This example app gathers recipes from
the [RNTL Cookbook](https://callstack.github.io/react-native-testing-library/cookbook).
# React Native Testing Library Cookbook App
Welcome to the React Native Testing Library (RNTL) Cookbook! This app is designed to provide developers with a collection of best practices, ready-made recipes, and tips & tricks to help you effectively test your React Native applications. Whether you’re just starting out with testing or looking to deepen your skills, this cookbook offers something for everyone.
Each recipe described in the Cookbook should have a corresponding code example screen in this repo.
Note:
Since examples will showcase usage of different dependencies, the dependencies in `package.json`
file will grow much larger that in a normal React Native. This is fine 🐶☕️🔥.
## Running the App
1. Clone the repo `git clone git@github.com:callstack/react-native-testing-library.git`
2. Go to the `examples/cookbook` directory `cd examples/cookbook`
3. Install dependencies `yarn`
4. Run the app `yarn start`
5. Run the app either on iOS or Android by clicking on `i` or `a` in the terminal.
## How to Contribute
We invite all developers, from beginners to experts, to contribute your own recipes! If you have a clever solution, best practice, or useful tip, we encourage you to:
1. Submit a Pull Request with your recipe.
2. Join the conversation on GitHub [here](https://github.com/callstack/react-native-testing-library/issues/1624) to discuss ideas, ask questions, or provide feedback.
## Screenshots From the App
| Home Screen | Phonebook with Net. Req. Example |
|-------------------------------------------------------|-----------------------------------------------------------------|
| ![home-screenshot](assets/readme/home-screenshot.png) | ![phonebook-screenshot](assets/readme/phonebook-screenshot.png) |
+3 -2
View File
@@ -82,6 +82,7 @@ type Recipe = {
};
const recipes: Recipe[] = [
{ id: 2, title: 'Welcome Screen with Custom Render', path: 'custom-render/' },
{ id: 1, title: 'Task List with Jotai', path: 'jotai/' },
{ id: 1, title: 'Welcome Screen with Custom Render', path: 'custom-render/' },
{ id: 2, title: 'Task List with Jotai', path: 'state-management/jotai/' },
{ id: 3, title: 'Phone book with\na Variety of Net. Req. Methods', path: 'network-requests/' },
];
@@ -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;
@@ -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: ida.kristensen@example.com')).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();
});
});
@@ -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: 'ida.kristensen@example.com',
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: 'elijah.ellis@example.com',
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: 'miro.halko@example.com',
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',
},
],
};
@@ -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;
};
@@ -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;
};
@@ -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' },
});
@@ -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' },
});
@@ -0,0 +1,6 @@
import * as React from 'react';
import PhoneBook from './PhoneBook';
export default function Example() {
return <PhoneBook />;
}
@@ -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;
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

+8
View File
@@ -2,6 +2,14 @@
// Import built-in Jest matchers
import '@testing-library/react-native/extend-expect';
import { server } from './app/network-requests/__tests__/test-utils';
// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
// Enable API mocking via Mock Service Worker (MSW)
beforeAll(() => server.listen());
// Reset any runtime request handlers we may add during the tests
afterEach(() => server.resetHandlers());
// Disable API mocking after the tests are done
afterAll(() => server.close());
+5 -4
View File
@@ -11,10 +11,10 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"expo": "^51.0.26",
"expo": "~51.0.31",
"expo-constants": "~16.0.2",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.21",
"expo-router": "~3.5.23",
"expo-splash-screen": "~0.27.5",
"expo-status-bar": "~1.12.1",
"jotai": "^2.8.4",
@@ -29,14 +29,15 @@
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@expo/metro-runtime": "~3.2.1",
"@testing-library/react-native": "^12.7.1",
"@expo/metro-runtime": "~3.2.3",
"@testing-library/react-native": "^12.7.2",
"@types/eslint": "^8.56.10",
"@types/jest": "^29.5.12",
"@types/react": "~18.2.45",
"@types/react-native-get-random-values": "^1",
"eslint": "^8.57.0",
"jest": "^29.7.0",
"msw": "^2.4.4",
"react-test-renderer": "18.2.0",
"typescript": "~5.3.3"
},
+371 -50
View File
@@ -1137,6 +1137,34 @@ __metadata:
languageName: node
linkType: hard
"@bundled-es-modules/cookie@npm:^2.0.0":
version: 2.0.0
resolution: "@bundled-es-modules/cookie@npm:2.0.0"
dependencies:
cookie: "npm:^0.5.0"
checksum: 10c0/0655dd331b35d7b5b6dd2301c3bcfb7233018c0e3235a40ced1d53f00463ab92dc01f0091f153812867bc0ef0f8e0a157a30acb16e8d7ef149702bf8db9fe7a6
languageName: node
linkType: hard
"@bundled-es-modules/statuses@npm:^1.0.1":
version: 1.0.1
resolution: "@bundled-es-modules/statuses@npm:1.0.1"
dependencies:
statuses: "npm:^2.0.1"
checksum: 10c0/c1a8ede3efa8da61ccda4b98e773582a9733edfbeeee569d4630785f8e018766202edb190a754a3ec7a7f6bd738e857829affc2fdb676b6dab4db1bb44e62785
languageName: node
linkType: hard
"@bundled-es-modules/tough-cookie@npm:^0.1.6":
version: 0.1.6
resolution: "@bundled-es-modules/tough-cookie@npm:0.1.6"
dependencies:
"@types/tough-cookie": "npm:^4.0.5"
tough-cookie: "npm:^4.1.4"
checksum: 10c0/28bcac878bff6b34719ba3aa8341e9924772ee55de5487680ebe784981ec9fccb70ed5d46f563e2404855a04de606f9e56aa4202842d4f5835bc04a4fe820571
languageName: node
linkType: hard
"@eslint-community/eslint-utils@npm:^4.2.0":
version: 4.4.0
resolution: "@eslint-community/eslint-utils@npm:4.4.0"
@@ -1188,9 +1216,9 @@ __metadata:
languageName: node
linkType: hard
"@expo/cli@npm:0.18.28":
version: 0.18.28
resolution: "@expo/cli@npm:0.18.28"
"@expo/cli@npm:0.18.29":
version: 0.18.29
resolution: "@expo/cli@npm:0.18.29"
dependencies:
"@babel/runtime": "npm:^7.20.0"
"@expo/code-signing-certificates": "npm:0.0.5"
@@ -1271,7 +1299,7 @@ __metadata:
ws: "npm:^8.12.1"
bin:
expo-internal: build/bin/cli
checksum: 10c0/fa956b1ddd2a61b62972058cc28a025a1d032effdb1b177fd27651437c368fa1ff8d93da29ea33f78735fbbc064da82d4111a9e09b4fcc3c496ca305f659229e
checksum: 10c0/6e9e86f37b84da600db01cdd554cd76ea6a94a50103fa54731d441dfb1b3a81ef25760c14da99cf6590588e001f5abbacec1b43b3414b61f862b491c12cf2568
languageName: node
linkType: hard
@@ -1422,12 +1450,12 @@ __metadata:
languageName: node
linkType: hard
"@expo/metro-runtime@npm:3.2.1, @expo/metro-runtime@npm:~3.2.1":
version: 3.2.1
resolution: "@expo/metro-runtime@npm:3.2.1"
"@expo/metro-runtime@npm:3.2.3, @expo/metro-runtime@npm:~3.2.3":
version: 3.2.3
resolution: "@expo/metro-runtime@npm:3.2.3"
peerDependencies:
react-native: "*"
checksum: 10c0/8ae119ade669444f6ae89d941b5afde8003750e1666e95fd0d670c52bb839db4ecc0676324db5399c231b3a79f9c11ea667074371d2d1ae702c69b11ded1d52f
checksum: 10c0/a5357c32663e516833feed8f6fd899e1a6ab6acf79b198e860bb82076512e07f95730420eefc87a354d4004d9482b29fbecadcbdcf59b6f8737bba4da03e405e
languageName: node
linkType: hard
@@ -1630,6 +1658,53 @@ __metadata:
languageName: node
linkType: hard
"@inquirer/confirm@npm:^3.0.0":
version: 3.2.0
resolution: "@inquirer/confirm@npm:3.2.0"
dependencies:
"@inquirer/core": "npm:^9.1.0"
"@inquirer/type": "npm:^1.5.3"
checksum: 10c0/a2cbfc8ae9c880bba4cce1993f5c399fb0d12741fdd574917c87fceb40ece62ffa60e35aaadf4e62d7c114f54008e45aee5d6d90497bb62d493996c02725d243
languageName: node
linkType: hard
"@inquirer/core@npm:^9.1.0":
version: 9.1.0
resolution: "@inquirer/core@npm:9.1.0"
dependencies:
"@inquirer/figures": "npm:^1.0.5"
"@inquirer/type": "npm:^1.5.3"
"@types/mute-stream": "npm:^0.0.4"
"@types/node": "npm:^22.5.2"
"@types/wrap-ansi": "npm:^3.0.0"
ansi-escapes: "npm:^4.3.2"
cli-spinners: "npm:^2.9.2"
cli-width: "npm:^4.1.0"
mute-stream: "npm:^1.0.0"
signal-exit: "npm:^4.1.0"
strip-ansi: "npm:^6.0.1"
wrap-ansi: "npm:^6.2.0"
yoctocolors-cjs: "npm:^2.1.2"
checksum: 10c0/c86cbd1980788dee4151002ed717b5664a79eec1d925e1b38896bbad079647af5c423eaaa39a2291ba4fdf78a33c541ea3f69cbbf030f03815eb523fa05230f8
languageName: node
linkType: hard
"@inquirer/figures@npm:^1.0.5":
version: 1.0.5
resolution: "@inquirer/figures@npm:1.0.5"
checksum: 10c0/ec9ba23db42cb33fa18eb919abf2a18e750e739e64c1883ce4a98345cd5711c60cac12d1faf56a859f52d387deb221c8d3dfe60344ee07955a9a262f8b821fe3
languageName: node
linkType: hard
"@inquirer/type@npm:^1.5.3":
version: 1.5.3
resolution: "@inquirer/type@npm:1.5.3"
dependencies:
mute-stream: "npm:^1.0.0"
checksum: 10c0/da92a7410efcb20cf12422558fb8e00136e2ff1746ae1d17ea05511e77139bf2044527d37a70e77f188f158099f7751ed808ca3f82769cbe99c1052509481e95
languageName: node
linkType: hard
"@isaacs/cliui@npm:^8.0.2":
version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2"
@@ -1975,6 +2050,20 @@ __metadata:
languageName: node
linkType: hard
"@mswjs/interceptors@npm:^0.35.0":
version: 0.35.0
resolution: "@mswjs/interceptors@npm:0.35.0"
dependencies:
"@open-draft/deferred-promise": "npm:^2.2.0"
"@open-draft/logger": "npm:^0.3.0"
"@open-draft/until": "npm:^2.0.0"
is-node-process: "npm:^1.2.0"
outvariant: "npm:^1.4.3"
strict-event-emitter: "npm:^0.5.1"
checksum: 10c0/7e1a03a32afb9dafd6bdd8a77b838d4c6cff61a9d0aecf76a898ca2715c9068a8dfd3a40869dfaf42c834bc833fc057ccc096c2819c8f9a3212469fddc543b66
languageName: node
linkType: hard
"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -2024,6 +2113,30 @@ __metadata:
languageName: node
linkType: hard
"@open-draft/deferred-promise@npm:^2.2.0":
version: 2.2.0
resolution: "@open-draft/deferred-promise@npm:2.2.0"
checksum: 10c0/eafc1b1d0fc8edb5e1c753c5e0f3293410b40dde2f92688211a54806d4136887051f39b98c1950370be258483deac9dfd17cf8b96557553765198ef2547e4549
languageName: node
linkType: hard
"@open-draft/logger@npm:^0.3.0":
version: 0.3.0
resolution: "@open-draft/logger@npm:0.3.0"
dependencies:
is-node-process: "npm:^1.2.0"
outvariant: "npm:^1.4.0"
checksum: 10c0/90010647b22e9693c16258f4f9adb034824d1771d3baa313057b9a37797f571181005bc50415a934eaf7c891d90ff71dcd7a9d5048b0b6bb438f31bef2c7c5c1
languageName: node
linkType: hard
"@open-draft/until@npm:^2.0.0, @open-draft/until@npm:^2.1.0":
version: 2.1.0
resolution: "@open-draft/until@npm:2.1.0"
checksum: 10c0/61d3f99718dd86bb393fee2d7a785f961dcaf12f2055f0c693b27f4d0cd5f7a03d498a6d9289773b117590d794a43cd129366fd8e99222e4832f67b1653d54cf
languageName: node
linkType: hard
"@pkgjs/parseargs@npm:^0.11.0":
version: 0.11.0
resolution: "@pkgjs/parseargs@npm:0.11.0"
@@ -2720,9 +2833,9 @@ __metadata:
languageName: node
linkType: hard
"@testing-library/react-native@npm:^12.7.1":
version: 12.7.1
resolution: "@testing-library/react-native@npm:12.7.1"
"@testing-library/react-native@npm:^12.7.2":
version: 12.7.2
resolution: "@testing-library/react-native@npm:12.7.2"
dependencies:
jest-matcher-utils: "npm:^29.7.0"
pretty-format: "npm:^29.7.0"
@@ -2735,7 +2848,7 @@ __metadata:
peerDependenciesMeta:
jest:
optional: true
checksum: 10c0/caaa4bdf97834b307b72af05c447ce40a2ba2ff40b464050bc29535caadf81981ea2873668445e633fdb3d13efccb136ef0932d6d9f4736bc6f7f98be98088d4
checksum: 10c0/0e4e26bd211056646f8b5c80e9177efc90affe0ddc7e1a2c22742a4e6da7129ec1f9125c7d233adddeb27f429fb3eb91e3f3bfa9e77e176f042475574546b001
languageName: node
linkType: hard
@@ -2855,6 +2968,15 @@ __metadata:
languageName: node
linkType: hard
"@types/mute-stream@npm:^0.0.4":
version: 0.0.4
resolution: "@types/mute-stream@npm:0.0.4"
dependencies:
"@types/node": "npm:*"
checksum: 10c0/944730fd7b398c5078de3c3d4d0afeec8584283bc694da1803fdfca14149ea385e18b1b774326f1601baf53898ce6d121a952c51eb62d188ef6fcc41f725c0dc
languageName: node
linkType: hard
"@types/node-forge@npm:^1.3.0":
version: 1.3.11
resolution: "@types/node-forge@npm:1.3.11"
@@ -2882,6 +3004,15 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^22.5.2":
version: 22.5.4
resolution: "@types/node@npm:22.5.4"
dependencies:
undici-types: "npm:~6.19.2"
checksum: 10c0/b445daa7eecd761ad4d778b882d6ff7bcc3b4baad2086ea9804db7c5d4a4ab0298b00d7f5315fc640a73b5a1d52bbf9628e09c9fec0cf44dbf9b4df674a8717d
languageName: node
linkType: hard
"@types/prop-types@npm:*":
version: 15.7.12
resolution: "@types/prop-types@npm:15.7.12"
@@ -2913,6 +3044,27 @@ __metadata:
languageName: node
linkType: hard
"@types/statuses@npm:^2.0.4":
version: 2.0.5
resolution: "@types/statuses@npm:2.0.5"
checksum: 10c0/4dacec0b29483a44be902a022a11a22b339de7a6e7b2059daa4f7add10cb6dbcc28d02d2a416fe9687e48d335906bf983065391836d4e7c847e55ddef4de8fad
languageName: node
linkType: hard
"@types/tough-cookie@npm:^4.0.5":
version: 4.0.5
resolution: "@types/tough-cookie@npm:4.0.5"
checksum: 10c0/68c6921721a3dcb40451543db2174a145ef915bc8bcbe7ad4e59194a0238e776e782b896c7a59f4b93ac6acefca9161fccb31d1ce3b3445cb6faa467297fb473
languageName: node
linkType: hard
"@types/wrap-ansi@npm:^3.0.0":
version: 3.0.0
resolution: "@types/wrap-ansi@npm:3.0.0"
checksum: 10c0/8d8f53363f360f38135301a06b596c295433ad01debd082078c33c6ed98b05a5c8fe8853a88265432126096084f4a135ec1564e3daad631b83296905509f90b3
languageName: node
linkType: hard
"@types/yargs-parser@npm:*":
version: 21.0.3
resolution: "@types/yargs-parser@npm:21.0.3"
@@ -3480,9 +3632,9 @@ __metadata:
languageName: node
linkType: hard
"babel-preset-expo@npm:~11.0.13":
version: 11.0.13
resolution: "babel-preset-expo@npm:11.0.13"
"babel-preset-expo@npm:~11.0.14":
version: 11.0.14
resolution: "babel-preset-expo@npm:11.0.14"
dependencies:
"@babel/plugin-proposal-decorators": "npm:^7.12.9"
"@babel/plugin-transform-export-namespace-from": "npm:^7.22.11"
@@ -3494,7 +3646,7 @@ __metadata:
babel-plugin-react-compiler: "npm:^0.0.0-experimental-592953e-20240517"
babel-plugin-react-native-web: "npm:~0.19.10"
react-refresh: "npm:^0.14.2"
checksum: 10c0/3a377380dd1cfabeb936082b03514638325563b8d7ee02063ccf254adbd53737e889d0439bafbd42a29b569b5f386768a354a8598e7f61390d14100883615308
checksum: 10c0/9d5bb94c21555610c67b7dbe0e592f1ab7f53571dfe72a3ed314768f983a847a9b1dd0efd70e9f172bd68e7dee53d3d012601b8dae27f0593fcdf99c41bcc66f
languageName: node
linkType: hard
@@ -3872,13 +4024,20 @@ __metadata:
languageName: node
linkType: hard
"cli-spinners@npm:^2.0.0, cli-spinners@npm:^2.5.0":
"cli-spinners@npm:^2.0.0, cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.9.2":
version: 2.9.2
resolution: "cli-spinners@npm:2.9.2"
checksum: 10c0/907a1c227ddf0d7a101e7ab8b300affc742ead4b4ebe920a5bf1bc6d45dce2958fcd195eb28fa25275062fe6fa9b109b93b63bc8033396ed3bcb50297008b3a3
languageName: node
linkType: hard
"cli-width@npm:^4.1.0":
version: 4.1.0
resolution: "cli-width@npm:4.1.0"
checksum: 10c0/1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f
languageName: node
linkType: hard
"cliui@npm:^6.0.0":
version: 6.0.0
resolution: "cliui@npm:6.0.0"
@@ -4114,6 +4273,13 @@ __metadata:
languageName: node
linkType: hard
"cookie@npm:^0.5.0":
version: 0.5.0
resolution: "cookie@npm:0.5.0"
checksum: 10c0/c01ca3ef8d7b8187bae434434582288681273b5a9ed27521d4d7f9f7928fe0c920df0decd9f9d3bbd2d14ac432b8c8cf42b98b3bdd5bfe0e6edddeebebe8b61d
languageName: node
linkType: hard
"cookie@npm:^0.6.0":
version: 0.6.0
resolution: "cookie@npm:0.6.0"
@@ -5009,14 +5175,14 @@ __metadata:
languageName: node
linkType: hard
"expo-font@npm:~12.0.9":
version: 12.0.9
resolution: "expo-font@npm:12.0.9"
"expo-font@npm:~12.0.10":
version: 12.0.10
resolution: "expo-font@npm:12.0.10"
dependencies:
fontfaceobserver: "npm:^2.1.0"
peerDependencies:
expo: "*"
checksum: 10c0/9c7b63b3a3ee89bfcdbc1704451019b956b451208f0eca3bb1e57b53dd5dcdfb4d080d423583b92f864889a2a5d7624985c0e5103c54b36b8daf813471696b41
checksum: 10c0/49b7da4c5099f74a3641841e8a684a15b743e0d63bfc60355c7b2cf0a5b33b4321b0657c282126795da5ef53778b4d29a765dc9d08fe395e4bc801662305dee8
languageName: node
linkType: hard
@@ -5039,35 +5205,37 @@ __metadata:
languageName: node
linkType: hard
"expo-modules-autolinking@npm:1.11.1":
version: 1.11.1
resolution: "expo-modules-autolinking@npm:1.11.1"
"expo-modules-autolinking@npm:1.11.2":
version: 1.11.2
resolution: "expo-modules-autolinking@npm:1.11.2"
dependencies:
chalk: "npm:^4.1.0"
commander: "npm:^7.2.0"
fast-glob: "npm:^3.2.5"
find-up: "npm:^5.0.0"
fs-extra: "npm:^9.1.0"
require-from-string: "npm:^2.0.2"
resolve-from: "npm:^5.0.0"
bin:
expo-modules-autolinking: bin/expo-modules-autolinking.js
checksum: 10c0/8d70dda4d63f8ab2323fae13f46191cb8f54d76d6c37f9dc449fbe5a393b5f36f975a9a77ec3257ff880b361999e723be7bc793cbd85eadf9899861cb574469c
checksum: 10c0/3a0da953bcd6f0db6374056216117c8764c8e8cddd51d9fd30990c09080704ecf1abc05425a352a21ae09255d9c9eae99ce34ca6091d27dcd1eb8f0c510e3578
languageName: node
linkType: hard
"expo-modules-core@npm:1.12.20":
version: 1.12.20
resolution: "expo-modules-core@npm:1.12.20"
"expo-modules-core@npm:1.12.24":
version: 1.12.24
resolution: "expo-modules-core@npm:1.12.24"
dependencies:
invariant: "npm:^2.2.4"
checksum: 10c0/f4b5a5c6b54b1fd8bd8ce896ffbbbc50cd0e25d7d55e16441c1c63da770f1f51aef16f7b9e4be745c921af48b99878d3ba14f9372cba6155119388060cce5351
checksum: 10c0/812180ff32f288843592ca38dbebd67878beeb41796a19dd639a2379f0dc728955cff7eabd3e1aedea5ed0c786561334afc41e8fcf206b800c863b8658706d5d
languageName: node
linkType: hard
"expo-router@npm:~3.5.21":
version: 3.5.21
resolution: "expo-router@npm:3.5.21"
"expo-router@npm:~3.5.23":
version: 3.5.23
resolution: "expo-router@npm:3.5.23"
dependencies:
"@expo/metro-runtime": "npm:3.2.1"
"@expo/metro-runtime": "npm:3.2.3"
"@expo/server": "npm:^0.4.0"
"@radix-ui/react-slot": "npm:1.0.1"
"@react-navigation/bottom-tabs": "npm:~6.5.7"
@@ -5092,7 +5260,7 @@ __metadata:
optional: true
react-native-reanimated:
optional: true
checksum: 10c0/84c279f2d6b36557e74b74195e72af9161ff05f65574fc09176a6988a8d0e3911711c7cc8fa3b92b2a08f97662f4d091f1cd5beb2dca14439e450351aa7b7d15
checksum: 10c0/b14b3598ed8bcfcb46d356203f5031a6ee7c4d905d951d22ffd45800329a4bbd1e833e50628f65c2c99d85b9aa94aac4625b4cdd272dc10530cc8d6ebc3ebcda
languageName: node
linkType: hard
@@ -5114,28 +5282,28 @@ __metadata:
languageName: node
linkType: hard
"expo@npm:^51.0.26":
version: 51.0.26
resolution: "expo@npm:51.0.26"
"expo@npm:~51.0.31":
version: 51.0.32
resolution: "expo@npm:51.0.32"
dependencies:
"@babel/runtime": "npm:^7.20.0"
"@expo/cli": "npm:0.18.28"
"@expo/cli": "npm:0.18.29"
"@expo/config": "npm:9.0.3"
"@expo/config-plugins": "npm:8.0.8"
"@expo/metro-config": "npm:0.18.11"
"@expo/vector-icons": "npm:^14.0.0"
babel-preset-expo: "npm:~11.0.13"
babel-preset-expo: "npm:~11.0.14"
expo-asset: "npm:~10.0.10"
expo-file-system: "npm:~17.0.1"
expo-font: "npm:~12.0.9"
expo-font: "npm:~12.0.10"
expo-keep-awake: "npm:~13.0.2"
expo-modules-autolinking: "npm:1.11.1"
expo-modules-core: "npm:1.12.20"
expo-modules-autolinking: "npm:1.11.2"
expo-modules-core: "npm:1.12.24"
fbemitter: "npm:^3.0.0"
whatwg-url-without-unicode: "npm:8.0.0-3"
bin:
expo: bin/cli
checksum: 10c0/092cd106e48b8f31eb7e21c782c38816569ba5a26d50c08b74f5bf795cb835e4f4f803e636392a87b2f3e703d60ab885f9b2d576796be2aa961b99201e455312
checksum: 10c0/8a5ba07402451e027b58a8e64a7051b91d45776f1b3397bfd71a1f2a9d7752a0e50d5343baf53b985f497de3e735f5494322b9f4a68c832c7a034b2d81748409
languageName: node
linkType: hard
@@ -5763,6 +5931,13 @@ __metadata:
languageName: node
linkType: hard
"graphql@npm:^16.8.1":
version: 16.9.0
resolution: "graphql@npm:16.9.0"
checksum: 10c0/a8850f077ff767377237d1f8b1da2ec70aeb7623cdf1dfc9e1c7ae93accc0c8149c85abe68923be9871a2934b1bce5a2496f846d4d56e1cfb03eaaa7ddba9b6a
languageName: node
linkType: hard
"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2":
version: 1.0.2
resolution: "has-bigints@npm:1.0.2"
@@ -5825,6 +6000,13 @@ __metadata:
languageName: node
linkType: hard
"headers-polyfill@npm:^4.0.2":
version: 4.0.3
resolution: "headers-polyfill@npm:4.0.3"
checksum: 10c0/53e85b2c6385f8d411945fb890c5369f1469ce8aa32a6e8d28196df38568148de640c81cf88cbc7c67767103dd9acba48f4f891982da63178fc6e34560022afe
languageName: node
linkType: hard
"hermes-estree@npm:0.19.1":
version: 0.19.1
resolution: "hermes-estree@npm:0.19.1"
@@ -6316,6 +6498,13 @@ __metadata:
languageName: node
linkType: hard
"is-node-process@npm:^1.2.0":
version: 1.2.0
resolution: "is-node-process@npm:1.2.0"
checksum: 10c0/5b24fda6776d00e42431d7bcd86bce81cb0b6cabeb944142fe7b077a54ada2e155066ad06dbe790abdb397884bdc3151e04a9707b8cd185099efbc79780573ed
languageName: node
linkType: hard
"is-number-object@npm:^1.0.4":
version: 1.0.7
resolution: "is-number-object@npm:1.0.7"
@@ -8105,6 +8294,45 @@ __metadata:
languageName: node
linkType: hard
"msw@npm:^2.4.4":
version: 2.4.4
resolution: "msw@npm:2.4.4"
dependencies:
"@bundled-es-modules/cookie": "npm:^2.0.0"
"@bundled-es-modules/statuses": "npm:^1.0.1"
"@bundled-es-modules/tough-cookie": "npm:^0.1.6"
"@inquirer/confirm": "npm:^3.0.0"
"@mswjs/interceptors": "npm:^0.35.0"
"@open-draft/until": "npm:^2.1.0"
"@types/cookie": "npm:^0.6.0"
"@types/statuses": "npm:^2.0.4"
chalk: "npm:^4.1.2"
graphql: "npm:^16.8.1"
headers-polyfill: "npm:^4.0.2"
is-node-process: "npm:^1.2.0"
outvariant: "npm:^1.4.2"
path-to-regexp: "npm:^6.2.0"
strict-event-emitter: "npm:^0.5.1"
type-fest: "npm:^4.9.0"
yargs: "npm:^17.7.2"
peerDependencies:
typescript: ">= 4.8.x"
peerDependenciesMeta:
typescript:
optional: true
bin:
msw: cli/index.js
checksum: 10c0/0be51fbd6ef1b4fd8906353b6e40473f9466ea7ef9cc805e49c5937886721db20801b2322535b456d523040c012afb34e0e76a5beaa4891cbdde2408d2652094
languageName: node
linkType: hard
"mute-stream@npm:^1.0.0":
version: 1.0.0
resolution: "mute-stream@npm:1.0.0"
checksum: 10c0/dce2a9ccda171ec979a3b4f869a102b1343dee35e920146776780de182f16eae459644d187e38d59a3d37adf85685e1c17c38cf7bfda7e39a9880f7a1d10a74c
languageName: node
linkType: hard
"mz@npm:^2.7.0":
version: 2.7.0
resolution: "mz@npm:2.7.0"
@@ -8493,6 +8721,13 @@ __metadata:
languageName: node
linkType: hard
"outvariant@npm:^1.4.0, outvariant@npm:^1.4.2, outvariant@npm:^1.4.3":
version: 1.4.3
resolution: "outvariant@npm:1.4.3"
checksum: 10c0/5976ca7740349cb8c71bd3382e2a762b1aeca6f33dc984d9d896acdf3c61f78c3afcf1bfe9cc633a7b3c4b295ec94d292048f83ea2b2594fae4496656eba992c
languageName: node
linkType: hard
"p-finally@npm:^1.0.0":
version: 1.0.0
resolution: "p-finally@npm:1.0.0"
@@ -8677,6 +8912,13 @@ __metadata:
languageName: node
linkType: hard
"path-to-regexp@npm:^6.2.0":
version: 6.2.2
resolution: "path-to-regexp@npm:6.2.2"
checksum: 10c0/4b60852d3501fd05ca9dd08c70033d73844e5eca14e41f499f069afa8364f780f15c5098002f93bd42af8b3514de62ac6e82a53b5662de881d2b08c9ef21ea6b
languageName: node
linkType: hard
"path-type@npm:^4.0.0":
version: 4.0.0
resolution: "path-type@npm:4.0.0"
@@ -8887,6 +9129,13 @@ __metadata:
languageName: node
linkType: hard
"psl@npm:^1.1.33":
version: 1.9.0
resolution: "psl@npm:1.9.0"
checksum: 10c0/6a3f805fdab9442f44de4ba23880c4eba26b20c8e8e0830eff1cb31007f6825dace61d17203c58bfe36946842140c97a1ba7f67bc63ca2d88a7ee052b65d97ab
languageName: node
linkType: hard
"pump@npm:^3.0.0":
version: 3.0.0
resolution: "pump@npm:3.0.0"
@@ -8939,6 +9188,13 @@ __metadata:
languageName: node
linkType: hard
"querystringify@npm:^2.1.1":
version: 2.2.0
resolution: "querystringify@npm:2.2.0"
checksum: 10c0/3258bc3dbdf322ff2663619afe5947c7926a6ef5fb78ad7d384602974c467fadfc8272af44f5eb8cddd0d011aae8fabf3a929a8eee4b86edcc0a21e6bd10f9aa
languageName: node
linkType: hard
"queue-microtask@npm:^1.2.2":
version: 1.2.3
resolution: "queue-microtask@npm:1.2.3"
@@ -9356,6 +9612,13 @@ __metadata:
languageName: node
linkType: hard
"requires-port@npm:^1.0.0":
version: 1.0.0
resolution: "requires-port@npm:1.0.0"
checksum: 10c0/b2bfdd09db16c082c4326e573a82c0771daaf7b53b9ce8ad60ea46aa6e30aaf475fe9b164800b89f93b748d2c234d8abff945d2551ba47bf5698e04cd7713267
languageName: node
linkType: hard
"resolve-cwd@npm:^3.0.0":
version: 3.0.0
resolution: "resolve-cwd@npm:3.0.0"
@@ -9498,21 +9761,22 @@ __metadata:
resolution: "root-workspace-0b6124@workspace:."
dependencies:
"@babel/core": "npm:^7.20.0"
"@expo/metro-runtime": "npm:~3.2.1"
"@testing-library/react-native": "npm:^12.7.1"
"@expo/metro-runtime": "npm:~3.2.3"
"@testing-library/react-native": "npm:^12.7.2"
"@types/eslint": "npm:^8.56.10"
"@types/jest": "npm:^29.5.12"
"@types/react": "npm:~18.2.45"
"@types/react-native-get-random-values": "npm:^1"
eslint: "npm:^8.57.0"
expo: "npm:^51.0.26"
expo: "npm:~51.0.31"
expo-constants: "npm:~16.0.2"
expo-linking: "npm:~6.3.1"
expo-router: "npm:~3.5.21"
expo-router: "npm:~3.5.23"
expo-splash-screen: "npm:~0.27.5"
expo-status-bar: "npm:~1.12.1"
jest: "npm:^29.7.0"
jotai: "npm:^2.8.4"
msw: "npm:^2.4.4"
nanoid: "npm:^3.3.7"
react: "npm:18.2.0"
react-dom: "npm:18.2.0"
@@ -9821,7 +10085,7 @@ __metadata:
languageName: node
linkType: hard
"signal-exit@npm:^4.0.1":
"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0":
version: 4.1.0
resolution: "signal-exit@npm:4.1.0"
checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83
@@ -10020,7 +10284,7 @@ __metadata:
languageName: node
linkType: hard
"statuses@npm:2.0.1":
"statuses@npm:2.0.1, statuses@npm:^2.0.1":
version: 2.0.1
resolution: "statuses@npm:2.0.1"
checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0
@@ -10048,6 +10312,13 @@ __metadata:
languageName: node
linkType: hard
"strict-event-emitter@npm:^0.5.1":
version: 0.5.1
resolution: "strict-event-emitter@npm:0.5.1"
checksum: 10c0/f5228a6e6b6393c57f52f62e673cfe3be3294b35d6f7842fc24b172ae0a6e6c209fa83241d0e433fc267c503bc2f4ffdbe41a9990ff8ffd5ac425ec0489417f7
languageName: node
linkType: hard
"strict-uri-encode@npm:^2.0.0":
version: 2.0.0
resolution: "strict-uri-encode@npm:2.0.0"
@@ -10498,6 +10769,18 @@ __metadata:
languageName: node
linkType: hard
"tough-cookie@npm:^4.1.4":
version: 4.1.4
resolution: "tough-cookie@npm:4.1.4"
dependencies:
psl: "npm:^1.1.33"
punycode: "npm:^2.1.1"
universalify: "npm:^0.2.0"
url-parse: "npm:^1.5.3"
checksum: 10c0/aca7ff96054f367d53d1e813e62ceb7dd2eda25d7752058a74d64b7266fd07be75908f3753a32ccf866a2f997604b414cfb1916d6e7f69bc64d9d9939b0d6c45
languageName: node
linkType: hard
"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
@@ -10588,6 +10871,13 @@ __metadata:
languageName: node
linkType: hard
"type-fest@npm:^4.9.0":
version: 4.26.1
resolution: "type-fest@npm:4.26.1"
checksum: 10c0/d2719ff8d380befe8a3c61068f37f28d6fa2849fd140c5d2f0f143099e371da6856aad7c97e56b83329d45bfe504afe9fd936a7cff600cc0d46aa9ffb008d6c6
languageName: node
linkType: hard
"typed-array-buffer@npm:^1.0.2":
version: 1.0.2
resolution: "typed-array-buffer@npm:1.0.2"
@@ -10707,6 +10997,13 @@ __metadata:
languageName: node
linkType: hard
"undici-types@npm:~6.19.2":
version: 6.19.8
resolution: "undici-types@npm:6.19.8"
checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344
languageName: node
linkType: hard
"undici@npm:^6.11.1":
version: 6.19.7
resolution: "undici@npm:6.19.7"
@@ -10788,6 +11085,13 @@ __metadata:
languageName: node
linkType: hard
"universalify@npm:^0.2.0":
version: 0.2.0
resolution: "universalify@npm:0.2.0"
checksum: 10c0/cedbe4d4ca3967edf24c0800cfc161c5a15e240dac28e3ce575c689abc11f2c81ccc6532c8752af3b40f9120fb5e454abecd359e164f4f6aa44c29cd37e194fe
languageName: node
linkType: hard
"universalify@npm:^1.0.0":
version: 1.0.0
resolution: "universalify@npm:1.0.0"
@@ -10839,6 +11143,16 @@ __metadata:
languageName: node
linkType: hard
"url-parse@npm:^1.5.3":
version: 1.5.10
resolution: "url-parse@npm:1.5.10"
dependencies:
querystringify: "npm:^2.1.1"
requires-port: "npm:^1.0.0"
checksum: 10c0/bd5aa9389f896974beb851c112f63b466505a04b4807cea2e5a3b7092f6fbb75316f0491ea84e44f66fed55f1b440df5195d7e3a8203f64fcefa19d182f5be87
languageName: node
linkType: hard
"use-latest-callback@npm:^0.2.1":
version: 0.2.1
resolution: "use-latest-callback@npm:0.2.1"
@@ -11329,7 +11643,7 @@ __metadata:
languageName: node
linkType: hard
"yargs@npm:^17.3.1, yargs@npm:^17.6.2":
"yargs@npm:^17.3.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2":
version: 17.7.2
resolution: "yargs@npm:17.7.2"
dependencies:
@@ -11350,3 +11664,10 @@ __metadata:
checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f
languageName: node
linkType: hard
"yoctocolors-cjs@npm:^2.1.2":
version: 2.1.2
resolution: "yoctocolors-cjs@npm:2.1.2"
checksum: 10c0/a0e36eb88fea2c7981eab22d1ba45e15d8d268626e6c4143305e2c1628fa17ebfaa40cd306161a8ce04c0a60ee0262058eab12567493d5eb1409780853454c6f
languageName: node
linkType: hard
-2
View File
@@ -1,7 +1,5 @@
import { resetToDefaults } from './src/pure';
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
beforeEach(() => {
resetToDefaults();
});
+3 -3
View File
@@ -70,13 +70,13 @@
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@callstack/eslint-config": "^15.0.0",
"@release-it/conventional-changelog": "^8.0.1",
"@release-it/conventional-changelog": "^9.0.0",
"@relmify/jest-serializer-strip-ansi": "^1.0.2",
"@types/jest": "^29.5.12",
"@types/react": "^18.3.3",
"@types/react-reconciler": "^0",
"babel-jest": "^29.7.0",
"del-cli": "^5.1.0",
"del-cli": "^6.0.0",
"eslint": "^8.57.0",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-prettier": "^4.2.1",
@@ -84,7 +84,7 @@
"jest": "^29.7.0",
"prettier": "^2.8.8",
"react": "18.3.1",
"react-native": "0.75.1",
"react-native": "0.76.0-rc.6",
"release-it": "^17.6.0",
"strip-ansi": "^6.0.1",
"typescript": "^5.5.4"
+5
View File
@@ -5,6 +5,11 @@
"name": "basics",
"label": "Basic Recipes"
},
{
"type": "dir",
"name": "advanced",
"label": "Advanced Recipes"
},
{
"type": "dir",
"name": "state-management",
@@ -0,0 +1 @@
["network-requests"]
@@ -0,0 +1,380 @@
# Network Requests
## Introduction
Mocking network requests is an essential part of testing React Native applications. By mocking
network
requests, you can control the data that is returned from the server and test how your application
behaves in different scenarios, such as when the request is successful or when it fails.
In this guide, we will show you how to mock network requests and guard your test suits from unwanted
and unmocked/unhandled network requests
:::info
To simulate a real-world scenario, we will use the [Random User Generator API](https://randomuser.me/) that provides random user data.
:::
## Phonebook Example
Let's assume we have a simple phonebook application that
uses [`fetch`](https://reactnative.dev/docs/network#using-fetch) for fetching Data from a server.
In our case, we have a list of contacts and favorites that we want to display in our application.
This is how the root of the application looks like:
```tsx title=network-requests/Phonebook.tsx
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} />
</>
);
};
```
We fetch the contacts from the server using the `getAllFavorites` function that utilizes `fetch`.
```tsx title=network-requests/api/getAllContacts.ts
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;
};
```
We have similar function for fetching the favorites, but this time limiting the results to 10.
```tsx title=network-requests/api/getAllFavorites.ts
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;
};
```
Our `FavoritesList` component is a simple component that displays the list of favorite contacts and
their avatars horizontally.
```tsx title=network-requests/components/FavoritesList.tsx
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 (
<View style={styles.loaderContainer}>
<Text>Figuring out your favorites...</Text>
</View>
);
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>
);
};
// Looking for styles?
// Check examples/cookbook/app/advanced/components/FavoritesList.tsx
const styles =
...
```
Our `ContactsList` component is similar to the `FavoritesList` component, but it displays the list
of
all contacts vertically.
```tsx title=network-requests/components/ContactsList.tsx
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>
);
};
// Looking for styles or FullScreenLoader component?
// Check examples/cookbook/app/advanced/components/ContactsList.tsx
const FullScreenLoader = () => ...
const styles = ...
```
## Start testing with a simple test
In our initial test we would like to test if the `PhoneBook` component renders the `FavoritesList`
and `ContactsList` components correctly.
We will need to mock the network requests and their corresponding responses to ensure that the component behaves as
expected. To mock the network requests we will use [MSW (Mock Service Worker)](https://mswjs.io/docs/getting-started).
:::note
We recommend using the Mock Service Worker (MSW) library to declaratively mock API communication in your tests instead of stubbing `fetch`, or relying on third-party adapters.
:::
:::info
You can install MSW by running `npm install msw --save-dev` or `yarn add msw --dev`.
More info regarding installation can be found in [MSW's getting started guide](https://mswjs.io/docs/getting-started#step-1-install).
Please make sure you're also aware of [MSW's setup guide](https://mswjs.io/docs/integrations/react-native).
Please be minded that the MSW's setup guide is potentially incomplete and might contain discrepancies/missing pieces.
:::
```tsx title=network-requests/Phonebook.test.tsx
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
import React from 'react';
import PhoneBook from '../PhoneBook';
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);
}),
];
// Setup a request interception server with the given request handlers.
const server = setupServer(...handlers);
// Enable API mocking via Mock Service Worker (MSW)
beforeAll(() => server.listen());
// Reset any runtime request handlers we may add during the tests
afterEach(() => server.resetHandlers());
// Disable API mocking after the tests are done
afterAll(() => server.close());
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: ida.kristensen@example.com')).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);
});
});
const DATA: { results: User[] } = {
results: [
{
name: {
title: 'Mrs',
first: 'Ida',
last: 'Kristensen',
},
email: 'ida.kristensen@example.com',
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',
},
// For brevity, we have omitted the rest of the users, you can still find them in
// examples/cookbook/app/network-requests/__tests__/test-utils.ts
...
],
};
```
:::info
More info regarding how to describe the network using request handlers, intercepting a request and handling its response can be found in the [MSW's documentation](https://mswjs.io/docs/getting-started#step-2-describe).
:::
## Testing error handling
As we are dealing with network requests, and things can go wrong, we should also cover the case when
the API request fails. In this case, we would like to test how our application behaves when the API request fails.
:::info
The nature of the network can be highly dynamic, which makes it challenging to describe it completely in a fixed list of request handlers.
MSW provides us the means to override any particular network behavior using the designated `.use()` API.
More info can be found in [MSW's Network behavior overrides documentation](https://mswjs.io/docs/best-practices/network-behavior-overrides)
:::
```tsx title=network-requests/Phonebook.test.tsx
...
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 the default response for all other requests that match URL and verb. (in our case get favorites)
return HttpResponse.json(DATA);
}),
);
};
describe('PhoneBook', () => {
...
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();
});
});
````
## Global guarding against unwanted API requests
As mistakes may happen, we might forget to mock a network request in one of our tests in the future.
To prevent us from happening, and alert when a certain network request is left unhandled, you may choose to
move MSW's server management from `PhoneBook.test.tsx` to Jest's setup file via [`setupFilesAfterEnv`](https://jestjs.io/docs/configuration#setupfilesafterenv-array).
```tsx title=examples/cookbook/jest-setup.ts
// Enable API mocking via Mock Service Worker (MSW)
beforeAll(() => server.listen());
// Reset any runtime request handlers we may add during the tests
afterEach(() => server.resetHandlers());
// Disable API mocking after the tests are done
afterAll(() => server.close());
// ... rest of your setup file
```
This setup will ensure you have the MSW server running before any test suite starts and stops it after all tests are done.
Which will result in a warning in the console if you forget to mock an API request in your test suite.
```bash
[MSW] Warning: intercepted a request without a matching request handler:
• GET https://randomuser.me/api/?results=25?results=25
```
## Conclusion
Testing a component that makes network requests in combination with MSW takes some initial preparation to configure and describe the overridden networks.
We can achieve that by using MSW's request handlers and intercepting APIs.
Once up and running we gain full grip over the network requests, their responses, statuses.
Doing so is crucial to be able to test how our application behaves in different
scenarios, such as when the request is successful or when it fails.
When global configuration is in place, MSW's will also warn us when an unhandled network requests has occurred throughout a test suite.
## Further Reading and Alternatives
Explore more advanced scenarios for mocking network requests with MSW:
- MSW's Basics - [Intercepting requests](https://mswjs.io/docs/basics/intercepting-requests) and/or [Mocking responses](https://mswjs.io/docs/basics/mocking-responses)
- MSW's Network behavior - how to describe [REST](https://mswjs.io/docs/network-behavior/rest) and/or [GraphQL](https://mswjs.io/docs/network-behavior/graphql) APIs
+26 -2
View File
@@ -1,5 +1,29 @@
# Introduction
This cookbook is intended to showcase best practices, tips & tricks, and ready-to-use recipes for using React Native Testing Library.
Welcome to the **React Native Testing Library (RNTL) Cookbook**!
This app is your go-to resource for learning how to effectively test React Native applications.
It provides a collection of **best practices**, **ready-made recipes**, and **tips & tricks** to
simplify and improve your testing workflow. Whether you’re a beginner just getting started or a
seasoned developer looking to sharpen your
skills, the Cookbook has something for everyone.
We invite you to contribute your favorite recipes to the Cookbook. More info [here](https://github.com/callstack/react-native-testing-library/issues/1624).
## What's Inside the Cookbook?
The Cookbook is currently organized into **three main chapters**:
- **Basic Recipes**: A great starting point, covering essential testing scenarios such as async
operations and custom render functions.
- **Advanced Recipes**: More complex scenarios like network requests and in the future, navigation
testing and more.
- **State Management Recipes**: Best practices for testing state management libraries
Each recipe includes a clear explanation along with a corresponding code example to help you get
hands-on with testing. Checkout
the [Cookbook App](https://github.com/callstack/react-native-testing-library/tree/main/examples/cookbook#rntl-cookbook) to see the
recipes in action.
## What's Next?
Join the conversation
on [GitHub](https://github.com/callstack/react-native-testing-library/issues/1624) here to discuss
ideas, ask questions, or provide feedback.
@@ -12,7 +12,7 @@ the developer experience.
Let's assume we have a simple task list component that uses Jotai for state management. The
component has a list of tasks, a text input for typing new task name and a button to add a new task to the list.
```tsx title=jotai/index.test.tsx
```tsx title=state-management/jotai/TaskList.tsx
import * as React from 'react';
import { Pressable, Text, TextInput, View } from 'react-native';
import { useAtom } from 'jotai';
@@ -65,7 +65,7 @@ We can test our `TaskList` component using React Native Testing Library's (RNTL)
function. Although it is sufficient to test the empty state of the `TaskList` component, it is not
enough to test the component with initial tasks present in the list.
```tsx title=jotai/index.test.tsx
```tsx title=status-management/jotai/__tests__/TaskList.test.tsx
import * as React from 'react';
import { render, screen, userEvent } from '@testing-library/react-native';
import { renderWithAtoms } from './test-utils';
@@ -88,7 +88,7 @@ initial values. We can create a custom render function that uses Jotai's `useHyd
hydrate the atoms with initial values. This function will accept the initial atoms and their
corresponding values as an argument.
```tsx title=test-utils.tsx
```tsx title=status-management/jotai/test-utils.tsx
import * as React from 'react';
import { render } from '@testing-library/react-native';
import { useHydrateAtoms } from 'jotai/utils';
@@ -144,7 +144,7 @@ We can now use the `renderWithAtoms` function to render the `TaskList` component
In our test, we populated only one atom and its initial value, but you can add other Jotai atoms and their corresponding values to the initialValues array as needed.
:::
```tsx title=jotai/index.test.tsx
```tsx title=status-management/jotai/__tests__/TaskList.test.tsx
=======
const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }];
@@ -173,7 +173,7 @@ test('renders a to do list with 1 items initially, and adds a new item', async (
In several cases, you might need to change an atom's state outside a React component. In our case,
we have a set of functions to get tasks and set tasks, which change the state of the task list atom.
```tsx title=state.ts
```tsx title=state-management/jotai/state.ts
import { atom, createStore } from 'jotai';
import { Task } from './types';
@@ -201,7 +201,7 @@ the initial to-do items in the store and then checking if the functions work as
No special setup is required to test these functions, as `store.set` is available by default by
Jotai.
```tsx title=jotai/index.test.tsx
```tsx title=state-management/jotai/__tests__/TaskList.test.tsx
import { addTask, getAllTasks, store, tasksAtom } from './state';
//...
+1 -1
View File
@@ -1,6 +1,6 @@
# Queries
Queries are one of the main building blocks for the React Native Testing Library. They enable you to find relevant elements in the element tree, which represents the your application's user interface when running under tests.
Queries are one of the main building blocks for the React Native Testing Library. They enable you to find relevant elements in the element tree, which represents your application's user interface when running under tests.
## Accessing queries
+1211 -1750
View File
File diff suppressed because it is too large Load Diff