Skip to content

✨ Feat(pk: unit testing): Add workspace support for monorepos #865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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