Jest setup for Native-Base on Nextjs with React-Native-Web
Miguel Niblock
Posted on May 31, 2022
TLDR
Here's a finished working example on how to configure jest to work with native-base on nextjs and react-native-web. https://github.com/MiguelNiblock/next-jest-nativebase
Intro
Native-base is my favorite ui component library for react-native. It works nicely on web and setup is easy with expo or even react-native-web. However, testing with jest becomes exponentially more challenging once you add this type of library into your project. Testing setup is no longer trivial, you really need to know what you're doing as you'll see below. By looking for solutions online I've come across many comments where people say they rather ditch using their favorite ui library, than forgo proper testing. My intention with this article has several purposes:
- To help others who have faced this situation in the pursuit of a universal react-native app.
- To urge library makers to simplify things for their users by either improving their documentation, or by shipping their code pre-compiled.
Background
I installed nativebase on a nextjs project managed by nx monorepo, and jest test fails to render the app due to safeareacontext issues.
I already read the testing article in docs, but it didn't help me. https://docs.nativebase.io/testing because when I add the mock from the setupFile.js, it also complains it cannot use import statement outside a module, this happens when the mock file from safeareacontext imports react.
Since nx makes things more complex, I'm making a minimal repro of the testing issue, in the hopes I can get a minimal sample to work correctly with some help.
templates provided by nativebase have no jest config. none of them. do u guys even test your library?. https://github.com/GeekyAnts/nativebase-templates
nextjs does come with basic jest setup, so I will follow the instructions to install on an existing nextjs project. there's one which uses expo adapter: https://docs.nativebase.io/install-next and another one which uses the native-base next adapter https://docs.nativebase.io/next-adapter . I don't know what the difference is, but I don't think it should affect testing. I'll go for using the native-base/next adapter, since in my nx monorepo the nextjs project is completely independent of the react-native project. If you're using expo, perhaps do the other version.
Research similar issues
this has a potential solution by adding transformignore patterns
https://github.com/GeekyAnts/NativeBase/issues/3045#issuecomment-580956513
this talks about the initial window metrics
https://github.com/GeekyAnts/NativeBase/issues/3907
this one also has the issue of "cannot import statement outside a module"
potential solution jest config https://github.com/GeekyAnts/NativeBase/issues/3105#issuecomment-886034272
another example of adding a transformIgnorepattern to solve the import issue https://github.com/GeekyAnts/NativeBase/issues/3001#issuecomment-718617355
same issue, no solution https://github.com/GeekyAnts/NativeBase/issues/1215
same issue https://stackoverflow.com/questions/45197514/unexpected-token-import-error-crna
another example of a transformignorepattern that helped some people https://github.com/GeekyAnts/NativeBase/issues/977#issuecomment-322541757
https://github.com/GeekyAnts/NativeBase/issues/977#issuecomment-353689768
https://github.com/GeekyAnts/NativeBase/issues/977#issuecomment-354403658
the "correct config for 2022" https://stackoverflow.com/a/71575172
or set up jest for esmodules
https://jestjs.io/docs/ecmascript-modules
but it doesn't support jest.mock in es module for jest
cool syntax to create pattern https://github.com/nrwl/nx/issues/812#issuecomment-429420861
https://github.com/nrwl/nx/issues/812#issuecomment-429488470
nx specific solution with allowJs https://github.com/nrwl/nx/issues/812#issuecomment-429467823
with nx and babel preset env https://github.com/nrwl/nx/issues/812#issuecomment-799930598
jest article from nextjs docs https://nextjs.org/docs/testing#setting-up-jest-with-babel
Start with a nextjs template ready for jest
so I will create a nextjs app from the "with-jest-babel" template. https://github.com/vercel/next.js/tree/canary/examples/with-jest-babel
tests pass initially. app runs.
this is the simple default test I'll be running throughout the following sections:
describe('Home', () => {
it('renders a heading', () => {
render(<Home />)
const heading = screen.getByRole('heading', {
name: /welcome to next\.js!/i,
})
expect(heading).toBeInTheDocument()
})
})
Install native base on existing nextjs
now I'll follow the instructions to install native-base with next adapter. Follow either this https://docs.nativebase.io/next-adapter or this https://docs.nativebase.io/install-next.
yarn add react-native-web native-base react-native-svg react-native-safe-area-context react-native
yarn add @native-base/next-adapter next-compose-plugins next-transpile-modules next-fonts -D
make pages/_document.ts
export { default } from "@native-base/next-adapter/document"
create next.config.js with initial adapter setup. Template didn't come with one, and native-base instructions say next.config.json. but that's wrong, it's "js".
configure the hello world example in _app.tsx
and index.tsx
.
Running next after installing native-base
with those changes, starting with a blank nextjs conf, now the nextjs app doesn't start.
error when running yarn dev (next dev
) :
Server Error
Error: "Document.getInitialProps()" should resolve to an object with a "html" prop set with a valid html string
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Call Stack
documentInitialProps
file:/home/user/MyProjects/next-jest-nativebase/node_modules/next/dist/server/render.js (822:23)
Honestly I expected Next to at least run after installing that next adapter. Now we must first fix Next, and then fix Jest, to work with native-base. Not fun, but won't give up.
I looked up some solutions online and tried to modify the document from native-base, but I hit a wall, so I ended up deleting the _document.js
altogether and then Nextjs could build with native-base hello world text.
this should probably be fixed and done right. but that's a different rabbit hole.
now back to testing.
Jest after installing native-base
the error is:
/home/user/MyProjects/next-jest-nativebase/node_modules/react-native/index.js:14
import typeof AccessibilityInfo from './Libraries/Components/AccessibilityInfo/AccessibilityInfo';
^^^^^^
SyntaxError: Cannot use import statement outside a module
that means it's trying to import stuff from react-native. I'm not sure if the solution is to alias react-native to react-native-web, or if the solution is to add react-native to transformIgnorePatterns.
Attempts:
1.
transformIgnorePatterns: "/node_modules/?!(react-native)"
SyntaxError: /home/user/MyProjects/next-jest-nativebase/node_modules/react-native/index.js: Unexpected token, expected "{" (14:7)
12 |
13 | // Components
> 14 | import typeof AccessibilityInfo from './Libraries/Components/AccessibilityInfo/AccessibilityInfo';
| ^
15 | import typeof ActivityIndicator from './Libraries/Components/ActivityIndicator/ActivityIndicator';
error was similar, but different. seems like now it's a type transpilation issue. react-native uses flow for types, so perhaps I need a preset or plugin that transpiles flow.
https://babeljs.io/docs/en/babel-preset-flow.html
2.
decided to add the suggestion in react-native-web docs for Jest seup, and add jest config preset: 'react-native-web'
. The error was now different but also suggests Flow must be transpiled
SyntaxError: /home/user/MyProjects/next-jest-nativebase/node_modules/react-native/Libraries/Utilities/codegenNativeComponent.js: Unexpected token, expected "from" (14:12)
12 |
13 | import requireNativeComponent from '../../Libraries/ReactNative/requireNativeComponent';
> 14 | import type {HostComponent} from '../../Libraries/Renderer/shims/ReactNativeTypes';
| ^
15 | import UIManager from '../ReactNative/UIManager';
3.
decided to add the "@babel/preset-flow" preset. error was now:
Cannot find module '../Utilities/Platform' from 'node_modules/react-native/Libraries/StyleSheet/processColor.js'
Require stack:
node_modules/react-native/Libraries/StyleSheet/processColor.js
node_modules/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js
node_modules/react-native/Libraries/ReactNative/getNativeComponentAttributes.js
node_modules/react-native/Libraries/ReactNative/requireNativeComponent.js
node_modules/react-native/Libraries/Utilities/codegenNativeComponent.js
node_modules/react-native-safe-area-context/lib/commonjs/specs/NativeSafeAreaProvider.js
node_modules/react-native-safe-area-context/lib/commonjs/NativeSafeAreaProvider.js
node_modules/react-native-safe-area-context/lib/commonjs/SafeAreaContext.js
node_modules/react-native-safe-area-context/lib/commonjs/index.js
node_modules/native-base/lib/commonjs/hooks/useSafeArea/index.js
node_modules/native-base/lib/commonjs/hooks/index.js
node_modules/native-base/lib/commonjs/components/primitives/Box/index.js
node_modules/native-base/lib/commonjs/components/composites/AspectRatio/index.js
node_modules/native-base/lib/commonjs/components/composites/index.js
node_modules/native-base/lib/commonjs/index.js
pages/index.tsx
__tests__/index.test.tsx
However, Jest was able to find:
'../Utilities/Platform.android.js'
'../Utilities/Platform.ios.js'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'json', 'node'].
See https://jestjs.io/docs/configuration#modulefileextensions-arraystring
interesting. so the Box is making use of safearea, which makes use of some react-native utils. and only native (mobile) ones are provided.
- so this tells me the type issue is gone, and now the issue is those react-native libs have no place in the Next web build. So perhaps we do have to manually alias react-native-web here in jest.
- Or to mock safearea.
4.
just as an experiment, I disabled the 'react-native-web' preset, (To see if it's actually doing something) and the error was:
ReferenceError: __DEV__ is not defined
5 |
6 | import React from "react"
> 7 | import { Box } from "native-base"
| ^
8 |
9 | export default function Home() {
10 | return (
5.
I reverted that, and added the module name mapper alias in jest, as suggested in docs of react-native-web
https://necolas.github.io/react-native-web/docs/multi-platform/
moduleNameMapper: {
...
"react-native$": "react-native-web",
...
},
got same error as attempt #3. so that means I do need to mock safearea.
there's the nb docs: https://docs.nativebase.io/testing but how do I actually implement that? I'm not testing the provider specifically I just want to make sure the whole app renders correctly. The Home component is calling the safearea from within the native-base internal dependency of component Box -> useSafeArea -> SafeAreaContext.
authors provide more info https://github.com/th3rdwave/react-native-safe-area-context#testing
they suggest adding this to jest setup file.
import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';
jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
6.
added the mock, as directed, and got this error:
Cannot find module '../Utilities/Platform' from 'node_modules/react-native/Libraries/StyleSheet/processColor.js'
Require stack:
node_modules/react-native/Libraries/StyleSheet/processColor.js
node_modules/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js
node_modules/react-native/Libraries/ReactNative/getNativeComponentAttributes.js
node_modules/react-native/Libraries/ReactNative/requireNativeComponent.js
node_modules/react-native/Libraries/Utilities/codegenNativeComponent.js
node_modules/react-native-safe-area-context/lib/commonjs/specs/NativeSafeAreaProvider.js
node_modules/react-native-safe-area-context/lib/commonjs/NativeSafeAreaProvider.js
node_modules/react-native-safe-area-context/lib/commonjs/SafeAreaContext.js
node_modules/react-native-safe-area-context/lib/commonjs/index.js
node_modules/react-native-safe-area-context/jest/mock.js
jest.setup.js
However, Jest was able to find:
'../Utilities/Platform.android.js'
'../Utilities/Platform.ios.js'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'json', 'node'].
See https://jestjs.io/docs/configuration#modulefileextensions-arraystring
this is the same error we got before (attempt #5), but this time it's being initiated from the setup file, that's the only difference. still the issue is that the mock is making use of mobile code and there's no replacement for it on web.
interesting to note, is the fact that the name alias of react-native to react-native-web is not being honored according to the call stack.
7.
I then adjusted the pattern of the module name mapper. From "react-native$": "react-native-web",
to "react-native": "react-native-web",
( Just removed the "$")
got new error:
TypeError: Cannot read property 'default' of undefined
8 | import mockSafeAreaContext from "react-native-safe-area-context/jest/mock"
9 |
> 10 | jest.mock("react-native-safe-area-context", () => mockSafeAreaContext)
| ^
11 |
this difference means the "$" was indeed preventing the resolution of rnw in place of react-native.
there's not much info in this error, so at first I was kinda clueless. but after glancing over the rnw package structure, I think it's just because in rnw module, there's no such directory equivalent to this, node_modules/react-native/Libraries/Utilities/codegenNativeComponent.js
, (i.e. this is not meant to come from react-native-web, at all) as is expected from react-native.
perhaps if rnw were typescript native, we could get a nicer callstack here too, but its written in flow, so I imagine that's why there's not a more detailed error message.
I think it's an issue with safe area context loading the native version instead of the web version .https://github.com/necolas/react-native-web/issues/2292#issuecomment-1129340357 . necolas (author of react-native-web) said:
Your bundler should be using the web implementation of react-native-safe-area-context instead of the native export. If you continue to have trouble loading the web implementation you should open an issue against their repository
so the problem with the mock they provide is it still loads the actual component including the native code.
const RNSafeAreaContext = jest.requireActual('react-native-safe-area-context');
after inspecting the files in safe area package, there's one from the callstack, NativeSafeAreaProvider, which has file platform extensions. but the one that gets imported in the error callstack (attempt #6) was the default plain ".js", whereas for our purposes it should be the ".web.js" version, perhaps.
8.
so let's tell Jest we want to prioritize importing the .web. version of any file.
set jest moduleFileExtensions setting to moduleFileExtensions: ["web.tsx", "tsx", "web.ts", "ts", "web.jsx", "jsx", "web.js", "js"],
also, remove the rnw alias altogether. // "react-native": "react-native-web",
new error:
`useTheme` must be used within `NativeBaseConfigProvider`
4 | describe('Home', () => {
5 | it('renders a heading', () => {
> 6 | render(<Home />)
| ^
7 |
8 | const heading = screen.getByRole('heading', {
9 | name: /welcome to next\.js!/i,
at useContext (node_modules/native-base/lib/commonjs/utils/createContext.tsx:22:13)
at Object.<anonymous> (__tests__/index.test.tsx:6:5)
that looks like it's just a natural usage error from native-base, so that must mean it's working.
the error now is because I'm trying to render a native-base element, without the provider. https://github.com/GeekyAnts/NativeBase/issues/4303#issuecomment-975835832
9.
so now I will add the provider to the test.
it("renders a heading", () => {
render(
<NativeBaseProvider>
<Home />
</NativeBaseProvider>
)
AND FINALLY IT'S WORKING!!!!!!!! I HAVE OFFICIALLY RUN A SUCCESSFUL TEST WITH NATIVE-BASE ON NEXTJS, CONFIGURING THE GUTS OUT OF JEST
I deserve a beer. I've been doing this all day on memorial day.
Posted on May 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.