Describe Your Question
Hi. I’m using OHIF 3.7.0 and I’m trying to run some Jest tests, but I get the error:
/ohif/dev/viewer/platform/ui/src/index.js:7
import * as Types from './types';
^^^^^^
SyntaxError: Cannot use import statement outside a module
1 | import React, { useState } from "react";
> 2 | import { Select } from "@ohif/ui";
| ^
at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1505:14)
at Object.<anonymous> (src/Form/form.tsx:2:1)
at Object.<anonymous> (__tests__/Form/form-test.tsx:4:1)
What steps can we follow to reproduce the bug?
- Using OHIF 3.7.0, create a new custom extension using the CLI, and display a React Component in it, using the component Select from the ohif/ui module (see my code below)
- Install the dependencies (see my code below) and write a test where you render the React Component mentioned in the previous step
- Configure Babel, Jest and Typescript using the code below
- Run Jest using
yarn jest
Project structure
- ohif
– dev
— extensions (for custom extensions)
— modes (for custom modes)
— viewer (the default OHIF’s repo)
Code
- My extension’s
package.json
file (snippet):
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/extension-cornerstone": "^3.0.0",
"@ohif/extension-default": "^3.0.0",
"@ohif/i18n": "^1.0.0",
"@ohif/ui": "^3.7.0",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^12.2.2",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1",
"webpack": "^5.50.0",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"react-i18next": "^14.1.2"
},
"devDependencies": {
"@babel/core": "^7.21.4",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "^7.24.6",
"@babel/preset-react": "^7.24.6",
"@babel/preset-typescript": "^7.13.0",
"@testing-library/dom": "^10.1.0",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.5.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"babel-eslint": "9.x",
"babel-jest": "^29.7.0",
"babel-loader": "^8.2.4",
"babel-plugin-inline-react-svg": "^2.0.2",
"babel-plugin-module-resolver": "^5.0.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0",
"cross-env": "^7.0.3",
"dotenv": "^14.1.0",
"eslint": "^5.0.1",
"eslint-loader": "^2.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-junit": "^16.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.1.4",
"typescript": "^5.4.5",
"webpack": "^5.50.0",
"webpack-cli": "^4.7.2",
"webpack-merge": "^5.7.3"
}
- My extension’s
babel.config.js
file
// https://babeljs.io/docs/en/options#babelrcroots
const { extendDefaultPlugins } = require('svgo');
module.exports = {
plugins: [
[
'inline-react-svg',
{
svgo: {
plugins: extendDefaultPlugins([
{
name: 'removeViewBox',
active: false,
},
]),
},
},
],
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
],
env: {
test: {
presets: [
[
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
'@babel/preset-env',
{
modules: 'commonjs',
debug: false,
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
],
},
production: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
development: {
presets: [
// WebPack handles ES6 --> Target Syntax
['@babel/preset-env', { modules: false }],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: ['react-hot-loader/babel'],
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
},
},
};
- My extension’s
jest.config.js
file
// https://github.com/facebook/jest/issues/3613
// Yarn Doctor: `npx @yarnpkg/doctor .` -->
// '<rootDir>' warning:
// Stringsimport { pathsToModuleNameMapper, JestConfigWithTsJest } from "ts-jest";
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig.json");
module.exports = {
preset: "ts-jest",
verbose: true,
// roots: ['<rootDir>/src'],
testMatch: ["<rootDir>/src/**/*.test.js", "<rootDir>/__tests__/**/*"],
testPathIgnorePatterns: ["<rootDir>/node_modules/"],
testEnvironment: "jsdom",
moduleFileExtensions: ["js", "jsx", "ts", "tsx"],
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/src/__mocks__/fileMock.js",
"\\.(css|less)$": "identity-obj-proxy",
...pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>" }),
},
// Setup
// setupFiles: ["jest-canvas-mock/lib/index.js"],
// Coverage
reporters: [
"default",
// Docs: https://www.npmjs.com/package/jest-junit
[
"jest-junit",
{
addFileAttribute: true, // CircleCI Only
},
],
],
collectCoverage: false,
collectCoverageFrom: [
"<rootDir>/src/**/*.{js,jsx}",
// Not
"!<rootDir>/src/**/*.test.js",
"!**/node_modules/**",
"!**/__tests__/**",
"!<rootDir>/dist/**",
],
};
- My extension’s
tsconfig.json
file:
{
"compilerOptions": {
"baseUrl": "./",
"checkJs": true,
"isolatedModules": true,
"emitDeclarationOnly": true,
"jsx": "react",
"resolveJsonModule": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": true,
"moduleResolution": "node",
"outDir": "./dist",
"paths": {
"@ohif/core": ["../../viewer/platform/core/src"],
"@ohif/ui": ["../../viewer/platform/ui/src"],
"@ohif/i18n": ["../../viewer/platform/i18n/src"],
"@ohif/app": ["../../viewer/platform/app/src"],
"@ohif/extension-common": ["../common/src"],
"~/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
- My extension (snippet):
import React, { useState } from "react";
import { Select } from "@ohif/ui";
const Form = ({ servicesManager, extensionManager, studies, dataSource }) => {
// Load the cohorts
const { MyService } = servicesManager.services;
const cohorts = [{ name: "Select cohort..." }].concat(
MyService.getState().memberCohorts,
);
const [cohort, setCohort] = useState<any | null>(null);
return (
<div className="h-[480px]">
<div className="border-primary-main px-2 py-2">
<div className="flex flex-row gap-2 py-2">
<label className="text-base text-primary-light whitespace-nowrap">
Select the cohort you want to work with:
</label>
<Select
id="cohorts"
isMulti={false}
isClearable={false}
closeMenuOnSelect={true}
hideSelectedOptions={false}
options={cohorts.map((c) => ({
label: c.name,
value: c.name,
}))}
onChange={(v) => setCohort(v)}
value={cohort}
/>
</div>
</div>
</div>
);
};
export default Form;
- My test:
import React from "react";
import { render } from "@testing-library/react";
import Form from "~/Form/form";
const mockServicesManager = {
services: {
MyService: {
getState: jest.fn().mockReturnValue({
memberCohorts: [{ name: 'Cohort 1' }, { name: 'Cohort 2' }],
}),
},
},
};
const mockExtensionManager = {};
const mockStudies = [];
const mockDataSource = {};
test("should render", () => {
render(
<Form
servicesManager={mockServicesManager}
extensionManager={mockExtensionManager}
studies={mockStudies}
dataSource={mockDataSource}
/>
);
})