@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.
- package/bin/models/tfs-data.d.ts +4 -3
- package/bin/models/tfs-data.js.map +1 -1
- package/bin/modules/ResultDataProvider.d.ts +3 -5
- package/bin/modules/ResultDataProvider.js +121 -107
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.d.ts +3 -2
- package/bin/modules/TestDataProvider.js +12 -7
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.js +2 -0
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/utils/testStepParserHelper.d.ts +11 -0
- package/bin/utils/testStepParserHelper.js +116 -0
- package/bin/utils/testStepParserHelper.js.map +1 -0
- package/package.json +1 -1
- package/src/models/tfs-data.ts +4 -3
- package/src/modules/ResultDataProvider.ts +146 -126
- package/src/modules/TestDataProvider.ts +16 -7
- package/src/modules/TicketsDataProvider.ts +2 -0
- package/src/utils/testStepParserHelper.ts +165 -0
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
418
|
+
* @param testItems Test cases
|
|
429
419
|
* @returns Array of linked Work Items
|
|
430
420
|
*/
|
|
431
|
-
private async fetchLinkedWi(project: string,
|
|
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
|
|
430
|
+
const getLinkedWisUrl = `${this.orgUrl}${project}/_apis/wit/workItems?ids=${testIdsString}&$expand=relations`;
|
|
435
431
|
|
|
436
432
|
// Fetch linked work items
|
|
437
|
-
const {
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
459
|
+
// Fetch work item details
|
|
460
|
+
const { value: relatedWorkItems } = await TFSServices.getItemContent(
|
|
461
|
+
getRelatedWiDataUrl,
|
|
462
|
+
this.token
|
|
463
|
+
);
|
|
452
464
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
481
|
+
const mappedFilteredWi = filteredWi.length > 0 ? this.MapLinkedWorkItem(filteredWi, project) : [];
|
|
482
|
+
summarizedItemMap.set(item.id, { ...item, linkItems: mappedFilteredWi });
|
|
483
|
+
}
|
|
463
484
|
}
|
|
464
485
|
|
|
465
|
-
|
|
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
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
+
}
|