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