diff --git a/CHANGELOG.md b/CHANGELOG.md index b8752cd..0c53154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ### Added +- `retryOf` property automatically populated with the UUID of the previous retry attempt when starting a retried test item. - Google Analytics improvements. ## [5.5.10] - 2026-02-05 diff --git a/__tests__/report-portal-client.spec.js b/__tests__/report-portal-client.spec.js index 4754279..0e127cf 100644 --- a/__tests__/report-portal-client.spec.js +++ b/__tests__/report-portal-client.spec.js @@ -92,6 +92,26 @@ describe('ReportPortal javascript client', () => { }); }); + describe('cleanItemRetriesChain', () => { + it('should clean itemRetriesChainLastTempIdMap alongside other maps', () => { + const client = new RPClient({ + apiKey: 'test', + project: 'test', + endpoint: 'https://abc.com', + }); + const key = 'launchId__parentId__name__'; + client.itemRetriesChainKeyMapByTempId.set('tempId1', key); + client.itemRetriesChainMap.set(key, Promise.resolve()); + client.itemRetriesChainLastTempIdMap.set(key, 'tempId1'); + + client.cleanItemRetriesChain(['tempId1']); + + expect(client.itemRetriesChainMap.has(key)).toBe(false); + expect(client.itemRetriesChainLastTempIdMap.has(key)).toBe(false); + expect(client.itemRetriesChainKeyMapByTempId.has('tempId1')).toBe(false); + }); + }); + describe('getRejectAnswer', () => { it('should return object with tempId and promise.reject with error', () => { const client = new RPClient({ @@ -879,6 +899,188 @@ describe('ReportPortal javascript client', () => { expect(client.itemRetriesChainMap.get).toHaveBeenCalledWith('id1__name__'); }); + + it('should include retryOf with the UUID of the previous retry in the request', async () => { + const client = new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }); + const previousRealId = 'previous-item-real-uuid'; + client.map = { + launchId: { + children: [], + promiseStart: Promise.resolve(), + realId: 'realLaunchId', + }, + }; + jest.spyOn(client.restClient, 'create').mockResolvedValue({ id: 'first-attempt-id' }); + jest.spyOn(client, 'getUniqId').mockReturnValueOnce('tempId1'); + + const firstItem = client.startTestItem( + { name: 'test', type: 'STEP', retry: false }, + 'launchId', + ); + await firstItem.promise; + + client.map[firstItem.tempId].realId = previousRealId; + + jest.spyOn(client.restClient, 'create').mockResolvedValue({ id: 'second-attempt-id' }); + jest.spyOn(client, 'getUniqId').mockReturnValueOnce('tempId2'); + + const retryItem = client.startTestItem( + { name: 'test', type: 'STEP', retry: true }, + 'launchId', + ); + await retryItem.promise; + + expect(client.restClient.create).toHaveBeenLastCalledWith( + 'item/', + expect.objectContaining({ + retryOf: previousRealId, + }), + ); + }); + + it('should not include retryOf when retry is false', async () => { + const client = new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }); + client.map = { + launchId: { + children: [], + promiseStart: Promise.resolve(), + realId: 'realLaunchId', + }, + }; + jest.spyOn(client.restClient, 'create').mockResolvedValue({ id: 'item-id' }); + jest.spyOn(client, 'getUniqId').mockReturnValueOnce('tempId1'); + + const item = client.startTestItem( + { name: 'test', type: 'STEP', retry: false }, + 'launchId', + ); + await item.promise; + + expect(client.restClient.create).toHaveBeenLastCalledWith( + 'item/', + expect.not.objectContaining({ + retryOf: expect.anything(), + }), + ); + }); + + it('should not include retryOf on the first attempt even with retry true', async () => { + const client = new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }); + client.map = { + launchId: { + children: [], + promiseStart: Promise.resolve(), + realId: 'realLaunchId', + }, + }; + jest.spyOn(client.restClient, 'create').mockResolvedValue({ id: 'item-id' }); + jest.spyOn(client, 'getUniqId').mockReturnValueOnce('tempId1'); + + const item = client.startTestItem( + { name: 'test', type: 'STEP', retry: true }, + 'launchId', + ); + await item.promise; + + expect(client.restClient.create).toHaveBeenLastCalledWith( + 'item/', + expect.not.objectContaining({ + retryOf: expect.anything(), + }), + ); + }); + + it('should update retryOf to last retry UUID in chain of multiple retries', async () => { + const client = new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }); + client.map = { + launchId: { + children: [], + promiseStart: Promise.resolve(), + realId: 'realLaunchId', + }, + }; + + jest.spyOn(client.restClient, 'create').mockResolvedValue({ id: 'uuid-attempt-1' }); + jest.spyOn(client, 'getUniqId').mockReturnValueOnce('tempId1'); + const firstItem = client.startTestItem( + { name: 'test', type: 'STEP', retry: false }, + 'launchId', + ); + await firstItem.promise; + + jest.spyOn(client.restClient, 'create').mockResolvedValue({ id: 'uuid-attempt-2' }); + jest.spyOn(client, 'getUniqId').mockReturnValueOnce('tempId2'); + const secondItem = client.startTestItem( + { name: 'test', type: 'STEP', retry: true }, + 'launchId', + ); + await secondItem.promise; + + expect(client.restClient.create).toHaveBeenLastCalledWith( + 'item/', + expect.objectContaining({ + retryOf: 'uuid-attempt-1', + }), + ); + + jest.spyOn(client.restClient, 'create').mockResolvedValue({ id: 'uuid-attempt-3' }); + jest.spyOn(client, 'getUniqId').mockReturnValueOnce('tempId3'); + const thirdItem = client.startTestItem( + { name: 'test', type: 'STEP', retry: true }, + 'launchId', + ); + await thirdItem.promise; + + expect(client.restClient.create).toHaveBeenLastCalledWith( + 'item/', + expect.objectContaining({ + retryOf: 'uuid-attempt-2', + }), + ); + }); + + it('should track itemRetriesChainLastTempIdMap for retry chain', () => { + const client = new RPClient({ + apiKey: 'startLaunchTest', + endpoint: 'https://rp.us/api/v1', + project: 'tst', + }); + client.map = { + launchId: { + children: [], + promiseStart: Promise.resolve(), + realId: 'realLaunchId', + }, + }; + jest.spyOn(client.restClient, 'create').mockResolvedValue({ id: 'item-id' }); + jest.spyOn(client, 'getUniqId').mockReturnValueOnce('tempId1'); + + client.startTestItem({ name: 'test', type: 'STEP' }, 'launchId'); + + const itemKey = client.calculateItemRetriesChainMapKey( + 'launchId', + undefined, + 'test', + undefined, + ); + expect(client.itemRetriesChainLastTempIdMap.get(itemKey)).toEqual('tempId1'); + }); }); describe('finishTestItem', () => { diff --git a/index.d.ts b/index.d.ts index fedd35d..573953d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -200,6 +200,12 @@ declare module '@reportportal/client-javascript' { startTime?: string | number; attributes?: Array<{ key?: string; value?: string } | string>; hasStats?: boolean; + retry?: boolean; + retryOf?: string; + codeRef?: string; + parameters?: Array<{ key: string; value: string }>; + uniqueId?: string; + testCaseId?: string; } /** @@ -239,6 +245,8 @@ declare module '@reportportal/client-javascript' { export interface FinishTestItemOptions { status?: string; endTime?: string | number; + retry?: boolean; + retryOf?: string; issue?: { issueType: string; comment?: string; diff --git a/lib/report-portal-client.js b/lib/report-portal-client.js index 7adbb25..0edf4ae 100644 --- a/lib/report-portal-client.js +++ b/lib/report-portal-client.js @@ -65,6 +65,7 @@ class RPClient { this.launchUuid = ''; this.itemRetriesChainMap = new Map(); this.itemRetriesChainKeyMapByTempId = new Map(); + this.itemRetriesChainLastTempIdMap = new Map(); } // eslint-disable-next-line valid-jsdoc @@ -93,6 +94,7 @@ class RPClient { if (key) { this.itemRetriesChainMap.delete(key); + this.itemRetriesChainLastTempIdMap.delete(key); } this.itemRetriesChainKeyMapByTempId.delete(id); @@ -558,6 +560,8 @@ class RPClient { testItemDataRQ.uniqueId, ); const executionItemPromise = testItemDataRQ.retry && this.itemRetriesChainMap.get(itemKey); + const previousRetryTempId = + testItemDataRQ.retry && this.itemRetriesChainLastTempIdMap.get(itemKey); const tempId = this.getUniqId(); this.map[tempId] = this.getNewItemObj((resolve, reject) => { @@ -570,6 +574,9 @@ class RPClient { url += `${realParentId}`; } testItemData.launchUuid = realLaunchId; + if (previousRetryTempId && this.map[previousRetryTempId]) { + testItemData.retryOf = this.map[previousRetryTempId].realId; + } this.logDebug(`Start test item with tempId ${tempId}`, testItemData); this.restClient.create(url, testItemData).then( (response) => { @@ -592,6 +599,7 @@ class RPClient { this.map[parentMapId].children.push(tempId); this.itemRetriesChainKeyMapByTempId.set(tempId, itemKey); this.itemRetriesChainMap.set(itemKey, this.map[tempId].promiseStart); + this.itemRetriesChainLastTempIdMap.set(itemKey, tempId); return { tempId,