@elisra-devops/docgen-data-provider 1.103.0 → 1.105.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/helpers/helper.d.ts +2 -1
- package/bin/helpers/helper.js +5 -4
- package/bin/helpers/helper.js.map +1 -1
- package/bin/models/tfs-data.d.ts +1 -0
- package/bin/models/tfs-data.js.map +1 -1
- package/bin/modules/ResultDataProvider.d.ts +1 -0
- package/bin/modules/ResultDataProvider.js +63 -1
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.d.ts +4 -0
- package/bin/modules/TestDataProvider.js +99 -14
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.js +14 -3
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/helper.test.js +2 -1
- package/bin/tests/helpers/helper.test.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +75 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/bin/tests/modules/testDataProvider.test.js +162 -9
- package/bin/tests/modules/testDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.test.js +66 -0
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/helpers/helper.ts +19 -4
- package/src/models/tfs-data.ts +1 -0
- package/src/modules/ResultDataProvider.ts +87 -1
- package/src/modules/TestDataProvider.ts +130 -22
- package/src/modules/TicketsDataProvider.ts +18 -5
- package/src/tests/helpers/helper.test.ts +2 -1
- package/src/tests/modules/ResultDataProvider.test.ts +94 -0
- package/src/tests/modules/testDataProvider.test.ts +225 -9
- package/src/tests/modules/ticketsDataProvider.test.ts +75 -0
|
@@ -59,6 +59,93 @@ export default class TestDataProvider {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
private getSuiteDescription(suite: any): string {
|
|
63
|
+
return String(suite?.description || suite?.suiteDescription || '').trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async getSuiteDescriptionsFromWorkItems(
|
|
67
|
+
project: string,
|
|
68
|
+
suiteIds: Array<string | number>
|
|
69
|
+
): Promise<Map<string, string>> {
|
|
70
|
+
const ids = Array.from(
|
|
71
|
+
new Set(
|
|
72
|
+
suiteIds
|
|
73
|
+
.map((id) => Number(id))
|
|
74
|
+
.filter((id) => Number.isFinite(id) && id > 0)
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
if (ids.length === 0) {
|
|
78
|
+
return new Map<string, string>();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const url = `${this.joinOrgProject(project)}/_apis/wit/workitemsbatch?api-version=7.1`;
|
|
82
|
+
const maxBatchSize = 200;
|
|
83
|
+
const chunks: number[][] = [];
|
|
84
|
+
for (let i = 0; i < ids.length; i += maxBatchSize) {
|
|
85
|
+
chunks.push(ids.slice(i, i + maxBatchSize));
|
|
86
|
+
}
|
|
87
|
+
const descriptions = new Map<string, string>();
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
for (const chunkIds of chunks) {
|
|
91
|
+
const payload = {
|
|
92
|
+
ids: chunkIds,
|
|
93
|
+
fields: ['System.Description'],
|
|
94
|
+
errorPolicy: 'Omit',
|
|
95
|
+
};
|
|
96
|
+
const response = await TFSServices.getItemContent(
|
|
97
|
+
url,
|
|
98
|
+
this.token,
|
|
99
|
+
'post',
|
|
100
|
+
payload,
|
|
101
|
+
{ 'Content-Type': 'application/json' }
|
|
102
|
+
);
|
|
103
|
+
const items = Array.isArray(response?.value) ? response.value : [];
|
|
104
|
+
items.forEach((wi: any) => {
|
|
105
|
+
const desc = String(wi?.fields?.['System.Description'] || '').trim();
|
|
106
|
+
if (wi?.id && desc) {
|
|
107
|
+
descriptions.set(String(wi.id), desc);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return descriptions;
|
|
112
|
+
} catch (error: any) {
|
|
113
|
+
logger.warn(`Failed to enrich suite descriptions from work items batch: ${error?.message || error}`);
|
|
114
|
+
return new Map<string, string>();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async normalizeAndEnrichSuitesResponse(project: string, data: any): Promise<any> {
|
|
119
|
+
const suites = Array.isArray(data?.testSuites)
|
|
120
|
+
? data.testSuites
|
|
121
|
+
: Array.isArray(data?.value)
|
|
122
|
+
? data.value
|
|
123
|
+
: Array.isArray(data)
|
|
124
|
+
? data
|
|
125
|
+
: [];
|
|
126
|
+
|
|
127
|
+
const mapped = suites.map((suite: any) => ({
|
|
128
|
+
...suite,
|
|
129
|
+
title: suite?.title || suite?.name || '',
|
|
130
|
+
parentSuiteId: suite?.parentSuiteId ?? suite?.parentSuite?.id ?? 0,
|
|
131
|
+
description: this.getSuiteDescription(suite),
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
const missingDescriptionIds = mapped
|
|
135
|
+
.filter((suite: any) => !String(suite?.description || '').trim())
|
|
136
|
+
.map((suite: any) => suite?.id);
|
|
137
|
+
const descriptionBySuiteId = await this.getSuiteDescriptionsFromWorkItems(project, missingDescriptionIds);
|
|
138
|
+
const enriched = mapped.map((suite: any) => ({
|
|
139
|
+
...suite,
|
|
140
|
+
description: suite?.description || descriptionBySuiteId.get(String(suite?.id)) || '',
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(data)) {
|
|
144
|
+
return { value: data, testSuites: enriched };
|
|
145
|
+
}
|
|
146
|
+
return { ...(data || {}), testSuites: enriched };
|
|
147
|
+
}
|
|
148
|
+
|
|
62
149
|
async GetTestSuiteByTestCase(testCaseId: string): Promise<any> {
|
|
63
150
|
const base = String(this.orgUrl || '').replace(/\/+$/, '');
|
|
64
151
|
let url = `${base}/_apis/testplan/suites?testCaseId=${testCaseId}`;
|
|
@@ -87,23 +174,11 @@ export default class TestDataProvider {
|
|
|
87
174
|
if (!planid) {
|
|
88
175
|
throw new Error('Plan not selected');
|
|
89
176
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const suites = Array.isArray(data?.value) ? data.value : Array.isArray(data) ? data : [];
|
|
96
|
-
const mapped = suites.map((suite: any) => ({
|
|
97
|
-
...suite,
|
|
98
|
-
title: suite?.title || suite?.name || '',
|
|
99
|
-
parentSuiteId: suite?.parentSuiteId ?? suite?.parentSuite?.id ?? 0,
|
|
100
|
-
}));
|
|
101
|
-
return { ...data, testSuites: mapped };
|
|
102
|
-
}
|
|
103
|
-
const url = `${this.joinOrgProject(
|
|
104
|
-
project
|
|
105
|
-
)}/_api/_testManagement/GetTestSuitesForPlan?__v=5&planId=${planid}`;
|
|
106
|
-
return await this.fetchWithCache(url);
|
|
177
|
+
const url = this.isBearerToken()
|
|
178
|
+
? `${this.joinOrgProject(project)}/_apis/testplan/Plans/${planid}/suites?includeChildren=true&api-version=7.0`
|
|
179
|
+
: `${this.joinOrgProject(project)}/_api/_testManagement/GetTestSuitesForPlan?__v=5&planId=${planid}`;
|
|
180
|
+
const data = await this.fetchWithCache(url);
|
|
181
|
+
return this.normalizeAndEnrichSuitesResponse(project, data);
|
|
107
182
|
}
|
|
108
183
|
|
|
109
184
|
async GetTestSuitesByPlan(
|
|
@@ -130,16 +205,22 @@ export default class TestDataProvider {
|
|
|
130
205
|
// These represent separate hierarchies that need to be processed independently
|
|
131
206
|
|
|
132
207
|
const topLevelSuites: string[] = [];
|
|
208
|
+
const filterAsStrings = new Set(suiteIdsFilter.map((id) => id.toString()));
|
|
209
|
+
const filterAsNumbers = new Set(
|
|
210
|
+
suiteIdsFilter
|
|
211
|
+
.map((id) => Number(id))
|
|
212
|
+
.filter((id) => Number.isFinite(id))
|
|
213
|
+
);
|
|
133
214
|
|
|
134
215
|
for (const filterSuiteId of suiteIdsFilter) {
|
|
135
216
|
const suite = suiteMap.get(filterSuiteId) || suiteMap.get(parseInt(filterSuiteId.toString()));
|
|
136
217
|
if (suite) {
|
|
218
|
+
const parentSuiteIdString = String(suite.parentSuiteId);
|
|
219
|
+
const parentSuiteIdNumber = Number(parentSuiteIdString);
|
|
137
220
|
// Check if this suite's parent is NOT in our filter (making it a top-level suite for our selection)
|
|
138
|
-
const parentInFilter =
|
|
139
|
-
(
|
|
140
|
-
|
|
141
|
-
parseInt(id.toString()) === suite.parentSuiteId
|
|
142
|
-
);
|
|
221
|
+
const parentInFilter =
|
|
222
|
+
filterAsStrings.has(parentSuiteIdString) ||
|
|
223
|
+
(Number.isFinite(parentSuiteIdNumber) && filterAsNumbers.has(parentSuiteIdNumber));
|
|
143
224
|
|
|
144
225
|
if (!parentInFilter && suite.parentSuiteId !== 0) {
|
|
145
226
|
topLevelSuites.push(filterSuiteId.toString());
|
|
@@ -322,6 +403,7 @@ export default class TestDataProvider {
|
|
|
322
403
|
testCase.title = test.fields['System.Title'];
|
|
323
404
|
testCase.area = test.fields['System.AreaPath'];
|
|
324
405
|
testCase.description = test.fields['System.Description'];
|
|
406
|
+
testCase.testPhase = this.extractTestPhase(test.fields);
|
|
325
407
|
testCase.url = url + test.id;
|
|
326
408
|
//testCase.steps = test.fields["Microsoft.VSTS.TCM.Steps"];
|
|
327
409
|
testCase.id = test.id;
|
|
@@ -460,6 +542,32 @@ export default class TestDataProvider {
|
|
|
460
542
|
return newRequirementRelation;
|
|
461
543
|
}
|
|
462
544
|
|
|
545
|
+
private extractTestPhase(fields: any): string {
|
|
546
|
+
if (!fields || typeof fields !== 'object') return '';
|
|
547
|
+
|
|
548
|
+
const direct =
|
|
549
|
+
fields['Custom.TestPhase'] ??
|
|
550
|
+
fields['Custom.Phase'] ??
|
|
551
|
+
fields['Microsoft.VSTS.Common.TestPhase'];
|
|
552
|
+
if (direct !== undefined && direct !== null) {
|
|
553
|
+
const value = typeof direct === 'object' ? direct?.displayName ?? direct?.name ?? direct : direct;
|
|
554
|
+
const normalized = String(value).trim();
|
|
555
|
+
if (normalized) return normalized;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const normalizedKey = Object.keys(fields).find((key) => key.toLowerCase().includes('testphase'));
|
|
559
|
+
if (normalizedKey) {
|
|
560
|
+
const value = fields[normalizedKey];
|
|
561
|
+
const normalized =
|
|
562
|
+
value && typeof value === 'object'
|
|
563
|
+
? String(value.displayName ?? value.name ?? '').trim()
|
|
564
|
+
: String(value ?? '').trim();
|
|
565
|
+
if (normalized) return normalized;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return '';
|
|
569
|
+
}
|
|
570
|
+
|
|
463
571
|
ParseSteps(steps: string) {
|
|
464
572
|
let stepsLsist: Array<TestSteps> = new Array<TestSteps>();
|
|
465
573
|
const start: string = ';P>';
|
|
@@ -199,12 +199,25 @@ export default class TicketsDataProvider {
|
|
|
199
199
|
const queriesWithChildren = await this.ensureQueryChildren(queries);
|
|
200
200
|
|
|
201
201
|
switch (normalizedDocType) {
|
|
202
|
-
case 'std':
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
'std'
|
|
202
|
+
case 'std':
|
|
203
|
+
case 'stp': {
|
|
204
|
+
const rootCandidates =
|
|
205
|
+
normalizedDocType === 'stp' ? (['stp', 'std'] as const) : (['std'] as const);
|
|
206
|
+
let stdRoot = queriesWithChildren;
|
|
207
|
+
let stdRootFound = false;
|
|
208
|
+
for (const candidate of rootCandidates) {
|
|
209
|
+
const lookup = await this.getDocTypeRoot(queriesWithChildren, candidate);
|
|
210
|
+
if (lookup.found) {
|
|
211
|
+
stdRoot = lookup.root;
|
|
212
|
+
stdRootFound = true;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
logger.debug(
|
|
217
|
+
`[GetSharedQueries][${normalizedDocType}] using ${
|
|
218
|
+
stdRootFound ? 'dedicated folder' : 'root queries'
|
|
219
|
+
}`
|
|
206
220
|
);
|
|
207
|
-
logger.debug(`[GetSharedQueries][std] using ${stdRootFound ? 'dedicated folder' : 'root queries'}`);
|
|
208
221
|
// Each branch describes the dedicated folder names, the fetch routine, and how to validate results.
|
|
209
222
|
const stdBranches = await this.fetchDocTypeBranches(queriesWithChildren, stdRoot, [
|
|
210
223
|
{
|
|
@@ -13,13 +13,14 @@ describe('Helper', () => {
|
|
|
13
13
|
describe('suiteData class', () => {
|
|
14
14
|
it('should create suiteData with correct properties', () => {
|
|
15
15
|
// Act
|
|
16
|
-
const suite = new suiteData('Test Suite', '123', '456', 2);
|
|
16
|
+
const suite = new suiteData('Test Suite', '123', '456', 2, 'Suite description');
|
|
17
17
|
|
|
18
18
|
// Assert
|
|
19
19
|
expect(suite.name).toBe('Test Suite');
|
|
20
20
|
expect(suite.id).toBe('123');
|
|
21
21
|
expect(suite.parent).toBe('456');
|
|
22
22
|
expect(suite.level).toBe(2);
|
|
23
|
+
expect(suite.description).toBe('Suite description');
|
|
23
24
|
expect(suite.url).toBeUndefined();
|
|
24
25
|
});
|
|
25
26
|
});
|
|
@@ -3505,6 +3505,100 @@ describe('ResultDataProvider', () => {
|
|
|
3505
3505
|
})
|
|
3506
3506
|
);
|
|
3507
3507
|
});
|
|
3508
|
+
|
|
3509
|
+
it('should prefer latest test-case revision fields (title/description/steps) in internal validation flow', async () => {
|
|
3510
|
+
jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
|
|
3511
|
+
jest.spyOn(resultDataProvider as any, 'fetchMewpScopedTestData').mockResolvedValueOnce([
|
|
3512
|
+
{
|
|
3513
|
+
testPointsItems: [{ testCaseId: 777, testCaseName: 'TC 777 (snapshot)' }],
|
|
3514
|
+
testCasesItems: [
|
|
3515
|
+
{
|
|
3516
|
+
workItem: {
|
|
3517
|
+
id: 777,
|
|
3518
|
+
workItemFields: [
|
|
3519
|
+
{ key: 'System.Title', value: 'TC 777 (snapshot title)' },
|
|
3520
|
+
{
|
|
3521
|
+
key: 'Microsoft.VSTS.TCM.Steps',
|
|
3522
|
+
value:
|
|
3523
|
+
'<steps><step id="2" type="ActionStep"><parameterizedString isformatted="true">Action</parameterizedString><parameterizedString isformatted="true">SR0001</parameterizedString></step></steps>',
|
|
3524
|
+
},
|
|
3525
|
+
{ key: 'System.Description', value: '<p>Snapshot description</p>' },
|
|
3526
|
+
],
|
|
3527
|
+
},
|
|
3528
|
+
},
|
|
3529
|
+
],
|
|
3530
|
+
},
|
|
3531
|
+
]);
|
|
3532
|
+
jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
|
|
3533
|
+
{
|
|
3534
|
+
workItemId: 9777,
|
|
3535
|
+
requirementId: 'SR7777',
|
|
3536
|
+
baseKey: 'SR7777',
|
|
3537
|
+
title: 'Req 7777',
|
|
3538
|
+
responsibility: 'ESUK',
|
|
3539
|
+
linkedTestCaseIds: [777],
|
|
3540
|
+
areaPath: 'MEWP\\Customer Requirements\\Level 2',
|
|
3541
|
+
},
|
|
3542
|
+
]);
|
|
3543
|
+
jest.spyOn(resultDataProvider as any, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(
|
|
3544
|
+
new Map([
|
|
3545
|
+
[
|
|
3546
|
+
777,
|
|
3547
|
+
{
|
|
3548
|
+
baseKeys: new Set(['SR7777']),
|
|
3549
|
+
fullCodes: new Set(['SR7777']),
|
|
3550
|
+
},
|
|
3551
|
+
],
|
|
3552
|
+
])
|
|
3553
|
+
);
|
|
3554
|
+
const fetchWorkItemsByIdsSpy = jest
|
|
3555
|
+
.spyOn(resultDataProvider as any, 'fetchWorkItemsByIds')
|
|
3556
|
+
.mockResolvedValueOnce([
|
|
3557
|
+
{
|
|
3558
|
+
id: 777,
|
|
3559
|
+
fields: {
|
|
3560
|
+
'System.Title': 'TC 777 (latest title)',
|
|
3561
|
+
'System.Description':
|
|
3562
|
+
'<p><b><u>Trial specific assumptions, constraints, dependencies and requirements</u></b></p><p>SR7777</p>',
|
|
3563
|
+
'Microsoft.VSTS.TCM.Steps':
|
|
3564
|
+
'<steps><step id="2" type="ActionStep"><parameterizedString isformatted="true">Action</parameterizedString><parameterizedString isformatted="true">SR7777</parameterizedString></step></steps>',
|
|
3565
|
+
},
|
|
3566
|
+
},
|
|
3567
|
+
]);
|
|
3568
|
+
jest.spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps').mockImplementation(
|
|
3569
|
+
async (...args: any[]) => [
|
|
3570
|
+
{
|
|
3571
|
+
stepId: '1',
|
|
3572
|
+
stepPosition: '1',
|
|
3573
|
+
action: 'Action',
|
|
3574
|
+
expected: String(args?.[0] || '').includes('SR7777') ? 'SR7777' : 'SR0001',
|
|
3575
|
+
isSharedStepTitle: false,
|
|
3576
|
+
},
|
|
3577
|
+
]
|
|
3578
|
+
);
|
|
3579
|
+
|
|
3580
|
+
const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
|
|
3581
|
+
'123',
|
|
3582
|
+
mockProjectName,
|
|
3583
|
+
[1]
|
|
3584
|
+
);
|
|
3585
|
+
|
|
3586
|
+
expect(fetchWorkItemsByIdsSpy).toHaveBeenCalledWith(mockProjectName, [777], false);
|
|
3587
|
+
expect((resultDataProvider as any).testStepParserHelper.parseTestSteps).toHaveBeenCalledWith(
|
|
3588
|
+
expect.stringContaining('SR7777'),
|
|
3589
|
+
expect.any(Map)
|
|
3590
|
+
);
|
|
3591
|
+
expect(result.rows).toHaveLength(1);
|
|
3592
|
+
expect(result.rows[0]).toEqual(
|
|
3593
|
+
expect.objectContaining({
|
|
3594
|
+
'Test Case ID': 777,
|
|
3595
|
+
'Test Case Title': 'TC 777 (latest title)',
|
|
3596
|
+
'Mentioned but Not Linked': '',
|
|
3597
|
+
'Linked but Not Mentioned': '',
|
|
3598
|
+
'Validation Status': 'Pass',
|
|
3599
|
+
})
|
|
3600
|
+
);
|
|
3601
|
+
});
|
|
3508
3602
|
});
|
|
3509
3603
|
|
|
3510
3604
|
describe('buildLinkedRequirementsByTestCase', () => {
|
|
@@ -204,8 +204,8 @@ describe('TestDataProvider', () => {
|
|
|
204
204
|
// Arrange
|
|
205
205
|
const mockData = {
|
|
206
206
|
testSuites: [
|
|
207
|
-
{ id: '123', name: 'Test Suite 1' },
|
|
208
|
-
{ id: '456', name: 'Test Suite 2' },
|
|
207
|
+
{ id: '123', name: 'Test Suite 1', suiteDescription: 'Legacy Desc 1' },
|
|
208
|
+
{ id: '456', name: 'Test Suite 2', description: 'Legacy Desc 2' },
|
|
209
209
|
],
|
|
210
210
|
};
|
|
211
211
|
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockData);
|
|
@@ -214,19 +214,110 @@ describe('TestDataProvider', () => {
|
|
|
214
214
|
const result = await testDataProvider.GetTestSuitesForPlan(mockProject, mockPlanId);
|
|
215
215
|
|
|
216
216
|
// Assert
|
|
217
|
-
expect(result).toEqual(
|
|
217
|
+
expect(result.testSuites).toEqual(
|
|
218
|
+
expect.arrayContaining([
|
|
219
|
+
expect.objectContaining({
|
|
220
|
+
id: '123',
|
|
221
|
+
title: 'Test Suite 1',
|
|
222
|
+
parentSuiteId: 0,
|
|
223
|
+
description: 'Legacy Desc 1',
|
|
224
|
+
}),
|
|
225
|
+
expect.objectContaining({
|
|
226
|
+
id: '456',
|
|
227
|
+
title: 'Test Suite 2',
|
|
228
|
+
parentSuiteId: 0,
|
|
229
|
+
description: 'Legacy Desc 2',
|
|
230
|
+
}),
|
|
231
|
+
])
|
|
232
|
+
);
|
|
218
233
|
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
219
234
|
`${mockOrgUrl.replace(/\/+$/, '')}/${mockProject}/_api/_testManagement/GetTestSuitesForPlan?__v=5&planId=${mockPlanId}`,
|
|
220
235
|
mockToken
|
|
221
236
|
);
|
|
222
237
|
});
|
|
223
238
|
|
|
239
|
+
it('should enrich missing suite descriptions via work items batch for PAT token', async () => {
|
|
240
|
+
const suitesPayload = {
|
|
241
|
+
testSuites: [
|
|
242
|
+
{ id: 11, name: 'Suite A' },
|
|
243
|
+
{ id: 22, name: 'Suite B', suiteDescription: 'From legacy payload' },
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
const workItemsPayload = {
|
|
247
|
+
value: [{ id: 11, fields: { 'System.Description': '<div>WI Desc A</div>' } }],
|
|
248
|
+
};
|
|
249
|
+
(TFSServices.getItemContent as jest.Mock).mockImplementation((url: string) => {
|
|
250
|
+
if (url.includes('/_apis/wit/workitemsbatch')) {
|
|
251
|
+
return Promise.resolve(workItemsPayload);
|
|
252
|
+
}
|
|
253
|
+
return Promise.resolve(suitesPayload);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const result = await testDataProvider.GetTestSuitesForPlan(mockProject, mockPlanId);
|
|
257
|
+
|
|
258
|
+
expect(TFSServices.getItemContent).toHaveBeenNthCalledWith(
|
|
259
|
+
1,
|
|
260
|
+
`${mockOrgUrl.replace(/\/+$/, '')}/${mockProject}/_api/_testManagement/GetTestSuitesForPlan?__v=5&planId=${mockPlanId}`,
|
|
261
|
+
mockToken
|
|
262
|
+
);
|
|
263
|
+
expect(TFSServices.getItemContent).toHaveBeenNthCalledWith(
|
|
264
|
+
2,
|
|
265
|
+
`${mockOrgUrl.replace(/\/+$/, '')}/${mockProject}/_apis/wit/workitemsbatch?api-version=7.1`,
|
|
266
|
+
mockToken,
|
|
267
|
+
'post',
|
|
268
|
+
{
|
|
269
|
+
ids: [11],
|
|
270
|
+
fields: ['System.Description'],
|
|
271
|
+
errorPolicy: 'Omit',
|
|
272
|
+
},
|
|
273
|
+
{ 'Content-Type': 'application/json' }
|
|
274
|
+
);
|
|
275
|
+
expect(result.testSuites).toEqual(
|
|
276
|
+
expect.arrayContaining([
|
|
277
|
+
expect.objectContaining({ id: 11, description: '<div>WI Desc A</div>' }),
|
|
278
|
+
expect.objectContaining({ id: 22, description: 'From legacy payload' }),
|
|
279
|
+
])
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should chunk work items batch requests when suite IDs exceed batch limit', async () => {
|
|
284
|
+
const suitesPayload = {
|
|
285
|
+
testSuites: Array.from({ length: 205 }, (_, i) => ({
|
|
286
|
+
id: i + 1,
|
|
287
|
+
name: `Suite ${i + 1}`,
|
|
288
|
+
})),
|
|
289
|
+
};
|
|
290
|
+
(TFSServices.getItemContent as jest.Mock).mockImplementation((url: string, _token: string, method?: string, data?: any) => {
|
|
291
|
+
if (url.includes('/_apis/wit/workitemsbatch') && method === 'post') {
|
|
292
|
+
const ids = Array.isArray(data?.ids) ? data.ids : [];
|
|
293
|
+
return Promise.resolve({
|
|
294
|
+
value: ids.map((id: number) => ({
|
|
295
|
+
id,
|
|
296
|
+
fields: { 'System.Description': `Desc ${id}` },
|
|
297
|
+
})),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return Promise.resolve(suitesPayload);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const result = await testDataProvider.GetTestSuitesForPlan(mockProject, mockPlanId);
|
|
304
|
+
|
|
305
|
+
expect((TFSServices.getItemContent as jest.Mock).mock.calls).toHaveLength(3);
|
|
306
|
+
expect((TFSServices.getItemContent as jest.Mock).mock.calls[1][2]).toBe('post');
|
|
307
|
+
expect((TFSServices.getItemContent as jest.Mock).mock.calls[1][3].ids).toHaveLength(200);
|
|
308
|
+
expect((TFSServices.getItemContent as jest.Mock).mock.calls[2][2]).toBe('post');
|
|
309
|
+
expect((TFSServices.getItemContent as jest.Mock).mock.calls[2][3].ids).toHaveLength(5);
|
|
310
|
+
|
|
311
|
+
expect(result.testSuites.find((s: any) => s.id === 1)?.description).toBe('Desc 1');
|
|
312
|
+
expect(result.testSuites.find((s: any) => s.id === 205)?.description).toBe('Desc 205');
|
|
313
|
+
});
|
|
314
|
+
|
|
224
315
|
it('should use testplan suites endpoint for bearer token and normalize response', async () => {
|
|
225
316
|
const bearerProvider = new TestDataProvider(mockOrgUrl, mockBearerToken);
|
|
226
317
|
const mockData = {
|
|
227
318
|
value: [
|
|
228
|
-
{ id: '123', name: 'Suite 1' },
|
|
229
|
-
{ id: '456', name: 'Suite 2', parentSuite: { id: '123' } },
|
|
319
|
+
{ id: '123', name: 'Suite 1', description: 'Desc 1' },
|
|
320
|
+
{ id: '456', name: 'Suite 2', parentSuite: { id: '123' }, suiteDescription: 'Desc 2' },
|
|
230
321
|
],
|
|
231
322
|
count: 2,
|
|
232
323
|
};
|
|
@@ -240,8 +331,58 @@ describe('TestDataProvider', () => {
|
|
|
240
331
|
);
|
|
241
332
|
expect(result.testSuites).toEqual(
|
|
242
333
|
expect.arrayContaining([
|
|
243
|
-
expect.objectContaining({ id: '123', title: 'Suite 1', parentSuiteId: 0 }),
|
|
244
|
-
expect.objectContaining({
|
|
334
|
+
expect.objectContaining({ id: '123', title: 'Suite 1', parentSuiteId: 0, description: 'Desc 1' }),
|
|
335
|
+
expect.objectContaining({
|
|
336
|
+
id: '456',
|
|
337
|
+
title: 'Suite 2',
|
|
338
|
+
parentSuiteId: '123',
|
|
339
|
+
description: 'Desc 2',
|
|
340
|
+
}),
|
|
341
|
+
])
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should enrich missing suite descriptions via work items batch for bearer token', async () => {
|
|
346
|
+
const bearerProvider = new TestDataProvider(mockOrgUrl, mockBearerToken);
|
|
347
|
+
const suitesPayload = {
|
|
348
|
+
value: [
|
|
349
|
+
{ id: 101, name: 'Suite A' },
|
|
350
|
+
{ id: 202, name: 'Suite B', description: 'Native Desc B' },
|
|
351
|
+
],
|
|
352
|
+
};
|
|
353
|
+
const workItemsPayload = {
|
|
354
|
+
value: [{ id: 101, fields: { 'System.Description': 'Enriched Desc A' } }],
|
|
355
|
+
};
|
|
356
|
+
(TFSServices.getItemContent as jest.Mock).mockImplementation((url: string) => {
|
|
357
|
+
if (url.includes('/_apis/wit/workitemsbatch')) {
|
|
358
|
+
return Promise.resolve(workItemsPayload);
|
|
359
|
+
}
|
|
360
|
+
return Promise.resolve(suitesPayload);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const result = await bearerProvider.GetTestSuitesForPlan(mockProject, mockPlanId);
|
|
364
|
+
|
|
365
|
+
expect(TFSServices.getItemContent).toHaveBeenNthCalledWith(
|
|
366
|
+
1,
|
|
367
|
+
`${mockOrgUrl.replace(/\/+$/, '')}/${mockProject}/_apis/testplan/Plans/${mockPlanId}/suites?includeChildren=true&api-version=7.0`,
|
|
368
|
+
mockBearerToken
|
|
369
|
+
);
|
|
370
|
+
expect(TFSServices.getItemContent).toHaveBeenNthCalledWith(
|
|
371
|
+
2,
|
|
372
|
+
`${mockOrgUrl.replace(/\/+$/, '')}/${mockProject}/_apis/wit/workitemsbatch?api-version=7.1`,
|
|
373
|
+
mockBearerToken,
|
|
374
|
+
'post',
|
|
375
|
+
{
|
|
376
|
+
ids: [101],
|
|
377
|
+
fields: ['System.Description'],
|
|
378
|
+
errorPolicy: 'Omit',
|
|
379
|
+
},
|
|
380
|
+
{ 'Content-Type': 'application/json' }
|
|
381
|
+
);
|
|
382
|
+
expect(result.testSuites).toEqual(
|
|
383
|
+
expect.arrayContaining([
|
|
384
|
+
expect.objectContaining({ id: 101, description: 'Enriched Desc A' }),
|
|
385
|
+
expect.objectContaining({ id: 202, description: 'Native Desc B' }),
|
|
245
386
|
])
|
|
246
387
|
);
|
|
247
388
|
});
|
|
@@ -268,7 +409,14 @@ describe('TestDataProvider', () => {
|
|
|
268
409
|
mockPlanId,
|
|
269
410
|
mockOrgUrl,
|
|
270
411
|
mockProject,
|
|
271
|
-
|
|
412
|
+
expect.arrayContaining([
|
|
413
|
+
expect.objectContaining({
|
|
414
|
+
id: '123',
|
|
415
|
+
title: 'Test Suite 1',
|
|
416
|
+
parentSuiteId: 0,
|
|
417
|
+
description: '',
|
|
418
|
+
}),
|
|
419
|
+
]),
|
|
272
420
|
mockSuiteId,
|
|
273
421
|
true
|
|
274
422
|
);
|
|
@@ -278,7 +426,7 @@ describe('TestDataProvider', () => {
|
|
|
278
426
|
it('should use bearer suites payload when token is bearer', async () => {
|
|
279
427
|
const bearerProvider = new TestDataProvider(mockOrgUrl, mockBearerToken);
|
|
280
428
|
const mockTestSuites = {
|
|
281
|
-
value: [{ id: '123', name: 'Suite 1', parentSuite: { id: 0 } }],
|
|
429
|
+
value: [{ id: '123', name: 'Suite 1', parentSuite: { id: 0 }, description: 'Suite Desc' }],
|
|
282
430
|
};
|
|
283
431
|
const mockSuiteData = [new suiteData('Suite 1', '123', '456', 1)];
|
|
284
432
|
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockTestSuites);
|
|
@@ -1046,6 +1194,74 @@ describe('TestDataProvider', () => {
|
|
|
1046
1194
|
);
|
|
1047
1195
|
});
|
|
1048
1196
|
|
|
1197
|
+
it('should populate testPhase from Custom.TestPhase', async () => {
|
|
1198
|
+
const suite = { id: '1', name: 'Suite 1' } as any;
|
|
1199
|
+
const testCases = {
|
|
1200
|
+
count: 1,
|
|
1201
|
+
value: [{ testCase: { id: 123, url: 'https://example.com/testcase/123' } }],
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
jest.spyOn(testDataProvider as any, 'fetchWithCache').mockResolvedValueOnce({
|
|
1205
|
+
id: 123,
|
|
1206
|
+
fields: {
|
|
1207
|
+
'System.Title': 'TC 123',
|
|
1208
|
+
'System.AreaPath': 'A',
|
|
1209
|
+
'System.Description': 'D',
|
|
1210
|
+
'Custom.TestPhase': 'SIT',
|
|
1211
|
+
'Microsoft.VSTS.TCM.Steps': null,
|
|
1212
|
+
},
|
|
1213
|
+
relations: [],
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
const res = await testDataProvider.StructureTestCase(
|
|
1217
|
+
mockProject,
|
|
1218
|
+
testCases as any,
|
|
1219
|
+
suite,
|
|
1220
|
+
false,
|
|
1221
|
+
false,
|
|
1222
|
+
false,
|
|
1223
|
+
new Map<string, string[]>(),
|
|
1224
|
+
new Map<string, string[]>()
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
expect(res).toHaveLength(1);
|
|
1228
|
+
expect(res[0].testPhase).toBe('SIT');
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
it('should extract testPhase from fallback key containing testphase', async () => {
|
|
1232
|
+
const suite = { id: '1', name: 'Suite 1' } as any;
|
|
1233
|
+
const testCases = {
|
|
1234
|
+
count: 1,
|
|
1235
|
+
value: [{ testCase: { id: 123, url: 'https://example.com/testcase/123' } }],
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
jest.spyOn(testDataProvider as any, 'fetchWithCache').mockResolvedValueOnce({
|
|
1239
|
+
id: 123,
|
|
1240
|
+
fields: {
|
|
1241
|
+
'System.Title': 'TC 123',
|
|
1242
|
+
'System.AreaPath': 'A',
|
|
1243
|
+
'System.Description': 'D',
|
|
1244
|
+
'Custom.MyTestPhaseField': { displayName: 'Regression' },
|
|
1245
|
+
'Microsoft.VSTS.TCM.Steps': null,
|
|
1246
|
+
},
|
|
1247
|
+
relations: [],
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
const res = await testDataProvider.StructureTestCase(
|
|
1251
|
+
mockProject,
|
|
1252
|
+
testCases as any,
|
|
1253
|
+
suite,
|
|
1254
|
+
false,
|
|
1255
|
+
false,
|
|
1256
|
+
false,
|
|
1257
|
+
new Map<string, string[]>(),
|
|
1258
|
+
new Map<string, string[]>()
|
|
1259
|
+
);
|
|
1260
|
+
|
|
1261
|
+
expect(res).toHaveLength(1);
|
|
1262
|
+
expect(res[0].testPhase).toBe('Regression');
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1049
1265
|
it('should return empty list when test case fetch fails', async () => {
|
|
1050
1266
|
const suite = { id: '1', name: 'Suite 1' } as any;
|
|
1051
1267
|
const testCases = {
|