@elisra-devops/docgen-data-provider 1.3.0 → 1.3.2

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,16 +1,17 @@
1
1
  import { TFSServices } from '../helpers/tfs';
2
- import { TestSteps } from '../models/tfs-data';
2
+ import { TestSteps, Workitem } from '../models/tfs-data';
3
3
  import * as xml2js from 'xml2js';
4
4
  import logger from '../utils/logger';
5
- import { error, log } from 'console';
6
-
5
+ import TestStepParserHelper from '../utils/testStepParserHelper';
7
6
  export default class ResultDataProvider {
8
7
  orgUrl: string = '';
9
8
  token: string = '';
10
9
 
10
+ private testStepParserHelper: TestStepParserHelper;
11
11
  constructor(orgUrl: string, token: string) {
12
12
  this.orgUrl = orgUrl;
13
13
  this.token = token;
14
+ this.testStepParserHelper = new TestStepParserHelper(orgUrl, token);
14
15
  }
15
16
 
16
17
  /**
@@ -198,34 +199,6 @@ export default class ResultDataProvider {
198
199
  }
199
200
  }
200
201
 
201
- /**
202
- * Parses test steps from XML format into a structured array.
203
- */
204
- private parseTestSteps(xmlSteps: string): { stepsList: TestSteps[] } {
205
- const stepsList: TestSteps[] = [];
206
-
207
- xml2js.parseString(xmlSteps, { explicitArray: false }, (err, result) => {
208
- if (err) {
209
- logger.warn('Failed to parse XML test steps.');
210
- return;
211
- }
212
-
213
- if (result.steps === undefined || parseInt(result.steps.$.last) === 0) {
214
- return;
215
- }
216
- const stepsArray = Array.isArray(result.steps?.step) ? result.steps.step : [result.steps?.step];
217
- for (let i = 0; i < stepsArray.length; i++) {
218
- const stepObj = stepsArray[i];
219
- const step = new TestSteps();
220
- step.stepId = Number(stepObj.$.id);
221
- step.action = stepObj.parameterizedString?.[0]?._ || '';
222
- step.expected = stepObj.parameterizedString?.[1]?._ || '';
223
- stepsList.push(step);
224
- }
225
- });
226
- return { stepsList };
227
- }
228
-
229
202
  /**
230
203
  * Aligns test steps with their corresponding iterations.
231
204
  */
@@ -240,9 +213,6 @@ export default class ResultDataProvider {
240
213
  const testCase = testItem.testCasesItems.find((tc: any) => tc.workItem.id === point.testCaseId);
241
214
  if (!testCase) continue;
242
215
  const iterationsMap = this.createIterationsMap(iterations, testCase.workItem.id);
243
-
244
- logger.debug(`parsing data for test case Id ${point.testCaseId}`);
245
-
246
216
  if (testCase.workItem.workItemFields.length === 0) {
247
217
  logger.warn(`Could not fetch the steps from WI ${JSON.stringify(testCase.workItem.id)}`);
248
218
  continue;
@@ -263,7 +233,7 @@ export default class ResultDataProvider {
263
233
  testCaseRevision: testCastObj.testCaseRevision,
264
234
  testName: point.testCaseName,
265
235
  stepIdentifier: stepIdentifier,
266
- stepNo: i + 1,
236
+ stepNo: actionResults[i].stepPosition,
267
237
  stepAction: actionResults[i].action,
268
238
  stepExpected: actionResults[i].expected,
269
239
  stepStatus:
@@ -275,31 +245,6 @@ export default class ResultDataProvider {
275
245
  stepComments: actionResults[i].errorMessage || '',
276
246
  };
277
247
 
278
- detailedResults.push(resultObj);
279
- }
280
- } else {
281
- const { stepsList: steps } = this.parseTestSteps(
282
- testCase.workItem.workItemFields[0]['Microsoft.VSTS.TCM.Steps']
283
- );
284
-
285
- if (steps.length === 0) {
286
- logger.warn(`No steps were found for WI ${testCase.workItem?.id}`);
287
- continue;
288
- }
289
- //In case of not tested
290
- logger.debug(`Test case ${point.testCaseId} does not have a test result`);
291
- for (let i = 0; i < steps.length; i++) {
292
- const resultObj = {
293
- testId: point.testCaseId,
294
- testName: point.testCaseName,
295
- stepIdentifier: steps[i].stepId,
296
- testCaseRevision: undefined,
297
- stepNo: i + 1,
298
- stepAction: steps[i].action,
299
- stepExpected: steps[i].expected,
300
- stepStatus: 'Not Run',
301
- stepComments: 'No Result',
302
- };
303
248
  detailedResults.push(resultObj);
304
249
  }
305
250
  }
@@ -374,22 +319,57 @@ export default class ResultDataProvider {
374
319
  return results;
375
320
  }
376
321
 
322
+ //Sorting step positions
323
+ private compareActionResults = (a: string, b: string) => {
324
+ const aParts = a.split('.').map(Number);
325
+ const bParts = b.split('.').map(Number);
326
+ const maxLength = Math.max(aParts.length, bParts.length);
327
+
328
+ for (let i = 0; i < maxLength; i++) {
329
+ const aNum = aParts[i] || 0; // Default to 0 if undefined
330
+ const bNum = bParts[i] || 0;
331
+
332
+ if (aNum > bNum) return 1;
333
+ if (aNum < bNum) return -1;
334
+ // If equal, continue to next segment
335
+ }
336
+
337
+ return 0; // Versions are equal
338
+ };
339
+
377
340
  /**
378
341
  * Fetches result Data data for a specific test point.
379
342
  */
380
343
  private async fetchResultData(projectName: string, testSuiteId: string, point: any) {
381
344
  const { lastRunId, lastResultId } = point;
382
345
  const resultData = await this.fetchResult(projectName, lastRunId.toString(), lastResultId.toString());
383
- logger.debug(
384
- `resultData is ${resultData ? 'available' : 'unavailable'} for run ${lastRunId} result ${lastResultId} `
385
- );
346
+
386
347
  const iteration =
387
348
  resultData.iterationDetails.length > 0
388
349
  ? resultData.iterationDetails[resultData.iterationDetails.length - 1]
389
350
  : undefined;
390
351
 
391
352
  if (resultData.stepsResultXml && iteration) {
392
- const { stepsList } = this.parseTestSteps(resultData.stepsResultXml);
353
+ const actionResultsWithSharedModels = iteration.actionResults.filter(
354
+ (result: any) => result.sharedStepModel
355
+ );
356
+ const actionResultsWithNoSharedModels = iteration.actionResults.filter(
357
+ (result: any) => !result.sharedStepModel
358
+ );
359
+ const sharedStepIdToRevisionLookupMap: Map<number, number> = new Map();
360
+
361
+ if (actionResultsWithSharedModels?.length > 0) {
362
+ actionResultsWithSharedModels.forEach((actionResult: any) => {
363
+ const { sharedStepModel } = actionResult;
364
+ sharedStepIdToRevisionLookupMap.set(Number(sharedStepModel.id), Number(sharedStepModel.revision));
365
+ });
366
+ }
367
+ const stepsList = await this.testStepParserHelper.parseTestSteps(
368
+ resultData.stepsResultXml,
369
+ sharedStepIdToRevisionLookupMap
370
+ );
371
+
372
+ sharedStepIdToRevisionLookupMap.clear();
393
373
 
394
374
  const stepMap = new Map<string, any>();
395
375
  for (const step of stepsList) {
@@ -397,12 +377,22 @@ export default class ResultDataProvider {
397
377
  }
398
378
 
399
379
  for (const actionResult of iteration.actionResults) {
380
+ //If the actions results holds a sharedStepModel then ignore it and continue
381
+ if (actionResult.sharedStepModel) {
382
+ continue;
383
+ }
384
+
400
385
  const step = stepMap.get(actionResult.stepIdentifier);
401
386
  if (step) {
387
+ actionResult.stepPosition = step.stepPosition;
402
388
  actionResult.action = step.action;
403
389
  actionResult.expected = step.expected;
404
390
  }
405
391
  }
392
+ //Sort by step position
393
+ iteration.actionResults = actionResultsWithNoSharedModels
394
+ .filter((result: any) => result.stepPosition)
395
+ .sort((a: any, b: any) => this.compareActionResults(a.stepPosition, b.stepPosition));
406
396
  }
407
397
  return resultData?.testCase
408
398
  ? {
@@ -425,56 +415,77 @@ export default class ResultDataProvider {
425
415
  /**
426
416
  * Fetches all the linked work items (WI) for the given test case.
427
417
  * @param project Project name
428
- * @param testCaseId Test case ID number
418
+ * @param testItems Test cases
429
419
  * @returns Array of linked Work Items
430
420
  */
431
- private async fetchLinkedWi(project: string, testCaseId: string): Promise<any[]> {
421
+ private async fetchLinkedWi(project: string, testItems: any[]): Promise<any[]> {
432
422
  try {
423
+ const summarizedItemMap: Map<number, any> = new Map();
424
+ const testIds = testItems.map((summeryItem) => {
425
+ summarizedItemMap.set(summeryItem.testId, { ...summeryItem, linkItems: [] });
426
+ return `${summeryItem.testId}`;
427
+ });
428
+ const testIdsString = testIds.join(',');
433
429
  // Construct URL to fetch linked work items
434
- const getLinkedWiUrl = `${this.orgUrl}${project}/_apis/wit/workItems/${testCaseId}?$expand=relations`;
430
+ const getLinkedWisUrl = `${this.orgUrl}${project}/_apis/wit/workItems?ids=${testIdsString}&$expand=relations`;
435
431
 
436
432
  // Fetch linked work items
437
- const { relations } = await TFSServices.getItemContent(getLinkedWiUrl, this.token);
433
+ const { value: workItems } = await TFSServices.getItemContent(getLinkedWisUrl, this.token);
434
+
435
+ if (workItems.length > 0) {
436
+ for (const wi of workItems) {
437
+ const { relations } = wi;
438
+ // Ensure relations is an array
439
+ const item = summarizedItemMap.get(wi.id);
440
+ if (!item) {
441
+ continue;
442
+ }
443
+ if (!Array.isArray(relations) || relations.length === 0) {
444
+ continue;
445
+ }
438
446
 
439
- // Ensure relations is an array
440
- if (!Array.isArray(relations) || relations.length === 0) {
441
- return [];
442
- }
447
+ const relationWorkItemsIds = relations
448
+ .filter((relation) => relation?.attributes?.name === 'Tests')
449
+ .map((relation) => relation.url.split('/').pop());
450
+
451
+ if (relationWorkItemsIds.length === 0) {
452
+ continue;
453
+ }
443
454
 
444
- // Filter relations by 'Tests' attribute and extract work item IDs
445
- const workItemIds = relations
446
- .filter((relation) => relation?.attributes?.name === 'Tests')
447
- .map((relation) => relation.url.split('/').pop());
455
+ // Construct URL to fetch work item details
456
+ const relatedWIIdsString = relationWorkItemsIds.join(',');
457
+ const getRelatedWiDataUrl = `${this.orgUrl}${project}/_apis/wit/workItems?ids=${relatedWIIdsString}&$expand=1`;
448
458
 
449
- if (workItemIds.length === 0) {
450
- return [];
451
- }
459
+ // Fetch work item details
460
+ const { value: relatedWorkItems } = await TFSServices.getItemContent(
461
+ getRelatedWiDataUrl,
462
+ this.token
463
+ );
452
464
 
453
- // Construct URL to fetch work item details
454
- const idsString = workItemIds.join(',');
455
- const getWiDataUrl = `${this.orgUrl}${project}/_apis/wit/workItems?ids=${idsString}&$expand=1`;
465
+ // Ensure workItems is an array
466
+ if (!Array.isArray(relatedWorkItems)) {
467
+ throw new Error('Unexpected format for work items data');
468
+ }
456
469
 
457
- // Fetch work item details
458
- const { value: workItems } = await TFSServices.getItemContent(getWiDataUrl, this.token);
470
+ // Filter work items based on type and state
471
+ const filteredWi = relatedWorkItems.filter(({ fields }) => {
472
+ const workItemType = fields?.['System.WorkItemType'];
473
+ const state = fields?.['System.State'];
474
+ return (
475
+ (workItemType === 'Change Request' || workItemType === 'Bug') &&
476
+ state !== 'Closed' &&
477
+ state !== 'Resolved'
478
+ );
479
+ });
459
480
 
460
- // Ensure workItems is an array
461
- if (!Array.isArray(workItems)) {
462
- throw new Error('Unexpected format for work items data');
481
+ const mappedFilteredWi = filteredWi.length > 0 ? this.MapLinkedWorkItem(filteredWi, project) : [];
482
+ summarizedItemMap.set(item.id, { ...item, linkItems: mappedFilteredWi });
483
+ }
463
484
  }
464
485
 
465
- // Filter work items based on type and state
466
- const filteredWi = workItems.filter(({ fields }) => {
467
- const workItemType = fields?.['System.WorkItemType'];
468
- const state = fields?.['System.State'];
469
- return (
470
- (workItemType === 'Change Request' || workItemType === 'Bug') &&
471
- state !== 'Closed' &&
472
- state !== 'Resolved'
473
- );
474
- });
486
+ return [...summarizedItemMap.values()];
475
487
 
476
488
  // Return mapped work items if any exist
477
- return filteredWi.length > 0 ? this.MapLinkedWorkItem(filteredWi, project) : [];
478
489
  } catch (error) {
479
490
  logger.error('Error fetching linked work items:', error);
480
491
  return []; // Return an empty array or handle it as needed
@@ -506,27 +517,13 @@ export default class ResultDataProvider {
506
517
  */
507
518
 
508
519
  private async fetchOpenPcrData(testItems: any[], projectName: string, combinedResults: any[]) {
509
- const linkedWorkItemsPromises = testItems.map((summaryItem) =>
510
- this.fetchLinkedWi(projectName, summaryItem.testId)
511
- .then((linkItems) => ({
512
- ...summaryItem,
513
- linkItems,
514
- }))
515
- .catch((error: any) => {
516
- logger.error(`Error occurred for testCase ${summaryItem.testId}: ${error.message}`);
517
- return { ...summaryItem, linkItems: [] };
518
- })
519
- );
520
-
521
- const linkedWorkItems = await Promise.all(linkedWorkItemsPromises);
522
-
520
+ const linkedWorkItems = await this.fetchLinkedWi(projectName, testItems);
523
521
  const flatOpenPcrsItems = linkedWorkItems
524
522
  .filter((item) => item.linkItems.length > 0)
525
523
  .flatMap((item) => {
526
524
  const { linkItems, ...restItem } = item;
527
525
  return linkItems.map((linkedItem: any) => ({ ...restItem, ...linkedItem }));
528
526
  });
529
-
530
527
  if (flatOpenPcrsItems?.length > 0) {
531
528
  // Add openPCR to combined results
532
529
  combinedResults.push({
@@ -749,19 +746,42 @@ export default class ResultDataProvider {
749
746
  }
750
747
  }
751
748
 
752
- private mapStepResultsForExecutionAppendix(detailedResults: any[]): any {
753
- return detailedResults?.length > 0
754
- ? detailedResults.map((result) => {
755
- return {
756
- testId: result.testId,
757
- testCaseRevision: result.testCaseRevision || undefined,
758
- stepNo: result.stepNo,
759
- stepIdentifier: result.stepIdentifier,
760
- stepStatus: result.stepStatus,
761
- stepComments: result.stepComments,
762
- };
763
- })
764
- : [];
749
+ // private mapStepResultsForExecutionAppendix(detailedResults: any[]): any {
750
+ // return detailedResults?.length > 0
751
+ // ? detailedResults.map((result) => {
752
+ // return {
753
+ // testId: result.testId,
754
+ // testCaseRevision: result.testCaseRevision || undefined,
755
+ // stepNo: result.stepNo,
756
+ // stepIdentifier: result.stepIdentifier,
757
+ // stepStatus: result.stepStatus,
758
+ // stepComments: result.stepComments,
759
+ // };
760
+ // })
761
+ // : [];
762
+ // }
763
+
764
+ private mapStepResultsForExecutionAppendix(detailedResults: any[]): Map<string, any> {
765
+ let testCaseIdToStepsMap: Map<string, any> = new Map();
766
+ detailedResults.forEach((result) => {
767
+ const testCaseRevision = result.testCaseRevision;
768
+ if (!testCaseIdToStepsMap.has(result.testId.toString())) {
769
+ testCaseIdToStepsMap.set(result.testId.toString(), { testCaseRevision, stepList: [] });
770
+ }
771
+ const value = testCaseIdToStepsMap.get(result.testId.toString());
772
+ const { stepList } = value;
773
+ stepList.push({
774
+ stepPosition: result.stepNo,
775
+ stepIdentifier: result.stepIdentifier,
776
+ action: result.stepAction,
777
+ expected: result.stepExpected,
778
+ stepStatus: result.stepStatus,
779
+ stepComments: result.stepComments,
780
+ });
781
+ testCaseIdToStepsMap.set(result.testId.toString(), { ...testCaseRevision, stepList: stepList });
782
+ });
783
+
784
+ return testCaseIdToStepsMap;
765
785
  }
766
786
 
767
787
  /**
@@ -10,14 +10,17 @@ import { TestCase } from '../models/tfs-data';
10
10
  import * as xml2js from 'xml2js';
11
11
 
12
12
  import logger from '../utils/logger';
13
+ import TestStepParserHelper from '../utils/testStepParserHelper';
13
14
 
14
15
  export default class TestDataProvider {
15
16
  orgUrl: string = '';
16
17
  token: string = '';
18
+ private testStepParserHelper: TestStepParserHelper;
17
19
 
18
20
  constructor(orgUrl: string, token: string) {
19
21
  this.orgUrl = orgUrl;
20
22
  this.token = token;
23
+ this.testStepParserHelper = new TestStepParserHelper(orgUrl, token);
21
24
  }
22
25
 
23
26
  async GetTestSuiteByTestCase(testCaseId: string): Promise<any> {
@@ -90,7 +93,7 @@ export default class TestDataProvider {
90
93
  CustomerRequirementId: boolean,
91
94
  includeBugs: boolean,
92
95
  includeSeverity: boolean,
93
- stepResultDetails?: any[]
96
+ stepResultDetailsMap?: Map<string, any>
94
97
  ): Promise<any> {
95
98
  let testCasesList: Array<any> = new Array<any>();
96
99
  const requirementToTestCaseTraceMap: Map<string, string[]> = new Map();
@@ -113,7 +116,7 @@ export default class TestDataProvider {
113
116
  includeSeverity,
114
117
  requirementToTestCaseTraceMap,
115
118
  testCaseToRequirementsTraceMap,
116
- stepResultDetails
119
+ stepResultDetailsMap
117
120
  );
118
121
 
119
122
  if (testCseseWithSteps.length > 0) testCasesList = [...testCasesList, ...testCseseWithSteps];
@@ -132,7 +135,7 @@ export default class TestDataProvider {
132
135
  includeSeverity: boolean,
133
136
  requirementToTestCaseTraceMap: Map<string, string[]>,
134
137
  testCaseToRequirementsTraceMap: Map<string, string[]>,
135
- stepResultDetails?: any[]
138
+ stepResultDetailsMap?: Map<string, any>
136
139
  ): Promise<Array<any>> {
137
140
  let url = this.orgUrl + project + '/_workitems/edit/';
138
141
  let testCasesUrlList: Array<any> = new Array<any>();
@@ -145,8 +148,7 @@ export default class TestDataProvider {
145
148
  for (let i = 0; i < testCases.count; i++) {
146
149
  try {
147
150
  let stepDetailObject =
148
- stepResultDetails?.find((result) => result.testId === Number(testCases.value[i].testCase.id)) ||
149
- undefined;
151
+ stepResultDetailsMap?.get(testCases.value[i].testCase.id.toString()) || undefined;
150
152
 
151
153
  let newurl = !stepDetailObject?.testCaseRevision
152
154
  ? testCases.value[i].testCase.url + '?$expand=All'
@@ -161,9 +163,16 @@ export default class TestDataProvider {
161
163
  //testCase.steps = test.fields["Microsoft.VSTS.TCM.Steps"];
162
164
  testCase.id = test.id;
163
165
  testCase.suit = suite.id;
164
- if (test.fields['Microsoft.VSTS.TCM.Steps'] != null) {
165
- let steps: Array<TestSteps> = this.ParseSteps(test.fields['Microsoft.VSTS.TCM.Steps']);
166
+
167
+ if (!stepDetailObject && test.fields['Microsoft.VSTS.TCM.Steps'] != null) {
168
+ let steps = await this.testStepParserHelper.parseTestSteps(
169
+ test.fields['Microsoft.VSTS.TCM.Steps'],
170
+ new Map<number, number>()
171
+ );
166
172
  testCase.steps = steps;
173
+ //In case its already parsed during the STR
174
+ } else if (stepDetailObject) {
175
+ testCase.steps = stepDetailObject.stepList;
167
176
  }
168
177
  if (test.relations) {
169
178
  for (const relation of test.relations) {
@@ -104,7 +104,9 @@ export default class TicketsDataProvider {
104
104
  if (path == '')
105
105
  url = `${this.orgUrl}${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all`;
106
106
  else url = `${this.orgUrl}${project}/_apis/wit/queries/${path}?$depth=2&$expand=all`;
107
+ logger.debug(`share query URL ${url}`);
107
108
  let queries: any = await TFSServices.getItemContent(url, this.token);
109
+ logger.debug(`share queries ${JSON.stringify(queries)}`);
108
110
 
109
111
  const { tree1: reqTestTree, tree2: testReqTree } = this.structureQueries(queries);
110
112
  return { reqTestTree, testReqTree };
@@ -0,0 +1,165 @@
1
+ import { TFSServices } from '../helpers/tfs';
2
+ import { TestSteps, Workitem } from '../models/tfs-data';
3
+ import * as xml2js from 'xml2js';
4
+ import logger from './logger';
5
+
6
+ export default class TestStepParserHelper {
7
+ private orgUrl: string;
8
+ private token: string;
9
+
10
+ constructor(orgUrl: string, token: string) {
11
+ this.orgUrl = orgUrl;
12
+ this.token = token;
13
+ }
14
+
15
+ private extractStepData(stepNode: any, stepPosition: string, parentStepId: string = ''): TestSteps {
16
+ const step = new TestSteps();
17
+ step.stepId = parentStepId === '' ? stepNode.$.id : `${parentStepId};${stepNode.$.id}`;
18
+ step.stepPosition = stepPosition;
19
+ step.action = stepNode.parameterizedString?.[0]?._ || '';
20
+ step.expected = stepNode.parameterizedString?.[1]?._ || '';
21
+ return step;
22
+ }
23
+
24
+ private async fetchSharedSteps(
25
+ sharedStepId: string,
26
+ sharedStepIdToRevisionLookupMap: Map<number, number>,
27
+ parentStepPosition: string,
28
+ parentStepId: string = ''
29
+ ): Promise<TestSteps[]> {
30
+ try {
31
+ let sharedStepsList: TestSteps[] = [];
32
+ const revision = sharedStepIdToRevisionLookupMap.get(Number(sharedStepId));
33
+
34
+ const wiUrl = revision
35
+ ? `${this.orgUrl}/_apis/wit/workitems/${sharedStepId}/revisions/${revision}`
36
+ : `${this.orgUrl}/_apis/wit/workitems/${sharedStepId}`;
37
+ const sharedStepWI = await TFSServices.getItemContent(wiUrl, this.token);
38
+
39
+ const stepsXML = sharedStepWI?.fields['Microsoft.VSTS.TCM.Steps'] || null;
40
+ if (stepsXML) {
41
+ const stepsList = await this.parseTestSteps(
42
+ stepsXML,
43
+ sharedStepIdToRevisionLookupMap,
44
+ `${parentStepPosition}.`,
45
+ parentStepId
46
+ );
47
+ sharedStepsList = stepsList;
48
+ }
49
+ return sharedStepsList;
50
+ } catch (err: any) {
51
+ const errorMsg = `failed to fetch shared step WI: ${err.message}`;
52
+ logger.error(errorMsg);
53
+ throw new Error(errorMsg);
54
+ }
55
+ }
56
+
57
+ private async processCompref(
58
+ comprefNode: any,
59
+ sharedStepIdToRevisionLookupMap: Map<number, number>,
60
+ stepPosition: string,
61
+ parentStepId: string = ''
62
+ ): Promise<TestSteps[]> {
63
+ const stepList: TestSteps[] = [];
64
+ if (comprefNode.$ && comprefNode.$.ref) {
65
+ //Nested Steps
66
+ const sharedStepId = comprefNode.$.ref;
67
+ const comprefStepId = comprefNode.$.id;
68
+ //Fetch the shared step data using the ID
69
+ const sharedSteps = await this.fetchSharedSteps(
70
+ sharedStepId,
71
+ sharedStepIdToRevisionLookupMap,
72
+ stepPosition,
73
+ comprefStepId
74
+ );
75
+ stepList.push(...sharedSteps);
76
+ }
77
+ // If 'compref' contains nested steps
78
+ if (comprefNode.children) {
79
+ for (const child of comprefNode.children) {
80
+ const nodeName = child['#name'];
81
+ const currentPosition = comprefNode.children.indexOf(child) + 1;
82
+ let currentPositionStr = '';
83
+ if (stepPosition.includes('.')) {
84
+ const positions = stepPosition.split('.');
85
+ const lastPosition = Number(positions.pop());
86
+ positions.push(`${currentPosition + lastPosition}`);
87
+ currentPositionStr = positions.join('.');
88
+ } else {
89
+ currentPositionStr = `${currentPosition + Number(stepPosition)}`;
90
+ }
91
+ if (nodeName === 'step') {
92
+ stepList.push(this.extractStepData(child, currentPositionStr, parentStepId));
93
+ } else if (nodeName === 'compref') {
94
+ // Handle nested 'compref' elements recursively
95
+ const nestedSteps = await this.processCompref(
96
+ child,
97
+ sharedStepIdToRevisionLookupMap,
98
+ currentPositionStr,
99
+ parentStepId
100
+ );
101
+ stepList.push(...nestedSteps);
102
+ }
103
+ }
104
+ }
105
+ return stepList;
106
+ }
107
+
108
+ private async processSteps(
109
+ parsedResultStepsXml: any,
110
+ sharedStepIdToRevisionLookupMap: Map<number, number>,
111
+ parentStepPosition: string,
112
+ parentStepId: string = ''
113
+ ): Promise<TestSteps[]> {
114
+ const stepsList: TestSteps[] = [];
115
+ const root = parsedResultStepsXml;
116
+
117
+ const children: any[] = root.children || [];
118
+ for (const child of children) {
119
+ const nodeName = child['#name'];
120
+ const currentStepPosition = children.indexOf(child) + 1;
121
+ if (nodeName === 'step') {
122
+ //Process a regular step
123
+ stepsList.push(
124
+ this.extractStepData(child, `${parentStepPosition}${currentStepPosition}`, parentStepId)
125
+ );
126
+ } else if (nodeName === 'compref') {
127
+ // Process shared steps
128
+ const sharedSteps = await this.processCompref(
129
+ child,
130
+ sharedStepIdToRevisionLookupMap,
131
+ `${parentStepPosition}${currentStepPosition}`,
132
+ parentStepId
133
+ );
134
+ stepsList.push(...sharedSteps);
135
+ }
136
+ }
137
+ return stepsList;
138
+ }
139
+
140
+ public async parseTestSteps(
141
+ xmlSteps: string,
142
+ sharedStepIdToRevisionLookupMap: Map<number, number>,
143
+ level: string = '',
144
+ parentStepId: string = ''
145
+ ): Promise<TestSteps[]> {
146
+ try {
147
+ const result = await xml2js.parseStringPromise(xmlSteps, {
148
+ explicitChildren: true,
149
+ preserveChildrenOrder: true,
150
+ explicitArray: false, // Prevents unnecessary arrays
151
+ childkey: 'children', // Key to store child elements
152
+ });
153
+ const stepsList = await this.processSteps(
154
+ result.steps,
155
+ sharedStepIdToRevisionLookupMap,
156
+ level,
157
+ parentStepId
158
+ );
159
+ return stepsList;
160
+ } catch (err) {
161
+ logger.error('Failed to parse XML test steps.', err);
162
+ return [];
163
+ }
164
+ }
165
+ }