From e970e5c721205b94c726f0bb89b3cb6cfa4476d4 Mon Sep 17 00:00:00 2001 From: Abdelrhman Arnos Date: Fri, 8 Mar 2024 15:14:24 +0100 Subject: [PATCH] EPMRPP-84613 || Playwright tags are parsed to the RP's attributes and attached to the tests (#133) * refactor: EPMRPP-84613 Playwright tags are parsed to the RP's attributes and attached to the tests * feat: unit-testing for tags * feat: extract tags from title * refactor: use grep & grepInvert from pw config * refactor: update the tag tests * feat: consider @key:value title attributes * style: format reporter.ts * test: fix client.startTestItem should be called with retry=true params * test: update tags tests in startSuiteTestReporting.spec.ts * test: update tags tests in startSuiteTestReporting.spec.ts * test: fix the tests of the tags * Update retriesReporting.spec.ts * Update src/__tests__/reporter/startSuiteTestReporting.spec.ts Co-authored-by: Ilya * Update src/__tests__/reporter/startSuiteTestReporting.spec.ts Co-authored-by: Ilya * doc: tag for test title in add attributes * Update README.md Co-authored-by: Ilya * Update README.md Co-authored-by: Ilya * Update README.md Co-authored-by: Ilya * doc: grep options under Reporting section in README * doc: update advanced Test Filtering with grep Options in README * doc: rm markdown in README --------- Co-authored-by: Ilya --- CHANGELOG.md | 2 + README.md | 245 +++++++++++------- .../reporter/retriesReporting.spec.ts | 4 +- .../reporter/startSuiteTestReporting.spec.ts | 154 +++++++---- src/reporter.ts | 52 +++- 5 files changed, 314 insertions(+), 143 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 499ab25..7737ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +### Added +- The Playwright's tags are parsed to the RP's attributes and attached to the tests. ## [5.1.7] - 2024-02-21 ### Changed diff --git a/README.md b/README.md index dc9f1ac..d9f034a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ # @reportportal/agent-js-playwright Agent to integrate Playwright with ReportPortal. -* More about [Playwright](https://playwright.dev/) -* More about [ReportPortal](http://reportportal.io/) + +- More about [Playwright](https://playwright.dev/) +- More about [ReportPortal](http://reportportal.io/) ## Installation + Install the agent in your project: + ```cmd npm install --save-dev @reportportal/agent-js-playwright ``` @@ -13,37 +16,38 @@ npm install --save-dev @reportportal/agent-js-playwright ## Configuration **1.** Create `playwright.config.ts` or `*.config.js` file with reportportal configuration: -```typescript - import { PlaywrightTestConfig } from '@playwright/test'; - - const RPconfig = { - apiKey: '00000000-0000-0000-0000-000000000000', - endpoint: 'https://your.reportportal.server/api/v1', - project: 'Your reportportal project name', - launch: 'Your launch name', - attributes: [ - { - key: 'key', - value: 'value', - }, - { - value: 'value', - }, - ], - description: 'Your launch description', - }; - const config: PlaywrightTestConfig = { - reporter: [['@reportportal/agent-js-playwright', RPconfig]], - testDir: './tests', - }; - export default config; +```typescript +import { PlaywrightTestConfig } from '@playwright/test'; + +const RPconfig = { + apiKey: '00000000-0000-0000-0000-000000000000', + endpoint: 'https://your.reportportal.server/api/v1', + project: 'Your reportportal project name', + launch: 'Your launch name', + attributes: [ + { + key: 'key', + value: 'value', + }, + { + value: 'value', + }, + ], + description: 'Your launch description', +}; + +const config: PlaywrightTestConfig = { + reporter: [['@reportportal/agent-js-playwright', RPconfig]], + testDir: './tests', +}; +export default config; ``` The full list of available options presented below. | Option | Necessity | Default | Description | -|---------------------------------------------|------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ------------------------------------------- | ---------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | apiKey | Required | | User's reportportal token from which you want to send requests. It can be found on the profile page of this user. | | endpoint | Required | | URL of your server. For example 'https://server:8080/api/v1'. | | launch | Required | | Name of launch at creation. | @@ -52,27 +56,28 @@ The full list of available options presented below. | description | Optional | '' | Launch description. | | rerun | Optional | false | Enable [rerun](https://reportportal.io/docs/dev-guides/RerunDevelopersGuide) | | rerunOf | Optional | Not set | UUID of launch you want to rerun. If not specified, reportportal will update the latest launch with the same name | -| mode | Optional | 'DEFAULT' | Results will be submitted to Launches page
*'DEBUG'* - Results will be submitted to Debug page. | -| 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. | +| mode | Optional | 'DEFAULT' | Results will be submitted to Launches page
_'DEBUG'_ - Results will be submitted to Debug page. | +| 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. | +| 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 | The object with `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, may contain other client options eg. [`timeout`](https://github.com/reportportal/client-javascript#timeout-30000ms-on-axios-requests).
Visit [client-javascript](https://github.com/reportportal/client-javascript) for more details. | | launchUuidPrint | Optional | false | Whether to print the current launch UUID. | | launchUuidPrintOutput | Optional | 'STDOUT' | Launch UUID printing output. Possible values: 'STDOUT', 'STDERR'. Works only if `launchUuidPrint` set to `true`. | | includeTestSteps | Optional | false | Allows you to see the test steps at the log level. | | includePlaywrightProjectNameToCodeReference | Optional | false | Includes Playwright project name to code reference. See [`testCaseId and codeRef calculation`](#setTestCaseId). It may be useful when you want to see the different history for the same test cases within different playwright projects. | -| extendTestDescriptionWithLastError | Optional | true | If set to `true` the latest error log will be attached to the test case description. | +| extendTestDescriptionWithLastError | Optional | true | If set to `true` the latest error log will be attached to the test case description. | | uploadVideo | Optional | true | Whether to attach the Playwright's [video](https://playwright.dev/docs/api/class-testoptions#test-options-video) to the test case. | | uploadTrace | Optional | true | Whether to attach the Playwright's [trace](https://playwright.dev/docs/api/class-testoptions#test-options-trace) to the test case. | | token | Deprecated | Not set | Use `apiKey` instead. | The following options can be overridden using ENVIRONMENT variables: -| Option | ENV variable | -|-------------|-----------------| -| launchId | RP_LAUNCH_ID | +| Option | ENV variable | +| -------- | ------------ | +| launchId | RP_LAUNCH_ID | **2.** Add script to `package.json` file: + ```json { "scripts": { @@ -87,6 +92,26 @@ When organizing tests, specify titles for `test.describe` blocks, as this is nec It is also required to specify playwright project names in `playwright.config.ts` when running the same tests in different playwright projects. +### Advanced Test Filtering with grep Options + +The `@reportportal/agent-js-playwright` integration recognizes `grep` and `grepInvert` from Playwright for test filtering. These options, when used, are automatically attached as launch attributes in ReportPortal for targeted test execution. + +Refer to Playwright documentation for [`grep`](https://playwright.dev/docs/api/class-testconfig#test-config-grep) and [`grepInvert`](https://playwright.dev/docs/api/class-testconfig#test-config-grep-invert) to learn about their usage. + +``` +**Example usage in `playwright.config.ts`:** + +```javascript +const config: PlaywrightTestConfig = { + grep: '@fast', // Only run tests tagged with @fast + grepInvert: '@slow', // Run tests except those tagged with @slow + reporter: [['@reportportal/agent-js-playwright', RPconfig]], + testDir: './tests', +}; +export default config; +``` +```` + ### Attachments Attachments can be easily added during test run via `testInfo.attach` according to the Playwright [docs](https://playwright.dev/docs/api/class-testinfo#test-info-attach). @@ -103,11 +128,11 @@ test('basic test', async ({ page }, testInfo) => { }); ``` -*Note:* attachment path can be provided instead of body. +_Note:_ attachment path can be provided instead of body. As an alternative to this approach the [`ReportingAPI`](#log) methods can be used. -*Note:* [`ReportingAPI`](#log) methods will send attachments to ReportPortal right after their call, unlike attachments provided via `testInfo.attach` that will be reported only on the test item finish. +_Note:_ [`ReportingAPI`](#log) methods will send attachments to ReportPortal right after their call, unlike attachments provided via `testInfo.attach` that will be reported only on the test item finish. ### Logging @@ -132,6 +157,7 @@ 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'`: + ```javascript import { ReportingApi } from '@reportportal/agent-js-playwright'; ``` @@ -143,11 +169,13 @@ All ReportingApi methods have an optional _suite_ parameter.
If you want to add a data to the suite, you must pass the suite name as the last parameter. ##### addAttributes + Add attributes (tags) to the current test. Should be called inside of corresponding test.
`ReportingApi.addAttributes(attributes: Array, suite?: string);`
**required**: `attributes`
**optional**: `suite`
Example: + ```javascript test('should have the correct attributes', () => { ReportingApi.addAttributes([ @@ -163,13 +191,25 @@ test('should have the correct attributes', () => { }); ``` +You can now add test attributes in test titles using `@tag` notation. This provides a succinct way to include metadata in test reports. Any `@tag` in a playwright test title will be used as an attribute in ReportPortal, but won't appear in the final test title on ReportPortal. + +Example: + +```javascript +test('@tag should have a tag at the beginning of the test title', () => { + expect(true).toBe(true); +}); +``` + ##### 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);`
**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'); @@ -178,14 +218,16 @@ test('should have the correct testCaseId', () => { ``` ##### log + Send logs to report portal for the current test. Should be called inside of corresponding test.
`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_
Example: + ```javascript -test('should contain logs with attachments',() => { +test('should contain logs with attachments', () => { const fileName = 'test.jpg'; const fileContent = fs.readFileSync(path.resolve(__dirname, './attachments', fileName)); const attachment = { @@ -200,6 +242,7 @@ test('should contain logs with attachments',() => { ``` ##### info, debug, warn, error, trace, fatal + Send logs with corresponding level to report portal for the current test. Should be called inside of corresponding test.
`ReportingApi.info(message: string, file?: Attachment, suite?: string);`
`ReportingApi.debug(message: string, file?: Attachment, suite?: string);`
@@ -210,26 +253,29 @@ Send logs with corresponding level to report portal for the current test. Should **required**: `message`
**optional**: `file`, `suite`
Example: + ```javascript test('should contain logs with attachments', () => { - ReportingApi.info('Log message'); - ReportingApi.debug('Log message'); - ReportingApi.warn('Log message'); - ReportingApi.error('Log message'); - ReportingApi.trace('Log message'); - ReportingApi.fatal('Log message'); - - expect(true).toBe(true); + ReportingApi.info('Log message'); + ReportingApi.debug('Log message'); + ReportingApi.warn('Log message'); + ReportingApi.error('Log message'); + ReportingApi.trace('Log message'); + ReportingApi.fatal('Log message'); + + expect(true).toBe(true); }); ``` ##### launchLog + Send logs to report portal for the current launch. Should be called inside of the any test or suite.
`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_
Example: + ```javascript test('should contain logs with attachments', async () => { const fileName = 'test.jpg'; @@ -246,6 +292,7 @@ test('should contain logs with attachments', async () => { ``` ##### launchInfo, launchDebug, launchWarn, launchError, launchTrace, launchFatal + Send logs with corresponding level to report portal for the current launch. Should be called inside of the any test or suite.
`ReportingApi.launchInfo(message: string, file?: Attachment);`
`ReportingApi.launchDebug(message: string, file?: Attachment);`
@@ -256,35 +303,39 @@ Send logs with corresponding level to report portal for the current launch. Shou **required**: `message`
**optional**: `file`
Example: + ```javascript test('should contain logs with attachments', () => { - ReportingApi.launchInfo('Log message'); - ReportingApi.launchDebug('Log message'); - ReportingApi.launchWarn('Log message'); - ReportingApi.launchError('Log message'); - ReportingApi.launchTrace('Log message'); - ReportingApi.launchFatal('Log message'); - - expect(true).toBe(true); + ReportingApi.launchInfo('Log message'); + ReportingApi.launchDebug('Log message'); + ReportingApi.launchWarn('Log message'); + ReportingApi.launchError('Log message'); + ReportingApi.launchTrace('Log message'); + ReportingApi.launchFatal('Log message'); + + expect(true).toBe(true); }); ``` ##### setStatus + Assign corresponding status to the current test item. Should be called inside of corresponding test.
`ReportingApi.setStatus(status: string, suite?: string);`
**required**: `status`
**optional**: `suite`
-where `status` must be one of the following: *passed*, *failed*, *stopped*, *skipped*, *interrupted*, *cancelled*
+where `status` must be one of the following: _passed_, _failed_, _stopped_, _skipped_, _interrupted_, _cancelled_
Example: + ```javascript test('should have status FAILED', () => { - ReportingApi.setStatus('failed'); - - expect(true).toBe(true); + ReportingApi.setStatus('failed'); + + expect(true).toBe(true); }); ``` ##### 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);`
@@ -294,31 +345,35 @@ Assign corresponding status to the current test item. Should be called inside of `ReportingApi.setStatusCancelled(suite?: string);`
**optional**: `suite`
Example: + ```javascript test('should call ReportingApi to set statuses', () => { - ReportingAPI.setStatusFailed(); - ReportingAPI.setStatusPassed(); - ReportingAPI.setStatusSkipped(); - ReportingAPI.setStatusStopped(); - ReportingAPI.setStatusInterrupted(); - ReportingAPI.setStatusCancelled(); + ReportingAPI.setStatusFailed(); + ReportingAPI.setStatusPassed(); + ReportingAPI.setStatusSkipped(); + ReportingAPI.setStatusStopped(); + ReportingAPI.setStatusInterrupted(); + ReportingAPI.setStatusCancelled(); }); ``` ##### setLaunchStatus + Assign corresponding status to the current launch. Should be called inside of the any test or suite.
`ReportingApi.setLaunchStatus(status: string);`
**required**: `status`
-where `status` must be one of the following: *passed*, *failed*, *stopped*, *skipped*, *interrupted*, *cancelled*
+where `status` must be one of the following: _passed_, _failed_, _stopped_, _skipped_, _interrupted_, _cancelled_
Example: + ```javascript -test('launch should have status FAILED', () => { - ReportingApi.setLaunchStatus('failed'); - expect(true).toBe(true); +test('launch should have status FAILED', () => { + ReportingApi.setLaunchStatus('failed'); + expect(true).toBe(true); }); ``` ##### setLaunchStatusFailed, setLaunchStatusPassed, setLaunchStatusSkipped, setLaunchStatusStopped, setLaunchStatusInterrupted, setLaunchStatusCancelled + Assign corresponding status to the current test item. Should be called inside of the any test or suite.
`ReportingApi.setLaunchStatusFailed();`
`ReportingApi.setLaunchStatusPassed();`
@@ -327,29 +382,33 @@ Assign corresponding status to the current test item. Should be called inside of `ReportingApi.setLaunchStatusInterrupted();`
`ReportingApi.setLaunchStatusCancelled();`
Example: + ```javascript test('should call ReportingApi to set launch statuses', () => { - ReportingAPI.setLaunchStatusFailed(); - ReportingAPI.setLaunchStatusPassed(); - ReportingAPI.setLaunchStatusSkipped(); - ReportingAPI.setLaunchStatusStopped(); - ReportingAPI.setLaunchStatusInterrupted(); - ReportingAPI.setLaunchStatusCancelled(); + ReportingAPI.setLaunchStatusFailed(); + ReportingAPI.setLaunchStatusPassed(); + ReportingAPI.setLaunchStatusSkipped(); + ReportingAPI.setLaunchStatusStopped(); + ReportingAPI.setLaunchStatusInterrupted(); + ReportingAPI.setLaunchStatusCancelled(); }); ``` ### Integration with Sauce Labs -To integrate with Sauce Labs just add attributes for the test case: +To integrate with Sauce Labs just add attributes for the test case: ```javascript -[{ - "key": "SLID", - "value": "# of the job in Sauce Labs" -}, { - "key": "SLDC", - "value": "EU (your job region in Sauce Labs)" -}] +[ + { + key: 'SLID', + value: '# of the job in Sauce Labs', + }, + { + key: 'SLDC', + value: 'EU (your job region in Sauce Labs)', + }, +]; ``` ## Issues troubleshooting @@ -360,17 +419,19 @@ There is known issue that in some cases launches not finished as expected in Rep This may happen in case of error thrown from `before`/`beforeAll` hooks, retries enabled and `fullyParallel: false`. Associated with [#85](https://github.com/reportportal/agent-js-playwright/issues/85).
In this case as a workaround we suggest to use `.skip()` and `.fixme()` annotations inside the test body: -use +use + ```javascript - test('example fail', async ({}) => { - test.fixme(); - expect(1).toBeGreaterThan(2); - }); +test('example fail', async ({}) => { + test.fixme(); + expect(1).toBeGreaterThan(2); +}); ``` -instead of +instead of + ```javascript - test.fixme('example fail', async ({}) => { - expect(1).toBeGreaterThan(2); - }); +test.fixme('example fail', async ({}) => { + expect(1).toBeGreaterThan(2); +}); ``` diff --git a/src/__tests__/reporter/retriesReporting.spec.ts b/src/__tests__/reporter/retriesReporting.spec.ts index 59a82ae..4c8c6be 100644 --- a/src/__tests__/reporter/retriesReporting.spec.ts +++ b/src/__tests__/reporter/retriesReporting.spec.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { RPReporter } from '../../reporter'; -import { mockConfig } from '../mocks/configMock'; import { RPClientMock } from '../mocks/RPClientMock'; +import { RPReporter } from '../../reporter'; import { StartTestObjType } from '../../models'; import { TEST_ITEM_TYPES } from '../../constants'; +import { mockConfig } from '../mocks/configMock'; describe('retries reporting', () => { const reporter = new RPReporter(mockConfig); diff --git a/src/__tests__/reporter/startSuiteTestReporting.spec.ts b/src/__tests__/reporter/startSuiteTestReporting.spec.ts index 76d02bb..78ef9dd 100644 --- a/src/__tests__/reporter/startSuiteTestReporting.spec.ts +++ b/src/__tests__/reporter/startSuiteTestReporting.spec.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { RPReporter } from '../../reporter'; -import { mockConfig } from '../mocks/configMock'; import { RPClientMock } from '../mocks/RPClientMock'; +import { RPReporter } from '../../reporter'; import { StartTestObjType } from '../../models'; import { TEST_ITEM_TYPES } from '../../constants'; +import { mockConfig } from '../mocks/configMock'; import path from 'path'; const rootSuite = 'tests/example.js'; @@ -27,6 +27,48 @@ const suiteName = 'suiteName'; describe('start reporting suite/test', () => { let reporter: RPReporter; let spyStartTestItem: jest.SpyInstance; + const expectedTestObj: StartTestObjType = { + name: 'testTitle', + type: TEST_ITEM_TYPES.STEP, + codeRef: 'tests/example.js/suiteName/testTitle', + retry: false, + }; + + const expectedParentSuiteObj: StartTestObjType = { + name: suiteName, + type: TEST_ITEM_TYPES.TEST, + codeRef: 'tests/example.js/suiteName', + }; + + const expectedRootParentSuiteObj: StartTestObjType = { + name: rootSuite, + type: TEST_ITEM_TYPES.SUITE, + codeRef: 'tests/example.js', + }; + + const expectedSuites = new Map([ + [ + rootSuite, + { + id: 'tempTestItemId', + name: rootSuite, + testInvocationsLeft: 1, + descendants: ['testItemId'], + }, + ], + [ + `${rootSuite}/${suiteName}`, + { + id: 'tempTestItemId', + name: suiteName, + descendants: ['testItemId'], + testInvocationsLeft: 1, + }, + ], + ]); + + const expectedTestItems = new Map([['testItemId', { id: 'tempTestItemId', name: 'testTitle' }]]); + const parentId = 'tempTestItemId'; const testCase = { title: 'testTitle', @@ -69,6 +111,10 @@ describe('start reporting suite/test', () => { reporter.launchId = 'tempLaunchId'; spyStartTestItem = jest.spyOn(reporter.client, 'startTestItem'); + + expectedTestObj.startTime = reporter.client.helpers.now(); + expectedParentSuiteObj.startTime = reporter.client.helpers.now(); + expectedRootParentSuiteObj.startTime = reporter.client.helpers.now(); }); afterEach(() => { @@ -76,50 +122,6 @@ describe('start reporting suite/test', () => { }); test('client.startTestItem should be called with corresponding params to report suites and test item', () => { - const expectedSuites = new Map([ - [ - rootSuite, - { - id: 'tempTestItemId', - name: rootSuite, - testInvocationsLeft: 1, - descendants: ['testItemId'], - }, - ], - [ - `${rootSuite}/${suiteName}`, - { - id: 'tempTestItemId', - name: suiteName, - descendants: ['testItemId'], - testInvocationsLeft: 1, - }, - ], - ]); - const expectedTestItems = new Map([ - ['testItemId', { id: 'tempTestItemId', name: 'testTitle' }], - ]); - const expectedRootParentSuiteObj: StartTestObjType = { - startTime: reporter.client.helpers.now(), - name: rootSuite, - type: TEST_ITEM_TYPES.SUITE, - codeRef: 'tests/example.js', - }; - const expectedParentSuiteObj: StartTestObjType = { - startTime: reporter.client.helpers.now(), - name: suiteName, - type: TEST_ITEM_TYPES.TEST, - codeRef: 'tests/example.js/suiteName', - }; - const expectedTestObj: StartTestObjType = { - startTime: reporter.client.helpers.now(), - name: 'testTitle', - type: TEST_ITEM_TYPES.STEP, - codeRef: 'tests/example.js/suiteName/testTitle', - retry: false, - }; - const parentId = 'tempTestItemId'; - // @ts-ignore reporter.onTestBegin(testCase); @@ -158,4 +160,64 @@ describe('start reporting suite/test', () => { expect(reporter.suites).toEqual(new Map()); expect(reporter.testItems).toEqual(new Map()); }); + + test('client.startTestItem should be called with corresponding params while one tag provided at the beginning of the test case title', () => { + // @ts-ignore + reporter.onTestBegin({ ...testCase, title: `@tag ${testCase.title}` }); + + // the first call for the root suite start + expect(spyStartTestItem).toHaveBeenNthCalledWith( + 1, + expectedRootParentSuiteObj, + reporter.launchId, + undefined, + ); + // the first call for the item parent suite start + expect(spyStartTestItem).toHaveBeenNthCalledWith( + 2, + expectedParentSuiteObj, + reporter.launchId, + parentId, + ); + // the third call for the test item start + expect(reporter.client.startTestItem).toHaveBeenNthCalledWith( + 3, + { ...expectedTestObj, attributes: [{ value: 'tag' }] }, + reporter.launchId, + parentId, + ); + expect(spyStartTestItem).toHaveBeenCalledTimes(3); + expect(reporter.suites).toEqual(expectedSuites); + expect(reporter.testItems).toEqual(expectedTestItems); + }); + + test('client.startTestItem should be called with corresponding params while one tag provided at the end of the test case title', () => { + // @ts-ignore + reporter.onTestBegin({ ...testCase, title: `${testCase.title} @tag` }); + + // the first call for the root suite start + expect(spyStartTestItem).toHaveBeenNthCalledWith( + 1, + expectedRootParentSuiteObj, + reporter.launchId, + undefined, + ); + // the first call for the item parent suite start + expect(spyStartTestItem).toHaveBeenNthCalledWith( + 2, + expectedParentSuiteObj, + reporter.launchId, + parentId, + ); + // the third call for the test item start + expect(reporter.client.startTestItem).toHaveBeenNthCalledWith( + 3, + { ...expectedTestObj, attributes: [{ value: 'tag' }] }, + reporter.launchId, + parentId, + ); + expect(spyStartTestItem).toHaveBeenCalledTimes(3); + expect(reporter.suites).toEqual(expectedSuites); + expect(reporter.testItems).toEqual(expectedTestItems); + }); }); diff --git a/src/reporter.ts b/src/reporter.ts index 5f995e9..0420de0 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -17,7 +17,13 @@ import RPClient from '@reportportal/client-javascript'; import stripAnsi from 'strip-ansi'; -import { Reporter, Suite as PWSuite, TestCase, TestResult } from '@playwright/test/reporter'; +import { + Reporter, + Suite as PWSuite, + TestCase, + TestResult, + FullConfig, +} from '@playwright/test/reporter'; import { Attribute, FinishTestItemObjType, @@ -266,7 +272,7 @@ export class RPReporter implements Reporter { }); } - onBegin(): void { + onBegin(config?: FullConfig): void { const { launch, description, attributes, skippedIssue, rerun, rerunOf, mode, launchId } = this.config; const systemAttributes: Attribute[] = getSystemAttributes(skippedIssue); @@ -282,6 +288,16 @@ export class RPReporter implements Reporter { mode: mode || LAUNCH_MODES.DEFAULT, id: launchId, }; + + // Extract grep and grepInvert from config and add as launch attributes + if (config?.grep) { + startLaunchObj.attributes.push({ key: 'grep', value: config.grep.toString() }); + } + + if (config?.grepInvert) { + startLaunchObj.attributes.push({ key: 'grepInvert', value: config.grepInvert.toString() }); + } + const { tempId, promise } = this.client.startLaunch(startLaunchObj); this.addRequestToPromisesQueue(promise, 'Failed to start launch.'); this.launchId = tempId; @@ -361,6 +377,12 @@ export class RPReporter implements Reporter { if (this.isLaunchFinishSend) { return; } + + const taggedTestTitle = test.title; + const untaggedTestTitle = this.#extractTagsFromTitle(test.title); + + test.title = untaggedTestTitle; + const playwrightProjectName = this.createSuites(test); const fullSuiteName = getCodeRef(test, test.parent.title); @@ -374,14 +396,20 @@ export class RPReporter implements Reporter { test.title, !includePlaywrightProjectNameToCodeReference && playwrightProjectName, ); + const { id: parentId } = parentSuiteObj; + const startTestItem: StartTestObjType = { - name: test.title, + name: untaggedTestTitle, startTime: this.client.helpers.now(), type: TEST_ITEM_TYPES.STEP, codeRef, retry: test.results?.length > 1, }; + + const attributes = this.#getAttributesFromTitle(taggedTestTitle); + if (attributes.length) startTestItem.attributes = attributes; + const stepObj = this.client.startTestItem(startTestItem, this.launchId, parentId); this.addRequestToPromisesQueue(stepObj.promise, 'Failed to start test.'); this.testItems.set(test.id, { @@ -635,4 +663,22 @@ export class RPReporter implements Reporter { printsToStdio(): boolean { return false; } + + #extractTagsFromTitle(title: string): string { + const tagRegex = /^@\w+\s*|\s*@\w+$/g; + const titleWithoutTags = title.replace(tagRegex, ''); + const trimmedTitle = titleWithoutTags.trim(); + + return trimmedTitle; + } + + #getAttributesFromTitle(title: string): Attribute[] { + const attributes = title.match(/@(\w+)(?::(\w+))?/g)?.map((tag) => { + const [key, value] = tag.slice(1).split(':'); + + return value ? { key, value } : { value: key }; + }); + + return attributes || []; + } }