@elisra-devops/docgen-data-provider 1.27.0 → 1.29.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.
@@ -1,8 +1,8 @@
1
- import { log } from 'winston';
1
+ import DataProviderUtils from '../utils/DataProviderUtils';
2
2
  import { TFSServices } from '../helpers/tfs';
3
- import { TestSteps } from '../models/tfs-data';
3
+ import { OpenPcrRequest, TestSteps } from '../models/tfs-data';
4
4
  import logger from '../utils/logger';
5
- import TestStepParserHelper from '../utils/testStepParserHelper';
5
+ import Utils from '../utils/testStepParserHelper';
6
6
  const pLimit = require('p-limit');
7
7
  /**
8
8
  * Provides methods to fetch, process, and summarize test data from Azure DevOps.
@@ -30,11 +30,11 @@ export default class ResultDataProvider {
30
30
  orgUrl: string = '';
31
31
  token: string = '';
32
32
  private limit = pLimit(10);
33
- private testStepParserHelper: TestStepParserHelper;
33
+ private testStepParserHelper: Utils;
34
34
  constructor(orgUrl: string, token: string) {
35
35
  this.orgUrl = orgUrl;
36
36
  this.token = token;
37
- this.testStepParserHelper = new TestStepParserHelper(orgUrl, token);
37
+ this.testStepParserHelper = new Utils(orgUrl, token);
38
38
  }
39
39
 
40
40
  /**
@@ -46,14 +46,20 @@ export default class ResultDataProvider {
46
46
  selectedSuiteIds?: number[],
47
47
  addConfiguration: boolean = false,
48
48
  isHierarchyGroupName: boolean = false,
49
- includeOpenPCRs: boolean = false,
49
+ openPcrRequest: OpenPcrRequest | null = null,
50
50
  includeTestLog: boolean = false,
51
51
  stepExecution?: any,
52
52
  stepAnalysis?: any,
53
53
  includeHardCopyRun: boolean = false
54
- ): Promise<any[]> {
54
+ ): Promise<{
55
+ combinedResults: any[];
56
+ openPcrToTestCaseTraceMap: Map<string, string[]>;
57
+ testCaseToOpenPcrTraceMap: Map<string, string[]>;
58
+ }> {
55
59
  const combinedResults: any[] = [];
56
60
  try {
61
+ const openPcrToTestCaseTraceMap = new Map<string, string[]>();
62
+ const testCaseToOpenPcrTraceMap = new Map<string, string[]>();
57
63
  // Fetch test suites
58
64
  const suites = await this.fetchTestSuites(
59
65
  testPlanId,
@@ -114,7 +120,7 @@ export default class ResultDataProvider {
114
120
  });
115
121
 
116
122
  // 3. Calculate Detailed Results Summary
117
- const testData = await this.fetchTestData(suites, projectName, testPlanId);
123
+ const testData = await this.fetchTestData(suites, projectName, testPlanId, false);
118
124
  const runResults = await this.fetchAllResultData(testData, projectName);
119
125
  const detailedStepResultsSummary = this.alignStepsWithIterations(testData, runResults)?.filter(
120
126
  (results) => results !== null
@@ -131,9 +137,14 @@ export default class ResultDataProvider {
131
137
  skin: 'detailed-test-result-table',
132
138
  });
133
139
 
134
- if (includeOpenPCRs) {
140
+ if (openPcrRequest?.openPcrMode === 'linked') {
135
141
  //5. Open PCRs data (only if enabled)
136
- await this.fetchOpenPcrData(testResultsSummary, projectName, combinedResults);
142
+ await this.fetchOpenPcrData(
143
+ testResultsSummary,
144
+ projectName,
145
+ openPcrToTestCaseTraceMap,
146
+ testCaseToOpenPcrTraceMap
147
+ );
137
148
  }
138
149
 
139
150
  //6. Test Log (only if enabled)
@@ -164,7 +175,7 @@ export default class ResultDataProvider {
164
175
  if (stepExecution && stepExecution.isEnabled) {
165
176
  const mappedAnalysisData =
166
177
  stepExecution.generateAttachments.isEnabled &&
167
- stepExecution.generateAttachments.runAttachmentMode !== 'planOnly'
178
+ stepExecution.generateAttachments.runAttachmentMode !== 'planOnly'
168
179
  ? runResults.filter((result) => result.iteration?.attachments?.length > 0)
169
180
  : [];
170
181
  const mappedAnalysisResultData =
@@ -181,7 +192,7 @@ export default class ResultDataProvider {
181
192
  });
182
193
  }
183
194
 
184
- return combinedResults;
195
+ return { combinedResults, openPcrToTestCaseTraceMap, testCaseToOpenPcrTraceMap };
185
196
  } catch (error: any) {
186
197
  logger.error(`Error during getCombinedResultsSummary: ${error.message}`);
187
198
  if (error.response) {
@@ -209,6 +220,7 @@ export default class ResultDataProvider {
209
220
  projectName: string,
210
221
  selectedSuiteIds: number[],
211
222
  selectedFields: string[],
223
+ allowCrossTestPlan: boolean,
212
224
  enableRunTestCaseFilter: boolean,
213
225
  enableRunStepStatusFilter: boolean
214
226
  ) {
@@ -218,17 +230,21 @@ export default class ResultDataProvider {
218
230
  );
219
231
  logger.debug(`Selected suite IDs: ${selectedSuiteIds}`);
220
232
  try {
233
+ logger.debug(`Fetching Plan info for test plan ID: ${testPlanId}, project name: ${projectName}`);
221
234
  const plan = await this.fetchTestPlanName(testPlanId, projectName);
235
+ logger.debug(`Fetching Test suites for test plan ID: ${testPlanId}, project name: ${projectName}`);
222
236
  const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
223
- const testData = await this.fetchTestData(suites, projectName, testPlanId);
237
+ logger.debug(`Fetching test data for test plan ID: ${testPlanId}, project name: ${projectName}`);
238
+ const testData = await this.fetchTestData(suites, projectName, testPlanId, allowCrossTestPlan);
239
+ logger.debug(`Fetching Run results for test data, project name: ${projectName}`);
224
240
  const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, selectedFields);
241
+ logger.debug(`Aligning steps with iterations for test reporter results`);
225
242
  const testReporterData = this.alignStepsWithIterationsTestReporter(
226
243
  testData,
227
244
  runResults,
228
245
  selectedFields,
229
246
  !enableRunTestCaseFilter
230
247
  );
231
-
232
248
  // Apply filters sequentially based on enabled flags
233
249
  let filteredResults = testReporterData;
234
250
 
@@ -406,13 +422,85 @@ export default class ResultDataProvider {
406
422
  : `${parts[1]}/${parts[parts.length - 1]}`;
407
423
  }
408
424
 
425
+ private async fetchCrossTestPoints(projectName: string, testCaseIds: any[]): Promise<any[]> {
426
+ try {
427
+ const url = `${this.orgUrl}${projectName}/_apis/test/points?api-version=6.0`
428
+ if (testCaseIds.length === 0) {
429
+ return []
430
+ }
431
+
432
+ const requestBody: any = {
433
+ PointsFilter: {
434
+ TestcaseIds: testCaseIds,
435
+ },
436
+ };
437
+
438
+ const { data: value } = await TFSServices.postRequest(url, this.token, 'Post', requestBody, null)
439
+ if (!value || !value.points || !Array.isArray(value.points)) {
440
+ logger.warn("No test points found or invalid response format");
441
+ return [];
442
+ }
443
+
444
+ // Group test points by test case ID
445
+ const pointsByTestCase = new Map();
446
+ value.points.forEach((point: any) => {
447
+ const testCaseId = point.testCase.id;
448
+
449
+ if (!pointsByTestCase.has(testCaseId)) {
450
+ pointsByTestCase.set(testCaseId, []);
451
+ }
452
+
453
+ pointsByTestCase.get(testCaseId).push(point);
454
+ });
455
+
456
+ // For each test case, find the point with the most recent run
457
+ const latestPoints: any[] = [];
458
+
459
+ for (const [testCaseId, points] of pointsByTestCase.entries()) {
460
+ // Sort by lastTestRun.id (descending), then by lastResult.id (descending)
461
+ const sortedPoints = points.sort((a: any, b: any) => {
462
+ // Parse IDs as numbers for proper comparison
463
+ const aRunId = parseInt(a.lastTestRun?.id || '0');
464
+ const bRunId = parseInt(b.lastTestRun?.id || '0');
465
+
466
+ if (aRunId !== bRunId) {
467
+ return bRunId - aRunId; // Sort by run ID first (descending)
468
+ }
469
+
470
+ const aResultId = parseInt(a.lastResult?.id || '0');
471
+ const bResultId = parseInt(b.lastResult?.id || '0');
472
+ return bResultId - aResultId; // Then by result ID (descending)
473
+ });
474
+
475
+ // Take the first item (most recent)
476
+ latestPoints.push(sortedPoints[0]);
477
+ }
478
+
479
+ // Fetch detailed information for each test point and map to required format
480
+ const detailedPoints = await Promise.all(
481
+ latestPoints.map(async (point: any) => {
482
+ const url = `${point.url}?witFields=Microsoft.VSTS.TCM.Steps&includePointDetails=true`
483
+ const detailedPoint = await TFSServices.getItemContent(url, this.token);
484
+ return this.mapTestPointForCrossPlans(detailedPoint, projectName);
485
+ // return this.mapTestPointForCrossPlans(detailedPoint, projectName);
486
+ })
487
+ );
488
+ return detailedPoints
489
+ } catch (err: any) {
490
+ logger.error(`Error during fetching Cross Test Points: ${err.message}`);
491
+ logger.error(`Error stack: ${err.stack}`);
492
+ return [];
493
+ }
494
+
495
+ }
496
+
409
497
  /**
410
498
  * Fetches test points by suite ID.
411
499
  */
412
500
  private async fetchTestPoints(
413
501
  projectName: string,
414
502
  testPlanId: string,
415
- testSuiteId: string
503
+ testSuiteId: string,
416
504
  ): Promise<any[]> {
417
505
  try {
418
506
  const url = `${this.orgUrl}${projectName}/_apis/testplan/Plans/${testPlanId}/Suites/${testSuiteId}/TestPoint?includePointDetails=true`;
@@ -441,6 +529,39 @@ export default class ResultDataProvider {
441
529
  };
442
530
  }
443
531
 
532
+
533
+ /**
534
+ * Maps raw test point data to a simplified object.
535
+ */
536
+ private mapTestPointForCrossPlans(testPoint: any, projectName: string): any {
537
+ return {
538
+ testCaseId: testPoint.testCase.id,
539
+ testCaseName: testPoint.testCase.name,
540
+ testCaseUrl: `${this.orgUrl}${projectName}/_workitems/edit/${testPoint.testCase.id}`,
541
+ configurationName: testPoint.configuration?.name,
542
+ outcome: testPoint.outcome || 'Not Run',
543
+ lastRunId: testPoint.lastTestRun?.id,
544
+ lastResultId: testPoint.lastResult?.id,
545
+ lastResultDetails: testPoint.lastResultDetails || {
546
+ "duration": 0,
547
+ "dateCompleted": "0000-00-00T00:00:00.000Z",
548
+ "runBy": { "displayName": "No tester", "id": "00000000-0000-0000-0000-000000000000" }
549
+ },
550
+ };
551
+ }
552
+
553
+ // Helper method to get all test points for a test case
554
+ async getTestPointsForTestCases(projectName: string, testCaseId: string[]): Promise<any> {
555
+ const url = `${this.orgUrl}${projectName}/_apis/test/points`;
556
+ const requestBody = {
557
+ PointsFilter: {
558
+ TestcaseIds: testCaseId,
559
+ },
560
+ };
561
+
562
+ return await TFSServices.postRequest(url, this.token, 'Post', requestBody, null);
563
+ }
564
+
444
565
  /**
445
566
  * Fetches test cases by suite ID.
446
567
  */
@@ -464,6 +585,10 @@ export default class ResultDataProvider {
464
585
  selectedFields?: string[]
465
586
  ): Promise<any> {
466
587
  try {
588
+ if (runId === '0' || resultId === '0') {
589
+ logger.warn(`Invalid runId or resultId: ${runId}, ${resultId}`);
590
+ return null;
591
+ }
467
592
  const url = `${this.orgUrl}${projectName}/_apis/test/runs/${runId}/results/${resultId}?detailsToInclude=Iterations`;
468
593
  const resultData = await TFSServices.getItemContent(url, this.token);
469
594
 
@@ -478,9 +603,6 @@ export default class ResultDataProvider {
478
603
  let relatedRequirements: any[] = [];
479
604
  let relatedBugs: any[] = [];
480
605
  let relatedCRs: any[] = [];
481
- //TODO: Add CR support as well, and also add the logic to fetch the CR details
482
- // TODO: Add logic for grabbing the relations from cross projects
483
-
484
606
  // Process selected fields if provided
485
607
  if (selectedFields?.length && isTestReporter) {
486
608
  const filtered = selectedFields
@@ -610,8 +732,8 @@ export default class ResultDataProvider {
610
732
  return actionResult.outcome === 'Unspecified'
611
733
  ? 'Not Run'
612
734
  : actionResult.outcome !== 'Not Run'
613
- ? actionResult.outcome
614
- : '';
735
+ ? actionResult.outcome
736
+ : '';
615
737
  }
616
738
 
617
739
  /**
@@ -645,6 +767,7 @@ export default class ResultDataProvider {
645
767
  }
646
768
  ): any[] {
647
769
  const detailedResults: any[] = [];
770
+
648
771
  if (!iterations || iterations?.length === 0) {
649
772
  return detailedResults;
650
773
  }
@@ -658,7 +781,7 @@ export default class ResultDataProvider {
658
781
 
659
782
  for (const testItem of testData) {
660
783
  for (const point of testItem.testPointsItems) {
661
- const testCase = testItem.testCasesItems.find((tc: any) => tc.workItem.id === point.testCaseId);
784
+ const testCase = testItem.testCasesItems.find((tc: any) => Number(tc.workItem.id) === Number(point.testCaseId));
662
785
  if (!testCase) continue;
663
786
 
664
787
  if (testCase.workItem.workItemFields.length === 0) {
@@ -747,13 +870,13 @@ export default class ResultDataProvider {
747
870
  resultObjectsToAdd.length > 0
748
871
  ? detailedResults.push(...resultObjectsToAdd)
749
872
  : detailedResults.push(
750
- options.createResultObject({
751
- testItem,
752
- point,
753
- fetchedTestCase,
754
- filteredFields,
755
- })
756
- );
873
+ options.createResultObject({
874
+ testItem,
875
+ point,
876
+ fetchedTestCase,
877
+ filteredFields,
878
+ })
879
+ );
757
880
  }
758
881
  }
759
882
 
@@ -818,17 +941,25 @@ export default class ResultDataProvider {
818
941
  /**
819
942
  * Fetches test data for all suites, including test points and test cases.
820
943
  */
821
- private async fetchTestData(suites: any[], projectName: string, testPlanId: string): Promise<any[]> {
944
+ private async fetchTestData(
945
+ suites: any[],
946
+ projectName: string,
947
+ testPlanId: string,
948
+ fetchCrossPlans: boolean = false
949
+ ): Promise<any[]> {
822
950
  return await Promise.all(
823
951
  suites.map((suite) =>
824
952
  this.limit(async () => {
825
953
  try {
826
- const testPointsItems = await this.fetchTestPoints(projectName, testPlanId, suite.testSuiteId);
954
+
827
955
  const testCasesItems = await this.fetchTestCasesBySuiteId(
828
956
  projectName,
829
957
  testPlanId,
830
958
  suite.testSuiteId
831
959
  );
960
+ const testCaseIds = testCasesItems.map((testCase: any) => testCase.workItem.id);
961
+ const testPointsItems = !fetchCrossPlans ? await this.fetchTestPoints(projectName, testPlanId, suite.testSuiteId) : await this.fetchCrossTestPoints(projectName, testCaseIds);
962
+
832
963
  return { ...suite, testPointsItems, testCasesItems };
833
964
  } catch (error: any) {
834
965
  logger.error(`Error occurred for suite ${suite.testSuiteId}: ${error.message}`);
@@ -921,6 +1052,13 @@ export default class ResultDataProvider {
921
1052
  additionalArgs: any[] = []
922
1053
  ): Promise<any> {
923
1054
  const { lastRunId, lastResultId } = point;
1055
+ if (!lastRunId || !lastResultId) {
1056
+ logger.warn(`Invalid lastRunId or lastResultId for point: ${JSON.stringify(point)}`);
1057
+ return null;
1058
+ } else if (lastRunId === '0' || lastResultId === '0') {
1059
+ logger.warn(`Invalid lastRunId or lastResultId: ${lastRunId}, ${lastResultId}`);
1060
+ return null;
1061
+ }
924
1062
  const resultData = await fetchResultMethod(
925
1063
  projectName,
926
1064
  lastRunId.toString(),
@@ -1099,21 +1237,32 @@ export default class ResultDataProvider {
1099
1237
  * Fetching Open PCRs data
1100
1238
  */
1101
1239
 
1102
- private async fetchOpenPcrData(testItems: any[], projectName: string, combinedResults: any[]) {
1240
+ private async fetchOpenPcrData(
1241
+ testItems: any[],
1242
+ projectName: string,
1243
+ openPcrToTestCaseTraceMap: Map<string, string[]>,
1244
+ testCaseToOpenPcrTraceMap: Map<string, string[]>
1245
+ ) {
1103
1246
  const linkedWorkItems = await this.fetchLinkedWi(projectName, testItems);
1104
- const flatOpenPcrsItems = linkedWorkItems
1105
- .filter((item) => item.linkItems.length > 0)
1106
- .flatMap((item) => {
1107
- const { linkItems, ...restItem } = item;
1108
- return linkItems.map((linkedItem: any) => ({ ...restItem, ...linkedItem }));
1109
- });
1110
- if (flatOpenPcrsItems?.length > 0) {
1111
- // Add openPCR to combined results
1112
- combinedResults.push({
1113
- contentControl: 'open-pcr-content-control',
1114
- data: flatOpenPcrsItems,
1115
- skin: 'open-pcr-table',
1247
+ for (const wi of linkedWorkItems) {
1248
+ const { linkItems, ...restItem } = wi;
1249
+ const stringifiedTestCase = JSON.stringify({
1250
+ id: restItem.testId,
1251
+ title: restItem.testName,
1252
+ testCaseUrl: restItem.testCaseUrl,
1253
+ runStatus: restItem.runStatus,
1116
1254
  });
1255
+ for (const linkedItem of linkItems) {
1256
+ const stringifiedPcr = JSON.stringify({
1257
+ pcrId: linkedItem.pcrId,
1258
+ workItemType: linkedItem.workItemType,
1259
+ title: linkedItem.title,
1260
+ severity: linkedItem.severity,
1261
+ pcrUrl: linkedItem.pcrUrl,
1262
+ });
1263
+ DataProviderUtils.addToTraceMap(openPcrToTestCaseTraceMap, stringifiedPcr, stringifiedTestCase);
1264
+ DataProviderUtils.addToTraceMap(testCaseToOpenPcrTraceMap, stringifiedTestCase, stringifiedPcr);
1265
+ }
1117
1266
  }
1118
1267
  }
1119
1268
 
@@ -1323,6 +1472,7 @@ export default class ResultDataProvider {
1323
1472
  testGroupName: testPoint.testGroupName,
1324
1473
  testId: testPoint.testCaseId,
1325
1474
  testName: testPoint.testCaseName,
1475
+ testCaseUrl: testPoint.testCaseUrl,
1326
1476
  runStatus: !includeHardCopyRun ? this.convertRunStatus(testPoint.outcome) : '',
1327
1477
  };
1328
1478
 
@@ -4,20 +4,21 @@ import { TestSteps, createMomRelation, createRequirementRelation } from '../mode
4
4
  import { TestCase } from '../models/tfs-data';
5
5
  import * as xml2js from 'xml2js';
6
6
  import logger from '../utils/logger';
7
- import TestStepParserHelper from '../utils/testStepParserHelper';
7
+ import Utils from '../utils/testStepParserHelper';
8
+ import DataProviderUtils from '../utils/DataProviderUtils';
8
9
  const pLimit = require('p-limit');
9
10
 
10
11
  export default class TestDataProvider {
11
12
  orgUrl: string = '';
12
13
  token: string = '';
13
- private testStepParserHelper: TestStepParserHelper;
14
+ private testStepParserHelper: Utils;
14
15
  private cache = new Map<string, any>(); // Cache for API responses
15
16
  private limit = pLimit(10);
16
17
 
17
18
  constructor(orgUrl: string, token: string) {
18
19
  this.orgUrl = orgUrl;
19
20
  this.token = token;
20
- this.testStepParserHelper = new TestStepParserHelper(orgUrl, token);
21
+ this.testStepParserHelper = new Utils(orgUrl, token);
21
22
  }
22
23
 
23
24
  private async fetchWithCache(url: string, ttlMs = 60000): Promise<any> {
@@ -226,10 +227,14 @@ export default class TestDataProvider {
226
227
  const stringifiedRequirement = JSON.stringify(newRequirementRelation);
227
228
 
228
229
  // Add the test case to the requirement-to-test-case trace map
229
- this.addToMap(requirementToTestCaseTraceMap, stringifiedRequirement, stringifiedTestCase);
230
+ DataProviderUtils.addToTraceMap(
231
+ requirementToTestCaseTraceMap,
232
+ stringifiedRequirement,
233
+ stringifiedTestCase
234
+ );
230
235
 
231
236
  // Add the requirement to the test-case-to-requirements trace map
232
- this.addToMap(
237
+ DataProviderUtils.addToTraceMap(
233
238
  testCaseToRequirementsTraceMap,
234
239
  stringifiedTestCase,
235
240
  stringifiedRequirement
@@ -120,9 +120,11 @@ export default class TicketsDataProvider {
120
120
  logger.debug(`doctype: ${docType}`);
121
121
  switch (docType) {
122
122
  case 'STD':
123
- return await this.fetchLinkedQueries(queries, false);
123
+ return await this.fetchLinkedReqTestQueries(queries, false);
124
124
  case 'STR':
125
- return await this.fetchLinkedQueries(queries, true);
125
+ const reqTestTrees = await this.fetchLinkedReqTestQueries(queries, false);
126
+ const openPcrTestTrees = await this.fetchLinkedOpenPcrTestQueries(queries, false);
127
+ return { reqTestTrees, openPcrTestTrees };
126
128
  case 'SVD':
127
129
  return await this.fetchAnyQueries(queries);
128
130
  default:
@@ -140,13 +142,41 @@ export default class TicketsDataProvider {
140
142
  * @param onlyTestReq get only test req
141
143
  * @returns ReqTestTree and TestReqTree
142
144
  */
143
- private async fetchLinkedQueries(queries: any, onlyTestReq: boolean = false) {
144
- const { tree1: reqTestTree, tree2: testReqTree } = await this.structureReqTestQueries(
145
+ private async fetchLinkedReqTestQueries(queries: any, onlyTestReq: boolean = false) {
146
+ const { tree1: reqTestTree, tree2: testReqTree } = await this.structureFetchedQueries(
145
147
  queries,
146
- onlyTestReq
148
+ onlyTestReq,
149
+ null,
150
+ ['Requirement'],
151
+ ['Test Case']
147
152
  );
148
153
  return { reqTestTree, testReqTree };
149
154
  }
155
+
156
+ /**
157
+ * Fetches and structures linked queries related to open PCR (Problem Change Request) tests.
158
+ *
159
+ * This method retrieves and organizes the relationships between "Test Case" and
160
+ * other entities such as "Bug" and "Change Request" into two tree structures.
161
+ *
162
+ * @param queries - The input queries to be processed and structured.
163
+ * @param onlySourceSide - A flag indicating whether to process only the source side of the queries.
164
+ * Defaults to `false`.
165
+ * @returns An object containing two tree structures:
166
+ * - `OpenPcrToTestTree`: The tree representing the relationship from Open PCR to Test Case.
167
+ * - `TestToOpenPcrTree`: The tree representing the relationship from Test Case to Open PCR.
168
+ */
169
+ private async fetchLinkedOpenPcrTestQueries(queries: any, onlySourceSide: boolean = false) {
170
+ const { tree1: OpenPcrToTestTree, tree2: TestToOpenPcrTree } = await this.structureFetchedQueries(
171
+ queries,
172
+ onlySourceSide,
173
+ null,
174
+ ['Bug', 'Change Request'],
175
+ ['Test Case']
176
+ );
177
+ return { OpenPcrToTestTree, TestToOpenPcrTree };
178
+ }
179
+
150
180
  private async fetchAnyQueries(queries: any) {
151
181
  const { tree1: systemOverviewQueryTree, tree2: knownBugsQueryTree } = await this.structureAllQueryPath(
152
182
  queries
@@ -756,12 +786,25 @@ export default class TicketsDataProvider {
756
786
  }
757
787
 
758
788
  /**
759
- * Recursively structures the query list for the requirement to test case and test case to requirement queries
789
+ * Recursively structures fetched queries into two hierarchical trees (tree1 and tree2)
790
+ * based on specific conditions. It processes both leaf and non-leaf nodes, fetching
791
+ * children if necessary, and builds the trees by matching source and target conditions.
792
+ *
793
+ * @param rootQuery - The root query object to process. It may contain children or be a leaf node.
794
+ * @param onlyTestReq - A boolean flag indicating whether to exclude requirement-to-test-case queries.
795
+ * @param parentId - The ID of the parent node, used to maintain the hierarchy. Defaults to `null`.
796
+ * @param sources - An array of source strings used to match queries.
797
+ * @param targets - An array of target strings used to match queries.
798
+ * @returns A promise that resolves to an object containing two trees (`tree1` and `tree2`),
799
+ * or `null` for each tree if no valid nodes are found.
800
+ * @throws Logs an error if an exception occurs during processing.
760
801
  */
761
- private async structureReqTestQueries(
802
+ private async structureFetchedQueries(
762
803
  rootQuery: any,
763
804
  onlyTestReq: boolean,
764
- parentId: any = null
805
+ parentId: any = null,
806
+ sources: string[],
807
+ targets: string[]
765
808
  ): Promise<any> {
766
809
  try {
767
810
  if (!rootQuery.hasChildren) {
@@ -770,7 +813,7 @@ export default class TicketsDataProvider {
770
813
  let tree1Node = null;
771
814
  let tree2Node = null;
772
815
  // Check if the query is a requirement to test case query
773
- if (!onlyTestReq && this.matchesReqTestCondition(wiql)) {
816
+ if (!onlyTestReq && this.matchesSourceTargetCondition(wiql, sources, targets)) {
774
817
  tree1Node = {
775
818
  id: rootQuery.id,
776
819
  pId: parentId,
@@ -781,7 +824,7 @@ export default class TicketsDataProvider {
781
824
  isValidQuery: true,
782
825
  };
783
826
  }
784
- if (this.matchesTestReqCondition(wiql)) {
827
+ if (this.matchesSourceTargetCondition(wiql, targets, sources)) {
785
828
  tree2Node = {
786
829
  id: rootQuery.id,
787
830
  pId: parentId,
@@ -802,13 +845,15 @@ export default class TicketsDataProvider {
802
845
  const queryUrl = `${rootQuery.url}?$depth=2&$expand=all`;
803
846
  const currentQuery = await TFSServices.getItemContent(queryUrl, this.token);
804
847
  return currentQuery
805
- ? await this.structureReqTestQueries(currentQuery, onlyTestReq, currentQuery.id)
848
+ ? await this.structureFetchedQueries(currentQuery, onlyTestReq, currentQuery.id, sources, targets)
806
849
  : { tree1: null, tree2: null };
807
850
  }
808
851
 
809
852
  // Process children recursively
810
853
  const childResults = await Promise.all(
811
- rootQuery.children.map((child: any) => this.structureReqTestQueries(child, onlyTestReq, rootQuery.id))
854
+ rootQuery.children.map((child: any) =>
855
+ this.structureFetchedQueries(child, onlyTestReq, rootQuery.id, sources, targets)
856
+ )
812
857
  );
813
858
 
814
859
  // Build tree1
@@ -848,20 +893,36 @@ export default class TicketsDataProvider {
848
893
  }
849
894
  }
850
895
 
851
- // check if the query is a requirement to test case query
852
- private matchesReqTestCondition(wiql: string): boolean {
853
- return (
854
- wiql.includes("Source.[System.WorkItemType] = 'Requirement'") &&
855
- wiql.includes("Target.[System.WorkItemType] = 'Test Case'")
856
- );
857
- }
896
+ /**
897
+ * Determines whether the given WIQL (Work Item Query Language) string matches the specified
898
+ * source and target conditions. It checks if the WIQL contains references to the specified
899
+ * source and target work item types.
900
+ *
901
+ * @param wiql - The WIQL string to evaluate.
902
+ * @param source - An array of source work item types to check for in the WIQL.
903
+ * @param target - An array of target work item types to check for in the WIQL.
904
+ * @returns A boolean indicating whether the WIQL includes at least one source work item type
905
+ * and at least one target work item type.
906
+ */
907
+ private matchesSourceTargetCondition(wiql: string, source: string[], target: string[]): boolean {
908
+ let isSourceIncluded = false;
909
+ let isTargetIncluded = false;
910
+
911
+ for (const src of source) {
912
+ if (wiql.includes(`Source.[System.WorkItemType] = '${src}'`)) {
913
+ isSourceIncluded = true;
914
+ break;
915
+ }
916
+ }
858
917
 
859
- // check if the query is a test case to requirement query
860
- private matchesTestReqCondition(wiql: string): boolean {
861
- return (
862
- wiql.includes("Source.[System.WorkItemType] = 'Test Case'") &&
863
- wiql.includes("Target.[System.WorkItemType] = 'Requirement'")
864
- );
918
+ for (const tgt of target) {
919
+ if (wiql.includes(`Target.[System.WorkItemType] = '${tgt}'`)) {
920
+ isTargetIncluded = true;
921
+ break;
922
+ }
923
+ }
924
+
925
+ return isSourceIncluded && isTargetIncluded;
865
926
  }
866
927
 
867
928
  // check if the query is a bug query