@elisra-devops/docgen-data-provider 1.73.0 → 1.75.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.
@@ -30,14 +30,14 @@ const pLimit = require('p-limit');
30
30
  */
31
31
  export default class ResultDataProvider {
32
32
  private static readonly MEWP_L2_COVERAGE_COLUMNS = [
33
- 'Customer Requirement',
34
- 'Requirement ID',
35
- 'Requirement Title',
36
- 'Responsibility',
37
- 'SAPWBS / Responsibility',
33
+ 'Customer ID',
34
+ 'Title (Customer name)',
35
+ 'Responsibility - SAPWBS (ESUK/IL)',
36
+ 'Test case id',
37
+ 'Test case title',
38
38
  'Number of passed steps',
39
39
  'Number of failed steps',
40
- 'Number of steps not run',
40
+ 'Number of not run tests',
41
41
  ];
42
42
 
43
43
  orgUrl: string = '';
@@ -414,26 +414,28 @@ export default class ResultDataProvider {
414
414
  };
415
415
  }
416
416
 
417
- const requirementIndex = new Map<
418
- string,
419
- { passed: number; failed: number; notRun: number }
420
- >();
417
+ const requirementIndex = new Map<string, Map<number, { passed: number; failed: number; notRun: number }>>();
418
+ const observedTestCaseIdsByRequirement = new Map<string, Set<number>>();
421
419
  const requirementKeys = new Set<string>();
422
420
  requirements.forEach((requirement) => {
423
421
  const key = this.toRequirementKey(requirement.requirementId);
424
422
  if (!key) return;
425
423
  requirementKeys.add(key);
426
- if (!requirementIndex.has(key)) {
427
- requirementIndex.set(key, { passed: 0, failed: 0, notRun: 0 });
428
- }
429
424
  });
430
425
 
431
426
  const parsedDefinitionStepsByTestCase = new Map<number, TestSteps[]>();
432
427
  const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
428
+ const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
433
429
 
434
430
  const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
435
431
  for (const runResult of runResults) {
436
- const testCaseId = Number(runResult?.testCaseId);
432
+ const testCaseId = this.extractMewpTestCaseId(runResult);
433
+ const runTestCaseTitle = this.toMewpComparableText(
434
+ runResult?.testCase?.name || runResult?.testCaseName || runResult?.testCaseTitle
435
+ );
436
+ if (Number.isFinite(testCaseId) && testCaseId > 0 && runTestCaseTitle && !testCaseTitleMap.has(testCaseId)) {
437
+ testCaseTitleMap.set(testCaseId, runTestCaseTitle);
438
+ }
437
439
  const actionResults = Array.isArray(runResult?.iteration?.actionResults)
438
440
  ? runResult.iteration.actionResults
439
441
  : [];
@@ -447,8 +449,10 @@ export default class ResultDataProvider {
447
449
  this.accumulateRequirementCountsFromStepText(
448
450
  `${String(actionResult?.action || '')} ${String(actionResult?.expected || '')}`,
449
451
  stepStatus,
452
+ testCaseId,
450
453
  requirementKeys,
451
- requirementIndex
454
+ requirementIndex,
455
+ observedTestCaseIdsByRequirement
452
456
  );
453
457
  }
454
458
  continue;
@@ -476,19 +480,20 @@ export default class ResultDataProvider {
476
480
  this.accumulateRequirementCountsFromStepText(
477
481
  `${String(step?.action || '')} ${String(step?.expected || '')}`,
478
482
  'notRun',
483
+ testCaseId,
479
484
  requirementKeys,
480
- requirementIndex
485
+ requirementIndex,
486
+ observedTestCaseIdsByRequirement
481
487
  );
482
488
  }
483
489
  }
484
490
 
485
- const rows: any[] = requirements.map((requirement) => {
486
- const key = this.toRequirementKey(requirement.requirementId);
487
- const summary = key && requirementIndex.has(key)
488
- ? requirementIndex.get(key)!
489
- : { passed: 0, failed: 0, notRun: 0 };
490
- return this.createMewpCoverageRow(requirement, summary);
491
- });
491
+ const rows = this.buildMewpCoverageRows(
492
+ requirements,
493
+ requirementIndex,
494
+ observedTestCaseIdsByRequirement,
495
+ testCaseTitleMap
496
+ );
492
497
 
493
498
  return {
494
499
  sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
@@ -550,23 +555,140 @@ export default class ResultDataProvider {
550
555
  title: string;
551
556
  responsibility: string;
552
557
  },
558
+ testCaseId: number | undefined,
559
+ testCaseTitle: string,
553
560
  stepSummary: { passed: number; failed: number; notRun: number }
554
561
  ) {
555
- const requirementId = String(requirement.requirementId || '').trim();
556
- const requirementTitle = String(requirement.title || '').trim();
557
- const customerRequirement = [requirementId, requirementTitle].filter(Boolean).join(' - ');
558
- const responsibility = String(requirement.responsibility || '').trim();
562
+ const customerId = this.formatMewpCustomerId(requirement.requirementId);
563
+ const customerTitle = this.toMewpComparableText(requirement.title);
564
+ const responsibility = this.toMewpComparableText(requirement.responsibility);
565
+ const safeTestCaseId = Number.isFinite(testCaseId) && Number(testCaseId) > 0 ? Number(testCaseId) : '';
559
566
 
560
567
  return {
561
- 'Customer Requirement': customerRequirement || requirementId || requirementTitle,
562
- 'Requirement ID': requirementId,
563
- 'Requirement Title': requirementTitle,
564
- Responsibility: responsibility,
565
- 'SAPWBS / Responsibility': responsibility,
568
+ 'Customer ID': customerId,
569
+ 'Title (Customer name)': customerTitle,
570
+ 'Responsibility - SAPWBS (ESUK/IL)': responsibility,
571
+ 'Test case id': safeTestCaseId,
572
+ 'Test case title': String(testCaseTitle || '').trim(),
566
573
  'Number of passed steps': Number.isFinite(stepSummary?.passed) ? stepSummary.passed : 0,
567
574
  'Number of failed steps': Number.isFinite(stepSummary?.failed) ? stepSummary.failed : 0,
568
- 'Number of steps not run': Number.isFinite(stepSummary?.notRun) ? stepSummary.notRun : 0,
575
+ 'Number of not run tests': Number.isFinite(stepSummary?.notRun) ? stepSummary.notRun : 0,
576
+ };
577
+ }
578
+
579
+ private formatMewpCustomerId(rawValue: string): string {
580
+ const normalized = this.normalizeMewpRequirementCode(this.toMewpComparableText(rawValue));
581
+ if (normalized) return normalized;
582
+
583
+ const onlyDigits = String(rawValue || '').replace(/\D/g, '');
584
+ if (onlyDigits) return `SR${onlyDigits}`;
585
+ return '';
586
+ }
587
+
588
+ private buildMewpCoverageRows(
589
+ requirements: Array<{
590
+ requirementId: string;
591
+ title: string;
592
+ responsibility: string;
593
+ linkedTestCaseIds: number[];
594
+ }>,
595
+ requirementIndex: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
596
+ observedTestCaseIdsByRequirement: Map<string, Set<number>>,
597
+ testCaseTitleMap: Map<number, string>
598
+ ): any[] {
599
+ const rows: any[] = [];
600
+ for (const requirement of requirements) {
601
+ const key = this.toRequirementKey(requirement.requirementId);
602
+ const linkedTestCaseIds = (requirement?.linkedTestCaseIds || []).filter(
603
+ (id) => Number.isFinite(id) && Number(id) > 0
604
+ );
605
+ const observedTestCaseIds = key
606
+ ? Array.from(observedTestCaseIdsByRequirement.get(key) || [])
607
+ : [];
608
+
609
+ const testCaseIds = Array.from(new Set<number>([...linkedTestCaseIds, ...observedTestCaseIds])).sort(
610
+ (a, b) => a - b
611
+ );
612
+
613
+ if (testCaseIds.length === 0) {
614
+ rows.push(
615
+ this.createMewpCoverageRow(requirement, undefined, '', {
616
+ passed: 0,
617
+ failed: 0,
618
+ notRun: 0,
619
+ })
620
+ );
621
+ continue;
622
+ }
623
+
624
+ for (const testCaseId of testCaseIds) {
625
+ const summary = key
626
+ ? requirementIndex.get(key)?.get(testCaseId) || { passed: 0, failed: 0, notRun: 0 }
627
+ : { passed: 0, failed: 0, notRun: 0 };
628
+ rows.push(
629
+ this.createMewpCoverageRow(
630
+ requirement,
631
+ testCaseId,
632
+ String(testCaseTitleMap.get(testCaseId) || ''),
633
+ summary
634
+ )
635
+ );
636
+ }
637
+ }
638
+
639
+ return rows;
640
+ }
641
+
642
+ private buildMewpTestCaseTitleMap(testData: any[]): Map<number, string> {
643
+ const map = new Map<number, string>();
644
+
645
+ const readTitleFromWorkItemFields = (workItemFields: any): string => {
646
+ if (!Array.isArray(workItemFields)) return '';
647
+ for (const field of workItemFields) {
648
+ const keyCandidates = [field?.key, field?.name, field?.referenceName, field?.id]
649
+ .map((item) => String(item || '').toLowerCase().trim());
650
+ const isTitleField =
651
+ keyCandidates.includes('system.title') || keyCandidates.includes('title');
652
+ if (!isTitleField) continue;
653
+ const value = this.toMewpComparableText(field?.value);
654
+ if (value) return value;
655
+ }
656
+ return '';
569
657
  };
658
+
659
+ for (const suite of testData || []) {
660
+ const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
661
+ for (const point of testPointsItems) {
662
+ const pointTestCaseId = Number(point?.testCaseId || point?.testCase?.id);
663
+ if (!Number.isFinite(pointTestCaseId) || pointTestCaseId <= 0 || map.has(pointTestCaseId)) continue;
664
+ const pointTitle = this.toMewpComparableText(point?.testCaseName || point?.testCase?.name);
665
+ if (pointTitle) map.set(pointTestCaseId, pointTitle);
666
+ }
667
+
668
+ const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
669
+ for (const testCase of testCasesItems) {
670
+ const id = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id);
671
+ if (!Number.isFinite(id) || id <= 0 || map.has(id)) continue;
672
+ const fromDirectFields = this.toMewpComparableText(
673
+ testCase?.testCaseName || testCase?.name || testCase?.workItem?.name
674
+ );
675
+ if (fromDirectFields) {
676
+ map.set(id, fromDirectFields);
677
+ continue;
678
+ }
679
+ const fromWorkItemField = readTitleFromWorkItemFields(testCase?.workItem?.workItemFields);
680
+ if (fromWorkItemField) {
681
+ map.set(id, fromWorkItemField);
682
+ }
683
+ }
684
+ }
685
+
686
+ return map;
687
+ }
688
+
689
+ private extractMewpTestCaseId(runResult: any): number {
690
+ const testCaseId = Number(runResult?.testCaseId || runResult?.testCase?.id || 0);
691
+ return Number.isFinite(testCaseId) ? testCaseId : 0;
570
692
  }
571
693
 
572
694
  private buildTestCaseStepsXmlMap(testData: any[]): Map<number, string> {
@@ -617,16 +739,30 @@ export default class ResultDataProvider {
617
739
  private accumulateRequirementCountsFromStepText(
618
740
  stepText: string,
619
741
  status: 'passed' | 'failed' | 'notRun',
742
+ testCaseId: number,
620
743
  requirementKeys: Set<string>,
621
- counters: Map<string, { passed: number; failed: number; notRun: number }>
744
+ counters: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
745
+ observedTestCaseIdsByRequirement: Map<string, Set<number>>
622
746
  ) {
747
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0) return;
748
+
623
749
  const codes = this.extractRequirementCodesFromText(stepText);
624
750
  for (const code of codes) {
625
751
  if (requirementKeys.size > 0 && !requirementKeys.has(code)) continue;
626
752
  if (!counters.has(code)) {
627
- counters.set(code, { passed: 0, failed: 0, notRun: 0 });
753
+ counters.set(code, new Map<number, { passed: number; failed: number; notRun: number }>());
754
+ }
755
+ const perTestCaseCounters = counters.get(code)!;
756
+ if (!perTestCaseCounters.has(testCaseId)) {
757
+ perTestCaseCounters.set(testCaseId, { passed: 0, failed: 0, notRun: 0 });
758
+ }
759
+
760
+ if (!observedTestCaseIdsByRequirement.has(code)) {
761
+ observedTestCaseIdsByRequirement.set(code, new Set<number>());
628
762
  }
629
- const counter = counters.get(code)!;
763
+ observedTestCaseIdsByRequirement.get(code)!.add(testCaseId);
764
+
765
+ const counter = perTestCaseCounters.get(testCaseId)!;
630
766
  if (status === 'passed') counter.passed += 1;
631
767
  else if (status === 'failed') counter.failed += 1;
632
768
  else counter.notRun += 1;
@@ -713,7 +849,7 @@ ORDER BY [System.Id]`;
713
849
  return {
714
850
  workItemId: Number(wi?.id || 0),
715
851
  requirementId: this.extractMewpRequirementIdentifier(fields, Number(wi?.id || 0)),
716
- title: String(fields['System.Title'] || ''),
852
+ title: this.toMewpComparableText(fields?.['System.Title'] || wi?.title),
717
853
  responsibility: this.deriveMewpResponsibility(fields),
718
854
  linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
719
855
  };
@@ -823,10 +959,20 @@ ORDER BY [System.Id]`;
823
959
  const titleCode = this.normalizeMewpRequirementCode(title);
824
960
  if (titleCode) return titleCode;
825
961
 
826
- return String(fallbackWorkItemId || '');
962
+ return fallbackWorkItemId ? `SR${fallbackWorkItemId}` : '';
827
963
  }
828
964
 
829
965
  private deriveMewpResponsibility(fields: Record<string, any>): string {
966
+ const explicitSapWbs = this.toMewpComparableText(fields?.['Custom.SAPWBS']);
967
+ const fromExplicitSapWbs = this.resolveMewpResponsibility(explicitSapWbs);
968
+ if (fromExplicitSapWbs) return fromExplicitSapWbs;
969
+ if (explicitSapWbs) return explicitSapWbs;
970
+
971
+ const explicitSapWbsByLabel = this.toMewpComparableText(fields?.['SAPWBS']);
972
+ const fromExplicitLabel = this.resolveMewpResponsibility(explicitSapWbsByLabel);
973
+ if (fromExplicitLabel) return fromExplicitLabel;
974
+ if (explicitSapWbsByLabel) return explicitSapWbsByLabel;
975
+
830
976
  const areaPath = this.toMewpComparableText(fields?.['System.AreaPath']);
831
977
  const fromAreaPath = this.resolveMewpResponsibility(areaPath);
832
978
  if (fromAreaPath) return fromAreaPath;
@@ -873,6 +1019,8 @@ ORDER BY [System.Id]`;
873
1019
  if (name) return String(name).trim();
874
1020
  const uniqueName = (value as any).uniqueName;
875
1021
  if (uniqueName) return String(uniqueName).trim();
1022
+ const objectValue = (value as any).value;
1023
+ if (objectValue !== undefined && objectValue !== null) return String(objectValue).trim();
876
1024
  }
877
1025
  return String(value).trim();
878
1026
  }
@@ -1075,7 +1075,7 @@ describe('ResultDataProvider', () => {
1075
1075
  });
1076
1076
 
1077
1077
  describe('getMewpL2CoverageFlatResults', () => {
1078
- it('should map SR ids from step action/expected and aggregate passed/failed/not-run by requirement', async () => {
1078
+ it('should map SR ids from steps and output requirement-test-case coverage rows', async () => {
1079
1079
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1080
1080
  jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1081
1081
  jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
@@ -1105,7 +1105,14 @@ describe('ResultDataProvider', () => {
1105
1105
  {
1106
1106
  workItemId: 5002,
1107
1107
  requirementId: 'SR1002',
1108
- title: 'Uncovered requirement',
1108
+ title: 'Referenced from non-linked step text',
1109
+ responsibility: 'IL',
1110
+ linkedTestCaseIds: [],
1111
+ },
1112
+ {
1113
+ workItemId: 5003,
1114
+ requirementId: 'SR1003',
1115
+ title: 'Not covered by any test case',
1109
1116
  responsibility: 'IL',
1110
1117
  linkedTestCaseIds: [],
1111
1118
  },
@@ -1113,6 +1120,7 @@ describe('ResultDataProvider', () => {
1113
1120
  jest.spyOn(resultDataProvider as any, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([
1114
1121
  {
1115
1122
  testCaseId: 101,
1123
+ testCase: { id: 101, name: 'TC 101' },
1116
1124
  iteration: {
1117
1125
  actionResults: [
1118
1126
  {
@@ -1127,6 +1135,7 @@ describe('ResultDataProvider', () => {
1127
1135
  },
1128
1136
  {
1129
1137
  testCaseId: 102,
1138
+ testCase: { id: 102, name: 'TC 102' },
1130
1139
  iteration: undefined,
1131
1140
  },
1132
1141
  ]);
@@ -1151,25 +1160,50 @@ describe('ResultDataProvider', () => {
1151
1160
  expect(result).toEqual(
1152
1161
  expect.objectContaining({
1153
1162
  sheetName: expect.stringContaining('MEWP L2 Coverage'),
1154
- columnOrder: expect.arrayContaining(['Requirement ID', 'Number of steps not run']),
1163
+ columnOrder: expect.arrayContaining(['Customer ID', 'Test case id', 'Number of not run tests']),
1155
1164
  })
1156
1165
  );
1157
1166
 
1158
- const covered = result.rows.find((row: any) => row['Requirement ID'] === 'SR1001');
1159
- const uncovered = result.rows.find((row: any) => row['Requirement ID'] === 'SR1002');
1167
+ const covered = result.rows.find(
1168
+ (row: any) => row['Customer ID'] === 'SR1001' && row['Test case id'] === 101
1169
+ );
1170
+ const inferredByStepText = result.rows.find(
1171
+ (row: any) => row['Customer ID'] === 'SR1002' && row['Test case id'] === 102
1172
+ );
1173
+ const uncovered = result.rows.find(
1174
+ (row: any) =>
1175
+ row['Customer ID'] === 'SR1003' &&
1176
+ (row['Test case id'] === '' || row['Test case id'] === undefined || row['Test case id'] === null)
1177
+ );
1160
1178
 
1161
1179
  expect(covered).toEqual(
1162
1180
  expect.objectContaining({
1181
+ 'Title (Customer name)': 'Covered requirement',
1182
+ 'Responsibility - SAPWBS (ESUK/IL)': 'ESUK',
1183
+ 'Test case title': 'TC 101',
1163
1184
  'Number of passed steps': 1,
1164
1185
  'Number of failed steps': 1,
1165
- 'Number of steps not run': 1,
1186
+ 'Number of not run tests': 1,
1187
+ })
1188
+ );
1189
+ expect(inferredByStepText).toEqual(
1190
+ expect.objectContaining({
1191
+ 'Title (Customer name)': 'Referenced from non-linked step text',
1192
+ 'Responsibility - SAPWBS (ESUK/IL)': 'IL',
1193
+ 'Test case title': 'TC 102',
1194
+ 'Number of passed steps': 0,
1195
+ 'Number of failed steps': 0,
1196
+ 'Number of not run tests': 1,
1166
1197
  })
1167
1198
  );
1168
1199
  expect(uncovered).toEqual(
1169
1200
  expect.objectContaining({
1201
+ 'Title (Customer name)': 'Not covered by any test case',
1202
+ 'Responsibility - SAPWBS (ESUK/IL)': 'IL',
1203
+ 'Test case title': '',
1170
1204
  'Number of passed steps': 0,
1171
1205
  'Number of failed steps': 0,
1172
- 'Number of steps not run': 1,
1206
+ 'Number of not run tests': 0,
1173
1207
  })
1174
1208
  );
1175
1209
  });
@@ -1235,13 +1269,15 @@ describe('ResultDataProvider', () => {
1235
1269
  [1]
1236
1270
  );
1237
1271
 
1238
- const row = result.rows.find((item: any) => item['Requirement ID'] === 'SR2001');
1272
+ const row = result.rows.find(
1273
+ (item: any) => item['Customer ID'] === 'SR2001' && item['Test case id'] === 101
1274
+ );
1239
1275
  expect(parseSpy).not.toHaveBeenCalled();
1240
1276
  expect(row).toEqual(
1241
1277
  expect.objectContaining({
1242
1278
  'Number of passed steps': 0,
1243
1279
  'Number of failed steps': 0,
1244
- 'Number of steps not run': 0,
1280
+ 'Number of not run tests': 0,
1245
1281
  })
1246
1282
  );
1247
1283
  });
@@ -1256,7 +1292,16 @@ describe('ResultDataProvider', () => {
1256
1292
  4321
1257
1293
  );
1258
1294
 
1259
- expect(requirementId).toBe('4321');
1295
+ expect(requirementId).toBe('SR4321');
1296
+ });
1297
+
1298
+ it('should derive responsibility from Custom.SAPWBS when present', () => {
1299
+ const responsibility = (resultDataProvider as any).deriveMewpResponsibility({
1300
+ 'Custom.SAPWBS': 'IL',
1301
+ 'System.AreaPath': 'MEWP\\ESUK',
1302
+ });
1303
+
1304
+ expect(responsibility).toBe('IL');
1260
1305
  });
1261
1306
  });
1262
1307