@elisra-devops/docgen-data-provider 1.22.0 → 1.24.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 (49) hide show
  1. package/bin/helpers/helper.js.map +1 -1
  2. package/bin/helpers/test/tfs.test.d.ts +1 -0
  3. package/bin/helpers/test/tfs.test.js +613 -0
  4. package/bin/helpers/test/tfs.test.js.map +1 -0
  5. package/bin/helpers/tfs.js +1 -1
  6. package/bin/helpers/tfs.js.map +1 -1
  7. package/bin/modules/GitDataProvider.js.map +1 -1
  8. package/bin/modules/PipelinesDataProvider.js +3 -2
  9. package/bin/modules/PipelinesDataProvider.js.map +1 -1
  10. package/bin/modules/ResultDataProvider.d.ts +200 -17
  11. package/bin/modules/ResultDataProvider.js +628 -195
  12. package/bin/modules/ResultDataProvider.js.map +1 -1
  13. package/bin/modules/TestDataProvider.js +7 -7
  14. package/bin/modules/TestDataProvider.js.map +1 -1
  15. package/bin/modules/TicketsDataProvider.d.ts +1 -1
  16. package/bin/modules/TicketsDataProvider.js +3 -2
  17. package/bin/modules/TicketsDataProvider.js.map +1 -1
  18. package/bin/modules/test/JfrogDataProvider.test.d.ts +1 -0
  19. package/bin/modules/test/JfrogDataProvider.test.js +110 -0
  20. package/bin/modules/test/JfrogDataProvider.test.js.map +1 -0
  21. package/bin/modules/test/ResultDataProvider.test.d.ts +1 -0
  22. package/bin/modules/test/ResultDataProvider.test.js +478 -0
  23. package/bin/modules/test/ResultDataProvider.test.js.map +1 -0
  24. package/bin/modules/test/gitDataProvider.test.js +424 -120
  25. package/bin/modules/test/gitDataProvider.test.js.map +1 -1
  26. package/bin/modules/test/managmentDataProvider.test.js +283 -28
  27. package/bin/modules/test/managmentDataProvider.test.js.map +1 -1
  28. package/bin/modules/test/pipelineDataProvider.test.js +229 -45
  29. package/bin/modules/test/pipelineDataProvider.test.js.map +1 -1
  30. package/bin/modules/test/testDataProvider.test.js +225 -81
  31. package/bin/modules/test/testDataProvider.test.js.map +1 -1
  32. package/bin/modules/test/ticketsDataProvider.test.js +310 -82
  33. package/bin/modules/test/ticketsDataProvider.test.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/helpers/helper.ts +16 -14
  36. package/src/helpers/test/tfs.test.ts +748 -0
  37. package/src/helpers/tfs.ts +1 -1
  38. package/src/modules/GitDataProvider.ts +10 -10
  39. package/src/modules/PipelinesDataProvider.ts +2 -2
  40. package/src/modules/ResultDataProvider.ts +834 -260
  41. package/src/modules/TestDataProvider.ts +8 -8
  42. package/src/modules/TicketsDataProvider.ts +5 -9
  43. package/src/modules/test/JfrogDataProvider.test.ts +171 -0
  44. package/src/modules/test/ResultDataProvider.test.ts +581 -0
  45. package/src/modules/test/gitDataProvider.test.ts +671 -187
  46. package/src/modules/test/managmentDataProvider.test.ts +386 -26
  47. package/src/modules/test/pipelineDataProvider.test.ts +281 -52
  48. package/src/modules/test/testDataProvider.test.ts +307 -105
  49. package/src/modules/test/ticketsDataProvider.test.ts +425 -129
@@ -1,9 +1,30 @@
1
1
  import { TFSServices } from '../helpers/tfs';
2
- import { TestSteps, Workitem } from '../models/tfs-data';
3
- import * as xml2js from 'xml2js';
2
+ import { TestSteps } from '../models/tfs-data';
4
3
  import logger from '../utils/logger';
5
4
  import TestStepParserHelper from '../utils/testStepParserHelper';
6
5
  const pLimit = require('p-limit');
6
+ /**
7
+ * Provides methods to fetch, process, and summarize test data from Azure DevOps.
8
+ *
9
+ * This class includes functionalities for:
10
+ * - Fetching test suites, test points, and test cases.
11
+ * - Aligning test steps with iterations and generating detailed results.
12
+ * - Fetching result data based on work items and test runs.
13
+ * - Summarizing test group results, test results, and detailed results.
14
+ * - Fetching linked work items and open PCRs (Problem Change Requests).
15
+ * - Generating test logs and mapping attachments for download.
16
+ * - Supporting test reporter functionalities with customizable field selection and filtering.
17
+ *
18
+ * Key Features:
19
+ * - Hierarchical and flat test suite processing.
20
+ * - Step-level and test-level result alignment.
21
+ * - Customizable result summaries and detailed reports.
22
+ * - Integration with Azure DevOps APIs for fetching and processing test data.
23
+ * - Support for additional configurations like open PCRs, test logs, and step execution analysis.
24
+ *
25
+ * Usage:
26
+ * Instantiate the class with the organization URL and token, and use the provided methods to fetch and process test data.
27
+ */
7
28
  export default class ResultDataProvider {
8
29
  orgUrl: string = '';
9
30
  token: string = '';
@@ -16,7 +37,278 @@ export default class ResultDataProvider {
16
37
  }
17
38
 
18
39
  /**
19
- * Retrieves test suites by test plan ID and processes them into a flat structure with hierarchy names.
40
+ * Combines the results of test group result summary, test results summary, and detailed results summary into a single key-value pair array.
41
+ */
42
+ public async getCombinedResultsSummary(
43
+ testPlanId: string,
44
+ projectName: string,
45
+ selectedSuiteIds?: number[],
46
+ addConfiguration: boolean = false,
47
+ isHierarchyGroupName: boolean = false,
48
+ includeOpenPCRs: boolean = false,
49
+ includeTestLog: boolean = false,
50
+ stepExecution?: any,
51
+ stepAnalysis?: any,
52
+ includeHardCopyRun: boolean = false
53
+ ): Promise<any[]> {
54
+ const combinedResults: any[] = [];
55
+ try {
56
+ // Fetch test suites
57
+ const suites = await this.fetchTestSuites(
58
+ testPlanId,
59
+ projectName,
60
+ selectedSuiteIds,
61
+ isHierarchyGroupName
62
+ );
63
+
64
+ // Prepare test data for summaries
65
+ const testPointsPromises = suites.map((suite) =>
66
+ this.limit(() =>
67
+ this.fetchTestPoints(projectName, testPlanId, suite.testSuiteId)
68
+ .then((testPointsItems) => ({ ...suite, testPointsItems }))
69
+ .catch((error: any) => {
70
+ logger.error(`Error occurred for suite ${suite.testSuiteId}: ${error.message}`);
71
+ return { ...suite, testPointsItems: [] };
72
+ })
73
+ )
74
+ );
75
+ const testPoints = await Promise.all(testPointsPromises);
76
+
77
+ // 1. Calculate Test Group Result Summary
78
+ const summarizedResults = testPoints
79
+ .filter((testPoint: any) => testPoint.testPointsItems && testPoint.testPointsItems.length > 0)
80
+ .map((testPoint: any) => {
81
+ const groupResultSummary = this.calculateGroupResultSummary(
82
+ testPoint.testPointsItems || [],
83
+ includeHardCopyRun
84
+ );
85
+ return { ...testPoint, groupResultSummary };
86
+ });
87
+
88
+ const totalSummary = this.calculateTotalSummary(summarizedResults, includeHardCopyRun);
89
+ const testGroupArray = summarizedResults.map((item: any) => ({
90
+ testGroupName: item.testGroupName,
91
+ ...item.groupResultSummary,
92
+ }));
93
+ testGroupArray.push({ testGroupName: 'Total', ...totalSummary });
94
+
95
+ // Add test group result summary to combined results
96
+ combinedResults.push({
97
+ contentControl: 'test-group-summary-content-control',
98
+ data: testGroupArray,
99
+ skin: 'test-result-test-group-summary-table',
100
+ });
101
+
102
+ // 2. Calculate Test Results Summary
103
+ const flattenedTestPoints = this.flattenTestPoints(testPoints);
104
+ const testResultsSummary = flattenedTestPoints.map((testPoint) =>
105
+ this.formatTestResult(testPoint, addConfiguration, includeHardCopyRun)
106
+ );
107
+
108
+ // Add test results summary to combined results
109
+ combinedResults.push({
110
+ contentControl: 'test-result-summary-content-control',
111
+ data: testResultsSummary,
112
+ skin: 'test-result-table',
113
+ });
114
+
115
+ // 3. Calculate Detailed Results Summary
116
+ const testData = await this.fetchTestData(suites, projectName, testPlanId);
117
+ const runResults = await this.fetchAllResultData(testData, projectName);
118
+ const detailedStepResultsSummary = this.alignStepsWithIterations(testData, runResults);
119
+ //Filter out all the results with no comment
120
+ const filteredDetailedResults = detailedStepResultsSummary.filter(
121
+ (result) => result && (result.stepComments !== '' || result.stepStatus === 'Failed')
122
+ );
123
+
124
+ // Add detailed results summary to combined results
125
+ combinedResults.push({
126
+ contentControl: 'detailed-test-result-content-control',
127
+ data: !includeHardCopyRun ? filteredDetailedResults : [],
128
+ skin: 'detailed-test-result-table',
129
+ });
130
+
131
+ if (includeOpenPCRs) {
132
+ //5. Open PCRs data (only if enabled)
133
+ await this.fetchOpenPcrData(testResultsSummary, projectName, combinedResults);
134
+ }
135
+
136
+ //6. Test Log (only if enabled)
137
+ if (includeTestLog) {
138
+ this.fetchTestLogData(flattenedTestPoints, combinedResults);
139
+ }
140
+
141
+ if (stepAnalysis && stepAnalysis.isEnabled) {
142
+ const mappedAnalysisData = runResults.filter(
143
+ (result) =>
144
+ result.comment ||
145
+ result.iteration?.attachments?.length > 0 ||
146
+ result.analysisAttachments?.length > 0
147
+ );
148
+
149
+ const mappedAnalysisResultData = stepAnalysis.generateRunAttachments.isEnabled
150
+ ? this.mapAttachmentsUrl(mappedAnalysisData, projectName)
151
+ : mappedAnalysisData;
152
+ if (mappedAnalysisResultData?.length > 0) {
153
+ combinedResults.push({
154
+ contentControl: 'appendix-a-content-control',
155
+ data: mappedAnalysisResultData,
156
+ skin: 'step-analysis-appendix-skin',
157
+ });
158
+ }
159
+ }
160
+
161
+ if (stepExecution && stepExecution.isEnabled) {
162
+ const mappedAnalysisData =
163
+ stepExecution.generateAttachments.isEnabled &&
164
+ stepExecution.generateAttachments.runAttachmentMode !== 'planOnly'
165
+ ? runResults.filter((result) => result.iteration?.attachments?.length > 0)
166
+ : [];
167
+ const mappedAnalysisResultData =
168
+ mappedAnalysisData.length > 0 ? this.mapAttachmentsUrl(mappedAnalysisData, projectName) : [];
169
+
170
+ const mappedDetailedResults = this.mapStepResultsForExecutionAppendix(
171
+ detailedStepResultsSummary,
172
+ mappedAnalysisResultData
173
+ );
174
+ combinedResults.push({
175
+ contentControl: 'appendix-b-content-control',
176
+ data: mappedDetailedResults,
177
+ skin: 'step-execution-appendix-skin',
178
+ });
179
+ }
180
+
181
+ return combinedResults;
182
+ } catch (error: any) {
183
+ logger.error(`Error during getCombinedResultsSummary: ${error.message}`);
184
+ if (error.response) {
185
+ logger.error(`Response Data: ${JSON.stringify(error.response.data)}`);
186
+ }
187
+ // Ensure the error is rethrown to propagate it correctly
188
+ throw new Error(error.message || 'Unknown error occurred during getCombinedResultsSummary');
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Fetches and processes test reporter results for a given test plan and project.
194
+ *
195
+ * @param testPlanId - The ID of the test plan to fetch results for.
196
+ * @param projectName - The name of the project associated with the test plan.
197
+ * @param selectedSuiteIds - An array of suite IDs to filter the test results.
198
+ * @param selectedFields - An array of field names to include in the results.
199
+ * @param enableRunStepStatusFilter - A flag to enable filtering out test steps with a "Not Run" status.
200
+ * @returns A promise that resolves to an array of test reporter results, formatted for use in a test-reporter-table.
201
+ *
202
+ * @throws Will log an error if any step in the process fails.
203
+ */
204
+ public async getTestReporterResults(
205
+ testPlanId: string,
206
+ projectName: string,
207
+ selectedSuiteIds: number[],
208
+ selectedFields: string[],
209
+ enableRunStepStatusFilter: boolean
210
+ ) {
211
+ const fetchedTestResults: any[] = [];
212
+ logger.debug(
213
+ `Fetching test reporter results for test plan ID: ${testPlanId}, project name: ${projectName}`
214
+ );
215
+ logger.debug(`Selected suite IDs: ${selectedSuiteIds}`);
216
+ try {
217
+ const plan = await this.fetchTestPlanName(testPlanId, projectName);
218
+ const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
219
+ const testData = await this.fetchTestData(suites, projectName, testPlanId);
220
+ const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, selectedFields);
221
+ const testReporterData = this.alignStepsWithIterationsTestReporter(
222
+ testData,
223
+ runResults,
224
+ selectedFields
225
+ );
226
+
227
+ const isNotRunStep = (result: any): boolean => {
228
+ return result && result.stepStatus === 'Not Run';
229
+ };
230
+
231
+ // Apply filters sequentially based on enabled flags
232
+ let filteredResults = testReporterData;
233
+
234
+ // filter: Test step run status
235
+ if (enableRunStepStatusFilter) {
236
+ filteredResults = filteredResults.filter((result) => !isNotRunStep(result));
237
+ }
238
+
239
+ // Use the final filtered results
240
+ fetchedTestResults.push({
241
+ contentControl: 'test-reporter-table',
242
+ customName: `Test Results for ${plan}`,
243
+ data: filteredResults || [],
244
+ skin: 'test-reporter-table',
245
+ });
246
+
247
+ return fetchedTestResults;
248
+ } catch (error: any) {
249
+ logger.error(`Error during getTestReporterResults: ${error.message}`);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Mapping each attachment to a proper URL for downloading it
255
+ * @param runResults Array of run results
256
+ */
257
+ public mapAttachmentsUrl(runResults: any[], project: string) {
258
+ return runResults.map((result) => {
259
+ if (!result.iteration) {
260
+ return result;
261
+ }
262
+ const { iteration, analysisAttachments, ...restResult } = result;
263
+ //add downloadUri field for each attachment
264
+ const baseDownloadUrl = `${this.orgUrl}${project}/_apis/test/runs/${result.lastRunId}/results/${result.lastResultId}/attachments`;
265
+ if (iteration && iteration.attachments?.length > 0) {
266
+ const { attachments, actionResults, ...restOfIteration } = iteration;
267
+ const attachmentPathToIndexMap: Map<string, number> =
268
+ this.CreateAttachmentPathIndexMap(actionResults);
269
+
270
+ const mappedAttachments = attachments.map((attachment: any) => ({
271
+ ...attachment,
272
+ stepNo: attachmentPathToIndexMap.has(attachment.actionPath)
273
+ ? attachmentPathToIndexMap.get(attachment.actionPath)
274
+ : undefined,
275
+ downloadUrl: `${baseDownloadUrl}/${attachment.id}/${attachment.name}`,
276
+ }));
277
+
278
+ restResult.iteration = { ...restOfIteration, attachments: mappedAttachments };
279
+ }
280
+ if (analysisAttachments && analysisAttachments.length > 0) {
281
+ restResult.analysisAttachments = analysisAttachments.map((attachment: any) => ({
282
+ ...attachment,
283
+ downloadUrl: `${baseDownloadUrl}/${attachment.id}/${attachment.fileName}`,
284
+ }));
285
+ }
286
+
287
+ return { ...restResult };
288
+ });
289
+ }
290
+
291
+ private async fetchTestPlanName(testPlanId: string, teamProject: string): Promise<string> {
292
+ try {
293
+ const url = `${this.orgUrl}${teamProject}/_apis/testplan/Plans/${testPlanId}?api-version=5.1`;
294
+ const testPlan = await TFSServices.getItemContent(url, this.token);
295
+ return testPlan.name;
296
+ } catch (error: any) {
297
+ logger.error(`Error during fetching Test Plan Name: ${error.message}`);
298
+ return '';
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Fetches test suites for a given test plan and project, optionally filtering by selected suite IDs.
304
+ *
305
+ * @param testPlanId - The ID of the test plan to fetch suites for.
306
+ * @param projectName - The name of the project containing the test plan.
307
+ * @param selectedSuiteIds - An optional array of suite IDs to filter the results by.
308
+ * @param isHierarchyGroupName - A flag indicating whether to build the test group name hierarchically. Defaults to `true`.
309
+ * @returns A promise that resolves to an array of objects containing `testSuiteId` and `testGroupName`.
310
+ * Returns an empty array if no test suites are found or an error occurs.
311
+ * @throws Will log an error message if fetching test suites fails.
20
312
  */
21
313
  private async fetchTestSuites(
22
314
  testPlanId: string,
@@ -107,6 +399,7 @@ export default class ResultDataProvider {
107
399
  }
108
400
 
109
401
  const parts = path.split('/');
402
+ if (parts.length - 1 === 1) return parts[1];
110
403
  return parts.length > 3
111
404
  ? `${parts[1]}/.../${parts[parts.length - 1]}`
112
405
  : `${parts[1]}/${parts[parts.length - 1]}`;
@@ -124,7 +417,7 @@ export default class ResultDataProvider {
124
417
  const url = `${this.orgUrl}${projectName}/_apis/testplan/Plans/${testPlanId}/Suites/${testSuiteId}/TestPoint?includePointDetails=true`;
125
418
  const { value: testPoints, count } = await TFSServices.getItemContent(url, this.token);
126
419
 
127
- return count !== 0 ? testPoints.map(this.mapTestPoint) : [];
420
+ return count !== 0 ? testPoints.map((testPoint: any) => this.mapTestPoint(testPoint, projectName)) : [];
128
421
  } catch (error: any) {
129
422
  logger.error(`Error during fetching Test Points: ${error.message}`);
130
423
  return [];
@@ -134,10 +427,11 @@ export default class ResultDataProvider {
134
427
  /**
135
428
  * Maps raw test point data to a simplified object.
136
429
  */
137
- private mapTestPoint(testPoint: any): any {
430
+ private mapTestPoint(testPoint: any, projectName: string): any {
138
431
  return {
139
432
  testCaseId: testPoint.testCaseReference.id,
140
433
  testCaseName: testPoint.testCaseReference.name,
434
+ testCaseUrl: `${this.orgUrl}${projectName}/_workitems/edit/${testPoint.testCaseReference.id}`,
141
435
  configurationName: testPoint.configuration?.name,
142
436
  outcome: testPoint.results?.outcome || 'Not Run',
143
437
  lastRunId: testPoint.results?.lastTestRunId,
@@ -157,35 +451,147 @@ export default class ResultDataProvider {
157
451
  const url = `${this.orgUrl}${projectName}/_apis/testplan/Plans/${testPlanId}/Suites/${suiteId}/TestCase?witFields=Microsoft.VSTS.TCM.Steps`;
158
452
 
159
453
  const { value: testCases } = await TFSServices.getItemContent(url, this.token);
454
+
160
455
  return testCases;
161
456
  }
162
457
 
163
458
  /**
164
- * Fetches iterations data by run and result IDs.
459
+ * Fetches result data based on a work item base (WiBase) for a specific test run and result.
460
+ *
461
+ * @param projectName - The name of the project in Azure DevOps.
462
+ * @param runId - The ID of the test run.
463
+ * @param resultId - The ID of the test result.
464
+ * @param options - Optional parameters for customizing the data retrieval.
465
+ * @param options.expandWorkItem - If true, expands all fields of the work item.
466
+ * @param options.selectedFields - An array of field names to filter the work item fields.
467
+ * @param options.processRelatedRequirements - If true, processes related requirements linked to the work item.
468
+ * @param options.includeFullErrorStack - If true, includes the full error stack in the logs when an error occurs.
469
+ * @returns A promise that resolves to an object containing the fetched result data, including:
470
+ * - `stepsResultXml`: The test steps result in XML format.
471
+ * - `analysisAttachments`: Attachments related to the test result analysis.
472
+ * - `testCaseRevision`: The revision number of the test case.
473
+ * - `filteredFields`: The filtered fields from the work item based on the selected fields.
474
+ * - `relatedRequirements`: An array of related requirements with details such as ID, title, customer ID, and URL.
475
+ * - `relatedBugs`: An array of related bugs with details such as ID, title, and URL.
476
+ * If an error occurs, logs the error and returns `null`.
477
+ *
478
+ * @throws Logs an error message if the data retrieval fails.
165
479
  */
166
- private async fetchResult(projectName: string, runId: string, resultId: string): Promise<any> {
480
+ private async fetchResultDataBasedOnWiBase(
481
+ projectName: string,
482
+ runId: string,
483
+ resultId: string,
484
+ options: {
485
+ expandWorkItem?: boolean;
486
+ selectedFields?: string[];
487
+ processRelatedRequirements?: boolean;
488
+ processRelatedBugs?: boolean;
489
+ includeFullErrorStack?: boolean;
490
+ } = {}
491
+ ): Promise<any> {
167
492
  try {
168
493
  const url = `${this.orgUrl}${projectName}/_apis/test/runs/${runId}/results/${resultId}?detailsToInclude=Iterations`;
169
494
  const resultData = await TFSServices.getItemContent(url, this.token);
495
+
170
496
  const attachmentsUrl = `${this.orgUrl}${projectName}/_apis/test/runs/${runId}/results/${resultId}/attachments`;
171
497
  const { value: analysisAttachments } = await TFSServices.getItemContent(attachmentsUrl, this.token);
172
- const wiUrl = `${this.orgUrl}${projectName}/_apis/wit/workItems/${resultData.testCase.id}/revisions/${resultData.testCaseRevision}`;
498
+
499
+ // Build workItem URL with optional expand parameter
500
+ const expandParam = options.expandWorkItem ? '?$expand=all' : '';
501
+ const wiUrl = `${this.orgUrl}${projectName}/_apis/wit/workItems/${resultData.testCase.id}/revisions/${resultData.testCaseRevision}${expandParam}`;
173
502
  const wiByRevision = await TFSServices.getItemContent(wiUrl, this.token);
503
+ let filteredFields: any = {};
504
+ let relatedRequirements: any[] = [];
505
+ let relatedBugs: any[] = [];
506
+
507
+ // Process selected fields if provided
508
+ if (
509
+ options.selectedFields?.length &&
510
+ (options.processRelatedRequirements || options.processRelatedBugs)
511
+ ) {
512
+ const filtered = options.selectedFields
513
+ ?.filter((field: string) => field.includes('@testCaseWorkItemField'))
514
+ ?.map((field: string) => field.split('@')[0]);
515
+ const selectedFieldSet = new Set(filtered);
516
+
517
+ if (selectedFieldSet.size !== 0) {
518
+ // Process related requirements if needed
519
+ const { relations } = wiByRevision;
520
+ if (relations) {
521
+ for (const relation of relations) {
522
+ if (
523
+ relation.rel?.includes('System.LinkTypes.Hierarchy') ||
524
+ relation.rel?.includes('Microsoft.VSTS.Common.TestedBy')
525
+ ) {
526
+ const relatedUrl = relation.url;
527
+ const wi = await TFSServices.getItemContent(relatedUrl, this.token);
528
+ if (wi.fields['System.WorkItemType'] === 'Requirement') {
529
+ const { id, fields, _links } = wi;
530
+ const requirementTitle = fields['System.Title'];
531
+ const customerFieldKey = Object.keys(fields).find((key) =>
532
+ key.toLowerCase().includes('customer')
533
+ );
534
+ const customerId = customerFieldKey ? fields[customerFieldKey] : undefined;
535
+ const url = _links.html.href;
536
+ relatedRequirements.push({ id, requirementTitle, customerId, url });
537
+ } else if (wi.fields['System.WorkItemType'] === 'Bug') {
538
+ const { id, fields, _links } = wi;
539
+ const bugTitle = fields['System.Title'];
540
+ const url = _links.html.href;
541
+ relatedBugs.push({ id, bugTitle, url });
542
+ }
543
+ }
544
+ }
545
+ }
174
546
 
547
+ // Filter fields based on selected field set
548
+ filteredFields = Object.keys(wiByRevision.fields)
549
+ .filter((key) => selectedFieldSet.has(key))
550
+ .reduce((obj: any, key) => {
551
+ obj[key] = wiByRevision.fields[key]?.displayName ?? wiByRevision.fields[key];
552
+ return obj;
553
+ }, {});
554
+ }
555
+ }
175
556
  return {
176
557
  ...resultData,
177
558
  stepsResultXml: wiByRevision.fields['Microsoft.VSTS.TCM.Steps'] || undefined,
178
559
  analysisAttachments,
179
560
  testCaseRevision: resultData.testCaseRevision,
561
+ filteredFields,
562
+ relatedRequirements,
563
+ relatedBugs,
180
564
  };
181
565
  } catch (error: any) {
182
566
  logger.error(`Error while fetching run result: ${error.message}`);
567
+ if (options.includeFullErrorStack) {
568
+ logger.error(`Error stack: ${error.stack}`);
569
+ }
183
570
  return null;
184
571
  }
185
572
  }
186
573
 
187
574
  /**
188
- * Converts run status from API format to a more readable format.
575
+ * Fetches result data based on the specified work item (WI) details.
576
+ *
577
+ * @param projectName - The name of the project associated with the work item.
578
+ * @param runId - The unique identifier of the test run.
579
+ * @param resultId - The unique identifier of the test result.
580
+ * @returns A promise that resolves to the result data.
581
+ */
582
+ private async fetchResultDataBasedOnWi(projectName: string, runId: string, resultId: string): Promise<any> {
583
+ return this.fetchResultDataBasedOnWiBase(projectName, runId, resultId);
584
+ }
585
+
586
+ /**
587
+ * Converts a run status string into a human-readable format.
588
+ *
589
+ * @param status - The status string to convert. Expected values are:
590
+ * - `'passed'`: Indicates the run was successful.
591
+ * - `'failed'`: Indicates the run was unsuccessful.
592
+ * - `'notApplicable'`: Indicates the run is not applicable.
593
+ * - Any other value will default to `'Not Run'`.
594
+ * @returns A human-readable string representing the run status.
189
595
  */
190
596
  private convertRunStatus(status: string): string {
191
597
  switch (status) {
@@ -200,7 +606,19 @@ export default class ResultDataProvider {
200
606
  }
201
607
  }
202
608
 
203
- private setRunStatus(actionResult: any) {
609
+ /**
610
+ * Converts the outcome of an action result based on specific conditions.
611
+ *
612
+ * - If the outcome is 'Unspecified' and the action result is a shared step title,
613
+ * it returns an empty string.
614
+ * - If the outcome is 'Unspecified' but not a shared step title, it returns 'Not Run'.
615
+ * - If the outcome is not 'Not Run', it returns the original outcome.
616
+ * - Otherwise, it returns an empty string.
617
+ *
618
+ * @param actionResult - The action result object containing the outcome and other properties.
619
+ * @returns A string representing the converted outcome.
620
+ */
621
+ private convertUnspecifiedRunStatus(actionResult: any) {
204
622
  if (actionResult.outcome === 'Unspecified' && actionResult.isSharedStepTitle) {
205
623
  return '';
206
624
  }
@@ -213,19 +631,49 @@ export default class ResultDataProvider {
213
631
  }
214
632
 
215
633
  /**
216
- * Aligns test steps with their corresponding iterations.
634
+ * Aligns test steps with iterations and generates detailed results based on the provided options.
635
+ *
636
+ * @param testData - An array of test data objects containing test points and test cases.
637
+ * @param iterations - An array of iteration data used to map test cases to their respective iterations.
638
+ * @param options - Configuration options for processing the test data.
639
+ * @param options.selectedFields - An optional array of selected fields to filter step-level properties.
640
+ * @param options.createResultObject - A callback function to create a result object for each test or step.
641
+ * @param options.shouldProcessStepLevel - A callback function to determine whether to process at the step level.
642
+ * @returns An array of detailed result objects, either at the test level or step level, depending on the options.
217
643
  */
218
- private alignStepsWithIterations(testData: any[], iterations: any[]): any[] {
644
+ private alignStepsWithIterationsBase(
645
+ testData: any[],
646
+ iterations: any[],
647
+ options: {
648
+ selectedFields?: any[];
649
+ createResultObject: (params: {
650
+ testItem: any;
651
+ point: any;
652
+ fetchedTestCase: any;
653
+ actionResult?: any;
654
+ filteredFields?: Set<string>;
655
+ }) => any;
656
+ shouldProcessStepLevel: (fetchedTestCase: any, filteredFields: Set<string>) => boolean;
657
+ }
658
+ ): any[] {
219
659
  const detailedResults: any[] = [];
220
660
  if (!iterations || iterations?.length === 0) {
221
661
  return detailedResults;
222
662
  }
223
663
 
664
+ // Process filtered fields if available
665
+ const filteredFields = new Set(
666
+ options.selectedFields
667
+ ?.filter((field: string) => field.includes('@stepsRunProperties'))
668
+ ?.map((field: string) => field.split('@')[0]) || []
669
+ );
670
+
224
671
  for (const testItem of testData) {
225
672
  for (const point of testItem.testPointsItems) {
226
673
  const testCase = testItem.testCasesItems.find((tc: any) => tc.workItem.id === point.testCaseId);
227
674
  if (!testCase) continue;
228
675
  const iterationsMap = this.createIterationsMap(iterations, testCase.workItem.id);
676
+
229
677
  if (testCase.workItem.workItemFields.length === 0) {
230
678
  logger.warn(`Could not fetch the steps from WI ${JSON.stringify(testCase.workItem.id)}`);
231
679
  continue;
@@ -233,29 +681,35 @@ export default class ResultDataProvider {
233
681
 
234
682
  if (point.lastRunId && point.lastResultId) {
235
683
  const iterationKey = `${point.lastRunId}-${point.lastResultId}-${testCase.workItem.id}`;
684
+ const fetchedTestCase = iterationsMap[iterationKey];
685
+
686
+ if (!fetchedTestCase || !fetchedTestCase.iteration) continue;
236
687
 
237
- const testCastObj = iterationsMap[iterationKey];
238
- if (!testCastObj || !testCastObj.iteration || !testCastObj.iteration.actionResults) continue;
239
-
240
- const { actionResults } = testCastObj?.iteration;
241
-
242
- for (let i = 0; i < actionResults?.length; i++) {
243
- const stepIdentifier = parseInt(actionResults[i].stepIdentifier, 10);
244
- const resultObj = {
245
- testId: point.testCaseId,
246
- testCaseRevision: testCastObj.testCaseRevision,
247
- testName: point.testCaseName,
248
- stepIdentifier: stepIdentifier,
249
- actionPath: actionResults[i].actionPath,
250
- stepNo: actionResults[i].stepPosition,
251
- stepAction: actionResults[i].action,
252
- stepExpected: actionResults[i].expected,
253
- isSharedStepTitle: actionResults[i].isSharedStepTitle,
254
- stepStatus: this.setRunStatus(actionResults[i]),
255
- stepComments: actionResults[i].errorMessage || '',
256
- };
688
+ // Determine if we should process at step level
689
+ const shouldProcessSteps = options.shouldProcessStepLevel(fetchedTestCase, filteredFields);
257
690
 
691
+ if (!shouldProcessSteps) {
692
+ // Create a test-level result object
693
+ const resultObj = options.createResultObject({
694
+ testItem,
695
+ point,
696
+ fetchedTestCase,
697
+ filteredFields,
698
+ });
258
699
  detailedResults.push(resultObj);
700
+ } else {
701
+ // Create step-level result objects
702
+ const { actionResults } = fetchedTestCase?.iteration;
703
+ for (let i = 0; i < actionResults?.length; i++) {
704
+ const resultObj = options.createResultObject({
705
+ testItem,
706
+ point,
707
+ fetchedTestCase,
708
+ actionResult: actionResults[i],
709
+ filteredFields,
710
+ });
711
+ detailedResults.push(resultObj);
712
+ }
259
713
  }
260
714
  }
261
715
  }
@@ -263,6 +717,44 @@ export default class ResultDataProvider {
263
717
  return detailedResults;
264
718
  }
265
719
 
720
+ /**
721
+ * Aligns test steps with their corresponding iterations by processing the provided test data and iterations.
722
+ * This method utilizes a base alignment function with custom logic for processing step-level data and creating result objects.
723
+ *
724
+ * @param testData - An array of test data objects containing information about test cases and their steps.
725
+ * @param iterations - An array of iteration objects containing action results for each test case.
726
+ * @returns An array of aligned step data objects, each containing detailed information about a test step and its status.
727
+ *
728
+ * The alignment process includes:
729
+ * - Filtering and processing step-level data based on the presence of iteration and action results.
730
+ * - Creating result objects for each step, including details such as test ID, step identifier, action path, step status, and comments.
731
+ */
732
+ private alignStepsWithIterations(testData: any[], iterations: any[]): any[] {
733
+ return this.alignStepsWithIterationsBase(testData, iterations, {
734
+ shouldProcessStepLevel: (fetchedTestCase) =>
735
+ fetchedTestCase != null &&
736
+ fetchedTestCase.iteration != null &&
737
+ fetchedTestCase.iteration.actionResults != null,
738
+ createResultObject: ({ point, fetchedTestCase, actionResult }) => {
739
+ if (!actionResult) return null;
740
+
741
+ const stepIdentifier = parseInt(actionResult.stepIdentifier, 10);
742
+ return {
743
+ testId: point.testCaseId,
744
+ testCaseRevision: fetchedTestCase.testCaseRevision,
745
+ testName: point.testCaseName,
746
+ stepIdentifier: stepIdentifier,
747
+ actionPath: actionResult.actionPath,
748
+ stepNo: actionResult.stepPosition,
749
+ stepAction: actionResult.action,
750
+ stepExpected: actionResult.expected,
751
+ isSharedStepTitle: actionResult.isSharedStepTitle,
752
+ stepStatus: this.convertUnspecifiedRunStatus(actionResult),
753
+ stepComments: actionResult.errorMessage || '',
754
+ };
755
+ },
756
+ });
757
+ }
266
758
  /**
267
759
  * Creates a mapping of iterations by their unique keys.
268
760
  */
@@ -301,12 +793,24 @@ export default class ResultDataProvider {
301
793
  }
302
794
 
303
795
  /**
304
- * Fetches result data for all test points within the given test data.
305
- */
306
- /**
307
- * Fetches result data for all test points within the given test data, sequentially to avoid context-related errors.
796
+ * Fetches all result data based on the provided test data, project name, and fetch strategy.
797
+ * This method processes the test data, filters valid test points, and sequentially fetches
798
+ * result data for each valid point using the provided fetch strategy.
799
+ *
800
+ * @param testData - An array of test data objects containing test suite and test points information.
801
+ * @param projectName - The name of the project for which the result data is being fetched.
802
+ * @param fetchStrategy - A function that defines the strategy for fetching result data. It takes
803
+ * the project name, test suite ID, a test point, and additional arguments,
804
+ * and returns a Promise resolving to the fetched result data.
805
+ * @param additionalArgs - An optional array of additional arguments to be passed to the fetch strategy.
806
+ * @returns A Promise that resolves to an array of fetched result data.
308
807
  */
309
- private async fetchAllResultData(testData: any[], projectName: string): Promise<any[]> {
808
+ private async fetchAllResultDataBase(
809
+ testData: any[],
810
+ projectName: string,
811
+ fetchStrategy: (projectName: string, testSuiteId: string, point: any, ...args: any[]) => Promise<any>,
812
+ additionalArgs: any[] = []
813
+ ): Promise<any[]> {
310
814
  const results = [];
311
815
 
312
816
  for (const item of testData) {
@@ -320,7 +824,7 @@ export default class ResultDataProvider {
320
824
 
321
825
  // Fetch results for each point sequentially
322
826
  for (const point of validPoints) {
323
- const resultData = await this.fetchResultData(projectName, testSuiteId, point);
827
+ const resultData = await fetchStrategy(projectName, testSuiteId, point, ...additionalArgs);
324
828
  if (resultData !== null) {
325
829
  results.push(resultData);
326
830
  }
@@ -331,6 +835,15 @@ export default class ResultDataProvider {
331
835
  return results;
332
836
  }
333
837
 
838
+ /**
839
+ * Fetches result data for all test points within the given test data, sequentially to avoid context-related errors.
840
+ */
841
+ private async fetchAllResultData(testData: any[], projectName: string): Promise<any[]> {
842
+ return this.fetchAllResultDataBase(testData, projectName, (projectName, testSuiteId, point) =>
843
+ this.fetchResultData(projectName, testSuiteId, point)
844
+ );
845
+ }
846
+
334
847
  //Sorting step positions
335
848
  private compareActionResults = (a: string, b: string) => {
336
849
  const aParts = a.split('.').map(Number);
@@ -350,11 +863,23 @@ export default class ResultDataProvider {
350
863
  };
351
864
 
352
865
  /**
353
- * Fetches result Data data for a specific test point.
866
+ * Base method for fetching result data for test points
354
867
  */
355
- private async fetchResultData(projectName: string, testSuiteId: string, point: any) {
868
+ private async fetchResultDataBase(
869
+ projectName: string,
870
+ testSuiteId: string,
871
+ point: any,
872
+ fetchResultMethod: (project: string, runId: string, resultId: string, ...args: any[]) => Promise<any>,
873
+ createResponseObject: (resultData: any, testSuiteId: string, point: any, ...args: any[]) => any,
874
+ additionalArgs: any[] = []
875
+ ): Promise<any> {
356
876
  const { lastRunId, lastResultId } = point;
357
- const resultData = await this.fetchResult(projectName, lastRunId.toString(), lastResultId.toString());
877
+ const resultData = await fetchResultMethod(
878
+ projectName,
879
+ lastRunId.toString(),
880
+ lastResultId.toString(),
881
+ ...additionalArgs
882
+ );
358
883
 
359
884
  const iteration =
360
885
  resultData.iterationDetails.length > 0
@@ -400,24 +925,41 @@ export default class ResultDataProvider {
400
925
  .filter((result: any) => result.stepPosition)
401
926
  .sort((a: any, b: any) => this.compareActionResults(a.stepPosition, b.stepPosition));
402
927
  }
928
+
403
929
  return resultData?.testCase
404
- ? {
405
- testCaseName: `${resultData.testCase.name} - ${resultData.testCase.id}`,
406
- testCaseId: resultData.testCase.id,
407
- testSuiteName: `${resultData.testSuite.name}`,
408
- testSuiteId,
409
- lastRunId,
410
- lastResultId,
411
- iteration,
412
- testCaseRevision: resultData.testCaseRevision,
413
- failureType: resultData.failureType,
414
- resolution: resultData.resolutionState,
415
- comment: resultData.comment,
416
- analysisAttachments: resultData.analysisAttachments,
417
- }
930
+ ? createResponseObject(resultData, testSuiteId, point, ...additionalArgs)
418
931
  : null;
419
932
  }
420
933
 
934
+ /**
935
+ * Fetches result Data for a specific test point
936
+ */
937
+ private async fetchResultData(projectName: string, testSuiteId: string, point: any) {
938
+ return this.fetchResultDataBase(
939
+ projectName,
940
+ testSuiteId,
941
+ point,
942
+ (project, runId, resultId) => this.fetchResultDataBasedOnWi(project, runId, resultId),
943
+ (resultData, testSuiteId, point) => ({
944
+ testCaseName: `${resultData.testCase.name} - ${resultData.testCase.id}`,
945
+ testCaseId: resultData.testCase.id,
946
+ testSuiteName: `${resultData.testSuite.name}`,
947
+ testSuiteId,
948
+ lastRunId: point.lastRunId,
949
+ lastResultId: point.lastResultId,
950
+ iteration:
951
+ resultData.iterationDetails.length > 0
952
+ ? resultData.iterationDetails[resultData.iterationDetails.length - 1]
953
+ : undefined,
954
+ testCaseRevision: resultData.testCaseRevision,
955
+ failureType: resultData.failureType,
956
+ resolution: resultData.resolutionState,
957
+ comment: resultData.comment,
958
+ analysisAttachments: resultData.analysisAttachments,
959
+ })
960
+ );
961
+ }
962
+
421
963
  /**
422
964
  * Fetches all the linked work items (WI) for the given test case.
423
965
  * @param project Project name
@@ -556,44 +1098,6 @@ export default class ResultDataProvider {
556
1098
  }
557
1099
  }
558
1100
 
559
- /**
560
- * Mapping each attachment to a proper URL for downloading it
561
- * @param runResults Array of run results
562
- */
563
- public mapAttachmentsUrl(runResults: any[], project: string) {
564
- return runResults.map((result) => {
565
- if (!result.iteration) {
566
- return result;
567
- }
568
- const { iteration, analysisAttachments, ...restResult } = result;
569
- //add downloadUri field for each attachment
570
- const baseDownloadUrl = `${this.orgUrl}${project}/_apis/test/runs/${result.lastRunId}/results/${result.lastResultId}/attachments`;
571
- if (iteration && iteration.attachments?.length > 0) {
572
- const { attachments, actionResults, ...restOfIteration } = iteration;
573
- const attachmentPathToIndexMap: Map<string, number> =
574
- this.CreateAttachmentPathIndexMap(actionResults);
575
-
576
- const mappedAttachments = attachments.map((attachment: any) => ({
577
- ...attachment,
578
- stepNo: attachmentPathToIndexMap.has(attachment.actionPath)
579
- ? attachmentPathToIndexMap.get(attachment.actionPath)
580
- : undefined,
581
- downloadUrl: `${baseDownloadUrl}/${attachment.id}/${attachment.name}`,
582
- }));
583
-
584
- restResult.iteration = { ...restOfIteration, attachments: mappedAttachments };
585
- }
586
- if (analysisAttachments && analysisAttachments.length > 0) {
587
- restResult.analysisAttachments = analysisAttachments.map((attachment: any) => ({
588
- ...attachment,
589
- downloadUrl: `${baseDownloadUrl}/${attachment.id}/${attachment.fileName}`,
590
- }));
591
- }
592
-
593
- return { ...restResult };
594
- });
595
- }
596
-
597
1101
  private CreateAttachmentPathIndexMap(actionResults: any) {
598
1102
  const attachmentPathToIndexMap: Map<string, number> = new Map();
599
1103
 
@@ -604,173 +1108,6 @@ export default class ResultDataProvider {
604
1108
  return attachmentPathToIndexMap;
605
1109
  }
606
1110
 
607
- /**
608
- * Combines the results of test group result summary, test results summary, and detailed results summary into a single key-value pair array.
609
- */
610
- public async getCombinedResultsSummary(
611
- testPlanId: string,
612
- projectName: string,
613
- selectedSuiteIds?: number[],
614
- addConfiguration: boolean = false,
615
- isHierarchyGroupName: boolean = false,
616
- includeOpenPCRs: boolean = false,
617
- includeTestLog: boolean = false,
618
- stepExecution?: any,
619
- stepAnalysis?: any,
620
- includeHardCopyRun: boolean = false
621
- ): Promise<any[]> {
622
- const combinedResults: any[] = [];
623
- try {
624
- // Fetch test suites
625
- const suites = await this.fetchTestSuites(
626
- testPlanId,
627
- projectName,
628
- selectedSuiteIds,
629
- isHierarchyGroupName
630
- );
631
-
632
- // Prepare test data for summaries
633
- const testPointsPromises = suites.map((suite) =>
634
- this.limit(() =>
635
- this.fetchTestPoints(projectName, testPlanId, suite.testSuiteId)
636
- .then((testPointsItems) => ({ ...suite, testPointsItems }))
637
- .catch((error: any) => {
638
- logger.error(`Error occurred for suite ${suite.testSuiteId}: ${error.message}`);
639
- return { ...suite, testPointsItems: [] };
640
- })
641
- )
642
- );
643
- const testPoints = await Promise.all(testPointsPromises);
644
-
645
- // 1. Calculate Test Group Result Summary
646
- const summarizedResults = testPoints
647
- .filter((testPoint: any) => testPoint.testPointsItems && testPoint.testPointsItems.length > 0)
648
- .map((testPoint: any) => {
649
- const groupResultSummary = this.calculateGroupResultSummary(
650
- testPoint.testPointsItems || [],
651
- includeHardCopyRun
652
- );
653
- return { ...testPoint, groupResultSummary };
654
- });
655
-
656
- const totalSummary = this.calculateTotalSummary(summarizedResults, includeHardCopyRun);
657
- const testGroupArray = summarizedResults.map((item: any) => ({
658
- testGroupName: item.testGroupName,
659
- ...item.groupResultSummary,
660
- }));
661
- testGroupArray.push({ testGroupName: 'Total', ...totalSummary });
662
-
663
- // Add test group result summary to combined results
664
- combinedResults.push({
665
- contentControl: 'test-group-summary-content-control',
666
- data: testGroupArray,
667
- skin: 'test-result-test-group-summary-table',
668
- });
669
-
670
- // 2. Calculate Test Results Summary
671
- const flattenedTestPoints = this.flattenTestPoints(testPoints);
672
- const testResultsSummary = flattenedTestPoints.map((testPoint) =>
673
- this.formatTestResult(testPoint, addConfiguration, includeHardCopyRun)
674
- );
675
-
676
- // Add test results summary to combined results
677
- combinedResults.push({
678
- contentControl: 'test-result-summary-content-control',
679
- data: testResultsSummary,
680
- skin: 'test-result-table',
681
- });
682
-
683
- // 3. Calculate Detailed Results Summary
684
- const testData = await this.fetchTestData(suites, projectName, testPlanId);
685
- const runResults = await this.fetchAllResultData(testData, projectName);
686
- const detailedStepResultsSummary = this.alignStepsWithIterations(testData, runResults);
687
- //Filter out all the results with no comment
688
- const filteredDetailedResults = detailedStepResultsSummary.filter(
689
- (result) => result && (result.stepComments !== '' || result.stepStatus === 'Failed')
690
- );
691
-
692
- // Add detailed results summary to combined results
693
- combinedResults.push({
694
- contentControl: 'detailed-test-result-content-control',
695
- data: !includeHardCopyRun ? filteredDetailedResults : [],
696
- skin: 'detailed-test-result-table',
697
- });
698
-
699
- if (includeOpenPCRs) {
700
- //5. Open PCRs data (only if enabled)
701
- await this.fetchOpenPcrData(testResultsSummary, projectName, combinedResults);
702
- }
703
-
704
- //6. Test Log (only if enabled)
705
- if (includeTestLog) {
706
- this.fetchTestLogData(flattenedTestPoints, combinedResults);
707
- }
708
-
709
- if (stepAnalysis && stepAnalysis.isEnabled) {
710
- const mappedAnalysisData = runResults.filter(
711
- (result) =>
712
- result.comment ||
713
- result.iteration?.attachments?.length > 0 ||
714
- result.analysisAttachments?.length > 0
715
- );
716
-
717
- const mappedAnalysisResultData = stepAnalysis.generateRunAttachments.isEnabled
718
- ? this.mapAttachmentsUrl(mappedAnalysisData, projectName)
719
- : mappedAnalysisData;
720
- if (mappedAnalysisResultData?.length > 0) {
721
- combinedResults.push({
722
- contentControl: 'appendix-a-content-control',
723
- data: mappedAnalysisResultData,
724
- skin: 'step-analysis-appendix-skin',
725
- });
726
- }
727
- }
728
-
729
- if (stepExecution && stepExecution.isEnabled) {
730
- const mappedAnalysisData =
731
- stepExecution.generateAttachments.isEnabled &&
732
- stepExecution.generateAttachments.runAttachmentMode !== 'planOnly'
733
- ? runResults.filter((result) => result.iteration?.attachments?.length > 0)
734
- : [];
735
- const mappedAnalysisResultData =
736
- mappedAnalysisData.length > 0 ? this.mapAttachmentsUrl(mappedAnalysisData, projectName) : [];
737
-
738
- const mappedDetailedResults = this.mapStepResultsForExecutionAppendix(
739
- detailedStepResultsSummary,
740
- mappedAnalysisResultData
741
- );
742
- combinedResults.push({
743
- contentControl: 'appendix-b-content-control',
744
- data: mappedDetailedResults,
745
- skin: 'step-execution-appendix-skin',
746
- });
747
- }
748
-
749
- return combinedResults;
750
- } catch (error: any) {
751
- logger.error(`Error during getCombinedResultsSummary: ${error.message}`);
752
- if (error.response) {
753
- logger.error(`Response Data: ${JSON.stringify(error.response.data)}`);
754
- }
755
- throw error;
756
- }
757
- }
758
-
759
- // private mapStepResultsForExecutionAppendix(detailedResults: any[]): any {
760
- // return detailedResults?.length > 0
761
- // ? detailedResults.map((result) => {
762
- // return {
763
- // testId: result.testId,
764
- // testCaseRevision: result.testCaseRevision || undefined,
765
- // stepNo: result.stepNo,
766
- // stepIdentifier: result.stepIdentifier,
767
- // stepStatus: result.stepStatus,
768
- // stepComments: result.stepComments,
769
- // };
770
- // })
771
- // : [];
772
- // }
773
-
774
1111
  private mapStepResultsForExecutionAppendix(detailedResults: any[], runResultData: any[]): Map<string, any> {
775
1112
  // Create maps first to avoid repeated lookups
776
1113
  const testCaseIdToStepsMap = new Map<string, any>();
@@ -948,4 +1285,241 @@ export default class ResultDataProvider {
948
1285
 
949
1286
  return formattedResult;
950
1287
  }
1288
+
1289
+ /**
1290
+ * Fetches result data based on the Work Item Test Reporter.
1291
+ *
1292
+ * This method retrieves detailed result data for a specific test run and result ID,
1293
+ * including related work items, selected fields, and additional processing options.
1294
+ *
1295
+ * @param projectName - The name of the project containing the test run.
1296
+ * @param runId - The unique identifier of the test run.
1297
+ * @param resultId - The unique identifier of the test result.
1298
+ * @param selectedFields - (Optional) An array of field names to include in the result data.
1299
+ * @returns A promise that resolves to the fetched result data.
1300
+ */
1301
+ private async fetchResultDataBasedOnWiTestReporter(
1302
+ projectName: string,
1303
+ runId: string,
1304
+ resultId: string,
1305
+ selectedFields?: string[]
1306
+ ): Promise<any> {
1307
+ return this.fetchResultDataBasedOnWiBase(projectName, runId, resultId, {
1308
+ expandWorkItem: true,
1309
+ selectedFields,
1310
+ processRelatedRequirements: true,
1311
+ processRelatedBugs: true,
1312
+ includeFullErrorStack: true,
1313
+ });
1314
+ }
1315
+
1316
+ /**
1317
+ * Fetches all result data for the test reporter by processing the provided test data.
1318
+ *
1319
+ * This method utilizes the `fetchAllResultDataBase` function to retrieve and process
1320
+ * result data for a specific project and test reporter. It applies a callback to fetch
1321
+ * result data for individual test points.
1322
+ *
1323
+ * @param testData - An array of test data objects to process.
1324
+ * @param projectName - The name of the project for which result data is being fetched.
1325
+ * @param selectedFields - An optional array of field names to include in the result data.
1326
+ * @returns A promise that resolves to an array of processed result data.
1327
+ */
1328
+ private async fetchAllResultDataTestReporter(
1329
+ testData: any[],
1330
+ projectName: string,
1331
+ selectedFields?: string[]
1332
+ ): Promise<any[]> {
1333
+ return this.fetchAllResultDataBase(
1334
+ testData,
1335
+ projectName,
1336
+ (projectName, testSuiteId, point, selectedFields) =>
1337
+ this.fetchResultDataForTestReporter(projectName, testSuiteId, point, selectedFields),
1338
+ [selectedFields]
1339
+ );
1340
+ }
1341
+
1342
+ /**
1343
+ * Aligns test steps with iterations for the test reporter by processing test data and iterations
1344
+ * and generating a structured result object based on the provided selected fields.
1345
+ *
1346
+ * @param testData - An array of test data objects to be processed.
1347
+ * @param iterations - An array of iteration objects to align with the test data.
1348
+ * @param selectedFields - An array of selected fields to determine which properties to include in the result.
1349
+ * @returns An array of structured result objects containing aligned test steps and iterations.
1350
+ *
1351
+ * The method uses a base alignment function and provides custom logic for:
1352
+ * - Determining whether step-level processing should occur based on the fetched test case and filtered fields.
1353
+ * - Creating a result object with properties such as suite name, test case details, priority, run information,
1354
+ * failure type, automation status, execution date, configuration name, state, error message, and related requirements.
1355
+ * - Including step-specific properties (e.g., step number, action, expected result, status, and comments) if action results are available
1356
+ * and the corresponding fields are selected.
1357
+ */
1358
+ private alignStepsWithIterationsTestReporter(
1359
+ testData: any[],
1360
+ iterations: any[],
1361
+ selectedFields: any[]
1362
+ ): any[] {
1363
+ return this.alignStepsWithIterationsBase(testData, iterations, {
1364
+ selectedFields,
1365
+ shouldProcessStepLevel: (fetchedTestCase, filteredFields) =>
1366
+ fetchedTestCase != null &&
1367
+ fetchedTestCase.iteration != null &&
1368
+ fetchedTestCase.iteration.actionResults != null &&
1369
+ filteredFields.size > 0,
1370
+
1371
+ createResultObject: ({
1372
+ testItem,
1373
+ point,
1374
+ fetchedTestCase,
1375
+ actionResult,
1376
+ filteredFields = new Set(),
1377
+ }) => {
1378
+ const baseObj = {
1379
+ suiteName: testItem.testGroupName,
1380
+ testCase: {
1381
+ id: point.testCaseId,
1382
+ title: point.testCaseName,
1383
+ url: point.testCaseUrl,
1384
+ result: fetchedTestCase.testCaseResult,
1385
+ comment: fetchedTestCase.iteration?.comment,
1386
+ },
1387
+ priority: fetchedTestCase.priority,
1388
+ runBy: fetchedTestCase.runBy,
1389
+ activatedBy: fetchedTestCase.activatedBy,
1390
+ assignedTo: fetchedTestCase.assignedTo,
1391
+ failureType: fetchedTestCase.failureType,
1392
+ automationStatus: fetchedTestCase.automationStatus,
1393
+ executionDate: fetchedTestCase.executionDate,
1394
+ configurationName: fetchedTestCase.configurationName,
1395
+ errorMessage: fetchedTestCase.errorMessage,
1396
+ relatedRequirements: fetchedTestCase.relatedRequirements,
1397
+ relatedBugs: fetchedTestCase.relatedBugs,
1398
+ };
1399
+
1400
+ // If we have action results, add step-specific properties
1401
+ if (actionResult) {
1402
+ return {
1403
+ ...baseObj,
1404
+ stepNo: actionResult.stepPosition,
1405
+ stepAction: filteredFields.has('includeSteps') ? actionResult.action : undefined,
1406
+ stepExpected: filteredFields.has('includeSteps') ? actionResult.expected : undefined,
1407
+ stepStatus: filteredFields.has('stepRunStatus')
1408
+ ? this.convertUnspecifiedRunStatus(actionResult)
1409
+ : undefined,
1410
+ stepComments: filteredFields.has('testStepComment') ? actionResult.errorMessage : undefined,
1411
+ };
1412
+ }
1413
+
1414
+ return baseObj;
1415
+ },
1416
+ });
1417
+ }
1418
+
1419
+ /**
1420
+ * Fetches result data for a test reporter based on the provided project, test suite, and point information.
1421
+ * This method processes the result data and formats it according to the selected fields.
1422
+ *
1423
+ * @param projectName - The name of the project.
1424
+ * @param testSuiteId - The ID of the test suite.
1425
+ * @param point - The test point containing details such as last run ID, result ID, and configuration name.
1426
+ * @param selectedFields - An optional array of field names to filter and include in the response.
1427
+ *
1428
+ * @returns A promise that resolves to the formatted result data object containing details about the test case,
1429
+ * test suite, last run, iteration, and other selected fields.
1430
+ */
1431
+ private async fetchResultDataForTestReporter(
1432
+ projectName: string,
1433
+ testSuiteId: string,
1434
+ point: any,
1435
+ selectedFields?: string[]
1436
+ ) {
1437
+ return this.fetchResultDataBase(
1438
+ projectName,
1439
+ testSuiteId,
1440
+ point,
1441
+ (project, runId, resultId, fields) =>
1442
+ this.fetchResultDataBasedOnWiTestReporter(project, runId, resultId, fields),
1443
+ (resultData, testSuiteId, point, selectedFields) => {
1444
+ const { lastRunId, lastResultId, configurationName, lastResultDetails } = point;
1445
+ const iteration =
1446
+ resultData.iterationDetails.length > 0
1447
+ ? resultData.iterationDetails[resultData.iterationDetails.length - 1]
1448
+ : undefined;
1449
+
1450
+ const resultDataResponse = {
1451
+ testCaseName: `${resultData.testCase.name} - ${resultData.testCase.id}`,
1452
+ testCaseId: resultData.testCase.id,
1453
+ testSuiteName: `${resultData.testSuite.name}`,
1454
+ testSuiteId,
1455
+ lastRunId,
1456
+ lastResultId,
1457
+ iteration,
1458
+ testCaseRevision: resultData.testCaseRevision,
1459
+ resolution: resultData.resolutionState,
1460
+ automationStatus: resultData.filteredFields['Microsoft.VSTS.TCM.AutomationStatus'] || undefined,
1461
+ analysisAttachments: resultData.analysisAttachments,
1462
+ failureType: undefined as string | undefined,
1463
+ comment: undefined as string | undefined,
1464
+ priority: undefined,
1465
+ runBy: undefined as string | undefined,
1466
+ executionDate: undefined as string | undefined,
1467
+ testCaseResult: undefined as any | undefined,
1468
+ errorMessage: undefined as string | undefined,
1469
+ configurationName: undefined as string | undefined,
1470
+ relatedRequirements: undefined,
1471
+ relatedBugs: undefined,
1472
+ lastRunResult: undefined as any,
1473
+ };
1474
+
1475
+ const filteredFields = selectedFields
1476
+ ?.filter((field: string) => field.includes('@runResultField') || field.includes('@linked'))
1477
+ ?.map((field: string) => field.split('@')[0]);
1478
+
1479
+ if (filteredFields && filteredFields.length > 0) {
1480
+ for (const field of filteredFields) {
1481
+ switch (field) {
1482
+ case 'priority':
1483
+ resultDataResponse.priority = resultData.priority;
1484
+ break;
1485
+ case 'testCaseResult':
1486
+ const outcome = resultData.iterationDetails[resultData.iterationDetails.length - 1]?.outcome;
1487
+ resultDataResponse.testCaseResult = {
1488
+ resultMessage: `${outcome} in Run ${lastRunId}`,
1489
+ url: `${this.orgUrl}${projectName}/_testManagement/runs?runId=${lastRunId}&_a=resultSummary&resultId=${lastResultId}`,
1490
+ };
1491
+ break;
1492
+ case 'failureType':
1493
+ resultDataResponse.failureType = resultData.failureType;
1494
+ break;
1495
+ case 'testCaseComment':
1496
+ resultDataResponse.comment = resultData.comment || undefined;
1497
+ break;
1498
+ case 'runBy':
1499
+ const runBy = lastResultDetails.runBy.displayName;
1500
+ resultDataResponse.runBy = runBy;
1501
+ break;
1502
+ case 'executionDate':
1503
+ resultDataResponse.executionDate = lastResultDetails.dateCompleted;
1504
+ break;
1505
+ case 'configurationName':
1506
+ resultDataResponse.configurationName = configurationName;
1507
+ break;
1508
+ case 'associatedRequirement':
1509
+ resultDataResponse.relatedRequirements = resultData.relatedRequirements;
1510
+ break;
1511
+ case 'associatedBug':
1512
+ resultDataResponse.relatedBugs = resultData.relatedBugs;
1513
+ break;
1514
+ default:
1515
+ logger.debug(`Field ${field} not handled`);
1516
+ break;
1517
+ }
1518
+ }
1519
+ }
1520
+ return resultDataResponse;
1521
+ },
1522
+ [selectedFields]
1523
+ );
1524
+ }
951
1525
  }