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 Enonic intro app.

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-environment-jsdom ts-node ts-jest

Configuration

Next, you will need to add and tune a few configuration files:

jest.config.ts

Jest needs to be configured to run Typescript tests. This is done by creating a jest.config.ts file in the root of the project:

jest.config.ts
import type { Config } from '@jest/types';


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}`;


const commonConfig: Config.InitialProjectOptions = {
  collectCoverageFrom: [
    `${DIR_SRC}/${AND_BELOW}/${SOURCE_FILES}`,
  ],

  // Insert Jest's globals (expect, test, describe, beforeEach etc.) into the
  // global environment. If you set this to false, you should import from @jest/globals, e.g.
  // injectGlobals: true, // Doesn't seem to work?
};

const clientSideConfig: Config.InitialProjectOptions = {
  ...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}`,
  ],
  transform: {
    "^.+\\.(ts|js)x?$": [
      'ts-jest',
      {
        tsconfig: `${DIR_SRC_JEST_CLIENT}/tsconfig.json`
      }
    ]
  }
};

const serverSideConfig: Config.InitialProjectOptions = {
  ...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`,
  },

  // A list of paths to modules that run some code to configure or set up the
  // testing environment. Each setupFile will be run once per test file. Since
  // every test runs in its own environment, these scripts will be executed in
  // the testing environment before executing setupFilesAfterEnv and before
  // the test code itself.
  setupFiles: [
    `<rootDir>/${DIR_SRC_JEST_SERVER}/setupFile.ts`
  ],

  // 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}`,
  ],

  transform: {
    "^.+\\.(ts|js)x?$": [
      'ts-jest',
      {
          tsconfig: `${DIR_SRC_JEST_SERVER}/tsconfig.json`
      }
    ]
  },
};

const customJestConfig: Config.InitialOptions = {
  coverageProvider: 'v8', // To get correct line numbers under jsdom
  passWithNoTests: true,
  projects: [clientSideConfig, serverSideConfig],
};

export default customJestConfig;

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.

When running tests the Typescript configuration files might need to look slightly different. This is the case for the src/jest/client/tsconfig.json and src/jest/server/tsconfig.json files:

src/jest/client/tsconfig.json
{
  "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

}
src/jest/server/tsconfig.json
{
  "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": {
    // 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": {
      "/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:

package.json
{
  "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:

build.gradle
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

Summary

With the above setup in place, it is possible to write tests in Typescript and run them using Jest. The tests will be run on every build and the coverage will be reported.

Let’s learn how to write our first test.


Contents

Contents