@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.
Files changed (31) hide show
  1. package/bin/helpers/helper.d.ts +2 -1
  2. package/bin/helpers/helper.js +5 -4
  3. package/bin/helpers/helper.js.map +1 -1
  4. package/bin/models/tfs-data.d.ts +1 -0
  5. package/bin/models/tfs-data.js.map +1 -1
  6. package/bin/modules/ResultDataProvider.d.ts +1 -0
  7. package/bin/modules/ResultDataProvider.js +63 -1
  8. package/bin/modules/ResultDataProvider.js.map +1 -1
  9. package/bin/modules/TestDataProvider.d.ts +4 -0
  10. package/bin/modules/TestDataProvider.js +99 -14
  11. package/bin/modules/TestDataProvider.js.map +1 -1
  12. package/bin/modules/TicketsDataProvider.js +14 -3
  13. package/bin/modules/TicketsDataProvider.js.map +1 -1
  14. package/bin/tests/helpers/helper.test.js +2 -1
  15. package/bin/tests/helpers/helper.test.js.map +1 -1
  16. package/bin/tests/modules/ResultDataProvider.test.js +75 -0
  17. package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
  18. package/bin/tests/modules/testDataProvider.test.js +162 -9
  19. package/bin/tests/modules/testDataProvider.test.js.map +1 -1
  20. package/bin/tests/modules/ticketsDataProvider.test.js +66 -0
  21. package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/helpers/helper.ts +19 -4
  24. package/src/models/tfs-data.ts +1 -0
  25. package/src/modules/ResultDataProvider.ts +87 -1
  26. package/src/modules/TestDataProvider.ts +130 -22
  27. package/src/modules/TicketsDataProvider.ts +18 -5
  28. package/src/tests/helpers/helper.test.ts +2 -1
  29. package/src/tests/modules/ResultDataProvider.test.ts +94 -0
  30. package/src/tests/modules/testDataProvider.test.ts +225 -9
  31. 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
- if (this.isBearerToken()) {
91
- const url = `${this.joinOrgProject(
92
- project
93
- )}/_apis/testplan/Plans/${planid}/suites?includeChildren=true&api-version=7.0`;
94
- const data = await this.fetchWithCache(url);
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 = suiteIdsFilter.some(
139
- (id) =>
140
- id.toString() === suite.parentSuiteId.toString() ||
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&gt;';
@@ -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
- const { root: stdRoot, found: stdRootFound } = await this.getDocTypeRoot(
204
- queriesWithChildren,
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(mockData);
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({ id: '456', title: 'Suite 2', parentSuiteId: '123' }),
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
- mockTestSuites.testSuites,
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 = {