๐ This is the second instalment of a series on building a Tetris clone using React. If you've missed the first, find it here.
Today we'll take a step towards starting the project. I'll discuss various options and choices which you might encounter when you're bootstrapping your own projects. It's important to talk about these - especially since a lot of tutorials and guides completely skip over the why - and you'll notice that not everything is crystal clear of has a single way to move forward.
๐ฎ In this series I'll show you all the steps to build a Tetris clone, abiding by the Tetris Guideline, the current specification that The Tetris Company enforces for making all new (2001 and later) Tetris game products alike in form.
๐ Tetris is licensed which means that if you intend to take this series of articles to build your own arcade puzzler, make sure to abide by the law, if you intend to commercially release it. Even if you provide a clone for free, you could still get a cease and desist. This reddit thread is pretty comprehensive how to go about this. Additionally, this Ars Technica article talks in-depth about how courts judge gaming clones using Tetris and the alleged clone Mino as an example.
๐ This series is purely meant as an educational, non-commercial resource. We'll only be using the fandom wiki as a resource and only use the name Tetris to indicate the type of game and not the actual company, game(s) or brand.
Table of Contents
- Table of Contents
- The Game Engine
- The Toolchain
- Initialisation of the project
- Setting up
typescript
correctly - Setting up
babel
correctly -
Setting up
eslint
correctly -
Setting up
jest
correctly -
Setting up
prettier
correctly - Conclusion
The Game Engine
Since this series has a game as its deliverable, it can be wise to pick a game engine. As taken from the WikiPedia article, a game engine is a software-development environment designed for people to build video games. There is an entire list of game engines, which isn't complete, and choosing which one to use for your game is such an endeavour that many have entire articles or video about it. In my opinion, if you're building a game, from scratch, and you have the time, potential and choice, you only need to ask yourself the following question:
- Do I ever want to go multiplayer? Pick Unreal Engine.
- Do I want to build a First-Person Shooter (single- or multiplayer)? Pick Unreal Engine.
- Otherwise, pick Unity.
I'm basing this on the hours and hours of GDC talks, and job listings! There are many more interesting engines, but if you need something which other people will trust and be able to work with, quickly, you probably need to pick one of these two.
If you're a one-person shop, and building for the web, there is a collection of javascript game engines, including well-known options such as GameMaker Studio (2).
However, since this series is building a Tetris clone using react, that is exactly what I'll use. Ask yourself: is React the right tool for the job? Meh, probably not (because there are better tools. Just because you can make something work, doesn't mean it was the right choice). Does that matter? It depends on the people you work with and the willingness of working around abstractions and challenges.
The Toolchain
Since react
is supposed to be used for this project, it is likely that this project will be built as a JavaScript application. JavaScript projects (and libraries) tend to have a (sub)set of tools, which I refer to as the "toolchain".
Package management
A package manager has its function it the name: it manages packages. JavaScript modules, as listed in your package manifest (the collection of packages that the project depends on, for example listing an URL or a name, and version or version-range) are dependencies for your project. The current popular ones include Yarn and NPM.
You might ask: "But don't I always need a package manager?" The answer to that is a short no. You can also opt to:
- Including all your dependencies locally, for example by vendoring (the act of storing dependencies locally to the project) them. This means you always have a working copy, without the need for the interwebs.
- Use a runtime that doesn't use packages in the traditional sense, such as deno, but also using unpkg, which makes your HTML file the dependency manifest and manager in one.
- Use system packages such as
.deb
ian packages, and manage dependencies using a system tool such asmake
and a Makefile. This technically still uses a package manager, but not in the same way as theYarn
ornpm
options.
๐ I'm choosing
Yarn
with the package manifest insidepackage.json
. I think there are many valid reasons to pick any of the options above. I've used most of them myself, including now defunct tools such as Bower. It often doesn't matter, and the decision should be made in line with company policies or project team alignment.
Bundler
A bundler in the JavaScript eco-system is not to be confused with the package manager bundler from the Ruby eco-system. In the JavaScript eco-system, it usually takes care of the following set of feature, or a sub-set thereof:
- collecting all the assets in your project (JS, HTML, files, images, CSS)
- stripping out unused assets (think tree-shaking, dead code/import elimination)
- applying transformations (transpilation e.g. Babel, post processing e.g. PostCSS)
- outputting code bundles (chunks, code splitting, cache-friendly output)
- error logging (more friendly)
- hot module replacement (automatically updating modules / assets during development)
Some of the tools I've used in the past and still use are Webpack, Parcel, Rollup, microbundle, Browserify and Brunch. The same can be achieved using a task runner such as Grunt or using Gulp, but in my experience, those tend to get out of hand fast.
The choice here, again, doesn't really matter. I think they all have their strengths and weaknesses, and you should pick whichever you feel comfortable with. If you foresee you'll need to customise a lot, some will be favourable over others. If your team knows one of them better than the others, that will probably be favourable. In general: a great bundler is replaceable.
๐ I'm not choosing anything yet! I'll let the rest of the toolchain dictate what bundler I want to go with. If done right, it won't be that costly to replace the bundler later. Perhaps I won't need any.
Compiler
Technically, babel is mostly a transpiler, as it compiles code to the same level of abstraction (think JavaScript ESNext to JavaScript ES3). A compiler generally compiles code to a lower level of abstraction (think Java to JVM / ByteCode, TypeScript to JavaScript). That said, Babel lists itself as a compiler, which it also is as it can remove TypeScript token from TypeScript code, yielding valid JavaScript
๐ Since I want some type-safety, and I'm better at using TypeScript (which does come with a compiler which also transpiles) than Flow (which is technically a static type checker and not a compiler or transpiler), I'm choosing TypeScript for compilation to type-check and then use
babel
to actually compile and then transpile the files to JavaScript.
Linting and Styleguides
According to WikiPedia, Lint, or a linter, is a tool that analyses source code to flag programming errors, bugs, stylistic errors, and suspicious constructs. Since I'll be using TypeScript
, I'm at least looking for a code-linter.
๐ TSLint has been deprecated, as Microsoft has rolled out an amazing toolset which enables ESLint to support Typescript, called
typescript-eslint
.
I also think that it's good practise to pick a coding style guide (e.g. do you use semicolons or not) and apply that to the project. Towards this goal, I'll use prettier
.
Testing libraries
Alright, this one is also not groundbreaking. Whilst there are a lot of options here, such as mocha, jasmine, tape, or one of my favourites AVA, I'll use jest. I personally think it has all the great features I love from AVA, but because Facebook uses it internally, there is quite a bit of React tooling that hooks perfectly into jest
.
Base Library
There are currently multiple options when you want to develop in "react":
-
react
: https://reactjs.org/ -
preact
: https://preactjs.com/ -
react-native-web
: https://github.com/necolas/react-native-web
๐ Without going to deep into detail, since this series is focused on writing a game using
react
, that's what I'll use; if there is a lot of ask by the end of the series, I'll write one about how to move fromreact
topreact
.
Bootstrap
If you've read the react docs, you might know that there are several "toolchains" out there. They are mostly wrappers providing a single Command-Line Interface (CLI) and come bundled with all the dependencies (tools), as listed above in the various categories. The React team primarily recommends a few solutions, and I tend to agree with them:
- If youโre learning React or creating a new single-page app, use Create React App.
- If youโre building a server-rendered website with Node.js, try Next.js.
- If youโre building a static content-oriented website, try Gatsby.
- If youโre building a component library or integrating with an existing codebase, try Neutrino, nwb, Parcel or Razzle.
I'd like to throw react-static
in the mix as well as an alternative to next.js
and gatsby
, which allows you to build super fast static content sites, hydrated with a react-app, without the requirement of using GraphQL
or a server.
This is a very important decision, because if you choose to use a bootstrapped project with one of the toolchains above, you'll be somewhat tied to their technologies, choice of configuration and general ideas. Most of the tools allow you to eject (to stop using the built-in defaults), but you'll still have to to a lot of work to move away.
๐ The project (Tetris clone) is probably completely feasible without a complete bootstrapped toolchain. That's why I've chosen not to use one of the bootstrapping toolchains. Often I run into meh behaviour when I need to add something that it currently doesn't support correctly out of the box, when I try to upgrade dependencies or anything similar. If I end up needing it, I can always add it later!
Initialisation of the project
# Create the directory for this new project
mkdir tetreact
# Move into that directory
cd tetreact
# Install dependencies
yarn add react react-dom
# Install development dependencies (explanation below)
yarn add typescript core-js@3 eslint eslint-config-prettier eslint-plugin-import -D
yarn add eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks -D
yarn add jest babel-jest prettier @babel/cli @babel/core @babel/preset-env -D
yarn add @babel/preset-react @babel/preset-typescript @typescript-eslint/eslint-plugin -D
yarn add @typescript-eslint/parser @testing-library/react @testing-library/jest-dom -D
yarn add @types/jest @types/react @types/react-dom -D
# Make this a git repository
git init
Here is why the following packages are being installed:
-
react
andreact-dom
are runtime packages for react, -
typescript
: used to type-check thets
andtsx
files, -
core-js
: a library that polyfills features. There is an older, version (@2
) and a newer version (@3
). -
eslint
: the core package for the linter, -
eslint-config-prettier
: turns off conflicting, stylistic rules that are handled by prettier, -
eslint-plugin-import
: adds rules and linting ofimport
andexport
statements, -
eslint-plugin-jsx-a11y
: adds accessibility rules on JSX elements, -
eslint-plugin-react
: adds React specific linting rules, -
eslint-plugin-react-hooks
: adds React Hooks specific linting rules, -
jest
: the testing framework, -
babel-jest
: makes it possible to run the test code through babel, -
@babel/cli
: allows me to run babel as a standalone command from the command line, -
@babel/core
: the core package for Babel, -
@babel/preset-env
: preset to determine which transformations need to be applied on the code, based on a list of browsers, -
@babel/preset-react
: preset that allows transpilation of JSX and ensures React's functional component's propertydisplayName
is correctly set, -
@babel/preset-typescript
: allows stripping TypeScript type tokens from files, leaving behind valid JavaScript, -
@typescript-eslint/eslint-plugin
: adds a lot of rules for linting TypeScript, -
@typescript-eslint/parser
: allowseslint
to use the TypeScript ESLint parser (which knows about type tokens), -
@testing-library/react
: adds officially recommended testing library, for react, -
@testing-library/jest-dom
: adds special matchers forjest
and the DOM, -
@types/*
: type definitions
You might think: "jee, that's a lot of dependencies", and yep, it's quite a few. However, when using something like create-react-app
, you are installing the same if not more dependencies, as these are dependencies of the react-scripts
project you'll be depending on. I've spent quite some time on getting this list to where it is, but feel free to make your own changes and/or additions.
Normally I would add these dependencies as I go, but I already did all the steps listed below, so I collected all the dependencies and listed them in two single commands for you to copy and paste.
Setting up typescript
correctly
The following is to setup typescript
. The dependencies added for this are:
-
typescript
: provides thetsc
typescript compiler and allows you to have a project version, different from a version e.g. bundled with your IDE or text editor.
Run the tsc --init
command in order to create the tsconfig.json
with the default settings.
yarn tsc --init
Now I need to make a few changes, all of which are explained below:
- // "incremental": true,
+ "incremental": true
- // "target": "es5",
+ "target": "esnext",
- // "jsx": "preserve",
+ "jsx": "preserve",
- // "noEmit": true,
+ "noEmit": true,
- // "isolatedModules": true,
+ "isolatedModules": true,
- // "moduleResolution": "node",
+ "moduleResolution": "node",
- // "allowSyntheticDefaultImports": true,
+ "allowSyntheticDefaultImports": true,
Remember, the goal is to have tsc
type-check the codebase. That means there doesn't need to be an output, hence noEmit
. Furthermore, it doesn't need to spend time transpiling to an older JavaScript, because babel
will take care of that, which means it can have an esnext
target. For the same reason, jsx
is set to preserve
and not react
. Babel will take care of that. Then there are a few options that make interoptability with other packages easier. Finally, isolatedModules
is required for the TypeScript over Babel functionality to work correctly.
Additionally, package.json
needs to get the "scripts"
key with a command that runs the type-checking.
+ "scripts": {
+ "lint:types": "yarn tsc"
+ }
Running yarn lint:types
should yield the following error:
error TS18003: No inputs were found in config file 'path/to/tetreact/tsconfig.json'. Specified
'include' paths were '["**/*"]' and 'exclude' paths were '[]'.
Found 1 error.
This is the correct error. There is nothing to compile! Let's add that:
mkdir src
touch src/App.tsx
Running yarn lint:types
should yield the following errors:
node_modules/@types/babel__template/index.d.ts:16:28 - error TS2583: Cannot find name 'Set'. Do
you need to change your target library? Try changing the `lib` compiler option to es2015 or later.
16 placeholderWhitelist?: Set<string>;
~~~
node_modules/@types/react/index.d.ts:377:23 - error TS2583: Cannot find name 'Set'. Do you need
to change your target library? Try changing the `lib` compiler option to es2015 or later.
377 interactions: Set<SchedulerInteraction>,
~~~
src/App.tsx:1:1 - error TS1208: All files must be modules when the '--isolatedModules' flag is
provided.
1
Let's start at the first two. These give an explicit option to fix the error.
- // "lib": [],
+ "lib": ["dom", "es2015"],
This is very similar to setting the correct env
in your .eslintrc
configuration file: I need to tell TypeScript that I'm in a browser environment (dom
) and that it should be able to access those constructs that have been introduced in es2015
.
The final error is because of the --isolatedModules
flag. When running the compiler with this flag/option, each file expects to be its own, free-standing module. A file is only a module if it import
s or export
s something. The reason for this flag isn't apparent: It's listed on the documentation of @babel/plugin-transform-typescript
as one of the caveats of "compiling" TypeScript using Babel. I have advanced knowledge here, but it would become clear in the next step.
I update the src/App.tsx
file:
import React from 'react'
export function App(): JSX.Element {
return <div>Hello world</div>
}
Finally, tsc
does not complain.
Setting up babel
correctly
Next up is making sure that babel
"compiles" the TypeScript code to JavaScript, applies transformations and hooks into the various plugins that I've installed.
-
core-js@3
: a library that polyfills features. There is an older, version (@2
) and a newer version (@3
); it uses used by@babel/preset-env
in conjunction with abrowerlist
configuration, -
@babel/cli
: allows me to run babel as a standalone command from the command line, -
@babel/core
: the core package for Babel, -
@babel/preset-env
: preset to determine which transformations need to be applied on the code, based on a list of browsers, -
@babel/preset-react
: preset that allows transpilation of JSX and ensures React's functional component's propertydisplayName
is correctly set, -
@babel/preset-typescript
: allows stripping TypeScript type tokens from files, leaving behind valid JavaScript.
Babel currently, at moment of writing, does not have an --init
command, but setting it up is not very complicated, albeit it takes some effort to get all the presets and plugins correctly listed. Since this is a project, per the babel documentation, the best way for this project is to create a JSON
configuration, called .babelrc
.
touch .babelrc
The contents are as follows, which I collected by taking the documentation of the three @babel/preset-*
plugins and applying them:
{
"presets": [
[
"@babel/preset-env", {
"targets": {
"node": "current"
},
"useBuiltIns": "usage",
"corejs": { "version": 3 }
}
],
"@babel/preset-typescript",
"@babel/preset-react"
],
"ignore": [
"node_modules",
"dist"
]
}
It's also a good idea to explicitly define the browserlists
key/configuration, even though since I'm building a cross-env cross-browser game, the setting can stay on defaults
. In order to do that, and in order to be abel to call babel
using @babel/cli
, in package.json
, I added the following:
{
"scripts": {
+ "build": "yarn babel src --out-dir dist --extensions \".ts,.tsx\"",
+ "watch": "yarn build --watch",
"lint:types": "yarn tsc"
},
"dependencies": {
...
"typescript": "^3.5.3"
},
+ "browserslist": [
+ "defaults"
+ ]
}
If you want a different target, make sure to follow the Browserlist best practices. You can also use a configuration file; pick whichever you like.
Let's see if this works!
$ yarn build
yarn run v1.16.0
warning package.json: No license field
$ babel src --out-dir dist --extensions ".ts,.tsx"
Successfully compiled 1 file with Babel.
Done in 1.67s.
In dist
I can now find App.js
, which does not have any type information. It should look something like this:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.App = App;
var _react = _interopRequireDefault(require("react"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function App() {
return _react.default.createElement("div", null, "Hello World!");
}
A few things to notice:
- It added
"use strict";
- It is using the
interopRequireDefault
to requirereact
's default export - It transpiled
JSX
to use_react.default.createElement
These three things would only happen if Babel is configured correctly.
Setting up eslint
correctly
Next step is making sure that the TypeScript code can be linted!
-
eslint
: the core package for the linter, -
eslint-config-prettier
: turns off conflicting, stylistic rules that are handled by prettier, -
eslint-plugin-import
: adds rules and linting ofimport
andexport
statements, -
eslint-plugin-jsx-a11y
: adds accessibility rules on JSX elements, -
eslint-plugin-react
: adds React specific linting rules, -
eslint-plugin-react-hooks
: adds React Hooks specific linting rules, -
@typescript-eslint/eslint-plugin
: adds a lot of rules for linting TypeScript, -
@typescript-eslint/parser
: allowseslint
to use the TypeScript ESLint parser (which knows about type tokens).
The eslint
core package comes with a CLI tool to initialise (and run) eslint
:
$ yarn eslint --init
? How would you like to use ESLint? To check syntax and find problems
? What type of modules does your project use? JavaScript modules (import/export)
? Which framework does your project use? React
? Where does your code run? Browser
? What format do you want your config file to be in? JSON
Successfully created .eslintrc.json file in path/to/tetreact
Depending on your configuration, and depending if you call yarn eslint
(execute eslint
from the local node_modules
) or plain eslint
(which might call the "globally" installed eslint
), the following message may appear:
The config that you've selected requires the following dependencies:
eslint-plugin-react@latest
? Would you like to install them now with npm? No
I choose "No"
because on one hand, it's already installed under devDependencies
and on the other hand, it will try to use npm
to install it if I say "yes"
(at moment of writing), which is something I don't want (as I am using yarn
).
As for the options: I personally like the .json
file, because it restricts me from solving something using JavaScript
, which makes the barrier to do something "hackly" a bit higher. I basically guard myself from trying to do something that is not supported out of the box. Your mileage may vary, but I like to use my dependencies with standard configuration, because it makes it easier to search for solutions and ask for support!
๐ If you run into an error that looks like this:
ESLint couldn't find the plugin "eslint-plugin-react". This can happen for a couple different reasons: ...
Even though it is installed locally, remove
eslint
from the global packages:yarn global remove eslint
If you're using an IDE with eslint
integration set-up, chances are that both App.js
(in the dist
folder) รกnd App.tsx
(in the src
folder) light up with errors. This is to be expected. It doesn't automagically configure .eslintrc.json
with all the plugins from your devDependencies
.
In order to get all the configurtion in, I edit the generated .eslintrc.json
.
- First, I mark the configuration as the
root
configuration. This prohibits anyeslint
configuration somewhere up the tree to apply rules to this project. - Next, I update the
parserOptions
and tell it to use the@typescript-eslint/parser
parser. My article on writing a TypeScript code Analyzer goes into a bit more detail on what the different@typescript-eslint/*
packages are and do. - Finally, there are all the
extends
. These take preset configurations that I want to apply to this configuration. The@typescript-eslint/*
andprettier/*
modules have documentation that explains in what order these should be placed.
{
+ "root": true,
+ "parser": "@typescript-eslint/parser",
"parserOptions": {
+ "project": "./tsconfig.json",
+ "ecmaFeatures": {
+ "jsx": true
+ },
"ecmaVersion": 2018,
"sourceType": "module"
},
"env": {
"browser": true,
"es6": true
},
- "extends": "eslint:recommended"
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended"
+ "plugin:react/recommended",
+ "prettier",
+ "prettier/@typescript-eslint",
+ "prettier/babel",
+ "prettier/react"
+ ],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"plugins": [
- "react",
+ "@typescript-eslint",
+ "react-hooks",
],
"rules": {
},
+ "settings": {
+ "react": {
+ "version": "detect"
+ }
+ }
}
The rules
are currently still empty, I'll get to that. First, let's test the configuration!
Testing the eslint
configuration
I change src/App.tsx
:
+ function Header() {
+ return <h1>Hello World!</h1>
+ }
export function App(): JSX.Element {
- return <div>Hello World!</div>
+ return <Header />
}
And add a new scripts
entry:
"scripts" {
"build": "yarn babel src --out-dir dist --extensions \".ts,.tsx\"",
"watch": "yarn build --watch",
+ "lint": "yarn eslint src/**/*",
"lint:types": "yarn tsc"
},
Now I run it!
yarn lint
$ eslint src/**/*
path/to/tetreact/src/App.tsx
3:1 warning Missing return type on function @typescript-eslint/explicit-function-return-type
โ 1 problem (0 errors, 1 warning)
Done in 4.01s.
Woopdiedo. A warning from the @typescript-eslint
plugin! This is exactly what I expect to see, so I can now move on fine-tuning the "rules"
.
Fine-tuning the rules
Normally I fine-tune the "rules"
as I develop a library or a project, or I use a set of rules that is pre-determined by a project lead. In the exercism/javascript-analyzer respository, I've added a document about the rules and why I chose them to be like this. The results are as listed below, which include the two react-hooks
rules at the bottom.
{
"rules": {
"@typescript-eslint/explicit-function-return-type": [
"warn", {
"allowExpressions": false,
"allowTypedFunctionExpressions": true,
"allowHigherOrderFunctions": true
}
],
"@typescript-eslint/explicit-member-accessibility": [
"warn", {
"accessibility": "no-public",
"overrides": {
"accessors": "explicit",
"constructors": "no-public",
"methods": "explicit",
"properties": "explicit",
"parameterProperties": "off"
}
}
],
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-parameter-properties": [
"warn", {
"allows": [
"private", "protected", "public",
"private readonly", "protected readonly", "public readonly"
]
}
],
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-use-before-define": [
"error", {
"functions": false,
"typedefs": false
}
],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
As I write more code, this ruleset may change, but for now this should suffice.
Setting up jest
correctly
Next up is making sure the code is testable.
I personally don't like to co-locate my test files next to my source files, but rather put all the tests in a separate directory. However this isn't better or preferred, just different. You can do whichever you like. If you co-locate the tests, make sure that your tests end with .test.ts
or .test.tsx
, and if you don't, the default folder is __tests__
. You can change these in the, soon to be, generated jest.config.js
.
The dependencies that matter are:
-
jest
: the testing framework, -
babel-jest
: makes it possible to run the test code through babel, -
@testing-library/react
: adds officially recommended testing library, for react, -
@testing-library/jest-dom
: adds special matchers forjest
and the DOM,
Just like some of the other tools, jest
comes with a CLI and an option that allows you to generate the configuration file.
$ yarn jest --init
โ Would you like to use Jest when running "test" script in "package.json"? ... yes
โ Choose the test environment that will be used for testing ยป jsdom (browser-like)
โ Do you want Jest to add coverage reports? ... yes
โ Automatically clear mock calls and instances between every test? ... no
This adds the test
script to "scripts"
in package.json
and adds a jest.config.js
file with defaults to the root directory.
The contents of the configuration file are all set correctly (given the answers as listed above), with the important ones being (you can go in and confirm):
-
coverageDirectory
should be set to"coverage"
, because I want coverage reports, -
testEnvironment
should not be set or set to"jest-environment-jsdom"
, because I don't want to have to run in a browser.
The babel-jest
package is automagically supported, out-of-the-box, without needing to set-up anything else. Since Babel is already configured correctly to "compile" the source code, and the test code has the same properties, no steps need to be taken in order to make the tests be "compiled" as well.
Then I want to integrate with the @testing-library/react
library, which provides a cleanup script that makes sure the React
application state and environment is reset (cleaned-up) after each test. Instead of including this in every test, it can be setup via the jest.config.js
file:
- // setupFilesAfterEnv: []
+ setupFilesAfterEnv: [
+ '@testing-library/react/cleanup-after-each'
+ ],
I use the default folder name for my tests:
mkdir __tests__
And now I create a smoke test __tests__/App.tsx
with the following:
import React from 'react'
import { render } from '@testing-library/react'
import { App } from '../src/App';
it('App renders heading', () => {
const {queryByText} = render(
<App />,
);
expect(queryByText(/Hi/)).toBeTruthy();
});
Finally I run the tests using the "scripts"
command that was added by yarn jest --init
:
yarn test
$ jest
FAIL __tests__/App.tsx
ร App renders heading (29ms)
โ App renders heading
expect(received).toBeTruthy()
Received: null
14 | );
15 |
> 16 | expect(queryByText(/Hi/)).toBeTruthy();
| ^
17 | });
18 |
at Object.toBeTruthy (__tests__/App.tsx:16:29)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 4.361s
Ran all test suites.
Ah. I'm rendering Hello World
, and not Hi
. So now I change the regular expression to test for Hello World
instead, and run the tests again:
$ jest
PASS __tests__/App.tsx
โ App renders heading (21ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.184s
Ran all test suites.
Done in 6.10s.
Enabling jest-dom
extensions
You might have noticed that there is another @testing-library
dependency. I want to use the '@testing-library/jest-dom/extend-expect'
visibility check toBeVisible
, instead of only testing if it exists via toBeTruthy
. I order to integrate with that package, I make the following change to the jest.config.js
:
setupFilesAfterEnv: [
'@testing-library/react/cleanup-after-each',
+ '@testing-library/jest-dom/extend-expect',
],
This change makes the extension (new matchers, including .toBeVisible
) available to all the tests.
I update the test to use these:
import React from 'react'
import { render } from '@testing-library/react'
import { App } from '../src/App'
it('App renders heading', () => {
const { container, queryByText } = render(
<App />,
);
- expect(queryByText(/Hello World/)).toBeTruthy()
+ expect(queryByText(/Hello World/)).toBeVisible()
}
Running the tests works, but my IDE gives an error on the toBeVisible
matcher. This is because TypeScript doesn't quite know that the expect
matchers have been extended. It's not good at inferring new types from dynamically executed code. Since there is no cross-file information between the jest
configuration and this test, I can't expect that to be magically picked up. Fortunately, there are various ways to solve this, for example, but not limited to:
- Add
import '@testing-library/jest-dom/extend-expect'
to each test file. This extends theexpect()
Matchers to include those provided by the library, - Make sure
typescript
knows this is always included (which is true, given thejest.config.js
changes).
In order to get the "always included" experience, I add a new file declarations.d.ts
and add a triple-slash directive. I generally stay clear of these directives, and even have an eslint
rule to disallow them, but in my experience, tooling is best when you run into something like this issue and use them. This might not be true if you follow this post some time in the future. You can do whatever works, perhaps an import
suffices:
touch __tests__/declarations.d.ts
/* eslint-disable @typescript-eslint/no-triple-slash-reference */
/// <reference types="@testing-library/jest-dom/extend-expect" />
What this does is tell TypeScript that for the current directory subtree (__tests__
), it should always add the package' types as defined by the directive. I can now also see that the error in __tests__/App.tsx
has been resolved and that it recognises .toBeVisible
.
Getting a coverage report
There are no new dependencies required for a coverage report as jest
comes bundled with built-in coverage.
In order to test if the coverage
is working correctly, I first change the App.tsx
src file to include a branch:
import React from 'react'
export interface AppProps {
headingText?: string
}
export function App({ headingText }: AppProps): JSX.Element | null {
if (headingText === undefined) {
return null
}
return <h1>{headingText}</h1>
}
Now, the app renders null
unless a headingText
is given. I also have to change the test to pass in "Hello World"
as the heading text, or the test will
fail:
- <App />
+ <App headingText="Hello World" />,
I run the test suite with coverage enabled:
yarn test --coverage
This runs the tests and they are passing; it also outputs the following table summary:
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 66.67 | 50 | 100 | 66.67 | |
App.tsx | 66.67 | 50 | 100 | 66.67 | 9 |
----------|----------|----------|----------|----------|-------------------|
Line 9 is inside a conditional branch (for when headerText === undefined
):
return null
This can be tested by explicitly adding a test.
it('App renders nothing without headingText', () => {
const { container } = render(
<App />,
)
expect(container.firstChild).toBeNull()
})
I generally don't like to test that things are not there, because often you have to make a few assumptions that are fragile at best (and therefore break easily), but just to test if jest
has been set-up correctly, this is fine, since I'll throw away these lines later:
$ jest --coverage
PASS __tests__/App.tsx
โ App renders heading (46ms)
โ App renders nothing without headingText (1ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
App.tsx | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.911s
Ran all test suites.
Done in 6.78s.
Setting up prettier
correctly
Finally, I can focus on setting up the (automatic) code formatter! I really like prettier
for the simple reason that it removes the need of discussing a lot of style choices. I don't think it always or even often generates pretty code, but that's okay. As their library improves, so does the output, and it's trivial to re-format all the code once they do.
-
eslint-config-prettier
: turns off style rules that are in conflict with prettier. You can see the variousprettier/*
lines in theeslint
configuration above. This has already been set-up. -
prettier
: the core package, including the CLI tools to run prettier.
Prettier has already been added to the eslint
configuration, so that part can be skipped.
The prettier
CLI doesn't have an --init
option at the moment of writing, so I create the configuration file manually:
touch .prettierrc.json
I've chosen to loosly follow the StandardJS
style, but it really doesn't matter. Pick a style and stick with it.
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"jsxSingleQuote": false
}
I also want to be able to run these as a script, so I add the following three "scripts"
:
"lint:types": "yarn tsc",
+ "lint:format": "yarn format --check",
+ "format": "yarn prettier \"{src,__{tests}__}/**/*.{ts,tsx}\"",
+ "format:fix": "yarn format --write",
"test": "yarn jest"
Automatically formatting
Since prettier
has been added as plugin to eslint
, it is already correctly integrated with eslint
. However, you might want code to be formatted on save. The prettier documentation lists a lot of IDEs and allow you to turn on formatting on save.
In general, I'm not a fan of running prettier on commit, because it slows down my commits, occasionally breaks things and I think it shouldn't be a concern of the commit to format the code. That said, I do think it's a good idea to add a check in the continuous integration (CI) to test the format of the project.
Conclusion
And that's it! The project is now in a pretty good state to start writing code. Yes, it took quite a bit to get here and a lot of the configuration setup above is exactly why tools such as create-react-app
or even the parcel
bundler exist. Note that I haven't actually dealt with some of the things that parcel
and webpack
deal with, such as importing images or other file types; I don't think I'll need it, and therefore I didn't add that.
A few things are left to do:
- Set-up CI,
- Add the
"name"
and "license"` fields, - Add the servability i.e. add the HTML file that we can see in a browser.
Next time I will actually write some game code, and perhaps the things just listed, but for now, this is all I give you.
Top comments (0)