Skip to main content

React Testing Library Integration with Jest and TypeScript

Intro

Let's navigate the hellscape of configuring react-testing-library into a project using Jest and TypeScript.

It took me so many hours to get this working. Top 10 frustrating coding moments of my life.

In this particular instance the framework we are integrating into is Docusaurus, which is causing a lot of the pain. But I think the lessons learned here will help in many other instances.

Packages to install

📘 react-testing-library setup

npm install @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom --save-dev

Explanation of Packages

  1. @testing-library/react:

    • This package provides simple and complete React DOM testing utilities that encourage good testing practices. It helps render React components, query elements within the rendered output, and interact with them in a way that closely resembles how a user would interact with your application.
  2. @testing-library/jest-dom:

    • This package extends Jest with custom matchers that make it easier to assert that elements are in the document and have expected attributes, styles, or other properties. It enhances the readability and expressiveness of your tests by providing matchers like toBeInTheDocument, toHaveAttribute, and more.
  3. @testing-library/user-event:

    • This package provides a set of utilities to simulate user events such as clicks, typing, and other interactions with the UI components. It is designed to simulate real user behavior more closely than the fireEvent method provided by @testing-library/react.
  4. jest-environment-jsdom:

    • This package sets up a Jest testing environment that uses jsdom, which is a JavaScript implementation of the DOM and HTML standards. It allows you to run tests that involve DOM manipulation in a Node.js environment, effectively simulating a browser-like environment.

jsdom environment

jsdom is a JavaScript implementation of the DOM and HTML standards, which means it can simulate a web browser environment within Node.js. It provides a virtual DOM that allows you to run tests involving DOM manipulation, rendering, and interaction as if they were running in an actual browser.

This is the environment that @testing-library/react needs to work properly, as it relies on the ability to render React components and interact with them in a way that closely resembles how a user would interact with your application in a browser.

Therefore we have to set up Jest to use the jsdom environment by adding the following configuration to your jest.config.js file:

jest.config.js
module.exports = {
testEnvironment: 'jsdom', // normally this is set to 'node'
};

Alernatively

You can leave the default testEnvironment: node and then specify that you would like to use the jsdom environment for a specific test file by adding a comment at the top of the file:

📘 jestjs.io > test environment string

my-component.test.js
// https://jestjs.io/docs/configuration#testenvironment-string

/**
* @jest-environment jsdom
*/

// rest of the test
tip

If you are worried about breaking vanilla Jest unit tests by changing the test environment to jsdom, don't worry about it. The jsdom environment is an extension of the node environment, so it should work seamlessly with your existing Jest tests.

Resolving Errors

I've run into many fun errors while trying to get this working. So many in fact that i'm going to document them all here and the solutions to them so that I can reduce my suffering next time I have to deal with all these config files.

.babelrc file

Some outdated online sources will tell you to create a .babelrc file that looks something like this:

.babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-typescript"]
}

While there are legitimate reasons to use .babelrc files, as of Babel 7 project wide configurations (which is what we are doing here) are now placed in the babel.config.js file.

📘 babeljs.io > config files

Cannot use import statement outside a module (module in node_modules)

Make sure that in your jest.config.ts file you specify the specific module in transformIgnorePatterns

jest.config.ts
//...
transformIgnorePatterns: ['<rootDir>/node_modules/(?!@docusaurus)'],
//...

It is not enough to ignore the whole node_modules directory like this

jest.config.ts
//...
transformIgnorePatterns: ['<rootDir>/node_modules/'],
//...

You must specify the specific module that you want to transform. Yes this does not make sense to me either.

Support for the experimental syntax 'jsx' isn't currently enabled

While trying to spin up with dev/start (not running tests)


ERROR in ./node_modules/@docusaurus/core/lib/client/clientEntry.js
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: /app/node_modules/@docusaurus/core/lib/client/clientEntry.js: Support for the experimental syntax 'jsx' isn't currently enabled (21:18):

19 | window.docusaurus = docusaurus;
20 | const container = document.getElementById('__docusaurus');
> 21 | const app = (<HelmetProvider>
| ^
22 | <BrowserRouter>
23 | <App />
24 | </BrowserRouter>

Add @babel/preset-react (https://github.com/babel/babel/tree/main/packages/babel-preset-react) to the 'presets' section of your Babel config to enable transformation.

encountering this error is solved by adding '@babel/preset-react' to babel.config.js file. Package has to be installed obviously to work.

babel.config.js
module.exports = {
presets: [
'@babel/preset-typescript',
'@babel/preset-env',
'@babel/preset-react',
],
};

React is not defined

While trying to spin up with dev/start (not running tests) is you get the following error:

react is not defined

because we don't have react explicitly imported at the top of every single .jsx or .tsx file. You could solve this by explicitly importing it into every affected file in your project (👎) or you can modify your '@babel/preset-react' settings in your babel.config.js.

babel.config.js
 presets: [
'@babel/preset-typescript',
'@babel/preset-env',
[
'@babel/preset-react',
{
runtime: 'automatic',
},
],
],
tip

this feature works on react ^17

Cannot find module '@testing-library/jest-dom/extend-expect'

While running jest

 FAIL  src/__tests__/homePage.test.js
● Test suite failed to run

Cannot find module '@testing-library/jest-dom/extend-expect' from 'src/__tests__/homePage.test.js'

1 | import React from 'react';
2 | import {render, screen} from '@testing-library/react';
> 3 | import '@testing-library/jest-dom/extend-expect';
| ^
4 | import Layout from '@theme/Layout';
5 |
6 | describe('Layout Page', () => {

at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/resolver.js:427:11)
at Object.require (src/__tests__/homePage.test.js:3:1)

Some outdated sources will tell you to import import '@testing-library/jest-dom/extend-expect' however this is incorrect and will throw this error. You have to drop the extend-expect part.

instead import jest-dom like this import '@testing-library/jest-dom/'

Cannot find module @site or @theme or @SOME_ALIAS

If you are getting an error message like this:

  FAIL  src/__tests__/homePage.test.js
● Test suite failed to run

Cannot find module '@theme/Layout' from 'src/__tests__/homePage.test.js'

2 | import {render, screen} from '@testing-library/react';
3 | import '@testing-library/jest-dom';
> 4 | import Layout from '@theme/Layout';
| ^
5 |
6 | describe('Layout Page', () => {
7 | test('renders the greeting message', () => {

You are likely using a framework that has set some alias to a file location like @site or @theme. Jest is not aware of these alias definitions however, so it doesn't know where to look.

We can begin to resolve this by using moduleNameMapper in jest.config.ts

import type {Config} from 'jest';

const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
transform: {
'^.+\\.js$': 'babel-jest',
'^.+\\.ts$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'@theme/(.*)': '@docusaurus/theme-classic/src/theme/$1',
'@site/(.*)': 'website/$1',
},
};

export default config;

and for me this doesn't completely solve the problem, but now we have a different error message (and thats progress!)

now we are finding the files, but they are in node_modules and so the transformers are ignoring them

 FAIL  src/__tests__/homePage.test.js
● Test suite failed to run

Jest encountered an unexpected token

Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

By default "node_modules" folder is ignored by transformers.

Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
• If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation

Details:

/app/node_modules/@docusaurus/theme-classic/src/theme/Layout/index.tsx:8
import React from 'react';
^^^^^^

SyntaxError: Cannot use import statement outside a module

2 | import {render, screen} from '@testing-library/react';
3 | import '@testing-library/jest-dom';
> 4 | import Layout from '@theme/Layout';

Here are some relevant threads on this issue:

📘 github > ts-jest > transform-node-module-explicitly

I did not find the advice there to work, however...

📘 github > ts-jest > paths mapping #jest-config-with-helper

I was able to make progress by using the ts-jest utilities shown above to modify the jest.config.ts file.

jest.config.ts
import {pathsToModuleNameMapper} from 'ts-jest';
// In the following statement, replace `./tsconfig` with the path to your `tsconfig` file
// which contains the path mapping (ie the `compilerOptions.paths` option):
import {compilerOptions} from './tsconfig.json';
import type {JestConfigWithTsJest} from 'ts-jest';

const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
transform: {
'^.+\\.js$': 'babel-jest',
'^.+\\.ts$': 'ts-jest',
},
roots: ['<rootDir>'],
modulePaths: [compilerOptions.baseUrl], // <-- This will be set to 'baseUrl' value
// module name maps now from tsconfig
moduleNameMapper: pathsToModuleNameMapper(
compilerOptions.paths /*, { prefix: '<rootDir>/' } */,
),
moduleDirectories: ['node_modules'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};

export default jestConfig;

so that now we are instructing jest to use the paths mapping from the tsconfig.json file.

tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"esModuleInterop": true,
"jsx": "preserve",
"lib": ["DOM"],
"moduleResolution": "bundler",
"module": "esnext",
"noEmit": true,
"types": [
"node",
"jest",
"@docusaurus/module-type-aliases",
"@docusaurus/theme-classic"
],
"baseUrl": ".",
"paths": {
"@site/*": ["./*"],
"@theme/*": ["./node_modules/@docusaurus/theme-classic/src/theme/*"]
},
"skipLibCheck": true
}
}
warning

Also verify that the specific module exists, for example I have an example of '@docusaurus/theme-common/internal' being referenced where I do not actually have that installed or the file is missing.

and that gives us a NEW error

Could not locate module X mapped as

 FAIL  src/__tests__/homePage.test.js
● Test suite failed to run

Configuration error:

Could not locate module @theme/Layout mapped as:
./node_modules/@docusaurus/theme-classic/src/theme/$1.

Please check your configuration for these entries:
{
"moduleNameMapper": {
"/^@theme\/(.*)$/": "./node_modules/@docusaurus/theme-classic/src/theme/$1"
},
"resolver": undefined
}

2 | import {render, screen} from '@testing-library/react';
3 | import '@testing-library/jest-dom';
> 4 | import Layout from '@theme/Layout';

so now we just have to configure our mapping correctly and hopefully we will be nearing the end of this hellscape.

If I change the

jest.config.ts
//...
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',
}),
//...

to include the rootDir prefix, I get back to the following error:

TypeScript files not being converted to commonjs

FAIL  src/__tests__/homePage.test.js
● Test suite failed to run

Jest encountered an unexpected token

Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

By default "node_modules" folder is ignored by transformers.

Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
• If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation

Details:

/app/node_modules/@docusaurus/theme-classic/src/theme/Layout/index.tsx:8
// highlight-next-line
import React from 'react';
^^^^^^

SyntaxError: Cannot use import statement outside a module

2 | import {render, screen} from '@testing-library/react';
3 | import '@testing-library/jest-dom';
> 4 | import Layout from '@theme/Layout';

let's try playing around with some of the ts-jest presets and see if that yields any fruit

📘 github > ts-jest > presets

Trying all manner of those didn't solve the problem, but I do think that i've determined that we should either be using preset or transform, but not both, as preset over-rides transform. So let's disable preset, and now we know that js and jsx files are transformed by babel-jest and ts and tsx files are transformed by ts-jest.

jest.config.ts
// no preset:
transform: {
'^.+\\.js$': 'babel-jest',
'^.+\\.ts$': 'ts-jest',
},

Furthermore, since our js files appear to be transformed correctly, and our ts files are not (the @theme/Layout) folder contains typescript files, we can conclude a few things:

  • items in the node modules folder are not being transformed as they should
  • ts-jest is not transforming any typescript files as it should.

Let's eliminate one of these options by making a simple typescript component and testing it.

...

And the result of that is that we are still getting import errors, so we know that we have an issue where ts-jest is not transforming our files the way that it should.

... and after many more iterations

Known working config files

These work for local (non modules) typescript files at least...

jest.config.ts
import {pathsToModuleNameMapper} from 'ts-jest';
import {compilerOptions} from './tsconfig.json';
import type {JestConfigWithTsJest} from 'ts-jest';

const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest/presets/js-with-babel',
testEnvironment: 'jsdom',
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: ['<rootDir>/node_modules/'],
roots: ['<rootDir>'],
modulePaths: [compilerOptions.baseUrl],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',
}),
moduleDirectories: ['node_modules', 'src'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};

export default jestConfig;
tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"esModuleInterop": true,
"jsx": "react",
"lib": ["DOM", "ESNext"],
"moduleResolution": "node",
"module": "esnext",
"noEmit": true,
"types": [
"node",
"jest",
"@docusaurus/module-type-aliases",
"@docusaurus/theme-classic"
],
"baseUrl": ".",
"paths": {
"@site/*": ["./*"],
"@theme/*": ["./node_modules/@docusaurus/theme-classic/src/theme/*"]
},
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*", "jest.config.ts"],
"exclude": ["node_modules"]
}
babel.config.js
module.exports = {
presets: [
'@babel/preset-typescript',
['@babel/preset-env', {targets: {node: 'current'}}],
[
'@babel/preset-react',
{
runtime: 'automatic',
},
],
],
};

Automated Amazon Reports

Automatically download Amazon Seller and Advertising reports to a private database. View beautiful, on demand, exportable performance reports.

bidbear.io