From 6b55b58190ef3aa254eb93718085b2264b0a2889 Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Fri, 14 Aug 2026 14:06:45 +0400 Subject: [PATCH 01/14] Update CHANGELOG and README --- CHANGELOG.md | 5 +++++ README.md | 22 ++++++++++++++++++++++ version_fragment | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c6d94fc..e54ea5d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### Changed +- The client has been migrated to TypeScript. It now ships with bundled type + definitions (no separate `@types` package required) and exposes constants, + models and helpers via subpath imports (e.g. + `@reportportal/client-javascript/constants`). ### Added - `retry_of` property is now automatically included in the `startTestItem` request payload when `retry: true` and a previous attempt exists in the diff --git a/README.md b/README.md index 43e66e9a..aabde539 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,28 @@ rpClient.checkConnect().then(() => { }); ``` +## TypeScript + +Starting from version 5.6.0, the client has been migrated to TypeScript and ships with bundled type definitions. It works with both CommonJS and ES module projects: + +```typescript +import RPClient from '@reportportal/client-javascript'; + +const rpClient = new RPClient({ + apiKey: 'reportportalApiKey', + endpoint: 'http://your-instance.com:8080/api/v1', + launch: 'LAUNCH_NAME', + project: 'PROJECT_NAME', +}); +``` + +Constants, models and helpers are available via subpath imports: + +```typescript +import { RP_STATUSES } from '@reportportal/client-javascript/constants'; +import PublicReportingAPI from '@reportportal/client-javascript/publicReportingAPI'; +``` + ## Configuration When creating a client instance, you need to specify the following options. diff --git a/version_fragment b/version_fragment index 9eb7b90e..acb503f9 100644 --- a/version_fragment +++ b/version_fragment @@ -1 +1 @@ -patch +minor From 89a59728d2b4ba91b4a2133d7f4b7af9ec9723d9 Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Fri, 14 Aug 2026 14:16:05 +0400 Subject: [PATCH 02/14] =?UTF-8?q?Update=20the=20README=20based=20on=20Code?= =?UTF-8?q?Rabbit=E2=80=99s=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aabde539..b800fb8f 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ import RPClient from '@reportportal/client-javascript'; const rpClient = new RPClient({ apiKey: 'reportportalApiKey', - endpoint: 'http://your-instance.com:8080/api/v1', + endpoint: 'https://your-instance.com:8080/api/v1', launch: 'LAUNCH_NAME', project: 'PROJECT_NAME', }); From 4d89fd3749fa4eb02ffa795722597fff6b28696d Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Tue, 18 Aug 2026 13:28:49 +0400 Subject: [PATCH 03/14] Resolve subpath imports without consumer eslint config Subpath files live under build/lib and were reachable only via package.json#exports. Filesystem-walking resolvers such as eslint-import-resolver-node, the default in eslint-plugin-import, don't read exports, so every subpath import was flagged import/no-unresolved and each consumer needed its own ignore rule. Build now emits a thin re-export (plus .d.ts) at each subpath location. These are never loaded at runtime, since exports still wins. They just give filesystem resolvers something to find, and restore the lib/** layout from 5.5.x. Generated files are gitignored and removed by npm run clean. --- .eslintignore | 10 ++ .gitignore | 12 ++ CHANGELOG.md | 6 + DEV_GUIDE.md | 18 +++ package.json | 25 +++- scripts/generate-resolver-facades.js | 164 +++++++++++++++++++++++++++ src/lib/helpers.ts | 10 ++ 7 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 scripts/generate-resolver-facades.js diff --git a/.eslintignore b/.eslintignore index bec94421..1245de7b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,13 @@ node_modules build __tests__/**/*.json +# Subpath facades generated by scripts/generate-resolver-facades.js +/lib +/helpers.js +/helpers.d.ts +/constants.js +/constants.d.ts +/models.js +/models.d.ts +/publicReportingAPI.js +/publicReportingAPI.d.ts diff --git a/.gitignore b/.gitignore index 7a1c2746..7d22247e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,15 @@ coverage.lcov coverage/ .npmrc build + +# Subpath facades generated by scripts/generate-resolver-facades.js +/.generated-facades.json +/lib +/helpers.js +/helpers.d.ts +/constants.js +/constants.d.ts +/models.js +/models.d.ts +/publicReportingAPI.js +/publicReportingAPI.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e54ea5d0..fad49a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ definitions (no separate `@types` package required) and exposes constants, models and helpers via subpath imports (e.g. `@reportportal/client-javascript/constants`). +- Every public subpath is now backed by a real file in the published package, generated at + build time next to the `exports` map. Tools that resolve imports by walking the filesystem + instead of reading `exports` — most notably `eslint-import-resolver-node`, the default + resolver of `eslint-plugin-import` — resolve subpath imports out of the box, so consumers + no longer need an `import/no-unresolved` ignore or an extra resolver dependency. The + `lib/**` paths published up to 5.5.x resolve again as well. ### Added - `retry_of` property is now automatically included in the `startTestItem` request payload when `retry: true` and a previous attempt exists in the diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md index 7e47ed35..f454bf13 100644 --- a/DEV_GUIDE.md +++ b/DEV_GUIDE.md @@ -3,6 +3,24 @@ Internal notes for contributors. This content is intentionally kept out of README.md, which is published to package registries. +## Subpath facades + +`npm run build` compiles to `build/` and then runs `scripts/generate-resolver-facades.js`, +which writes a one-line re-export at every public subpath location — `helpers.js`, +`constants.js`, `models.js`, `publicReportingAPI.js` and a `lib/**` mirror of the compiled +tree — together with matching `.d.ts` files. + +Those files are never loaded at runtime: Node, TypeScript and bundlers all read the +`exports` / `typesVersions` maps in `package.json` and go straight to `build/lib`. They +exist for tools that resolve imports by walking the filesystem, chiefly +`eslint-import-resolver-node` (the default resolver of `eslint-plugin-import`), which +otherwise reports `import/no-unresolved` for every subpath import and forces each consumer +to configure an ignore. They also keep the `lib/**` paths published up to 5.5.x resolvable. + +Everything the script writes is gitignored, recorded in `.generated-facades.json` and +removed by `npm run clean`. When a new subpath is added to `exports`, add the matching alias +to the `aliases` map in the script — the `lib/**` mirror updates itself. + ## Code knowledge graph This repo carries a local **code knowledge graph** ([colbymchenry/codegraph](https://github.com/colbymchenry/codegraph)) diff --git a/package.json b/package.json index e91fe887..595bbc60 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "author": "ReportPortal.io", "scripts": { "codegraph": "bash scripts/codegraph.sh", - "build": "npm run clean && tsc", - "clean": "rimraf ./build", + "build": "npm run clean && tsc && npm run facades", + "facades": "node scripts/generate-resolver-facades.js", + "clean": "node scripts/generate-resolver-facades.js --clean && rimraf ./build", "lint": "eslint \"src/**/*.ts\"", "format": "npm run lint -- --fix", "test": "jest", @@ -18,7 +19,16 @@ }, "files": [ "/build", - "/VERSION" + "/VERSION", + "/lib", + "/helpers.js", + "/helpers.d.ts", + "/constants.js", + "/constants.d.ts", + "/models.js", + "/models.d.ts", + "/publicReportingAPI.js", + "/publicReportingAPI.d.ts" ], "main": "./build/lib/report-portal-client", "types": "./build/lib/report-portal-client.d.ts", @@ -70,6 +80,15 @@ "require": "./build/lib/*.js" } }, + "typesVersions": { + "*": { + "constants": ["./build/lib/constants/index.d.ts"], + "models": ["./build/lib/models/index.d.ts"], + "helpers": ["./build/lib/helpers.d.ts"], + "publicReportingAPI": ["./build/lib/publicReportingAPI.d.ts"], + "lib/*": ["./build/lib/*"] + } + }, "engines": { "node": ">=14.17.0" }, diff --git a/scripts/generate-resolver-facades.js b/scripts/generate-resolver-facades.js new file mode 100644 index 00000000..338230c2 --- /dev/null +++ b/scripts/generate-resolver-facades.js @@ -0,0 +1,164 @@ +/* eslint-disable no-console */ +/** + * Generates plain filesystem facades for every public subpath of the package. + * + * The compiled output lives under `build/lib`, and subpaths such as + * `@reportportal/client-javascript/helpers` are mapped there through + * `package.json#exports` / `#typesVersions`. Node, TypeScript and bundlers all read those + * maps, but tools that resolve imports by walking the filesystem do not — most notably + * `eslint-import-resolver-node`, the default resolver of `eslint-plugin-import`, which + * reports `import/no-unresolved` for every subpath import. + * + * To keep those tools working without any consumer-side configuration, this script emits a + * thin file at each subpath location that simply re-exports the real module. The files are + * never loaded at runtime (the `exports` map always wins), they only give filesystem-based + * resolvers something to find. They also restore the `lib/**` layout published up to 5.5.x, + * so imports written against the pre-TypeScript releases keep resolving. + * + * Everything written here is generated at build time, listed in `MANIFEST` and removed by + * `--clean`; nothing is meant to be edited or committed by hand. + */ + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.join(__dirname, '..'); +const BUILD_LIB = path.join(ROOT, 'build', 'lib'); +const MANIFEST = path.join(ROOT, '.generated-facades.json'); + +const toPosix = (p) => p.split(path.sep).join('/'); + +const readManifest = () => { + try { + const entries = JSON.parse(fs.readFileSync(MANIFEST, 'utf8')); + return Array.isArray(entries) ? entries : []; + } catch (e) { + return []; + } +}; + +/** Removes the files listed in the manifest, plus any directories left empty behind them. */ +const clean = () => { + const entries = readManifest(); + const dirs = new Set(); + + entries.forEach((entry) => { + const target = path.join(ROOT, entry); + if (fs.existsSync(target)) { + fs.rmSync(target); + } + dirs.add(path.dirname(target)); + }); + + // Deepest first, so nested directories empty out before their parents are checked. + [...dirs] + .sort((a, b) => b.length - a.length) + .forEach((dir) => { + if (dir !== ROOT && fs.existsSync(dir) && fs.readdirSync(dir).length === 0) { + fs.rmdirSync(dir); + } + }); + + if (fs.existsSync(MANIFEST)) { + fs.rmSync(MANIFEST); + } + + return entries.length; +}; + +/** Every `.js` file under `build/lib`, as paths relative to `build/lib`. */ +const collectModules = (dir = BUILD_LIB) => + fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const absolute = path.join(dir, entry.name); + if (entry.isDirectory()) { + return collectModules(absolute); + } + return entry.isFile() && entry.name.endsWith('.js') + ? [toPosix(path.relative(BUILD_LIB, absolute)).replace(/\.js$/, '')] + : []; + }); + +/** + * Maps a subpath the package exposes to the `build/lib` module backing it. + * `lib/**` mirrors the compiled tree; the short aliases match the `exports` map. + */ +const buildFacadeMap = () => { + const modules = collectModules(); + const facades = new Map(modules.map((module) => [`lib/${module}`, module])); + + const aliases = { + helpers: 'helpers', + publicReportingAPI: 'publicReportingAPI', + constants: 'constants/index', + models: 'models/index', + }; + + Object.entries(aliases).forEach(([subpath, module]) => { + if (modules.includes(module)) { + facades.set(subpath, module); + } + }); + + return facades; +}; + +/** Relative specifier from a facade to its target module, e.g. `../../build/lib/helpers`. */ +const specifierFor = (subpath, module) => { + const from = path.dirname(path.join(ROOT, subpath)); + const to = path.join(BUILD_LIB, module); + const relative = toPosix(path.relative(from, to)); + return relative.startsWith('.') ? relative : `./${relative}`; +}; + +/** + * Declaration facades must mirror the export style of their target: `export =` modules + * cannot be re-exported with `export *`, and `export *` never carries a default export. + */ +const declarationFacade = (specifier, declaration) => { + const source = fs.readFileSync(declaration, 'utf8'); + + if (/^export = /m.test(source)) { + return `import target = require('${specifier}');\nexport = target;\n`; + } + + const lines = [`export * from '${specifier}';`]; + if (/^export default /m.test(source)) { + lines.push(`export { default } from '${specifier}';`); + } + return `${lines.join('\n')}\n`; +}; + +const generate = () => { + if (!fs.existsSync(BUILD_LIB)) { + throw new Error(`Nothing to generate from: ${toPosix(path.relative(ROOT, BUILD_LIB))} is missing, run "tsc" first.`); + } + + clean(); + + const written = []; + + buildFacadeMap().forEach((module, subpath) => { + const specifier = specifierFor(subpath, module); + const jsFacade = path.join(ROOT, `${subpath}.js`); + fs.mkdirSync(path.dirname(jsFacade), { recursive: true }); + fs.writeFileSync(jsFacade, `module.exports = require('${specifier}');\n`); + written.push(`${subpath}.js`); + + const declaration = path.join(BUILD_LIB, `${module}.d.ts`); + if (fs.existsSync(declaration)) { + fs.writeFileSync(path.join(ROOT, `${subpath}.d.ts`), declarationFacade(specifier, declaration)); + written.push(`${subpath}.d.ts`); + } + }); + + written.sort(); + fs.writeFileSync(MANIFEST, `${JSON.stringify(written, null, 2)}\n`); + + return written.length; +}; + +if (process.argv.includes('--clean')) { + console.log(`Removed ${clean()} generated resolver facade(s).`); +} else { + console.log(`Generated ${generate()} resolver facade(s).`); +} diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 96036906..857fd828 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -110,6 +110,15 @@ export const saveLaunchUuidToFile = (launchUuid: string): void => { }); }; +export const formatMicrosecondsToISOString = (microseconds: number): string => { + const milliseconds = Math.floor(microseconds / 1000); + const microsecondsRemainder = microseconds % 1000; + + return new Date(milliseconds) + .toISOString() + .replace('Z', `${String(microsecondsRemainder).padStart(3, '0')}Z`); +}; + // Default export preserves the historical CommonJS shape (`module.exports = { ... }`) // so consumers importing `helpers` as a default still work. export default { @@ -121,4 +130,5 @@ export default { getSystemAttributes, generateTestCaseId, saveLaunchUuidToFile, + formatMicrosecondsToISOString, }; From 918a7204c6284a10dedc9625257b439ec6b97faf Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Tue, 18 Aug 2026 13:42:00 +0400 Subject: [PATCH 04/14] Type launch and test item responses --- src/lib/models/responses.ts | 47 +++++++++++ src/lib/report-portal-client.ts | 144 +++++++++++++++++++------------- 2 files changed, 131 insertions(+), 60 deletions(-) diff --git a/src/lib/models/responses.ts b/src/lib/models/responses.ts index 8995a111..4ceaad55 100644 --- a/src/lib/models/responses.ts +++ b/src/lib/models/responses.ts @@ -6,6 +6,7 @@ export interface StartLaunchResponse { export interface FinishLaunchResponse { id?: string; + number?: number; link?: string; [key: string]: unknown; } @@ -20,6 +21,12 @@ export interface FinishTestItemResponse { [key: string]: unknown; } +/** Response of updating a launch. */ +export interface UpdateLaunchResponse { + message?: string; + [key: string]: unknown; +} + export interface LogResponse { id?: string; [key: string]: unknown; @@ -43,3 +50,43 @@ export interface ServerInfoResponse { }; [key: string]: unknown; } + +export interface LaunchResource { + owner?: string; + description?: string; + locked?: boolean; + id: number; + uuid: string; + name: string; + number: number; + startTime: string; + endTime?: string; + lastModified?: string; + status: string; + statistics?: { + executions?: Record; + defects?: Record; + }; + attributes?: Array<{ key?: string; value: string }>; + mode?: 'DEFAULT' | 'DEBUG'; + analysing?: string[]; + approximateDuration?: number; + hasRetries?: boolean; + rerun?: boolean; + metadata?: Record; + retentionPolicy?: 'IMPORTANT' | 'REGULAR'; + [key: string]: unknown; +} + +/** A page of launches. */ +export interface PageLaunchResource { + content: LaunchResource[]; + page: { + number?: number; + size?: number; + totalElements?: number; + totalPages?: number; + hasNext?: boolean; + }; + [key: string]: unknown; +} diff --git a/src/lib/report-portal-client.ts b/src/lib/report-portal-client.ts index 98cd0a9a..2156cc04 100644 --- a/src/lib/report-portal-client.ts +++ b/src/lib/report-portal-client.ts @@ -21,28 +21,32 @@ import type { } from './models/requests'; import type { FinishLaunchResponse, + FinishTestItemResponse, LaunchSearchResponse, MergeLaunchesResponse, + PageLaunchResource, ServerInfoResponse, StartLaunchResponse, StartTestItemResponse, + UpdateLaunchResponse, } from './models/responses'; const MULTIPART_BOUNDARY = Math.floor(Math.random() * 10000000000).toString(); -type PromiseExecutor = ( - resolve: (value?: unknown) => void, - reject: (reason?: unknown) => void, +// Executor for a launch or item promise. +type PromiseExecutor = ( + resolve: (value: T) => void, + reject: (reason?: Error) => void, ) => void; -interface ItemObj { - promiseStart: Promise; +interface ItemObj { + promiseStart: Promise; realId: string; children: string[]; finishSend: boolean; - promiseFinish: Promise; - resolveFinish: (value?: unknown) => void; - rejectFinish: (reason?: unknown) => void; + promiseFinish: Promise; + resolveFinish: (value: T) => void; + rejectFinish: (reason?: Error) => void; } type RequestPromiseFunc = (itemUuid: string, launchUuid: string) => Promise; @@ -149,22 +153,22 @@ class RPClient { return randomUUID(); } - getRejectAnswer(tempId: string, error: Error): ClientResponse { + getRejectAnswer(tempId: string, error: Error): ClientResponse { return { tempId, - promise: Promise.reject(error), + promise: Promise.reject(error), }; } - getNewItemObj(startPromiseFunc: PromiseExecutor): ItemObj { - let resolveFinish!: (value?: unknown) => void; - let rejectFinish!: (reason?: unknown) => void; - const obj: ItemObj = { - promiseStart: new Promise(startPromiseFunc), + getNewItemObj(startPromiseFunc: PromiseExecutor): ItemObj { + let resolveFinish!: (value: T) => void; + let rejectFinish!: (reason?: Error) => void; + const obj: ItemObj = { + promiseStart: new Promise(startPromiseFunc), realId: '', children: [], finishSend: false, - promiseFinish: new Promise((resolve, reject) => { + promiseFinish: new Promise((resolve, reject) => { resolveFinish = resolve; rejectFinish = reject; }), @@ -180,11 +184,11 @@ class RPClient { }); } - checkConnect(): Promise { + checkConnect(): Promise { const url = [this.config.endpoint.replace('/v2', '/v1'), this.config.project, 'launch'] .join('/') .concat('?page.page=1&page.size=1'); - return this.restClient.request('GET', url, {}); + return this.restClient.request('GET', url, {}); } getServerInfoUrl(): string { @@ -215,12 +219,19 @@ class RPClient { /** * Start launch and report it. */ - startLaunch(launchDataRQ: StartLaunchOptions): ClientResponse { + startLaunch( + launchDataRQ: StartLaunchOptions, + ): ClientResponse { const tempId = this.getUniqId(); + // Result of starting or reusing a launch. + let launchObj: ItemObj; if (launchDataRQ.id) { this.logDebug(`Use existing launch with tempId ${tempId}`, launchDataRQ); - this.map[tempId] = this.getNewItemObj((resolve) => resolve(launchDataRQ)); + launchObj = this.getNewItemObj((resolve) => + resolve(launchDataRQ), + ); + this.map[tempId] = launchObj as ItemObj; this.map[tempId].realId = launchDataRQ.id; this.launchUuid = launchDataRQ.id; } else { @@ -243,44 +254,50 @@ class RPClient { attributes, }; - this.map[tempId] = this.getNewItemObj((resolve, reject) => { - const url = 'launch'; - this.logDebug(`Start launch with tempId ${tempId}`, launchData); - this.restClient.create(url, launchData).then( - (response) => { - this.map[tempId].realId = response.id; - this.launchUuid = response.id; - if (this.config.launchUuidPrint) { - this.config.launchUuidPrintOutput(this.launchUuid); - } + launchObj = this.getNewItemObj( + (resolve, reject) => { + const url = 'launch'; + this.logDebug(`Start launch with tempId ${tempId}`, launchData); + this.restClient.create(url, launchData).then( + (response) => { + this.map[tempId].realId = response.id; + this.launchUuid = response.id; + if (this.config.launchUuidPrint) { + this.config.launchUuidPrintOutput(this.launchUuid); + } - if (this.isLaunchMergeRequired) { - helpers.saveLaunchIdToFile(response.id); - } + if (this.isLaunchMergeRequired) { + helpers.saveLaunchIdToFile(response.id); + } - this.logDebug(`Success start launch with tempId ${tempId}`, response); - resolve(response); - }, - (error) => { - this.logDebug(`Error start launch with tempId ${tempId}`, error); - console.dir(error); - reject(error); - }, - ); - }); + this.logDebug(`Success start launch with tempId ${tempId}`, response); + resolve(response); + }, + (error) => { + this.logDebug(`Error start launch with tempId ${tempId}`, error); + console.dir(error); + reject(error); + }, + ); + }, + ); + this.map[tempId] = launchObj as ItemObj; } this.triggerStatisticsEvent().catch(console.error); return { tempId, - promise: this.map[tempId].promiseStart, + promise: launchObj.promiseStart, }; } /** * Finish launch. */ - finishLaunch(launchTempId: string, finishExecutionRQ: FinishLaunchOptions = {}): ClientResponse { - const launchObj = this.map[launchTempId]; + finishLaunch( + launchTempId: string, + finishExecutionRQ: FinishLaunchOptions = {}, + ): ClientResponse { + const launchObj = this.map[launchTempId] as ItemObj | undefined; if (!launchObj) { return this.getRejectAnswer( launchTempId, @@ -413,7 +430,10 @@ class RPClient { /** * Update launch. */ - updateLaunch(launchTempId: string, launchData: UpdateLaunchOptions): ClientResponse { + updateLaunch( + launchTempId: string, + launchData: UpdateLaunchOptions, + ): ClientResponse { const launchObj = this.map[launchTempId]; if (!launchObj) { return this.getRejectAnswer( @@ -421,9 +441,9 @@ class RPClient { new Error(`Launch with tempId "${launchTempId}" not found`), ); } - let resolvePromise!: (value?: unknown) => void; - let rejectPromise!: (reason?: unknown) => void; - const promise = new Promise((resolve, reject) => { + let resolvePromise!: (value: UpdateLaunchResponse) => void; + let rejectPromise!: (reason?: Error) => void; + const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; }); @@ -432,7 +452,7 @@ class RPClient { () => { const url = ['launch', launchObj.realId, 'update'].join('/'); this.logDebug(`Update launch with tempId ${launchTempId}`, launchData); - this.restClient.update(url, launchData).then( + this.restClient.update(url, launchData).then( (response) => { this.logDebug(`Launch with tempId ${launchTempId} were successfully updated`, response); resolvePromise(response); @@ -461,7 +481,7 @@ class RPClient { testItemDataRQ: StartTestItemOptions, launchTempId: string, parentTempId?: string, - ): ClientResponse { + ): ClientResponse { let parentMapId = launchTempId; const launchObj = this.map[launchTempId]; if (!launchObj) { @@ -509,7 +529,7 @@ class RPClient { const executionItemPromise = testItemDataRQ.retry && this.itemRetriesChainMap.get(itemKey); const tempId = this.getUniqId(); - this.map[tempId] = this.getNewItemObj((resolve, reject) => { + const itemObj = this.getNewItemObj((resolve, reject) => { (executionItemPromise || parentPromise).then( (prevResponse) => { const realLaunchId = this.map[launchTempId].realId; @@ -542,21 +562,25 @@ class RPClient { }, ); }); + this.map[tempId] = itemObj as ItemObj; this.map[parentMapId].children.push(tempId); this.itemRetriesChainKeyMapByTempId.set(tempId, itemKey); - this.itemRetriesChainMap.set(itemKey, this.map[tempId].promiseStart); + this.itemRetriesChainMap.set(itemKey, itemObj.promiseStart); return { tempId, - promise: this.map[tempId].promiseStart, + promise: itemObj.promiseStart, }; } /** * Finish Suite or Step level. */ - finishTestItem(itemTempId: string, finishTestItemRQ: FinishTestItemOptions = {}): ClientResponse { - const itemObj = this.map[itemTempId]; + finishTestItem( + itemTempId: string, + finishTestItemRQ: FinishTestItemOptions = {}, + ): ClientResponse { + const itemObj = this.map[itemTempId] as ItemObj | undefined; if (!itemObj) { return this.getRejectAnswer( itemTempId, @@ -773,8 +797,8 @@ class RPClient { return Buffer.concat(buffers); } - finishTestItemPromiseStart( - itemObj: ItemObj, + finishTestItemPromiseStart( + itemObj: ItemObj, itemTempId: string, finishTestItemData: FinishTestItemRQ, ): void { @@ -783,7 +807,7 @@ class RPClient { const url = ['item', itemObj.realId].join('/'); this.logDebug(`Finish test item with tempId ${itemTempId}`, itemObj); this.restClient - .update(url, Object.assign(finishTestItemData, { launchUuid: this.launchUuid })) + .update(url, Object.assign(finishTestItemData, { launchUuid: this.launchUuid })) .then( (response) => { this.logDebug(`Success finish item with tempId ${itemTempId}`, response); From 06d6eaaafb4300275f09cb108f6a234fc6f0098b Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Tue, 25 Aug 2026 11:24:15 +0400 Subject: [PATCH 05/14] Remove formatMicrosecondsToISOString function from helpers.ts --- src/lib/helpers.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 857fd828..96036906 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -110,15 +110,6 @@ export const saveLaunchUuidToFile = (launchUuid: string): void => { }); }; -export const formatMicrosecondsToISOString = (microseconds: number): string => { - const milliseconds = Math.floor(microseconds / 1000); - const microsecondsRemainder = microseconds % 1000; - - return new Date(milliseconds) - .toISOString() - .replace('Z', `${String(microsecondsRemainder).padStart(3, '0')}Z`); -}; - // Default export preserves the historical CommonJS shape (`module.exports = { ... }`) // so consumers importing `helpers` as a default still work. export default { @@ -130,5 +121,4 @@ export default { getSystemAttributes, generateTestCaseId, saveLaunchUuidToFile, - formatMicrosecondsToISOString, }; From 1748deb39240c4adcb763b400683109a2a55b891 Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Tue, 25 Aug 2026 11:46:16 +0400 Subject: [PATCH 06/14] coderrabitai crf --- DEV_GUIDE.md | 14 ++++++++------ scripts/generate-resolver-facades.js | 5 ++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md index f454bf13..4987d492 100644 --- a/DEV_GUIDE.md +++ b/DEV_GUIDE.md @@ -10,12 +10,14 @@ which writes a one-line re-export at every public subpath location — `helpers. `constants.js`, `models.js`, `publicReportingAPI.js` and a `lib/**` mirror of the compiled tree — together with matching `.d.ts` files. -Those files are never loaded at runtime: Node, TypeScript and bundlers all read the -`exports` / `typesVersions` maps in `package.json` and go straight to `build/lib`. They -exist for tools that resolve imports by walking the filesystem, chiefly -`eslint-import-resolver-node` (the default resolver of `eslint-plugin-import`), which -otherwise reports `import/no-unresolved` for every subpath import and forces each consumer -to configure an ignore. They also keep the `lib/**` paths published up to 5.5.x resolvable. +Supported package imports resolve through the `exports` / `typesVersions` maps in +`package.json` straight to `build/lib`; a direct deep import (e.g. +`require('@reportportal/client-javascript/lib/helpers')`) can still load a generated facade +file instead. The facades mainly exist for tools that resolve imports by walking the +filesystem, chiefly `eslint-import-resolver-node` (the default resolver of +`eslint-plugin-import`), which otherwise reports `import/no-unresolved` for every subpath +import and forces each consumer to configure an ignore. They also keep the `lib/**` paths +published up to 5.5.x resolvable. Everything the script writes is gitignored, recorded in `.generated-facades.json` and removed by `npm run clean`. When a new subpath is added to `exports`, add the matching alias diff --git a/scripts/generate-resolver-facades.js b/scripts/generate-resolver-facades.js index 338230c2..1bd0a109 100644 --- a/scripts/generate-resolver-facades.js +++ b/scripts/generate-resolver-facades.js @@ -33,7 +33,10 @@ const readManifest = () => { const entries = JSON.parse(fs.readFileSync(MANIFEST, 'utf8')); return Array.isArray(entries) ? entries : []; } catch (e) { - return []; + if (e.code === 'ENOENT') { + return []; + } + throw e; } }; From 43a152d21aa525de59fac2cce0ae5b712a6f5a02 Mon Sep 17 00:00:00 2001 From: Ilya_Hancharyk Date: Sun, 30 Aug 2026 15:22:02 +0200 Subject: [PATCH 07/14] EPMRPP-89496 || Simplify facades generation for eslint resolver compatibility --- .eslintignore | 1 - .gitignore | 2 - CHANGELOG.md | 13 +-- DEV_GUIDE.md | 36 ++++--- package.json | 1 - scripts/generate-resolver-facades.js | 143 ++++++--------------------- 6 files changed, 59 insertions(+), 137 deletions(-) diff --git a/.eslintignore b/.eslintignore index 1245de7b..b6ed6049 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,7 +2,6 @@ node_modules build __tests__/**/*.json # Subpath facades generated by scripts/generate-resolver-facades.js -/lib /helpers.js /helpers.d.ts /constants.js diff --git a/.gitignore b/.gitignore index 7d22247e..e3d7eda9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,6 @@ coverage/ build # Subpath facades generated by scripts/generate-resolver-facades.js -/.generated-facades.json -/lib /helpers.js /helpers.d.ts /constants.js diff --git a/CHANGELOG.md b/CHANGELOG.md index fad49a35..5fadfaef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,13 @@ definitions (no separate `@types` package required) and exposes constants, models and helpers via subpath imports (e.g. `@reportportal/client-javascript/constants`). -- Every public subpath is now backed by a real file in the published package, generated at - build time next to the `exports` map. Tools that resolve imports by walking the filesystem - instead of reading `exports` — most notably `eslint-import-resolver-node`, the default - resolver of `eslint-plugin-import` — resolve subpath imports out of the box, so consumers - no longer need an `import/no-unresolved` ignore or an extra resolver dependency. The - `lib/**` paths published up to 5.5.x resolve again as well. +- Deep `lib/**` imports published up to 5.5.x (e.g. + `require('@reportportal/client-javascript/lib/helpers')`) still resolve for Node, + TypeScript and bundlers via the `exports` map, but are no longer backed by physical files. + Tools that resolve imports by walking the filesystem instead of reading `exports` — most + notably `eslint-import-resolver-node`, the default resolver of `eslint-plugin-import` — + will report `import/no-unresolved` for these paths. Switch to the new subpath aliases + (`constants`, `models`, `helpers`, `publicReportingAPI`) instead. ### Added - `retry_of` property is now automatically included in the `startTestItem` request payload when `retry: true` and a previous attempt exists in the diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md index 4987d492..aa70f55f 100644 --- a/DEV_GUIDE.md +++ b/DEV_GUIDE.md @@ -6,22 +6,30 @@ README.md, which is published to package registries. ## Subpath facades `npm run build` compiles to `build/` and then runs `scripts/generate-resolver-facades.js`, -which writes a one-line re-export at every public subpath location — `helpers.js`, -`constants.js`, `models.js`, `publicReportingAPI.js` and a `lib/**` mirror of the compiled -tree — together with matching `.d.ts` files. +which writes a one-line re-export at each public subpath alias — `helpers.js`, +`constants.js`, `models.js`, `publicReportingAPI.js` — together with matching `.d.ts` files. Supported package imports resolve through the `exports` / `typesVersions` maps in -`package.json` straight to `build/lib`; a direct deep import (e.g. -`require('@reportportal/client-javascript/lib/helpers')`) can still load a generated facade -file instead. The facades mainly exist for tools that resolve imports by walking the -filesystem, chiefly `eslint-import-resolver-node` (the default resolver of -`eslint-plugin-import`), which otherwise reports `import/no-unresolved` for every subpath -import and forces each consumer to configure an ignore. They also keep the `lib/**` paths -published up to 5.5.x resolvable. - -Everything the script writes is gitignored, recorded in `.generated-facades.json` and -removed by `npm run clean`. When a new subpath is added to `exports`, add the matching alias -to the `aliases` map in the script — the `lib/**` mirror updates itself. +`package.json` straight to `build/lib` for Node, TypeScript and bundlers; these files are +never on that path. They exist only for tools that resolve imports by walking the filesystem +instead of reading `exports`, chiefly `eslint-import-resolver-node` (the default resolver of +`eslint-plugin-import`), which otherwise reports `import/no-unresolved` for these subpath +imports and forces each consumer to configure an ignore. + +The alias list in the script is intentionally fixed and small — it does not mirror every +internal module under `build/lib`. Deep `lib/**` imports as published up to 5.5.x (e.g. +`require('@reportportal/client-javascript/lib/rest')`) resolve via the `exports` map for +Node, TypeScript and bundlers, but are **not** backed by a physical `lib/**` tree — a +filesystem-based resolver hitting one of those undocumented deep paths still needs a local +ignore. If you're bumping a first-party agent past this version and it imports +`@reportportal/client-javascript/lib/**` directly, switch it to the matching short alias in +the same PR rather than adding it here. + +Everything the script writes is gitignored and removed by `npm run clean`. It recomputes +each facade's export style (`export *` vs `export =` vs default) from the real compiled +module on every build, so it can't silently drift the way a hand-written file could. When +you add a new top-level subpath to `exports`, add the matching entry to the `ALIASES` map in +the script. ## Code knowledge graph diff --git a/package.json b/package.json index 595bbc60..b29938c9 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "files": [ "/build", "/VERSION", - "/lib", "/helpers.js", "/helpers.d.ts", "/constants.js", diff --git a/scripts/generate-resolver-facades.js b/scripts/generate-resolver-facades.js index 1bd0a109..d456291f 100644 --- a/scripts/generate-resolver-facades.js +++ b/scripts/generate-resolver-facades.js @@ -1,22 +1,21 @@ -/* eslint-disable no-console */ /** - * Generates plain filesystem facades for every public subpath of the package. + * Generates filesystem facades for the package's public subpath aliases. * - * The compiled output lives under `build/lib`, and subpaths such as - * `@reportportal/client-javascript/helpers` are mapped there through - * `package.json#exports` / `#typesVersions`. Node, TypeScript and bundlers all read those - * maps, but tools that resolve imports by walking the filesystem do not — most notably + * `helpers`, `constants`, `models` and `publicReportingAPI` are mapped to `build/lib` + * through `package.json#exports` / `#typesVersions`. Node, TypeScript and bundlers all read + * those maps, but tools that resolve imports by walking the filesystem do not — most notably * `eslint-import-resolver-node`, the default resolver of `eslint-plugin-import`, which - * reports `import/no-unresolved` for every subpath import. + * reports `import/no-unresolved` for these subpath imports. * - * To keep those tools working without any consumer-side configuration, this script emits a - * thin file at each subpath location that simply re-exports the real module. The files are - * never loaded at runtime (the `exports` map always wins), they only give filesystem-based - * resolvers something to find. They also restore the `lib/**` layout published up to 5.5.x, - * so imports written against the pre-TypeScript releases keep resolving. + * To keep those tools working without any consumer-side configuration, this script writes a + * thin file at each alias location that simply re-exports the real module. The files are + * never loaded at runtime (the `exports` map always wins); they only give filesystem-based + * resolvers something to find. * - * Everything written here is generated at build time, listed in `MANIFEST` and removed by - * `--clean`; nothing is meant to be edited or committed by hand. + * The alias list is intentionally fixed and small — this does not attempt to mirror every + * internal module under `build/lib` (see DEV_GUIDE.md#subpath-facades for why). Everything + * written here is gitignored and regenerated on every build; nothing is meant to be edited + * or committed by hand. */ const fs = require('fs'); @@ -24,93 +23,23 @@ const path = require('path'); const ROOT = path.join(__dirname, '..'); const BUILD_LIB = path.join(ROOT, 'build', 'lib'); -const MANIFEST = path.join(ROOT, '.generated-facades.json'); const toPosix = (p) => p.split(path.sep).join('/'); -const readManifest = () => { - try { - const entries = JSON.parse(fs.readFileSync(MANIFEST, 'utf8')); - return Array.isArray(entries) ? entries : []; - } catch (e) { - if (e.code === 'ENOENT') { - return []; - } - throw e; - } +/** Public subpath alias -> module it re-exports, relative to `build/lib`. */ +const ALIASES = { + helpers: 'helpers', + constants: 'constants/index', + models: 'models/index', + publicReportingAPI: 'publicReportingAPI', }; -/** Removes the files listed in the manifest, plus any directories left empty behind them. */ -const clean = () => { - const entries = readManifest(); - const dirs = new Set(); - - entries.forEach((entry) => { - const target = path.join(ROOT, entry); - if (fs.existsSync(target)) { - fs.rmSync(target); - } - dirs.add(path.dirname(target)); - }); - - // Deepest first, so nested directories empty out before their parents are checked. - [...dirs] - .sort((a, b) => b.length - a.length) - .forEach((dir) => { - if (dir !== ROOT && fs.existsSync(dir) && fs.readdirSync(dir).length === 0) { - fs.rmdirSync(dir); - } - }); - - if (fs.existsSync(MANIFEST)) { - fs.rmSync(MANIFEST); - } - - return entries.length; -}; - -/** Every `.js` file under `build/lib`, as paths relative to `build/lib`. */ -const collectModules = (dir = BUILD_LIB) => - fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { - const absolute = path.join(dir, entry.name); - if (entry.isDirectory()) { - return collectModules(absolute); - } - return entry.isFile() && entry.name.endsWith('.js') - ? [toPosix(path.relative(BUILD_LIB, absolute)).replace(/\.js$/, '')] - : []; - }); - -/** - * Maps a subpath the package exposes to the `build/lib` module backing it. - * `lib/**` mirrors the compiled tree; the short aliases match the `exports` map. - */ -const buildFacadeMap = () => { - const modules = collectModules(); - const facades = new Map(modules.map((module) => [`lib/${module}`, module])); - - const aliases = { - helpers: 'helpers', - publicReportingAPI: 'publicReportingAPI', - constants: 'constants/index', - models: 'models/index', - }; - - Object.entries(aliases).forEach(([subpath, module]) => { - if (modules.includes(module)) { - facades.set(subpath, module); - } - }); +const facadeFiles = () => Object.keys(ALIASES).flatMap((name) => [`${name}.js`, `${name}.d.ts`]); - return facades; -}; - -/** Relative specifier from a facade to its target module, e.g. `../../build/lib/helpers`. */ -const specifierFor = (subpath, module) => { - const from = path.dirname(path.join(ROOT, subpath)); - const to = path.join(BUILD_LIB, module); - const relative = toPosix(path.relative(from, to)); - return relative.startsWith('.') ? relative : `./${relative}`; +const clean = () => { + const files = facadeFiles().filter((file) => fs.existsSync(path.join(ROOT, file))); + files.forEach((file) => fs.rmSync(path.join(ROOT, file))); + return files.length; }; /** @@ -136,32 +65,20 @@ const generate = () => { throw new Error(`Nothing to generate from: ${toPosix(path.relative(ROOT, BUILD_LIB))} is missing, run "tsc" first.`); } - clean(); + Object.entries(ALIASES).forEach(([name, module]) => { + const specifier = `./build/lib/${module}`; - const written = []; - - buildFacadeMap().forEach((module, subpath) => { - const specifier = specifierFor(subpath, module); - const jsFacade = path.join(ROOT, `${subpath}.js`); - fs.mkdirSync(path.dirname(jsFacade), { recursive: true }); - fs.writeFileSync(jsFacade, `module.exports = require('${specifier}');\n`); - written.push(`${subpath}.js`); + fs.writeFileSync(path.join(ROOT, `${name}.js`), `module.exports = require('${specifier}');\n`); const declaration = path.join(BUILD_LIB, `${module}.d.ts`); - if (fs.existsSync(declaration)) { - fs.writeFileSync(path.join(ROOT, `${subpath}.d.ts`), declarationFacade(specifier, declaration)); - written.push(`${subpath}.d.ts`); - } + fs.writeFileSync(path.join(ROOT, `${name}.d.ts`), declarationFacade(specifier, declaration)); }); - written.sort(); - fs.writeFileSync(MANIFEST, `${JSON.stringify(written, null, 2)}\n`); - - return written.length; + return Object.keys(ALIASES).length; }; if (process.argv.includes('--clean')) { - console.log(`Removed ${clean()} generated resolver facade(s).`); + console.log(`Removed ${clean()} generated resolver facade file(s).`); } else { console.log(`Generated ${generate()} resolver facade(s).`); } From d4eb3813f5934b79831a51f5ec997cbb8718693b Mon Sep 17 00:00:00 2001 From: Ilya_Hancharyk Date: Sun, 30 Aug 2026 15:36:15 +0200 Subject: [PATCH 08/14] EPMRPP-89496 || Chore: remove nested 'lib' dir --- CHANGELOG.md | 4 +- DEV_GUIDE.md | 6 +- __tests__/config.spec.js | 4 +- __tests__/helpers.spec.js | 2 +- __tests__/oauth.spec.js | 2 +- __tests__/proxyHelper.spec.js | 2 +- __tests__/publicReportingAPI.spec.js | 4 +- __tests__/report-portal-client.spec.js | 6 +- __tests__/rest.spec.js | 6 +- jest.config.js | 18 +++--- package.json | 71 ++++++++++++------------ scripts/generate-resolver-facades.js | 16 +++--- src/{lib => }/commons/config.ts | 0 src/{lib => }/commons/errors.ts | 0 src/{lib => }/constants/events.ts | 0 src/{lib => }/constants/index.ts | 0 src/{lib => }/constants/launchModes.ts | 0 src/{lib => }/constants/logLevels.ts | 0 src/{lib => }/constants/outputs.ts | 0 src/{lib => }/constants/statuses.ts | 0 src/{lib => }/constants/testItemTypes.ts | 0 src/{lib => }/helpers.ts | 0 src/{lib => }/logger.ts | 0 src/{lib => }/models/common.ts | 0 src/{lib => }/models/config.ts | 0 src/{lib => }/models/index.ts | 0 src/{lib => }/models/reporting.ts | 0 src/{lib => }/models/requests.ts | 0 src/{lib => }/models/responses.ts | 0 src/{lib => }/oauth.ts | 0 src/{lib => }/pjson.ts | 2 +- src/{lib => }/proxyHelper.ts | 0 src/{lib => }/publicReportingAPI.ts | 0 src/{lib => }/report-portal-client.ts | 4 +- src/{lib => }/rest.ts | 0 src/statistics/constants.ts | 2 +- src/statistics/statistics.ts | 2 +- 37 files changed, 75 insertions(+), 76 deletions(-) rename src/{lib => }/commons/config.ts (100%) rename src/{lib => }/commons/errors.ts (100%) rename src/{lib => }/constants/events.ts (100%) rename src/{lib => }/constants/index.ts (100%) rename src/{lib => }/constants/launchModes.ts (100%) rename src/{lib => }/constants/logLevels.ts (100%) rename src/{lib => }/constants/outputs.ts (100%) rename src/{lib => }/constants/statuses.ts (100%) rename src/{lib => }/constants/testItemTypes.ts (100%) rename src/{lib => }/helpers.ts (100%) rename src/{lib => }/logger.ts (100%) rename src/{lib => }/models/common.ts (100%) rename src/{lib => }/models/config.ts (100%) rename src/{lib => }/models/index.ts (100%) rename src/{lib => }/models/reporting.ts (100%) rename src/{lib => }/models/requests.ts (100%) rename src/{lib => }/models/responses.ts (100%) rename src/{lib => }/oauth.ts (100%) rename src/{lib => }/pjson.ts (88%) rename src/{lib => }/proxyHelper.ts (100%) rename src/{lib => }/publicReportingAPI.ts (100%) rename src/{lib => }/report-portal-client.ts (99%) rename src/{lib => }/rest.ts (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fadfaef..e590b608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,12 @@ definitions (no separate `@types` package required) and exposes constants, models and helpers via subpath imports (e.g. `@reportportal/client-javascript/constants`). -- Deep `lib/**` imports published up to 5.5.x (e.g. +- **Breaking Change** Deep `lib/**` imports published up to 5.5.x (e.g. `require('@reportportal/client-javascript/lib/helpers')`) still resolve for Node, TypeScript and bundlers via the `exports` map, but are no longer backed by physical files. Tools that resolve imports by walking the filesystem instead of reading `exports` — most notably `eslint-import-resolver-node`, the default resolver of `eslint-plugin-import` — - will report `import/no-unresolved` for these paths. Switch to the new subpath aliases + will report [`import/no-unresolved`](https://github.com/import-js/eslint-plugin-import/issues/1810) for these paths. Switch to the new subpath aliases (`constants`, `models`, `helpers`, `publicReportingAPI`) instead. ### Added - `retry_of` property is now automatically included in the `startTestItem` diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md index aa70f55f..3190f914 100644 --- a/DEV_GUIDE.md +++ b/DEV_GUIDE.md @@ -10,14 +10,14 @@ which writes a one-line re-export at each public subpath alias — `helpers.js`, `constants.js`, `models.js`, `publicReportingAPI.js` — together with matching `.d.ts` files. Supported package imports resolve through the `exports` / `typesVersions` maps in -`package.json` straight to `build/lib` for Node, TypeScript and bundlers; these files are -never on that path. They exist only for tools that resolve imports by walking the filesystem +`package.json` straight to `build` for Node, TypeScript and bundlers; these files are never +on that path. They exist only for tools that resolve imports by walking the filesystem instead of reading `exports`, chiefly `eslint-import-resolver-node` (the default resolver of `eslint-plugin-import`), which otherwise reports `import/no-unresolved` for these subpath imports and forces each consumer to configure an ignore. The alias list in the script is intentionally fixed and small — it does not mirror every -internal module under `build/lib`. Deep `lib/**` imports as published up to 5.5.x (e.g. +internal module under `build`. Deep `lib/**` imports as published up to 5.5.x (e.g. `require('@reportportal/client-javascript/lib/rest')`) resolve via the `exports` map for Node, TypeScript and bundlers, but are **not** backed by a physical `lib/**` tree — a filesystem-based resolver hitting one of those undocumented deep paths still needs a local diff --git a/__tests__/config.spec.js b/__tests__/config.spec.js index 7cf6c937..daaf3b7d 100644 --- a/__tests__/config.spec.js +++ b/__tests__/config.spec.js @@ -1,8 +1,8 @@ -const { getClientConfig, getRequiredOption, getApiKey } = require('../src/lib/commons/config'); +const { getClientConfig, getRequiredOption, getApiKey } = require('../src/commons/config'); const { ReportPortalRequiredOptionError, ReportPortalValidationError, -} = require('../src/lib/commons/errors'); +} = require('../src/commons/errors'); describe('Config commons test suite', () => { describe('getRequiredOption', () => { diff --git a/__tests__/helpers.spec.js b/__tests__/helpers.spec.js index 3c905d21..cb9fc1d7 100644 --- a/__tests__/helpers.spec.js +++ b/__tests__/helpers.spec.js @@ -1,7 +1,7 @@ const os = require('os'); const fs = require('fs'); const glob = require('glob'); -const helpers = require('../src/lib/helpers'); +const helpers = require('../src/helpers'); const pjson = require('../package.json'); describe('Helpers', () => { diff --git a/__tests__/oauth.spec.js b/__tests__/oauth.spec.js index bd27ff2a..a8128a5a 100644 --- a/__tests__/oauth.spec.js +++ b/__tests__/oauth.spec.js @@ -1,6 +1,6 @@ const axios = require('axios'); const { HttpsProxyAgent } = require('https-proxy-agent'); -const OAuthInterceptor = require('../src/lib/oauth'); +const OAuthInterceptor = require('../src/oauth'); jest.mock('axios', () => ({ post: jest.fn(), diff --git a/__tests__/proxyHelper.spec.js b/__tests__/proxyHelper.spec.js index c588d1fe..4cab9023 100644 --- a/__tests__/proxyHelper.spec.js +++ b/__tests__/proxyHelper.spec.js @@ -5,7 +5,7 @@ const { getProxyConfig, createProxyAgents, getProxyAgentForUrl, -} = require('../src/lib/proxyHelper'); +} = require('../src/proxyHelper'); describe('proxyHelper', () => { const originalEnv = process.env; diff --git a/__tests__/publicReportingAPI.spec.js b/__tests__/publicReportingAPI.spec.js index 4f52ee6a..71c770f7 100644 --- a/__tests__/publicReportingAPI.spec.js +++ b/__tests__/publicReportingAPI.spec.js @@ -1,5 +1,5 @@ -const PublicReportingAPI = require('../src/lib/publicReportingAPI'); -const { EVENTS } = require('../src/lib/constants/events'); +const PublicReportingAPI = require('../src/publicReportingAPI'); +const { EVENTS } = require('../src/constants/events'); describe('PublicReportingAPI', () => { it('setDescription should trigger process.emit with correct parameters', () => { diff --git a/__tests__/report-portal-client.spec.js b/__tests__/report-portal-client.spec.js index 67ed9f12..847bacf4 100644 --- a/__tests__/report-portal-client.spec.js +++ b/__tests__/report-portal-client.spec.js @@ -1,7 +1,7 @@ const process = require('process'); -const RPClient = require('../src/lib/report-portal-client'); -const helpers = require('../src/lib/helpers'); -const { OUTPUT_TYPES } = require('../src/lib/constants/outputs'); +const RPClient = require('../src/report-portal-client'); +const helpers = require('../src/helpers'); +const { OUTPUT_TYPES } = require('../src/constants/outputs'); describe('ReportPortal javascript client', () => { afterEach(() => { diff --git a/__tests__/rest.spec.js b/__tests__/rest.spec.js index ec8e2222..352d70c9 100644 --- a/__tests__/rest.spec.js +++ b/__tests__/rest.spec.js @@ -1,9 +1,9 @@ const nock = require('nock'); const isEqual = require('lodash/isEqual'); const http = require('http'); -const RestClient = require('../src/lib/rest'); -const OAuthInterceptor = require('../src/lib/oauth'); -const logger = require('../src/lib/logger'); +const RestClient = require('../src/rest'); +const OAuthInterceptor = require('../src/oauth'); +const logger = require('../src/logger'); describe('RestClient', () => { const originalEnv = process.env; diff --git a/jest.config.js b/jest.config.js index 3273f319..1fa7326a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,14 +7,16 @@ module.exports = { testRegex: '/__tests__/.*\\.(test|spec).js$', testEnvironment: 'node', collectCoverageFrom: [ - 'src/lib/**/*.ts', - '!src/lib/logger.ts', - '!src/lib/pjson.ts', - '!src/lib/models/**', - '!src/lib/constants/index.ts', - '!src/lib/constants/launchModes.ts', - '!src/lib/constants/logLevels.ts', - '!src/lib/constants/testItemTypes.ts', + 'src/**/*.ts', + '!src/statistics/**', + '!src/types/**', + '!src/logger.ts', + '!src/pjson.ts', + '!src/models/**', + '!src/constants/index.ts', + '!src/constants/launchModes.ts', + '!src/constants/logLevels.ts', + '!src/constants/testItemTypes.ts', ], coverageThreshold: { global: { diff --git a/package.json b/package.json index b29938c9..12d2e281 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,6 @@ "test:coverage": "jest --coverage", "prepublishOnly": "npm run build" }, - "directories": { - "lib": "./build/lib" - }, "files": [ "/build", "/VERSION", @@ -29,63 +26,63 @@ "/publicReportingAPI.js", "/publicReportingAPI.d.ts" ], - "main": "./build/lib/report-portal-client", - "types": "./build/lib/report-portal-client.d.ts", + "main": "./build/report-portal-client", + "types": "./build/report-portal-client.d.ts", "exports": { ".": { - "types": "./build/lib/report-portal-client.d.ts", - "import": "./build/lib/report-portal-client.js", - "require": "./build/lib/report-portal-client.js" + "types": "./build/report-portal-client.d.ts", + "import": "./build/report-portal-client.js", + "require": "./build/report-portal-client.js" }, "./constants": { - "types": "./build/lib/constants/index.d.ts", - "import": "./build/lib/constants/index.js", - "require": "./build/lib/constants/index.js" + "types": "./build/constants/index.d.ts", + "import": "./build/constants/index.js", + "require": "./build/constants/index.js" }, "./models": { - "types": "./build/lib/models/index.d.ts", - "import": "./build/lib/models/index.js", - "require": "./build/lib/models/index.js" + "types": "./build/models/index.d.ts", + "import": "./build/models/index.js", + "require": "./build/models/index.js" }, "./helpers": { - "types": "./build/lib/helpers.d.ts", - "import": "./build/lib/helpers.js", - "require": "./build/lib/helpers.js" + "types": "./build/helpers.d.ts", + "import": "./build/helpers.js", + "require": "./build/helpers.js" }, "./publicReportingAPI": { - "types": "./build/lib/publicReportingAPI.d.ts", - "import": "./build/lib/publicReportingAPI.js", - "require": "./build/lib/publicReportingAPI.js" + "types": "./build/publicReportingAPI.d.ts", + "import": "./build/publicReportingAPI.js", + "require": "./build/publicReportingAPI.js" }, "./package.json": "./package.json", "./lib/constants": { - "types": "./build/lib/constants/index.d.ts", - "import": "./build/lib/constants/index.js", - "require": "./build/lib/constants/index.js" + "types": "./build/constants/index.d.ts", + "import": "./build/constants/index.js", + "require": "./build/constants/index.js" }, "./lib/models": { - "types": "./build/lib/models/index.d.ts", - "import": "./build/lib/models/index.js", - "require": "./build/lib/models/index.js" + "types": "./build/models/index.d.ts", + "import": "./build/models/index.js", + "require": "./build/models/index.js" }, "./lib/*": { - "types": "./build/lib/*.d.ts", - "import": "./build/lib/*.js", - "require": "./build/lib/*.js" + "types": "./build/*.d.ts", + "import": "./build/*.js", + "require": "./build/*.js" }, "./lib/*.js": { - "types": "./build/lib/*.d.ts", - "import": "./build/lib/*.js", - "require": "./build/lib/*.js" + "types": "./build/*.d.ts", + "import": "./build/*.js", + "require": "./build/*.js" } }, "typesVersions": { "*": { - "constants": ["./build/lib/constants/index.d.ts"], - "models": ["./build/lib/models/index.d.ts"], - "helpers": ["./build/lib/helpers.d.ts"], - "publicReportingAPI": ["./build/lib/publicReportingAPI.d.ts"], - "lib/*": ["./build/lib/*"] + "constants": ["./build/constants/index.d.ts"], + "models": ["./build/models/index.d.ts"], + "helpers": ["./build/helpers.d.ts"], + "publicReportingAPI": ["./build/publicReportingAPI.d.ts"], + "lib/*": ["./build/*"] } }, "engines": { diff --git a/scripts/generate-resolver-facades.js b/scripts/generate-resolver-facades.js index d456291f..ad5e467e 100644 --- a/scripts/generate-resolver-facades.js +++ b/scripts/generate-resolver-facades.js @@ -1,7 +1,7 @@ /** * Generates filesystem facades for the package's public subpath aliases. * - * `helpers`, `constants`, `models` and `publicReportingAPI` are mapped to `build/lib` + * `helpers`, `constants`, `models` and `publicReportingAPI` are mapped to `build` * through `package.json#exports` / `#typesVersions`. Node, TypeScript and bundlers all read * those maps, but tools that resolve imports by walking the filesystem do not — most notably * `eslint-import-resolver-node`, the default resolver of `eslint-plugin-import`, which @@ -13,7 +13,7 @@ * resolvers something to find. * * The alias list is intentionally fixed and small — this does not attempt to mirror every - * internal module under `build/lib` (see DEV_GUIDE.md#subpath-facades for why). Everything + * internal module under `build` (see DEV_GUIDE.md#subpath-facades for why). Everything * written here is gitignored and regenerated on every build; nothing is meant to be edited * or committed by hand. */ @@ -22,11 +22,11 @@ const fs = require('fs'); const path = require('path'); const ROOT = path.join(__dirname, '..'); -const BUILD_LIB = path.join(ROOT, 'build', 'lib'); +const BUILD_DIR = path.join(ROOT, 'build'); const toPosix = (p) => p.split(path.sep).join('/'); -/** Public subpath alias -> module it re-exports, relative to `build/lib`. */ +/** Public subpath alias -> module it re-exports, relative to `build`. */ const ALIASES = { helpers: 'helpers', constants: 'constants/index', @@ -61,16 +61,16 @@ const declarationFacade = (specifier, declaration) => { }; const generate = () => { - if (!fs.existsSync(BUILD_LIB)) { - throw new Error(`Nothing to generate from: ${toPosix(path.relative(ROOT, BUILD_LIB))} is missing, run "tsc" first.`); + if (!fs.existsSync(BUILD_DIR)) { + throw new Error(`Nothing to generate from: ${toPosix(path.relative(ROOT, BUILD_DIR))} is missing, run "tsc" first.`); } Object.entries(ALIASES).forEach(([name, module]) => { - const specifier = `./build/lib/${module}`; + const specifier = `./build/${module}`; fs.writeFileSync(path.join(ROOT, `${name}.js`), `module.exports = require('${specifier}');\n`); - const declaration = path.join(BUILD_LIB, `${module}.d.ts`); + const declaration = path.join(BUILD_DIR, `${module}.d.ts`); fs.writeFileSync(path.join(ROOT, `${name}.d.ts`), declarationFacade(specifier, declaration)); }); diff --git a/src/lib/commons/config.ts b/src/commons/config.ts similarity index 100% rename from src/lib/commons/config.ts rename to src/commons/config.ts diff --git a/src/lib/commons/errors.ts b/src/commons/errors.ts similarity index 100% rename from src/lib/commons/errors.ts rename to src/commons/errors.ts diff --git a/src/lib/constants/events.ts b/src/constants/events.ts similarity index 100% rename from src/lib/constants/events.ts rename to src/constants/events.ts diff --git a/src/lib/constants/index.ts b/src/constants/index.ts similarity index 100% rename from src/lib/constants/index.ts rename to src/constants/index.ts diff --git a/src/lib/constants/launchModes.ts b/src/constants/launchModes.ts similarity index 100% rename from src/lib/constants/launchModes.ts rename to src/constants/launchModes.ts diff --git a/src/lib/constants/logLevels.ts b/src/constants/logLevels.ts similarity index 100% rename from src/lib/constants/logLevels.ts rename to src/constants/logLevels.ts diff --git a/src/lib/constants/outputs.ts b/src/constants/outputs.ts similarity index 100% rename from src/lib/constants/outputs.ts rename to src/constants/outputs.ts diff --git a/src/lib/constants/statuses.ts b/src/constants/statuses.ts similarity index 100% rename from src/lib/constants/statuses.ts rename to src/constants/statuses.ts diff --git a/src/lib/constants/testItemTypes.ts b/src/constants/testItemTypes.ts similarity index 100% rename from src/lib/constants/testItemTypes.ts rename to src/constants/testItemTypes.ts diff --git a/src/lib/helpers.ts b/src/helpers.ts similarity index 100% rename from src/lib/helpers.ts rename to src/helpers.ts diff --git a/src/lib/logger.ts b/src/logger.ts similarity index 100% rename from src/lib/logger.ts rename to src/logger.ts diff --git a/src/lib/models/common.ts b/src/models/common.ts similarity index 100% rename from src/lib/models/common.ts rename to src/models/common.ts diff --git a/src/lib/models/config.ts b/src/models/config.ts similarity index 100% rename from src/lib/models/config.ts rename to src/models/config.ts diff --git a/src/lib/models/index.ts b/src/models/index.ts similarity index 100% rename from src/lib/models/index.ts rename to src/models/index.ts diff --git a/src/lib/models/reporting.ts b/src/models/reporting.ts similarity index 100% rename from src/lib/models/reporting.ts rename to src/models/reporting.ts diff --git a/src/lib/models/requests.ts b/src/models/requests.ts similarity index 100% rename from src/lib/models/requests.ts rename to src/models/requests.ts diff --git a/src/lib/models/responses.ts b/src/models/responses.ts similarity index 100% rename from src/lib/models/responses.ts rename to src/models/responses.ts diff --git a/src/lib/oauth.ts b/src/oauth.ts similarity index 100% rename from src/lib/oauth.ts rename to src/oauth.ts diff --git a/src/lib/pjson.ts b/src/pjson.ts similarity index 88% rename from src/lib/pjson.ts rename to src/pjson.ts index 00c08a18..6842d865 100644 --- a/src/lib/pjson.ts +++ b/src/pjson.ts @@ -7,7 +7,7 @@ interface PackageJson { } // Resolve the package's own package.json by walking up from this module's directory. -// Works both from compiled output (`lib/`) and from source when run via ts-jest (`src/lib/`). +// Works both from compiled output (`build/`) and from source when run via ts-jest (`src/`). function findPackageJson(dir: string): PackageJson { let current = dir; for (;;) { diff --git a/src/lib/proxyHelper.ts b/src/proxyHelper.ts similarity index 100% rename from src/lib/proxyHelper.ts rename to src/proxyHelper.ts diff --git a/src/lib/publicReportingAPI.ts b/src/publicReportingAPI.ts similarity index 100% rename from src/lib/publicReportingAPI.ts rename to src/publicReportingAPI.ts diff --git a/src/lib/report-portal-client.ts b/src/report-portal-client.ts similarity index 99% rename from src/lib/report-portal-client.ts rename to src/report-portal-client.ts index 2156cc04..650a3438 100644 --- a/src/lib/report-portal-client.ts +++ b/src/report-portal-client.ts @@ -3,8 +3,8 @@ import { URLSearchParams } from 'url'; import * as helpers from './helpers'; import RestClient from './rest'; import { getClientConfig } from './commons/config'; -import Statistics from '../statistics/statistics'; -import { EVENT_NAME } from '../statistics/constants'; +import Statistics from './statistics/statistics'; +import { EVENT_NAME } from './statistics/constants'; import { STATUSES } from './constants/statuses'; import type { AgentParams, Attachment, ClientResponse } from './models/common'; import type { NormalizedClientConfig, ReportPortalConfig } from './models/config'; diff --git a/src/lib/rest.ts b/src/rest.ts similarity index 100% rename from src/lib/rest.ts rename to src/rest.ts diff --git a/src/statistics/constants.ts b/src/statistics/constants.ts index 2c4784f3..fa3c780a 100644 --- a/src/statistics/constants.ts +++ b/src/statistics/constants.ts @@ -1,6 +1,6 @@ import os from 'os'; import path from 'path'; -import { PJSON_NAME, PJSON_VERSION } from '../lib/pjson'; +import { PJSON_NAME, PJSON_VERSION } from '../pjson'; export const ENCODING = 'utf-8'; export { PJSON_NAME, PJSON_VERSION }; diff --git a/src/statistics/statistics.ts b/src/statistics/statistics.ts index 61eb11ab..de1f07c5 100644 --- a/src/statistics/statistics.ts +++ b/src/statistics/statistics.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { MEASUREMENT_ID, API_KEY, PJSON_NAME, PJSON_VERSION, INTERPRETER } from './constants'; import { getClientId } from './client-id'; -import type { AgentParams } from '../lib/models/common'; +import type { AgentParams } from '../models/common'; interface EventParams { interpreter: string | null; From 0bdc767165803ce3c50c85bec80cb89daef8b217 Mon Sep 17 00:00:00 2001 From: Ilya_Hancharyk Date: Sun, 30 Aug 2026 16:02:22 +0200 Subject: [PATCH 09/14] EPMRPP-89496 || Chore: use types only export for models dir --- .eslintignore | 1 - .gitignore | 1 - DEV_GUIDE.md | 12 +++++++++++- package.json | 10 +--------- scripts/generate-resolver-facades.js | 27 +++++++++++++++++++-------- src/constants/index.ts | 1 + src/constants/mergeTypes.ts | 4 ++++ src/models/requests.ts | 6 +----- 8 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 src/constants/mergeTypes.ts diff --git a/.eslintignore b/.eslintignore index b6ed6049..01f55811 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,7 +6,6 @@ __tests__/**/*.json /helpers.d.ts /constants.js /constants.d.ts -/models.js /models.d.ts /publicReportingAPI.js /publicReportingAPI.d.ts diff --git a/.gitignore b/.gitignore index e3d7eda9..aa492f49 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ build /helpers.d.ts /constants.js /constants.d.ts -/models.js /models.d.ts /publicReportingAPI.js /publicReportingAPI.d.ts diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md index 3190f914..79f21339 100644 --- a/DEV_GUIDE.md +++ b/DEV_GUIDE.md @@ -7,7 +7,17 @@ README.md, which is published to package registries. `npm run build` compiles to `build/` and then runs `scripts/generate-resolver-facades.js`, which writes a one-line re-export at each public subpath alias — `helpers.js`, -`constants.js`, `models.js`, `publicReportingAPI.js` — together with matching `.d.ts` files. +`constants.js`, `publicReportingAPI.js` — together with matching `.d.ts` files. `models` only +gets a `.d.ts` facade: `src/models/**` is TypeScript types with no runtime value (confirmed — +the compiled `build/models/*.js` files are all empty `__esModule` stubs), so +`package.json#exports["./models"]` has no `import` / `require` condition. Node enforces that +regardless of whether a `.js` file physically exists at the root, so writing one would just be +misleading. If a consumer's TypeScript setup runs with `isolatedModules` and they import from +`models` without `import type`, they'll now get `ERR_PACKAGE_PATH_NOT_EXPORTED` at runtime +instead of a silently-empty object — that's intentional; there are no known consumers of this +brand-new subpath yet, so this is the cheapest point to make the contract strict. If a genuine +runtime value ever needs to live under `models`, move it to `constants/` (like `MERGE_TYPES`, +which used to live here) instead of adding a runtime condition back. Supported package imports resolve through the `exports` / `typesVersions` maps in `package.json` straight to `build` for Node, TypeScript and bundlers; these files are never diff --git a/package.json b/package.json index 12d2e281..1390a56a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "/helpers.d.ts", "/constants.js", "/constants.d.ts", - "/models.js", "/models.d.ts", "/publicReportingAPI.js", "/publicReportingAPI.d.ts" @@ -40,9 +39,7 @@ "require": "./build/constants/index.js" }, "./models": { - "types": "./build/models/index.d.ts", - "import": "./build/models/index.js", - "require": "./build/models/index.js" + "types": "./build/models/index.d.ts" }, "./helpers": { "types": "./build/helpers.d.ts", @@ -60,11 +57,6 @@ "import": "./build/constants/index.js", "require": "./build/constants/index.js" }, - "./lib/models": { - "types": "./build/models/index.d.ts", - "import": "./build/models/index.js", - "require": "./build/models/index.js" - }, "./lib/*": { "types": "./build/*.d.ts", "import": "./build/*.js", diff --git a/scripts/generate-resolver-facades.js b/scripts/generate-resolver-facades.js index ad5e467e..5d7b2504 100644 --- a/scripts/generate-resolver-facades.js +++ b/scripts/generate-resolver-facades.js @@ -26,15 +26,24 @@ const BUILD_DIR = path.join(ROOT, 'build'); const toPosix = (p) => p.split(path.sep).join('/'); -/** Public subpath alias -> module it re-exports, relative to `build`. */ +/** + * Public subpath alias -> module it re-exports, relative to `build`. + * + * `typesOnly: true` means `package.json#exports` has no `import` / `require` condition for + * that alias (it re-exports pure TypeScript types with no runtime value — see + * DEV_GUIDE.md#subpath-facades) — Node rejects `require()`/`import()` of it regardless of + * whether a `.js` file physically exists, so writing one would be misleading; only the + * `.d.ts` facade is generated. + */ const ALIASES = { - helpers: 'helpers', - constants: 'constants/index', - models: 'models/index', - publicReportingAPI: 'publicReportingAPI', + helpers: { module: 'helpers' }, + constants: { module: 'constants/index' }, + models: { module: 'models/index', typesOnly: true }, + publicReportingAPI: { module: 'publicReportingAPI' }, }; -const facadeFiles = () => Object.keys(ALIASES).flatMap((name) => [`${name}.js`, `${name}.d.ts`]); +const facadeFiles = () => + Object.entries(ALIASES).flatMap(([name, { typesOnly }]) => (typesOnly ? [`${name}.d.ts`] : [`${name}.js`, `${name}.d.ts`])); const clean = () => { const files = facadeFiles().filter((file) => fs.existsSync(path.join(ROOT, file))); @@ -65,10 +74,12 @@ const generate = () => { throw new Error(`Nothing to generate from: ${toPosix(path.relative(ROOT, BUILD_DIR))} is missing, run "tsc" first.`); } - Object.entries(ALIASES).forEach(([name, module]) => { + Object.entries(ALIASES).forEach(([name, { module, typesOnly }]) => { const specifier = `./build/${module}`; - fs.writeFileSync(path.join(ROOT, `${name}.js`), `module.exports = require('${specifier}');\n`); + if (!typesOnly) { + fs.writeFileSync(path.join(ROOT, `${name}.js`), `module.exports = require('${specifier}');\n`); + } const declaration = path.join(BUILD_DIR, `${module}.d.ts`); fs.writeFileSync(path.join(ROOT, `${name}.d.ts`), declarationFacade(specifier, declaration)); diff --git a/src/constants/index.ts b/src/constants/index.ts index 2a3e4739..0f25aa21 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -2,5 +2,6 @@ export { STATUSES, RP_STATUSES } from './statuses'; export { TEST_ITEM_TYPES } from './testItemTypes'; export { PREDEFINED_LOG_LEVELS, LOG_LEVELS } from './logLevels'; export { LAUNCH_MODES } from './launchModes'; +export { MERGE_TYPES } from './mergeTypes'; export { EVENTS } from './events'; export { OUTPUT_TYPES, OutputHandler } from './outputs'; diff --git a/src/constants/mergeTypes.ts b/src/constants/mergeTypes.ts new file mode 100644 index 00000000..dace5cf4 --- /dev/null +++ b/src/constants/mergeTypes.ts @@ -0,0 +1,4 @@ +export enum MERGE_TYPES { + BASIC = 'BASIC', + DEEP = 'DEEP', +} diff --git a/src/models/requests.ts b/src/models/requests.ts index 09413e34..0daec14d 100644 --- a/src/models/requests.ts +++ b/src/models/requests.ts @@ -1,6 +1,7 @@ import { Attachment, Attribute, Issue } from './common'; import { LAUNCH_MODES } from '../constants/launchModes'; import { LOG_LEVELS } from '../constants/logLevels'; +import { MERGE_TYPES } from '../constants/mergeTypes'; import { STATUSES } from '../constants/statuses'; import { TEST_ITEM_TYPES } from '../constants/testItemTypes'; @@ -85,11 +86,6 @@ export interface LogOptions { file?: Attachment; } -export enum MERGE_TYPES { - BASIC = 'BASIC', - DEEP = 'DEEP', -} - export interface MergeLaunchesOptions { extendSuitesDescription?: boolean; description?: string; From 174f30baa724a310dc8d3e16c43ec4b13577b38e Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Mon, 31 Aug 2026 15:22:59 +0400 Subject: [PATCH 10/14] EPMRPP-89496 || Update Jest configuration and add TypeScript test files --- .../{client-id.spec.js => client-id.spec.ts} | 22 +- __tests__/{config.spec.js => config.spec.ts} | 18 +- .../{helpers.spec.js => helpers.spec.ts} | 17 +- __tests__/{oauth.spec.js => oauth.spec.ts} | 179 ++-- ...roxyHelper.spec.js => proxyHelper.spec.ts} | 57 +- ...API.spec.js => publicReportingAPI.spec.ts} | 4 +- ...t.spec.js => report-portal-client.spec.ts} | 929 +++++++++++------- __tests__/{rest.spec.js => rest.spec.ts} | 202 ++-- ...{statistics.spec.js => statistics.spec.ts} | 56 +- jest.config.js | 5 +- package-lock.json | 8 + package.json | 26 +- tsconfig.spec.json | 10 + 13 files changed, 944 insertions(+), 589 deletions(-) rename __tests__/{client-id.spec.js => client-id.spec.ts} (84%) rename __tests__/{config.spec.js => config.spec.ts} (86%) rename __tests__/{helpers.spec.js => helpers.spec.ts} (88%) rename __tests__/{oauth.spec.js => oauth.spec.ts} (72%) rename __tests__/{proxyHelper.spec.js => proxyHelper.spec.ts} (86%) rename __tests__/{publicReportingAPI.spec.js => publicReportingAPI.spec.ts} (95%) rename __tests__/{report-portal-client.spec.js => report-portal-client.spec.ts} (61%) rename __tests__/{rest.spec.js => rest.spec.ts} (73%) rename __tests__/{statistics.spec.js => statistics.spec.ts} (68%) create mode 100644 tsconfig.spec.json diff --git a/__tests__/client-id.spec.js b/__tests__/client-id.spec.ts similarity index 84% rename from __tests__/client-id.spec.js rename to __tests__/client-id.spec.ts index ff9f8dde..393f1ff4 100644 --- a/__tests__/client-id.spec.js +++ b/__tests__/client-id.spec.ts @@ -1,11 +1,14 @@ -const fs = require('fs'); -const util = require('util'); -const path = require('path'); -const { randomUUID } = require('crypto'); +import fs from 'fs'; +import util from 'util'; +import path from 'path'; +import { randomUUID } from 'crypto'; const testHomeDir = path.join(__dirname, '__tmp__', 'rp-home'); process.env.RP_CLIENT_JS_HOME = testHomeDir; -const { getClientId } = require('../src/statistics/client-id'); + +// A static `import` would be hoisted above the `process.env` assignment above, so the module +// (which reads RP_CLIENT_JS_HOME at load time) is loaded lazily via a dynamic import instead. +let getClientId: () => Promise; const uuidv4Validation = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; const clientIdFile = path.join(testHomeDir, '.rp', 'rp.properties'); @@ -14,18 +17,21 @@ const unlink = util.promisify(fs.unlink); const readFile = util.promisify(fs.readFile); const writeFile = util.promisify(fs.writeFile); const removeTestHomeDir = () => fs.promises.rm(testHomeDir, { recursive: true, force: true }); -const unlinkFile = async (filePath) => { +const unlinkFile = async (filePath: string) => { try { await unlink(filePath); } catch (error) { - if (error.code !== 'ENOENT') { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error; } } }; describe('Client ID test suite', () => { - beforeAll(removeTestHomeDir); + beforeAll(async () => { + await removeTestHomeDir(); + ({ getClientId } = await import('../src/statistics/client-id')); + }); afterAll(removeTestHomeDir); it('getClientId should return the same client ID for two calls', async () => { diff --git a/__tests__/config.spec.js b/__tests__/config.spec.ts similarity index 86% rename from __tests__/config.spec.js rename to __tests__/config.spec.ts index daaf3b7d..33dc1e3c 100644 --- a/__tests__/config.spec.js +++ b/__tests__/config.spec.ts @@ -1,8 +1,6 @@ -const { getClientConfig, getRequiredOption, getApiKey } = require('../src/commons/config'); -const { - ReportPortalRequiredOptionError, - ReportPortalValidationError, -} = require('../src/commons/errors'); +import { getClientConfig, getRequiredOption, getApiKey } from '../src/commons/config'; +import { ReportPortalRequiredOptionError, ReportPortalValidationError } from '../src/commons/errors'; +import type { ReportPortalConfig } from '../src/models/config'; describe('Config commons test suite', () => { describe('getRequiredOption', () => { @@ -26,7 +24,7 @@ describe('Config commons test suite', () => { it('should throw ReportPortalRequiredOptionError in case of option not present in options', () => { let error; try { - getRequiredOption({ other: 1 }, 'project'); + getRequiredOption({ other: 1 } as unknown as { project: unknown }, 'project'); } catch (e) { error = e; } @@ -73,7 +71,7 @@ describe('Config commons test suite', () => { describe('getClientConfig', () => { it('should print ReportPortalValidationError error to the console in case of options is not an object type', () => { jest.spyOn(console, 'dir').mockImplementation(); - getClientConfig('options'); + getClientConfig('options' as unknown as ReportPortalConfig); expect(console.dir).toHaveBeenCalledWith( new ReportPortalValidationError('`options` must be an object.'), @@ -85,7 +83,7 @@ describe('Config commons test suite', () => { getClientConfig({ apiKey: '123', project: 'prj', - }); + } as unknown as ReportPortalConfig); expect(console.dir).toHaveBeenCalledWith(new ReportPortalRequiredOptionError('endpoint')); }); @@ -95,7 +93,7 @@ describe('Config commons test suite', () => { getClientConfig({ apiKey: '123', endpoint: 'https://abc.com', - }); + } as unknown as ReportPortalConfig); expect(console.dir).toHaveBeenCalledWith(new ReportPortalRequiredOptionError('project')); }); @@ -105,7 +103,7 @@ describe('Config commons test suite', () => { getClientConfig({ project: 'prj', endpoint: 'https://abc.com', - }); + } as unknown as ReportPortalConfig); expect(console.dir).toHaveBeenCalledWith(new ReportPortalRequiredOptionError('apiKey')); }); diff --git a/__tests__/helpers.spec.js b/__tests__/helpers.spec.ts similarity index 88% rename from __tests__/helpers.spec.js rename to __tests__/helpers.spec.ts index cb9fc1d7..ac26555a 100644 --- a/__tests__/helpers.spec.js +++ b/__tests__/helpers.spec.ts @@ -1,8 +1,9 @@ -const os = require('os'); -const fs = require('fs'); -const glob = require('glob'); -const helpers = require('../src/helpers'); -const pjson = require('../package.json'); +import os from 'os'; +import fs from 'fs'; +import * as glob from 'glob'; +import * as helpers from '../src/helpers'; +import type { TestItemParameter } from '../src/models/requests'; +import pjson from '../package.json'; describe('Helpers', () => { describe('formatName', () => { @@ -22,7 +23,7 @@ describe('Helpers', () => { describe('now', () => { it('returns milliseconds from unix time', () => { - expect(new Date() - helpers.now()).toBeLessThan(100); // less than 100 miliseconds difference + expect(Number(new Date()) - helpers.now()).toBeLessThan(100); // less than 100 miliseconds difference }); }); @@ -50,7 +51,7 @@ describe('Helpers', () => { it('should return correct system attributes', () => { jest.spyOn(os, 'type').mockReturnValue('osType'); jest.spyOn(os, 'arch').mockReturnValue('osArchitecture'); - jest.spyOn(os, 'totalmem').mockReturnValue('1'); + jest.spyOn(os, 'totalmem').mockReturnValue(1); const nodeVersion = process.version; const expectedAttr = [ { @@ -106,7 +107,7 @@ describe('Helpers', () => { key: 'keyThree', value: 'valueThree', }, - ]; + ] as TestItemParameter[]; const testCaseId = helpers.generateTestCaseId('codeRef', parameters); diff --git a/__tests__/oauth.spec.js b/__tests__/oauth.spec.ts similarity index 72% rename from __tests__/oauth.spec.js rename to __tests__/oauth.spec.ts index a8128a5a..0e1e0589 100644 --- a/__tests__/oauth.spec.js +++ b/__tests__/oauth.spec.ts @@ -1,12 +1,64 @@ -const axios = require('axios'); -const { HttpsProxyAgent } = require('https-proxy-agent'); -const OAuthInterceptor = require('../src/oauth'); +import axios, { AxiosInstance } from 'axios'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import OAuthInterceptor from '../src/oauth'; jest.mock('axios', () => ({ post: jest.fn(), - isAxiosError: jest.fn((error) => !!error && typeof error === 'object' && error.isAxiosError === true), + isAxiosError: jest.fn( + (error: unknown) => !!error && typeof error === 'object' && (error as { isAxiosError?: boolean }).isAxiosError === true, + ), })); +// The whole `axios` module is replaced by the factory above, so its real (unmocked) type +// doesn't reflect what's actually exported at runtime - this alias gives typed access to the +// mock methods (`.mockResolvedValue`, `.mock.calls`, ...) on the two functions the factory does provide. +const mockedAxios = axios as unknown as { + post: jest.Mock; + isAxiosError: jest.Mock; +}; + +// `accessToken` / `refreshToken` / `tokenExpiresAt` / `tokenRenewPromise` are private on +// OAuthInterceptor by design (external callers only need `getAccessToken`/`attach`); these tests +// deliberately reach into that internal state to set up scenarios, so they need a typed escape hatch. +interface OAuthInterceptorInternal { + accessToken: string | null; + refreshToken: string | null; + tokenExpiresAt: number | null; + tokenRenewPromise: Promise | null; + getAccessToken(): Promise; + attach(axiosInstance: AxiosInstance): void; + logDebug(message: string, data?: unknown): void; +} +const asInternal = (interceptor: OAuthInterceptor): OAuthInterceptorInternal => + interceptor as unknown as OAuthInterceptorInternal; + +// Minimal stand-in for the axios instance passed to `attach()` - only `interceptors.request.use` +// is exercised, so the mock only needs to capture the two handlers it's called with. +type RequestHandler = (config: { headers: Record; url: string }) => Promise<{ + headers: Record; + url: string; +}>; +type RejectionHandler = (error: unknown) => Promise; +const createAxiosInstanceMock = () => { + let requestHandler: RequestHandler | undefined; + let rejectionHandler: RejectionHandler | undefined; + const axiosInstance = { + interceptors: { + request: { + use: jest.fn((fulfilled: RequestHandler, rejected: RejectionHandler) => { + requestHandler = fulfilled; + rejectionHandler = rejected; + }), + }, + }, + }; + return { + axiosInstance, + getRequestHandler: () => requestHandler as RequestHandler, + getRejectionHandler: () => rejectionHandler as RejectionHandler, + }; +}; + describe('OAuthInterceptor', () => { const baseConfig = { tokenEndpoint: 'https://auth.example.com/oauth/token', @@ -20,14 +72,14 @@ describe('OAuthInterceptor', () => { const DEFAULT_TOKEN_EXPIRATION_MS = 3600000; beforeEach(() => { - axios.post.mockReset(); + mockedAxios.post.mockReset(); }); it('requests an access token using password grant on first call', async () => { const baseTime = 1700000000000; const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime); const oauthInterceptor = new OAuthInterceptor(baseConfig); - axios.post.mockResolvedValue({ + mockedAxios.post.mockResolvedValue({ data: { access_token: 'token-123', refresh_token: 'refresh-123', @@ -38,8 +90,8 @@ describe('OAuthInterceptor', () => { const token = await oauthInterceptor.getAccessToken(); expect(token).toBe('token-123'); - expect(axios.post).toHaveBeenCalledTimes(1); - const [url, params, config] = axios.post.mock.calls[0]; + expect(mockedAxios.post).toHaveBeenCalledTimes(1); + const [url, params, config] = mockedAxios.post.mock.calls[0]; expect(url).toBe(baseConfig.tokenEndpoint); expect(params).toBeInstanceOf(URLSearchParams); @@ -51,8 +103,8 @@ describe('OAuthInterceptor', () => { expect(params.get('scope')).toBe(baseConfig.scope); expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' }); expect(config.httpsAgent).toBeDefined(); // Default agent added - expect(oauthInterceptor.refreshToken).toBe('refresh-123'); - expect(oauthInterceptor.tokenExpiresAt).toBe(baseTime + 120000); + expect(asInternal(oauthInterceptor).refreshToken).toBe('refresh-123'); + expect(asInternal(oauthInterceptor).tokenExpiresAt).toBe(baseTime + 120000); nowSpy.mockRestore(); }); @@ -60,14 +112,14 @@ describe('OAuthInterceptor', () => { it('returns cached token when it is not expiring soon', async () => { const baseTime = 1700000100000; const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime); - const oauthInterceptor = new OAuthInterceptor(baseConfig); + const oauthInterceptor = asInternal(new OAuthInterceptor(baseConfig)); oauthInterceptor.accessToken = 'cached-token'; oauthInterceptor.tokenExpiresAt = baseTime + TOKEN_REFRESH_THRESHOLD_MS + 5000; const token = await oauthInterceptor.getAccessToken(); expect(token).toBe('cached-token'); - expect(axios.post).not.toHaveBeenCalled(); + expect(mockedAxios.post).not.toHaveBeenCalled(); nowSpy.mockRestore(); }); @@ -75,11 +127,11 @@ describe('OAuthInterceptor', () => { it('refreshes token using stored refresh token when it is close to expiring', async () => { const baseTime = 1700000200000; const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime); - const oauthInterceptor = new OAuthInterceptor(baseConfig); + const oauthInterceptor = asInternal(new OAuthInterceptor(baseConfig)); oauthInterceptor.accessToken = 'stale-token'; oauthInterceptor.refreshToken = 'stored-refresh'; oauthInterceptor.tokenExpiresAt = baseTime + TOKEN_REFRESH_THRESHOLD_MS - 1000; - axios.post.mockResolvedValue({ + mockedAxios.post.mockResolvedValue({ data: { access_token: 'fresh-token', refresh_token: 'fresh-refresh', @@ -89,8 +141,8 @@ describe('OAuthInterceptor', () => { const token = await oauthInterceptor.getAccessToken(); expect(token).toBe('fresh-token'); - expect(axios.post).toHaveBeenCalledTimes(1); - const [, params] = axios.post.mock.calls[0]; + expect(mockedAxios.post).toHaveBeenCalledTimes(1); + const [, params] = mockedAxios.post.mock.calls[0]; expect(params.get('grant_type')).toBe('refresh_token'); expect(params.get('refresh_token')).toBe('stored-refresh'); expect(oauthInterceptor.refreshToken).toBe('fresh-refresh'); @@ -102,19 +154,19 @@ describe('OAuthInterceptor', () => { it('waits for ongoing token renewal and reuses the resolved token', async () => { const baseTime = 1700000300000; const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime); - const oauthInterceptor = new OAuthInterceptor(baseConfig); + const oauthInterceptor = asInternal(new OAuthInterceptor(baseConfig)); - let resolveRequest; + let resolveRequest: (value: unknown) => void; const tokenResponsePromise = new Promise((resolve) => { resolveRequest = resolve; }); - axios.post.mockReturnValue(tokenResponsePromise); + mockedAxios.post.mockReturnValue(tokenResponsePromise); const firstCall = oauthInterceptor.getAccessToken(); const secondCall = oauthInterceptor.getAccessToken(); - expect(axios.post).toHaveBeenCalledTimes(1); - resolveRequest({ + expect(mockedAxios.post).toHaveBeenCalledTimes(1); + resolveRequest!({ data: { access_token: 'shared-token', refresh_token: 'shared-refresh', @@ -134,7 +186,7 @@ describe('OAuthInterceptor', () => { it('logs an error and throws descriptive message when token request fails', async () => { const oauthInterceptor = new OAuthInterceptor(baseConfig); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - axios.post.mockRejectedValue({ + mockedAxios.post.mockRejectedValue({ isAxiosError: true, response: { status: 400, @@ -156,7 +208,7 @@ describe('OAuthInterceptor', () => { const oauthInterceptor = new OAuthInterceptor(baseConfig); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); // Response resolves successfully but is missing the access_token field. - axios.post.mockResolvedValue({ data: { expires_in: 120 } }); + mockedAxios.post.mockResolvedValue({ data: { expires_in: 120 } }); await expect(oauthInterceptor.getAccessToken()).rejects.toThrow( 'OAuth token request failed: No access token received from OAuth server', @@ -172,7 +224,7 @@ describe('OAuthInterceptor', () => { const oauthInterceptor = new OAuthInterceptor(baseConfig); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); // A plain Error (not an AxiosError, so no response payload to include). - axios.post.mockRejectedValue(new Error('network is unreachable')); + mockedAxios.post.mockRejectedValue(new Error('network is unreachable')); await expect(oauthInterceptor.getAccessToken()).rejects.toThrow( 'OAuth token request failed: network is unreachable', @@ -183,21 +235,12 @@ describe('OAuthInterceptor', () => { it('propagates request errors through the attached rejection handler', async () => { const oauthInterceptor = new OAuthInterceptor(baseConfig); - let rejectionHandler; - const axiosInstance = { - interceptors: { - request: { - use: jest.fn((fulfilled, rejected) => { - rejectionHandler = rejected; - }), - }, - }, - }; + const { axiosInstance, getRejectionHandler } = createAxiosInstanceMock(); - oauthInterceptor.attach(axiosInstance); + oauthInterceptor.attach(axiosInstance as unknown as AxiosInstance); const error = new Error('request setup failed'); - await expect(rejectionHandler(error)).rejects.toBe(error); + await expect(getRejectionHandler()(error)).rejects.toBe(error); }); it('logs debug messages only when debug mode is enabled', () => { @@ -216,25 +259,16 @@ describe('OAuthInterceptor', () => { it('injects Authorization header through attached request interceptor', async () => { const baseTime = 1700000400000; const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime); - const oauthInterceptor = new OAuthInterceptor(baseConfig); + const oauthInterceptor = asInternal(new OAuthInterceptor(baseConfig)); oauthInterceptor.accessToken = 'cached-token'; oauthInterceptor.tokenExpiresAt = baseTime + TOKEN_REFRESH_THRESHOLD_MS + 1000; - let requestHandler; - const axiosInstance = { - interceptors: { - request: { - use: jest.fn((fulfilled) => { - requestHandler = fulfilled; - }), - }, - }, - }; + const { axiosInstance, getRequestHandler } = createAxiosInstanceMock(); - oauthInterceptor.attach(axiosInstance); - const requestConfig = await requestHandler({ headers: {}, url: '/launch' }); + oauthInterceptor.attach(axiosInstance as unknown as AxiosInstance); + const requestConfig = await getRequestHandler()({ headers: {}, url: '/launch' }); expect(requestConfig.headers.Authorization).toBe('Bearer cached-token'); - expect(axios.post).not.toHaveBeenCalled(); + expect(mockedAxios.post).not.toHaveBeenCalled(); nowSpy.mockRestore(); }); @@ -244,19 +278,10 @@ describe('OAuthInterceptor', () => { const error = new Error('refresh failed'); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); const tokenSpy = jest.spyOn(oauthInterceptor, 'getAccessToken').mockRejectedValue(error); - let requestHandler; - const axiosInstance = { - interceptors: { - request: { - use: jest.fn((fulfilled) => { - requestHandler = fulfilled; - }), - }, - }, - }; + const { axiosInstance, getRequestHandler } = createAxiosInstanceMock(); - oauthInterceptor.attach(axiosInstance); - const requestConfig = await requestHandler({ headers: {}, url: '/launch' }); + oauthInterceptor.attach(axiosInstance as unknown as AxiosInstance); + const requestConfig = await getRequestHandler()({ headers: {}, url: '/launch' }); expect(requestConfig.headers.Authorization).toBeUndefined(); expect(consoleSpy).toHaveBeenCalledWith( @@ -273,13 +298,13 @@ describe('OAuthInterceptor', () => { const baseTime = 1700000500000; const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime); const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); - const oauthInterceptor = new OAuthInterceptor(baseConfig); + const oauthInterceptor = asInternal(new OAuthInterceptor(baseConfig)); oauthInterceptor.accessToken = 'old-token'; oauthInterceptor.refreshToken = 'expired-refresh-token'; oauthInterceptor.tokenExpiresAt = baseTime - 1000; // Token already expired // First call (refresh token) fails - axios.post + mockedAxios.post .mockRejectedValueOnce({ isAxiosError: true, response: { @@ -299,15 +324,15 @@ describe('OAuthInterceptor', () => { const token = await oauthInterceptor.getAccessToken(); expect(token).toBe('new-token-from-password'); - expect(axios.post).toHaveBeenCalledTimes(2); + expect(mockedAxios.post).toHaveBeenCalledTimes(2); // First call should be refresh_token grant - const [, firstParams] = axios.post.mock.calls[0]; + const [, firstParams] = mockedAxios.post.mock.calls[0]; expect(firstParams.get('grant_type')).toBe('refresh_token'); expect(firstParams.get('refresh_token')).toBe('expired-refresh-token'); // Second call should be password grant - const [, secondParams] = axios.post.mock.calls[1]; + const [, secondParams] = mockedAxios.post.mock.calls[1]; expect(secondParams.get('grant_type')).toBe('password'); expect(secondParams.get('username')).toBe(baseConfig.username); expect(secondParams.get('password')).toBe(baseConfig.password); @@ -330,12 +355,12 @@ describe('OAuthInterceptor', () => { const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime); const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); - const oauthInterceptor = new OAuthInterceptor(baseConfig); + const oauthInterceptor = asInternal(new OAuthInterceptor(baseConfig)); oauthInterceptor.refreshToken = 'expired-refresh-token'; oauthInterceptor.tokenExpiresAt = baseTime - 1000; // Both calls fail - axios.post + mockedAxios.post .mockRejectedValueOnce({ isAxiosError: true, response: { @@ -355,7 +380,7 @@ describe('OAuthInterceptor', () => { 'OAuth password grant fallback failed: 401 - {"error":"invalid_credentials"}', ); - expect(axios.post).toHaveBeenCalledTimes(2); + expect(mockedAxios.post).toHaveBeenCalledTimes(2); expect(consoleWarnSpy).toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith( '[OAuth] OAuth password grant fallback failed: 401 - {"error":"invalid_credentials"}', @@ -380,7 +405,7 @@ describe('OAuthInterceptor', () => { }, }; const oauthInterceptor = new OAuthInterceptor(configWithProxy); - axios.post.mockResolvedValue({ + mockedAxios.post.mockResolvedValue({ data: { access_token: 'token-with-proxy', expires_in: 120, @@ -390,8 +415,8 @@ describe('OAuthInterceptor', () => { const token = await oauthInterceptor.getAccessToken(); expect(token).toBe('token-with-proxy'); - expect(axios.post).toHaveBeenCalledTimes(1); - const [url, , config] = axios.post.mock.calls[0]; + expect(mockedAxios.post).toHaveBeenCalledTimes(1); + const [url, , config] = mockedAxios.post.mock.calls[0]; expect(url).toBe(baseConfig.tokenEndpoint); expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' }); @@ -415,7 +440,7 @@ describe('OAuthInterceptor', () => { }, }, }); - axios.post.mockResolvedValue({ + mockedAxios.post.mockResolvedValue({ data: { access_token: 'token-debug-proxy', expires_in: 120 }, }); @@ -446,7 +471,7 @@ describe('OAuthInterceptor', () => { }, }; const oauthInterceptor = new OAuthInterceptor(configWithNoProxy); - axios.post.mockResolvedValue({ + mockedAxios.post.mockResolvedValue({ data: { access_token: 'token-no-proxy', expires_in: 120, @@ -456,8 +481,8 @@ describe('OAuthInterceptor', () => { const token = await oauthInterceptor.getAccessToken(); expect(token).toBe('token-no-proxy'); - expect(axios.post).toHaveBeenCalledTimes(1); - const [url, , config] = axios.post.mock.calls[0]; + expect(mockedAxios.post).toHaveBeenCalledTimes(1); + const [url, , config] = mockedAxios.post.mock.calls[0]; expect(url).toBe(baseConfig.tokenEndpoint); expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' }); diff --git a/__tests__/proxyHelper.spec.js b/__tests__/proxyHelper.spec.ts similarity index 86% rename from __tests__/proxyHelper.spec.js rename to __tests__/proxyHelper.spec.ts index 4cab9023..ca7f8535 100644 --- a/__tests__/proxyHelper.spec.js +++ b/__tests__/proxyHelper.spec.ts @@ -1,11 +1,12 @@ -const { HttpsProxyAgent } = require('https-proxy-agent'); -const { HttpProxyAgent } = require('http-proxy-agent'); -const { +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { HttpProxyAgent } from 'http-proxy-agent'; +import { shouldBypassProxy, getProxyConfig, createProxyAgents, getProxyAgentForUrl, -} = require('../src/proxyHelper'); +} from '../src/proxyHelper'; +import type { RestClientConfig } from '../src/models/config'; describe('proxyHelper', () => { const originalEnv = process.env; @@ -30,7 +31,7 @@ describe('proxyHelper', () => { describe('shouldBypassProxy', () => { it('returns false when noProxy is not provided', () => { expect(shouldBypassProxy('http://example.com', '')).toBe(false); - expect(shouldBypassProxy('http://example.com', null)).toBe(false); + expect(shouldBypassProxy('http://example.com', null as unknown as string)).toBe(false); expect(shouldBypassProxy('http://example.com', undefined)).toBe(false); }); @@ -85,12 +86,12 @@ describe('proxyHelper', () => { }); it('returns null when proxy is explicitly disabled', () => { - const config = { proxy: false }; + const config: RestClientConfig = { proxy: false }; expect(getProxyConfig('http://example.com', config)).toBeNull(); }); it('returns null when URL matches noProxy from config', () => { - const config = { noProxy: 'example.com,localhost' }; + const config: RestClientConfig = { noProxy: 'example.com,localhost' }; expect(getProxyConfig('http://example.com', config)).toBeNull(); expect(getProxyConfig('http://localhost', config)).toBeNull(); }); @@ -102,12 +103,12 @@ describe('proxyHelper', () => { it('prefers noProxy from config over environment', () => { process.env.NO_PROXY = 'other.com'; - const config = { noProxy: 'example.com' }; + const config: RestClientConfig = { noProxy: 'example.com' }; expect(getProxyConfig('http://example.com', config)).toBeNull(); }); it('returns proxy URL from string config', () => { - const config = { proxy: 'http://proxy.example.com:8080' }; + const config: RestClientConfig = { proxy: 'http://proxy.example.com:8080' }; const result = getProxyConfig('http://target.com', config); expect(result).toEqual({ proxyUrl: 'http://proxy.example.com:8080', @@ -115,7 +116,7 @@ describe('proxyHelper', () => { }); it('returns proxy URL from object config', () => { - const config = { + const config: RestClientConfig = { proxy: { protocol: 'https', host: 'proxy.example.com', @@ -129,7 +130,7 @@ describe('proxyHelper', () => { }); it('returns proxy URL with authentication', () => { - const config = { + const config: RestClientConfig = { proxy: { protocol: 'http', host: 'proxy.example.com', @@ -147,7 +148,7 @@ describe('proxyHelper', () => { }); it('uses http as default protocol for object config', () => { - const config = { + const config: RestClientConfig = { proxy: { host: 'proxy.example.com', port: 8080, @@ -184,7 +185,7 @@ describe('proxyHelper', () => { it('prioritizes explicit config over environment variables', () => { process.env.HTTPS_PROXY = 'http://env-proxy.com:8080'; - const config = { proxy: 'http://config-proxy.com:9090' }; + const config: RestClientConfig = { proxy: 'http://config-proxy.com:9090' }; const result = getProxyConfig('https://target.com', config); expect(result).toEqual({ proxyUrl: 'http://config-proxy.com:9090', @@ -196,47 +197,47 @@ describe('proxyHelper', () => { it('returns default agent when no proxy is configured', () => { const agents = createProxyAgents('http://example.com', {}); expect(agents.httpAgent).toBeDefined(); - expect(agents.httpAgent.constructor.name).toBe('Agent'); + expect(agents.httpAgent!.constructor.name).toBe('Agent'); }); it('creates HttpProxyAgent for HTTP URLs', () => { - const config = { proxy: 'http://proxy.example.com:8080' }; + const config: RestClientConfig = { proxy: 'http://proxy.example.com:8080' }; const agents = createProxyAgents('http://target.com', config); expect(agents.httpAgent).toBeInstanceOf(HttpProxyAgent); expect(agents.httpsAgent).toBeUndefined(); }); it('creates HttpsProxyAgent for HTTPS URLs', () => { - const config = { proxy: 'http://proxy.example.com:8080' }; + const config: RestClientConfig = { proxy: 'http://proxy.example.com:8080' }; const agents = createProxyAgents('https://target.com', config); expect(agents.httpsAgent).toBeInstanceOf(HttpsProxyAgent); expect(agents.httpAgent).toBeUndefined(); }); it('returns default agent when URL is in noProxy list', () => { - const config = { + const config: RestClientConfig = { proxy: 'http://proxy.example.com:8080', noProxy: 'target.com', }; const agents = createProxyAgents('http://target.com', config); expect(agents.httpAgent).toBeDefined(); - expect(agents.httpAgent.constructor.name).toBe('Agent'); + expect(agents.httpAgent!.constructor.name).toBe('Agent'); }); it('returns default HTTPS agent for HTTPS URLs in noProxy list', () => { - const config = { + const config: RestClientConfig = { proxy: 'http://proxy.example.com:8080', noProxy: 'target.com', }; const agents = createProxyAgents('https://target.com', config); expect(agents.httpsAgent).toBeDefined(); - expect(agents.httpsAgent.constructor.name).toBe('Agent'); + expect(agents.httpsAgent!.constructor.name).toBe('Agent'); }); }); describe('getProxyAgentForUrl', () => { it('returns appropriate agent based on URL protocol', () => { - const config = { proxy: 'http://proxy.example.com:8080' }; + const config: RestClientConfig = { proxy: 'http://proxy.example.com:8080' }; const httpAgents = getProxyAgentForUrl('http://target.com', config); expect(httpAgents.httpAgent).toBeInstanceOf(HttpProxyAgent); @@ -246,13 +247,13 @@ describe('proxyHelper', () => { }); it('respects noProxy configuration', () => { - const config = { + const config: RestClientConfig = { proxy: 'http://proxy.example.com:8080', noProxy: 'localhost,target.com', }; const agents = getProxyAgentForUrl('http://target.com', config); expect(agents.httpAgent).toBeDefined(); - expect(agents.httpAgent.constructor.name).toBe('Agent'); + expect(agents.httpAgent!.constructor.name).toBe('Agent'); }); it('works with environment variables', () => { @@ -266,12 +267,12 @@ describe('proxyHelper', () => { process.env.NO_PROXY = 'target.com'; const agents = getProxyAgentForUrl('http://target.com', {}); expect(agents.httpAgent).toBeDefined(); - expect(agents.httpAgent.constructor.name).toBe('Agent'); + expect(agents.httpAgent!.constructor.name).toBe('Agent'); }); }); describe('credential sanitization in debug logs', () => { - let consoleLogSpy; + let consoleLogSpy: jest.SpyInstance; beforeEach(() => { consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); @@ -282,7 +283,7 @@ describe('proxyHelper', () => { }); it('sanitizes proxy credentials in debug logs when using proxy config object', () => { - const config = { + const config: RestClientConfig = { proxy: { protocol: 'https', host: 'proxy.example.com', @@ -309,7 +310,7 @@ describe('proxyHelper', () => { }); it('sanitizes proxy credentials in debug logs when using proxy URL string', () => { - const config = { + const config: RestClientConfig = { proxy: 'https://myuser:mypassword@proxy.example.com:8080', debug: true, }; @@ -325,7 +326,7 @@ describe('proxyHelper', () => { }); it('logs proxy URL normally when no credentials are present', () => { - const config = { + const config: RestClientConfig = { proxy: 'https://proxy.example.com:8080', debug: true, }; diff --git a/__tests__/publicReportingAPI.spec.js b/__tests__/publicReportingAPI.spec.ts similarity index 95% rename from __tests__/publicReportingAPI.spec.js rename to __tests__/publicReportingAPI.spec.ts index 71c770f7..f744dbdd 100644 --- a/__tests__/publicReportingAPI.spec.js +++ b/__tests__/publicReportingAPI.spec.ts @@ -1,5 +1,5 @@ -const PublicReportingAPI = require('../src/publicReportingAPI'); -const { EVENTS } = require('../src/constants/events'); +import PublicReportingAPI from '../src/publicReportingAPI'; +import { EVENTS } from '../src/constants/events'; describe('PublicReportingAPI', () => { it('setDescription should trigger process.emit with correct parameters', () => { diff --git a/__tests__/report-portal-client.spec.js b/__tests__/report-portal-client.spec.ts similarity index 61% rename from __tests__/report-portal-client.spec.js rename to __tests__/report-portal-client.spec.ts index 847bacf4..d79fd4bf 100644 --- a/__tests__/report-portal-client.spec.js +++ b/__tests__/report-portal-client.spec.ts @@ -1,7 +1,132 @@ -const process = require('process'); -const RPClient = require('../src/report-portal-client'); -const helpers = require('../src/helpers'); -const { OUTPUT_TYPES } = require('../src/constants/outputs'); +import process from 'process'; +import RPClient from '../src/report-portal-client'; +import RestClient from '../src/rest'; +import * as helpers from '../src/helpers'; +import { OUTPUT_TYPES } from '../src/constants/outputs'; +import type { AgentParams, Attachment, ClientResponse } from '../src/models/common'; +import type { NormalizedClientConfig } from '../src/models/config'; +import type { + FinishLaunchOptions, + FinishTestItemOptions, + FinishTestItemRQ, + LogOptions, + MergeLaunchesOptions, + StartLaunchOptions, + StartTestItemOptions, + UpdateLaunchOptions, +} from '../src/models/requests'; +import type { + FinishLaunchResponse, + PageLaunchResource, + ServerInfoResponse, + StartLaunchResponse, + StartTestItemResponse, + UpdateLaunchResponse, +} from '../src/models/responses'; + +// Mirrors the (non-exported) internal item/launch record kept in `RPClient.map`. +interface ItemObj { + promiseStart: Promise; + realId: string; + children: string[]; + finishSend: boolean; + promiseFinish: Promise; + resolveFinish: (value: T) => void; + rejectFinish: (reason?: Error) => void; +} + +type RequestPromiseFunc = (itemUuid: string, launchUuid: string) => Promise; + +// `eventName` / `eventParams` are private on Statistics, but these tests assert on them directly. +interface StatisticsInternal { + eventName: string; + eventParams: Record; + setInstanceID(instanceID: string): void; + trackEvent(): Promise; +} + +// Almost every member of RPClient is `private` by design (the public surface is the reporting API +// only). These tests are deliberately whitebox: they seed `map`, stub `restClient`/`statistics` and +// assert on internal state, so they need a typed escape hatch into those members. +// A standalone interface (not an intersection with RPClient) is required - intersecting a type +// literal with a class that has private members collapses to `never`. +interface RPClientInternal { + config: NormalizedClientConfig; + debug?: boolean; + isLaunchMergeRequired: boolean; + apiKey: string | null; + token: string | null; + map: Record; + baseURL: string; + headers: Record; + helpers: typeof helpers; + restClient: RestClient; + statistics: StatisticsInternal; + launchUuid: string; + itemRetriesChainMap: Map>; + itemRetriesChainKeyMapByTempId: Map; + + logDebug(msg: unknown, dataMsg?: unknown): void; + calculateItemRetriesChainMapKey( + launchId: string, + parentId: string | undefined, + name: string, + itemId?: string, + ): string; + cleanItemRetriesChain(tempIds: string[]): void; + getUniqId(): string; + getRejectAnswer(tempId: string, error: Error): ClientResponse; + cleanMap(ids: string[]): void; + checkConnect(): Promise; + getServerInfoUrl(): string; + fetchServerInfo(): Promise; + triggerStatisticsEvent(): Promise; + startLaunch( + launchDataRQ: StartLaunchOptions, + ): ClientResponse; + finishLaunch( + launchTempId: string, + finishExecutionRQ?: FinishLaunchOptions, + ): ClientResponse; + getMergeLaunchesRequest( + launchIds: Array, + mergeOptions?: MergeLaunchesOptions, + ): Record; + mergeLaunches(mergeOptions?: MergeLaunchesOptions): Promise | undefined; + getPromiseFinishAllItems(launchTempId: string): Promise; + updateLaunch( + launchTempId: string, + launchData: UpdateLaunchOptions, + ): ClientResponse; + startTestItem( + testItemDataRQ: StartTestItemOptions, + launchTempId: string, + parentTempId?: string, + ): ClientResponse; + finishTestItem( + itemTempId: string, + finishTestItemRQ?: FinishTestItemOptions, + ): ClientResponse; + saveLog(itemObj: ItemObj, requestPromiseFunc: RequestPromiseFunc): ClientResponse; + sendLog(itemTempId: string, saveLogRQ: LogOptions, fileObj?: Attachment): ClientResponse; + sendLogWithoutFile(itemTempId: string, saveLogRQ: LogOptions): ClientResponse; + // `fileObj` is optional here only so the tests can reproduce the original JS calls that omit it. + sendLogWithFile(itemTempId: string, saveLogRQ: LogOptions, fileObj?: Attachment): ClientResponse; + getRequestLogWithFile(saveLogRQ: LogOptions, fileObj: Attachment): Promise; + buildMultiPartStream(jsonPart: unknown[], filePart: Attachment, boundary: string): Buffer; + finishTestItemPromiseStart( + itemObj: ItemObj, + itemTempId: string, + finishTestItemData: FinishTestItemRQ, + ): void; +} + +const asInternal = (client: RPClient): RPClientInternal => client as unknown as RPClientInternal; + +// The tests seed `map` with deliberately partial item records (only the fields the code path under +// test reads), so the literals need to be widened to the real map shape. +const asMap = (map: Record): Record => + map as unknown as Record; describe('ReportPortal javascript client', () => { afterEach(() => { @@ -10,11 +135,13 @@ describe('ReportPortal javascript client', () => { describe('constructor', () => { it('creates the client instance without error', () => { - const client = new RPClient({ - apiKey: 'test', - project: 'test', - endpoint: 'https://abc.com', - }); + const client = asInternal( + new RPClient({ + apiKey: 'test', + project: 'test', + endpoint: 'https://abc.com', + }), + ); expect(client.config.apiKey).toBe('test'); expect(client.config.project).toBe('test'); @@ -100,7 +227,8 @@ describe('ReportPortal javascript client', () => { endpoint: 'https://abc.com', }); - const rejectAnswer = client.getRejectAnswer('tempId', 'error'); + // The original test intentionally rejects with a plain string, not an Error. + const rejectAnswer = client.getRejectAnswer('tempId', 'error' as unknown as Error); expect(rejectAnswer.tempId).toEqual('tempId'); return expect(rejectAnswer.promise).rejects.toEqual('error'); @@ -109,16 +237,18 @@ describe('ReportPortal javascript client', () => { describe('cleanMap', () => { it('should delete element with id', () => { - const client = new RPClient({ - apiKey: 'test', - project: 'test', - endpoint: 'https://abc.com', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'test', + project: 'test', + endpoint: 'https://abc.com', + }), + ); + client.map = asMap({ id1: 'firstElement', id2: 'secondElement', id3: 'thirdElement', - }; + }); client.cleanMap(['id1', 'id2']); @@ -128,11 +258,13 @@ describe('ReportPortal javascript client', () => { describe('checkConnect', () => { it('should return promise', () => { - const client = new RPClient({ - apiKey: 'test', - project: 'test', - endpoint: 'https://abc.com', - }); + const client = asInternal( + new RPClient({ + apiKey: 'test', + project: 'test', + endpoint: 'https://abc.com', + }), + ); jest.spyOn(client.restClient, 'request').mockReturnValue(Promise.resolve('ok')); const request = client.checkConnect(); @@ -153,11 +285,13 @@ describe('ReportPortal javascript client', () => { }); it('should call statistics.trackEvent if REPORTPORTAL_CLIENT_JS_NO_ANALYTICS is not set', async () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); jest.spyOn(client, 'fetchServerInfo').mockResolvedValue({}); jest.spyOn(client.statistics, 'trackEvent').mockImplementation(); @@ -167,12 +301,15 @@ describe('ReportPortal javascript client', () => { }); it('should not call statistics.trackEvent if REPORTPORTAL_CLIENT_JS_NO_ANALYTICS is true', async () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - process.env.REPORTPORTAL_CLIENT_JS_NO_ANALYTICS = true; + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + // The original test assigns the boolean `true`, not the string 'true'. + process.env.REPORTPORTAL_CLIENT_JS_NO_ANALYTICS = true as unknown as string; jest.spyOn(client.statistics, 'trackEvent').mockImplementation(); await client.triggerStatisticsEvent(); @@ -181,19 +318,22 @@ describe('ReportPortal javascript client', () => { }); it('should create statistics object with agentParams is not empty', () => { - const agentParams = { + const agentParams: AgentParams = { name: 'name', version: 'version', }; - const client = new RPClient( - { - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }, - agentParams, + const client = asInternal( + new RPClient( + { + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }, + agentParams, + ), ); - process.env.REPORTPORTAL_CLIENT_JS_NO_ANALYTICS = false; + // The original test assigns the boolean `false`, not the string 'false'. + process.env.REPORTPORTAL_CLIENT_JS_NO_ANALYTICS = false as unknown as string; expect(client.statistics.eventName).toEqual('start_launch'); expect(client.statistics.eventParams).toEqual( @@ -205,11 +345,13 @@ describe('ReportPortal javascript client', () => { }); it('should create statistics object without agentParams if they are empty', () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); expect(client.statistics.eventName).toEqual('start_launch'); expect(client.statistics.eventParams).not.toEqual( @@ -221,12 +363,14 @@ describe('ReportPortal javascript client', () => { }); it('should fetch server info and set instanceID before tracking event', async () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - const serverInfoResponse = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + const serverInfoResponse: ServerInfoResponse = { extensions: { result: { 'server.details.instance': 'test-instance-123', @@ -245,11 +389,13 @@ describe('ReportPortal javascript client', () => { }); it('should still track event with not_set instanceID if fetchServerInfo fails', async () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); jest.spyOn(client, 'fetchServerInfo').mockRejectedValue(new Error('Network error')); jest.spyOn(client.statistics, 'trackEvent').mockImplementation(); jest.spyOn(client.statistics, 'setInstanceID'); @@ -261,11 +407,13 @@ describe('ReportPortal javascript client', () => { }); it('should not set instanceID if server info does not contain it', async () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); jest.spyOn(client, 'fetchServerInfo').mockResolvedValue({}); jest.spyOn(client.statistics, 'trackEvent').mockImplementation(); jest.spyOn(client.statistics, 'setInstanceID'); @@ -301,11 +449,13 @@ describe('ReportPortal javascript client', () => { describe('fetchServerInfo', () => { it('should call restClient.request with correct URL', async () => { - const client = new RPClient({ - apiKey: 'test', - project: 'test', - endpoint: 'https://rp.us/api/v1', - }); + const client = asInternal( + new RPClient({ + apiKey: 'test', + project: 'test', + endpoint: 'https://rp.us/api/v1', + }), + ); const serverInfo = { extensions: { result: {} } }; jest.spyOn(client.restClient, 'request').mockResolvedValue(serverInfo); @@ -330,11 +480,13 @@ describe('ReportPortal javascript client', () => { system: true, }, ]; - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); const myPromise = Promise.resolve({ id: 'testidlaunch' }); const time = 12345734; jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); @@ -359,11 +511,13 @@ describe('ReportPortal javascript client', () => { system: true, }, ]; - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); const myPromise = Promise.resolve({ id: 'testidlaunch' }); const time = 12345734; jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); @@ -389,20 +543,23 @@ describe('ReportPortal javascript client', () => { }); it('dont start new launch if launchDataRQ.id is not empty', () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); const myPromise = Promise.resolve({ id: 'testidlaunch' }); const startTime = 12345734; const id = 12345734; jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); + // The original test passes a numeric `id`, while the API type declares it as a string. client.startLaunch({ startTime, id, - }); + } as unknown as StartLaunchOptions); expect(client.restClient.create).not.toHaveBeenCalled(); expect(client.launchUuid).toEqual(id); @@ -410,12 +567,14 @@ describe('ReportPortal javascript client', () => { it('should log Launch UUID if enabled', () => { jest.spyOn(OUTPUT_TYPES, 'STDOUT').mockImplementation(); - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - launchUuidPrint: true, - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + launchUuidPrint: true, + }), + ); const myPromise = Promise.resolve({ id: 'testidlaunch' }); const time = 12345734; jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); @@ -430,13 +589,16 @@ describe('ReportPortal javascript client', () => { it('should log Launch UUID into STDERR if enabled', () => { jest.spyOn(OUTPUT_TYPES, 'STDERR').mockImplementation(); - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - launchUuidPrint: true, - launchUuidPrintOutput: 'stderr', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + launchUuidPrint: true, + // The original test uses a lowercase value; the config normalizes it to upper case. + launchUuidPrintOutput: 'stderr', + } as unknown as ConstructorParameters[0]), + ); const myPromise = Promise.resolve({ id: 'testidlaunch' }); const time = 12345734; jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); @@ -451,13 +613,16 @@ describe('ReportPortal javascript client', () => { it('should log Launch UUID into STDOUT if invalid output is set', () => { jest.spyOn(OUTPUT_TYPES, 'STDOUT').mockImplementation(); - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - launchUuidPrint: true, - launchUuidPrintOutput: 'asdfgh', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + launchUuidPrint: true, + // Intentionally invalid output type - the client must fall back to STDOUT. + launchUuidPrintOutput: 'asdfgh', + } as unknown as ConstructorParameters[0]), + ); const myPromise = Promise.resolve({ id: 'testidlaunch' }); const time = 12345734; jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); @@ -472,13 +637,15 @@ describe('ReportPortal javascript client', () => { it('should log Launch UUID into ENVIRONMENT if enabled', () => { jest.spyOn(OUTPUT_TYPES, 'ENVIRONMENT').mockImplementation(); - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - launchUuidPrint: true, - launchUuidPrintOutput: 'environment', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + launchUuidPrint: true, + launchUuidPrintOutput: 'environment', + } as unknown as ConstructorParameters[0]), + ); const myPromise = Promise.resolve({ id: 'testidlaunch' }); const time = 12345734; jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); @@ -493,13 +660,15 @@ describe('ReportPortal javascript client', () => { it('should log Launch UUID into FILE if enabled', () => { jest.spyOn(OUTPUT_TYPES, 'FILE').mockImplementation(); - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - launchUuidPrint: true, - launchUuidPrintOutput: 'file', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + launchUuidPrint: true, + launchUuidPrintOutput: 'file', + } as unknown as ConstructorParameters[0]), + ); const myPromise = Promise.resolve({ id: 'testidlaunch' }); const time = 12345734; jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); @@ -514,11 +683,13 @@ describe('ReportPortal javascript client', () => { it('should not log Launch UUID if not enabled', () => { jest.spyOn(OUTPUT_TYPES, 'STDOUT').mockImplementation(); - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); const myPromise = Promise.resolve({ id: 'testidlaunch' }); const time = 12345734; jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); @@ -534,15 +705,17 @@ describe('ReportPortal javascript client', () => { describe('finishLaunch', () => { it('should call getRejectAnswer if there is no launchTempId with suitable launchTempId', () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - client.map = { + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + client.map = asMap({ id1: { children: ['child1'], }, - }; + }); jest.spyOn(client, 'getRejectAnswer').mockImplementation(); - client.finishLaunch('id2', { some: 'data' }); + client.finishLaunch('id2', { some: 'data' } as unknown as FinishLaunchOptions); expect(client.getRejectAnswer).toHaveBeenCalledWith( 'id2', @@ -551,23 +724,27 @@ describe('ReportPortal javascript client', () => { }); it('should trigger promiseFinish', async () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - client.map = { + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + client.map = asMap({ id1: { children: ['child1'], promiseStart: Promise.resolve(), - resolveFinish: jest.fn().mockResolvedValue(), + resolveFinish: jest.fn().mockResolvedValue(undefined), }, child1: { - promiseFinish: jest.fn().mockResolvedValue(), + promiseFinish: jest.fn().mockResolvedValue(undefined), }, - }; + }); jest.spyOn(client.restClient, 'update').mockResolvedValue({ link: 'link' }); - await client.finishLaunch('id1', { some: 'data' }).promise; + await client.finishLaunch('id1', { some: 'data' } as unknown as FinishLaunchOptions).promise; - expect(client.map.child1.promiseFinish().then).toBeDefined(); + // `promiseFinish` is a jest mock here, not a promise - the original test calls it. + const child1PromiseFinish = client.map.child1.promiseFinish as unknown as jest.Mock; + expect(child1PromiseFinish().then).toBeDefined(); }); }); @@ -583,12 +760,14 @@ describe('ReportPortal javascript client', () => { name: 'Test launch name', attributes: [{ value: 'value' }], }; - const client = new RPClient({ - apiKey: 'test', - project: 'test', - endpoint: 'https://abc.com', - attributes: [{ value: 'value' }], - }); + const client = asInternal( + new RPClient({ + apiKey: 'test', + project: 'test', + endpoint: 'https://abc.com', + attributes: [{ value: 'value' }], + }), + ); jest.spyOn(client.helpers, 'now').mockReturnValue(12345734); const mergeLaunches = client.getMergeLaunchesRequest(['12345', '12346']); @@ -607,13 +786,15 @@ describe('ReportPortal javascript client', () => { name: 'launch', attributes: [{ value: 'value' }], }; - const client = new RPClient({ - apiKey: 'test', - project: 'test', - endpoint: 'https://abc.com', - launch: 'launch', - attributes: [{ value: 'value' }], - }); + const client = asInternal( + new RPClient({ + apiKey: 'test', + project: 'test', + endpoint: 'https://abc.com', + launch: 'launch', + attributes: [{ value: 'value' }], + }), + ); jest.spyOn(client.helpers, 'now').mockReturnValue(12345734); const mergeLaunches = client.getMergeLaunchesRequest(['12345', '12346']); @@ -636,12 +817,14 @@ describe('ReportPortal javascript client', () => { }; it('should call rest client with required parameters', async () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - isLaunchMergeRequired: true, - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + isLaunchMergeRequired: true, + }), + ); const myPromise = Promise.resolve({ id: 'testidlaunch' }); jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); @@ -655,22 +838,27 @@ describe('ReportPortal javascript client', () => { const promise = client.mergeLaunches(); - expect(promise.then).toBeDefined(); + expect(promise!.then).toBeDefined(); await promise; expect(client.restClient.create).toHaveBeenCalledWith('launch/merge', fakeMergeDataRQ); }); it('should not call rest client if something went wrong', async () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - isLaunchMergeRequired: true, - }); + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + isLaunchMergeRequired: true, + }), + ); - jest.spyOn(client.helpers, 'readLaunchesFromFile').mockReturnValue('launchUUid'); - jest.spyOn(client.restClient, 'retrieveSyncAPI').mockResolvedValue(); - jest.spyOn(client.restClient, 'create').mockRejectedValue(); + // The original test returns a bare string instead of the declared string[]. + jest + .spyOn(client.helpers, 'readLaunchesFromFile') + .mockReturnValue('launchUUid' as unknown as string[]); + jest.spyOn(client.restClient, 'retrieveSyncAPI').mockResolvedValue(undefined); + jest.spyOn(client.restClient, 'create').mockRejectedValue(undefined); await client.mergeLaunches(); expect(client.restClient.create).not.toHaveBeenCalled(); @@ -692,19 +880,21 @@ describe('ReportPortal javascript client', () => { describe('getPromiseFinishAllItems', () => { it('should return promise', (done) => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id1: { children: ['child1'], }, child1: { promiseFinish: Promise.resolve(), }, - }; + }); const promise = client.getPromiseFinishAllItems('id1'); @@ -715,19 +905,21 @@ describe('ReportPortal javascript client', () => { describe('updateLaunch', () => { it('should call getRejectAnswer if there is no launchTempId with suitable launchTempId', () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id1: { children: ['child1'], }, - }; + }); jest.spyOn(client, 'getRejectAnswer').mockImplementation(); - client.updateLaunch('id2', { some: 'data' }); + client.updateLaunch('id2', { some: 'data' } as unknown as UpdateLaunchOptions); expect(client.getRejectAnswer).toHaveBeenCalledWith( 'id2', @@ -736,20 +928,22 @@ describe('ReportPortal javascript client', () => { }); it('should return object with tempId and promise', () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id1: { children: ['child1'], promiseFinish: Promise.resolve(), }, - }; - jest.spyOn(client.restClient, 'update').mockResolvedValue(); + }); + jest.spyOn(client.restClient, 'update').mockResolvedValue(undefined); - const result = client.updateLaunch('id1', { some: 'data' }); + const result = client.updateLaunch('id1', { some: 'data' } as unknown as UpdateLaunchOptions); expect(result.tempId).toEqual('id1'); return expect(result.promise).resolves.toBeUndefined(); @@ -758,19 +952,21 @@ describe('ReportPortal javascript client', () => { describe('startTestItem', () => { it('should call getRejectAnswer if there is no launchTempId with suitable launchTempId', () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id1: { children: ['child1'], }, - }; + }); jest.spyOn(client, 'getRejectAnswer').mockImplementation(); - client.startTestItem({}, 'id2'); + client.startTestItem({} as unknown as StartTestItemOptions, 'id2'); expect(client.getRejectAnswer).toHaveBeenCalledWith( 'id2', @@ -779,56 +975,66 @@ describe('ReportPortal javascript client', () => { }); it('should call getRejectAnswer if launchObj.finishSend is true', () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id1: { children: ['child1'], finishSend: true, }, - }; + }); jest.spyOn(client, 'getRejectAnswer').mockImplementation(); const error = new Error( 'Launch with tempId "id1" is already finished, you can not add an item to it', ); - client.startTestItem({}, 'id1'); + client.startTestItem({} as unknown as StartTestItemOptions, 'id1'); expect(client.getRejectAnswer).toHaveBeenCalledWith('id1', error); }); it('should call getRejectAnswer if there is no parentObj with suitable parentTempId', () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id: { children: ['id1'], }, id1: { children: ['child1'], }, - }; + }); jest.spyOn(client, 'getRejectAnswer').mockImplementation(); const error = new Error('Item with tempId "id3" not found'); - client.startTestItem({ testCaseId: 'testCaseId' }, 'id1', 'id3'); + client.startTestItem( + { testCaseId: 'testCaseId' } as unknown as StartTestItemOptions, + 'id1', + 'id3', + ); expect(client.getRejectAnswer).toHaveBeenCalledWith('id1', error); }); it('should return object with tempId and promise', () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id: { children: ['id1', '4n5pxq24kpiob12og9'], promiseStart: Promise.resolve(), @@ -840,24 +1046,30 @@ describe('ReportPortal javascript client', () => { '4n5pxq24kpiob12og9': { promiseStart: Promise.resolve(), }, - }; - jest.spyOn(client.itemRetriesChainMap, 'get').mockResolvedValue(); + }); + jest.spyOn(client.itemRetriesChainMap, 'get').mockResolvedValue(undefined); jest.spyOn(client.restClient, 'create').mockResolvedValue({}); jest.spyOn(client, 'getUniqId').mockReturnValue('4n5pxq24kpiob12og9'); - const result = client.startTestItem({ retry: false }, 'id1', 'id'); + const result = client.startTestItem( + { retry: false } as unknown as StartTestItemOptions, + 'id1', + 'id', + ); expect(result.tempId).toEqual('4n5pxq24kpiob12og9'); return expect(result.promise).resolves.toBeDefined(); }); it('should get previous try promise from itemRetriesChainMap if retry is true', async () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id: { children: ['id1', '4n5pxq24kpiob12og9'], promiseStart: Promise.resolve(), @@ -869,33 +1081,35 @@ describe('ReportPortal javascript client', () => { '4n5pxq24kpiob12og9': { promiseStart: Promise.resolve(), }, - }; + }); jest.spyOn(client, 'calculateItemRetriesChainMapKey').mockReturnValue('id1__name__'); jest.spyOn(client, 'getUniqId').mockReturnValue('4n5pxq24kpiob12og9'); jest.spyOn(client.itemRetriesChainMap, 'get').mockImplementation(); jest.spyOn(client.restClient, 'create').mockResolvedValue({}); - await client.startTestItem({ retry: true }, 'id1').promise; + await client.startTestItem({ retry: true } as unknown as StartTestItemOptions, 'id1').promise; expect(client.itemRetriesChainMap.get).toHaveBeenCalledWith('id1__name__'); }); it('should include retry_of with the previous item UUID when retry is true', async () => { - const client = new RPClient({ - apiKey: 'test', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); + const client = asInternal( + new RPClient({ + apiKey: 'test', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); const prevRealId = 'prev-item-uuid-1234'; const prevPromise = Promise.resolve({ id: prevRealId }); - client.map = { + client.map = asMap({ launchId: { children: [], finishSend: false, promiseStart: Promise.resolve(), }, - }; + }); const itemKey = client.calculateItemRetriesChainMapKey( 'launchId', undefined, 'My test', undefined, @@ -906,7 +1120,7 @@ describe('ReportPortal javascript client', () => { jest.spyOn(client, 'getUniqId').mockReturnValue('newTempId'); await client.startTestItem( - { name: 'My test', type: 'STEP', retry: true }, + { name: 'My test', type: 'STEP', retry: true } as unknown as StartTestItemOptions, 'launchId', ).promise; @@ -917,23 +1131,25 @@ describe('ReportPortal javascript client', () => { }); it('should not include retry_of when retry is true but no previous entry exists', async () => { - const client = new RPClient({ - apiKey: 'test', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'test', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ launchId: { children: [], finishSend: false, promiseStart: Promise.resolve(), }, - }; + }); jest.spyOn(client.restClient, 'create').mockResolvedValue({ id: 'new-item-uuid' }); jest.spyOn(client, 'getUniqId').mockReturnValue('newTempId'); await client.startTestItem( - { name: 'My test', type: 'STEP', retry: true }, + { name: 'My test', type: 'STEP', retry: true } as unknown as StartTestItemOptions, 'launchId', ).promise; @@ -944,19 +1160,21 @@ describe('ReportPortal javascript client', () => { }); it('should not include retry_of when retry is false', async () => { - const client = new RPClient({ - apiKey: 'test', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); + const client = asInternal( + new RPClient({ + apiKey: 'test', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); const prevPromise = Promise.resolve({ id: 'prev-item-uuid-1234' }); - client.map = { + client.map = asMap({ launchId: { children: [], finishSend: false, promiseStart: Promise.resolve(), }, - }; + }); const itemKey = client.calculateItemRetriesChainMapKey( 'launchId', undefined, 'My test', undefined, ); @@ -966,7 +1184,7 @@ describe('ReportPortal javascript client', () => { jest.spyOn(client, 'getUniqId').mockReturnValue('newTempId'); await client.startTestItem( - { name: 'My test', type: 'STEP', retry: false }, + { name: 'My test', type: 'STEP', retry: false } as unknown as StartTestItemOptions, 'launchId', ).promise; @@ -979,16 +1197,18 @@ describe('ReportPortal javascript client', () => { describe('finishTestItem', () => { it('should call getRejectAnswer if there is no itemObj with suitable itemTempId', () => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id1: { children: ['child1'], }, - }; + }); jest.spyOn(client, 'getRejectAnswer').mockImplementation(); client.finishTestItem('id2', {}); @@ -1000,12 +1220,14 @@ describe('ReportPortal javascript client', () => { }); it('should call finishTestItemPromiseStart with correct parameters', (done) => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id: { children: ['id1'], promiseFinish: Promise.resolve(), @@ -1014,7 +1236,7 @@ describe('ReportPortal javascript client', () => { children: ['child1'], promiseFinish: Promise.resolve(), }, - }; + }); client.launchUuid = 'launchUuid'; jest.spyOn(client, 'cleanMap').mockImplementation(); jest.spyOn(client, 'finishTestItemPromiseStart').mockImplementation(); @@ -1034,12 +1256,14 @@ describe('ReportPortal javascript client', () => { }); it('should call finishTestItemPromiseStart with correct parameters if smt went wrong', (done) => { - const client = new RPClient({ - apiKey: 'startLaunchTest', - endpoint: 'https://rp.us/api/v1', - project: 'tst', - }); - client.map = { + const client = asInternal( + new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }), + ); + client.map = asMap({ id: { children: ['id1'], promiseFinish: Promise.resolve(), @@ -1048,7 +1272,7 @@ describe('ReportPortal javascript client', () => { children: ['child1'], promiseFinish: Promise.reject(), }, - }; + }); client.launchUuid = 'launchUuid'; jest.spyOn(client, 'cleanMap').mockImplementation(); jest.spyOn(client, 'finishTestItemPromiseStart').mockImplementation(); @@ -1069,33 +1293,36 @@ describe('ReportPortal javascript client', () => { }); it('should automatically add NOT_ISSUE when status is SKIPPED and skippedIsNotIssue is true', function (done) { - const mockClient = new RPClient( - { - apiKey: 'test', - endpoint: 'https://reportportal-stub-url', - launch: 'test launch', - project: 'test project', - skippedIsNotIssue: true, - }, - { name: 'test', version: '1.0.0' }, + const mockClient = asInternal( + new RPClient( + { + apiKey: 'test', + endpoint: 'https://reportportal-stub-url', + launch: 'test launch', + project: 'test project', + skippedIsNotIssue: true, + }, + { name: 'test', version: '1.0.0' }, + ), ); const spyFinishTestItemPromiseStart = jest .spyOn(mockClient, 'finishTestItemPromiseStart') .mockImplementation(() => {}); - mockClient.map = { + mockClient.map = asMap({ testItemId: { children: [], finishSend: false, promiseFinish: Promise.resolve(), resolveFinish: () => {}, }, - }; + }); + // The original test uses the raw 'skipped' string rather than the STATUSES enum member. const finishTestItemRQ = { status: 'skipped', - }; + } as unknown as FinishTestItemOptions; mockClient.finishTestItem('testItemId', finishTestItemRQ); @@ -1117,33 +1344,36 @@ describe('ReportPortal javascript client', () => { }); it('should not add NOT_ISSUE when status is SKIPPED and skippedIsNotIssue is false', function (done) { - const mockClient = new RPClient( - { - apiKey: 'test', - endpoint: 'https://reportportal-stub-url', - launch: 'test launch', - project: 'test project', - skippedIsNotIssue: false, - }, - { name: 'test', version: '1.0.0' }, + const mockClient = asInternal( + new RPClient( + { + apiKey: 'test', + endpoint: 'https://reportportal-stub-url', + launch: 'test launch', + project: 'test project', + skippedIsNotIssue: false, + }, + { name: 'test', version: '1.0.0' }, + ), ); const spyFinishTestItemPromiseStart = jest .spyOn(mockClient, 'finishTestItemPromiseStart') .mockImplementation(() => {}); - mockClient.map = { + mockClient.map = asMap({ testItemId: { children: [], finishSend: false, promiseFinish: Promise.resolve(), resolveFinish: () => {}, }, - }; + }); + // The original test uses the raw 'skipped' string rather than the STATUSES enum member. const finishTestItemRQ = { status: 'skipped', - }; + } as unknown as FinishTestItemOptions; mockClient.finishTestItem('testItemId', finishTestItemRQ); @@ -1172,22 +1402,26 @@ describe('ReportPortal javascript client', () => { describe('saveLog', () => { it('should return object with tempId and promise', () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - client.map = { + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + client.map = asMap({ id1: { children: ['child1'], }, - }; + }); jest.spyOn(client, 'getUniqId').mockReturnValue('4n5pxq24kpiob12og9'); - jest.spyOn(client.restClient, 'create').mockResolvedValue(); + jest.spyOn(client.restClient, 'create').mockResolvedValue(undefined); const result = client.saveLog( { promiseStart: Promise.resolve(), realId: 'realId', children: [], - }, - client.restClient.create, + } as unknown as ItemObj, + // The original test passes `restClient.create` directly; its signature differs from the + // (itemUuid, launchUuid) request function saveLog expects, but it's mocked out anyway. + client.restClient.create as unknown as RequestPromiseFunc, ); expect(result.tempId).toEqual('4n5pxq24kpiob12og9'); @@ -1197,17 +1431,29 @@ describe('ReportPortal javascript client', () => { describe('sendLog', () => { it('should return sendLogWithFile if fileObj is not empty', () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - jest.spyOn(client, 'sendLogWithFile').mockReturnValue('sendLogWithFile'); + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + jest + .spyOn(client, 'sendLogWithFile') + .mockReturnValue('sendLogWithFile' as unknown as ClientResponse); - const result = client.sendLog('itemTempId', { message: 'message' }, { name: 'name' }); + const result = client.sendLog( + 'itemTempId', + { message: 'message' }, + { name: 'name' } as unknown as Attachment, + ); expect(result).toEqual('sendLogWithFile'); }); it('should return sendLogWithoutFile if fileObj is empty', () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - jest.spyOn(client, 'sendLogWithoutFile').mockReturnValue('sendLogWithoutFile'); + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + jest + .spyOn(client, 'sendLogWithoutFile') + .mockReturnValue('sendLogWithoutFile' as unknown as ClientResponse); const result = client.sendLog('itemTempId', { message: 'message' }); @@ -1217,12 +1463,14 @@ describe('ReportPortal javascript client', () => { describe('sendLogWithoutFile', () => { it('should call getRejectAnswer if there is no itemObj with suitable itemTempId', () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - client.map = { + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + client.map = asMap({ id1: { children: ['child1'], }, - }; + }); jest.spyOn(client, 'getRejectAnswer').mockImplementation(); client.sendLogWithoutFile('itemTempId', {}); @@ -1234,13 +1482,15 @@ describe('ReportPortal javascript client', () => { }); it('should return saveLog function', () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - client.map = { + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + client.map = asMap({ itemTempId: { children: ['child1'], }, - }; - jest.spyOn(client, 'saveLog').mockReturnValue('saveLog'); + }); + jest.spyOn(client, 'saveLog').mockReturnValue('saveLog' as unknown as ClientResponse); const result = client.sendLogWithoutFile('itemTempId', {}); @@ -1250,14 +1500,17 @@ describe('ReportPortal javascript client', () => { describe('sendLogWithFile', () => { it('should call getRejectAnswer if there is no itemObj with suitable itemTempId', () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - client.map = { + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + client.map = asMap({ id1: { children: ['child1'], }, - }; + }); jest.spyOn(client, 'getRejectAnswer').mockImplementation(); + // The original test calls this without the (required) fileObj argument. client.sendLogWithFile('itemTempId', {}); expect(client.getRejectAnswer).toHaveBeenCalledWith( @@ -1267,13 +1520,15 @@ describe('ReportPortal javascript client', () => { }); it('should return saveLog function', () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - client.map = { + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + client.map = asMap({ itemTempId: { children: ['child1'], }, - }; - jest.spyOn(client, 'saveLog').mockReturnValue('saveLog'); + }); + jest.spyOn(client, 'saveLog').mockReturnValue('saveLog' as unknown as ClientResponse); const result = client.sendLogWithFile('itemTempId', {}); @@ -1283,31 +1538,39 @@ describe('ReportPortal javascript client', () => { describe('getRequestLogWithFile', () => { it('should return restClient.create', () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - client.map = { + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + client.map = asMap({ id1: { children: ['child1'], }, - }; - jest.spyOn(client, 'buildMultiPartStream').mockReturnValue(); + }); + jest + .spyOn(client, 'buildMultiPartStream') + .mockReturnValue(undefined as unknown as Buffer); jest.spyOn(client.restClient, 'create').mockResolvedValue('value'); - const result = client.getRequestLogWithFile({}, { name: 'name' }); + const result = client.getRequestLogWithFile({}, { name: 'name' } as unknown as Attachment); return expect(result).resolves.toBe('value'); }); it('should return restClient.create with error', () => { - const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); - client.map = { + const client = asInternal( + new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }), + ); + client.map = asMap({ id1: { children: ['child1'], }, - }; - jest.spyOn(client, 'buildMultiPartStream').mockReturnValue(); - jest.spyOn(client.restClient, 'create').mockRejectedValue(); + }); + jest + .spyOn(client, 'buildMultiPartStream') + .mockReturnValue(undefined as unknown as Buffer); + jest.spyOn(client.restClient, 'create').mockRejectedValue(undefined); - const result = client.getRequestLogWithFile({}, { name: 'name' }); + const result = client.getRequestLogWithFile({}, { name: 'name' } as unknown as Attachment); expect(result.catch).toBeDefined(); }); diff --git a/__tests__/rest.spec.js b/__tests__/rest.spec.ts similarity index 73% rename from __tests__/rest.spec.js rename to __tests__/rest.spec.ts index 352d70c9..174ed6fb 100644 --- a/__tests__/rest.spec.js +++ b/__tests__/rest.spec.ts @@ -1,9 +1,32 @@ -const nock = require('nock'); -const isEqual = require('lodash/isEqual'); -const http = require('http'); -const RestClient = require('../src/rest'); -const OAuthInterceptor = require('../src/oauth'); -const logger = require('../src/logger'); +import nock from 'nock'; +import isEqual from 'lodash/isEqual'; +import http from 'http'; +import type { AxiosInstance, AxiosRequestConfig } from 'axios'; +import type { IAxiosRetryConfig } from 'axios-retry'; +import RestClient from '../src/rest'; +import OAuthInterceptor from '../src/oauth'; +import * as logger from '../src/logger'; +import type { RestClientConfig, RestClientOptions } from '../src/models/config'; + +// `baseURL` / `headers` / `restClientConfig` / `axiosInstance` are private on RestClient, and +// `getRestConfig` returns a plain stripped-down object rather than a true AxiosRequestConfig - +// this gives the tests typed access to that internal shape without loosening the real API. +interface RestClientInternal { + baseURL: string; + headers?: Record; + restClientConfig?: RestClientConfig; + axiosInstance: AxiosInstance; + buildPath(path?: string): string; + getRestConfig(): Record; + getRetryConfig(): IAxiosRetryConfig; + retrieve(path: string, options?: AxiosRequestConfig): Promise; + create(path: string, data: unknown, options?: AxiosRequestConfig): Promise; + update(path: string, data: unknown, options?: AxiosRequestConfig): Promise; + delete(path: string, data: unknown, options?: AxiosRequestConfig): Promise; + retrieveSyncAPI(path: string, options?: AxiosRequestConfig): Promise; +} +const asInternal = (client: RestClient): RestClientInternal => + client as unknown as RestClientInternal; describe('RestClient', () => { const originalEnv = process.env; @@ -24,7 +47,7 @@ describe('RestClient', () => { process.env = originalEnv; }); - const options = { + const options: RestClientOptions = { baseURL: 'http://report-portal-host:8080/api/v1', headers: { 'User-Agent': 'NodeJS', @@ -37,16 +60,18 @@ describe('RestClient', () => { timeout: 0, }, }; - const noOptions = {}; - const getRetryAttempts = (client) => client.getRetryConfig().retries + 1; - const restClient = new RestClient(options); - const restClientNoRetry = new RestClient({ - ...options, - restClientConfig: { - ...options.restClientConfig, - retry: 0, - }, - }); + const noOptions: AxiosRequestConfig = {}; + const getRetryAttempts = (client: RestClientInternal) => client.getRetryConfig().retries! + 1; + const restClient = asInternal(new RestClient(options)); + const restClientNoRetry = asInternal( + new RestClient({ + ...options, + restClientConfig: { + ...options.restClientConfig, + retry: 0, + }, + }), + ); const retryAttempts = getRetryAttempts(restClient); const unathorizedError = { @@ -68,29 +93,31 @@ describe('RestClient', () => { it('adds Logger to axios instance if enabled', () => { const spyLogger = jest.spyOn(logger, 'addLogger').mockReturnValue(); - const optionsWithLoggerEnabled = { + const optionsWithLoggerEnabled: RestClientOptions = { ...options, restClientConfig: { ...options.restClientConfig, debug: true, }, }; - const client = new RestClient(optionsWithLoggerEnabled); + const client = asInternal(new RestClient(optionsWithLoggerEnabled)); expect(spyLogger).toHaveBeenCalledWith(client.axiosInstance); }); it('attaches an OAuth interceptor to the axios instance when oauthConfig is provided', () => { const attachSpy = jest.spyOn(OAuthInterceptor.prototype, 'attach'); - const client = new RestClient({ - ...options, - oauthConfig: { - tokenEndpoint: 'https://auth.example.com/oauth/token', - username: 'user', - password: 'password', - clientId: 'client-id', - }, - }); + const client = asInternal( + new RestClient({ + ...options, + oauthConfig: { + tokenEndpoint: 'https://auth.example.com/oauth/token', + username: 'user', + password: 'password', + clientId: 'client-id', + }, + }), + ); expect(attachSpy).toHaveBeenCalledWith(client.axiosInstance); @@ -104,28 +131,30 @@ describe('RestClient', () => { const mathRandomSpy = jest.spyOn(Math, 'random').mockImplementationOnce(() => 0); expect(retryConfig.retries).toBe(6); - expect(retryAttempts).toBe(retryConfig.retries + 1); + expect(retryAttempts).toBe(retryConfig.retries! + 1); expect(retryConfig.shouldResetTimeout).toBe(true); - expect(retryConfig.retryDelay(1)).toBe(200); + expect(retryConfig.retryDelay!(1, undefined as never)).toBe(200); mathRandomSpy.mockImplementationOnce(() => 1); - expect(retryConfig.retryDelay(4)).toBeCloseTo(1600 * 0.6); + expect(retryConfig.retryDelay!(4, undefined as never)).toBeCloseTo(1600 * 0.6); mathRandomSpy.mockImplementationOnce(() => 0); - expect(retryConfig.retryDelay(10)).toBe(5000); + expect(retryConfig.retryDelay!(10, undefined as never)).toBe(5000); mathRandomSpy.mockRestore(); }); it('uses custom retry attempts when a numeric value is provided', (done) => { const customRetries = 2; - const client = new RestClient({ - ...options, - restClientConfig: { - ...options.restClientConfig, - retry: customRetries, - }, - }); + const client = asInternal( + new RestClient({ + ...options, + restClientConfig: { + ...options.restClientConfig, + retry: customRetries, + }, + }), + ); expect(getRetryAttempts(client)).toBe(customRetries + 1); const scope = nock(options.baseURL) @@ -133,7 +162,7 @@ describe('RestClient', () => { .times(getRetryAttempts(client)) .replyWithError(netErrConnectionResetError); - client.retrieve('users/custom-retry-number', noOptions).catch((error) => { + client.retrieve('users/custom-retry-number', noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(netErrConnectionResetError.message); expect(scope.isDone()).toBeTruthy(); @@ -144,17 +173,19 @@ describe('RestClient', () => { it('merges retry configuration object from settings', () => { const customDelay = () => 250; - const client = new RestClient({ - ...options, - restClientConfig: { - ...options.restClientConfig, - retry: { - retries: 4, - retryDelay: customDelay, - shouldResetTimeout: true, + const client = asInternal( + new RestClient({ + ...options, + restClientConfig: { + ...options.restClientConfig, + retry: { + retries: 4, + retryDelay: customDelay, + shouldResetTimeout: true, + }, }, - }, - }); + }), + ); const retryConfig = client.getRetryConfig(); @@ -168,24 +199,29 @@ describe('RestClient', () => { const timeoutError = { message: 'timeout of 1ms exceeded', }; - expect(retryConfig.retryCondition(timeoutError)).toBe(true); + expect(retryConfig.retryCondition!(timeoutError as never)).toBe(true); }); it('handles undefined restClientConfig without crashing during retries', () => { - const client = new RestClient({ - baseURL: options.baseURL, - headers: options.headers, - restClientConfig: undefined, - }); + const client = asInternal( + new RestClient({ + baseURL: options.baseURL, + headers: options.headers, + restClientConfig: undefined, + }), + ); const retryConfig = client.getRetryConfig(); expect(retryConfig.retries).toBe(6); const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - const onRetry = retryConfig.onRetry; - + const onRetry = retryConfig.onRetry!; + expect(() => { - onRetry(1, { code: 'ECONNABORTED' }, { method: 'GET', url: 'http://test.com' }); + onRetry(1, { code: 'ECONNABORTED' } as never, { + method: 'GET', + url: 'http://test.com', + } as never); }).not.toThrow(); consoleSpy.mockRestore(); @@ -202,24 +238,28 @@ describe('RestClient', () => { describe('getRestConfig', () => { it("return {} in case agent property doesn't exist", () => { - const client = new RestClient({ - ...options, - restClientConfig: {}, - }); + const client = asInternal( + new RestClient({ + ...options, + restClientConfig: {}, + }), + ); expect(client.getRestConfig()).toEqual({}); }); it('creates object with correct properties with http(s) agent', () => { - const client = new RestClient({ - ...options, - restClientConfig: { - agent: { - rejectUnauthorized: false, + const client = asInternal( + new RestClient({ + ...options, + restClientConfig: { + agent: { + rejectUnauthorized: false, + }, + timeout: 10000, }, - timeout: 10000, - }, - }); + }), + ); const config = client.getRestConfig(); @@ -247,7 +287,7 @@ describe('RestClient', () => { it('catches NETWORK errors', (done) => { const scope = nock(options.baseURL).get('/users').replyWithError(netErrConnectionResetError); - restClientNoRetry.retrieve('users', noOptions).catch((error) => { + restClientNoRetry.retrieve('users', noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(netErrConnectionResetError.message); expect(scope.isDone()).toBeTruthy(); @@ -259,7 +299,7 @@ describe('RestClient', () => { it('catches API errors', (done) => { const scope = nock(options.baseURL).get('/users').reply(403, unathorizedError); - restClientNoRetry.retrieve('users', noOptions).catch((error) => { + restClientNoRetry.retrieve('users', noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(unauthorizedErrorMessage); expect(scope.isDone()).toBeTruthy(); @@ -293,7 +333,7 @@ describe('RestClient', () => { .post('/users', (body) => isEqual(body, newUser)) .replyWithError(netErrConnectionResetError); - restClientNoRetry.create('users', newUser, noOptions).catch((error) => { + restClientNoRetry.create('users', newUser, noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(netErrConnectionResetError.message); expect(scope.isDone()).toBeTruthy(); @@ -309,7 +349,7 @@ describe('RestClient', () => { .post('/users', (body) => isEqual(body, newUser)) .reply(403, unathorizedError); - restClientNoRetry.create('users', newUser, noOptions).catch((error) => { + restClientNoRetry.create('users', newUser, noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(unauthorizedErrorMessage); expect(scope.isDone()).toBeTruthy(); @@ -343,7 +383,7 @@ describe('RestClient', () => { .put('/users/1', (body) => isEqual(body, newUserInfo)) .replyWithError(netErrConnectionResetError); - restClientNoRetry.update('users/1', newUserInfo, noOptions).catch((error) => { + restClientNoRetry.update('users/1', newUserInfo, noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(netErrConnectionResetError.message); expect(scope.isDone()).toBeTruthy(); @@ -359,7 +399,7 @@ describe('RestClient', () => { .put('/users/1', (body) => isEqual(body, newUserInfo)) .reply(403, unathorizedError); - restClientNoRetry.update('users/1', newUserInfo, noOptions).catch((error) => { + restClientNoRetry.update('users/1', newUserInfo, noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(unauthorizedErrorMessage); expect(scope.isDone()).toBeTruthy(); @@ -391,7 +431,7 @@ describe('RestClient', () => { .delete('/users/1') .replyWithError(netErrConnectionResetError); - restClientNoRetry.delete('users/1', emptyBody, noOptions).catch((error) => { + restClientNoRetry.delete('users/1', emptyBody, noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(netErrConnectionResetError.message); expect(scope.isDone()).toBeTruthy(); @@ -405,7 +445,7 @@ describe('RestClient', () => { const scope = nock(options.baseURL).delete('/users/1').reply(403, unathorizedError); - restClientNoRetry.delete('users/1', emptyBody, noOptions).catch((error) => { + restClientNoRetry.delete('users/1', emptyBody, noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(unauthorizedErrorMessage); expect(scope.isDone()).toBeTruthy(); @@ -432,7 +472,7 @@ describe('RestClient', () => { it('catches NETWORK errors', (done) => { const scope = nock(options.baseURL).get('/users').replyWithError(netErrConnectionResetError); - restClientNoRetry.retrieveSyncAPI('users', noOptions).catch((error) => { + restClientNoRetry.retrieveSyncAPI('users', noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(netErrConnectionResetError.message); expect(scope.isDone()).toBeTruthy(); @@ -444,7 +484,7 @@ describe('RestClient', () => { it('catches API errors', (done) => { const scope = nock(options.baseURL).get('/users').reply(403, unathorizedError); - restClientNoRetry.retrieveSyncAPI('users', noOptions).catch((error) => { + restClientNoRetry.retrieveSyncAPI('users', noOptions).catch((error: Error) => { expect(error instanceof Error).toBeTruthy(); expect(error.message).toMatch(unauthorizedErrorMessage); expect(scope.isDone()).toBeTruthy(); diff --git a/__tests__/statistics.spec.js b/__tests__/statistics.spec.ts similarity index 68% rename from __tests__/statistics.spec.js rename to __tests__/statistics.spec.ts index 0f2047a0..46689fe8 100644 --- a/__tests__/statistics.spec.js +++ b/__tests__/statistics.spec.ts @@ -1,10 +1,11 @@ -const axios = require('axios'); -const Statistics = require('../src/statistics/statistics'); -const { MEASUREMENT_ID, API_KEY } = require('../src/statistics/constants'); +import axios, { AxiosResponse } from 'axios'; +import Statistics from '../src/statistics/statistics'; +import { MEASUREMENT_ID, API_KEY } from '../src/statistics/constants'; +import type { AgentParams } from '../src/models/common'; const uuidv4Validation = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; -const agentParams = { +const agentParams: AgentParams = { name: 'AgentName', version: 'AgentVersion', }; @@ -42,41 +43,36 @@ const agentRequestValidation = expect.objectContaining({ events: expect.arrayContaining([expect.objectContaining(agentEventValidationObject)]), }); +// The client doesn't do anything with the resolved response, it just awaits it not throwing - +// so a minimal stand-in cast to AxiosResponse is enough to satisfy the mock's return type. +const fakeAxiosResponse = { send: () => {} } as unknown as AxiosResponse; + describe('Statistics', () => { afterEach(() => { jest.clearAllMocks(); }); it('should send proper event to axios', async () => { - jest.spyOn(axios, 'post').mockReturnValue({ - send: () => {}, // eslint-disable-line - }); + const postSpy = jest.spyOn(axios, 'post').mockResolvedValue(fakeAxiosResponse); const statistics = new Statistics(eventName, agentParams); await statistics.trackEvent(); - expect(axios.post).toHaveBeenCalledTimes(1); - expect(axios.post).toHaveBeenCalledWith(url, agentRequestValidation); + expect(postSpy).toHaveBeenCalledTimes(1); + expect(postSpy).toHaveBeenCalledWith(url, agentRequestValidation); }); - [ - undefined, - {}, - { - name: null, - version: null, - }, - ].forEach((params) => { + ( + [undefined, {}, { name: null, version: null }] as unknown as (AgentParams | undefined)[] + ).forEach((params) => { it(`should not fail if agent params: ${JSON.stringify(params)}`, async () => { - jest.spyOn(axios, 'post').mockReturnValue({ - send: () => {}, // eslint-disable-line - }); + const postSpy = jest.spyOn(axios, 'post').mockResolvedValue(fakeAxiosResponse); const statistics = new Statistics(eventName, params); await statistics.trackEvent(); - expect(axios.post).toHaveBeenCalledTimes(1); - expect(axios.post).toHaveBeenCalledWith(url, baseRequestValidation); + expect(postSpy).toHaveBeenCalledTimes(1); + expect(postSpy).toHaveBeenCalledWith(url, baseRequestValidation); }); it('Should properly handle errors if any', async () => { @@ -94,16 +90,14 @@ describe('Statistics', () => { describe('setInstanceID', () => { it('should set instanceID in event params', async () => { - jest.spyOn(axios, 'post').mockReturnValue({ - send: () => {}, // eslint-disable-line - }); + const postSpy = jest.spyOn(axios, 'post').mockResolvedValue(fakeAxiosResponse); const statistics = new Statistics(eventName, agentParams); statistics.setInstanceID('test-instance-id'); await statistics.trackEvent(); - expect(axios.post).toHaveBeenCalledTimes(1); - expect(axios.post).toHaveBeenCalledWith( + expect(postSpy).toHaveBeenCalledTimes(1); + expect(postSpy).toHaveBeenCalledWith( url, expect.objectContaining({ events: expect.arrayContaining([ @@ -118,15 +112,13 @@ describe('Statistics', () => { }); it('should not include instanceID if setInstanceID was not called', async () => { - jest.spyOn(axios, 'post').mockReturnValue({ - send: () => {}, // eslint-disable-line - }); + const postSpy = jest.spyOn(axios, 'post').mockResolvedValue(fakeAxiosResponse); const statistics = new Statistics(eventName, agentParams); await statistics.trackEvent(); - expect(axios.post).toHaveBeenCalledTimes(1); - const callArgs = axios.post.mock.calls[0][1]; + expect(postSpy).toHaveBeenCalledTimes(1); + const callArgs = postSpy.mock.calls[0][1] as { events: { params: Record }[] }; expect(callArgs.events[0].params).not.toHaveProperty('instanceID'); }); }); diff --git a/jest.config.js b/jest.config.js index 1fa7326a..d8bc89ba 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,10 +1,9 @@ module.exports = { transform: { - '^.+\\.ts$': ['ts-jest', { diagnostics: false, tsconfig: 'tsconfig.json' }], - '^.+\\.js$': 'babel-jest', + '^.+\\.ts$': ['ts-jest', { tsconfig: 'tsconfig.spec.json' }], }, moduleFileExtensions: ['ts', 'js', 'json'], - testRegex: '/__tests__/.*\\.(test|spec).js$', + testRegex: '/__tests__/.*\\.(test|spec).ts$', testEnvironment: 'node', collectCoverageFrom: [ 'src/**/*.ts', diff --git a/package-lock.json b/package-lock.json index b348d265..647a6311 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.25", "@types/node": "^18.19.8", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "^5.62.0", @@ -1408,6 +1409,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "18.19.130", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", diff --git a/package.json b/package.json index 1390a56a..ced0f027 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,14 @@ "author": "ReportPortal.io", "scripts": { "codegraph": "bash scripts/codegraph.sh", + "typecheck": "tsc --noEmit", "build": "npm run clean && tsc && npm run facades", "facades": "node scripts/generate-resolver-facades.js", "clean": "node scripts/generate-resolver-facades.js --clean && rimraf ./build", "lint": "eslint \"src/**/*.ts\"", "format": "npm run lint -- --fix", - "test": "jest", - "test:coverage": "jest --coverage", + "test": "npm run typecheck && jest", + "test:coverage": "npm run typecheck && jest --coverage", "prepublishOnly": "npm run build" }, "files": [ @@ -70,11 +71,21 @@ }, "typesVersions": { "*": { - "constants": ["./build/constants/index.d.ts"], - "models": ["./build/models/index.d.ts"], - "helpers": ["./build/helpers.d.ts"], - "publicReportingAPI": ["./build/publicReportingAPI.d.ts"], - "lib/*": ["./build/*"] + "constants": [ + "./build/constants/index.d.ts" + ], + "models": [ + "./build/models/index.d.ts" + ], + "helpers": [ + "./build/helpers.d.ts" + ], + "publicReportingAPI": [ + "./build/publicReportingAPI.d.ts" + ], + "lib/*": [ + "./build/*" + ] } }, "engines": { @@ -92,6 +103,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.25", "@types/node": "^18.19.8", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "^5.62.0", diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 00000000..33eb27ca --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true, + "declaration": false, + "resolveJsonModule": true + }, + "include": ["src/**/*", "__tests__/**/*"] +} From 47b252060eda9fb06d0a69e2cfc1ada47555f85a Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Tue, 1 Sep 2026 13:27:43 +0400 Subject: [PATCH 11/14] Preserve valid config options when getClientConfig fails --- src/commons/config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commons/config.ts b/src/commons/config.ts index 3f3bc2f0..5f46844c 100644 --- a/src/commons/config.ts +++ b/src/commons/config.ts @@ -83,7 +83,10 @@ const DEFAULT_CLIENT_CONFIG: NormalizedClientConfig = { }; export const getClientConfig = (options: ReportPortalConfig): NormalizedClientConfig => { - let calculatedOptions = DEFAULT_CLIENT_CONFIG; + let calculatedOptions: NormalizedClientConfig = + options && typeof options === 'object' + ? ({ ...DEFAULT_CLIENT_CONFIG, ...options } as NormalizedClientConfig) + : DEFAULT_CLIENT_CONFIG; try { if (typeof options !== 'object') { throw new ReportPortalValidationError('`options` must be an object.'); From 2b0041ecffe4470425381b1201a275ac1e22bf8a Mon Sep 17 00:00:00 2001 From: maria-hambardzumian <164881199+maria-hambardzumian@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:07:24 +0400 Subject: [PATCH 12/14] Refactor getClientConfig to simplify options handling Simplified the initialization of calculatedOptions to always start with DEFAULT_CLIENT_CONFIG. --- src/commons/config.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/commons/config.ts b/src/commons/config.ts index 5f46844c..739d2f05 100644 --- a/src/commons/config.ts +++ b/src/commons/config.ts @@ -83,10 +83,8 @@ const DEFAULT_CLIENT_CONFIG: NormalizedClientConfig = { }; export const getClientConfig = (options: ReportPortalConfig): NormalizedClientConfig => { - let calculatedOptions: NormalizedClientConfig = - options && typeof options === 'object' - ? ({ ...DEFAULT_CLIENT_CONFIG, ...options } as NormalizedClientConfig) - : DEFAULT_CLIENT_CONFIG; + let calculatedOptions = DEFAULT_CLIENT_CONFIG; + try { if (typeof options !== 'object') { throw new ReportPortalValidationError('`options` must be an object.'); From e3e9a24fa96ebfffad81ac862be515336826f92e Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Fri, 4 Sep 2026 13:11:30 +0400 Subject: [PATCH 13/14] Format fix --- src/commons/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/config.ts b/src/commons/config.ts index 739d2f05..25634e77 100644 --- a/src/commons/config.ts +++ b/src/commons/config.ts @@ -84,7 +84,7 @@ const DEFAULT_CLIENT_CONFIG: NormalizedClientConfig = { export const getClientConfig = (options: ReportPortalConfig): NormalizedClientConfig => { let calculatedOptions = DEFAULT_CLIENT_CONFIG; - + try { if (typeof options !== 'object') { throw new ReportPortalValidationError('`options` must be an object.'); From 583628c0e61d5f6ad607ce768fd0c3cf0d724d65 Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Fri, 4 Sep 2026 13:49:32 +0400 Subject: [PATCH 14/14] Update Node.js engine requirement to version 16.0.0 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ced0f027..0c90b358 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ } }, "engines": { - "node": ">=14.17.0" + "node": ">=16.0.0" }, "dependencies": { "axios": "^1.15.2",