@testing-library/react-native -> render -> "TypeError: Cannot read properties of undefined (reading 'exists')"

747 views Asked by At

I've looked through many similar questions, but none seem to be facing the same issue I am having...

enter image description here

ScreenLogin.test.tsx

import React from 'react';
import { render, screen } from '@testing-library/react-native';
import ScreenLogin from './ScreenLogin';

describe('Login screen...', () => {
  it('renders', async () => {
    render(<ScreenLogin />);
    const userInput = screen.getByPlaceholderText('Username');
    // screen.debug();
    expect(1).toBe(1);
  });
});

ScreenLogin.tsx

import React, { useState } from 'react';
import { Image, StyleSheet, Text, TextInput, View } from 'react-native';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { MaterialCommunityIcons as Icon } from '@expo/vector-icons';
import { BtnMain, MainView } from '@app/components';
import { useAuthStore } from '@app/stores';
import { apiGetCurrentUser, apiLogin } from '@app/apis';

const validationSchema = Yup.object({
  username: Yup.string().required('Username required'),
  password: Yup.string().required('Password required')
});

export default function ScreenLogin(): JSX.Element {
  const [isLoggingIn, setIsLoggingIn] = useState(false);
  const [hidePassword, setHidePassword] = useState(true);
  const { setIsViewerAuthenticated, setViewerInfo } = useAuthStore(store => store);

  const loginHander = async (values: { username: string; password: string }): Promise<void> => {
    try {
      setIsLoggingIn(true);
      const responseToken = await apiLogin(values.username, values.password);
      if (!responseToken) {
        throw new Error('Access Denied');
      }
      await setIsViewerAuthenticated(responseToken);
      const responseViewerInfo = await apiGetCurrentUser();
      await setViewerInfo(responseViewerInfo);
    } catch (error: any) {
      throw error;
    } finally {
      setIsLoggingIn(false);
    }
  };

  return (
    <MainView>
      <Formik
        initialValues={{
          username: '',
          password: '',
          submitError: null
        }}
        validationSchema={validationSchema}
        onSubmit={(values, { setErrors }) =>
          loginHander(values).catch(error => setErrors({ submitError: error.message }))
        }
      >
        {({
          handleChange,
          handleBlur,
          handleSubmit,
          values,
          errors
          // isValid, dirty
        }) => (
          <View style={styles.container}>
            <View style={styles.form}>
              <View>
                <TextInput
                  style={styles.inputMain}
                  placeholder="Username"
                  onBlur={handleBlur('username')}
                  onChangeText={handleChange('username')}
                  value={values.username}
                />
                {errors.username && <Text style={styles.error}>{errors.username}</Text>}
              </View>
              <View>
                <View style={styles.inputContainer}>
                  <TextInput
                    style={styles.inputPswd}
                    placeholder="Password"
                    secureTextEntry={hidePassword}
                    onBlur={handleBlur('password')}
                    onChangeText={handleChange('password')}
                    value={values.password}
                  />
                  <Icon
                    style={styles.eyeIcon}
                    onPress={() => setHidePassword(!hidePassword)}
                    name={hidePassword ? 'eye-off' : 'eye'}
                    size={20}
                  />
                </View>
                {errors.password && <Text style={styles.error}>{errors.password}</Text>}
              </View>
              <View>
                <BtnMain
                  btnName="Login"
                  // isDisabled={isLoggingIn || !dirty || !isValid}
                  isLoading={isLoggingIn}
                  btnStyles={styles.btn}
                  btnTextStyles={styles.txtLogin}
                  onPress={handleSubmit}
                />
                {errors.submitError && <Text style={styles.submitError}>{errors.submitError}</Text>}
              </View>
            </View>
          </View>
        )}
      </Formik>
    </MainView>
  );
}

package.json

{
  "name": "hello_world",
  "version": "1.0.0",
  "main": "node_modules/expo/AppEntry.js",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "test": "jest --config=jest.config.json",
    "test:coverage": "jest --config=jest.config.json --coverage",
    "test:watch": "jest --config=jest.config.json --watch"
  },
  "dependencies": {
    "@react-native-masked-view/masked-view": "0.2.8",
    "@react-native-picker/picker": "^1.8.3",
    "@react-navigation/native": "^6.1.6",
    "@react-navigation/native-stack": "^6.9.12",
    "@react-navigation/stack": "^6.3.16",
    "axios": "^1.4.0",
    "expo": "~48.0.15",
    "expo-constants": "~14.2.1",
    "expo-linear-gradient": "~12.1.2",
    "expo-linking": "~4.0.1",
    "expo-router": "^1.5.3",
    "expo-secure-store": "~12.1.1",
    "expo-splash-screen": "~0.18.2",
    "expo-status-bar": "~1.4.4",
    "formik": "^2.4.2",
    "jest-expo": "^49.0.0",
    "lodash": "^4.17.21",
    "react": "18.2.0",
    "react-native": "0.71.8",
    "react-native-config": "^1.5.1",
    "react-native-gesture-handler": "~2.9.0",
    "react-native-linear-gradient": "^2.6.2",
    "react-native-picker-select": "^8.0.4",
    "react-native-safe-area-context": "4.5.0",
    "react-native-screens": "~3.20.0",
    "yup": "^1.2.0",
    "zustand": "^4.3.8"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@babel/preset-env": "^7.22.9",
    "@jest/globals": "^29.6.1",
    "@testing-library/jest-native": "^5.4.2",
    "@testing-library/react-native": "^12.1.2",
    "@types/jest": "^29.5.3",
    "@types/lodash.debounce": "^4.0.7",
    "@types/node": "^20.4.2",
    "@types/react": "~18.0.14",
    "@types/react-native": "^0.72.2",
    "@types/react-test-renderer": "^18.0.0",
    "@typescript-eslint/eslint-plugin": "^6.1.0",
    "@typescript-eslint/parser": "^6.1.0",
    "babel-jest": "^29.6.1",
    "babel-plugin-jest-hoist": "^29.5.0",
    "babel-plugin-module-resolver": "^5.0.0",
    "eslint": "^8.45.0",
    "eslint-config-prettier": "^8.8.0",
    "eslint-plugin-header": "^3.1.1",
    "eslint-plugin-import": "^2.27.5",
    "eslint-plugin-jsdoc": "^46.4.4",
    "eslint-plugin-jsx-a11y": "^6.7.1",
    "eslint-plugin-prettier": "^5.0.0",
    "eslint-plugin-react": "^7.32.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "jest": "^29.2.1",
    "jest-environment-jsdom": "^29.6.1",
    "react-test-renderer": "^18.2.0",
    "ts-jest": "^29.1.1",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.4"
  },
  "private": true,
}

Edit

jest.config.json

{
  "preset": "jest-expo",
  "transformIgnorePatterns": [
    "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|native-notify)"
  ],
  "testEnvironment": "node",
  "testMatch": ["**/*.spec.{js,jsx,ts,tsx}", "**/*.test.{js,jsx,ts,tsx}"],
  "collectCoverageFrom": [
    "<rootDir>/**/*.{js,jsx,ts,tsx}",
    "**/*.{js,jsx,ts,tsx}",
    "!**/coverage/**",
    "!**/node_modules/**",
    "!**/babel.config.js"
  ],
  "coveragePathIgnorePatterns": ["\\\\node_modules\\\\"],
  "globals": {
    "ts-jest": {
      "diagnostics": false,
      "tsConfig": "tsconfig.json"
    }
  },
  "moduleFileExtensions": ["ts", "tsx", "js", "jsx"],
  "moduleDirectories": ["node_modules"]
}

tsconfig.json

{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "jsx": "react",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "node",
    "noEmit": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": false,
    "paths": {
      "@/*": ["/*/index", "/*"],
      "@app/*": ["app/*/index", "app/*"]
    },
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "es5",
    "types": ["node", "jest"],
    "useUnknownInCatchVariables": true
  },
  "include": ["app", "App.tsx", "test", "App.test.tsx", "**/*.[jt]s?(x)"],
  "exclude": ["node_modules", ".expo", "yarn.lock", "coverage"]
}

tsconfig.spec.json

{
  "extends": "./tsconfig.json"
}

.eslint.json

{
  "env": {
    "browser": true,
    "es2021": true,
    "jest": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "prettier",
    "plugin:import/errors",
    "plugin:import/warnings",
    "plugin:import/typescript"
  ],
  "globals": {
    "fetch": false
  },
  "overrides": [
    {
      "files": ["*.js", "*.mjs"],
      "rules": {
        "@typescript-eslint/ban-types": "off",
        "@typescript-eslint/explicit-module-boundary-types": "off",
        "@typescript-eslint/no-empty-function": "off",
        "@typescript-eslint/no-empty-interface": "off",
        "@typescript-eslint/no-explicit-any": "off",
        "@typescript-eslint/no-non-null-assertion": "off",
        "@typescript-eslint/no-var-requires": "off"
      }
    },
    {
      "files": ["*.ts", "*.tsx"],
      "extends": [
        "plugin:@typescript-eslint/recommended",
        "plugin:@typescript-eslint/recommended-requiring-type-checking"
      ],
      "parserOptions": {
        "project": ["./tsconfig.json"]
      },
      "rules": {
        "@typescript-eslint/explicit-module-boundary-types": "off",
        "@typescript-eslint/no-floating-promises": "off"
      }
    }
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "allowImportExportEverywhere": true,
    "ecmaFeatures": { "jsx": true },
    "ecmaVersion": "latest",
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "plugins": ["@typescript-eslint", "import", "prettier", "react", "react-hooks"],
  "rules": {
    "@typescript-eslint/ban-types": [
      "error",
      {
        "extendDefaults": true,
        "types": { "{}": false }
      }
    ],
    "@typescript-eslint/explicit-module-boundary-types": "warn",
    "@typescript-eslint/no-empty-function": "off",
    "@typescript-eslint/no-empty-interface": "off",
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/no-non-null-assertion": "off",
    "class-methods-use-this": "off",
    "comma-dangle": "off",
    "indent": "off",
    "indent-legacy": 0,
    "import/no-unresolved": 0,
    "import/named": 0,
    "import/namespace": 0,
    "import/default": 0,
    "import/no-named-as-default-member": 0,
    "no-param-reassign": [2, { "props": false }],
    "no-tabs": ["off", { "allowIndentationTabs": true }],
    "no-use-before-define": "warn",
    "no-unused-vars": "warn",
    "quotes": ["error", "single", { "avoidEscape": true }],
    "react-hooks/rules-of-hooks": "off",
    "react-hooks/exhaustive-deps": "warn",
    "react/jsx-filename-extension": "off",
    "react/jsx-uses-react": "off",
    "react/jsx-uses-vars": "error",
    "react/prop-types": "off",
    "react/react-in-jsx-scope": "off",
    "react/require-default-props": "off",
    "sort-imports": [
      "error",
      {
        "ignoreCase": false,
        "ignoreDeclarationSort": true,
        "ignoreMemberSort": false,
        "memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
      }
    ]
  },
  "settings": {
    "react": {
      "createClass": "createReactClass",
      "pragma": "React",
      "fragment": "Fragment",
      "version": "detect"
    }
  }
}

Other passing testcase App.test.tsx

import React from 'react';
import { render, screen } from '@testing-library/react-native';

import App from './App';

jest.useFakeTimers();

describe('Loading screen is shown when App starts', () => {
  it('has 1 child', async () => {
    render(<App />);
    const loadingText = screen.getByText('Loading');
    expect(loadingText).toBeTruthy();
  });
});

App.tsx

import React, { useEffect } from 'react';
import { NavigationConductor } from '@app/navigation';
import { useLoadingStore } from '@app/stores';

export default function App() {
  // variables
  const { appStartInitializeData } = useLoadingStore(store => store);

  // setup
  useEffect(() => {
    appStartInitializeData();
  }, []);

  // render
  return <NavigationConductor />;
}

enter image description here

1

There are 1 answers

6
bytehala On BEST ANSWER

It's difficult to replicate what you're experiencing without seeing how your jest.config.json and jestsetup files look like.

However, based on the error message, it looks like expo-asset is not being mocked properly.

You can try this solution:

// Inside the jestsetup.js file
jest.mock('expo-font');
jest.mock('expo-asset');

If that doesn't work, you'll have to show us your jest config and setup files.

Source: https://github.com/expo/expo/issues/21434#issuecomment-1451498428