Skip to content
Open
48 changes: 43 additions & 5 deletions packages/unit-testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Suitecloud Unit Testing allows you to use unit testing with [Jest](https://jestj
- Allows you to create custom stubs for any module used in SuiteScript 2.x files.

For more information about the available SuitScript 2.x modules, see [SuiteScript 2.x Modules](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_4220488571.html).
For more information about all the mockable stubs, see the CORE_STUBS list in [SuiteCloudJestConfiguration.js](./jest-configuration/SuiteCloudJestConfiguration.js).
For a complete list of available stubs, see [Available Stubs](./stubs/README.md).

## Prerequisites
- Node.js version 22 LTS
Expand Down Expand Up @@ -77,8 +77,9 @@ The `jest.config.js` file must follow a specific structure. Depending on your Su
const SuiteCloudJestConfiguration = require("@oracle/suitecloud-unit-testing/jest-configuration/SuiteCloudJestConfiguration");

module.exports = SuiteCloudJestConfiguration.build({
projectFolder: 'src', //or your SuiteCloud project folder
projectType: SuiteCloudJestConfiguration.ProjectType.ACP,
projectFolder: 'src', // or your SuiteCloud project folder
projectType: SuiteCloudJestConfiguration.ProjectType.ACP,
rootDir: '.' // optional: automatically detected in monorepos
});
```

Expand All @@ -87,11 +88,47 @@ module.exports = SuiteCloudJestConfiguration.build({
const SuiteCloudJestConfiguration = require("@oracle/suitecloud-unit-testing/jest-configuration/SuiteCloudJestConfiguration");

module.exports = SuiteCloudJestConfiguration.build({
projectFolder: 'src', //or your SuiteCloud project folder
projectType: SuiteCloudJestConfiguration.ProjectType.SUITEAPP,
projectFolder: 'src', // or your SuiteCloud project folder
projectType: SuiteCloudJestConfiguration.ProjectType.SUITEAPP,
rootDir: '.' // optional: automatically detected in monorepos
});
```

### Project Structure and Root Directory Configuration

The `rootDir` property is optional with enhanced workspace detection. The configuration automatically:
- Detects common monorepo/workspace setups (pnpm, Yarn/npm workspaces, Lerna)
- Defaults to current directory in standalone projects
- Configures proper module resolution across workspaces
- Scopes test execution to the current package directory

Example project structures:

```
Standard Project Structure:
└── my-netsuite-project/ 👈 rootDir: "."
├── node_modules/
├── src/
├── __tests__/
└── jest.config.js

Monorepo Structure:
└── monorepo/
├── node_modules/
├── package.json # With workspaces configuration
└── packages/
└── my-suiteapp/ 👈 rootDir automatically detected
├── src/
├── __tests__/
└── jest.config.js
```

When working in a monorepo:
- Tests are automatically scoped to your current package directory
- Module resolution is configured across the workspace
- No manual rootDir configuration is required
- Supports pnpm, Yarn/npm workspaces, and Lerna configurations

## SuiteCloud Unit Testing Examples

Here you can find two examples on how to use SuiteCloud Unit Testing with a SuiteCloud project.
Expand Down Expand Up @@ -133,6 +170,7 @@ const SuiteCloudJestConfiguration = require("@oracle/suitecloud-unit-testing/jes
module.exports = SuiteCloudJestConfiguration.build({
projectFolder: 'src',
projectType: SuiteCloudJestConfiguration.ProjectType.ACP,
rootDir: '.'
});
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const CORE_STUBS_PATH = `${TESTING_FRAMEWORK_PATH}/stubs`;
const nodeModulesToTransform = [CORE_STUBS_PATH].join('|');
const SUITESCRIPT_FOLDER_REGEX = '^SuiteScripts(.*)$';
const ProjectInfoService = require('../services/ProjectInfoService');
const fs = require('fs');

const PROJECT_TYPE = {
SUITEAPP: 'SUITEAPP',
Expand Down Expand Up @@ -909,14 +910,28 @@ class SuiteCloudAdvancedJestConfiguration {
assert(options.projectType, "The 'projectType' property must be specified to generate a SuiteCloud Jest configuration");
this.projectFolder = this._getProjectFolder(options.projectFolder);
this.projectType = options.projectType;
this.customStubs = options.customStubs;
if (this.customStubs == null) {
this.customStubs = [];
}

this.customStubs = options.customStubs || [];
this.rootDir = this._detectWorkspaceRoot() || options.rootDir;

this.projectInfoService = new ProjectInfoService(this.projectFolder);
}

_detectWorkspaceRoot() {
let currentDir = process.cwd();
for (let i = 0; i < 5; i++) {
if (fs.existsSync(`${currentDir}/pnpm-workspace.yaml`) ||
fs.existsSync(`${currentDir}/lerna.json`) ||
(fs.existsSync(`${currentDir}/package.json`) &&
JSON.parse(fs.readFileSync(`${currentDir}/package.json`)).workspaces)) {
return currentDir;
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) break;
currentDir = parentDir;
}
return null;
}

_getProjectFolder(projectFolder) {
if (process.argv && process.argv.length > 0) {
for (let i = 0; i < process.argv.length; i++) {
Expand All @@ -942,8 +957,10 @@ class SuiteCloudAdvancedJestConfiguration {

_generateStubsModuleNameMapperEntries() {
const stubs = {};
const rootDirPrefix = this.rootDir ? this.rootDir : '<rootDir>';

const forEachFn = (stub) => {
stubs[`^${stub.module}$`] = stub.path;
stubs[`^${stub.module}$`] = stub.path.replace('<rootDir>', rootDirPrefix);
};
CORE_STUBS.forEach(forEachFn);
this.customStubs.forEach(forEachFn);
Expand All @@ -956,13 +973,21 @@ class SuiteCloudAdvancedJestConfiguration {
suiteScriptsFolder[SUITESCRIPT_FOLDER_REGEX] = this._getSuiteScriptFolderPath();

const customizedModuleNameMapper = Object.assign({}, this._generateStubsModuleNameMapperEntries(), suiteScriptsFolder);
return {

const config = {
transformIgnorePatterns: [`/node_modules/(?!${nodeModulesToTransform})`],
transform: {
'^.+\\.js$': `<rootDir>/node_modules/${TESTING_FRAMEWORK_PATH}/jest-configuration/SuiteCloudJestTransformer.js`,
'^.+\\.js$': `${this.rootDir || '<rootDir>'}/node_modules/${TESTING_FRAMEWORK_PATH}/jest-configuration/SuiteCloudJestTransformer.js`,
},
moduleNameMapper: customizedModuleNameMapper,
roots: [process.cwd()]
};

if (this.rootDir) {
config.rootDir = this.rootDir;
}

return config;
}
}

Expand Down
Loading