Skip to content

Instantly share code, notes, and snippets.

@kpunith8
Created September 17, 2021 11:22
Show Gist options
  • Save kpunith8/51d43ed6adaaa5698e49ed2cab3f514e to your computer and use it in GitHub Desktop.
Save kpunith8/51d43ed6adaaa5698e49ed2cab3f514e to your computer and use it in GitHub Desktop.
Jest setup with babel and webpack-5 (React testing library, Jest)
const requiredPlugins = [
'@emotion',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-optional-chaining',
]
const plugins = {
development: requiredPlugins,
test: requiredPlugins,
production: requiredPlugins,
}
const requiredPresets = [
[
'@babel/preset-react',
{runtime: 'automatic', importSource: '@emotion/react'},
],
]
const development = [
...requiredPresets,
[
'@babel/preset-env',
{
targets: 'last 1 chrome version',
useBuiltIns: 'usage',
corejs: 3,
shippedProposals: true,
},
],
]
const presets = {
development,
test: development,
production: [
...requiredPresets,
[
'@babel/preset-env',
{
targets: 'defaults',
useBuiltIns: 'usage',
corejs: 3,
shippedProposals: true,
},
],
],
}
module.exports = api => {
const env = api.env()
return {
plugins: plugins[env],
presets: presets[env],
sourceMap: true,
}
}
/* istanbul ignore file */
const fileMock = ''
export default fileMock
// Default timezone for testing
process.env.TZ = 'UTC'
module.exports = {
clearMocks: true,
collectCoverage: true,
coverageDirectory: 'coverage',
moduleNameMapper: {
'.svg': '<rootDir>/config/jest/mocks/file-mock.js',
'\\.css$': 'identity-obj-proxy', // Package
},
setupFilesAfterEnv: ['./config/jest/jest.setup.js'],
testEnvironment: './config/jest/jest.environment.js',
moduleFileExtensions: ['js', 'yml'],
transform: {
'\\.js?$': 'babel-jest', // Install these packages
'\\.yml$': 'yaml-jest',
},
transformIgnorePatterns: ['<root-dir>/node_modules(?!/@other-packages|d3-.*)/'],
}
/* istanbul ignore file */
const JSDOMEnvironment = require('jest-environment-jsdom')
const fs = require('fs')
const yaml = require('js-yaml')
const I18n = require('i18n-js')
const _ = require('lodash')
class JestEnvironment extends JSDOMEnvironment {
async setup() {
await super.setup()
// Load I18n translations for Jest, since webpack isn't running
I18n.locale = 'en'
I18n.translations = yaml.load(
fs.readFileSync('./config/locales/en.yml', 'utf8')
)
this.global.I18n = I18n
this.global._ = _
}
}
module.exports = JestEnvironment
/* istanbul ignore file */
import sinon from 'sinon'
import '@testing-library/jest-dom'
import {
act,
configure,
fireEvent,
prettyDOM,
render,
waitFor,
} from '@testing-library/react'
// Sinon should only be used for stubbing object properties
global.sinon = sinon
// eslint-disable-next-line no-console
const consoleError = console.error
// eslint-disable-next-line no-undef
jest.spyOn(console, 'error').mockImplementation((...args) => {
const message = _.head(_.castArray(args))
if (
message.indexOf('Request to server failed with') < 0 &&
message.indexOf(
'React will try to recreate this component tree from scratch using the error boundary you provided'
) < 0 &&
message.indexOf(
"Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot"
)
)
consoleError(...args)
})
// eslint-disable-next-line no-console
const consoleWarn = console.warn
// eslint-disable-next-line no-undef
jest.spyOn(console, 'warn').mockImplementation((...args) => {
const message = _.head(_.castArray(args))
if (
// https://github.com/ReactTraining/react-router/issues/7460
message.indexOf(
'You should call navigate() in a useEffect, not when your component is first rendered'
) < 0
)
consoleWarn(...args)
})
configure({
getElementError: message => {
const error = new Error(message)
error.name = 'TestingLibraryElementError'
// This stack trace gets drowned out by the tippy style block at the top of
// our index.html page, and just adds a ton of noise to our test output.
// It'd be nice to look into how we might be able to focus this in on the
// relevant part of the DOM, filtering out the boilerplate around it. Maybe
// there's something we could do with a component's root element className
// to scope the output?
//
// This shouldn't impact unit tests that aren't using RTL.
error.stack = null
return error
},
})
global.act = act
global.fireEvent = fireEvent
global.prettyDOM = prettyDOM
global.render = (...args) => {
const {container, ...rest} = render(...args)
const find = container.querySelector.bind(container)
const findAll = container.querySelectorAll.bind(container)
return {container, find, findAll, ...rest}
}
global.waitFor = waitFor
global.lastCall = fn => fn.mock.calls[fn.mock.calls.length - 1]
global.config = {
api: '/api'
}
global.cache = {
// Default values for an app
}
{
"scripts": {
"chk": "check-dependencies",
"build": "rm -rf ./public && NODE_ENV=production webpack --config ./webpack.config.prod.js",
"start": "npm run chk && rm -rf ./public && NODE_ENV=development webpack serve --config ./webpack.config.dev.js",
"test": "npm run chk && npx jest --maxWorkers=50%",
"test:silent": "npx jest --maxWorkers=50% --reporters jest-silent-reporter --collectCoverage=false",
"test:ci": "npx jest --runInBand",
"test:watch": "npx jest --watch",
"test:dev": "npx majestic",
"test:debug": "node --inspect node_modules/.bin/jest --watch --runInBand"
},
"devDependencies": {
"@babel/cli": "7.13.0",
"@babel/core": "7.13.8",
"@babel/plugin-proposal-object-rest-spread": "7.13.8",
"@babel/plugin-proposal-optional-chaining": "7.13.8",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.13.9",
"@babel/preset-react": "7.12.13",
"@emotion/babel-plugin": "11.1.2",
"terser-webpack-plugin": "5.1.1",
"webpack": "5.24.2",
"webpack-cli": "4.5.0",
"webpack-config-utils": "2.3.1",
"webpack-dev-server": "3.11.2",
"check-dependencies": "1.1.0",
}
}
// Webpack Version 5.50
const {resolve} = require('path')
const _ = require('lodash')
const {copySync, readFileSync} = require('fs-extra')
const webpack = require('webpack')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const {getIfUtils} = require('webpack-config-utils')
const {mkdirSync, processTemplates} = require('./scripts/common') // https://gist.github.com/kpunith8/0ac775bb3bcf42e0f0b2a5782f656f0c#file-common-js
const getPackages = () =>
JSON.parse(readFileSync(process.env.npm_package_json, 'utf8'))
const {ifProduction} = getIfUtils(process.env.NODE_ENV)
const publicDir = resolve(__dirname, 'public')
const host = process.env.HOST || 'localhost'
class ProcessTemplatesPlugin {
apply(compiler) {
const logger = compiler.getInfrastructureLogger('process-templates-plugin')
compiler.hooks.done.tap('ProcessTemplatesPlugin', () => {
try {
processTemplates(process.env.APP_ENV)
} catch (err) {
logger.error(err)
process.exit(1)
}
})
}
}
// This allows us to override @monaco-editor/react's default behavior of loading monaco from the JSDelivr CDN
class CopyMonacoPlugin {
apply(compiler) {
const logger = compiler.getInfrastructureLogger('copy-monaco-plugin')
compiler.hooks.done.tap('CopyMonacoPlugin', () => {
try {
const version = getPackages().dependencies['monaco-editor']
copySync(
'./node_modules/monaco-editor/min/vs',
mkdirSync(`./public/vs/${version}`)
)
} catch (err) {
logger.error(err)
process.exit(1)
}
})
}
}
// Builds a webpack compatible alias map that can be used to force webpack to
// use our dependencies in lieu of a module's installed dependencies. This is
// especially useful when `npm link` is being used.
const modulePeersAlias = moduleName => {
const modulePkg = require(resolve(
__dirname,
`node_modules/${moduleName}/package.json`
))
return _.transform(
modulePkg.peerDependencies,
(result, _value, key) => {
const depName = _.head(key.split('/'))
result[depName] = resolve(__dirname, `node_modules/${depName}`)
},
{}
)
}
module.exports = {
devServer: {
contentBase: publicDir,
historyApiFallback: true,
host,
port: 8181,
},
devtool: 'eval-source-map',
entry: {
application: resolve(__dirname, 'app', 'index.js'),
},
mode: 'development',
module: {
strictExportPresence: true,
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {cacheDirectory: true},
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.yml$/,
use: ['json-loader', 'yaml2json-loader'],
},
{
test: /\.ico$/,
include: [resolve(__dirname, 'app', 'assets', 'favicons')],
loader: 'file-loader',
options: {
name: 'favicons/[name].[ext]',
},
},
{
test: /\.(png|svg)$/,
include: [resolve(__dirname, 'app', 'assets')],
loader: 'file-loader',
options: {
name: 'images/[name].[ext]',
},
},
{
test: /\.svg$/,
exclude: [resolve(__dirname, 'app', 'assets')],
loader: 'svg-react-loader',
},
{
test: /\.(woff2|ttf)$/,
loader: 'file-loader',
options: {
name: () =>
ifProduction(
'fonts/[name][contenthash].[ext]',
'fonts/[name].[ext]'
),
},
},
],
},
output: {
filename: 'javascripts/[name].js',
path: publicDir,
publicPath: '/',
},
plugins: _.compact([
new MiniCssExtractPlugin({
filename: ifProduction(
'stylesheets/[name].[contenthash].css',
'stylesheets/[name].css'
),
}),
new webpack.ProvidePlugin({
I18n: 'i18n-js',
_: 'lodash',
}),
new ProcessTemplatesPlugin(),
new CopyMonacoPlugin(),
]),
resolve: {
alias: {
...modulePeersAlias('package-name'),
},
extensions: ['.js', '.css', '.svg'],
fallback: {
buffer: false,
fs: false,
},
modules: [resolve(__dirname, 'app'), 'node_modules'],
symlinks: false,
},
watchOptions: {
ignored: /node_modules(?!\/@local-package)/, // used to link locally for development
},
infrastructureLogging: {
level: 'verbose',
debug: /process-templates-plugin|copy-monaco-plugin/,
},
}
const webpack = require('webpack')
const {resolve} = require('path')
const TerserPlugin = require('terser-webpack-plugin')
const {StatsWriterPlugin} = require('webpack-stats-plugin')
const webpackDevConfig = require('./webpack.config.dev')
const webpackProdConfig = {
...webpackDevConfig,
output: {
chunkFilename: 'javascripts/[id].chunk.[contenthash].js',
filename: 'javascripts/[name].[contenthash].js',
path: resolve(__dirname, 'public'),
publicPath: '/',
},
devtool: 'source-map',
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
new TerserPlugin({
parallel: true,
terserOptions: {
output: {
ascii_only: true,
},
},
}),
],
},
plugins: [
...webpackDevConfig.plugins,
new StatsWriterPlugin({
filename: 'manifest.json',
transform: data =>
JSON.stringify(
{
css: data.assetsByChunkName.application[0],
js: data.assetsByChunkName.application[1],
},
null,
2
),
}),
],
}
module.exports = [
{
...webpackProdConfig,
entry: {
application: resolve(__dirname, 'app', 'index.js'),
},
},
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment