Adding Emotion, Typescript, and Jest to Gatsby
Purpose
The purpose of this is to detail how to add Emotion, Typescript, Jest, and React-testing-library to an existing project.
Table of Contents
I will be going through adding the above items based on using the
Gatsby Default Starter
This is essentially a guide to adding the above technologies to an existing Gatsby project.
Adding Typescript
npm install --save-dev typescript
Typescript is now in your project! However, typescript on its own does not do much.
In addition, we must now add typescript to Gatsby.
npm install gatsby-plugin-typescript
Now lets add it to our gatsby-config.js
file.
// gatsby-config.js
modules.exports = {
// Above code omitted
plugins: [
// Other plugins
"gatsby-plugin-typescript",
],
}
Lets now configure ESLint to work with typescript to lint our files.
Adding ESLint
The Gatsby default comes with a .prettierrc
file defined.
It does not however come with a .eslintrc.js
defined in the root directory.
So, lets add it.
// .eslintrc.js
module.exports = {
root: true,
env: {
node: true,
browser: true,
es6: true,
},
parser: "@typescript-eslint/parser",
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: "module",
},
rules: {
"prettier/prettier": "error",
indent: ["error", 2],
"linebreak-style": ["error", "unix"],
camelcase: "off",
"@typescript-eslint/camelcase": ["error", { properties: "never" }],
"react/prop-types": "off",
},
plugins: ["@typescript-eslint", "prettier", "react", "jest"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:jest/recommended",
],
settings: {
react: {
version: "detect",
},
"import/resolver": {
alias: [
["~components", "./src/components"],
["~", "./src/"],
],
},
},
}
Add an .eslintignore
with the following config:
// .eslintignore
node_modules
dist
coverage
gatsby-*
We will also have to define a tsconfig.json
file.
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"jsx": "preserve",
"lib": ["dom", "esnext"],
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"isolatedModules": false,
"esModuleInterop": true,
"noUnusedLocals": false,
"allowJs": true,
"baseUrl": ".",
"paths": {
"~*": ["src/*"],
"~components/*": ["src/components/*"]
}
},
"exclude": ["node_modules", "public", ".cache", "gatsby*"]
}
Next, install the packages required to get ESlint to work.
npm install --save-dev eslint @typescript-eslint/parser \
@typescript-eslint/eslint-plugin eslint-plugin-jest eslint-plugin-react \
eslint-plugin-prettier eslint-import-resolver-alias
We save these as dev dependencies because they are not needed for runtime files.
Now, typescript should be working in your editor of choice using ESLint.
To confirm its working from the command line, let’s add some scripts to our package.json
// package.json
{
// Above code omitted
scripts: {
// Other scripts
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"type-check": "tsc --noEmit",
}
}
Now we can run:
npm run type-check
npm run lint # runs eslint
So now, we are able to change our .js
files to .tsx
files. I won’t go over it here,
but I will have the corrected .tsx
files in my gatsby starter.
Adding Emotion
What is emotion? Emotion is a CSS-in-JS solution similar to styled components.
I used emotion in a previous project and enjoyed using it, so I wanted to add it to this starter.
As a bonus, css-in-js snapshot testing is great for looking for style changes when we add Jest later.
npm install gatsby-plugin-emotion @emotion/core @emotion/styled
After this, add the following to your gatsby-config.js
// gatsby-config.js
module.exports = {
// ...
plugins: [
// ...additional plugins
`gatsby-plugin-emotion`,
],
}
Now, you’re all set to add emotion to your files. Again, I won’t go over that here, but the updated files will be in my starter.
Adding Jest
Now, lets add unit testing.
npm install --save-dev \
@types/jest @types/node jest ts-jest \
babel-jest react-test-renderer \
babel-preset-gatsby identity-obj-proxy
Just a quick note, ts-jest runs typechecking which jest does not run by default.
Add a testing scripts to package.json
// package.json
module.exports {
// ...code above omitted
scripts: {
// ... Above scripts omitted
"test": "jest",
"test:watch": "jest --watch",
"test:watchAll": "jest --watchAll"
}
}
Alright, now lets actually make jest work in Gatsby.
Create a jest.config.js
file and add the following content:
// jest.config.js
module.exports = {
transform: {
"^.+\\.[jt]sx?$": `<rootDir>/jest-preprocess.js`,
},
moduleNameMapper: {
".+\\.(css|styl|less|sass|scss)$": `identity-obj-proxy`,
".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": `<rootDir>/__mocks__/file-mock.js`,
"~(.*)$": "<rootDir>/src/$1",
},
testPathIgnorePatterns: [`node_modules`, `\\.cache`, `<rootDir>.*/public`],
transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`],
globals: {
__PATH_PREFIX__: ``,
},
testURL: `http://localhost`,
setupFiles: [`<rootDir>/loadershim.js`],
}
Now create a jest-preprocess.js
with the following content:
// jest-preprocess.js
const babelOptions = {
presets: ["babel-preset-gatsby", "@babel/preset-typescript"],
}
module.exports = require("babel-jest").createTransformer(babelOptions)
Now, we need to add a __mocks__
directory with a file-mock.js
file.
// __mocks__/file-mock.js
module.exports = "test-file-stub"
We’ll also add a gatsby mock file __mocks__/gatsby.js
.
// __mocks__/gatsby.js
const React = require("react")
const gatsby = jest.requireActual("gatsby")
module.exports = {
...gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(
// these props are invalid for an `a` tag
({
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
ref,
replace,
to,
...rest
}) =>
React.createElement("a", {
...rest,
href: to,
})
),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn(),
}
Then we add a loadershim.js
file.
// loadershim.js
global.___loader = {
enqueue: jest.fn(),
}
Adding Emotion Snapshot Testing
By default, jest does not know how to serialize the css provided by Emotion. Lets change this so we can have meaningful snapshot testing.
npm install --save-dev jest-emotion babel-plugin-emotion
Now, we must add this to our jest-preprocess.js
file.
// jest-preprocess.js
const babelOptions = {
presets: [
"babel-preset-gatsby",
"@emotion/babel-preset-css-prop",
"@babel/preset-typescript",
],
plugins: ["emotion"],
}
module.exports = require("babel-jest").createTransformer(babelOptions)
Now we must create a setup-test-env.js
file to be able to add the snapshot serialization.
// setup-test-env.js
import { createSerializer } from "jest-emotion"
import * as emotion from "@emotion/core"
expect.addSnapshotSerializer(createSerializer(emotion))
Finally, tell your jest.config.js
to setup this file.
// jest.config.js
modules.exports = {
// Above code omitted
jest: {
setupFilesAfterEnv: [`<rootDir>/setup-test-env.js`],
},
// Below code omitted
}
Phew, that was a lot of work simply to add testing. No wonder why no one bothers
testing anything! you could stop here if you’d like, but I really enjoy
working with React-Testing-Library
Adding React-Testing-Library
npm install --save-dev react-testing @testing-library/react \
@testing-library/jest-dom @types/testing-library__react
Now add the line import "@testing-library/jest-dom/extend-expect"
to your setup-test-env.js
file.
// setup-test-env.js
import { createSerializer } from "jest-emotion"
import * as emotion from "@emotion/core"
import "@testing-library/jest-dom/extend-expect"
expect.addSnapshotSerializer(createSerializer(emotion))
Writing the first test
Writing your first test
There are many ways to add tests, I prefer having a top level __tests__
directory.
mkdir __tests__
In this directory is where i can add integration tests, unit tests etc.
Below is an example of one of my tested components in my starter. Just a note, i do use import aliases so it wont be a relative path for importing.
// __tests__/components/header.test.tsx
import React from "react"
import { render } from "@testing-library/react"
import Header from "~components/header"
describe("Unit testing", () => {
test("Should render a header with the given testid", () => {
const { getByTestId } = render(
<Header siteTitle="test-title" className="header" data-testid="header" />
)
const header = getByTestId("header")
expect(header).toHaveClass("header")
expect(header).toHaveTextContent("test-title")
})
})
describe("Snapshot testing", () => {
test("Should render a header without error", () => {
const { asFragment } = render(
<Header siteTitle="test-title" className="header" data-testid="header" />
)
expect(asFragment()).toMatchSnapshot()
})
test("Renders a header without a siteTitle defined", () => {
const { asFragment } = render(<Header />)
expect(asFragment()).toMatchSnapshot()
})
})
And that’s it! You should be up and running using Jest / React-testing-library. This was much longer than expected so I may add another post about adding Cypress for E2E testing.
Quick Start
If you’ve done this before, if you know what you’re doing, and feel confident, below is the quick guide as to everything covered above. I don’t recommend this if you don’t have prior experience implementing Gatsby, Typescript, Emotion, and Jest. Proceed at your own risk.
I know what I'm doing - Let's do this
# Create directories
mkdir -p __tests__/components __mocks__/
# Create files
touch jest.config.js jest-preprocess.js loadershim.js __mocks__/file-mock.js \
setup-test-env.js .eslintrc.js .eslintignore __mocks__/gatsby.js
# add development packages
npm install --save-dev typescript eslint @typescript-eslint/parser \
@typescript-eslint/eslint-plugin eslint-plugin-jest eslint-plugin-react \
eslint-plugin-prettier eslint-import-resolver-alias @types/jest @types/node \
jest ts-jest babel-jest react-test-renderer babel-preset-gatsby \
identity-obj-proxy jest-emotion babel-plugin-emotion \
react-testing @testing-library/react @testing-library/jest-dom @types/testing-library__react
# add runtime packages
npm install gatsby-plugin-typescript gatsby-plugin-emotion @emotion/core @emotion/styled
// package.json
{
// Above code omitted
scripts: {
// Other scripts
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"type-check": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
"test:watchAll": "jest --watchAll"
}
}
// .eslintrc.js
module.exports = {
root: true,
env: {
node: true,
browser: true,
es6: true,
},
parser: "@typescript-eslint/parser",
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: "module",
},
rules: {
"prettier/prettier": "error",
indent: ["error", 2],
"linebreak-style": ["error", "unix"],
camelcase: "off",
"@typescript-eslint/camelcase": ["error", { properties: "never" }],
"react/prop-types": "off",
},
plugins: ["@typescript-eslint", "prettier", "react", "jest"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:jest/recommended",
],
settings: {
react: {
version: "detect",
},
"import/resolver": {
alias: [
["~components", "./src/components"],
["~", "./src/"],
],
},
},
}
// .eslintignore
node_modules
dist
coverage
gatsby-*
// gatsby-config.js
module.exports = {
// ...
plugins: [
// ...additional plugins
"gatsby-plugin-typescript",
"gatsby-plugin-emotion",
],
}
// loadershim.js
global.___loader = {
enqueue: jest.fn(),
}
// jest-preprocess.js
const babelOptions = {
presets: [
"babel-preset-gatsby",
"@emotion/babel-preset-css-prop",
"@babel/preset-typescript",
],
plugins: ["emotion"],
}
module.exports = require("babel-jest").createTransformer(babelOptions)
// setup-test-env.js
import { createSerializer } from "jest-emotion"
import * as emotion from "@emotion/core"
import "@testing-library/jest-dom/extend-expect"
expect.addSnapshotSerializer(createSerializer(emotion))
// jest.config.js
module.exports = {
transform: {
"^.+\\.[jt]sx?$": `<rootDir>/jest-preprocess.js`,
},
moduleNameMapper: {
".+\\.(css|styl|less|sass|scss)$": `identity-obj-proxy`,
".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": `<rootDir>/__mocks__/file-mock.js`,
"~(.*)$": "<rootDir>/src/$1",
},
testPathIgnorePatterns: [`node_modules`, `\\.cache`, `<rootDir>.*/public`],
transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`],
globals: {
__PATH_PREFIX__: ``,
},
testURL: `http://localhost`,
setupFiles: [`<rootDir>/loadershim.js`],
setupFilesAfterEnv: ["<rootDir>/setup-test-env.js"],
}
// __mocks__/file-mock.js
module.exports = "test-file-stub"
// __mocks__/gatsby.js
const React = require("react")
const gatsby = jest.requireActual("gatsby")
module.exports = {
...gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(
// these props are invalid for an `a` tag
({
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
ref,
replace,
to,
...rest
}) =>
React.createElement("a", {
...rest,
href: to,
})
),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn(),
}
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"jsx": "preserve",
"lib": ["dom", "esnext"],
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"isolatedModules": false,
"esModuleInterop": true,
"noUnusedLocals": false,
"allowJs": true,
"baseUrl": ".",
"paths": {
"~*": ["src/*"],
"~components/*": ["src/components/*"]
}
},
"exclude": ["node_modules", "public", ".cache", "gatsby*"]
}
Useful Resources / Resources used
Jest
React-Testing-Library
- https://www.gatsbyjs.org/docs/testing-react-components/
- https://www.gatsbyjs.org/docs/testing-css-in-js/
Typescript
Emotion
- https://emotion.sh/docs/introduction
- https://www.gatsbyjs.org/docs/emotion/
- https://www.gatsbyjs.org/packages/gatsby-plugin-emotion/
Eslint
- https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md
- https://github.com/prettier/eslint-plugin-prettier
- https://eslint.org/
These are not all the links I used, there was a lot of googling and stackoverflow involved, but this is a pretty good starting point. Hope this helped! Good luck out there!
Also, here’s the starter I created in the process of all this it has some additions not covered here.