Setup
Contents
This chapter covers setting up Jest for your Enonic app.
Sample project
You may follow the steps in this guide to configure Jest for an existing project. For simplicity, we will base this tutorial on the TypeScript starter.
To create a project for the tutorial, run the following command:
enonic create jest-tutorial -r starter-ts
This will create an app and place it in a folder called jest-tutorial.
Node modules
In order to write tests in TypeScript and run them using Jest, a few Node modules need to be installed.
Run the following command at the root of the Enonic project you created in the previous step:
npm install --save-dev jest jest-environment-jsdom @swc/core @swc/jest
Configuration
Next, you will need to add and tune a few configuration files:
jest.config.mjs
Jest needs to be configured to run Typescript tests. This is done by creating a jest.config.mjs file in the root of the project:
const DIR_SRC = 'src/main/resources';
const DIR_SRC_JEST = 'src/jest';
const DIR_SRC_JEST_CLIENT = `${DIR_SRC_JEST}/client`;
const DIR_SRC_JEST_SERVER = `${DIR_SRC_JEST}/server`;
const AND_BELOW = '**';
const SOURCE_FILES = `*.{ts,tsx}`;
const TEST_EXT = `{spec,test}.{ts,tsx}`;
const TEST_FILES = `*.${TEST_EXT}`;
// TypeScript (and TSX) test files are transpiled by SWC. Unlike ts-jest, SWC
// only strips types — it does not type-check. Type errors in tests are caught
// by your editor and the `check:types` script.
const transform = {
'^.+\\.(ts|js)x?$': [
'@swc/jest',
{
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
target: 'es2022',
},
sourceMaps: 'inline', // Important to get correct line numbers when running coverage tests
},
],
};
const commonConfig = {
collectCoverageFrom: [
`${DIR_SRC}/${AND_BELOW}/${SOURCE_FILES}`,
],
transform,
};
const clientSideConfig = {
...commonConfig,
displayName: {
color: 'white',
name: 'CLIENT',
},
// A map from regular expressions to module names or to arrays of module
// names that allow to stub out resources, like images or styles with a
// single module.
// Use <rootDir> string token to refer to rootDir value if you want to use
// file paths.
// Additionally, you can substitute captured regex groups using numbered
// backreferences.
moduleNameMapper: {
'/assets/(.*)': `<rootDir>/${DIR_SRC}/assets/$1`,
},
// Run clientside tests with DOM globals such as document and window
testEnvironment: 'jsdom',
// The glob patterns Jest uses to detect test files. By default it looks for
// .js, .jsx, .ts and .tsx files inside of __tests__ folders, as well as any
// files with a suffix of .test or .spec (e.g. Component.test.js or
// Component.spec.js). It will also find files called test.js or spec.js.
// (default: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[jt]s?(x)"
// ])
testMatch: [
`<rootDir>/${DIR_SRC_JEST_CLIENT}/${AND_BELOW}/${TEST_FILES}`,
],
};
const serverSideConfig = {
...commonConfig,
displayName: {
color: 'blue',
name: 'SERVER',
},
// A set of global variables that need to be available in all test
// environments.
// If you specify a global reference value (like an object or array) here,
// and some code mutates that value in the midst of running a test, that
// mutation will not be persisted across test runs for other test files.
// In addition, the globals object must be json-serializable, so it can't be
// used to specify global functions. For that, you should use setupFiles.
globals: {
app: {
name: 'com.example.myproject',
config: {},
version: '1.0.0'
},
},
// A map from regular expressions to module names or to arrays of module
// names that allow to stub out resources, like images or styles with a
// single module.
// Use <rootDir> string token to refer to rootDir value if you want to use
// file paths.
// Additionally, you can substitute captured regex groups using numbered
// backreferences.
moduleNameMapper: {
'/lib/myproject/(.*)': `<rootDir>/${DIR_SRC}/lib/myproject/$1`,
},
// Run serverside tests without DOM globals such as document and window
testEnvironment: 'node',
// The glob patterns Jest uses to detect test files. By default it looks for
// .js, .jsx, .ts and .tsx files inside of __tests__ folders, as well as any
// files with a suffix of .test or .spec (e.g. Component.test.js or
// Component.spec.js). It will also find files called test.js or spec.js.
// (default: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[jt]s?(x)"
// ])
testMatch: [
`<rootDir>/${DIR_SRC_JEST_SERVER}/${AND_BELOW}/${TEST_FILES}`,
],
};
/** @type {import('jest').Config} */
const customJestConfig = {
coverageProvider: 'v8', // To get correct line numbers under jsdom
passWithNoTests: true,
projects: [clientSideConfig, serverSideConfig],
};
export default customJestConfig;
Test files are transpiled by SWC (via @swc/jest), which strips the types without checking them — type errors in tests are caught by your editor. This is also why the config is a plain .mjs file: the project uses TypeScript 7, the native compiler, whose npm package does not ship the JavaScript compiler API that ts-jest and ts-node (needed for a jest.config.ts) depend on.
Read more about configuring Jest here.
tsconfig.json
In an Enonic Typescript project there can be both server-side and client-side code. In order for such code to be properly type checked and compiled, multiple tsconfig.json files are used.
Test files need a slightly different Typescript configuration than the sources they test. Jest itself does not read these files (SWC only strips types), but they give your editor the right type context for the test files. This is the purpose of the src/jest/client/tsconfig.json and src/jest/server/tsconfig.json files:
{
"extends": "../../main/resources/assets/tsconfig.json",
"include": [
"./**/*.spec.ts",
"./**/*.spec.tsx",
"./**/*.test.ts",
"./**/*.test.tsx",
],
"compilerOptions": {
// "baseUrl": ".",
"esModuleInterop": true,
// A series of entries which re-map imports to lookup locations relative
// to the baseUrl if set, or to the tsconfig file itself otherwise.
"paths": {
"/assets/*": ["../../main/resources/assets/*"],
},
"sourceMap": true, // Important to get correct line numbers when running coverage tests
// By default all visible ”@types” packages are included in your
// compilation. Packages in node_modules/@types of any enclosing folder
// are considered visible. For example, that means packages within
// ./node_modules/@types/, ../node_modules/@types/,
// ../../node_modules/@types/, and so on.
// If types is specified, only packages listed will be included in the
// global scope.
// This feature differs from typeRoots in that it is about specifying
// only the exact types you want included, whereas typeRoots supports
// saying you want particular folders.
// "types": [
// "jest", // Doesn't even work for test files in this folder?
// ],
}, // compilerOptions
}
{
"extends": "../../main/resources/tsconfig.json",
// Specifies an array of filenames or patterns to include in the program.
// These filenames are resolved relative to the directory containing the
// tsconfig.json file.
"include": [
"./**/*.spec.ts",
"./**/*.spec.tsx",
"./**/*.test.ts",
"./**/*.test.tsx",
],
"compilerOptions": {
// Import CommonJS modules in compliance with es6 modules spec
"esModuleInterop": true,
// A series of entries which re-map imports to lookup locations relative
// to the baseUrl if set, or to the tsconfig file itself otherwise.
"paths": {
// Keep in sync with src/main/resources/tsconfig.json — explicit entries
// make the IDE's auto-import suggest the runtime-correct specifiers.
"/lib/xp/admin": ["../../../node_modules/@enonic-types/lib-admin/admin"],
"/lib/xp/app": ["../../../node_modules/@enonic-types/lib-app/app"],
"/lib/xp/auditlog": ["../../../node_modules/@enonic-types/lib-auditlog/auditlog"],
"/lib/xp/auth": ["../../../node_modules/@enonic-types/lib-auth/auth"],
"/lib/xp/cluster": ["../../../node_modules/@enonic-types/lib-cluster/cluster"],
"/lib/xp/common": ["../../../node_modules/@enonic-types/lib-common/common"],
"/lib/xp/content": ["../../../node_modules/@enonic-types/lib-content/content"],
"/lib/xp/context": ["../../../node_modules/@enonic-types/lib-context/context"],
"/lib/xp/event": ["../../../node_modules/@enonic-types/lib-event/event"],
"/lib/xp/export": ["../../../node_modules/@enonic-types/lib-export/export"],
"/lib/xp/grid": ["../../../node_modules/@enonic-types/lib-grid/grid"],
"/lib/xp/i18n": ["../../../node_modules/@enonic-types/lib-i18n/i18n"],
"/lib/xp/io": ["../../../node_modules/@enonic-types/lib-io/io"],
"/lib/xp/mail": ["../../../node_modules/@enonic-types/lib-mail/mail"],
"/lib/xp/node": ["../../../node_modules/@enonic-types/lib-node/node"],
"/lib/xp/portal": ["../../../node_modules/@enonic-types/lib-portal/portal"],
"/lib/xp/project": ["../../../node_modules/@enonic-types/lib-project/project"],
"/lib/xp/repo": ["../../../node_modules/@enonic-types/lib-repo/repo"],
"/lib/xp/scheduler": ["../../../node_modules/@enonic-types/lib-scheduler/scheduler"],
"/lib/xp/schema": ["../../../node_modules/@enonic-types/lib-schema/schema"],
"/lib/xp/sse": ["../../../node_modules/@enonic-types/lib-sse/sse"],
"/lib/xp/task": ["../../../node_modules/@enonic-types/lib-task/task"],
"/lib/xp/value": ["../../../node_modules/@enonic-types/lib-value/value"],
"/lib/xp/vhost": ["../../../node_modules/@enonic-types/lib-vhost/vhost"],
"/lib/xp/websocket": ["../../../node_modules/@enonic-types/lib-websocket/websocket"],
"/lib/xp/*": ["../../../node_modules/@enonic-types/lib-*"],
"/*": ["../../main/resources/*"],
},
"sourceMap": true, // Important to get correct line numbers when running coverage tests
"types": [
"@enonic-types/global",
// "jest", // Doesn't even work for test files in this folder?
"node", // console
],
}, // compilerOptions
}
Execution
Finally, we wire the test command to execute Jest tests.
Using NPM
In order to run tests the following needs to be added to the package.json file:
{
"scripts": {
"test": "jest --no-cache --coverage"
}
}
You should now be able to run the tests with the following command:
npm test
You may also run a specific test like this: npm test src/jest/server/fibonacci.test.ts |
Using Enonic CLI
You may also run the tests using Enonic CLI. CLI uses the default build system (Gradle), so you must update the build.gradle file with the following lines:
tasks.register('npmTest', NpmTask) {
args = [
'run',
'test'
]
dependsOn npmInstall
environment = [
'FORCE_COLOR': 'true',
]
inputs.dir 'src/jest'
outputs.dir 'coverage'
}
test.dependsOn npmTest
As you can see, this simply tells Gradle to invoke the NPM’s test script. You may now test with the following command as well:
enonic project test