diff --git a/packages/unit-testing/README.md b/packages/unit-testing/README.md index 8a17a484..bdb58ade 100644 --- a/packages/unit-testing/README.md +++ b/packages/unit-testing/README.md @@ -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 @@ -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 }); ``` @@ -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. @@ -133,6 +170,7 @@ const SuiteCloudJestConfiguration = require("@oracle/suitecloud-unit-testing/jes module.exports = SuiteCloudJestConfiguration.build({ projectFolder: 'src', projectType: SuiteCloudJestConfiguration.ProjectType.ACP, + rootDir: '.' }); ``` diff --git a/packages/unit-testing/jest-configuration/SuiteCloudJestConfiguration.js b/packages/unit-testing/jest-configuration/SuiteCloudJestConfiguration.js index 229c4306..c66dcf4e 100644 --- a/packages/unit-testing/jest-configuration/SuiteCloudJestConfiguration.js +++ b/packages/unit-testing/jest-configuration/SuiteCloudJestConfiguration.js @@ -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', @@ -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++) { @@ -942,8 +957,10 @@ class SuiteCloudAdvancedJestConfiguration { _generateStubsModuleNameMapperEntries() { const stubs = {}; + const rootDirPrefix = this.rootDir ? this.rootDir : ''; + const forEachFn = (stub) => { - stubs[`^${stub.module}$`] = stub.path; + stubs[`^${stub.module}$`] = stub.path.replace('', rootDirPrefix); }; CORE_STUBS.forEach(forEachFn); this.customStubs.forEach(forEachFn); @@ -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$': `/node_modules/${TESTING_FRAMEWORK_PATH}/jest-configuration/SuiteCloudJestTransformer.js`, + '^.+\\.js$': `${this.rootDir || ''}/node_modules/${TESTING_FRAMEWORK_PATH}/jest-configuration/SuiteCloudJestTransformer.js`, }, moduleNameMapper: customizedModuleNameMapper, + roots: [process.cwd()] }; + + if (this.rootDir) { + config.rootDir = this.rootDir; + } + + return config; } } diff --git a/packages/unit-testing/stubs/README.md b/packages/unit-testing/stubs/README.md new file mode 100644 index 00000000..972ac56a --- /dev/null +++ b/packages/unit-testing/stubs/README.md @@ -0,0 +1,525 @@ +# Available SuiteScript 2.x Stubs + +This directory contains all the available stubs for SuiteScript 2.x modules. Each stub provides mock implementations that can be used in your unit tests. + +## Currently Supported Modules + +
+Action + +- `N/action` - Core action module + - `find()` - Search for available record actions + - `get()` - Get executable record action + - `execute()` - Execute record action + - `executeBulk()` - Execute bulk record action +- `N/action/instance` - Action instance operations + - Properties: description, id, label, parameters, recordType + - Methods: execute(), executeBulk() +
+ +
+Authentication + +- `N/auth` - Authentication operations + - `changeEmail()` - Change user email + - `changePassword()` - Change user password +
+ +
+Cache + +- `N/cache` - Caching operations + - `getCache()` - Get named, scoped cache + - Scope types: PRIVATE, PROTECTED, PUBLIC +- `N/cache/instance` - Cache instance management + - Methods: get(), put(), remove() + - Properties: name, scope +
+ +
+Certificate Control + +- `N/certificateControl` - Certificate management + - Operations: createCertificate(), deleteCertificate(), loadCertificate() + - Types: PFX, P12, PEM +- `N/certificateControl/certificate` - Certificate operations + - Properties: file, subsidiaries, restrictions, notifications + - Methods: save(), toJSON() +
+ +
+Commerce + +- `N/commerce/recordView` - Commerce record viewing + - `viewItems()` - Get item field values + - `viewWebsite()` - Get website field values +- `N/commerce/promising` - Date promising functionality + - `getAvailableDate()` - Calculate promise dates +- `N/commerce/webstore/order` - Webstore order management + - `createOrLoad()` - Access Sales Order record + - `save()` - Update sales order +- `N/commerce/webstore/shopper` - Shopper management + - Methods: getCurrentShopper(), createCustomer(), createGuest() +- `N/commerce/webstore/shopper/instance` - Individual shopper operations + - Properties: currencyId, languageLocale, subsidiaryId, details +
+ +
+Compression + +- `N/compress` - Compression utilities + - `gzip()` - Compress with gzip + - `gunzip()` - Decompress gzip + - `createArchiver()` - Create archive +- `N/compress/archiver` - Archive creation and management + - Methods: add(), archive() + - Supported formats: CPIO, TAR, TGZ, TBZ2, ZIP +
+ +
+Configuration + +- `N/config` - System configuration access + - `load()` - Load configuration object + - Types: USER_PREFERENCES, COMPANY_INFORMATION, FEATURES, etc. +
+ +
+Cryptography + +- `N/crypto` - Core cryptography operations + - Methods: createSecretKey(), createHash(), createHmac() + - Algorithms: SHA1, SHA256, SHA512, MD5 +- `N/crypto/certificate/*` + - `signedXml` - XML signing operations + - `signer` - Digital signing capabilities + - `verifier` - Signature verification +- `N/crypto/cipher` - Encryption operations + - Methods: update(), final() +- `N/crypto/cipherPayload` - Encrypted data handling + - Properties: iv, ciphertext +- `N/crypto/decipher` - Decryption operations + - Methods: update(), final() +- `N/crypto/hash` - Hashing functionality + - Methods: update(), digest() +- `N/crypto/hmac` - HMAC operations + - Methods: update(), digest() +- `N/crypto/secretKey` - Secret key management +
+ +
+Currency + +- `N/currency` - Currency operations + - Methods: exchangeRate(), getExchangeRate() + - Support for currency conversion and exchange rates +
+ +
+Current Record + +- `N/currentRecord` - Current record operations + - Methods: get(), create(), load() +- `N/currentRecord/instance` - Current record instance + - Properties: id, isDynamic, type + - Methods: getValue(), setValue(), save() +- `N/currentRecord/field` - Field operations + - Methods: getValue(), setText(), getField() +- `N/currentRecord/sublist` - Sublist management + - Methods: getLineCount(), getSublistValue(), setSublistValue() +
+ +
+Dataset + +- `N/dataset` - Dataset operations + - Methods: load(), save(), getData() +- `N/dataset/instance` - Dataset instance management +- `N/dataset/condition` - Dataset conditions +- `N/dataset/column` - Dataset columns +- `N/dataset/join` - Dataset joins +
+ +
+Dataset Link + +- `N/datasetLink` - Dataset linking operations + - Methods: create(), link(), unlink() +- `N/datasetLink/instance` - Dataset link instance +
+ +
+Email + +- `N/email` - Email operations + - Methods: send(), sendBulk(), sendCampaign() + - Support for attachments and templates +
+ +
+Encode + +- `N/encode` - Encoding utilities + - Methods: convert(), escape(), unescape() + - Support for various encoding formats +
+ +
+Error + +- `N/error` - Error handling + - Methods: create(), throwSuiteScriptError() +- `N/error/suiteScriptError` - SuiteScript specific errors +- `N/error/userEventError` - User event specific errors +
+ +
+File + +- `N/file` - File operations + - Methods: create(), load(), delete(), copy() +- `N/file/instance` - File instance + - Properties: id, name, size, url + - Methods: getContents(), setContents(), save() +- `N/file/fileLines` - File line operations +- `N/file/fileSegments` - File segment operations +- `N/file/reader` - File reading utilities +
+ +
+Format + +- `N/format` - Formatting utilities + - Methods: format(), parse(), Type definitions +- `N/format/i18n` - Internationalization + - Currency formatting + - Number formatting + - Phone number handling +
+ +
+HTTP/HTTPS + +- `N/http` - HTTP operations + - Methods: get(), post(), put(), delete() +- `N/http/clientResponse` - HTTP response handling +- `N/https` - HTTPS operations + - Methods: get(), post(), put(), delete() +- `N/https/clientResponse` - HTTPS response handling +- `N/https/secretKey` - HTTPS security +
+ +
+Record + +- `N/record` - Core record operations + - Methods: create(), load(), copy(), transform() + - Type definitions and constants +- `N/record/instance` - Record instance management + - Methods: getValue(), setValue(), save() + - Sublist operations +- `N/record/field` - Field operations + - Methods: getValue(), setText(), getField() +- `N/record/column` - Column management +- `N/record/line` - Line item operations +- `N/record/sublist` - Sublist management +
+ +
+Search + +- `N/search` - Search operations + - Methods: create(), load(), global() + - Search types and filters +- `N/search/instance` - Search instance + - Methods: run(), save(), getFilters() +- `N/search/column` - Search column configuration +- `N/search/filter` - Search filtering +- `N/search/result` - Search result handling +- `N/search/resultSet` - Result set operations +- `N/search/setting` - Search settings +
+ +
+SFTP + +- `N/sftp` - SFTP operations + - Methods: createConnection(), connect() +- `N/sftp/connection` - SFTP connection management + - Methods: upload(), download(), list() +
+ +
+SSO + +- `N/sso` - Single Sign-On operations + - Methods: generateToken(), validateToken() +
+ +
+SuiteApp Info + +- `N/suiteAppInfo` - SuiteApp information operations + - Methods: getSuiteAppInfo(), getModuleInfo() +
+ +
+Task + +- `N/task` - Task management + - Methods: create(), checkStatus(), submit() +- Task types: + - CSV Import + - Map/Reduce + - Scheduled Script + - Search + - SuiteQL + - Workflow +
+ +
+Transaction + +- `N/transaction` - Transaction operations + - Methods: void(), commit(), rollback() +
+ +
+Translation + +- `N/translation` - Translation operations + - Methods: get(), load() +- `N/translation/handle` - Translation handling +
+ +
+UI + +- `N/ui` - UI operations +- `N/ui/dialog` - Dialog management + - Methods: create(), show() +- `N/ui/message` - Message handling + - Methods: create(), show() +- `N/ui/serverWidget` - Server widget creation + - Components: form, field, sublist, tab +
+ +
+URL + +- `N/url` - URL operations + - Methods: format(), resolve() + - URL formatting and resolution +
+ +
+Util + +- `N/util` - Utility operations + - Methods: isArray(), isBoolean(), isDate() + - Various utility functions +
+ +
+Workflow + +- `N/workflow` - Workflow operations + - Methods: initiate(), trigger() + - Workflow state management +
+ +
+Workbook + +- `N/workbook` - Workbook operations + - Methods: create(), load(), save() +- `N/workbook/section` - Section management + - Methods: addSection(), removeSection() +- `N/workbook/style` - Style configuration + - Properties: font, alignment, borders +- `N/workbook/pivot` - Pivot table operations + - Methods: createPivotTable(), refresh() +- `N/workbook/table` - Table management + - Methods: addRow(), addColumn() +- `N/workbook/chart` - Chart creation and management +- `N/workbook/range` - Range operations +- `N/workbook/field` - Field management +
+ +
+XML + +- `N/xml` - XML operations + - Methods: Parser, XPath +- `N/xml/document` - XML document handling +- `N/xml/element` - XML element operations +- `N/xml/node` - XML node management +
+ +## Using Stubs in Tests + +
+Record Operations Example + +```javascript +import record from 'N/record'; +import Record from 'N/record/instance'; + +jest.mock('N/record'); +jest.mock('N/record/instance'); + +describe('Record Operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should perform record operations', () => { + // given + record.load.mockReturnValue(Record); + Record.save.mockReturnValue(123); + + // when + const loadedRecord = record.load({ type: 'salesorder', id: 123 }); + loadedRecord.setValue({ fieldId: 'memo', value: 'test memo' }); + const savedId = loadedRecord.save(); + + // then + expect(record.load).toHaveBeenCalledWith({ type: 'salesorder', id: 123 }); + expect(Record.setValue).toHaveBeenCalledWith({ fieldId: 'memo', value: 'test memo' }); + expect(Record.save).toHaveBeenCalled(); + expect(savedId).toBe(123); + }); +}); +``` +
+ +
+Search Operations Example + +```javascript +import search from 'N/search'; +import SearchInstance from 'N/search/instance'; +import SearchResultSet from 'N/search/resultSet'; + +jest.mock('N/search'); +jest.mock('N/search/instance'); +jest.mock('N/search/resultSet'); + +describe('Search Operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should perform search operations', () => { + // given + search.create.mockReturnValue(SearchInstance); + SearchInstance.run.mockReturnValue(SearchResultSet); + + const mockResults = [ + { id: '1', getValue: jest.fn().mockReturnValue('SO123') }, + { id: '2', getValue: jest.fn().mockReturnValue('SO124') } + ]; + + SearchResultSet.each.mockImplementation(callback => { + mockResults.forEach(result => callback(result)); + return true; + }); + + // when + const searchInstance = search.create({ + type: 'salesorder', + filters: [], + columns: ['tranid'] + }); + + const resultSet = searchInstance.run(); + const tranIds = []; + + resultSet.each(result => { + tranIds.push(result.getValue({ name: 'tranid' })); + return true; + }); + + // then + expect(search.create).toHaveBeenCalledWith({ + type: 'salesorder', + filters: [], + columns: ['tranid'] + }); + expect(SearchInstance.run).toHaveBeenCalled(); + expect(tranIds).toEqual(['SO123', 'SO124']); + }); +}); +``` +
+ +
+Crypto Operations Example + +```javascript +import crypto from 'N/crypto'; +import Cipher from 'N/crypto/cipher'; + +jest.mock('N/crypto'); +jest.mock('N/crypto/cipher'); + +describe('Crypto Operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should perform crypto operations', () => { + // given + crypto.createCipher.mockReturnValue(Cipher); + Cipher.final.mockReturnValue({ iv: 'test-iv', ciphertext: 'encrypted-data' }); + + // when + const cipher = crypto.createCipher({ + algorithm: crypto.EncryptionAlg.AES, + key: 'mykey' + }); + + cipher.update({ input: 'test data' }); + const result = cipher.final(); + + // then + expect(crypto.createCipher).toHaveBeenCalledWith({ + algorithm: crypto.EncryptionAlg.AES, + key: 'mykey' + }); + expect(Cipher.update).toHaveBeenCalledWith({ input: 'test data' }); + expect(result).toEqual({ iv: 'test-iv', ciphertext: 'encrypted-data' }); + }); +}); +``` +
+ +## Creating Custom Stubs + +
+Custom Stub Example + +If you need to use a module that isn't stubbed yet, you can create your own custom stub. See the [Custom Stub Example](../README.md#custom-stub-example) in the main README. + +Example custom stub: +```javascript +define([], function() { + var customModule = function() {}; + + customModule.prototype.myMethod = function(options) {}; + + return new customModule(); +}); +``` +
+ +## Module Documentation + +For detailed information about each module's capabilities and methods: +- Refer to the JSDoc comments in the respective stub files +- Check the [NetSuite Help Center](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_4220488571.html) +- See the [SuiteScript 2.x API Reference](https://docs.oracle.com/en/cloud/saas/netsuite/nsoa-online-help/chapter_4220488571.html) + +## Contributing + +If you find any issues with existing stubs or would like to contribute new stubs, please follow our [contribution guidelines](../../../CONTRIBUTING.md). \ No newline at end of file