From a5b8d88f8c92e7303e05499ec2b2d2680f6ed071 Mon Sep 17 00:00:00 2001 From: Bam6ycha <84175555+Bam6ycha@users.noreply.github.com> Date: Tue, 17 Oct 2023 15:24:05 +0500 Subject: [PATCH] EPMRPP-78272 || Implement attaching data using Playwright testInfo.attach (#123) * EPMRPP-78272 || Attach additional data via testInfo.attach * EPMRPP-78272 || Fix failed tests * EPMRPP-78272 || Code review fixes - 2 * EPMRPP-78272 || Increase test coverage * EPMRPP-78272 || Code review fixes - 2 - Remove unused interface - Reuse ReportingApi.setStatus method * EPMRPP-78272 || Code review fixes - 3 --- .eslintrc | 5 +- .prettierignore | 2 + CHANGELOG.md | 2 + README.md | 58 +++--- package.json | 25 ++- src/__tests__/promises/reportingApi.spec.ts | 192 ++++++++++++++++++ .../reporter/finishSuiteReporting.spec.ts | 2 + .../reporter/finishTestItemReporting.spec.ts | 122 ++++++++++- src/__tests__/reporter/logReporting.spec.ts | 1 + src/__tests__/utils.spec.ts | 112 ++++++++++ src/constants/index.ts | 1 + src/constants/testInfo.ts | 32 +++ src/index.ts | 1 + src/models/reporting.ts | 7 + src/promises/index.ts | 1 + src/promises/reportingApi.ts | 137 +++++++++++++ src/reporter.ts | 20 +- src/reportingApi.ts | 123 ++++++++--- src/utils.ts | 40 +++- 19 files changed, 818 insertions(+), 65 deletions(-) create mode 100644 .prettierignore create mode 100644 src/__tests__/promises/reportingApi.spec.ts create mode 100644 src/constants/testInfo.ts create mode 100644 src/promises/index.ts create mode 100644 src/promises/reportingApi.ts diff --git a/.eslintrc b/.eslintrc index dbb4ec4..a61a91f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,7 +18,7 @@ "parserOptions": { "ecmaVersion": 2018, "sourceType": "module", - "project": "./tsconfig.eslint.json" + "project": "./tsconfig.eslint.json" }, "rules": { "no-plusplus": 0, @@ -33,6 +33,7 @@ "@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/no-implied-eval": 0, "dot-notation": 0, - "@typescript-eslint/naming-convention": 0 + "@typescript-eslint/naming-convention": 0, + "consistent-return": "warn" } } diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..60700ed --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# Ignore all md files +*.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ca94b..e59c72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +### Added +- `ReportingApi` from `@reportportal/agent-js-playwright/promises` methods (***addAttributes, setDescription, setTestCaseId, setStatus***, and all methods for setting custom statuses for test or suite) now using ***testInfo.attach*** method to attach custom data to test case. ## [5.1.4] - 2023-10-05 ## Changed diff --git a/README.md b/README.md index 444e6ac..5adf8bd 100644 --- a/README.md +++ b/README.md @@ -130,9 +130,9 @@ As an alternative to this approach the [`ReportingAPI`](#log) methods can be use This reporter provides Reporting API to use it directly in tests to send some additional data to the report. -To start using the `ReportingApi` in tests, just import it from `'@reportportal/agent-js-playwright'`: +To start using the `ReportingApi` in tests, just import it from `'@reportportal/agent-js-playwright/promises'`: ```javascript -import { ReportingApi } from '@reportportal/agent-js-playwright'; +import { ReportingApi } from '@reportportal/agent-js-playwright/promises' ``` #### Reporting API methods @@ -143,13 +143,15 @@ If you want to add a data to the suite, you must pass the suite name as the last ##### addAttributes Add attributes (tags) to the current test. Should be called inside of corresponding test.
-`ReportingApi.addAttributes(attributes: Array, suite?: string);`
+`ReportingApi.addAttributes(attributes: Array, suite?: string):Promise;`
**required**: `attributes`
**optional**: `suite`
Example: ```javascript -test('should have the correct attributes', () => { - ReportingApi.addAttributes([ +import { ReportingApi } from '@reportportal/agent-js-playwright/promises' + +test('should have the correct attributes', async () => { + await ReportingApi.addAttributes([ { key: 'testKey', value: 'testValue', @@ -164,14 +166,16 @@ test('should have the correct attributes', () => { ##### setTestCaseId Set test case id to the current test ([About test case id](https://reportportal.io/docs/Test-case-ID%3Ewhat-is-it-test-case-id)). Should be called inside of corresponding test.
-`ReportingApi.setTestCaseId(id: string, suite?: string);`
+`ReportingApi.setTestCaseId(id: string, suite?: string):Promise;`
**required**: `id`
**optional**: `suite`
If `testCaseId` not specified, it will be generated automatically based on [codeRef](https://reportportal.io/docs/Test-case-ID%3Ewhat-does-happen-if-you-do-not-report-items-with-test-case-id-).
Example: ```javascript -test('should have the correct testCaseId', () => { - ReportingApi.setTestCaseId('itemTestCaseId'); +import { ReportingApi } from '@reportportal/agent-js-playwright/promises' + +test('should have the correct testCaseId', async () => { + await ReportingApi.setTestCaseId('itemTestCaseId'); expect(true).toBe(true); }); ``` @@ -270,14 +274,16 @@ test('should contain logs with attachments', () => { ##### setStatus Assign corresponding status to the current test item. Should be called inside of corresponding test.
-`ReportingApi.setStatus(status: string, suite?: string);`
+`ReportingApi.setStatus(status: string, suite?: string):Promise;`
**required**: `status`
**optional**: `suite`
where `status` must be one of the following: *passed*, *failed*, *stopped*, *skipped*, *interrupted*, *cancelled*
Example: ```javascript -test('should have status FAILED', () => { - ReportingApi.setStatus('failed'); +import { ReportingApi } from '@reportportal/agent-js-playwright/promises' + +test('should have status FAILED', async () => { + await ReportingApi.setStatus('failed'); expect(true).toBe(true); }); @@ -285,22 +291,26 @@ test('should have status FAILED', () => { ##### setStatusFailed, setStatusPassed, setStatusSkipped, setStatusStopped, setStatusInterrupted, setStatusCancelled Assign corresponding status to the current test item. Should be called inside of corresponding test.
-`ReportingApi.setStatusFailed(suite?: string);`
-`ReportingApi.setStatusPassed(suite?: string);`
-`ReportingApi.setStatusSkipped(suite?: string);`
-`ReportingApi.setStatusStopped(suite?: string);`
-`ReportingApi.setStatusInterrupted(suite?: string);`
-`ReportingApi.setStatusCancelled(suite?: string);`
+`ReportingApi.setStatusFailed(suite?: string):Promise;`
+`ReportingApi.setStatusPassed(suite?: string):Promise;`
+`ReportingApi.setStatusSkipped(suite?: string):Promise;`
+`ReportingApi.setStatusStopped(suite?: string):Promise;`
+`ReportingApi.setStatusInterrupted(suite?: string):Promise;`
+`ReportingApi.setStatusCancelled(suite?: string):Promise;`
**optional**: `suite`
Example: ```javascript -test('should call ReportingApi to set statuses', () => { - ReportingAPI.setStatusFailed(); - ReportingAPI.setStatusPassed(); - ReportingAPI.setStatusSkipped(); - ReportingAPI.setStatusStopped(); - ReportingAPI.setStatusInterrupted(); - ReportingAPI.setStatusCancelled(); +import { ReportingApi } from '@reportportal/agent-js-playwright/promises' + +test('should call ReportingApi to set statuses', async () => { + await Promise.all[ + ReportingAPI.setStatusFailed(), + ReportingAPI.setStatusPassed(), + ReportingAPI.setStatusSkipped(), + ReportingAPI.setStatusStopped(), + ReportingAPI.setStatusInterrupted(), + ReportingAPI.setStatusCancelled(), + ] }); ``` diff --git a/package.json b/package.json index b822edd..51beab7 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,29 @@ "name": "@reportportal/agent-js-playwright", "version": "5.1.4", "description": "Agent to integrate Playwright with ReportPortal.", - "main": "build/index.js", - "types": "build/index.d.ts", + "exports": { + ".": { + "node": "./build/index.js", + "import": "./build/index.js", + "require": "./build/index.js", + "default": "./build/index.js" + }, + "./promises": { + "node": "./build/promises/index.js", + "default": "./build/promises/index.js", + "require": "./build/promises/index.js", + "import": "./build/promises/index.js" + } + }, + "typesVersions": { + "*": { + "promises": [ + "./build/promises/index.d.ts" + ] + } + }, + "types": "./build/index.d.ts", + "main": "./build/index.js", "scripts": { "build": "npm run clean && tsc", "clean": "rimraf ./build", diff --git a/src/__tests__/promises/reportingApi.spec.ts b/src/__tests__/promises/reportingApi.spec.ts new file mode 100644 index 0000000..b2b44db --- /dev/null +++ b/src/__tests__/promises/reportingApi.spec.ts @@ -0,0 +1,192 @@ +/* + * Copyright 2021 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. + * + */ + +import { ReportingApi } from '../../promises'; +import * as utils from '../../utils'; +import { LOG_LEVELS, STATUSES } from '../../constants'; + +const reportingApiStatusMethods = [ + { method: 'setStatusPassed', status: 'passed' }, + { method: 'setStatusFailed', status: 'failed' }, + { method: 'setStatusSkipped', status: 'skipped' }, + { method: 'setStatusStopped', status: 'stopped' }, + { method: 'setStatusInterrupted', status: 'interrupted' }, + { method: 'setStatusCancelled', status: 'cancelled' }, + { method: 'setStatusInfo', status: 'info' }, + { method: 'setStatusWarn', status: 'warn' }, +]; + +const reportingApiLaunchStatusMethods = [ + { method: 'setLaunchStatusPassed', status: 'passed' }, + { method: 'setLaunchStatusFailed', status: 'failed' }, + { method: 'setLaunchStatusSkipped', status: 'skipped' }, + { method: 'setLaunchStatusStopped', status: 'stopped' }, + { method: 'setLaunchStatusInterrupted', status: 'interrupted' }, + { method: 'setLaunchStatusCancelled', status: 'cancelled' }, + { method: 'setLaunchStatusInfo', status: 'info' }, + { method: 'setLaunchStatusWarn', status: 'warn' }, +]; + +const reportingApiLogMethods = [ + { method: 'trace', level: 'TRACE' }, + { method: 'debug', level: 'DEBUG' }, + { method: 'info', level: 'INFO' }, + { method: 'warn', level: 'WARN' }, + { method: 'error', level: 'ERROR' }, + { method: 'fatal', level: 'FATAL' }, +]; + +const reportingApiLaunchLogMethods = [ + { method: 'launchTrace', level: 'TRACE' }, + { method: 'launchDebug', level: 'DEBUG' }, + { method: 'launchInfo', level: 'INFO' }, + { method: 'launchWarn', level: 'WARN' }, + { method: 'launchError', level: 'ERROR' }, + { method: 'launchFatal', level: 'FATAL' }, +]; + +describe('reportingApi', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('addAttributes should call sendEventToReporter with params', () => { + const attrs = [ + { + key: 'key', + value: 'value', + }, + ]; + const suite = 'suite'; + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + ReportingApi.addAttributes(attrs, suite); + + expect(spySendEventToReporter).toHaveBeenCalledTimes(1); + }); + + test('setDescription should call sendEventToReporter with params', () => { + const description = 'description'; + const suite = 'suite'; + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + ReportingApi.setDescription(description, suite); + + expect(spySendEventToReporter).toHaveBeenCalledTimes(1); + }); + + test('setTestCaseId should call sendEventToReporter with params', () => { + const testCaseId = 'TestCaseIdForTheSuite'; + const suite = 'suite'; + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + ReportingApi.setTestCaseId(testCaseId, suite); + + expect(spySendEventToReporter).toHaveBeenCalledTimes(1); + }); + + describe('Item status reporting', () => { + reportingApiStatusMethods.map(({ method, status }) => { + test(`${method} should call sendEventToReporter with ${status} status`, () => { + const suite = 'suite'; + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + // @ts-ignore + ReportingApi[method](suite); + + expect(spySendEventToReporter).toHaveBeenCalledTimes(1); + }); + }); + + test('ReportingApi.setStatus should call sendEventToReporter with provided status', () => { + const suite = 'suite'; + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + ReportingApi.setStatus(STATUSES.PASSED, suite); + + expect(spySendEventToReporter).toHaveBeenCalledTimes(1); + }); + }); + + describe('Launch status reporting', () => { + reportingApiLaunchStatusMethods.map(({ method, status }) => { + test(`${method} should call sendEventToReporter with ${status} status`, () => { + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + // @ts-ignore + ReportingApi[method](); + + expect(spySendEventToReporter).toHaveBeenCalledTimes(1); + }); + }); + + test('ReportingApi.setLaunchStatus should call sendEventToReporter with provided status', () => { + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + const status = 'PASSED'; + ReportingApi.setLaunchStatus(status); + + expect(spySendEventToReporter).toHaveBeenCalledTimes(1); + }); + }); + + describe('Logs reporting', () => { + const file = { + name: 'filename', + type: 'image/png', + content: Buffer.from([1, 2, 3, 4, 5, 6, 7]).toString('base64'), + }; + const suite = 'suite'; + + reportingApiLogMethods.map(({ method, level }) => { + test(`${method} should call ReportingApi.log with ${level} level`, () => { + const spyLogFunc = jest.spyOn(ReportingApi, 'log'); + + // @ts-ignore + ReportingApi[method]('message', file, suite); + + expect(spyLogFunc).toHaveBeenCalledTimes(1); + }); + }); + + test('ReportingApi.log should call sendEventToReporter with params', () => { + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + ReportingApi.log(LOG_LEVELS.INFO, 'message', file, suite); + + expect(spySendEventToReporter).toHaveBeenCalledTimes(1); + }); + }); + + describe('Launch logs reporting', () => { + const file = { + name: 'filename', + type: 'image/png', + content: Buffer.from([1, 2, 3, 4, 5, 6, 7]).toString('base64'), + }; + + reportingApiLaunchLogMethods.map(({ method, level }) => { + test(`${method} should call ReportingApi.launchLog with ${level} level`, () => { + const spyLogFunc = jest.spyOn(ReportingApi, 'launchLog'); + + // @ts-ignore + ReportingApi[method]('message', file); + + expect(spyLogFunc).toHaveBeenCalledTimes(1); + }); + }); + + test('ReportingApi.launchLog should call sendEventToReporter with params', () => { + const spySendEventToReporter = jest.spyOn(utils, 'sendEventToReporter'); + ReportingApi.launchLog(LOG_LEVELS.INFO, 'message', file); + + expect(spySendEventToReporter).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/__tests__/reporter/finishSuiteReporting.spec.ts b/src/__tests__/reporter/finishSuiteReporting.spec.ts index c136818..44c0cc9 100644 --- a/src/__tests__/reporter/finishSuiteReporting.spec.ts +++ b/src/__tests__/reporter/finishSuiteReporting.spec.ts @@ -32,6 +32,8 @@ describe('finish suites on finish all of their children', () => { const testCase = { title: 'testTitle', id: 'testItemId', + //@ts-ignore + results: [{ attachments: [] }], parent: { title: rootSuite, project: () => ({ name: rootSuite }), diff --git a/src/__tests__/reporter/finishTestItemReporting.spec.ts b/src/__tests__/reporter/finishTestItemReporting.spec.ts index 46ed03a..1eb821f 100644 --- a/src/__tests__/reporter/finishTestItemReporting.spec.ts +++ b/src/__tests__/reporter/finishTestItemReporting.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { EVENTS } from '@reportportal/client-javascript/lib/constants/events'; import { RPReporter } from '../../reporter'; import { mockConfig } from '../mocks/configMock'; import { RPClientMock } from '../mocks/RPClientMock'; @@ -27,6 +28,8 @@ describe('finish test reporting', () => { const testCase = { title: 'testTitle', id: 'testItemId', + //@ts-ignore + results: [{ attachments: [] }], parent: { title: rootSuite, project: () => ({ name: rootSuite }), @@ -99,8 +102,30 @@ describe('finish test reporting', () => { description: 'description', }; - // @ts-ignore - await reporter.onTestEnd({ ...testCase, outcome: () => 'expected' }, result); + await reporter.onTestEnd( + { + ...testCase, + outcome: () => 'expected', + results: [ + // @ts-ignore + { + attachments: [ + { + name: EVENTS.ADD_ATTRIBUTES, + body: Buffer.from(JSON.stringify([{ key: 'key', value: 'value' }])), + contentType: 'application/json', + }, + { + name: EVENTS.SET_DESCRIPTION, + body: Buffer.from('description'), + contentType: 'text/plain', + }, + ], + }, + ], + }, + result, + ); expect(reporter.client.finishTestItem).toHaveBeenCalledTimes(3); expect(reporter.client.finishTestItem).toHaveBeenNthCalledWith( @@ -124,7 +149,30 @@ describe('finish test reporting', () => { issue: { issueType: 'NOT_ISSUE' }, }; // @ts-ignore - await reporter.onTestEnd({ ...testCase, outcome: () => 'skipped' }, result); + await reporter.onTestEnd( + { + ...testCase, + outcome: () => 'skipped', + results: [ + // @ts-ignore + { + attachments: [ + { + name: EVENTS.ADD_ATTRIBUTES, + body: Buffer.from(JSON.stringify([{ key: 'key', value: 'value' }])), + contentType: 'application/json', + }, + { + name: EVENTS.SET_DESCRIPTION, + body: Buffer.from('description'), + contentType: 'text/plain', + }, + ], + }, + ], + }, + result, + ); expect(reporter.client.finishTestItem).toHaveBeenCalledTimes(3); expect(reporter.client.finishTestItem).toHaveBeenNthCalledWith( @@ -189,4 +237,72 @@ describe('finish test reporting', () => { expect(reporter.client.finishTestItem).toHaveBeenCalledWith('1214r1', finishStepObject); }); + + test('client.finishTestItem should call reporter.client.finishTestItem with correct values', async () => { + const result = { status: 'passed' }; + + await reporter.onTestEnd( + { + ...testCase, + outcome: () => 'expected', + results: [ + // @ts-ignore + { + attachments: [ + { + name: EVENTS.ADD_ATTRIBUTES, + contentType: 'application/json', + body: Buffer.from( + JSON.stringify([ + { key: 'key1', value: 'value1', system: false }, + { key: 'key2', value: 'value2', system: false }, + ]), + ), + }, + { + name: EVENTS.SET_DESCRIPTION, + contentType: 'plain/text', + body: Buffer.from('Description'), + }, + { + name: EVENTS.SET_STATUS, + contentType: 'plain/text', + body: Buffer.from('skipped'), + }, + { + name: EVENTS.SET_STATUS, + contentType: 'plain/text', + body: Buffer.from('interrupted'), + }, + { + name: EVENTS.SET_TEST_CASE_ID, + contentType: 'plain/text', + body: Buffer.from('testCaseId'), + }, + { + name: 'notAllowedField', + contentType: 'plain/text', + body: Buffer.from('notAllowedValue'), + }, + ], + }, + ], + }, + result, + ); + + const finishStepObject: FinishTestItemObjType = { + endTime: reporter.client.helpers.now(), + status: STATUSES.INTERRUPTED, + attributes: [{ key: 'key', value: 'value' }], + description: 'description', + testCaseId: 'testCaseId', + }; + + expect(reporter.client.finishTestItem).toHaveBeenNthCalledWith( + 1, + 'tempTestItemId', + finishStepObject, + ); + }); }); diff --git a/src/__tests__/reporter/logReporting.spec.ts b/src/__tests__/reporter/logReporting.spec.ts index dc5bbaa..b0ea2dd 100644 --- a/src/__tests__/reporter/logReporting.spec.ts +++ b/src/__tests__/reporter/logReporting.spec.ts @@ -110,6 +110,7 @@ describe('logs reporting', () => { const testCase = { title: 'testTitle', id: 'testItemId', + results: [{ attachments: [{}] }], parent: { title: playwrightProjectName, location: 'tests/example.js', diff --git a/src/__tests__/utils.spec.ts b/src/__tests__/utils.spec.ts index 72addaf..f416d58 100644 --- a/src/__tests__/utils.spec.ts +++ b/src/__tests__/utils.spec.ts @@ -15,6 +15,7 @@ * */ +import { EVENTS } from '@reportportal/client-javascript/lib/constants/events'; // @ts-ignore import { name as pjsonName, version as pjsonVersion } from '../../package.json'; import { @@ -27,6 +28,7 @@ import { getAttachments, isErrorLog, calculateRpStatus, + getAdditionalInfo, } from '../utils'; import fs from 'fs'; import path from 'path'; @@ -36,6 +38,8 @@ import { BASIC_ATTACHMENT_CONTENT_TYPES, BASIC_ATTACHMENT_NAMES, } from '../constants'; +import { TestCase } from '@playwright/test/reporter'; +import { TestAdditionalInfo } from '../models/reporting'; describe('testing utils', () => { test('isFalse', () => { @@ -418,4 +422,112 @@ describe('testing utils', () => { expect(status).toBe(STATUSES.PASSED); }); }); + + describe('getAdditionalInfo', () => { + test('Should collect only allowed fields', () => { + const testCase = { + results: [ + { + attachments: [ + { + name: EVENTS.ADD_ATTRIBUTES, + contentType: 'application/json', + body: Buffer.from( + JSON.stringify([ + { key: 'key1', value: 'value1', system: true }, + { key: 'key2', value: 'value2', system: true }, + ]), + ), + }, + { + name: EVENTS.SET_DESCRIPTION, + contentType: 'plain/text', + body: Buffer.from('Description'), + }, + { + name: EVENTS.SET_STATUS, + contentType: 'plain/text', + body: Buffer.from('skipped'), + }, + { + name: EVENTS.SET_STATUS, + contentType: 'plain/text', + body: Buffer.from('failed'), + }, + { + name: EVENTS.SET_TEST_CASE_ID, + contentType: 'plain/text', + body: Buffer.from('testCaseId'), + }, + { + name: 'notAllowedField', + contentType: 'plain/text', + body: Buffer.from('notAllowedValue'), + }, + ], + }, + ], + }; + + const expectedResult: TestAdditionalInfo = { + attributes: [ + { key: 'key1', value: 'value1', system: true }, + { key: 'key2', value: 'value2', system: true }, + ], + description: 'Description', + status: 'failed', + testCaseId: 'testCaseId', + }; + + const additionalInfo = getAdditionalInfo(testCase as TestCase); + + expect(additionalInfo).toEqual(expectedResult); + }); + + test('Should recover from error in case if JSON is not valid', () => { + const testCase = { + results: [ + { + attachments: [ + { + name: EVENTS.ADD_ATTRIBUTES, + contentType: 'application/json', + body: Buffer.from( + `{ key: 'key1', value: 'value1', system: true }, + { key: 'key2', value: 'value2', system: true }`, + ), + }, + { + name: EVENTS.SET_DESCRIPTION, + contentType: 'plain/text', + body: Buffer.from('Description'), + }, + { + name: EVENTS.SET_STATUS, + contentType: 'plain/text', + body: Buffer.from('skipped'), + }, + ], + }, + ], + }; + + const expectedResult: TestAdditionalInfo = { + attributes: [], + description: 'Description', + status: 'skipped', + testCaseId: '', + }; + + const error = new Error('Unexpected token k in JSON at position 2'); + + console.error = jest.fn(); + + const additionalInfo = getAdditionalInfo(testCase as TestCase); + + expect(console.error).toBeCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(error.message); + expect(additionalInfo).toEqual(expectedResult); + }); + }); }); diff --git a/src/constants/index.ts b/src/constants/index.ts index 7eee66e..37a8201 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -19,6 +19,7 @@ export { LAUNCH_MODES } from './launchModes'; export { TEST_ITEM_TYPES } from './testItemTypes'; export { STATUSES } from './statuses'; export { LOG_LEVELS } from './logLevels'; +export { RPTestInfo, RpEventsToAdditionalInfoMap } from './testInfo'; export { TestAnnotation, TestOutcome, diff --git a/src/constants/testInfo.ts b/src/constants/testInfo.ts new file mode 100644 index 0000000..adb3ee8 --- /dev/null +++ b/src/constants/testInfo.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2023 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. + * + */ + +import { EVENTS } from '@reportportal/client-javascript/lib/constants/events'; + +export enum RPTestInfo { + STATUS = 'status', + ATTRIBUTES = 'attributes', + DESCRIPTION = 'description', + TEST_CASE_ID = 'testCaseId', +} + +export const RpEventsToAdditionalInfoMap = { + [EVENTS.ADD_ATTRIBUTES]: RPTestInfo.ATTRIBUTES, + [EVENTS.SET_DESCRIPTION]: RPTestInfo.DESCRIPTION, + [EVENTS.SET_TEST_CASE_ID]: RPTestInfo.TEST_CASE_ID, + [EVENTS.SET_STATUS]: RPTestInfo.STATUS, +}; diff --git a/src/index.ts b/src/index.ts index ce7e3dc..8e49b49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { RPReporter } from './reporter'; export { ReportingApi } from './reportingApi'; export { LOG_LEVELS, STATUSES } from './constants'; +export { ReportPortalConfig } from './models'; export default RPReporter; diff --git a/src/models/reporting.ts b/src/models/reporting.ts index c2021ae..3a60729 100644 --- a/src/models/reporting.ts +++ b/src/models/reporting.ts @@ -66,3 +66,10 @@ export interface LogRQ { export interface TestStepWithId extends TestStep { id: string; } + +export interface TestAdditionalInfo { + status?: string; + attributes?: Attribute[]; + description?: string; + testCaseId?: string; +} diff --git a/src/promises/index.ts b/src/promises/index.ts new file mode 100644 index 0000000..206037a --- /dev/null +++ b/src/promises/index.ts @@ -0,0 +1 @@ +export { ReportingApi } from './reportingApi'; diff --git a/src/promises/reportingApi.ts b/src/promises/reportingApi.ts new file mode 100644 index 0000000..c1fb134 --- /dev/null +++ b/src/promises/reportingApi.ts @@ -0,0 +1,137 @@ +/* + * Copyright 2023 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. + * + */ + +import { EVENTS } from '@reportportal/client-javascript/lib/constants/events'; +import { sendEventToReporter } from '../utils'; +import { Attribute } from '../models'; +import { STATUSES, LOG_LEVELS } from '../constants'; +import { Attachment } from '../models/reporting'; +import { test } from '@playwright/test'; + +export const ReportingApi = { + addAttributes: (attrs: Attribute[], suite?: string): Promise => { + if (suite) { + sendEventToReporter(EVENTS.ADD_ATTRIBUTES, attrs, suite); + + return Promise.resolve(); + } + + return test.info().attach(EVENTS.ADD_ATTRIBUTES, { + body: JSON.stringify(attrs), + contentType: 'application/json', + }); + }, + + setDescription: (description: string, suite?: string): Promise => { + if (suite) { + sendEventToReporter(EVENTS.SET_DESCRIPTION, description, suite); + + return Promise.resolve(); + } + + return test.info().attach(EVENTS.SET_DESCRIPTION, { + body: description, + contentType: 'text/plain', + }); + }, + setTestCaseId: (testCaseId: string, suite?: string): Promise => { + if (suite) { + sendEventToReporter(EVENTS.SET_TEST_CASE_ID, testCaseId, suite); + + return Promise.resolve(); + } + + return test.info().attach(EVENTS.SET_TEST_CASE_ID, { + body: testCaseId, + contentType: 'text/plain', + }); + }, + setStatus: (status: STATUSES, suite?: string): Promise => { + if (suite) { + sendEventToReporter(EVENTS.SET_STATUS, status, suite); + + return Promise.resolve(); + } + + return test.info().attach(EVENTS.SET_STATUS, { + body: status, + contentType: 'text/plain', + }); + }, + setStatusPassed: (suite?: string): Promise => + ReportingApi.setStatus(STATUSES.PASSED, suite), + setStatusFailed: (suite?: string): Promise => + ReportingApi.setStatus(STATUSES.FAILED, suite), + setStatusSkipped: (suite?: string): Promise => + ReportingApi.setStatus(STATUSES.SKIPPED, suite), + setStatusStopped: (suite?: string): Promise => + ReportingApi.setStatus(STATUSES.STOPPED, suite), + setStatusInterrupted: (suite?: string): Promise => + ReportingApi.setStatus(STATUSES.INTERRUPTED, suite), + setStatusCancelled: (suite?: string): Promise => + ReportingApi.setStatus(STATUSES.CANCELLED, suite), + setStatusInfo: (suite?: string): Promise => ReportingApi.setStatus(STATUSES.INFO, suite), + setStatusWarn: (suite?: string): Promise => ReportingApi.setStatus(STATUSES.WARN, suite), + + setLaunchStatus: (status: keyof typeof STATUSES): void => + sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, status), + setLaunchStatusPassed: (): void => sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, STATUSES.PASSED), + setLaunchStatusFailed: (): void => sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, STATUSES.FAILED), + setLaunchStatusSkipped: (): void => + sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, STATUSES.SKIPPED), + setLaunchStatusStopped: (): void => + sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, STATUSES.STOPPED), + setLaunchStatusInterrupted: (): void => + sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, STATUSES.INTERRUPTED), + setLaunchStatusCancelled: (): void => + sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, STATUSES.CANCELLED), + setLaunchStatusInfo: (): void => sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, STATUSES.INFO), + setLaunchStatusWarn: (): void => sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, STATUSES.WARN), + + log: ( + level: LOG_LEVELS = LOG_LEVELS.INFO, + message = '', + file?: Attachment, + suite?: string, + ): void => sendEventToReporter(EVENTS.ADD_LOG, { level, message, file }, suite), + launchLog: (level: LOG_LEVELS = LOG_LEVELS.INFO, message = '', file?: Attachment): void => + sendEventToReporter(EVENTS.ADD_LAUNCH_LOG, { level, message, file }), + trace: (message: string, file?: Attachment, suite?: string): void => + ReportingApi.log(LOG_LEVELS.TRACE, message, file, suite), + debug: (message: string, file?: Attachment, suite?: string): void => + ReportingApi.log(LOG_LEVELS.DEBUG, message, file, suite), + info: (message: string, file?: Attachment, suite?: string): void => + ReportingApi.log(LOG_LEVELS.INFO, message, file, suite), + warn: (message: string, file?: Attachment, suite?: string): void => + ReportingApi.log(LOG_LEVELS.WARN, message, file, suite), + error: (message: string, file?: Attachment, suite?: string): void => + ReportingApi.log(LOG_LEVELS.ERROR, message, file, suite), + fatal: (message: string, file?: Attachment, suite?: string): void => + ReportingApi.log(LOG_LEVELS.FATAL, message, file, suite), + launchTrace: (message: string, file?: Attachment): void => + ReportingApi.launchLog(LOG_LEVELS.TRACE, message, file), + launchDebug: (message: string, file?: Attachment): void => + ReportingApi.launchLog(LOG_LEVELS.DEBUG, message, file), + launchInfo: (message: string, file?: Attachment): void => + ReportingApi.launchLog(LOG_LEVELS.INFO, message, file), + launchWarn: (message: string, file?: Attachment): void => + ReportingApi.launchLog(LOG_LEVELS.WARN, message, file), + launchError: (message: string, file?: Attachment): void => + ReportingApi.launchLog(LOG_LEVELS.ERROR, message, file), + launchFatal: (message: string, file?: Attachment): void => + ReportingApi.launchLog(LOG_LEVELS.FATAL, message, file), +}; diff --git a/src/reporter.ts b/src/reporter.ts index 152be5a..0437792 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -37,6 +37,7 @@ import { } from './constants'; import { calculateRpStatus, + getAdditionalInfo, getAgentInfo, getAttachments, getCodeRef, @@ -344,8 +345,6 @@ export class RPReporter implements Reporter { ...(status && { status }), ...(logs && { logs }), // TODO: may be send it on suite start }); - - this.suitesInfo.delete(currentSuiteTitle); } return projectName; @@ -452,17 +451,22 @@ export class RPReporter implements Reporter { if (!savedTestItem) { return Promise.resolve(); } + + const additionalInfo = getAdditionalInfo(test); + const { id: testItemId, - attributes, - description, - testCaseId, - status: predefinedStatus, + attributes = additionalInfo.attributes, + description = additionalInfo.description, + testCaseId = additionalInfo.testCaseId, + status: predefinedStatus = additionalInfo.status, } = 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); } @@ -489,7 +493,7 @@ export class RPReporter implements Reporter { level: LOG_LEVELS.ERROR, message: stacktrace, }); - testDescription = (description || '').concat(`\n\`\`\`error\n${stacktrace}\n\`\`\``); + testDescription = description.concat(`\n\`\`\`error\n${stacktrace}\n\`\`\``); } [...this.nestedSteps.entries()].forEach(([key, value]) => { @@ -511,7 +515,7 @@ export class RPReporter implements Reporter { endTime: this.client.helpers.now(), status, ...(withoutIssue && { issue: { issueType: 'NOT_ISSUE' } }), - ...(attributes && { attributes }), + ...(attributes.length !== 0 && { attributes }), ...(testDescription && { description: testDescription }), ...(testCaseId && { testCaseId }), }; diff --git a/src/reportingApi.ts b/src/reportingApi.ts index e36ccd9..32d6cf1 100644 --- a/src/reportingApi.ts +++ b/src/reportingApi.ts @@ -21,31 +21,106 @@ import { Attribute } from './models'; import { STATUSES, LOG_LEVELS } from './constants'; import { Attachment } from './models/reporting'; +const getDepreciationMessage = (methodName: string): string => + `This method is deprecated. Use ${methodName} from @reportportal/agent-js-playwright/promises instead`; + export const ReportingApi = { - addAttributes: (attrs: Attribute[], suite?: string): void => - sendEventToReporter(EVENTS.ADD_ATTRIBUTES, attrs, suite), - setDescription: (description: string, suite?: string): void => - sendEventToReporter(EVENTS.SET_DESCRIPTION, description, suite), - setTestCaseId: (testCaseId: string, suite?: string): void => - sendEventToReporter(EVENTS.SET_TEST_CASE_ID, testCaseId, suite), - setStatus: (status: keyof typeof STATUSES, suite?: string): void => - sendEventToReporter(EVENTS.SET_STATUS, status, suite), - setStatusPassed: (suite?: string): void => - sendEventToReporter(EVENTS.SET_STATUS, STATUSES.PASSED, suite), - setStatusFailed: (suite?: string): void => - sendEventToReporter(EVENTS.SET_STATUS, STATUSES.FAILED, suite), - setStatusSkipped: (suite?: string): void => - sendEventToReporter(EVENTS.SET_STATUS, STATUSES.SKIPPED, suite), - setStatusStopped: (suite?: string): void => - sendEventToReporter(EVENTS.SET_STATUS, STATUSES.STOPPED, suite), - setStatusInterrupted: (suite?: string): void => - sendEventToReporter(EVENTS.SET_STATUS, STATUSES.INTERRUPTED, suite), - setStatusCancelled: (suite?: string): void => - sendEventToReporter(EVENTS.SET_STATUS, STATUSES.CANCELLED, suite), - setStatusInfo: (suite?: string): void => - sendEventToReporter(EVENTS.SET_STATUS, STATUSES.INFO, suite), - setStatusWarn: (suite?: string): void => - sendEventToReporter(EVENTS.SET_STATUS, STATUSES.WARN, suite), + /** + * @deprecated Use `addAttributes` from `@reportportal/agent-js-playwright/promises` instead + */ + addAttributes: (attrs: Attribute[], suite?: string): void => { + console.warn(getDepreciationMessage('addAttributes')); + sendEventToReporter(EVENTS.ADD_ATTRIBUTES, attrs, suite); + }, + + /** + * @deprecated Use `setDescription` from `@reportportal/agent-js-playwright/promises` instead + */ + setDescription: (description: string, suite?: string): void => { + console.warn(getDepreciationMessage('setDescription')); + sendEventToReporter(EVENTS.SET_DESCRIPTION, description, suite); + }, + + /** + * @deprecated Use `setTestCaseId` from `@reportportal/agent-js-playwright/promises` instead + */ + setTestCaseId: (testCaseId: string, suite?: string): void => { + console.warn(getDepreciationMessage('setTestCaseId')); + sendEventToReporter(EVENTS.SET_TEST_CASE_ID, testCaseId, suite); + }, + + /** + * @deprecated Use `setStatus` from `@reportportal/agent-js-playwright/promises` instead + */ + setStatus: (status: keyof typeof STATUSES, suite?: string): void => { + console.warn(getDepreciationMessage('setStatus')); + sendEventToReporter(EVENTS.SET_STATUS, status, suite); + }, + + /** + * @deprecated Use `setStatusPassed` from `@reportportal/agent-js-playwright/promises` instead + */ + setStatusPassed: (suite?: string): void => { + console.warn(getDepreciationMessage('setStatusPassed')); + sendEventToReporter(EVENTS.SET_STATUS, STATUSES.PASSED, suite); + }, + + /** + * @deprecated Use `setStatusFailed` from `@reportportal/agent-js-playwright/promises` instead + */ + setStatusFailed: (suite?: string): void => { + console.warn(getDepreciationMessage('setStatusPassed')); + sendEventToReporter(EVENTS.SET_STATUS, STATUSES.FAILED, suite); + }, + + /** + * @deprecated Use `setStatusSkipped` from `@reportportal/agent-js-playwright/promises` instead + */ + setStatusSkipped: (suite?: string): void => { + console.warn(getDepreciationMessage('setStatusSkipped')); + sendEventToReporter(EVENTS.SET_STATUS, STATUSES.SKIPPED, suite); + }, + + /** + * @deprecated Use `setStatusStopped` from `@reportportal/agent-js-playwright/promises` instead + */ + setStatusStopped: (suite?: string): void => { + console.warn(getDepreciationMessage('setStatusStopped')); + sendEventToReporter(EVENTS.SET_STATUS, STATUSES.STOPPED, suite); + }, + + /** + * @deprecated Use `setStatusInterrupted` from `@reportportal/agent-js-playwright/promises` instead + */ + setStatusInterrupted: (suite?: string): void => { + console.warn(getDepreciationMessage('setStatusInterrupted')); + sendEventToReporter(EVENTS.SET_STATUS, STATUSES.INTERRUPTED, suite); + }, + + /** + * @deprecated Use `setStatusCancelled` from `@reportportal/agent-js-playwright/promises` instead + */ + setStatusCancelled: (suite?: string): void => { + console.warn(getDepreciationMessage('setStatusCancelled')); + sendEventToReporter(EVENTS.SET_STATUS, STATUSES.CANCELLED, suite); + }, + + /** + * @deprecated Use `setStatusInfo` from `@reportportal/agent-js-playwright/promises` instead + */ + setStatusInfo: (suite?: string): void => { + console.warn(getDepreciationMessage('setStatusInfo')); + sendEventToReporter(EVENTS.SET_STATUS, STATUSES.INFO, suite); + }, + + /** + * @deprecated Use `setStatusWarn` from `@reportportal/agent-js-playwright/promises` instead + */ + setStatusWarn: (suite?: string): void => { + console.warn(getDepreciationMessage('setStatusWarn')); + sendEventToReporter(EVENTS.SET_STATUS, STATUSES.WARN, suite); + }, + setLaunchStatus: (status: keyof typeof STATUSES): void => sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, status), setLaunchStatusPassed: (): void => sendEventToReporter(EVENTS.SET_LAUNCH_STATUS, STATUSES.PASSED), diff --git a/src/utils.ts b/src/utils.ts index 1a29c85..f15b648 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,6 +15,7 @@ * */ +import { EVENTS } from '@reportportal/client-javascript/lib/constants/events'; import { TestCase, TestStatus, TestResult } from '@playwright/test/reporter'; import fs from 'fs'; import path from 'path'; @@ -29,7 +30,9 @@ import { BASIC_ATTACHMENT_CONTENT_TYPES, TEST_ANNOTATION_TYPES, TEST_OUTCOME_TYPES, + RpEventsToAdditionalInfoMap, } from './constants'; +import { TestAdditionalInfo } from './models/reporting'; const fsPromises = fs.promises; @@ -123,13 +126,13 @@ export const getAttachments = async ( fileContent = body; } else { if (!fs.existsSync(attachmentPath)) { - return; + return undefined; } fileContent = await fsPromises.readFile(attachmentPath); } } catch (e) { console.error(e); - return; + return undefined; } return { @@ -175,3 +178,36 @@ export const calculateRpStatus = ( return calculatedStatus; }; + +export const getAdditionalInfo = (test: TestCase): TestAdditionalInfo => { + const initialValue: TestAdditionalInfo = { + attributes: [], + description: '', + testCaseId: '', + status: '', + }; + + return test.results.reduce( + (additionalInfo, { attachments = [] }) => + Object.assign( + additionalInfo, + attachments.reduce((acc, { name, body }) => { + if (name in RpEventsToAdditionalInfoMap) { + try { + const value = body.toString(); + + return Object.assign(acc, { + [RpEventsToAdditionalInfoMap[name]]: + name === EVENTS.ADD_ATTRIBUTES ? JSON.parse(value) : value, + }); + } catch (error: unknown) { + console.error((error as Error).message); + } + } + + return acc; + }, initialValue), + ), + initialValue, + ); +};