From 213e89b45c41cafbf731ac0c425c1665cb708596 Mon Sep 17 00:00:00 2001 From: "reportportal.io" Date: Wed, 29 Oct 2025 16:42:42 +0000 Subject: [PATCH 1/9] 5.3.2 -> 5.3.3-SNAPSHOT --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 84197c8..93b90b0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.3.2 +5.3.3-SNAPSHOT From acfc9e3e179de9bc5557be42252bf11e4ee9aaa2 Mon Sep 17 00:00:00 2001 From: Ilya Date: Mon, 3 Nov 2025 13:06:11 +0100 Subject: [PATCH 2/9] Update publish.yml --- .github/workflows/publish.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5bdfb22..06dc865 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,6 +17,10 @@ on: repository_dispatch: types: [version-released] +permissions: + id-token: write # Required for OIDC + contents: read + jobs: build: runs-on: ubuntu-latest From 83234901bf0bea8a02303c8fb033128190615c18 Mon Sep 17 00:00:00 2001 From: Ilya_Hancharyk Date: Mon, 3 Nov 2025 15:57:58 +0100 Subject: [PATCH 3/9] EPMRPP-109260 || Update workflows. Add NPM OIDC support --- .github/workflows/CI-pipeline.yml | 15 ++++++++++----- .github/workflows/publish.yml | 25 +++++++++++-------------- .github/workflows/release.yml | 10 +++++----- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/.github/workflows/CI-pipeline.yml b/.github/workflows/CI-pipeline.yml index 3af84ed..e5dd8e1 100644 --- a/.github/workflows/CI-pipeline.yml +++ b/.github/workflows/CI-pipeline.yml @@ -1,4 +1,4 @@ -# Copyright 2024 EPAM Systems +# Copyright 2025 EPAM Systems # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -34,16 +34,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 24 + - name: Install dependencies run: npm install + - name: Build the source code run: npm run build + - name: Run lint run: npm run lint + - name: Run tests and check coverage run: npm run test:coverage diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 06dc865..559b709 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -# Copyright 2024 EPAM Systems +# Copyright 2025 EPAM Systems # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -18,19 +18,19 @@ on: types: [version-released] permissions: - id-token: write # Required for OIDC - contents: read + id-token: write # Required for NPM OIDC + contents: write jobs: build: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 24 - name: Install dependencies run: npm install - name: Build the source code @@ -45,11 +45,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 24 registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: npm install @@ -57,15 +57,12 @@ jobs: run: npm run build - name: Publish to NPM run: | - npm config set //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN npm config list npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 24 registry-url: 'https://npm.pkg.github.com' scope: '@reportportal' - name: Publish to GPR diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31a730e..e10fb4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -# Copyright 2024 EPAM Systems +# Copyright 2025 EPAM Systems # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -33,7 +33,7 @@ jobs: releaseVersion: ${{ steps.exposeVersion.outputs.releaseVersion }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Read version id: readVersion run: | @@ -78,9 +78,9 @@ jobs: versionInfo: ${{ steps.readChangelogEntry.outputs.log_entry }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '12' - name: Configure git @@ -139,7 +139,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Create Release id: createRelease uses: actions/create-release@v1 From f276347a1d0e0291d7ff9c7f617508478abf1050 Mon Sep 17 00:00:00 2001 From: maria-hambardzumian <164881199+maria-hambardzumian@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:20:04 +0400 Subject: [PATCH 4/9] EPMRPP-109177 || Add custom log levels support for Playwright agent (#198) * EPMRPP-109177 || Add custom log levels support for Playwright agent * EPMRPP-109177 || crf - 1 * EPMRPP-109177 || format fix * EPMRPP-109177 || code fix --------- Co-authored-by: maria-hambardzumian --- CHANGELOG.md | 2 ++ README.md | 10 ++++-- src/__tests__/reporter/logReporting.spec.ts | 10 +++--- src/__tests__/reporter/onStdErr.spec.ts | 10 +++--- src/__tests__/reportingApi.spec.ts | 36 +++++++++++++++++++-- src/constants/index.ts | 2 +- src/constants/logLevels.ts | 4 ++- src/index.ts | 2 +- src/reporter.ts | 8 ++--- src/reportingApi.ts | 34 ++++++++++--------- 10 files changed, 81 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71809d6..7aee592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +### Added +- Support for custom log levels in `ReportingApi.log()` and `ReportingApi.launchLog()` methods. You can now pass any string as a log level in addition to the PREDEFINED_LOG_LEVELS enum values (TRACE, DEBUG, INFO, WARN, ERROR, FATAL). ## [5.3.2] - 2025-10-29 ### Changed diff --git a/README.md b/README.md index 35bd56e..01dffed 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ Send logs to report portal for the current test. Should be called inside of corr `ReportingApi.log(level: LOG_LEVELS, message: string, file?: Attachment, suite?: string);`
**required**: `level`, `message`
**optional**: `file`, `suite`
-where `level` can be one of the following: _TRACE_, _DEBUG_, _WARN_, _INFO_, _ERROR_, _FATAL_
+where `level` can be one of the following: _TRACE_, _DEBUG_, _WARN_, _INFO_, _ERROR_, _FATAL_, or a custom log level string
Example: ```javascript @@ -291,6 +291,9 @@ test('should contain logs with attachments', () => { content: fileContent.toString('base64'), }; ReportingApi.log('INFO', 'info log with attachment', attachment); + + // Custom log level + ReportingApi.log('CUSTOM_LEVEL', 'custom log message', attachment); expect(true).toBe(true); }); @@ -328,7 +331,7 @@ Send logs to report portal for the current launch. Should be called inside of th `ReportingApi.launchLog(level: LOG_LEVELS, message: string, file?: Attachment);`
**required**: `level`, `message`
**optional**: `file`
-where `level` can be one of the following: _TRACE_, _DEBUG_, _WARN_, _INFO_, _ERROR_, _FATAL_
+where `level` can be one of the following: _TRACE_, _DEBUG_, _WARN_, _INFO_, _ERROR_, _FATAL_, or a custom log level string
Example: ```javascript @@ -341,6 +344,9 @@ test('should contain logs with attachments', async () => { content: fileContent.toString('base64'), }; ReportingApi.launchLog('INFO', 'info log with attachment', attachment); + + // Custom log level + ReportingApi.launchLog('CUSTOM_LAUNCH_LEVEL', 'custom launch log message', attachment); await expect(true).toBe(true); }); diff --git a/src/__tests__/reporter/logReporting.spec.ts b/src/__tests__/reporter/logReporting.spec.ts index 9b13103..f334894 100644 --- a/src/__tests__/reporter/logReporting.spec.ts +++ b/src/__tests__/reporter/logReporting.spec.ts @@ -18,7 +18,7 @@ import helpers from '@reportportal/client-javascript/lib/helpers'; import { RPReporter } from '../../reporter'; import { mockConfig } from '../mocks/configMock'; import { RPClientMock, mockedDate } from '../mocks/RPClientMock'; -import { LOG_LEVELS } from '../../constants'; +import { PREDEFINED_LOG_LEVELS } from '../../constants'; const playwrightProjectName = 'projectName'; const tempTestItemId = 'tempTestItemId'; @@ -37,7 +37,7 @@ describe('logs reporting', () => { }; const log = { - level: LOG_LEVELS.INFO, + level: PREDEFINED_LOG_LEVELS.INFO, message: 'info log', file, }; @@ -51,7 +51,7 @@ describe('logs reporting', () => { }; const expectedSendLogObj = { time: mockedDate, - level: LOG_LEVELS.INFO, + level: PREDEFINED_LOG_LEVELS.INFO, message: 'info log', }; @@ -66,7 +66,7 @@ describe('logs reporting', () => { const expectedSendLogObj = { time: mockedDate, - level: LOG_LEVELS.INFO, + level: PREDEFINED_LOG_LEVELS.INFO, message: 'info log', }; @@ -156,7 +156,7 @@ describe('logs reporting', () => { await reporter.onTestEnd(testCase, result); expect(reporter.sendLog).toHaveBeenCalledWith(tempTestItemId, { - level: LOG_LEVELS.ERROR, + level: PREDEFINED_LOG_LEVELS.ERROR, message: result.error.stack, }); }); diff --git a/src/__tests__/reporter/onStdErr.spec.ts b/src/__tests__/reporter/onStdErr.spec.ts index c09286a..5918ca5 100644 --- a/src/__tests__/reporter/onStdErr.spec.ts +++ b/src/__tests__/reporter/onStdErr.spec.ts @@ -17,7 +17,7 @@ import { RPReporter } from '../../reporter'; import { mockConfig } from '../mocks/configMock'; import { RPClientMock } from '../mocks/RPClientMock'; -import { LOG_LEVELS } from '../../constants'; +import { PREDEFINED_LOG_LEVELS } from '../../constants'; describe('onStdErr testing', () => { const reporter = new RPReporter(mockConfig); @@ -28,22 +28,22 @@ describe('onStdErr testing', () => { titlePath: () => ['rootSuite', 'suiteName', 'testTitle'], }; - test('onStdErr call sendTestItemLog with LOG_LEVELS.ERROR', () => { + test('onStdErr call sendTestItemLog with PREDEFINED_LOG_LEVELS.ERROR', () => { jest.spyOn(reporter, 'sendTestItemLog'); // @ts-ignore reporter.onStdErr('Some error log', testCase); expect(reporter.sendTestItemLog).toHaveBeenCalledWith( - { level: LOG_LEVELS.ERROR, message: 'Some error log' }, + { level: PREDEFINED_LOG_LEVELS.ERROR, message: 'Some error log' }, testCase, ); }); - test('onStdErr call sendTestItemLog with LOG_LEVELS.WARN', () => { + test('onStdErr call sendTestItemLog with PREDEFINED_LOG_LEVELS.WARN', () => { jest.spyOn(reporter, 'sendTestItemLog'); // @ts-ignore reporter.onStdErr('Some warn message', testCase); expect(reporter.sendTestItemLog).toHaveBeenCalledWith( - { level: LOG_LEVELS.WARN, message: 'Some warn message' }, + { level: PREDEFINED_LOG_LEVELS.WARN, message: 'Some warn message' }, testCase, ); }); diff --git a/src/__tests__/reportingApi.spec.ts b/src/__tests__/reportingApi.spec.ts index 984ab39..2a965ee 100644 --- a/src/__tests__/reportingApi.spec.ts +++ b/src/__tests__/reportingApi.spec.ts @@ -18,7 +18,7 @@ import helpers from '@reportportal/client-javascript/lib/helpers'; import { ReportingApi } from '../reportingApi'; import * as utils from '../utils'; -import { LOG_LEVELS } from '../constants'; +import { PREDEFINED_LOG_LEVELS } from '../constants'; import { mockedDate } from './mocks/RPClientMock'; const reportingApiStatusMethods = [ @@ -183,7 +183,22 @@ describe('reportingApi', () => { time: mockedDate, }; const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); - ReportingApi.log(LOG_LEVELS.INFO, 'message', file, suite); + ReportingApi.log(PREDEFINED_LOG_LEVELS.INFO, 'message', file, suite); + + expect(spySendEventToReporter).toHaveBeenCalledWith(event, expectedData, suite); + }); + + test('ReportingApi.log should accept custom log level as string', () => { + const event = 'rp:addLog'; + const customLevel = 'CUSTOM_LEVEL'; + const expectedData = { + file, + level: customLevel, + message: 'custom message', + time: mockedDate, + }; + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + ReportingApi.log(customLevel, 'custom message', file, suite); expect(spySendEventToReporter).toHaveBeenCalledWith(event, expectedData, suite); }); @@ -216,7 +231,22 @@ describe('reportingApi', () => { time: mockedDate, }; const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); - ReportingApi.launchLog(LOG_LEVELS.INFO, 'message', file); + ReportingApi.launchLog(PREDEFINED_LOG_LEVELS.INFO, 'message', file); + + expect(spySendEventToReporter).toHaveBeenCalledWith(event, expectedData); + }); + + test('ReportingApi.launchLog should accept custom log level as string', () => { + const event = 'rp:addLaunchLog'; + const customLevel = 'CUSTOM_LAUNCH_LEVEL'; + const expectedData = { + file, + level: customLevel, + message: 'custom launch message', + time: mockedDate, + }; + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + ReportingApi.launchLog(customLevel, 'custom launch message', file); expect(spySendEventToReporter).toHaveBeenCalledWith(event, expectedData); }); diff --git a/src/constants/index.ts b/src/constants/index.ts index 7eee66e..035d5b0 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -18,7 +18,7 @@ export { LAUNCH_MODES } from './launchModes'; export { TEST_ITEM_TYPES } from './testItemTypes'; export { STATUSES } from './statuses'; -export { LOG_LEVELS } from './logLevels'; +export { PREDEFINED_LOG_LEVELS, LOG_LEVELS } from './logLevels'; export { TestAnnotation, TestOutcome, diff --git a/src/constants/logLevels.ts b/src/constants/logLevels.ts index 9d6e941..9cdabcc 100644 --- a/src/constants/logLevels.ts +++ b/src/constants/logLevels.ts @@ -15,7 +15,7 @@ * */ -export enum LOG_LEVELS { +export enum PREDEFINED_LOG_LEVELS { TRACE = 'TRACE', DEBUG = 'DEBUG', WARN = 'WARN', @@ -23,3 +23,5 @@ export enum LOG_LEVELS { ERROR = 'ERROR', FATAL = 'FATAL', } + +export type LOG_LEVELS = PREDEFINED_LOG_LEVELS | string; diff --git a/src/index.ts b/src/index.ts index ce7e3dc..36ed9c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { RPReporter } from './reporter'; export { ReportingApi } from './reportingApi'; -export { LOG_LEVELS, STATUSES } from './constants'; +export { PREDEFINED_LOG_LEVELS, LOG_LEVELS, STATUSES } from './constants'; export default RPReporter; diff --git a/src/reporter.ts b/src/reporter.ts index 7a6bd6f..8842805 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -30,7 +30,7 @@ import { } from './models'; import { LAUNCH_MODES, - LOG_LEVELS, + PREDEFINED_LOG_LEVELS, STATUSES, TEST_ITEM_TYPES, TEST_ANNOTATION_TYPES, @@ -156,7 +156,7 @@ export class RPReporter implements Reporter { onStdErr(chunk: string | Buffer, test?: TestCase): void { if (test) { const message = String(chunk); - const level = isErrorLog(message) ? LOG_LEVELS.ERROR : LOG_LEVELS.WARN; + const level = isErrorLog(message) ? PREDEFINED_LOG_LEVELS.ERROR : PREDEFINED_LOG_LEVELS.WARN; this.sendTestItemLog({ level, message }, test); } } @@ -240,7 +240,7 @@ export class RPReporter implements Reporter { sendLog( tempId: string, - { level = LOG_LEVELS.INFO, message = '', time = clientHelpers.now(), file }: LogRQ, + { level = PREDEFINED_LOG_LEVELS.INFO, message = '', time = clientHelpers.now(), file }: LogRQ, ): void { const { promise } = this.client.sendLog( tempId, @@ -531,7 +531,7 @@ export class RPReporter implements Reporter { if (result.error) { const stacktrace = stripAnsi(result.error.stack || result.error.message); this.sendLog(testItemId, { - level: LOG_LEVELS.ERROR, + level: PREDEFINED_LOG_LEVELS.ERROR, message: stacktrace, }); if (this.config.extendTestDescriptionWithLastError) { diff --git a/src/reportingApi.ts b/src/reportingApi.ts index 2f9703e..cc83767 100644 --- a/src/reportingApi.ts +++ b/src/reportingApi.ts @@ -19,7 +19,7 @@ import { EVENTS } from '@reportportal/client-javascript/lib/constants/events'; import clientHelpers from '@reportportal/client-javascript/lib/helpers'; import { sendEventToReporter } from './utils'; import { Attribute } from './models'; -import { STATUSES, LOG_LEVELS } from './constants'; +import { STATUSES, PREDEFINED_LOG_LEVELS, LOG_LEVELS } from './constants'; import { Attachment } from './models/reporting'; export const ReportingApi = { @@ -63,36 +63,40 @@ export const ReportingApi = { setLaunchStatusWarn: (): void => sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, STATUSES.WARN), log: ( - level: LOG_LEVELS = LOG_LEVELS.INFO, + level: LOG_LEVELS = PREDEFINED_LOG_LEVELS.INFO, message = '', file?: Attachment, suite?: string, ): void => sendEventToReporter(EVENTS.ADD_LOG, { level, message, file, time: clientHelpers.now() }, suite), - launchLog: (level: LOG_LEVELS = LOG_LEVELS.INFO, message = '', file?: Attachment): void => + launchLog: ( + level: LOG_LEVELS = PREDEFINED_LOG_LEVELS.INFO, + message = '', + file?: Attachment, + ): void => sendEventToReporter(EVENTS.ADD_LAUNCH_LOG, { level, message, file, time: clientHelpers.now() }), trace: (message: string, file?: Attachment, suite?: string): void => - ReportingApi.log(LOG_LEVELS.TRACE, message, file, suite), + ReportingApi.log(PREDEFINED_LOG_LEVELS.TRACE, message, file, suite), debug: (message: string, file?: Attachment, suite?: string): void => - ReportingApi.log(LOG_LEVELS.DEBUG, message, file, suite), + ReportingApi.log(PREDEFINED_LOG_LEVELS.DEBUG, message, file, suite), info: (message: string, file?: Attachment, suite?: string): void => - ReportingApi.log(LOG_LEVELS.INFO, message, file, suite), + ReportingApi.log(PREDEFINED_LOG_LEVELS.INFO, message, file, suite), warn: (message: string, file?: Attachment, suite?: string): void => - ReportingApi.log(LOG_LEVELS.WARN, message, file, suite), + ReportingApi.log(PREDEFINED_LOG_LEVELS.WARN, message, file, suite), error: (message: string, file?: Attachment, suite?: string): void => - ReportingApi.log(LOG_LEVELS.ERROR, message, file, suite), + ReportingApi.log(PREDEFINED_LOG_LEVELS.ERROR, message, file, suite), fatal: (message: string, file?: Attachment, suite?: string): void => - ReportingApi.log(LOG_LEVELS.FATAL, message, file, suite), + ReportingApi.log(PREDEFINED_LOG_LEVELS.FATAL, message, file, suite), launchTrace: (message: string, file?: Attachment): void => - ReportingApi.launchLog(LOG_LEVELS.TRACE, message, file), + ReportingApi.launchLog(PREDEFINED_LOG_LEVELS.TRACE, message, file), launchDebug: (message: string, file?: Attachment): void => - ReportingApi.launchLog(LOG_LEVELS.DEBUG, message, file), + ReportingApi.launchLog(PREDEFINED_LOG_LEVELS.DEBUG, message, file), launchInfo: (message: string, file?: Attachment): void => - ReportingApi.launchLog(LOG_LEVELS.INFO, message, file), + ReportingApi.launchLog(PREDEFINED_LOG_LEVELS.INFO, message, file), launchWarn: (message: string, file?: Attachment): void => - ReportingApi.launchLog(LOG_LEVELS.WARN, message, file), + ReportingApi.launchLog(PREDEFINED_LOG_LEVELS.WARN, message, file), launchError: (message: string, file?: Attachment): void => - ReportingApi.launchLog(LOG_LEVELS.ERROR, message, file), + ReportingApi.launchLog(PREDEFINED_LOG_LEVELS.ERROR, message, file), launchFatal: (message: string, file?: Attachment): void => - ReportingApi.launchLog(LOG_LEVELS.FATAL, message, file), + ReportingApi.launchLog(PREDEFINED_LOG_LEVELS.FATAL, message, file), }; From 06e375f3101178d622138ef29758ef21d1256efb Mon Sep 17 00:00:00 2001 From: Ilya Date: Tue, 2 Dec 2025 15:55:44 +0100 Subject: [PATCH 5/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 01dffed..15fe2a0 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ const rpConfig = { | skippedIssue | Optional | true | reportportal provides feature to mark skipped tests as not 'To Investigate'.
Option could be equal boolean values:
_true_ - skipped tests considered as issues and will be marked as 'To Investigate' on reportportal.
_false_ - skipped tests will not be marked as 'To Investigate' on application. | | debug | Optional | false | This flag allows seeing the logs of the client-javascript. Useful for debugging. | | launchId | Optional | Not set | The _ID_ of an already existing launch. The launch must be in 'IN*PROGRESS' status while the tests are running. Please note that if this \_ID* is provided, the launch will not be finished at the end of the run and must be finished separately. | -| restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). May contain `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, and other client options e.g. `proxy`, [`timeout`](https://github.com/reportportal/client-javascript#timeout-30000ms-on-axios-requests). For debugging and displaying logs the `debug: true` option can be used. Use the retry property (number or axios-retry config) to customise [automatic retries](https://github.com/reportportal/client-javascript?tab=readme-ov-file#retry-configuration).
Visit [client-javascript](https://github.com/reportportal/client-javascript) for more details. | +| restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). May contain `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, and other client options e.g. `proxy`, [`timeout`](https://github.com/reportportal/client-javascript#timeout-30000ms-on-axios-requests). For debugging and displaying logs the `debug: true` option can be used. Use the retry property (number or axios-retry config) to customise [automatic retries](https://github.com/reportportal/client-javascript?tab=readme-ov-file#retry-configuration).
Visit [client-javascript](https://github.com/reportportal/client-javascript?tab=readme-ov-file#http-client-options) for more details. | | headers | Optional | {} | The object with custom headers for internal http client. | | launchUuidPrint | Optional | false | Whether to print the current launch UUID. | | launchUuidPrintOutput | Optional | 'STDOUT' | Launch UUID printing output. Possible values: 'STDOUT', 'STDERR', 'FILE', 'ENVIRONMENT'. Works only if `launchUuidPrint` set to `true`. File format: `rp-launch-uuid-${launch_uuid}.tmp`. Env variable: `RP_LAUNCH_UUID`, note that the env variable is only available in the reporter process (it cannot be obtained from tests). | From 8cfbc1f38ff0dc03173f5e132a923726fc4096ce Mon Sep 17 00:00:00 2001 From: maria-hambardzumian <164881199+maria-hambardzumian@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:40:51 +0400 Subject: [PATCH 6/9] EPMRPP-106695 || Report logs under related nested step (#196) * EPMRPP-106695 || Report logs under related nested step * EPMRPP-106695 || fix test-coverage * EPMRPP-106695 || crf * EPMRPP-106695 || lint fix * EPMRPP-106695 || crf - 2 --------- Co-authored-by: maria-hambardzumian --- jest.config.js | 1 + src/__tests__/reporter/logReporting.spec.ts | 4 + src/__tests__/setup.ts | 40 ++++++++ src/reporter.ts | 108 ++++++++++++++++---- 4 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 src/__tests__/setup.ts diff --git a/jest.config.js b/jest.config.js index a702a49..0d4ce0d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,6 +17,7 @@ module.exports = { roots: ['/src'], + setupFilesAfterEnv: ['/src/__tests__/setup.ts'], transform: { '.ts': [ 'ts-jest', diff --git a/src/__tests__/reporter/logReporting.spec.ts b/src/__tests__/reporter/logReporting.spec.ts index f334894..bc4c5d9 100644 --- a/src/__tests__/reporter/logReporting.spec.ts +++ b/src/__tests__/reporter/logReporting.spec.ts @@ -42,6 +42,10 @@ describe('logs reporting', () => { file, }; + beforeEach(() => { + reporter.logTime = 0; + }); + describe('send log', () => { test('should send custom log for test item with params', () => { const spySendLog = jest.spyOn(reporter.client, 'sendLog'); diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 0000000..6ab5159 --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2025 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +jest.mock('@reportportal/client-javascript/lib/helpers', () => ({ + now: jest.fn(() => new Date().valueOf()), + formatName: jest.fn((name: string) => { + const MIN = 3; + const MAX = 256; + const len = name.length; + return (len < MIN ? name + new Array(MIN - len + 1).join('.') : name).slice(-MAX); + }), + getSystemAttribute: jest.fn(() => []), + generateTestCaseId: jest.fn((codeRef?: string, params?: any[]) => { + if (!codeRef) { + return; + } + if (!params) { + return codeRef; + } + const parameters = params.reduce( + (result, item) => (item.value ? result.concat(item.value) : result), + [], + ); + return `${codeRef}[${parameters}]`; + }), +})); diff --git a/src/reporter.ts b/src/reporter.ts index 8842805..a73d28b 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -92,8 +92,14 @@ export class RPReporter implements Reporter { nestedSteps: Map = new Map(); + activeSteps: Map = new Map(); + + logTime = 0; + isLaunchFinishSend = false; + loggedErrors: Map> = new Map(); + constructor(config: ReportPortalConfig) { this.config = { uploadTrace: true, @@ -222,10 +228,15 @@ export class RPReporter implements Reporter { const logs = (suiteItem?.logs || []).concat(log); this.suitesInfo.set(suiteName, { ...suiteItem, logs }); } else if (test) { - const testItem = this.testItems.get(test.id); - - if (testItem) { - this.sendLog(testItem.id, log); + const activeStepStack = this.activeSteps.get(test.id); + if (activeStepStack && activeStepStack.length > 0) { + const activeStepId = activeStepStack[activeStepStack.length - 1]; + this.sendLog(activeStepId, log); + } else { + const testItem = this.testItems.get(test.id); + if (testItem) { + this.sendLog(testItem.id, log); + } } } } @@ -240,8 +251,15 @@ export class RPReporter implements Reporter { sendLog( tempId: string, - { level = PREDEFINED_LOG_LEVELS.INFO, message = '', time = clientHelpers.now(), file }: LogRQ, + { level = PREDEFINED_LOG_LEVELS.INFO, message = '', time, file }: LogRQ, ): void { + if (!time) { + const now = clientHelpers.now(); + // Increment by at least 1ms to ensure chronological order + time = Math.max(now, this.logTime + 1); + this.logTime = time; + } + const { promise } = this.client.sendLog( tempId, { @@ -445,6 +463,10 @@ export class RPReporter implements Reporter { name: step.title, id: tempId, }); + + const activeStepStack = this.activeSteps.get(test.id) || []; + activeStepStack.push(tempId); + this.activeSteps.set(test.id, activeStepStack); } onStepEnd(test: TestCase, result: TestResult, step: TestStepWithId): void { @@ -456,6 +478,25 @@ export class RPReporter implements Reporter { const nestedStep = this.nestedSteps.get(fullStepName); if (!nestedStep) return; + if (step.error) { + const errorMessages = this.loggedErrors.get(test.id); + const isLogged = errorMessages?.has(step.error.message); + + if (!isLogged) { + const stacktrace = stripAnsi(step.error.stack || step.error.message || ''); + this.sendLog(nestedStep.id, { + level: PREDEFINED_LOG_LEVELS.ERROR, + message: stacktrace, + }); + + if (!errorMessages) { + this.loggedErrors.set(test.id, new Set([step.error.message])); + } else { + errorMessages.add(step.error.message); + } + } + } + const stepFinishObj = { status: step.error ? STATUSES.FAILED : STATUSES.PASSED, endTime: clientHelpers.now(), @@ -465,6 +506,19 @@ export class RPReporter implements Reporter { this.addRequestToPromisesQueue(promise, 'Failed to finish nested step.'); this.nestedSteps.delete(fullStepName); + + const activeStepStack = this.activeSteps.get(test.id); + if (activeStepStack && activeStepStack.length > 0) { + const stepIndex = activeStepStack.indexOf(nestedStep.id); + if (stepIndex !== -1) { + activeStepStack.splice(stepIndex, 1); + } + if (activeStepStack.length === 0) { + this.activeSteps.delete(test.id); + } else { + this.activeSteps.set(test.id, activeStepStack); + } + } } processAnnotations({ annotations, test }: { annotations: Annotation[]; test?: TestCase }): void { @@ -528,30 +582,41 @@ export class RPReporter implements Reporter { }); } + const hasUnfinishedNestedSteps = [...this.nestedSteps.keys()].some((key) => + key.includes(test.id), + ); + if (result.error) { const stacktrace = stripAnsi(result.error.stack || result.error.message); - this.sendLog(testItemId, { - level: PREDEFINED_LOG_LEVELS.ERROR, - message: stacktrace, - }); + const errorMessages = this.loggedErrors.get(test.id); + const isLogged = errorMessages?.has(result.error.message); + + if (!hasUnfinishedNestedSteps && !isLogged) { + this.sendLog(testItemId, { + level: PREDEFINED_LOG_LEVELS.ERROR, + message: stacktrace, + }); + } if (this.config.extendTestDescriptionWithLastError) { testDescription = (description || '').concat(`\n\`\`\`error\n${stacktrace}\n\`\`\``); } } - [...this.nestedSteps.entries()].forEach(([key, value]) => { - if (key.includes(test.id)) { - const { id: stepId } = value; - const itemObject = { - status: result.status === 'timedOut' ? STATUSES.INTERRUPTED : STATUSES.FAILED, - endTime: clientHelpers.now(), - }; + const unfinishedSteps = [...this.nestedSteps.entries()].filter(([key]) => + key.includes(test.id), + ); + + unfinishedSteps.reverse().forEach(([key, value]) => { + const { id: stepId } = value; + const itemObject = { + status: result.status === 'timedOut' ? STATUSES.INTERRUPTED : STATUSES.FAILED, + endTime: clientHelpers.now(), + }; - const { promise } = this.client.finishTestItem(stepId, itemObject); - this.addRequestToPromisesQueue(promise, 'Failed to finish nested step.'); + const { promise } = this.client.finishTestItem(stepId, itemObject); + this.addRequestToPromisesQueue(promise, 'Failed to finish nested step.'); - this.nestedSteps.delete(key); - } + this.nestedSteps.delete(key); }); const finishTestItemObj: FinishTestItemObjType = { @@ -567,6 +632,9 @@ export class RPReporter implements Reporter { this.addRequestToPromisesQueue(promise, 'Failed to finish test.'); this.testItems.delete(test.id); + this.activeSteps.delete(test.id); + this.loggedErrors.delete(test.id); + this.updateAncestorsTestInvocations(test, result); const fullParentName = getCodeRef(test, test.parent.title); From 78e3178fa8eff83cc8c419fd5c089a23f7ff90a7 Mon Sep 17 00:00:00 2001 From: maria-hambardzumian <164881199+maria-hambardzumian@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:14:33 +0400 Subject: [PATCH 7/9] EPMRPP-108516 || Report attachments under related nested step (#199) * EPMRPP-106695 || Report logs under related nested step * EPMRPP-106695 || fix test-coverage * EPMRPP-106695 || crf * EPMRPP-106695 || lint fix * EPMRPP-108516 || Report attachments under related nested step * EPMRPP-108516 || crf --------- Co-authored-by: maria-hambardzumian --- src/models/reporting.ts | 6 ++++++ src/reporter.ts | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/models/reporting.ts b/src/models/reporting.ts index f636ecc..076cc7b 100644 --- a/src/models/reporting.ts +++ b/src/models/reporting.ts @@ -65,4 +65,10 @@ export interface LogRQ { export interface TestStepWithId extends TestStep { id: string; + attachments: Array<{ + name: string; + contentType: string; + path?: string; + body?: Buffer; + }>; } diff --git a/src/reporter.ts b/src/reporter.ts index a73d28b..e446422 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -100,6 +100,8 @@ export class RPReporter implements Reporter { loggedErrors: Map> = new Map(); + stepAttachments: Map> = new Map(); + constructor(config: ReportPortalConfig) { this.config = { uploadTrace: true, @@ -469,7 +471,7 @@ export class RPReporter implements Reporter { this.activeSteps.set(test.id, activeStepStack); } - onStepEnd(test: TestCase, result: TestResult, step: TestStepWithId): void { + async onStepEnd(test: TestCase, result: TestResult, step: TestStepWithId): Promise { const { includeTestSteps } = this.config; if (!includeTestSteps) return; @@ -496,6 +498,31 @@ export class RPReporter implements Reporter { } } } + if (step.attachments?.length) { + try { + const { uploadVideo, uploadTrace } = this.config; + const attachmentsFiles = await getAttachments( + step.attachments, + { + uploadVideo, + uploadTrace, + }, + step.title, + ); + const attachmentNames = this.stepAttachments.get(test.id) || new Set(); + + attachmentsFiles.forEach((file) => { + this.sendLog(nestedStep.id, { + message: `Attachment ${file.name} with type ${file.type}`, + file, + }); + attachmentNames.add(file.name); + }); + this.stepAttachments.set(test.id, attachmentNames); + } catch (error) { + console.error(`Failed to process attachments for step "${step.title}":`, error); + } + } const stepFinishObj = { status: step.error ? STATUSES.FAILED : STATUSES.PASSED, @@ -574,7 +601,9 @@ export class RPReporter implements Reporter { test.title, ); // TODO: use bulk log request - attachmentsFiles.forEach((file) => { + const stepAttachmentNames = this.stepAttachments.get(test.id) || new Set(); + const filteredFiles = attachmentsFiles.filter((file) => !stepAttachmentNames.has(file.name)); + filteredFiles.forEach((file) => { this.sendLog(testItemId, { message: `Attachment ${file.name} with type ${file.type}`, file, @@ -634,6 +663,7 @@ export class RPReporter implements Reporter { this.activeSteps.delete(test.id); this.loggedErrors.delete(test.id); + this.stepAttachments.delete(test.id); this.updateAncestorsTestInvocations(test, result); From 8c426d23eaa1a9f52f039af6cdc6f2ff867006c4 Mon Sep 17 00:00:00 2001 From: maria-hambardzumian <164881199+maria-hambardzumian@users.noreply.github.com> Date: Mon, 5 Jan 2026 05:12:10 +0400 Subject: [PATCH 8/9] EPMRPP-110764 || Use skippedIsNotIssue from client (#202) * EPMRPP-110764 || Use skippedIsNotIssue from client * EPMRPP-110764 || Adjustments --------- Co-authored-by: maria-hambardzumian --- package-lock.json | 336 ++++++++++++++++-- package.json | 2 +- src/__tests__/mocks/configMock.ts | 2 + .../reporter/finishTestItemReporting.spec.ts | 298 +++++++++++++++- .../reporter/launchesReporting.spec.ts | 10 +- src/__tests__/utils.spec.ts | 52 ++- src/reporter.ts | 24 +- src/utils.ts | 25 +- 8 files changed, 642 insertions(+), 107 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31cb070..dd6545e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,10 @@ "packages": { "": { "name": "@reportportal/agent-js-playwright", - "version": "5.3.1", + "version": "5.3.2", "license": "Apache-2.0", "dependencies": { - "@reportportal/client-javascript": "~5.5.2", + "@reportportal/client-javascript": "^5.5.8", "strip-ansi": "~6.0.1" }, "devDependencies": { @@ -662,6 +662,123 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "dev": true, @@ -1122,14 +1239,14 @@ } }, "node_modules/@reportportal/client-javascript": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/@reportportal/client-javascript/-/client-javascript-5.5.2.tgz", - "integrity": "sha512-aF790psvHGY/R4KDLydu++LkjtVAe98KJw7RVqliUDixc7RFirXImsI/AUdUnf4PAulpcIu7o3dwXLXjf946+A==", + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/@reportportal/client-javascript/-/client-javascript-5.5.8.tgz", + "integrity": "sha512-p91lp/ttV1YlhoDglsSIlTV1cSpCsqS4MsFcbe7i88sFlQqqiyWPjKnWHgIt4UK9jN2zJvAdxit5p1nyXQpiWw==", "license": "Apache-2.0", "dependencies": { "axios": "^1.12.2", "axios-retry": "^4.5.0", - "glob": "^8.1.0", + "glob": "^11.1.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "ini": "^2.0.0", @@ -1489,6 +1606,8 @@ }, "node_modules/agent-base": { "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "engines": { "node": ">= 14" @@ -1552,7 +1671,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1867,6 +1985,7 @@ }, "node_modules/balanced-match": { "version": "1.0.2", + "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { @@ -2093,7 +2212,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2104,7 +2222,6 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -2154,7 +2271,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2343,6 +2459,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.208", "dev": true, @@ -2361,7 +2483,6 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/enquirer": { @@ -3373,6 +3494,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.4", "license": "MIT", @@ -3389,6 +3538,7 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -3528,17 +3678,23 @@ } }, "node_modules/glob": { - "version": "8.1.0", - "license": "ISC", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "license": "BlueOak-1.0.0", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=12" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3555,21 +3711,19 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "license": "ISC", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/globals": { @@ -3744,6 +3898,8 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -3755,6 +3911,8 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -3823,6 +3981,7 @@ }, "node_modules/inflight": { "version": "1.0.6", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -3831,10 +3990,13 @@ }, "node_modules/inherits": { "version": "2.0.4", + "dev": true, "license": "ISC" }, "node_modules/ini": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "license": "ISC", "engines": { "node": ">=10" @@ -4001,7 +4163,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4251,7 +4412,6 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -4315,6 +4475,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest": { "version": "29.7.0", "dev": true, @@ -5120,6 +5295,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "license": "MIT" @@ -5269,6 +5453,7 @@ }, "node_modules/once": { "version": "1.4.0", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5356,6 +5541,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -5402,7 +5593,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5413,6 +5603,31 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-type": { "version": "4.0.0", "dev": true, @@ -5972,7 +6187,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5983,7 +6197,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6159,7 +6372,21 @@ }, "node_modules/string-width": { "version": "4.2.3", - "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6233,6 +6460,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "dev": true, @@ -6632,6 +6872,8 @@ }, "node_modules/uniqid": { "version": "5.4.0", + "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-5.4.0.tgz", + "integrity": "sha512-38JRbJ4Fj94VmnC7G/J/5n5SC7Ab46OM5iNtSstB/ko3l1b5g7ALt4qzHFgGciFkyiRNtDXtLNb+VsxtMSE77A==", "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -6673,6 +6915,8 @@ }, "node_modules/uuid": { "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -6711,7 +6955,6 @@ }, "node_modules/which": { "version": "2.0.2", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -6833,8 +7076,27 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", + "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index a96883f..c59a211 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test:coverage": "jest --coverage" }, "dependencies": { - "@reportportal/client-javascript": "~5.5.2", + "@reportportal/client-javascript": "^5.5.8", "strip-ansi": "~6.0.1" }, "files": [ diff --git a/src/__tests__/mocks/configMock.ts b/src/__tests__/mocks/configMock.ts index 554fa02..ab2737d 100644 --- a/src/__tests__/mocks/configMock.ts +++ b/src/__tests__/mocks/configMock.ts @@ -25,4 +25,6 @@ export const mockConfig: ReportPortalConfig = { description: 'Launch description', attributes: [], extendTestDescriptionWithLastError: true, + uploadVideo: true, + uploadTrace: true, }; diff --git a/src/__tests__/reporter/finishTestItemReporting.spec.ts b/src/__tests__/reporter/finishTestItemReporting.spec.ts index 62bf95c..eb433d3 100644 --- a/src/__tests__/reporter/finishTestItemReporting.spec.ts +++ b/src/__tests__/reporter/finishTestItemReporting.spec.ts @@ -20,6 +20,7 @@ import { mockConfig } from '../mocks/configMock'; import { RPClientMock, mockedDate } from '../mocks/RPClientMock'; import { FinishTestItemObjType } from '../../models'; import { STATUSES } from '../../constants'; +import * as utils from '../../utils'; const rootSuite = 'rootSuite'; const suiteName = 'suiteName'; @@ -100,7 +101,6 @@ describe('finish test reporting', () => { }); test('client.finishTestItem should be called with test item id', async () => { - reporter.config.skippedIssue = true; const result = { status: 'passed', }; @@ -123,8 +123,7 @@ describe('finish test reporting', () => { expect(reporter.testItems.size).toBe(0); }); - test('client.finishTestItem should be called with issueType NOT_ISSUE', async () => { - reporter.config.skippedIssue = false; + test('client.finishTestItem should be called with skipped status (issue handling delegated to client)', async () => { const result = { status: 'skipped', }; @@ -133,7 +132,6 @@ describe('finish test reporting', () => { status: result.status, attributes: [{ key: 'key', value: 'value' }], description: 'description', - issue: { issueType: 'NOT_ISSUE' }, }; // @ts-ignore await reporter.onTestEnd({ ...testCase, outcome: () => 'skipped' }, result); @@ -201,4 +199,296 @@ describe('finish test reporting', () => { expect(reporter.client.finishTestItem).toHaveBeenCalledWith('1214r1', finishStepObject); }); + + describe('attachment handling', () => { + let getAttachmentsSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + // Reset reporter state + reporter = new RPReporter(mockConfig); + reporter.client = new RPClientMock(mockConfig); + reporter.launchId = 'tempLaunchId'; + reporter.testItems = new Map([['testItemId', { id: 'tempTestItemId', name: 'testTitle' }]]); + reporter.suites = new Map([ + [ + rootSuite, + { + id: 'rootsuiteId', + name: rootSuite, + testInvocationsLeft: 1, + descendants: ['testItemId'], + }, + ], + [ + `${rootSuite}/${suiteName}`, + { + id: 'suiteId', + name: suiteName, + testInvocationsLeft: 1, + descendants: ['testItemId'], + }, + ], + ]); + // @ts-ignore + reporter.addAttributes([{ key: 'key', value: 'value' }], testCase); + // @ts-ignore + reporter.setDescription('description', testCase); + }); + + afterEach(() => { + if (getAttachmentsSpy) { + getAttachmentsSpy.mockRestore(); + } + }); + + test('should process attachments and send logs for each attachment', async () => { + const mockAttachments = [ + { + name: 'screenshot.png', + contentType: 'image/png', + path: '/path/to/screenshot.png', + }, + { + name: 'video.webm', + contentType: 'video/webm', + path: '/path/to/video.webm', + }, + ]; + + const mockAttachmentFiles = [ + { + name: 'testtitle_screenshot.png', + type: 'image/png', + content: Buffer.from('screenshot content'), + }, + { + name: 'testtitle_video.webm', + type: 'video/webm', + content: Buffer.from('video content'), + }, + ]; + + getAttachmentsSpy = jest + .spyOn(utils, 'getAttachments') + .mockResolvedValue(mockAttachmentFiles); + const sendLogSpy = jest.spyOn(reporter, 'sendLog'); + + const result = { + status: 'passed', + attachments: mockAttachments, + }; + + // @ts-ignore + await reporter.onTestEnd({ ...testCase, outcome: () => 'expected' }, result); + + expect(utils.getAttachments).toHaveBeenCalledWith( + mockAttachments, + { + uploadVideo: true, + uploadTrace: true, + }, + testCase.title, + ); + + expect(sendLogSpy).toHaveBeenCalledTimes(2); + expect(sendLogSpy).toHaveBeenCalledWith('tempTestItemId', { + message: 'Attachment testtitle_screenshot.png with type image/png', + file: mockAttachmentFiles[0], + }); + expect(sendLogSpy).toHaveBeenCalledWith('tempTestItemId', { + message: 'Attachment testtitle_video.webm with type video/webm', + file: mockAttachmentFiles[1], + }); + }); + + test('should filter out attachments already in stepAttachmentNames', async () => { + const mockAttachments = [ + { + name: 'screenshot.png', + contentType: 'image/png', + path: '/path/to/screenshot.png', + }, + { + name: 'video.webm', + contentType: 'video/webm', + path: '/path/to/video.webm', + }, + ]; + + const mockAttachmentFiles = [ + { + name: 'testtitle_screenshot.png', + type: 'image/png', + content: Buffer.from('screenshot content'), + }, + { + name: 'testtitle_video.webm', + type: 'video/webm', + content: Buffer.from('video content'), + }, + ]; + + getAttachmentsSpy = jest + .spyOn(utils, 'getAttachments') + .mockResolvedValue(mockAttachmentFiles); + const sendLogSpy = jest.spyOn(reporter, 'sendLog'); + + reporter.stepAttachments.set(testCase.id, new Set(['testtitle_screenshot.png'])); + + const result = { + status: 'passed', + attachments: mockAttachments, + }; + + // @ts-ignore + await reporter.onTestEnd({ ...testCase, outcome: () => 'expected' }, result); + + expect(sendLogSpy).toHaveBeenCalledTimes(1); + expect(sendLogSpy).toHaveBeenCalledWith('tempTestItemId', { + message: 'Attachment testtitle_video.webm with type video/webm', + file: mockAttachmentFiles[1], + }); + }); + + test('should handle empty stepAttachmentNames set', async () => { + const mockAttachments = [ + { + name: 'screenshot.png', + contentType: 'image/png', + path: '/path/to/screenshot.png', + }, + ]; + + const mockAttachmentFiles = [ + { + name: 'testtitle_screenshot.png', + type: 'image/png', + content: Buffer.from('screenshot content'), + }, + ]; + + getAttachmentsSpy = jest + .spyOn(utils, 'getAttachments') + .mockResolvedValue(mockAttachmentFiles); + const sendLogSpy = jest.spyOn(reporter, 'sendLog'); + + const result = { + status: 'passed', + attachments: mockAttachments, + }; + + // @ts-ignore + await reporter.onTestEnd({ ...testCase, outcome: () => 'expected' }, result); + + expect(sendLogSpy).toHaveBeenCalledTimes(1); + expect(sendLogSpy).toHaveBeenCalledWith('tempTestItemId', { + message: 'Attachment testtitle_screenshot.png with type image/png', + file: mockAttachmentFiles[0], + }); + }); + + test('should respect uploadVideo config when processing attachments', async () => { + const customConfig = { + ...mockConfig, + uploadVideo: false, + uploadTrace: true, + }; + + const reporterWithConfig = new RPReporter(customConfig); + reporterWithConfig.client = new RPClientMock(customConfig); + reporterWithConfig.launchId = 'tempLaunchId'; + reporterWithConfig.testItems = new Map([ + ['testItemId', { id: 'tempTestItemId', name: 'testTitle' }], + ]); + reporterWithConfig.suites = new Map([ + [ + rootSuite, + { + id: 'rootsuiteId', + name: rootSuite, + testInvocationsLeft: 1, + descendants: ['testItemId'], + }, + ], + [ + `${rootSuite}/${suiteName}`, + { + id: 'suiteId', + name: suiteName, + testInvocationsLeft: 1, + descendants: ['testItemId'], + }, + ], + ]); + + const mockAttachments = [ + { + name: 'screenshot.png', + contentType: 'image/png', + path: '/path/to/screenshot.png', + }, + ]; + + const mockAttachmentFiles = [ + { + name: 'testtitle_screenshot.png', + type: 'image/png', + content: Buffer.from('screenshot content'), + }, + ]; + + getAttachmentsSpy = jest + .spyOn(utils, 'getAttachments') + .mockResolvedValue(mockAttachmentFiles); + + const result = { + status: 'passed', + attachments: mockAttachments, + }; + + // @ts-ignore + await reporterWithConfig.onTestEnd({ ...testCase, outcome: () => 'expected' }, result); + + expect(utils.getAttachments).toHaveBeenCalledWith( + mockAttachments, + { + uploadVideo: false, + uploadTrace: true, + }, + testCase.title, + ); + }); + + test('should not process attachments if result.attachments is empty', async () => { + getAttachmentsSpy = jest.spyOn(utils, 'getAttachments'); + const sendLogSpy = jest.spyOn(reporter, 'sendLog'); + + const result: any = { + status: 'passed', + attachments: [], + }; + + // @ts-ignore + await reporter.onTestEnd({ ...testCase, outcome: () => 'expected' }, result); + + expect(getAttachmentsSpy).not.toHaveBeenCalled(); + expect(sendLogSpy).not.toHaveBeenCalled(); + }); + + test('should not process attachments if result.attachments is undefined', async () => { + getAttachmentsSpy = jest.spyOn(utils, 'getAttachments'); + const sendLogSpy = jest.spyOn(reporter, 'sendLog'); + + const result = { + status: 'passed', + }; + + // @ts-ignore + await reporter.onTestEnd({ ...testCase, outcome: () => 'expected' }, result); + + expect(getAttachmentsSpy).not.toHaveBeenCalled(); + expect(sendLogSpy).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/__tests__/reporter/launchesReporting.spec.ts b/src/__tests__/reporter/launchesReporting.spec.ts index de9a539..8a1920a 100644 --- a/src/__tests__/reporter/launchesReporting.spec.ts +++ b/src/__tests__/reporter/launchesReporting.spec.ts @@ -18,8 +18,8 @@ import helpers from '@reportportal/client-javascript/lib/helpers'; import { RPReporter } from '../../reporter'; import { StartLaunchObjType } from '../../models'; -import { getSystemAttributes } from '../../utils'; import { LAUNCH_MODES } from '../../constants'; +import { getSystemAttribute } from '../../utils'; import { mockConfig } from '../mocks/configMock'; import { RPClientMock, mockedDate } from '../mocks/RPClientMock'; @@ -33,7 +33,7 @@ describe('start launch', () => { const startLaunchObj: StartLaunchObjType = { name: mockConfig.launch, startTime: mockedDate, - attributes: getSystemAttributes(), + attributes: [...(mockConfig.attributes || []), getSystemAttribute()], description: mockConfig.description, mode: LAUNCH_MODES.DEFAULT, }; @@ -60,7 +60,7 @@ describe('start launch', () => { const startLaunchObj: StartLaunchObjType = { name: customConfig.launch, startTime: mockedDate, - attributes: getSystemAttributes(), + attributes: [...(customConfig.attributes || []), getSystemAttribute()], description: customConfig.description, mode: customConfig.mode, }; @@ -87,7 +87,7 @@ describe('start launch', () => { const startLaunchObj: StartLaunchObjType = { name: customConfig.launch, startTime: mockedDate, - attributes: getSystemAttributes(), + attributes: [...(customConfig.attributes || []), getSystemAttribute()], description: customConfig.description, mode: LAUNCH_MODES.DEFAULT, id: 'id', @@ -110,7 +110,7 @@ describe('start launch', () => { const startLaunchObj: StartLaunchObjType = { name: mockConfig.launch, startTime: mockedDate, - attributes: getSystemAttributes(), + attributes: [...(mockConfig.attributes || []), getSystemAttribute()], description: mockConfig.description, mode: LAUNCH_MODES.DEFAULT, id: 'id', diff --git a/src/__tests__/utils.spec.ts b/src/__tests__/utils.spec.ts index 703ea2c..1641ebc 100644 --- a/src/__tests__/utils.spec.ts +++ b/src/__tests__/utils.spec.ts @@ -20,7 +20,6 @@ import { name as pjsonName, version as pjsonVersion } from '../../package.json'; import { getAgentInfo, getCodeRef, - getSystemAttributes, promiseErrorHandler, sendEventToReporter, isFalse, @@ -127,32 +126,6 @@ describe('testing utils', () => { }); }); - describe('getSystemAttributes', () => { - const expectedRes = [ - { - key: 'agent', - value: `${pjsonName}|${pjsonVersion}`, - system: true, - }, - ]; - test('should return the list of system attributes', () => { - const systemAttributes = getSystemAttributes(); - - expect(systemAttributes).toEqual(expectedRes); - }); - - test('should return expected list of system attributes in case skippedIssue=false', () => { - const systemAttributes = getSystemAttributes(false); - const skippedIssueAttribute = { - key: 'skippedIssue', - value: 'false', - system: true, - }; - - expect(systemAttributes).toEqual([...expectedRes, skippedIssueAttribute]); - }); - }); - describe('getCodeRef', () => { const projectName = 'Google Chrome tests'; const mockedTest = { @@ -369,6 +342,31 @@ describe('testing utils', () => { expect(attachmentResult).toEqual(expectedAttachments); }); + test('should log error to console when reading file fails', async () => { + const readError = new Error('Read file error'); + const spyConsoleError = jest.spyOn(console, 'error').mockImplementation(); + + jest.spyOn(fs.promises, 'stat').mockImplementationOnce(() => Promise.resolve({} as fs.Stats)); + jest.spyOn(fs.promises, 'readFile').mockImplementationOnce(async () => { + throw readError; + }); + + const attachments = [ + { + name: 'filename1', + contentType: 'image/png', + path: 'path/to/attachment', + }, + ]; + + const attachmentResult = await getAttachments(attachments); + + expect(spyConsoleError).toHaveBeenCalledWith(readError); + expect(attachmentResult).toEqual([]); + + spyConsoleError.mockRestore(); + }); + describe('with attachments options', () => { test('should return empty attachment list without trace in case of uploadTrace option is false', async () => { const attachments = [ diff --git a/src/reporter.ts b/src/reporter.ts index e446422..a02759e 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -41,7 +41,7 @@ import { getAgentInfo, getAttachments, getCodeRef, - getSystemAttributes, + getSystemAttribute, isErrorLog, isFalse, promiseErrorHandler, @@ -113,7 +113,13 @@ export class RPReporter implements Reporter { const agentInfo = getAgentInfo(); - this.client = new RPClient(this.config, agentInfo); + this.client = new RPClient( + { + ...this.config, + skippedIsNotIssue: isFalse(this.config.skippedIssue), + }, + agentInfo, + ); } addRequestToPromisesQueue(promise: Promise, failMessage: string): void { @@ -299,16 +305,14 @@ export class RPReporter implements Reporter { onBegin(): void { // reset the flag in case the Playwright will reuse the reporter instance this.isLaunchFinishSend = false; - const { launch, description, attributes, skippedIssue, rerun, rerunOf, mode, launchId } = - this.config; - const systemAttributes: Attribute[] = getSystemAttributes(skippedIssue); + const { launch, description, attributes, rerun, rerunOf, mode, launchId } = this.config; + const systemAttribute = getSystemAttribute(); const startLaunchObj: StartLaunchObjType = { name: launch, startTime: clientHelpers.now(), description, - attributes: - attributes && attributes.length ? attributes.concat(systemAttributes) : systemAttributes, + attributes: [...(attributes || []), systemAttribute], rerun, rerunOf, mode: mode || LAUNCH_MODES.DEFAULT, @@ -581,15 +585,10 @@ export class RPReporter implements Reporter { testCaseId, status: predefinedStatus, } = savedTestItem; - let withoutIssue; let testDescription = description; const calculatedStatus = calculateRpStatus(test.outcome(), result.status, test.annotations); const status = predefinedStatus || calculatedStatus; - if (status === STATUSES.SKIPPED) { - withoutIssue = isFalse(this.config.skippedIssue); - } - // TODO: cover with tests if (result.attachments?.length) { const { uploadVideo, uploadTrace } = this.config; const attachmentsFiles = await getAttachments( @@ -651,7 +650,6 @@ export class RPReporter implements Reporter { const finishTestItemObj: FinishTestItemObjType = { endTime: clientHelpers.now(), status, - ...(withoutIssue && { issue: { issueType: 'NOT_ISSUE' } }), ...(attributes && { attributes }), ...(testDescription && { description: testDescription }), ...(testCaseId && { testCaseId }), diff --git a/src/utils.ts b/src/utils.ts index 663b41f..3dfe1b7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -48,26 +48,11 @@ export const getAgentInfo = (): { version: string; name: string } => ({ name: pjsonName, }); -export const getSystemAttributes = (skippedIssue = true): Array => { - const systemAttributes = [ - { - key: 'agent', - value: `${pjsonName}|${pjsonVersion}`, - system: true, - }, - ]; - - if (isFalse(skippedIssue)) { - const skippedIssueAttribute = { - key: 'skippedIssue', - value: 'false', - system: true, - }; - systemAttributes.push(skippedIssueAttribute); - } - - return systemAttributes; -}; +export const getSystemAttribute = (): Attribute => ({ + key: 'agent', + value: `${getAgentInfo().name}|${getAgentInfo().version}`, + system: true, +}); type testItemPick = Pick; From 055c3f079e9d3f85931aa2c3d521be07b4777ea2 Mon Sep 17 00:00:00 2001 From: maria-hambardzumian <164881199+maria-hambardzumian@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:38:57 +0400 Subject: [PATCH 9/9] EPMRPP-86853 || Report skipping reason for the skipped test (#204) * EPMRPP-86853 || Report skipping reason for the skipped test * EPMRPP-86853 || Adjustments * EPMRPP-86853 || Fix test description concatenation --------- Co-authored-by: maria-hambardzumian --- .../reporter/finishTestItemReporting.spec.ts | 50 ++++++++++++++++++- src/__tests__/utils.spec.ts | 46 ++++++++++++++++- src/reporter.ts | 9 +++- src/utils.ts | 26 ++++++---- 4 files changed, 119 insertions(+), 12 deletions(-) diff --git a/src/__tests__/reporter/finishTestItemReporting.spec.ts b/src/__tests__/reporter/finishTestItemReporting.spec.ts index eb433d3..e2d6247 100644 --- a/src/__tests__/reporter/finishTestItemReporting.spec.ts +++ b/src/__tests__/reporter/finishTestItemReporting.spec.ts @@ -19,7 +19,7 @@ import { RPReporter } from '../../reporter'; import { mockConfig } from '../mocks/configMock'; import { RPClientMock, mockedDate } from '../mocks/RPClientMock'; import { FinishTestItemObjType } from '../../models'; -import { STATUSES } from '../../constants'; +import { STATUSES, TEST_ANNOTATION_TYPES } from '../../constants'; import * as utils from '../../utils'; const rootSuite = 'rootSuite'; @@ -145,6 +145,54 @@ describe('finish test reporting', () => { expect(reporter.testItems.size).toBe(0); }); + test.each([ + [ + TEST_ANNOTATION_TYPES.SKIP, + 'Cannot run suite.', + '**Skip reason: Cannot run suite.**\ndescription', + ], + [ + TEST_ANNOTATION_TYPES.FIXME, + 'Feature not implemented.', + '**Skip reason: Feature not implemented.**\ndescription', + ], + ])( + 'client.finishTestItem should be called with %s reason prepended to description', + async (type, reason, expectedDescription) => { + const testCaseWithAnnotation = { + ...testCase, + annotations: [{ type, description: reason }], + outcome: () => 'skipped', + }; + // @ts-ignore + await reporter.onTestEnd(testCaseWithAnnotation, { status: 'skipped' }); + + expect(reporter.client.finishTestItem).toHaveBeenNthCalledWith(1, 'tempTestItemId', { + endTime: mockedDate, + status: 'skipped', + attributes: [{ key: 'key', value: 'value' }], + description: expectedDescription, + }); + }, + ); + + test('client.finishTestItem should be called with skip reason as description when no existing description', async () => { + reporter.testItems = new Map([['testItemId', { id: 'tempTestItemId', name: 'testTitle' }]]); + const testCaseWithSkipAnnotation = { + ...testCase, + annotations: [{ type: TEST_ANNOTATION_TYPES.SKIP, description: 'Cannot run suite.' }], + outcome: () => 'skipped', + }; + // @ts-ignore + await reporter.onTestEnd(testCaseWithSkipAnnotation, { status: 'skipped' }); + + expect(reporter.client.finishTestItem).toHaveBeenNthCalledWith(1, 'tempTestItemId', { + endTime: mockedDate, + status: 'skipped', + description: '**Skip reason: Cannot run suite.**', + }); + }); + test('client.finishTestItem should not be called in case of test item not found', async () => { const result = { status: 'passed', diff --git a/src/__tests__/utils.spec.ts b/src/__tests__/utils.spec.ts index 1641ebc..16704f8 100644 --- a/src/__tests__/utils.spec.ts +++ b/src/__tests__/utils.spec.ts @@ -27,6 +27,7 @@ import { isErrorLog, fileExists, calculateRpStatus, + getSkipReason, } from '../utils'; import fs from 'fs'; import path from 'path'; @@ -35,8 +36,8 @@ import { TestOutcome, BASIC_ATTACHMENT_CONTENT_TYPES, BASIC_ATTACHMENT_NAMES, + TEST_ANNOTATION_TYPES, } from '../constants'; -import { RPReporter } from '../reporter'; const mockAnnotations: any[] = []; @@ -465,4 +466,47 @@ describe('testing utils', () => { expect(status).toBe(STATUSES.PASSED); }); }); + + describe('getSkipReason', () => { + test.each([ + [ + [{ type: TEST_ANNOTATION_TYPES.SKIP, description: 'Cannot run suite.' }], + 'Cannot run suite.', + ], + [ + [{ type: TEST_ANNOTATION_TYPES.FIXME, description: 'Feature not implemented.' }], + 'Feature not implemented.', + ], + [ + [ + { type: TEST_ANNOTATION_TYPES.SKIP, description: 'First reason' }, + { type: TEST_ANNOTATION_TYPES.SKIP, description: 'Second reason' }, + ], + 'First reason', + ], + [ + [ + { type: TEST_ANNOTATION_TYPES.SKIP, description: 'Skip reason' }, + { type: TEST_ANNOTATION_TYPES.FIXME, description: 'Fixme reason' }, + ], + 'Skip reason', + ], + ])('should return skip reason for %j', (annotations, expected) => { + expect(getSkipReason(annotations)).toBe(expected); + }); + + test.each([ + { + annotations: [{ type: TEST_ANNOTATION_TYPES.SKIP }], + name: 'skip annotation without description', + }, + { + annotations: [{ type: 'custom', description: 'Some description' }], + name: 'non-skip annotation', + }, + { annotations: [], name: 'empty annotations array' }, + ])('should return undefined for $name', ({ annotations }) => { + expect(getSkipReason(annotations)).toBeUndefined(); + }); + }); }); diff --git a/src/reporter.ts b/src/reporter.ts index a02759e..170ae60 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -41,6 +41,7 @@ import { getAgentInfo, getAttachments, getCodeRef, + getSkipReason, getSystemAttribute, isErrorLog, isFalse, @@ -587,6 +588,12 @@ export class RPReporter implements Reporter { } = savedTestItem; let testDescription = description; const calculatedStatus = calculateRpStatus(test.outcome(), result.status, test.annotations); + + const skipReason = getSkipReason(test.annotations); + if (skipReason) { + const skipReasonText = `**Skip reason: ${skipReason}**`; + testDescription = testDescription ? `${skipReasonText}\n${testDescription}` : skipReasonText; + } const status = predefinedStatus || calculatedStatus; if (result.attachments?.length) { @@ -626,7 +633,7 @@ export class RPReporter implements Reporter { }); } if (this.config.extendTestDescriptionWithLastError) { - testDescription = (description || '').concat(`\n\`\`\`error\n${stacktrace}\n\`\`\``); + testDescription = (testDescription || '').concat(`\n\`\`\`error\n${stacktrace}\n\`\`\``); } } diff --git a/src/utils.ts b/src/utils.ts index 3dfe1b7..cd09466 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,23 +15,22 @@ * */ -import { TestCase, TestStatus, TestResult } from '@playwright/test/reporter'; +import { TestCase, TestResult, TestStatus } from '@playwright/test/reporter'; import fs from 'fs'; import path from 'path'; // @ts-ignore import { name as pjsonName, version as pjsonVersion } from '../package.json'; -import { Attribute, Attachment, AttachmentsConfig } from './models'; +import { Attachment, AttachmentsConfig, Attribute } from './models'; import { - STATUSES, - TestAnnotation, - TestOutcome, - BASIC_ATTACHMENT_NAMES, BASIC_ATTACHMENT_CONTENT_TYPES, + BASIC_ATTACHMENT_NAMES, + STATUSES, TEST_ANNOTATION_TYPES, TEST_OUTCOME_TYPES, + TestAnnotation, + TestOutcome, } from './constants'; import { test } from '@playwright/test'; -import { RPReporter } from './reporter'; const fsPromises = fs.promises; @@ -200,9 +199,18 @@ export const safeParse = (input: unknown) => { } try { - const parsed = JSON.parse(input); - return parsed; + return JSON.parse(input); } catch { return input; } }; + +export const getSkipReason = (annotations: TestAnnotation[]): string | undefined => { + const skipAnnotation = annotations.find( + (annotation) => + (annotation.type === TEST_ANNOTATION_TYPES.SKIP || + annotation.type === TEST_ANNOTATION_TYPES.FIXME) && + annotation.description, + ); + return skipAnnotation?.description; +};