@elisra-devops/docgen-data-provider 1.63.12 → 1.67.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 (94) hide show
  1. package/.github/workflows/ci.yml +26 -9
  2. package/.github/workflows/release.yml +9 -10
  3. package/bin/helpers/tfs.d.ts +3 -0
  4. package/bin/helpers/tfs.js +44 -7
  5. package/bin/helpers/tfs.js.map +1 -1
  6. package/bin/modules/GitDataProvider.d.ts +10 -0
  7. package/bin/modules/GitDataProvider.js +10 -0
  8. package/bin/modules/GitDataProvider.js.map +1 -1
  9. package/bin/modules/MangementDataProvider.js +7 -1
  10. package/bin/modules/MangementDataProvider.js.map +1 -1
  11. package/bin/modules/TestDataProvider.js +0 -1
  12. package/bin/modules/TestDataProvider.js.map +1 -1
  13. package/bin/modules/TicketsDataProvider.d.ts +63 -27
  14. package/bin/modules/TicketsDataProvider.js +226 -122
  15. package/bin/modules/TicketsDataProvider.js.map +1 -1
  16. package/bin/tests/helpers/helper.test.js +279 -0
  17. package/bin/tests/helpers/helper.test.js.map +1 -0
  18. package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
  19. package/bin/tests/helpers/tfs.test.js.map +1 -0
  20. package/bin/tests/index.test.js +25 -0
  21. package/bin/tests/index.test.js.map +1 -0
  22. package/bin/tests/models/tfs-data.test.js +160 -0
  23. package/bin/tests/models/tfs-data.test.js.map +1 -0
  24. package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
  25. package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
  26. package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
  27. package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
  28. package/bin/tests/modules/gitDataProvider.test.js +1888 -0
  29. package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
  30. package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +39 -31
  31. package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
  32. package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
  33. package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
  34. package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
  35. package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
  36. package/bin/tests/modules/testDataProvider.test.js +717 -0
  37. package/bin/tests/modules/testDataProvider.test.js.map +1 -0
  38. package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
  39. package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
  40. package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
  41. package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
  42. package/bin/tests/utils/DataProviderUtils.test.js +61 -0
  43. package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
  44. package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
  45. package/bin/tests/utils/testStepParserHelper.test.js +359 -0
  46. package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
  47. package/package.json +10 -1
  48. package/src/helpers/tfs.ts +51 -7
  49. package/src/modules/GitDataProvider.ts +10 -0
  50. package/src/modules/MangementDataProvider.ts +6 -1
  51. package/src/modules/TestDataProvider.ts +0 -1
  52. package/src/modules/TicketsDataProvider.ts +311 -151
  53. package/src/tests/helpers/helper.test.ts +337 -0
  54. package/src/tests/helpers/tfs.test.ts +1092 -0
  55. package/src/tests/index.test.ts +28 -0
  56. package/src/tests/models/tfs-data.test.ts +203 -0
  57. package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
  58. package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
  59. package/src/tests/modules/gitDataProvider.test.ts +2628 -0
  60. package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +63 -32
  61. package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
  62. package/src/tests/modules/testDataProvider.test.ts +1046 -0
  63. package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
  64. package/src/tests/utils/DataProviderUtils.test.ts +76 -0
  65. package/src/tests/utils/testStepParserHelper.test.ts +437 -0
  66. package/tsconfig.json +1 -0
  67. package/bin/helpers/test/tfs.test.js.map +0 -1
  68. package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
  69. package/bin/modules/test/ResultDataProvider.test.js +0 -444
  70. package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
  71. package/bin/modules/test/gitDataProvider.test.js +0 -433
  72. package/bin/modules/test/gitDataProvider.test.js.map +0 -1
  73. package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
  74. package/bin/modules/test/pipelineDataProvider.test.js +0 -237
  75. package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
  76. package/bin/modules/test/testDataProvider.test.js +0 -234
  77. package/bin/modules/test/testDataProvider.test.js.map +0 -1
  78. package/bin/modules/test/ticketsDataProvider.test.js +0 -322
  79. package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
  80. package/src/helpers/test/tfs.test.ts +0 -748
  81. package/src/modules/test/JfrogDataProvider.test.ts +0 -171
  82. package/src/modules/test/ResultDataProvider.test.ts +0 -542
  83. package/src/modules/test/gitDataProvider.test.ts +0 -691
  84. package/src/modules/test/pipelineDataProvider.test.ts +0 -292
  85. package/src/modules/test/testDataProvider.test.ts +0 -318
  86. package/src/modules/test/ticketsDataProvider.test.ts +0 -434
  87. /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
  88. /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
  89. /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
  90. /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
  91. /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
  92. /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
  93. /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
  94. /package/bin/{modules/test/ticketsDataProvider.test.d.ts → tests/modules/managmentDataProvider.test.d.ts} +0 -0
@@ -0,0 +1,2571 @@
1
+ import { TFSServices } from '../../helpers/tfs';
2
+ import ResultDataProvider from '../../modules/ResultDataProvider';
3
+ import logger from '../../utils/logger';
4
+ import Utils from '../../utils/testStepParserHelper';
5
+
6
+ // Mock dependencies
7
+ jest.mock('../../helpers/tfs');
8
+ jest.mock('../../utils/logger');
9
+ jest.mock('../../utils/testStepParserHelper');
10
+ jest.mock('../../modules/TicketsDataProvider', () => {
11
+ return {
12
+ __esModule: true,
13
+ default: jest.fn().mockImplementation(() => ({
14
+ GetQueryResultsFromWiql: jest.fn(),
15
+ })),
16
+ };
17
+ });
18
+ jest.mock('p-limit', () => () => (fn: Function) => fn());
19
+
20
+ describe('ResultDataProvider', () => {
21
+ let resultDataProvider: ResultDataProvider;
22
+ const mockOrgUrl = 'https://dev.azure.com/organization/';
23
+ const mockToken = 'mock-token';
24
+ const mockProjectName = 'test-project';
25
+ const mockTestPlanId = '12345';
26
+
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ resultDataProvider = new ResultDataProvider(mockOrgUrl, mockToken);
30
+ });
31
+
32
+ describe('Utility methods', () => {
33
+ describe('flattenSuites', () => {
34
+ it('should flatten a hierarchical suite structure into a single-level array', () => {
35
+ // Arrange
36
+ const suites = [
37
+ {
38
+ id: 1,
39
+ name: 'Parent 1',
40
+ children: [
41
+ { id: 2, name: 'Child 1' },
42
+ {
43
+ id: 3,
44
+ name: 'Child 2',
45
+ children: [{ id: 4, name: 'Grandchild 1' }],
46
+ },
47
+ ],
48
+ },
49
+ { id: 5, name: 'Parent 2' },
50
+ ];
51
+
52
+ // Act
53
+ const result: any[] = (resultDataProvider as any).flattenSuites(suites);
54
+
55
+ // Assert
56
+ expect(result).toHaveLength(5);
57
+ expect(result.map((s) => s.id)).toEqual([1, 2, 3, 4, 5]);
58
+ });
59
+ });
60
+
61
+ describe('filterSuites', () => {
62
+ it('should filter suites based on selected suite IDs', () => {
63
+ // Arrange
64
+ const testSuites = [
65
+ { id: 1, name: 'Suite 1', parentSuite: { id: 0 } },
66
+ { id: 2, name: 'Suite 2', parentSuite: { id: 1 } },
67
+ { id: 3, name: 'Suite 3', parentSuite: { id: 1 } },
68
+ ];
69
+ const selectedSuiteIds = [1, 3];
70
+
71
+ // Act
72
+ const result: any[] = (resultDataProvider as any).filterSuites(testSuites, selectedSuiteIds);
73
+
74
+ // Assert
75
+ expect(result).toHaveLength(2);
76
+ expect(result.map((s) => s.id)).toEqual([1, 3]);
77
+ });
78
+
79
+ it('should return all suites with parent when no suite IDs are selected', () => {
80
+ // Arrange
81
+ const testSuites = [
82
+ { id: 1, name: 'Suite 1', parentSuite: { id: 0 } },
83
+ { id: 2, name: 'Suite 2', parentSuite: { id: 1 } },
84
+ { id: 3, name: 'Suite 3', parentSuite: null },
85
+ ];
86
+
87
+ // Act
88
+ const result: any[] = (resultDataProvider as any).filterSuites(testSuites);
89
+
90
+ // Assert
91
+ expect(result).toHaveLength(2);
92
+ expect(result.map((s) => s.id)).toEqual([1, 2]);
93
+ });
94
+ });
95
+
96
+ describe('buildTestGroupName', () => {
97
+ it('should return simple suite name when hierarchy is disabled', () => {
98
+ // Arrange
99
+ const suiteMap = new Map([[1, { id: 1, name: 'Suite 1', parentSuite: { id: 0 } }]]);
100
+
101
+ // Act
102
+ const result = (resultDataProvider as any).buildTestGroupName(1, suiteMap, false);
103
+
104
+ // Assert
105
+ expect(result).toBe('Suite 1');
106
+ });
107
+
108
+ it('should build hierarchical name with parent info', () => {
109
+ // Arrange
110
+ const suiteMap = new Map([
111
+ [1, { id: 1, name: 'Parent', parentSuite: null }],
112
+ [2, { id: 2, name: 'Child', parentSuite: { id: 1 } }],
113
+ ]);
114
+
115
+ // Act
116
+ const result = (resultDataProvider as any).buildTestGroupName(2, suiteMap, true);
117
+
118
+ // Assert
119
+ expect(result).toBe('Child');
120
+ });
121
+
122
+ it('should abbreviate deep hierarchies', () => {
123
+ // Arrange
124
+ const suiteMap = new Map([
125
+ [1, { id: 1, name: 'Root', parentSuite: null }],
126
+ [2, { id: 2, name: 'Level1', parentSuite: { id: 1 } }],
127
+ [3, { id: 3, name: 'Level2', parentSuite: { id: 2 } }],
128
+ [4, { id: 4, name: 'Level3', parentSuite: { id: 3 } }],
129
+ ]);
130
+
131
+ // Act
132
+ const result = (resultDataProvider as any).buildTestGroupName(4, suiteMap, true);
133
+
134
+ // Assert
135
+ expect(result).toBe('Level1/.../Level3');
136
+ });
137
+ });
138
+
139
+ describe('convertRunStatus', () => {
140
+ it('should convert API status to readable format', () => {
141
+ // Arrange & Act & Assert
142
+ expect((resultDataProvider as any).convertRunStatus('passed')).toBe('Passed');
143
+ expect((resultDataProvider as any).convertRunStatus('failed')).toBe('Failed');
144
+ expect((resultDataProvider as any).convertRunStatus('notApplicable')).toBe('Not Applicable');
145
+ expect((resultDataProvider as any).convertRunStatus('unknown')).toBe('Not Run');
146
+ });
147
+ });
148
+
149
+ describe('compareActionResults', () => {
150
+ it('should compare version-like step positions correctly', () => {
151
+ // Act & Assert
152
+ const compare = (resultDataProvider as any).compareActionResults;
153
+ expect(compare('1', '2')).toBe(-1);
154
+ expect(compare('2', '1')).toBe(1);
155
+ expect(compare('1.1', '1.2')).toBe(-1);
156
+ expect(compare('1.2', '1.1')).toBe(1);
157
+ expect(compare('1.1', '1.1')).toBe(0);
158
+ expect(compare('1.1.1', '1.1')).toBe(1);
159
+ expect(compare('1.1', '1.1.1')).toBe(-1);
160
+ });
161
+ });
162
+ });
163
+
164
+ describe('Data fetching methods', () => {
165
+ describe('fetchTestSuites', () => {
166
+ it('should fetch and process test suites correctly', async () => {
167
+ // Arrange
168
+ const mockTestSuites = {
169
+ value: [
170
+ {
171
+ id: 1,
172
+ name: 'Root Suite',
173
+ children: [{ id: 2, name: 'Child Suite 1', parentSuite: { id: 1 } }],
174
+ },
175
+ ],
176
+ count: 1,
177
+ };
178
+
179
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockTestSuites);
180
+
181
+ // Act
182
+ const result = await (resultDataProvider as any).fetchTestSuites(mockTestPlanId, mockProjectName);
183
+
184
+ // Assert
185
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
186
+ `${mockOrgUrl}${mockProjectName}/_apis/testplan/Plans/${mockTestPlanId}/Suites?asTreeView=true`,
187
+ mockToken
188
+ );
189
+ expect(result).toHaveLength(1);
190
+ expect(result[0]).toHaveProperty('testSuiteId', 2);
191
+ expect(result[0]).toHaveProperty('testGroupName');
192
+ });
193
+
194
+ it('should handle errors and return empty array', async () => {
195
+ // Arrange
196
+ const mockError = new Error('API error');
197
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(mockError);
198
+
199
+ // Act
200
+ const result = await (resultDataProvider as any).fetchTestSuites(mockTestPlanId, mockProjectName);
201
+
202
+ // Assert
203
+ expect(logger.error).toHaveBeenCalled();
204
+ expect(result).toEqual([]);
205
+ });
206
+ });
207
+
208
+ describe('fetchTestPoints', () => {
209
+ it('should fetch and map test points correctly', async () => {
210
+ // Arrange
211
+ const mockSuiteId = '123';
212
+ const mockTestPoints = {
213
+ value: [
214
+ {
215
+ testCaseReference: { id: 1, name: 'Test Case 1' },
216
+ configuration: { name: 'Config 1' },
217
+ results: {
218
+ outcome: 'passed',
219
+ lastTestRunId: 100,
220
+ lastResultId: 200,
221
+ lastResultDetails: { dateCompleted: '2023-01-01', runBy: { displayName: 'Test User' } },
222
+ },
223
+ },
224
+ ],
225
+ count: 1,
226
+ };
227
+
228
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockTestPoints);
229
+
230
+ // Act
231
+ const result = await (resultDataProvider as any).fetchTestPoints(
232
+ mockProjectName,
233
+ mockTestPlanId,
234
+ mockSuiteId
235
+ );
236
+
237
+ // Assert
238
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
239
+ `${mockOrgUrl}${mockProjectName}/_apis/testplan/Plans/${mockTestPlanId}/Suites/${mockSuiteId}/TestPoint?includePointDetails=true`,
240
+ mockToken
241
+ );
242
+ expect(result).toHaveLength(1);
243
+ expect(result[0]).toEqual({
244
+ testCaseId: 1,
245
+ testCaseName: 'Test Case 1',
246
+ configurationName: 'Config 1',
247
+ outcome: 'passed',
248
+ lastRunId: 100,
249
+ lastResultId: 200,
250
+ lastResultDetails: { dateCompleted: '2023-01-01', runBy: { displayName: 'Test User' } },
251
+ testCaseUrl: 'https://dev.azure.com/organization/test-project/_workitems/edit/1',
252
+ });
253
+ });
254
+
255
+ it('should handle errors and return empty array', async () => {
256
+ // Arrange
257
+ const mockSuiteId = '123';
258
+ const mockError = new Error('API error');
259
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(mockError);
260
+
261
+ // Act
262
+ const result = await (resultDataProvider as any).fetchTestPoints(
263
+ mockProjectName,
264
+ mockTestPlanId,
265
+ mockSuiteId
266
+ );
267
+
268
+ // Assert
269
+ expect(logger.error).toHaveBeenCalled();
270
+ expect(result).toEqual([]);
271
+ });
272
+ });
273
+ });
274
+
275
+ describe('Data transformation methods', () => {
276
+ describe('mapTestPoint', () => {
277
+ it('should transform test point data correctly', () => {
278
+ // Arrange
279
+ const testPoint = {
280
+ testCaseReference: { id: 1, name: 'Test Case 1' },
281
+ configuration: { name: 'Config 1' },
282
+ results: {
283
+ outcome: 'passed',
284
+ lastTestRunId: 100,
285
+ lastResultId: 200,
286
+ lastResultDetails: { dateCompleted: '2023-01-01', runBy: { displayName: 'Test User' } },
287
+ },
288
+ };
289
+
290
+ // Act
291
+ const result = (resultDataProvider as any).mapTestPoint(testPoint, mockProjectName);
292
+
293
+ // Assert
294
+ expect(result).toEqual({
295
+ testCaseId: 1,
296
+ testCaseName: 'Test Case 1',
297
+ configurationName: 'Config 1',
298
+ outcome: 'passed',
299
+ lastRunId: 100,
300
+ lastResultId: 200,
301
+ lastResultDetails: { dateCompleted: '2023-01-01', runBy: { displayName: 'Test User' } },
302
+ testCaseUrl: 'https://dev.azure.com/organization/test-project/_workitems/edit/1',
303
+ });
304
+ });
305
+
306
+ it('should handle missing fields', () => {
307
+ // Arrange
308
+ const testPoint = {
309
+ testCaseReference: { id: 1, name: 'Test Case 1' },
310
+ // No configuration or results
311
+ };
312
+
313
+ // Act
314
+ const result = (resultDataProvider as any).mapTestPoint(testPoint, mockProjectName);
315
+
316
+ // Assert
317
+ expect(result).toEqual({
318
+ testCaseId: 1,
319
+ testCaseName: 'Test Case 1',
320
+ configurationName: undefined,
321
+ outcome: 'Not Run',
322
+ lastRunId: undefined,
323
+ lastResultId: undefined,
324
+ lastResultDetails: undefined,
325
+ testCaseUrl: 'https://dev.azure.com/organization/test-project/_workitems/edit/1',
326
+ });
327
+ });
328
+ });
329
+
330
+ describe('calculateGroupResultSummary', () => {
331
+ it('should return empty strings when includeHardCopyRun is true', () => {
332
+ // Arrange
333
+ const testPoints = [{ outcome: 'passed' }, { outcome: 'failed' }];
334
+
335
+ // Act
336
+ const result = (resultDataProvider as any).calculateGroupResultSummary(testPoints, true);
337
+
338
+ // Assert
339
+ expect(result).toEqual({
340
+ passed: '',
341
+ failed: '',
342
+ notApplicable: '',
343
+ blocked: '',
344
+ notRun: '',
345
+ total: '',
346
+ successPercentage: '',
347
+ });
348
+ });
349
+
350
+ it('should calculate summary statistics correctly', () => {
351
+ // Arrange
352
+ const testPoints = [
353
+ { outcome: 'passed' },
354
+ { outcome: 'passed' },
355
+ { outcome: 'failed' },
356
+ { outcome: 'notApplicable' },
357
+ { outcome: 'blocked' },
358
+ { outcome: 'something else' },
359
+ ];
360
+
361
+ // Act
362
+ const result = (resultDataProvider as any).calculateGroupResultSummary(testPoints, false);
363
+
364
+ // Assert
365
+ expect(result).toEqual({
366
+ passed: 2,
367
+ failed: 1,
368
+ notApplicable: 1,
369
+ blocked: 1,
370
+ notRun: 1,
371
+ total: 6,
372
+ successPercentage: '33.33%',
373
+ });
374
+ });
375
+
376
+ it('should handle empty array', () => {
377
+ // Arrange
378
+ const testPoints: any[] = [];
379
+
380
+ // Act
381
+ const result = (resultDataProvider as any).calculateGroupResultSummary(testPoints, false);
382
+
383
+ // Assert
384
+ expect(result).toEqual({
385
+ passed: 0,
386
+ failed: 0,
387
+ notApplicable: 0,
388
+ blocked: 0,
389
+ notRun: 0,
390
+ total: 0,
391
+ successPercentage: '0.00%',
392
+ });
393
+ });
394
+ });
395
+
396
+ describe('mapAttachmentsUrl', () => {
397
+ it('should map attachment URLs correctly', () => {
398
+ // Arrange
399
+ const mockRunResults = [
400
+ {
401
+ testCaseId: 1,
402
+ lastRunId: 100,
403
+ lastResultId: 200,
404
+ iteration: {
405
+ attachments: [{ id: 1, name: 'attachment1.png', actionPath: 'path1' }],
406
+ actionResults: [{ actionPath: 'path1', stepPosition: '1.1' }],
407
+ },
408
+ analysisAttachments: [{ id: 2, fileName: 'analysis1.txt' }],
409
+ },
410
+ ];
411
+
412
+ // Act
413
+ const result = resultDataProvider.mapAttachmentsUrl(mockRunResults, mockProjectName);
414
+
415
+ // Assert
416
+ expect(result[0].iteration.attachments[0].downloadUrl).toBe(
417
+ `${mockOrgUrl}${mockProjectName}/_apis/test/runs/100/results/200/attachments/1/attachment1.png`
418
+ );
419
+ expect(result[0].iteration.attachments[0].stepNo).toBe('1.1');
420
+ expect(result[0].analysisAttachments[0].downloadUrl).toBe(
421
+ `${mockOrgUrl}${mockProjectName}/_apis/test/runs/100/results/200/attachments/2/analysis1.txt`
422
+ );
423
+ });
424
+
425
+ it('should handle missing iteration', () => {
426
+ // Arrange
427
+ const mockRunResults = [
428
+ {
429
+ testCaseId: 1,
430
+ lastRunId: 100,
431
+ lastResultId: 200,
432
+ // No iteration
433
+ analysisAttachments: [{ id: 2, fileName: 'analysis1.txt' }],
434
+ },
435
+ ];
436
+
437
+ // Act
438
+ const result = resultDataProvider.mapAttachmentsUrl(mockRunResults, mockProjectName);
439
+
440
+ // Assert
441
+ expect(result[0]).toEqual(mockRunResults[0]);
442
+ });
443
+ });
444
+ });
445
+
446
+ describe('formatTestResult', () => {
447
+ it('should format test result correctly', () => {
448
+ // Arrange
449
+ const testPoint = {
450
+ testCaseId: 1,
451
+ testCaseName: 'Test Case 1',
452
+ testGroupName: 'Suite 1',
453
+ testCaseUrl: 'https://example.com/workitems/1',
454
+ configurationName: 'Config 1',
455
+ outcome: 'passed',
456
+ };
457
+
458
+ // Act
459
+ const result = (resultDataProvider as any).formatTestResult(testPoint, true, false);
460
+
461
+ // Assert
462
+ expect(result.testId).toBe(1);
463
+ expect(result.testName).toBe('Test Case 1');
464
+ expect(result.configuration).toBe('Config 1');
465
+ expect(result.runStatus).toBe('Passed');
466
+ });
467
+
468
+ it('should return empty strings when includeHardCopyRun is true', () => {
469
+ // Arrange
470
+ const testPoint = {
471
+ testCaseId: 1,
472
+ testCaseName: 'Test Case 1',
473
+ testGroupName: 'Suite 1',
474
+ outcome: 'passed',
475
+ };
476
+
477
+ // Act
478
+ const result = (resultDataProvider as any).formatTestResult(testPoint, false, true);
479
+
480
+ // Assert
481
+ expect(result.runStatus).toBe('');
482
+ });
483
+ });
484
+
485
+ describe('calculateTotalSummary', () => {
486
+ it('should calculate total summary from summarized results', () => {
487
+ // Arrange
488
+ const summarizedResults = [
489
+ { groupResultSummary: { passed: 2, failed: 1, notApplicable: 0, blocked: 0, notRun: 1, total: 4 } },
490
+ { groupResultSummary: { passed: 3, failed: 0, notApplicable: 1, blocked: 1, notRun: 0, total: 5 } },
491
+ ];
492
+
493
+ // Act
494
+ const result = (resultDataProvider as any).calculateTotalSummary(summarizedResults, false);
495
+
496
+ // Assert
497
+ expect(result.passed).toBe(5);
498
+ expect(result.failed).toBe(1);
499
+ expect(result.notApplicable).toBe(1);
500
+ expect(result.blocked).toBe(1);
501
+ expect(result.notRun).toBe(1);
502
+ expect(result.total).toBe(9);
503
+ });
504
+
505
+ it('should return empty strings when includeHardCopyRun is true', () => {
506
+ // Arrange
507
+ const summarizedResults = [{ groupResultSummary: { passed: 2, failed: 1, total: 3 } }];
508
+
509
+ // Act
510
+ const result = (resultDataProvider as any).calculateTotalSummary(summarizedResults, true);
511
+
512
+ // Assert
513
+ expect(result.passed).toBe('');
514
+ expect(result.failed).toBe('');
515
+ expect(result.total).toBe('');
516
+ });
517
+ });
518
+
519
+ describe('flattenTestPoints', () => {
520
+ it('should flatten test points from suites', () => {
521
+ // Arrange
522
+ const testPoints = [
523
+ { testSuiteId: 1, testGroupName: 'Suite 1', testPointsItems: [{ id: 1 }, { id: 2 }] },
524
+ { testSuiteId: 2, testGroupName: 'Suite 2', testPointsItems: [{ id: 3 }] },
525
+ ];
526
+
527
+ // Act
528
+ const result = (resultDataProvider as any).flattenTestPoints(testPoints);
529
+
530
+ // Assert
531
+ expect(result).toHaveLength(3);
532
+ expect(result[0].testGroupName).toBe('Suite 1');
533
+ expect(result[2].testGroupName).toBe('Suite 2');
534
+ });
535
+ });
536
+
537
+ describe('createSuiteMap', () => {
538
+ it('should create a map of suites by ID', () => {
539
+ // Arrange
540
+ const suites = [
541
+ { id: 1, name: 'Suite 1', children: [{ id: 2, name: 'Suite 2' }] },
542
+ { id: 3, name: 'Suite 3' },
543
+ ];
544
+
545
+ // Act
546
+ const result = (resultDataProvider as any).createSuiteMap(suites);
547
+
548
+ // Assert
549
+ expect(result).toBeInstanceOf(Map);
550
+ expect(result.size).toBe(3);
551
+ expect(result.get(1).name).toBe('Suite 1');
552
+ expect(result.get(2).name).toBe('Suite 2');
553
+ });
554
+ });
555
+
556
+ describe('isNotRunStep', () => {
557
+ it('should return true for not run step', () => {
558
+ // Arrange
559
+ const step = { stepStatus: 'Not Run' };
560
+
561
+ // Act
562
+ const result = (resultDataProvider as any).isNotRunStep(step);
563
+
564
+ // Assert
565
+ expect(result).toBe(true);
566
+ });
567
+
568
+ it('should return false for run step', () => {
569
+ // Arrange
570
+ const step = { stepStatus: 'Passed' };
571
+
572
+ // Act
573
+ const result = (resultDataProvider as any).isNotRunStep(step);
574
+
575
+ // Assert
576
+ expect(result).toBe(false);
577
+ });
578
+ });
579
+
580
+ describe('CreateAttachmentPathIndexMap', () => {
581
+ it('should create map of action paths to step positions', () => {
582
+ // Arrange
583
+ const actionResults = [
584
+ { actionPath: 'path1', stepPosition: '1.1' },
585
+ { actionPath: 'path2', stepPosition: '1.2' },
586
+ ];
587
+
588
+ // Act
589
+ const result = (resultDataProvider as any).CreateAttachmentPathIndexMap(actionResults);
590
+
591
+ // Assert
592
+ expect(result).toBeInstanceOf(Map);
593
+ expect(result.get('path1')).toBe('1.1');
594
+ expect(result.get('path2')).toBe('1.2');
595
+ });
596
+ });
597
+
598
+ describe('getTestPointsForTestCases', () => {
599
+ it('should fetch test points for test cases', async () => {
600
+ // Arrange
601
+ const mockTestCaseIds = ['1', '2'];
602
+ const mockResponse = { data: { points: [{ id: 1 }] } };
603
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce(mockResponse);
604
+
605
+ // Act
606
+ const result = await resultDataProvider.getTestPointsForTestCases(mockProjectName, mockTestCaseIds);
607
+
608
+ // Assert
609
+ expect(TFSServices.postRequest).toHaveBeenCalled();
610
+ expect(result).toEqual(mockResponse);
611
+ });
612
+ });
613
+
614
+ describe('mapActionPathToPosition', () => {
615
+ it('should map action paths to positions', () => {
616
+ // Arrange
617
+ const actionResults = [
618
+ { testId: 1, actionPath: 'path1', stepNo: '1.1' },
619
+ { testId: 1, actionPath: 'path2', stepNo: '1.2' },
620
+ ];
621
+
622
+ // Act
623
+ const result = (resultDataProvider as any).mapActionPathToPosition(actionResults);
624
+
625
+ // Assert
626
+ expect(result).toBeInstanceOf(Map);
627
+ expect(result.get('1-path1')).toBe('1.1');
628
+ expect(result.get('1-path2')).toBe('1.2');
629
+ });
630
+ });
631
+
632
+ describe('fetchTestPlanName', () => {
633
+ it('should fetch test plan name', async () => {
634
+ // Arrange
635
+ const mockPlan = { name: 'Test Plan 1' };
636
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockPlan);
637
+
638
+ // Act
639
+ const result = await (resultDataProvider as any).fetchTestPlanName(mockTestPlanId, mockProjectName);
640
+
641
+ // Assert
642
+ expect(result).toBe('Test Plan 1');
643
+ });
644
+
645
+ it('should return empty string on error', async () => {
646
+ // Arrange
647
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
648
+
649
+ // Act
650
+ const result = await (resultDataProvider as any).fetchTestPlanName(mockTestPlanId, mockProjectName);
651
+
652
+ // Assert
653
+ expect(result).toBe('');
654
+ expect(logger.error).toHaveBeenCalled();
655
+ });
656
+ });
657
+
658
+ describe('fetchTestCasesBySuiteId', () => {
659
+ it('should fetch test cases by suite ID', async () => {
660
+ // Arrange
661
+ const mockSuiteId = '123';
662
+ const mockTestCases = { value: [{ workItem: { id: 1 } }] };
663
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockTestCases);
664
+
665
+ // Act
666
+ const result = await (resultDataProvider as any).fetchTestCasesBySuiteId(
667
+ mockProjectName,
668
+ mockTestPlanId,
669
+ mockSuiteId
670
+ );
671
+
672
+ // Assert
673
+ expect(result).toEqual(mockTestCases.value);
674
+ });
675
+ });
676
+
677
+ describe('mapTestPointForCrossPlans', () => {
678
+ it('should map test point for cross plans', () => {
679
+ // Arrange
680
+ const testPoint = {
681
+ testCase: { id: 1, name: 'Test Case 1' },
682
+ testSuite: { id: 2, name: 'Suite 1' },
683
+ configuration: { name: 'Config 1' },
684
+ outcome: 'passed',
685
+ lastTestRun: { id: '100' },
686
+ lastResult: { id: '200' },
687
+ };
688
+
689
+ // Act
690
+ const result = (resultDataProvider as any).mapTestPointForCrossPlans(testPoint, mockProjectName);
691
+
692
+ // Assert
693
+ expect(result.testCaseId).toBe(1);
694
+ expect(result.testCaseName).toBe('Test Case 1');
695
+ expect(result.outcome).toBe('passed');
696
+ expect(result.lastRunId).toBe('100');
697
+ });
698
+
699
+ it('should provide default values for missing fields', () => {
700
+ // Arrange
701
+ const testPoint = {
702
+ testCase: { id: 1, name: 'Test Case 1' },
703
+ testSuite: { id: 2, name: 'Suite 1' },
704
+ };
705
+
706
+ // Act
707
+ const result = (resultDataProvider as any).mapTestPointForCrossPlans(testPoint, mockProjectName);
708
+
709
+ // Assert
710
+ expect(result.outcome).toBe('Not Run');
711
+ expect(result.lastResultDetails).toBeDefined();
712
+ expect(result.lastResultDetails.duration).toBe(0);
713
+ });
714
+ });
715
+
716
+ describe('mapStepResultsForExecutionAppendix', () => {
717
+ it('should map step results for execution appendix', () => {
718
+ // Arrange
719
+ const detailedResults = [
720
+ {
721
+ testId: 1,
722
+ testCaseRevision: { rev: 1 },
723
+ stepNo: '1.1',
724
+ stepIdentifier: 'step1',
725
+ stepAction: 'Do something',
726
+ stepExpected: 'Something happens',
727
+ stepStatus: 'Passed',
728
+ stepComments: '',
729
+ isSharedStepTitle: false,
730
+ actionPath: 'path1',
731
+ },
732
+ ];
733
+ const runResultData = [
734
+ {
735
+ testCaseId: 1,
736
+ iteration: {
737
+ attachments: [{ name: 'attachment.png', actionPath: 'path1', downloadUrl: 'http://example.com' }],
738
+ },
739
+ },
740
+ ];
741
+
742
+ // Act
743
+ const result = (resultDataProvider as any).mapStepResultsForExecutionAppendix(
744
+ detailedResults,
745
+ runResultData
746
+ );
747
+
748
+ // Assert
749
+ expect(result).toBeInstanceOf(Map);
750
+ expect(result.has('1')).toBe(true);
751
+ });
752
+ });
753
+
754
+ describe('fetchCrossTestPoints', () => {
755
+ it('should return empty array when no test case IDs provided', async () => {
756
+ // Act
757
+ const result = await (resultDataProvider as any).fetchCrossTestPoints(mockProjectName, []);
758
+
759
+ // Assert
760
+ expect(result).toEqual([]);
761
+ });
762
+
763
+ it('should handle error and return empty array', async () => {
764
+ // Arrange
765
+ (TFSServices.postRequest as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
766
+
767
+ // Act
768
+ const result = await (resultDataProvider as any).fetchCrossTestPoints(mockProjectName, [1, 2]);
769
+
770
+ // Assert
771
+ expect(result).toEqual([]);
772
+ expect(logger.error).toHaveBeenCalled();
773
+ });
774
+
775
+ it('should return empty array when API returns invalid response format', async () => {
776
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce({ data: {} });
777
+
778
+ const result = await (resultDataProvider as any).fetchCrossTestPoints(mockProjectName, [1, 2]);
779
+
780
+ expect(result).toEqual([]);
781
+ expect(logger.warn).toHaveBeenCalledWith('No test points found or invalid response format');
782
+ });
783
+
784
+ it('should pick the latest point per test case and map details with defaults', async () => {
785
+ (TFSServices.postRequest as jest.Mock).mockResolvedValueOnce({
786
+ data: {
787
+ points: [
788
+ {
789
+ testCase: { id: 1 },
790
+ lastTestRun: { id: '1' },
791
+ lastResult: { id: '5' },
792
+ url: 'https://example.com/points/1',
793
+ },
794
+ {
795
+ testCase: { id: 1 },
796
+ lastTestRun: { id: '2' },
797
+ lastResult: { id: '1' },
798
+ url: 'https://example.com/points/2',
799
+ },
800
+ {
801
+ testCase: { id: 2 },
802
+ lastTestRun: { id: '1' },
803
+ lastResult: { id: '1' },
804
+ url: 'https://example.com/points/3',
805
+ },
806
+ ],
807
+ },
808
+ });
809
+
810
+ (TFSServices.getItemContent as jest.Mock)
811
+ .mockResolvedValueOnce({
812
+ testCase: { id: 1, name: 'TC 1' },
813
+ testSuite: { id: 10, name: 'Suite' },
814
+ configuration: { name: 'Config' },
815
+ outcome: 'passed',
816
+ lastTestRun: { id: '2' },
817
+ lastResult: { id: '1' },
818
+ })
819
+ .mockResolvedValueOnce({
820
+ testCase: { id: 2, name: 'TC 2' },
821
+ testSuite: { id: 11, name: 'Suite' },
822
+ configuration: { name: 'Config' },
823
+ outcome: 'failed',
824
+ lastTestRun: { id: '1' },
825
+ lastResult: { id: '1' },
826
+ lastResultDetails: { duration: 5, dateCompleted: '2023-01-01', runBy: { displayName: 'User' } },
827
+ });
828
+
829
+ const result = await (resultDataProvider as any).fetchCrossTestPoints(mockProjectName, [1, 2]);
830
+
831
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
832
+ 'https://example.com/points/2?witFields=Microsoft.VSTS.TCM.Steps&includePointDetails=true',
833
+ mockToken
834
+ );
835
+ expect(result).toHaveLength(2);
836
+ const tc1 = result.find((r: any) => r.testCaseId === 1);
837
+ expect(tc1.lastResultDetails).toEqual(
838
+ expect.objectContaining({
839
+ duration: 0,
840
+ runBy: expect.objectContaining({ displayName: 'No tester' }),
841
+ })
842
+ );
843
+ });
844
+ });
845
+
846
+ describe('fetchLinkedWi', () => {
847
+ it('should fetch linked work items and filter only open Bugs/Change Requests', async () => {
848
+ const testItems = [
849
+ {
850
+ testId: 1,
851
+ testName: 'TC 1',
852
+ testCaseUrl: 'http://example.com/tc/1',
853
+ runStatus: 'Passed',
854
+ },
855
+ ];
856
+
857
+ (TFSServices.getItemContent as jest.Mock)
858
+ .mockResolvedValueOnce({
859
+ value: [
860
+ {
861
+ id: 1,
862
+ relations: [
863
+ { url: `${mockOrgUrl}_apis/wit/workItems/100` },
864
+ { url: `${mockOrgUrl}_apis/wit/workItems/101` },
865
+ ],
866
+ },
867
+ ],
868
+ })
869
+ .mockResolvedValueOnce({
870
+ value: [
871
+ {
872
+ id: 100,
873
+ fields: {
874
+ 'System.WorkItemType': 'Bug',
875
+ 'System.State': 'Active',
876
+ 'System.Title': 'Open bug',
877
+ 'Microsoft.VSTS.Common.Severity': '1 - Critical',
878
+ },
879
+ },
880
+ {
881
+ id: 101,
882
+ fields: {
883
+ 'System.WorkItemType': 'Bug',
884
+ 'System.State': 'Closed',
885
+ 'System.Title': 'Closed bug',
886
+ },
887
+ },
888
+ ],
889
+ });
890
+
891
+ const result = await (resultDataProvider as any).fetchLinkedWi(mockProjectName, testItems);
892
+
893
+ expect(result).toHaveLength(1);
894
+ expect(result[0].linkItems).toHaveLength(1);
895
+ expect(result[0].linkItems[0]).toEqual(
896
+ expect.objectContaining({
897
+ pcrId: 100,
898
+ workItemType: 'Bug',
899
+ title: 'Open bug',
900
+ pcrUrl: `${mockOrgUrl}${mockProjectName}/_workitems/edit/100`,
901
+ })
902
+ );
903
+ });
904
+ });
905
+
906
+ describe('getTestReporterResults filtering', () => {
907
+ it('should apply errorFilterMode=both and run-step filter and return the filtered rows', async () => {
908
+ const testReporterRows = [
909
+ {
910
+ testCase: {
911
+ comment: 'has comment',
912
+ result: { resultMessage: 'Failed in Run 1' },
913
+ },
914
+ stepComments: 'step comment',
915
+ stepStatus: 'Failed',
916
+ },
917
+ {
918
+ testCase: {
919
+ comment: '',
920
+ result: { resultMessage: 'Passed' },
921
+ },
922
+ stepComments: '',
923
+ stepStatus: 'Not Run',
924
+ },
925
+ ];
926
+
927
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
928
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([]);
929
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([]);
930
+ jest.spyOn(resultDataProvider as any, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([]);
931
+ jest
932
+ .spyOn(resultDataProvider as any, 'alignStepsWithIterationsTestReporter')
933
+ .mockReturnValueOnce(testReporterRows);
934
+
935
+ const linkedQueryRequest = {
936
+ linkedQueryMode: 'none',
937
+ testAssociatedQuery: { wiql: { href: 'https://example.com/wiql' }, columns: [] },
938
+ };
939
+
940
+ const result = await resultDataProvider.getTestReporterResults(
941
+ 'planId',
942
+ mockProjectName,
943
+ [],
944
+ [],
945
+ false,
946
+ true,
947
+ true,
948
+ linkedQueryRequest,
949
+ 'both'
950
+ );
951
+
952
+ expect(result).toBeDefined();
953
+ const first = (result as any[])[0];
954
+ expect(first).toBeDefined();
955
+ expect(first.data).toHaveLength(1);
956
+ expect(first.data[0].stepStatus).toBe('Failed');
957
+ });
958
+
959
+ it('should filter only test case results when errorFilterMode=onlyTestCaseResult', async () => {
960
+ const rows = [
961
+ {
962
+ testCase: { comment: 'c', result: { resultMessage: 'Passed' } },
963
+ stepComments: '',
964
+ stepStatus: 'Not Run',
965
+ },
966
+ {
967
+ testCase: { comment: '', result: { resultMessage: 'Failed in Run 1' } },
968
+ stepComments: '',
969
+ stepStatus: 'Not Run',
970
+ },
971
+ {
972
+ testCase: { comment: '', result: { resultMessage: 'Passed' } },
973
+ stepComments: '',
974
+ stepStatus: 'Not Run',
975
+ },
976
+ ];
977
+
978
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
979
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([]);
980
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([]);
981
+ jest.spyOn(resultDataProvider as any, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([]);
982
+ jest.spyOn(resultDataProvider as any, 'alignStepsWithIterationsTestReporter').mockReturnValueOnce(rows);
983
+
984
+ const res = await resultDataProvider.getTestReporterResults(
985
+ 'planId',
986
+ mockProjectName,
987
+ [],
988
+ [],
989
+ false,
990
+ true,
991
+ false,
992
+ { linkedQueryMode: 'none', testAssociatedQuery: { wiql: { href: 'x' }, columns: [] } },
993
+ 'onlyTestCaseResult'
994
+ );
995
+
996
+ expect((res as any[])[0].data).toHaveLength(2);
997
+ });
998
+
999
+ it('should filter only step results when errorFilterMode=onlyTestStepsResult', async () => {
1000
+ const rows = [
1001
+ {
1002
+ testCase: { comment: '', result: { resultMessage: 'Passed' } },
1003
+ stepComments: 'x',
1004
+ stepStatus: 'Not Run',
1005
+ },
1006
+ {
1007
+ testCase: { comment: '', result: { resultMessage: 'Passed' } },
1008
+ stepComments: '',
1009
+ stepStatus: 'Failed',
1010
+ },
1011
+ {
1012
+ testCase: { comment: '', result: { resultMessage: 'Passed' } },
1013
+ stepComments: '',
1014
+ stepStatus: 'Not Run',
1015
+ },
1016
+ ];
1017
+
1018
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1019
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([]);
1020
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([]);
1021
+ jest.spyOn(resultDataProvider as any, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([]);
1022
+ jest.spyOn(resultDataProvider as any, 'alignStepsWithIterationsTestReporter').mockReturnValueOnce(rows);
1023
+
1024
+ const res = await resultDataProvider.getTestReporterResults(
1025
+ 'planId',
1026
+ mockProjectName,
1027
+ [],
1028
+ [],
1029
+ false,
1030
+ true,
1031
+ false,
1032
+ { linkedQueryMode: 'none', testAssociatedQuery: { wiql: { href: 'x' }, columns: [] } },
1033
+ 'onlyTestStepsResult'
1034
+ );
1035
+
1036
+ expect((res as any[])[0].data).toHaveLength(2);
1037
+ });
1038
+
1039
+ it('should execute query mode and call TicketsDataProvider when linkedQueryMode=query', async () => {
1040
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1041
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([]);
1042
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([]);
1043
+ jest.spyOn(resultDataProvider as any, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([]);
1044
+ jest.spyOn(resultDataProvider as any, 'alignStepsWithIterationsTestReporter').mockReturnValueOnce([]);
1045
+
1046
+ const linkedQueryRequest = {
1047
+ linkedQueryMode: 'query',
1048
+ testAssociatedQuery: {
1049
+ wiql: { href: 'https://example.com/wiql' },
1050
+ columns: [{ referenceName: 'X', name: 'X' }],
1051
+ },
1052
+ };
1053
+
1054
+ await resultDataProvider.getTestReporterResults(
1055
+ 'planId',
1056
+ mockProjectName,
1057
+ [],
1058
+ [],
1059
+ false,
1060
+ true,
1061
+ false,
1062
+ linkedQueryRequest,
1063
+ 'none'
1064
+ );
1065
+
1066
+ const TicketsProviderMock: any = require('../../modules/TicketsDataProvider').default;
1067
+ expect(TicketsProviderMock).toHaveBeenCalled();
1068
+ const instance = TicketsProviderMock.mock.results[0].value;
1069
+ expect(instance.GetQueryResultsFromWiql).toHaveBeenCalledWith(
1070
+ 'https://example.com/wiql',
1071
+ true,
1072
+ expect.any(Map)
1073
+ );
1074
+ });
1075
+ });
1076
+
1077
+ describe('fetchResultDataForTestReporter (runResultField switch)', () => {
1078
+ it('should populate requested runResultField values including testCaseResult URL branches', async () => {
1079
+ jest
1080
+ .spyOn(resultDataProvider as any, 'fetchResultDataBase')
1081
+ .mockImplementation(async (...args: any[]) => {
1082
+ const testSuiteId = args[1];
1083
+ const formatter = args[4];
1084
+ const extra = args[5] as any[];
1085
+ const selectedFields = extra[0];
1086
+ const isQueryMode = extra[1];
1087
+ const pt = extra[2];
1088
+ return formatter(
1089
+ {
1090
+ testCase: { id: 1, name: 'TC 1' },
1091
+ testSuite: { name: 'Suite' },
1092
+ testCaseRevision: 7,
1093
+ resolutionState: 'x',
1094
+ failureType: 'FT',
1095
+ priority: 2,
1096
+ outcome: 'passed',
1097
+ iterationDetails: [],
1098
+ filteredFields: { 'Custom.Field1': 'v1' },
1099
+ relatedRequirements: [],
1100
+ relatedBugs: [],
1101
+ relatedCRs: [],
1102
+ },
1103
+ testSuiteId,
1104
+ pt,
1105
+ selectedFields,
1106
+ isQueryMode
1107
+ );
1108
+ });
1109
+
1110
+ const selectedFields = [
1111
+ 'priority@runResultField',
1112
+ 'testCaseResult@runResultField',
1113
+ 'testCaseComment@runResultField',
1114
+ 'failureType@runResultField',
1115
+ 'runBy@runResultField',
1116
+ 'executionDate@runResultField',
1117
+ 'configurationName@runResultField',
1118
+ 'unknownField@runResultField',
1119
+ ];
1120
+
1121
+ const point = {
1122
+ lastRunId: 10,
1123
+ lastResultId: 20,
1124
+ configurationName: 'Cfg',
1125
+ lastResultDetails: { runBy: { displayName: 'User' }, dateCompleted: '2023-01-01' },
1126
+ };
1127
+
1128
+ const res = await (resultDataProvider as any).fetchResultDataForTestReporter(
1129
+ mockProjectName,
1130
+ 'suite1',
1131
+ point,
1132
+ selectedFields,
1133
+ false
1134
+ );
1135
+
1136
+ expect(res.priority).toBe(2);
1137
+ expect(res.testCaseResult).toEqual(
1138
+ expect.objectContaining({
1139
+ resultMessage: expect.stringContaining('Run 10'),
1140
+ url: expect.stringContaining('runId=10'),
1141
+ })
1142
+ );
1143
+ expect(res.runBy).toBe('User');
1144
+ expect(res.executionDate).toBe('2023-01-01');
1145
+ expect(res.configurationName).toBe('Cfg');
1146
+ expect(res.customFields).toEqual(expect.objectContaining({ field1: 'v1' }));
1147
+ });
1148
+
1149
+ it('should set testCaseResult url empty when lastRunId/lastResultId are undefined', async () => {
1150
+ jest
1151
+ .spyOn(resultDataProvider as any, 'fetchResultDataBase')
1152
+ .mockImplementation(async (...args: any[]) => {
1153
+ const testSuiteId = args[1];
1154
+ const point = args[2];
1155
+ const formatter = args[4];
1156
+ const extra = args[5] as any[];
1157
+ return formatter(
1158
+ {
1159
+ testCase: { id: 1, name: 'TC 1' },
1160
+ testSuite: { name: 'Suite' },
1161
+ testCaseRevision: 1,
1162
+ resolutionState: 'x',
1163
+ failureType: 'FT',
1164
+ priority: 1,
1165
+ outcome: 'passed',
1166
+ iterationDetails: [],
1167
+ },
1168
+ testSuiteId,
1169
+ point,
1170
+ extra[0]
1171
+ );
1172
+ });
1173
+
1174
+ const res = await (resultDataProvider as any).fetchResultDataForTestReporter(
1175
+ mockProjectName,
1176
+ 'suite1',
1177
+ {
1178
+ lastRunId: undefined,
1179
+ lastResultId: undefined,
1180
+ configurationName: 'Cfg',
1181
+ lastResultDetails: { runBy: { displayName: 'U' }, dateCompleted: 'd' },
1182
+ },
1183
+ ['testCaseResult@runResultField'],
1184
+ false
1185
+ );
1186
+
1187
+ expect(res.testCaseResult).toEqual(expect.objectContaining({ url: '' }));
1188
+ });
1189
+ });
1190
+
1191
+ describe('fetchResultDataBasedOnWiBase', () => {
1192
+ it('should return null and warn when runId/resultId are 0 and no point is provided', async () => {
1193
+ const res = await (resultDataProvider as any).fetchResultDataBasedOnWiBase(mockProjectName, '0', '0');
1194
+ expect(res).toBeNull();
1195
+ expect(logger.warn).toHaveBeenCalled();
1196
+ });
1197
+
1198
+ it('should build synthetic result for Active state when runId/resultId are 0 and point is provided', async () => {
1199
+ const point = {
1200
+ testCaseId: '123',
1201
+ testCaseName: 'TC 123',
1202
+ outcome: 'passed',
1203
+ testSuite: { id: '1', name: 'Suite' },
1204
+ };
1205
+
1206
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
1207
+ id: 123,
1208
+ rev: 7,
1209
+ fields: {
1210
+ 'System.State': 'Active',
1211
+ 'System.CreatedDate': '2023-01-01T00:00:00',
1212
+ 'Microsoft.VSTS.TCM.Priority': 2,
1213
+ 'System.Title': 'Title 123',
1214
+ 'Microsoft.VSTS.TCM.Steps': '<steps></steps>',
1215
+ },
1216
+ relations: null,
1217
+ });
1218
+
1219
+ const selectedFields = ['System.Title@testCaseWorkItemField'];
1220
+ const res = await (resultDataProvider as any).fetchResultDataBasedOnWiBase(
1221
+ mockProjectName,
1222
+ '0',
1223
+ '0',
1224
+ true,
1225
+ selectedFields,
1226
+ false,
1227
+ point
1228
+ );
1229
+
1230
+ expect(res).toEqual(
1231
+ expect.objectContaining({
1232
+ id: 0,
1233
+ failureType: 'None',
1234
+ testCaseRevision: 7,
1235
+ stepsResultXml: '<steps></steps>',
1236
+ filteredFields: { 'System.Title': 'Title 123' },
1237
+ })
1238
+ );
1239
+ });
1240
+
1241
+ it('should append linked relations and filter testCaseWorkItemField when isTestReporter=true and isQueryMode=false', async () => {
1242
+ (TFSServices.getItemContent as jest.Mock).mockReset();
1243
+
1244
+ const selectedFields = ['associatedBug@linked', 'System.Title@testCaseWorkItemField'];
1245
+
1246
+ // 1) run result
1247
+ (TFSServices.getItemContent as jest.Mock)
1248
+ .mockResolvedValueOnce({
1249
+ testCase: { id: 123 },
1250
+ testCaseRevision: 7,
1251
+ testSuite: { name: 'S' },
1252
+ })
1253
+ // 2) attachments
1254
+ .mockResolvedValueOnce({ value: [] })
1255
+ // 3) wiByRevision (with relations)
1256
+ .mockResolvedValueOnce({
1257
+ id: 123,
1258
+ fields: {
1259
+ 'Microsoft.VSTS.TCM.Steps': '<steps></steps>',
1260
+ 'System.Title': { displayName: 'My Title' },
1261
+ },
1262
+ relations: [{ rel: 'System.LinkTypes.Related', url: 'https://example.com/wi/200' }],
1263
+ })
1264
+ // 4) linked bug
1265
+ .mockResolvedValueOnce({
1266
+ id: 200,
1267
+ fields: { 'System.WorkItemType': 'Bug', 'System.State': 'Active', 'System.Title': 'B200' },
1268
+ _links: { html: { href: 'http://example.com/200' } },
1269
+ });
1270
+
1271
+ const res = await (resultDataProvider as any).fetchResultDataBasedOnWiBase(
1272
+ mockProjectName,
1273
+ '10',
1274
+ '20',
1275
+ true,
1276
+ selectedFields,
1277
+ false,
1278
+ undefined,
1279
+ false
1280
+ );
1281
+
1282
+ expect(res).toEqual(expect.objectContaining({ testCaseRevision: 7 }));
1283
+ expect(res.filteredFields).toEqual({ 'System.Title': 'My Title' });
1284
+ expect(res.relatedBugs).toEqual(
1285
+ expect.arrayContaining([expect.objectContaining({ id: 200, title: 'B200', workItemType: 'Bug' })])
1286
+ );
1287
+ });
1288
+ });
1289
+
1290
+ describe('alignStepsWithIterationsBase - additional branches', () => {
1291
+ it('should include not-run test cases when enabled and includeItemsWithNoIterations=true (creates test-level result)', () => {
1292
+ const testData = [
1293
+ {
1294
+ testPointsItems: [{ testCaseId: 123, lastRunId: undefined, lastResultId: undefined }],
1295
+ testCasesItems: [
1296
+ { workItem: { id: 123, workItemFields: [{ key: 'Steps', value: '<steps></steps>' }] } },
1297
+ ],
1298
+ },
1299
+ ];
1300
+ const iterations = [
1301
+ { testCaseId: 123, lastRunId: undefined, lastResultId: undefined, iteration: null },
1302
+ ];
1303
+
1304
+ const createResultObject = jest.fn().mockReturnValue({ ok: true });
1305
+ const shouldProcessStepLevel = jest.fn().mockReturnValue(false);
1306
+
1307
+ const res = (resultDataProvider as any).alignStepsWithIterationsBase(
1308
+ testData,
1309
+ iterations,
1310
+ true,
1311
+ true,
1312
+ true,
1313
+ {
1314
+ selectedFields: [],
1315
+ createResultObject,
1316
+ shouldProcessStepLevel,
1317
+ }
1318
+ );
1319
+
1320
+ expect(res).toEqual([{ ok: true }]);
1321
+ expect(createResultObject).toHaveBeenCalled();
1322
+ });
1323
+
1324
+ it('should skip items without iterations when includeItemsWithNoIterations=false', () => {
1325
+ const testData = [
1326
+ {
1327
+ testPointsItems: [{ testCaseId: 123, lastRunId: undefined, lastResultId: undefined }],
1328
+ testCasesItems: [
1329
+ { workItem: { id: 123, workItemFields: [{ key: 'Steps', value: '<steps></steps>' }] } },
1330
+ ],
1331
+ },
1332
+ ];
1333
+ const iterations = [
1334
+ { testCaseId: 123, lastRunId: undefined, lastResultId: undefined, iteration: null },
1335
+ ];
1336
+
1337
+ const res = (resultDataProvider as any).alignStepsWithIterationsBase(
1338
+ testData,
1339
+ iterations,
1340
+ true,
1341
+ false,
1342
+ true,
1343
+ {
1344
+ selectedFields: [],
1345
+ createResultObject: jest.fn().mockReturnValue({ ok: true }),
1346
+ shouldProcessStepLevel: jest.fn().mockReturnValue(false),
1347
+ }
1348
+ );
1349
+
1350
+ expect(res).toEqual([]);
1351
+ });
1352
+
1353
+ it('should fallback to test-level result when step-level processing enabled but actionResults is empty', () => {
1354
+ const testData = [
1355
+ {
1356
+ testPointsItems: [{ testCaseId: 123, lastRunId: '10', lastResultId: '20' }],
1357
+ testCasesItems: [
1358
+ { workItem: { id: 123, workItemFields: [{ key: 'Steps', value: '<steps></steps>' }] } },
1359
+ ],
1360
+ },
1361
+ ];
1362
+ const iterations = [
1363
+ {
1364
+ testCaseId: 123,
1365
+ lastRunId: '10',
1366
+ lastResultId: '20',
1367
+ iteration: { actionResults: [] },
1368
+ },
1369
+ ];
1370
+
1371
+ const createResultObject = jest.fn().mockReturnValue({ mode: 'test-level' });
1372
+ const shouldProcessStepLevel = jest.fn().mockReturnValue(true);
1373
+
1374
+ const res = (resultDataProvider as any).alignStepsWithIterationsBase(
1375
+ testData,
1376
+ iterations,
1377
+ false,
1378
+ true,
1379
+ false,
1380
+ {
1381
+ selectedFields: ['includeSteps@stepsRunProperties'],
1382
+ createResultObject,
1383
+ shouldProcessStepLevel,
1384
+ }
1385
+ );
1386
+
1387
+ expect(res).toEqual([{ mode: 'test-level' }]);
1388
+ expect(createResultObject).toHaveBeenCalledTimes(1);
1389
+ });
1390
+ });
1391
+
1392
+ describe('getCombinedResultsSummary - optional outputs', () => {
1393
+ it('should include open PCRs, test log, appendix-a and appendix-b when enabled', async () => {
1394
+ jest
1395
+ .spyOn(resultDataProvider as any, 'fetchTestSuites')
1396
+ .mockResolvedValueOnce([{ testSuiteId: '1', testGroupName: 'Group 1' }]);
1397
+
1398
+ jest.spyOn(resultDataProvider as any, 'fetchTestPoints').mockResolvedValueOnce([
1399
+ {
1400
+ testCaseId: 1,
1401
+ testCaseName: 'TC 1',
1402
+ testCaseUrl: 'http://example.com/1',
1403
+ configurationName: 'Cfg',
1404
+ outcome: 'passed',
1405
+ lastRunId: 10,
1406
+ lastResultId: 20,
1407
+ lastResultDetails: { dateCompleted: '2023-01-01T00:00:00.000Z', runBy: { displayName: 'User 1' } },
1408
+ },
1409
+ ]);
1410
+
1411
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([]);
1412
+
1413
+ // runResults for appendix-a filtering
1414
+ jest.spyOn(resultDataProvider as any, 'fetchAllResultData').mockResolvedValueOnce([
1415
+ {
1416
+ comment: 'has comment',
1417
+ iteration: { attachments: [] },
1418
+ analysisAttachments: [],
1419
+ lastRunId: 1,
1420
+ lastResultId: 2,
1421
+ },
1422
+ ]);
1423
+
1424
+ jest.spyOn(resultDataProvider as any, 'alignStepsWithIterations').mockReturnValueOnce([]);
1425
+
1426
+ const openSpy = jest
1427
+ .spyOn(resultDataProvider as any, 'fetchOpenPcrData')
1428
+ .mockResolvedValueOnce(undefined);
1429
+
1430
+ // Ensure appendix-b mapping runs but doesn't require attachments
1431
+ jest
1432
+ .spyOn(resultDataProvider as any, 'mapStepResultsForExecutionAppendix')
1433
+ .mockReturnValueOnce(new Map());
1434
+
1435
+ const res = await resultDataProvider.getCombinedResultsSummary(
1436
+ mockTestPlanId,
1437
+ mockProjectName,
1438
+ undefined,
1439
+ false,
1440
+ false,
1441
+ { openPcrMode: 'linked', openPcrLinkedQuery: { wiql: { href: 'x' }, columns: [] } } as any,
1442
+ true,
1443
+ { isEnabled: true, generateAttachments: { isEnabled: true, runAttachmentMode: 'planOnly' } },
1444
+ { isEnabled: true, generateRunAttachments: { isEnabled: false } },
1445
+ false
1446
+ );
1447
+
1448
+ expect(openSpy).toHaveBeenCalled();
1449
+
1450
+ const hasTestLog = res.combinedResults.some(
1451
+ (x: any) => x.contentControl === 'test-execution-content-control'
1452
+ );
1453
+ expect(hasTestLog).toBe(true);
1454
+
1455
+ const hasAppendixA = res.combinedResults.some(
1456
+ (x: any) => x.contentControl === 'appendix-a-content-control'
1457
+ );
1458
+ expect(hasAppendixA).toBe(true);
1459
+
1460
+ const hasAppendixB = res.combinedResults.some(
1461
+ (x: any) => x.contentControl === 'appendix-b-content-control'
1462
+ );
1463
+ expect(hasAppendixB).toBe(true);
1464
+ });
1465
+ });
1466
+
1467
+ describe('appendLinkedRelations', () => {
1468
+ it('should append requirement/bug/cr when enabled and skip closed items', async () => {
1469
+ const relations = [
1470
+ { rel: 'System.LinkTypes.Related', url: 'https://example.com/wi/1' },
1471
+ { rel: 'System.LinkTypes.Related', url: 'https://example.com/wi/2' },
1472
+ { rel: 'System.LinkTypes.Related', url: 'https://example.com/wi/3' },
1473
+ { rel: 'System.LinkTypes.Related', url: 'https://example.com/wi/4' },
1474
+ ];
1475
+ const relatedRequirements: any[] = [];
1476
+ const relatedBugs: any[] = [];
1477
+ const relatedCRs: any[] = [];
1478
+ const selected = new Set(['associatedRequirement', 'associatedBug', 'associatedCR']);
1479
+
1480
+ (TFSServices.getItemContent as jest.Mock)
1481
+ .mockResolvedValueOnce({
1482
+ id: 1,
1483
+ fields: {
1484
+ 'System.WorkItemType': 'Requirement',
1485
+ 'System.State': 'Active',
1486
+ 'System.Title': 'Req 1',
1487
+ 'Custom.CustomerRequirementId': 'CUST-1',
1488
+ },
1489
+ _links: { html: { href: 'http://example.com/1' } },
1490
+ })
1491
+ .mockResolvedValueOnce({
1492
+ id: 2,
1493
+ fields: {
1494
+ 'System.WorkItemType': 'Bug',
1495
+ 'System.State': 'Active',
1496
+ 'System.Title': 'Bug 2',
1497
+ },
1498
+ _links: { html: { href: 'http://example.com/2' } },
1499
+ })
1500
+ .mockResolvedValueOnce({
1501
+ id: 3,
1502
+ fields: {
1503
+ 'System.WorkItemType': 'Change Request',
1504
+ 'System.State': 'Active',
1505
+ 'System.Title': 'CR 3',
1506
+ },
1507
+ _links: { html: { href: 'http://example.com/3' } },
1508
+ })
1509
+ .mockResolvedValueOnce({
1510
+ id: 4,
1511
+ fields: {
1512
+ 'System.WorkItemType': 'Bug',
1513
+ 'System.State': 'Closed',
1514
+ 'System.Title': 'Closed bug',
1515
+ },
1516
+ _links: { html: { href: 'http://example.com/4' } },
1517
+ });
1518
+
1519
+ await (resultDataProvider as any).appendLinkedRelations(
1520
+ relations,
1521
+ relatedRequirements,
1522
+ relatedBugs,
1523
+ relatedCRs,
1524
+ { id: 123 },
1525
+ selected
1526
+ );
1527
+
1528
+ expect(relatedRequirements).toHaveLength(1);
1529
+ expect(relatedRequirements[0]).toEqual(
1530
+ expect.objectContaining({ id: 1, customerId: 'CUST-1', workItemType: 'Requirement' })
1531
+ );
1532
+ expect(relatedBugs).toHaveLength(1);
1533
+ expect(relatedCRs).toHaveLength(1);
1534
+ });
1535
+
1536
+ it('should log an error when fetching a related item fails', async () => {
1537
+ const relations = [{ rel: 'System.LinkTypes.Related', url: 'https://example.com/wi/1' }];
1538
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(new Error('network'));
1539
+
1540
+ await (resultDataProvider as any).appendLinkedRelations(
1541
+ relations,
1542
+ [],
1543
+ [],
1544
+ [],
1545
+ { id: 999 },
1546
+ new Set(['associatedBug'])
1547
+ );
1548
+
1549
+ expect(logger.error).toHaveBeenCalledWith(
1550
+ expect.stringContaining('Could not append related work item to test case 999')
1551
+ );
1552
+ });
1553
+ });
1554
+
1555
+ describe('appendQueryRelations', () => {
1556
+ it('should append query relations when map has items', () => {
1557
+ // Arrange
1558
+ const testCaseId = 1;
1559
+ const relatedRequirements: any[] = [];
1560
+ const relatedBugs: any[] = [];
1561
+ const relatedCRs: any[] = [];
1562
+
1563
+ // Set up the map
1564
+ (resultDataProvider as any).testToAssociatedItemMap = new Map([
1565
+ [
1566
+ 1,
1567
+ [
1568
+ {
1569
+ id: 100,
1570
+ fields: { 'System.Title': 'Req 1', 'System.WorkItemType': 'Requirement' },
1571
+ _links: { html: { href: 'http://example.com/100' } },
1572
+ },
1573
+ ],
1574
+ ],
1575
+ ]);
1576
+ (resultDataProvider as any).querySelectedColumns = [];
1577
+
1578
+ // Act
1579
+ (resultDataProvider as any).appendQueryRelations(
1580
+ testCaseId,
1581
+ relatedRequirements,
1582
+ relatedBugs,
1583
+ relatedCRs
1584
+ );
1585
+
1586
+ // Assert
1587
+ expect(relatedRequirements).toHaveLength(1);
1588
+ expect(relatedRequirements[0].id).toBe(100);
1589
+ });
1590
+
1591
+ it('should map Requirement/Bug/Change Request into correct buckets', () => {
1592
+ const testCaseId = 1;
1593
+ const relatedRequirements: any[] = [];
1594
+ const relatedBugs: any[] = [];
1595
+ const relatedCRs: any[] = [];
1596
+
1597
+ jest.spyOn(resultDataProvider as any, 'standardCustomField').mockReturnValue({});
1598
+
1599
+ (resultDataProvider as any).testToAssociatedItemMap = new Map([
1600
+ [
1601
+ 1,
1602
+ [
1603
+ {
1604
+ id: 10,
1605
+ fields: { 'System.Title': 'R', 'System.WorkItemType': 'Requirement', X: '1' },
1606
+ _links: { html: { href: 'http://example.com/10' } },
1607
+ },
1608
+ {
1609
+ id: 11,
1610
+ fields: { 'System.Title': 'B', 'System.WorkItemType': 'Bug', Y: '2' },
1611
+ _links: { html: { href: 'http://example.com/11' } },
1612
+ },
1613
+ {
1614
+ id: 12,
1615
+ fields: { 'System.Title': 'C', 'System.WorkItemType': 'Change Request', Z: '3' },
1616
+ _links: { html: { href: 'http://example.com/12' } },
1617
+ },
1618
+ ],
1619
+ ],
1620
+ ]);
1621
+ (resultDataProvider as any).querySelectedColumns = [];
1622
+
1623
+ (resultDataProvider as any).appendQueryRelations(
1624
+ testCaseId,
1625
+ relatedRequirements,
1626
+ relatedBugs,
1627
+ relatedCRs
1628
+ );
1629
+
1630
+ expect(relatedRequirements).toHaveLength(1);
1631
+ expect(relatedBugs).toHaveLength(1);
1632
+ expect(relatedCRs).toHaveLength(1);
1633
+ });
1634
+
1635
+ it('should handle empty map', () => {
1636
+ // Arrange
1637
+ const testCaseId = 1;
1638
+ const relatedRequirements: any[] = [];
1639
+ const relatedBugs: any[] = [];
1640
+ const relatedCRs: any[] = [];
1641
+ (resultDataProvider as any).testToAssociatedItemMap = new Map();
1642
+
1643
+ // Act
1644
+ (resultDataProvider as any).appendQueryRelations(
1645
+ testCaseId,
1646
+ relatedRequirements,
1647
+ relatedBugs,
1648
+ relatedCRs
1649
+ );
1650
+
1651
+ // Assert
1652
+ expect(relatedRequirements).toHaveLength(0);
1653
+ });
1654
+ });
1655
+
1656
+ describe('convertUnspecifiedRunStatus', () => {
1657
+ it('should return empty string for null actionResult', () => {
1658
+ // Act
1659
+ const result = (resultDataProvider as any).convertUnspecifiedRunStatus(null);
1660
+
1661
+ // Assert
1662
+ expect(result).toBe('');
1663
+ });
1664
+
1665
+ it('should return empty string for Unspecified shared step title', () => {
1666
+ // Arrange
1667
+ const actionResult = { outcome: 'Unspecified', isSharedStepTitle: true };
1668
+
1669
+ // Act
1670
+ const result = (resultDataProvider as any).convertUnspecifiedRunStatus(actionResult);
1671
+
1672
+ // Assert
1673
+ expect(result).toBe('');
1674
+ });
1675
+
1676
+ it('should return Not Run for Unspecified non-shared step', () => {
1677
+ // Arrange
1678
+ const actionResult = { outcome: 'Unspecified', isSharedStepTitle: false };
1679
+
1680
+ // Act
1681
+ const result = (resultDataProvider as any).convertUnspecifiedRunStatus(actionResult);
1682
+
1683
+ // Assert
1684
+ expect(result).toBe('Not Run');
1685
+ });
1686
+
1687
+ it('should return original outcome for non-Unspecified status', () => {
1688
+ // Arrange
1689
+ const actionResult = { outcome: 'Passed', isSharedStepTitle: false };
1690
+
1691
+ // Act
1692
+ const result = (resultDataProvider as any).convertUnspecifiedRunStatus(actionResult);
1693
+
1694
+ // Assert
1695
+ expect(result).toBe('Passed');
1696
+ });
1697
+ });
1698
+
1699
+ describe('fetchResultDataBasedOnWi', () => {
1700
+ it('should call fetchResultDataBasedOnWiBase', async () => {
1701
+ // Arrange
1702
+ const mockResult = { id: 1, outcome: 'passed' };
1703
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockResult);
1704
+
1705
+ // Act - just verify it doesn't throw
1706
+ const spy = jest.spyOn(resultDataProvider as any, 'fetchResultDataBasedOnWiBase');
1707
+ try {
1708
+ await (resultDataProvider as any).fetchResultDataBasedOnWi(mockProjectName, '100', '200');
1709
+ } catch {
1710
+ // Expected to fail due to missing mocks
1711
+ }
1712
+
1713
+ // Assert
1714
+ expect(spy).toHaveBeenCalledWith(mockProjectName, '100', '200');
1715
+ });
1716
+ });
1717
+
1718
+ describe('alignStepsWithIterationsBase', () => {
1719
+ it('should return empty array when no iterations', () => {
1720
+ // Arrange
1721
+ const testData: any[] = [];
1722
+ const iterations: any[] = [];
1723
+ const options = {
1724
+ createResultObject: jest.fn(),
1725
+ shouldProcessStepLevel: jest.fn(),
1726
+ };
1727
+
1728
+ // Act
1729
+ const result = (resultDataProvider as any).alignStepsWithIterationsBase(
1730
+ testData,
1731
+ iterations,
1732
+ false,
1733
+ false,
1734
+ false,
1735
+ options
1736
+ );
1737
+
1738
+ // Assert
1739
+ expect(result).toEqual([]);
1740
+ });
1741
+
1742
+ it('should return [null] when fetchedTestCase.iteration.actionResults is null (shouldProcessStepLevel=false)', () => {
1743
+ const testData = [
1744
+ {
1745
+ testGroupName: 'G',
1746
+ testPointsItems: [{ testCaseId: 1, testCaseName: 'TC', lastRunId: 10, lastResultId: 20 }],
1747
+ testCasesItems: [{ workItem: { id: 1, workItemFields: [{ key: 'Steps', value: '<steps />' }] } }],
1748
+ },
1749
+ ];
1750
+ const iterations = [
1751
+ {
1752
+ testCaseId: 1,
1753
+ lastRunId: 10,
1754
+ lastResultId: 20,
1755
+ iteration: { actionResults: null },
1756
+ testCaseRevision: 1,
1757
+ },
1758
+ ];
1759
+
1760
+ const res = (resultDataProvider as any).alignStepsWithIterations(testData, iterations);
1761
+ expect(res).toEqual([null]);
1762
+ });
1763
+
1764
+ it('should include a null row when actionResults contains an undefined element', () => {
1765
+ const testData = [
1766
+ {
1767
+ testGroupName: 'G',
1768
+ testPointsItems: [{ testCaseId: 1, testCaseName: 'TC', lastRunId: 10, lastResultId: 20 }],
1769
+ testCasesItems: [{ workItem: { id: 1, workItemFields: [{ key: 'Steps', value: '<steps />' }] } }],
1770
+ },
1771
+ ];
1772
+ const iterations = [
1773
+ {
1774
+ testCaseId: 1,
1775
+ lastRunId: 10,
1776
+ lastResultId: 20,
1777
+ iteration: { actionResults: [undefined] },
1778
+ testCaseRevision: 1,
1779
+ },
1780
+ ];
1781
+
1782
+ const res = (resultDataProvider as any).alignStepsWithIterations(testData, iterations);
1783
+ expect(res).toEqual([null]);
1784
+ });
1785
+ });
1786
+
1787
+ describe('standardCustomField', () => {
1788
+ it('should standardize custom fields with columns', () => {
1789
+ // Arrange
1790
+ const fields = { 'Custom.Field1': 'value1', 'Custom.Field2': 'value2' };
1791
+ const columns = [
1792
+ { referenceName: 'Custom.Field1', name: 'Field 1' },
1793
+ { referenceName: 'Custom.Field2', name: 'Field 2' },
1794
+ ];
1795
+
1796
+ // Act
1797
+ const result = (resultDataProvider as any).standardCustomField(fields, columns);
1798
+
1799
+ // Assert
1800
+ expect(result).toBeDefined();
1801
+ expect(result.field1).toBe('value1');
1802
+ });
1803
+
1804
+ it('should handle uppercase field names', () => {
1805
+ // Arrange
1806
+ const fields = { 'Custom.ABC': 'value1' };
1807
+ const columns = [{ referenceName: 'Custom.ABC', name: 'ABC' }];
1808
+
1809
+ // Act
1810
+ const result = (resultDataProvider as any).standardCustomField(fields, columns);
1811
+
1812
+ // Assert
1813
+ expect(result.abc).toBe('value1');
1814
+ });
1815
+
1816
+ it('should skip standard fields', () => {
1817
+ // Arrange
1818
+ const fields = { 'System.Id': 1, 'Custom.Field1': 'value1' };
1819
+ const columns = [
1820
+ { referenceName: 'System.Id', name: 'id' },
1821
+ { referenceName: 'Custom.Field1', name: 'Field 1' },
1822
+ ];
1823
+
1824
+ // Act
1825
+ const result = (resultDataProvider as any).standardCustomField(fields, columns);
1826
+
1827
+ // Assert
1828
+ expect(result.id).toBeUndefined();
1829
+ expect(result.field1).toBe('value1');
1830
+ });
1831
+
1832
+ it('should handle null/undefined field values', () => {
1833
+ // Arrange
1834
+ const fields = { 'Custom.Field1': null };
1835
+ const columns = [{ referenceName: 'Custom.Field1', name: 'Field 1' }];
1836
+
1837
+ // Act
1838
+ const result = (resultDataProvider as any).standardCustomField(fields, columns);
1839
+
1840
+ // Assert
1841
+ expect(result.field1).toBeNull();
1842
+ });
1843
+
1844
+ it('should handle fields without columns', () => {
1845
+ // Arrange
1846
+ const fields = { 'Custom.Field1': 'value1', 'System.Title': 'Title' };
1847
+
1848
+ // Act
1849
+ const result = (resultDataProvider as any).standardCustomField(fields);
1850
+
1851
+ // Assert
1852
+ expect(result).toBeDefined();
1853
+ });
1854
+
1855
+ it('should handle displayName property', () => {
1856
+ // Arrange
1857
+ const fields = { 'Custom.Field1': { displayName: 'Display Value' } };
1858
+ const columns = [{ referenceName: 'Custom.Field1', name: 'Field 1' }];
1859
+
1860
+ // Act
1861
+ const result = (resultDataProvider as any).standardCustomField(fields, columns);
1862
+
1863
+ // Assert
1864
+ expect(result.field1).toBe('Display Value');
1865
+ });
1866
+ });
1867
+
1868
+ describe('getTestOutcome', () => {
1869
+ it('should return outcome from last iteration', () => {
1870
+ // Arrange
1871
+ const resultData = {
1872
+ iterationDetails: [{ outcome: 'Failed' }, { outcome: 'Passed' }],
1873
+ outcome: 'Failed',
1874
+ };
1875
+
1876
+ // Act
1877
+ const result = (resultDataProvider as any).getTestOutcome(resultData);
1878
+
1879
+ // Assert
1880
+ expect(result).toBe('Passed');
1881
+ });
1882
+
1883
+ it('should return result outcome when no iteration details', () => {
1884
+ // Arrange
1885
+ const resultData = { outcome: 'Passed' };
1886
+
1887
+ // Act
1888
+ const result = (resultDataProvider as any).getTestOutcome(resultData);
1889
+
1890
+ // Assert
1891
+ expect(result).toBe('Passed');
1892
+ });
1893
+
1894
+ it('should return default outcome when no data', () => {
1895
+ // Arrange
1896
+ const resultData = {};
1897
+
1898
+ // Act
1899
+ const result = (resultDataProvider as any).getTestOutcome(resultData);
1900
+
1901
+ // Assert
1902
+ expect(result).toBe('NotApplicable');
1903
+ });
1904
+ });
1905
+
1906
+ describe('createIterationsMap', () => {
1907
+ it('should create iterations map from results', () => {
1908
+ // Arrange
1909
+ const iterations = [{ lastRunId: 100, lastResultId: 200, testCase: { id: 1 } }];
1910
+
1911
+ // Act
1912
+ const result = (resultDataProvider as any).createIterationsMap(iterations, false, false);
1913
+
1914
+ // Assert
1915
+ expect(result).toBeDefined();
1916
+ });
1917
+
1918
+ it('should create iterations map from results with iteration', () => {
1919
+ // Arrange
1920
+ const iterations = [{ lastRunId: 100, lastResultId: 200, testCaseId: 1, iteration: { id: 1 } }];
1921
+
1922
+ // Act
1923
+ const result = (resultDataProvider as any).createIterationsMap(iterations, false, false);
1924
+
1925
+ // Assert
1926
+ expect(result).toBeDefined();
1927
+ expect(result['100-200-1']).toBeDefined();
1928
+ });
1929
+
1930
+ it('should create iterations map for test reporter mode', () => {
1931
+ // Arrange
1932
+ const iterations = [{ lastRunId: 100, lastResultId: 200, testCaseId: 1 }];
1933
+
1934
+ // Act
1935
+ const result = (resultDataProvider as any).createIterationsMap(iterations, true, false);
1936
+
1937
+ // Assert
1938
+ expect(result['100-200-1']).toBeDefined();
1939
+ });
1940
+
1941
+ it('should include not run test cases when flag is set', () => {
1942
+ // Arrange
1943
+ const iterations = [{ testCaseId: 1 }];
1944
+
1945
+ // Act
1946
+ const result = (resultDataProvider as any).createIterationsMap(iterations, false, true);
1947
+
1948
+ // Assert
1949
+ expect(result['1']).toBeDefined();
1950
+ });
1951
+ });
1952
+
1953
+ describe('alignStepsWithIterationsBase', () => {
1954
+ it('should return empty array when no iterations', () => {
1955
+ // Arrange
1956
+ const testData: any[] = [];
1957
+ const iterations: any[] = [];
1958
+ const options = {
1959
+ createResultObject: jest.fn(),
1960
+ shouldProcessStepLevel: jest.fn(),
1961
+ };
1962
+
1963
+ // Act
1964
+ const result = (resultDataProvider as any).alignStepsWithIterationsBase(
1965
+ testData,
1966
+ iterations,
1967
+ false,
1968
+ false,
1969
+ false,
1970
+ options
1971
+ );
1972
+
1973
+ // Assert
1974
+ expect(result).toEqual([]);
1975
+ });
1976
+ });
1977
+
1978
+ describe('alignStepsWithIterations', () => {
1979
+ it('should return empty array when no iterations', () => {
1980
+ // Arrange
1981
+ const testData: any[] = [];
1982
+ const iterations: any[] = [];
1983
+
1984
+ // Act
1985
+ const result = (resultDataProvider as any).alignStepsWithIterations(testData, iterations);
1986
+
1987
+ // Assert
1988
+ expect(result).toEqual([]);
1989
+ });
1990
+ });
1991
+
1992
+ describe('fetchTestData', () => {
1993
+ it('should fetch test data for suites', async () => {
1994
+ // Arrange
1995
+ const suites = [{ testSuiteId: 1, testGroupName: 'Suite 1' }];
1996
+ const mockTestCases = { value: [{ workItem: { id: 1 } }] };
1997
+ const mockTestPoints = { value: [{ testCaseReference: { id: 1 } }], count: 1 };
1998
+
1999
+ (TFSServices.getItemContent as jest.Mock)
2000
+ .mockResolvedValueOnce(mockTestCases)
2001
+ .mockResolvedValueOnce(mockTestPoints);
2002
+
2003
+ // Act
2004
+ const result = await (resultDataProvider as any).fetchTestData(
2005
+ suites,
2006
+ mockProjectName,
2007
+ mockTestPlanId,
2008
+ false
2009
+ );
2010
+
2011
+ // Assert
2012
+ expect(result).toHaveLength(1);
2013
+ expect(result[0].testCasesItems).toBeDefined();
2014
+ });
2015
+
2016
+ it('should handle errors gracefully', async () => {
2017
+ // Arrange
2018
+ const suites = [{ testSuiteId: 1, testGroupName: 'Suite 1' }];
2019
+ (TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
2020
+
2021
+ // Act
2022
+ const result = await (resultDataProvider as any).fetchTestData(
2023
+ suites,
2024
+ mockProjectName,
2025
+ mockTestPlanId,
2026
+ false
2027
+ );
2028
+
2029
+ // Assert
2030
+ expect(result).toHaveLength(1);
2031
+ expect(logger.error).toHaveBeenCalled();
2032
+ });
2033
+ });
2034
+
2035
+ describe('fetchAllResultData', () => {
2036
+ it('should return empty array when no test data', async () => {
2037
+ // Arrange
2038
+ const testData: any[] = [];
2039
+
2040
+ // Act
2041
+ const result = await (resultDataProvider as any).fetchAllResultData(testData, mockProjectName);
2042
+
2043
+ // Assert
2044
+ expect(result).toEqual([]);
2045
+ });
2046
+
2047
+ it('should fetch result data for test points', async () => {
2048
+ // Arrange
2049
+ const testData = [
2050
+ {
2051
+ testPointsItems: [{ testCaseId: 1, lastRunId: 100, lastResultId: 200 }],
2052
+ },
2053
+ ];
2054
+ const mockResult = {
2055
+ testCase: { id: 1 },
2056
+ iteration: { actionResults: [] },
2057
+ };
2058
+ const mockAttachments = { value: [] };
2059
+ const mockWi = { fields: {} };
2060
+
2061
+ (TFSServices.getItemContent as jest.Mock)
2062
+ .mockResolvedValueOnce(mockResult)
2063
+ .mockResolvedValueOnce(mockAttachments)
2064
+ .mockResolvedValueOnce(mockWi);
2065
+
2066
+ // Act
2067
+ const result = await (resultDataProvider as any).fetchAllResultData(testData, mockProjectName);
2068
+
2069
+ // Assert
2070
+ expect(result).toBeDefined();
2071
+ });
2072
+
2073
+ it('should log response data and rethrow when an error with response is thrown', async () => {
2074
+ const err: any = new Error('boom');
2075
+ err.response = { data: { detail: 'bad' } };
2076
+
2077
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockRejectedValueOnce(err);
2078
+
2079
+ await expect(
2080
+ resultDataProvider.getCombinedResultsSummary(mockTestPlanId, mockProjectName)
2081
+ ).rejects.toThrow('boom');
2082
+
2083
+ expect(logger.error).toHaveBeenCalledWith('Error during getCombinedResultsSummary: boom');
2084
+ expect(logger.error).toHaveBeenCalledWith('Response Data: {"detail":"bad"}');
2085
+ });
2086
+ });
2087
+
2088
+ describe('fetchAllResultDataTestReporter', () => {
2089
+ it('should return empty array when no test data', async () => {
2090
+ // Arrange
2091
+ const testData: any[] = [];
2092
+
2093
+ // Act
2094
+ const result = await (resultDataProvider as any).fetchAllResultDataTestReporter(
2095
+ testData,
2096
+ mockProjectName,
2097
+ [],
2098
+ false
2099
+ );
2100
+
2101
+ // Assert
2102
+ expect(result).toEqual([]);
2103
+ });
2104
+ });
2105
+
2106
+ describe('alignStepsWithIterationsTestReporter', () => {
2107
+ it('should return empty array when no iterations', () => {
2108
+ // Arrange
2109
+ const testData: any[] = [];
2110
+ const iterations: any[] = [];
2111
+
2112
+ // Act
2113
+ const result = (resultDataProvider as any).alignStepsWithIterationsTestReporter(
2114
+ testData,
2115
+ iterations,
2116
+ [],
2117
+ false
2118
+ );
2119
+
2120
+ // Assert
2121
+ expect(result).toEqual([]);
2122
+ });
2123
+ });
2124
+
2125
+ describe('fetchAllResultDataBase', () => {
2126
+ it('should return empty array when no test data', async () => {
2127
+ // Arrange
2128
+ const testData: any[] = [];
2129
+ const fetchStrategy = jest.fn();
2130
+
2131
+ // Act
2132
+ const result = await (resultDataProvider as any).fetchAllResultDataBase(
2133
+ testData,
2134
+ mockProjectName,
2135
+ false,
2136
+ fetchStrategy
2137
+ );
2138
+
2139
+ // Assert
2140
+ expect(result).toEqual([]);
2141
+ });
2142
+
2143
+ it('should filter out points without run/result IDs when not test reporter', async () => {
2144
+ // Arrange
2145
+ const testData = [
2146
+ {
2147
+ testSuiteId: 1,
2148
+ testPointsItems: [
2149
+ { testCaseId: 1 }, // No lastRunId/lastResultId
2150
+ ],
2151
+ },
2152
+ ];
2153
+ const fetchStrategy = jest.fn();
2154
+
2155
+ // Act
2156
+ const result = await (resultDataProvider as any).fetchAllResultDataBase(
2157
+ testData,
2158
+ mockProjectName,
2159
+ false,
2160
+ fetchStrategy
2161
+ );
2162
+
2163
+ // Assert
2164
+ expect(fetchStrategy).not.toHaveBeenCalled();
2165
+ expect(result).toEqual([]);
2166
+ });
2167
+ });
2168
+
2169
+ describe('fetchResultDataBase', () => {
2170
+ it('should fetch result data for a point', async () => {
2171
+ // Arrange
2172
+ const point = { lastRunId: 100, lastResultId: 200 };
2173
+ const mockResultData = {
2174
+ testCase: { id: 1 },
2175
+ iterationDetails: [],
2176
+ };
2177
+ const fetchResultMethod = jest.fn().mockResolvedValue(mockResultData);
2178
+ const createResponseObject = jest.fn().mockReturnValue({ id: 1 });
2179
+
2180
+ // Act
2181
+ const result = await (resultDataProvider as any).fetchResultDataBase(
2182
+ mockProjectName,
2183
+ '1',
2184
+ point,
2185
+ fetchResultMethod,
2186
+ createResponseObject
2187
+ );
2188
+
2189
+ // Assert
2190
+ expect(fetchResultMethod).toHaveBeenCalled();
2191
+ expect(result).toBeDefined();
2192
+ });
2193
+ });
2194
+
2195
+ describe('getCombinedResultsSummary', () => {
2196
+ it('should return combined results summary with expected content controls', async () => {
2197
+ const mockTestSuites = {
2198
+ value: [
2199
+ {
2200
+ id: 1,
2201
+ name: 'Root Suite',
2202
+ children: [{ id: 2, name: 'Child Suite 1', parentSuite: { id: 1 } }],
2203
+ },
2204
+ ],
2205
+ count: 1,
2206
+ };
2207
+
2208
+ const mockTestPoints = {
2209
+ value: [
2210
+ {
2211
+ testCaseReference: { id: 1, name: 'Test Case 1' },
2212
+ configuration: { name: 'Config 1' },
2213
+ results: {
2214
+ outcome: 'passed',
2215
+ lastTestRunId: 100,
2216
+ lastResultId: 200,
2217
+ lastResultDetails: { dateCompleted: '2023-01-01', runBy: { displayName: 'Test User' } },
2218
+ },
2219
+ },
2220
+ ],
2221
+ count: 1,
2222
+ };
2223
+
2224
+ const mockTestCases = {
2225
+ value: [
2226
+ {
2227
+ workItem: {
2228
+ id: 1,
2229
+ workItemFields: [{ key: 'Steps', value: '<steps>...</steps>' }],
2230
+ },
2231
+ },
2232
+ ],
2233
+ };
2234
+
2235
+ const mockResult = {
2236
+ testCase: { id: 1, name: 'Test Case 1' },
2237
+ testSuite: { id: 2, name: 'Child Suite 1' },
2238
+ iterationDetails: [
2239
+ {
2240
+ actionResults: [
2241
+ { stepIdentifier: '1', outcome: 'Passed', errorMessage: '', actionPath: 'path1' },
2242
+ ],
2243
+ attachments: [],
2244
+ },
2245
+ ],
2246
+ testCaseRevision: 1,
2247
+ failureType: null,
2248
+ resolutionState: null,
2249
+ comment: null,
2250
+ };
2251
+
2252
+ // Setup mocks for API calls
2253
+ (TFSServices.getItemContent as jest.Mock)
2254
+ .mockResolvedValueOnce(mockTestSuites) // fetchTestSuites
2255
+ .mockResolvedValueOnce(mockTestPoints) // fetchTestPoints
2256
+ .mockResolvedValueOnce(mockTestCases) // fetchTestCasesBySuiteId
2257
+ .mockResolvedValueOnce(mockResult) // fetchResult
2258
+ .mockResolvedValueOnce({ value: [] }) // fetchResult - attachments
2259
+ .mockResolvedValueOnce({ fields: {} }); // fetchResult - wiByRevision
2260
+
2261
+ const mockTestStepParserHelper = (resultDataProvider as any).testStepParserHelper;
2262
+ mockTestStepParserHelper.parseTestSteps.mockResolvedValueOnce([
2263
+ {
2264
+ stepId: 1,
2265
+ stepPosition: '1',
2266
+ action: 'Do something',
2267
+ expected: 'Something happens',
2268
+ isSharedStepTitle: false,
2269
+ },
2270
+ ]);
2271
+
2272
+ // Act
2273
+ const result = await resultDataProvider.getCombinedResultsSummary(
2274
+ mockTestPlanId,
2275
+ mockProjectName,
2276
+ undefined,
2277
+ true
2278
+ );
2279
+
2280
+ // Assert
2281
+ expect(result.combinedResults.length).toBeGreaterThan(0);
2282
+ expect(result.combinedResults[0]).toHaveProperty(
2283
+ 'contentControl',
2284
+ 'test-group-summary-content-control'
2285
+ );
2286
+ expect(result.combinedResults[1]).toHaveProperty(
2287
+ 'contentControl',
2288
+ 'test-result-summary-content-control'
2289
+ );
2290
+ expect(result.combinedResults[2]).toHaveProperty(
2291
+ 'contentControl',
2292
+ 'detailed-test-result-content-control'
2293
+ );
2294
+ });
2295
+ });
2296
+
2297
+ describe('fetchResultDataBase - shared step mapping', () => {
2298
+ it('should map parsed steps into actionResults, filter missing stepPosition, and sort by stepPosition', async () => {
2299
+ const point = { testCaseId: 1, lastRunId: 10, lastResultId: 20 };
2300
+ const fetchResultMethod = jest.fn().mockResolvedValue({
2301
+ testCase: { id: 1, name: 'TC' },
2302
+ stepsResultXml: '<steps></steps>',
2303
+ iterationDetails: [
2304
+ {
2305
+ actionResults: [
2306
+ { stepIdentifier: '2', actionPath: 'p2', sharedStepModel: { id: 5, revision: 7 } },
2307
+ { stepIdentifier: '999', actionPath: 'px' },
2308
+ { stepIdentifier: '1', actionPath: 'p1' },
2309
+ ],
2310
+ },
2311
+ ],
2312
+ });
2313
+
2314
+ const createResponseObject = (resultData: any) => ({ iteration: resultData.iterationDetails[0] });
2315
+
2316
+ const helper = (resultDataProvider as any).testStepParserHelper;
2317
+ helper.parseTestSteps.mockImplementationOnce(async (_xml: any, map: Map<number, number>) => {
2318
+ // cover sharedStepIdToRevisionLookupMap population
2319
+ expect(map.get(5)).toBe(7);
2320
+ return [
2321
+ { stepId: 1, stepPosition: '1', action: 'A1', expected: 'E1', isSharedStepTitle: false },
2322
+ { stepId: 2, stepPosition: '2', action: 'A2', expected: 'E2', isSharedStepTitle: true },
2323
+ ];
2324
+ });
2325
+
2326
+ const res = await (resultDataProvider as any).fetchResultDataBase(
2327
+ mockProjectName,
2328
+ 'suite1',
2329
+ point,
2330
+ fetchResultMethod,
2331
+ createResponseObject,
2332
+ []
2333
+ );
2334
+
2335
+ const actionResults = res.iteration.actionResults;
2336
+ // 999 should be filtered out (no stepPosition)
2337
+ expect(actionResults).toHaveLength(2);
2338
+ // sorted by stepPosition numeric
2339
+ expect(actionResults[0]).toEqual(expect.objectContaining({ stepIdentifier: '1', action: 'A1' }));
2340
+ expect(actionResults[1]).toEqual(
2341
+ expect.objectContaining({ stepIdentifier: '2', action: 'A2', isSharedStepTitle: true })
2342
+ );
2343
+ });
2344
+ });
2345
+
2346
+ describe('getCombinedResultsSummary - appendix branches', () => {
2347
+ it('should use mapAttachmentsUrl when stepAnalysis.generateRunAttachments is enabled and stepExecution.runAttachmentMode != planOnly', async () => {
2348
+ jest
2349
+ .spyOn(resultDataProvider as any, 'fetchTestSuites')
2350
+ .mockResolvedValueOnce([{ testSuiteId: '1', testGroupName: 'Group 1' }]);
2351
+
2352
+ jest.spyOn(resultDataProvider as any, 'fetchTestPoints').mockResolvedValueOnce([
2353
+ {
2354
+ testCaseId: 1,
2355
+ testCaseName: 'TC 1',
2356
+ testCaseUrl: 'http://example.com/1',
2357
+ configurationName: 'Cfg',
2358
+ outcome: 'passed',
2359
+ lastRunId: 10,
2360
+ lastResultId: 20,
2361
+ lastResultDetails: { dateCompleted: '2023-01-01T00:00:00.000Z', runBy: { displayName: 'User 1' } },
2362
+ },
2363
+ ]);
2364
+
2365
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([]);
2366
+
2367
+ // ensure all predicates in stepAnalysis/filter can be true (comment false, attachments true, analysisAttachments true)
2368
+ const runResults = [
2369
+ {
2370
+ comment: '',
2371
+ iteration: { attachments: [{ actionPath: 'p', name: 'n', downloadUrl: 'd' }] },
2372
+ analysisAttachments: [{ id: 1 }],
2373
+ },
2374
+ ];
2375
+ jest.spyOn(resultDataProvider as any, 'fetchAllResultData').mockResolvedValueOnce(runResults);
2376
+ jest.spyOn(resultDataProvider as any, 'alignStepsWithIterations').mockReturnValueOnce([]);
2377
+
2378
+ const mapSpy = jest
2379
+ .spyOn(resultDataProvider as any, 'mapAttachmentsUrl')
2380
+ .mockReturnValueOnce(runResults as any)
2381
+ .mockReturnValueOnce(runResults as any);
2382
+
2383
+ jest
2384
+ .spyOn(resultDataProvider as any, 'mapStepResultsForExecutionAppendix')
2385
+ .mockReturnValueOnce(new Map());
2386
+
2387
+ const res = await resultDataProvider.getCombinedResultsSummary(
2388
+ mockTestPlanId,
2389
+ mockProjectName,
2390
+ undefined,
2391
+ false,
2392
+ false,
2393
+ null,
2394
+ false,
2395
+ { isEnabled: true, generateAttachments: { isEnabled: true, runAttachmentMode: 'runOnly' } },
2396
+ { isEnabled: true, generateRunAttachments: { isEnabled: true } },
2397
+ false
2398
+ );
2399
+
2400
+ expect(mapSpy).toHaveBeenCalled();
2401
+ expect(res.combinedResults.some((x: any) => x.contentControl === 'appendix-a-content-control')).toBe(
2402
+ true
2403
+ );
2404
+ expect(res.combinedResults.some((x: any) => x.contentControl === 'appendix-b-content-control')).toBe(
2405
+ true
2406
+ );
2407
+ });
2408
+ });
2409
+
2410
+ describe('alignStepsWithIterationsTestReporter - step-level rows', () => {
2411
+ it('should emit step-level fields when includeSteps/stepRunStatus/testStepComment are selected', () => {
2412
+ const testData = [
2413
+ {
2414
+ testGroupName: 'G',
2415
+ testPointsItems: [
2416
+ {
2417
+ testCaseId: 123,
2418
+ testCaseName: 'TC',
2419
+ testCaseUrl: 'u',
2420
+ lastRunId: 10,
2421
+ lastResultId: 20,
2422
+ },
2423
+ ],
2424
+ testCasesItems: [
2425
+ { workItem: { id: 123, workItemFields: [{ key: 'Steps', value: '<steps></steps>' }] } },
2426
+ ],
2427
+ },
2428
+ ];
2429
+ const iterations = [
2430
+ {
2431
+ testCaseId: 123,
2432
+ lastRunId: 10,
2433
+ lastResultId: 20,
2434
+ iteration: {
2435
+ actionResults: [
2436
+ {
2437
+ stepIdentifier: '1',
2438
+ stepPosition: '1',
2439
+ action: 'A',
2440
+ expected: 'E',
2441
+ outcome: 'Unspecified',
2442
+ isSharedStepTitle: false,
2443
+ errorMessage: 'err',
2444
+ },
2445
+ ],
2446
+ },
2447
+ testCaseResult: 'Failed',
2448
+ comment: 'c',
2449
+ runBy: { displayName: 'u' },
2450
+ failureType: 'ft',
2451
+ executionDate: 'd',
2452
+ configurationName: 'cfg',
2453
+ relatedRequirements: [],
2454
+ relatedBugs: [],
2455
+ relatedCRs: [],
2456
+ customFields: {},
2457
+ },
2458
+ ];
2459
+
2460
+ const res = (resultDataProvider as any).alignStepsWithIterationsTestReporter(
2461
+ testData,
2462
+ iterations,
2463
+ [
2464
+ 'includeSteps@stepsRunProperties',
2465
+ 'stepRunStatus@stepsRunProperties',
2466
+ 'testStepComment@stepsRunProperties',
2467
+ ],
2468
+ true
2469
+ );
2470
+
2471
+ expect(res).toHaveLength(1);
2472
+ expect(res[0]).toEqual(
2473
+ expect.objectContaining({
2474
+ stepNo: '1',
2475
+ stepAction: 'A',
2476
+ stepExpected: 'E',
2477
+ stepStatus: 'Not Run',
2478
+ stepComments: 'err',
2479
+ })
2480
+ );
2481
+ });
2482
+
2483
+ it('should omit step fields when no @stepsRunProperties are selected', () => {
2484
+ const testData = [
2485
+ {
2486
+ testGroupName: 'G',
2487
+ testPointsItems: [
2488
+ { testCaseId: 123, testCaseName: 'TC', testCaseUrl: 'u', lastRunId: 10, lastResultId: 20 },
2489
+ ],
2490
+ testCasesItems: [
2491
+ { workItem: { id: 123, workItemFields: [{ key: 'Steps', value: '<steps></steps>' }] } },
2492
+ ],
2493
+ },
2494
+ ];
2495
+ const iterations = [
2496
+ {
2497
+ testCaseId: 123,
2498
+ lastRunId: 10,
2499
+ lastResultId: 20,
2500
+ iteration: {
2501
+ actionResults: [{ stepIdentifier: '1', stepPosition: '1', action: 'A', expected: 'E' }],
2502
+ },
2503
+ testCaseResult: 'Passed',
2504
+ comment: '',
2505
+ runBy: { displayName: 'u' },
2506
+ failureType: '',
2507
+ executionDate: 'd',
2508
+ configurationName: 'cfg',
2509
+ relatedRequirements: [],
2510
+ relatedBugs: [],
2511
+ relatedCRs: [],
2512
+ customFields: {},
2513
+ },
2514
+ ];
2515
+
2516
+ const res = (resultDataProvider as any).alignStepsWithIterationsTestReporter(
2517
+ testData,
2518
+ iterations,
2519
+ [],
2520
+ true
2521
+ );
2522
+ expect(res).toHaveLength(1);
2523
+ expect(res[0].stepNo).toBeUndefined();
2524
+ });
2525
+ });
2526
+
2527
+ describe('fetchOpenPcrData', () => {
2528
+ it('should populate both trace maps using linked work items', async () => {
2529
+ const testItems = [
2530
+ {
2531
+ testId: 1,
2532
+ testName: 'T1',
2533
+ testCaseUrl: 'u1',
2534
+ runStatus: 'Passed',
2535
+ },
2536
+ ];
2537
+ const linked = [
2538
+ {
2539
+ testId: 1,
2540
+ testName: 'T1',
2541
+ testCaseUrl: 'u1',
2542
+ runStatus: 'Passed',
2543
+ linkItems: [
2544
+ {
2545
+ pcrId: 10,
2546
+ workItemType: 'Bug',
2547
+ title: 'B10',
2548
+ severity: '2',
2549
+ pcrUrl: 'p10',
2550
+ },
2551
+ ],
2552
+ },
2553
+ ];
2554
+
2555
+ jest.spyOn(resultDataProvider as any, 'fetchLinkedWi').mockResolvedValueOnce(linked);
2556
+
2557
+ const openPcrToTestCaseTraceMap = new Map<string, string[]>();
2558
+ const testCaseToOpenPcrTraceMap = new Map<string, string[]>();
2559
+
2560
+ await (resultDataProvider as any).fetchOpenPcrData(
2561
+ testItems,
2562
+ mockProjectName,
2563
+ openPcrToTestCaseTraceMap,
2564
+ testCaseToOpenPcrTraceMap
2565
+ );
2566
+
2567
+ expect(openPcrToTestCaseTraceMap.size).toBe(1);
2568
+ expect(testCaseToOpenPcrTraceMap.size).toBe(1);
2569
+ });
2570
+ });
2571
+ });