Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
202 changes: 202 additions & 0 deletions __tests__/report-portal-client.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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', () => {
Expand Down
8 changes: 8 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions lib/report-portal-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -93,6 +94,7 @@ class RPClient {

if (key) {
this.itemRetriesChainMap.delete(key);
this.itemRetriesChainLastTempIdMap.delete(key);
}

this.itemRetriesChainKeyMapByTempId.delete(id);
Expand Down Expand Up @@ -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) => {
Expand All @@ -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) => {
Expand All @@ -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,
Expand Down
Loading