├── .eslintrc.js
├── .expo-shared
└── assets.json
├── .gitignore
├── App.js
├── app.json
├── assets
├── adaptive-icon.png
├── favicon.png
├── icon.png
└── splash.png
├── babel.config.js
├── package.json
├── setupJest.js
├── src
├── components
│ └── Form.js
└── screens
│ ├── Example.js
│ ├── SignIn.js
│ └── __tests__
│ ├── SignIn.test.js
│ └── __snapshots__
│ └── SignIn.test.js.snap
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ["handlebarlabs"],
4 | rules: {},
5 | };
6 |
--------------------------------------------------------------------------------
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p8
6 | *.p12
7 | *.key
8 | *.mobileprovision
9 | *.orig.*
10 | web-build/
11 |
12 | # macOS
13 | .DS_Store
14 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { NavigationContainer } from "@react-navigation/native";
4 | import { createStackNavigator } from "@react-navigation/stack";
5 |
6 | import SignIn from "./src/screens/SignIn";
7 | import Example from "./src/screens/Example";
8 |
9 | const Stack = createStackNavigator();
10 |
11 | export default () => (
12 |
13 |
14 |
19 |
24 |
25 |
26 | );
27 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "ScreenTestDemo",
4 | "slug": "ScreenTestDemo",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "splash": {
9 | "image": "./assets/splash.png",
10 | "resizeMode": "contain",
11 | "backgroundColor": "#ffffff"
12 | },
13 | "updates": {
14 | "fallbackToCacheTimeout": 0
15 | },
16 | "assetBundlePatterns": [
17 | "**/*"
18 | ],
19 | "ios": {
20 | "supportsTablet": true
21 | },
22 | "android": {
23 | "adaptiveIcon": {
24 | "foregroundImage": "./assets/adaptive-icon.png",
25 | "backgroundColor": "#FFFFFF"
26 | }
27 | },
28 | "web": {
29 | "favicon": "./assets/favicon.png"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactNativeSchool/testing-screen-react-native-example/0e981dff76538768ffe584a752b18a0c7c8faaf1/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactNativeSchool/testing-screen-react-native-example/0e981dff76538768ffe584a752b18a0c7c8faaf1/assets/favicon.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactNativeSchool/testing-screen-react-native-example/0e981dff76538768ffe584a752b18a0c7c8faaf1/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactNativeSchool/testing-screen-react-native-example/0e981dff76538768ffe584a752b18a0c7c8faaf1/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "web": "expo start --web",
8 | "eject": "expo eject",
9 | "test": "jest"
10 | },
11 | "dependencies": {
12 | "@react-native-community/masked-view": "0.1.10",
13 | "@react-navigation/native": "^5.9.3",
14 | "@react-navigation/stack": "^5.14.3",
15 | "expo": "~40.0.0",
16 | "expo-status-bar": "~1.0.3",
17 | "react": "16.13.1",
18 | "react-dom": "16.13.1",
19 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
20 | "react-native-gesture-handler": "~1.8.0",
21 | "react-native-reanimated": "~1.13.0",
22 | "react-native-safe-area-context": "3.1.9",
23 | "react-native-screens": "~2.15.2",
24 | "react-native-web": "~0.13.12"
25 | },
26 | "devDependencies": {
27 | "@babel/core": "~7.9.0",
28 | "@testing-library/jest-native": "^3.4.3",
29 | "@testing-library/react-native": "^7.1.0",
30 | "eslint": "^7.20.0",
31 | "eslint-config-handlebarlabs": "^0.0.6",
32 | "jest-expo": "^40.0.2",
33 | "jest-fetch-mock": "^3.0.3"
34 | },
35 | "private": true,
36 | "jest": {
37 | "preset": "jest-expo",
38 | "transformIgnorePatterns": [
39 | "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
40 | ],
41 | "setupFilesAfterEnv": [
42 | "@testing-library/jest-native/extend-expect"
43 | ],
44 | "setupFiles": [
45 | "./setupJest.js"
46 | ]
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/setupJest.js:
--------------------------------------------------------------------------------
1 | require("jest-fetch-mock").enableMocks();
2 |
--------------------------------------------------------------------------------
/src/components/Form.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | StyleSheet,
4 | Text,
5 | View,
6 | TextInput,
7 | TouchableOpacity,
8 | } from "react-native";
9 |
10 | const styles = StyleSheet.create({
11 | inputContainer: {
12 | backgroundColor: "#f4f6f8",
13 | paddingVertical: 10,
14 | paddingHorizontal: 15,
15 | borderRadius: 10,
16 | marginVertical: 5,
17 | borderWidth: 1,
18 | borderColor: "#f4f6f8",
19 | },
20 | inputContainerError: {
21 | borderColor: "#cc0011",
22 | },
23 | row: {
24 | flexDirection: "row",
25 | justifyContent: "space-between",
26 | },
27 | inputLabel: {
28 | fontSize: 10,
29 | color: "#b4b6b8",
30 | },
31 | input: {
32 | color: "#353031",
33 | fontWeight: "bold",
34 | fontSize: 14,
35 | marginTop: 3,
36 | marginRight: 10,
37 | flex: 1,
38 | },
39 | button: {
40 | backgroundColor: "#9374b7",
41 | alignItems: "center",
42 | justifyContent: "center",
43 | paddingVertical: 16,
44 | borderRadius: 10,
45 | marginTop: 10,
46 | },
47 | buttonText: {
48 | color: "#fff",
49 | fontSize: 14,
50 | fontWeight: "bold",
51 | },
52 | errorContainer: {
53 | paddingVertical: 10,
54 | },
55 | errorText: {
56 | fontSize: 14,
57 | color: "#cc0011",
58 | },
59 | });
60 |
61 | export const Input = ({ label, error, ...props }) => {
62 | const containerStyles = [styles.inputContainer];
63 | if (error) {
64 | containerStyles.push(styles.inputContainerError);
65 | }
66 |
67 | return (
68 |
69 | {label}
70 |
71 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | export const Button = ({ text, onPress, ...props }) => {
79 | return (
80 |
81 | {text}
82 |
83 | );
84 | };
85 |
86 | export const ErrorText = ({ messages = [] }) => {
87 | const displayMessages = messages.filter((msg) => msg !== undefined);
88 |
89 | return (
90 |
91 | {displayMessages.map((msg) => (
92 |
93 | {msg}
94 |
95 | ))}
96 |
97 | );
98 | };
99 |
--------------------------------------------------------------------------------
/src/screens/Example.js:
--------------------------------------------------------------------------------
1 | export default () => null;
2 |
--------------------------------------------------------------------------------
/src/screens/SignIn.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { StyleSheet, Text, KeyboardAvoidingView } from "react-native";
3 |
4 | import { Input, Button, ErrorText } from "../components/Form";
5 |
6 | const styles = StyleSheet.create({
7 | container: {
8 | flex: 1,
9 | backgroundColor: "#fff",
10 | justifyContent: "center",
11 | paddingHorizontal: 40,
12 | },
13 | headerText: {
14 | color: "#353031",
15 | fontWeight: "bold",
16 | fontSize: 34,
17 | marginBottom: 10,
18 | },
19 | });
20 |
21 | const useLoginFormState = ({ navigation }) => {
22 | const [username, setUsername] = useState("");
23 | const [password, setPassword] = useState("");
24 | const [submit, setSubmit] = useState(false);
25 |
26 | let isUsernameValid = false;
27 | let isPasswordValid = false;
28 |
29 | if (username === "example") {
30 | isUsernameValid = true;
31 | }
32 |
33 | if (password === "asdf") {
34 | isPasswordValid = true;
35 | }
36 |
37 | return {
38 | username: {
39 | value: username,
40 | set: setUsername,
41 | valid: isUsernameValid,
42 | },
43 | password: {
44 | value: password,
45 | set: setPassword,
46 | valid: isPasswordValid,
47 | },
48 | submit: {
49 | value: submit,
50 | set: () => {
51 | setSubmit(true);
52 |
53 | if (isUsernameValid && isPasswordValid) {
54 | fetch("https://jsonplaceholder.typicode.com/users", {
55 | method: "POST",
56 | body: JSON.stringify({
57 | username,
58 | password,
59 | }),
60 | })
61 | .then((response) => response.json())
62 | .then(() => {
63 | navigation.push("App");
64 | })
65 | .catch((error) => {
66 | console.log("error", error);
67 | });
68 | }
69 | },
70 | },
71 | };
72 | };
73 |
74 | export default ({ navigation }) => {
75 | const { username, password, submit } = useLoginFormState({ navigation });
76 |
77 | let usernameErrorMsg;
78 | let passwordErrorMsg;
79 |
80 | if (submit.value && !username.valid) {
81 | usernameErrorMsg = "Invalid username.";
82 | }
83 |
84 | if (submit.value && !password.valid) {
85 | passwordErrorMsg = "Invalid password.";
86 | }
87 |
88 | return (
89 |
90 | Login
91 |
98 |
106 |
107 |
108 |
109 | );
110 | };
111 |
--------------------------------------------------------------------------------
/src/screens/__tests__/SignIn.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, fireEvent, act } from "@testing-library/react-native";
3 |
4 | import SignIn from "../SignIn";
5 |
6 | const flushMicrotasksQueue = () =>
7 | new Promise((resolve) => setImmediate(resolve));
8 |
9 | it("renders default elements", () => {
10 | const { getAllByText, getByPlaceholderText } = render();
11 |
12 | expect(getAllByText("Login").length).toBe(2);
13 | getByPlaceholderText("example");
14 | getByPlaceholderText("***");
15 | });
16 |
17 | it("shows invalid input messages", () => {
18 | const { getByTestId, getByText } = render();
19 |
20 | fireEvent.press(getByTestId("SignIn.Button"));
21 |
22 | getByText("Invalid username.");
23 | getByText("Invalid password.");
24 | });
25 |
26 | it("shows invalid user name error message", () => {
27 | const { getByTestId, getByText, queryAllByText } = render();
28 |
29 | fireEvent.changeText(getByTestId("SignIn.passwordInput"), "asdf");
30 |
31 | fireEvent.press(getByTestId("SignIn.Button"));
32 |
33 | getByText("Invalid username.");
34 | expect(queryAllByText("Invalid password.").length).toBe(0);
35 |
36 | fireEvent.changeText(getByTestId("SignIn.usernameInput"), "invalid input");
37 |
38 | getByText("Invalid username.");
39 | expect(queryAllByText("Invalid password.").length).toBe(0);
40 | });
41 |
42 | it("shows invalid password error message", () => {
43 | const { getByTestId, getByText, queryAllByText } = render();
44 |
45 | fireEvent.changeText(getByTestId("SignIn.usernameInput"), "example");
46 |
47 | fireEvent.press(getByTestId("SignIn.Button"));
48 |
49 | getByText("Invalid password.");
50 | expect(queryAllByText("Invalid username.").length).toBe(0);
51 |
52 | fireEvent.changeText(getByTestId("SignIn.passwordInput"), "invalid input");
53 |
54 | getByText("Invalid password.");
55 | expect(queryAllByText("Invalid username.").length).toBe(0);
56 | });
57 |
58 | it("handles valid input submission", async () => {
59 | fetch.mockResponseOnce(JSON.stringify({ passes: true }));
60 |
61 | const pushMock = jest.fn();
62 | const { getByTestId } = render();
63 |
64 | fireEvent.changeText(getByTestId("SignIn.usernameInput"), "example");
65 | fireEvent.changeText(getByTestId("SignIn.passwordInput"), "asdf");
66 | fireEvent.press(getByTestId("SignIn.Button"));
67 |
68 | expect(fetch.mock.calls).toMatchSnapshot();
69 | await act(flushMicrotasksQueue);
70 |
71 | expect(pushMock).toBeCalledWith("App");
72 | });
73 |
--------------------------------------------------------------------------------
/src/screens/__tests__/__snapshots__/SignIn.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`handles valid input submission 1`] = `
4 | Array [
5 | Array [
6 | "https://jsonplaceholder.typicode.com/users",
7 | Object {
8 | "body": "{\\"username\\":\\"example\\",\\"password\\":\\"asdf\\"}",
9 | "method": "POST",
10 | },
11 | ],
12 | ]
13 | `;
14 |
--------------------------------------------------------------------------------