So, my issue is a bit difficult to descibe because our repository (which is at work, so I unfortunately can't just provide a link to it) is pretty big and complex, but I'll do my best. First of all, the problem:
The Problem
I run jest
and the result is something like the following:
FAIL packages/components/src/PlusMinus/index.test.tsx● Test suite failed to run TypeError: Cannot read properties of undefined (reading 'spacing') 15 | justifyContent: 'center', 16 | alignItems: 'center',> 17 | paddingRight: Metrics.spacing.xxs, | ^ 18 | }, 19 | }); 20 | at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27) at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1)Test Suites: 1 failed, 1 total
The error message is the same for all tests of files that import anything from inside our monorepo (via @packages/
alias) – incl. the exact same component, IconWithBadge
that apparently can't import the Metrics
module. See below for the complete output of all our current test suites.
PASS packages/package1/src/utils/index.test.ts Testing *****function()√ Normal items (5 ms)√ Other items (2 ms)√ Combo items (5 ms) Testing *****function()√ Multiple identical items (1 ms)√ Multiple different items, one theme each (1 ms)√ Multiple different items, with a unique theme for each entry (3 ms) PASS packages/utils/src/transformations/transformations.test.ts test transformation helpers√ spliceImmutably() (4 ms)√ momentToShortenedDateString() (4 ms)√ sortByProperty() (3 ms)√ roundByPrecision() (1 ms)√ parseDotNotation()√ versionStringToArray() (1 ms)√ capitalizeString() (1 ms)√ normalizeString() (2 ms)√ normalizeUmlauts1() (3 ms)√ normalizeUmlauts2() (2 ms) FAIL packages/utils/src/validation/validation.test.ts● Test suite failed to run TypeError: Cannot read properties of undefined (reading 'spacing') 15 | justifyContent: 'center', 16 | alignItems: 'center',> 17 | paddingRight: Metrics.spacing.xxs, | ^ 18 | }, 19 | }); 20 | at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27) at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1) FAIL packages/customer/src/screens/Profile/components/CustomerCard/components/PinInput/index.test.tsx● Test suite failed to run TypeError: Cannot read properties of undefined (reading 'spacing') 15 | justifyContent: 'center', 16 | alignItems: 'center',> 17 | paddingRight: Metrics.spacing.xxs, | ^ 18 | }, 19 | }); 20 | at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27) at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1) FAIL packages/package2/src/screens/Screen1/components/Component1/index.test.tsx● Test suite failed to run TypeError: Cannot read properties of undefined (reading 'spacing') 15 | justifyContent: 'center', 16 | alignItems: 'center',> 17 | paddingRight: Metrics.spacing.xxs, | ^ 18 | }, 19 | }); 20 | at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27) at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1) FAIL packages/components/src/PlusMinus/index.test.tsx● Test suite failed to run TypeError: Cannot read properties of undefined (reading 'spacing') 15 | justifyContent: 'center', 16 | alignItems: 'center',> 17 | paddingRight: Metrics.spacing.xxs, | ^ 18 | }, 19 | }); 20 | at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27) at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1) FAIL packages/package3/src/SomeService/utils/index.test.ts● Test suite failed to run TypeError: Cannot read properties of undefined (reading 'spacing') 15 | justifyContent: 'center', 16 | alignItems: 'center',> 17 | paddingRight: Metrics.spacing.xxs, | ^ 18 | }, 19 | }); 20 | at Object.<anonymous> (packages/components/src/IconWithBadge/styles.ts:17:27) at Object.<anonymous> (packages/components/src/IconWithBadge/index.tsx:9:1)Test Suites: 5 failed, 2 passed, 7 totalTests: 16 passed, 16 totalSnapshots: 0 totalTime: 8.832 s
It seems relatively obvious to me that this must be some kind of compilation error because the important thing to note here is:
In a React Native context all of this code works perfectly fine!
Meaning, when building and and running the app via react-native
commands, like usual, everything works and I don't get any errors. It's only in the context of jest
testing that I get this error.
Also, let's say, I remove all instances of Metrics
from IconWithBadge
and run jest
again. Then, it will find another file that imports Metrics
where it's miraculously undefined
.
Now, to give you an overview of how our repository is structured and configured, let's have a look at the environment.
The Environment
We have a monorepo with a structure similar to this:
.├── app-directory│├── android│├── ios│├── src││├── navigation│││└── ... all the react-navigation navigators││└── App.tsx│├── babel.config.js│├── index.js│├── metro.config.js│├── package.json│├── react-native.config.js│└── tsconfig.json├── e2e│├── tests││└── testSomething.e2e.ts│├── config.json│└── environment.js├── packages│├── package1││├── src││├── package.json││├── tsconfig.build.json││└── tsconfig.json│├── package2││├── src││├── package.json││├── tsconfig.build.json││└── tsconfig.json│└── ...more packages, all look similar├── .eslintignore├── .eslintrc.yml├── .gitattributes├── .gitignore├── .babel.config.js├── detox.config.js├── get-babel-config.js├── jest.config.js├── package.json├── tsconfig.app.json├── tsconfig.json├── tsconfig.spec.json└── yarn.lock
We have both component tests (via jest-native and RN Testing Library) and plain unit tests inside of the repo. We also have detox
tests, as you can deduce from the existence of an e2e
directory, but that's not important at the moment.
All code (test and app code) is written in TypeScript, and for the React Native part it's all compiled via Babel, as is recommended by the RN team. For the tests however, I have configured ts-jest to compile the code to CommonJS, which is required by jest
. It is specifically configured to compile the TypeScript code with ttypescript
and use babel-jest
for all JavaScript code.
To show you how exactly it is all configured, I'll paste my most important configs here:
The Configs
Jest Config
Since we use Expo's unimodules, we also have jest-expo
installed, which sets up the Expo-side of jest-testing properly. With this and ts-jest
in mind, our jest.config.js
looks like this:
const cloneDeep = require('lodash/cloneDeep');const expoPreset = cloneDeep(require('jest-expo/jest-preset'));const tsjPreset = cloneDeep(require('ts-jest/presets').jsWithBabel);delete expoPreset.transform['^.+\\.(js|ts|tsx)$'];/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */const config = { bail: true, globals: {'ts-jest': { compiler: 'ttypescript', tsconfig: 'tsconfig.spec.json', isolatedModules: true, diagnostics: { warnOnly: true, }, }, }, moduleDirectories: ['node_modules', // add the directory with the test-utils.js file, for example:'test-utils', // a utility folder'packages', // packages folder __dirname, // the root directory ], moduleFileExtensions: ['ts', 'tsx', 'js'], moduleNameMapper: {'^@packages/utils/(.*)$': '<rootDir>/packages/utils/src/$1','^@packages/services/(.*)$': '<rootDir>/packages/services/src/$1','^@packages/(.*)$': '<rootDir>/packages/$1/src','^test-utils$': '<rootDir>/test-utils', }, rootDir: process.cwd(), setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'], testTimeout: 10000, transform: Object.assign(tsjPreset.transform, expoPreset.transform), transformIgnorePatterns: [ `${ expoPreset.transformIgnorePatterns[0].split('svg)')[0] }svg|@packages/*|react-native-ultimate-config|react-native-calendars|react-native-permissions|react-native-swipe-gestures|react-native-iphone-x-helper|react-native-webview|react-native-reanimated|react-native-qrcode-svg|react-native-linear-gradient|@react-native-masked-view/*|react-native-svg-path-gradient|react-native-safe-area-context|redux-middleware-flipper|react-native-flipper)`, ], verbose: true,};module.exports = Object.assign(expoPreset, Object.assign(tsjPreset, config));
Babel config
./get-babel-config.js
/** * @param {import('@types/babel__core').ConfigAPI} api * @param {boolean} isRoot * @returns {import('@types/babel__core').TransformOptions} */module.exports = function (api, isRoot = true) { const relativeRoot = isRoot ? './' : '../'; /** @type {import('@types/babel__core').TransformOptions} */ const config = { presets: [ ['@babel/preset-env', {targets: 'defaults', loose: true}], ['module:metro-react-native-babel-preset', {useTransformReactJSXExperimental: true}], ], sourceMaps: 'inline', }; config.plugins = ['lodash', ['module-resolver', { root: [relativeRoot], extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'], alias: {'^@packages/services/(.+)': `${relativeRoot}packages/services/src/\\1`,'^@packages/utils/(.+)': `${relativeRoot}packages/utils/src/\\1`,'^@packages/(.+)': `${relativeRoot}packages/\\1/src`,'^test-utils': `${relativeRoot}test-utils`, }, }, ], ['@babel/plugin-transform-react-jsx', {runtime: 'automatic'}], ]; if (api.env('production')) { config.plugins.push(['transform-remove-console', {exclude: ['error', 'warn', 'info', 'time', 'timeEnd']}, ]); } return config;};
./app-directory/babel.config.js
const getConfig = require('../get-babel-config');module.exports = function (api) { return getConfig(api, false);};
./babel.config.js
const getConfig = require('./get-babel-config');module.exports = function (api) { return getConfig(api);};
TypeScript Config
./tsconfig.json
{"compilerOptions": {"target": "ES6","module": "ES6","allowJs": false,"jsx": "react-native","resolveJsonModule": true,"declaration": true,"declarationMap": true,"sourceMap": true,"composite": true,"importHelpers": true,"isolatedModules": true,"strict": true,"noImplicitAny": true,"strictNullChecks": true,"strictFunctionTypes": true,"strictPropertyInitialization": true,"skipLibCheck": true,"noImplicitThis": true,"alwaysStrict": true,"noUnusedLocals": true,"noUnusedParameters": true,"noImplicitReturns": true,"forceConsistentCasingInFileNames": true,"moduleResolution": "node","baseUrl": ".","paths": {"@packages/*": ["./packages/*/src" ],"@packages/services/*": ["./packages/services/src/*" ],"@packages/utils/*": ["./packages/utils/src/*" ] },"types": ["node","jest","detox" ],"allowSyntheticDefaultImports": true,"esModuleInterop": true,"plugins": [ {"transform": "@zerollup/ts-transform-paths" }, {"transform": "typescript-transform-react-jsx-source" } ] },"exclude": ["node_modules","**/*.js" ]}
./tsconfig.spec.json
{"extends": "./tsconfig.json","compilerOptions": {"target": "ES5","jsx": "react-jsx","paths": {"@packages/*": ["./packages/*/src" ],"@packages/services/*": ["./packages/services/src/*" ],"@packages/utils/*": ["./packages/utils/src/*" ],"test-utils": ["./test-utils" ] } }}
Packacke JSON
./package.json
{"private": true,"workspaces": {"packages": ["packages/*","app-directory" ] },"license": "MIT","scripts": {"lint": "eslint \"**/*.{js,ts,tsx}\"","typescript": "tsc","test:jest": "jest","test:jest:plusminus-only": "jest ./packages/components/src/PlusMinus/index.test.tsx","test:detox-ios": "detox build -c ios && detox test -c ios -l verbose","test:detox-ios-ci": "detox build -c ios && detox test -c ios --workers 2 --headless --record-logs all --cleanup","test:detox-android": "detox build -c android && detox test -c android -l verbose --record-logs all","test:detox-android-ci": "detox build -c android && detox test -c android --workers 2 --headless --record-logs all --cleanup","publish": "lerna publish","prerelease": "lerna run clean","in-app": "yarn --cwd app-directory","postinstall": "patch-package" },"devDependencies": {"@babel/cli": "^7.15.4","@babel/core": "^7.15.5","@babel/preset-env": "^7.15.6","@babel/runtime": "^7.15.4","@packages/build-tools": "^1.0.31","@react-native-community/eslint-config": "^3.0.1","@testing-library/jest-native": "^4.0.2","@testing-library/react-native": "^7.2.0","@types/detox": "^17.14.2","@types/i18n-js": "^3.8.2","@types/jest": "^27.0.1","@types/lodash": "^4.14.173","@types/node": "^16.9.2","@types/react": "^17.0.21","@types/react-native": "^0.65.0","@types/react-native-calendars": "^1.1264.2","@types/react-native-vector-icons": "^6.4.8","@types/react-test-renderer": "^17.0.1","@types/react-redux": "^7.1.18","@typescript-eslint/eslint-plugin": "^4.31.1","@typescript-eslint/parser": "^4.31.1","@zerollup/ts-transform-paths": "^1.7.18","babel-jest": "^27.2.0","babel-plugin-lodash": "^3.3.4","babel-plugin-module-resolver": "^4.1.0","babel-plugin-transform-remove-console": "^6.9.4","detox": "^18.20.3","eslint": "^7.32.0","eslint-config-prettier": "^8.3.0","eslint-plugin-detox": "^1.0.0","eslint-plugin-import": "^2.24.2","eslint-plugin-jest": "^24.4.2","eslint-plugin-prettier": "^4.0.0","eslint-plugin-testing-library": "^4.12.2","jest": "^27.2.0","jest-expo": "^42.1.0","lerna": "^4.0.0","metro-react-native-babel-preset": "^0.66.2","patch-package": "^6.4.7","postinstall-postinstall": "^2.1.0","prettier": "^2.4.1","react-native-codegen": "^0.0.7","react-test-renderer": "^17.0.2","run-script-os": "^1.1.6","sanitize-filename": "^1.6.3","ts-jest": "^27.0.5","tslib": "^2.3.1","ttypescript": "^1.5.12","typescript": "^4.4.3","typescript-transform-react-jsx-source": "^2.0.0" },"prettier": {"tabWidth": 2,"bracketSpacing": false,"jsxBracketSameLine": true,"singleQuote": true,"trailingComma": "all","printWidth": 100,"endOfLine": "lf" },"resolutions": {"jest-expo/babel-jest": "^27.2.0","jest-expo/jest": "^27.2.0","jest-expo/react-test-renderer": "^17.0.2" }}
./app-directory/package.json
{"private": true,"scripts": {"start-server": "react-native start --reset-cache","doctor": "react-native doctor","run-app": "react-native run-android --variant=devdemoDebug","run-app:release": "react-native run-android --variant=devdemoRelease","install-pods": "rm -rf ios/Pods && cd ios && pod install --clean-install && cd ..","test": "jest --passWithNoTests" },"dependencies": {"@packages/authentication": "^1.0.31","@packages/package1": "^1.0.31","@packages/package2": "^1.0.31","@packages/components": "^1.0.31","@packages/customer": "^1.0.31","@packages/errors": "^1.0.31","@packages/package3": "^1.0.31","@packages/package4": "^1.0.31","@packages/package5": "^1.0.31","@packages/hooks": "^1.0.31","@packages/package6": "^1.0.31","@packages/package7": "^1.0.31","@packages/package8": "^1.0.31","@packages/screens": "^1.0.31","@packages/services": "^1.0.31","@packages/store": "^1.0.31","@packages/themes": "^1.0.31","@packages/utils": "^1.0.31","@packages/package9": "^1.0.31","@packages/package10": "^1.0.31","@ctrl/tinycolor": "^3.4.0","@react-native-async-storage/async-storage": "^1.15.8","@react-native-clipboard/clipboard": "^1.8.4","@react-native-community/blur": "^3.6.0","@react-native-community/netinfo": "^6.0.2","@react-native-community/viewpager": "4.x","@react-native-masked-view/masked-view": "^0.2.6","@react-navigation/bottom-tabs": "^5.11.15","@react-navigation/drawer": "^5.12.9","@react-navigation/native": "^5.9.8","@react-navigation/stack": "^5.14.9","@reduxjs/toolkit": "^1.6.1","expo-barcode-scanner": "^10.2.2","expo-local-authentication": "^11.1.1","lottie-ios": "3.2.3","lottie-react-native": "^4.0.3","moment": "^2.29.1","react": "^17.0.2","react-native": "^0.65.1","react-native-bootsplash": "^3.2.5","react-native-camera": "3.x","react-native-console-time-polyfill": "^1.2.3","react-native-device-info": "^8.3.3","react-native-exception-handler": "^2.10.10","react-native-flipper": "^0.109.0","react-native-gesture-handler": "^1.10.3","react-native-inappbrowser-reborn": "^3.6.3","react-native-keychain": "^7.0.0","react-native-linear-gradient": "^2.5.6","react-native-localize": "^2.1.4","react-native-permissions": "^3.0.5","react-native-reanimated": "^1.13.3","react-native-restart": "^0.0.22","react-native-safe-area-context": "^3.3.2","react-native-screens": "^3.7.2","react-native-share": "^7.1.0","react-native-simple-toast": "^1.1.3","react-native-svg": "^12.1.1","react-native-ultimate-config": "^3.4.1","react-native-unimodules": "^0.14.8","react-native-url-polyfill": "^1.3.0","react-native-vector-icons": "7.0.0","react-native-view-shot": "^3.1.2","react-native-wallet-passes": "^1.2.2","react-native-webview": "11.13.0","react-redux": "^7.2.5","redux": "^4.1.1","rn-fetch-blob": "^0.12.0" },"devDependencies": {"@packages/types-and-enums": "^1.0.31" }}
Summary
This should be all the pertinent information. If there's anything missing/you need additional info on anything, let me know! To sum up the question:
Why is
jest
not able to resolve this package when it works totally fine during normal development/production? How might I be able to solve this problem?
Asking all jest
experts here! 😅 I hope you can help me.