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,
+ );
+};