@elisra-devops/docgen-data-provider 1.86.0 → 1.87.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elisra-devops/docgen-data-provider",
3
- "version": "1.86.0",
3
+ "version": "1.87.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -878,15 +878,36 @@ export default class ResultDataProvider {
878
878
  const missingFamilyMembers = new Set<string>();
879
879
  for (const [baseKey, mentionedCodes] of mentionedCodesByBase.entries()) {
880
880
  const familyCodes = requirementFamilies.get(baseKey);
881
+ const mentionedCodesList = [...mentionedCodes];
882
+ const hasBaseMention = mentionedCodesList.some((code) => !/-\d+$/.test(code));
883
+ const mentionedSpecificMembers = mentionedCodesList.filter((code) => /-\d+$/.test(code));
884
+
881
885
  if (familyCodes?.size) {
882
- const missingInFamily = [...familyCodes].filter((code) => !linkedFullCodes.has(code));
883
- if (missingInFamily.length === 0) continue;
884
- const linkedInFamilyCount = familyCodes.size - missingInFamily.length;
885
- if (linkedInFamilyCount === 0) {
886
- missingBaseWhenFamilyUncovered.add(baseKey);
887
- } else {
888
- for (const code of missingInFamily) {
889
- missingFamilyMembers.add(code);
886
+ // Base mention ("SR0054") validates against child coverage when children exist.
887
+ // If no child variants exist, fallback to the single standalone requirement code.
888
+ if (hasBaseMention) {
889
+ const familyCodesList = [...familyCodes];
890
+ const childFamilyCodes = familyCodesList.filter((code) => /-\d+$/.test(code));
891
+ const targetFamilyCodes = childFamilyCodes.length > 0 ? childFamilyCodes : familyCodesList;
892
+ const missingInTargetFamily = targetFamilyCodes.filter((code) => !linkedFullCodes.has(code));
893
+
894
+ if (missingInTargetFamily.length > 0) {
895
+ const hasAnyLinkedInFamily = familyCodesList.some((code) => linkedFullCodes.has(code));
896
+ if (!hasAnyLinkedInFamily) {
897
+ missingBaseWhenFamilyUncovered.add(baseKey);
898
+ } else {
899
+ for (const code of missingInTargetFamily) {
900
+ missingFamilyMembers.add(code);
901
+ }
902
+ }
903
+ }
904
+ continue;
905
+ }
906
+
907
+ // Specific mention ("SR0054-1") validates as exact-match only.
908
+ for (const code of mentionedSpecificMembers) {
909
+ if (!linkedFullCodes.has(code)) {
910
+ missingSpecificMentionedNoFamily.add(code);
890
911
  }
891
912
  }
892
913
  continue;
@@ -960,11 +981,11 @@ export default class ResultDataProvider {
960
981
  return String(a[0]).localeCompare(String(b[0]));
961
982
  })
962
983
  .map(([stepRef, requirementIds]) => {
963
- const requirementList = [...requirementIds].sort((a, b) => a.localeCompare(b));
964
- return `${stepRef}: ${requirementList.join(', ')}`;
984
+ const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
985
+ return `${stepRef}: ${groupedRequirementList}`;
965
986
  })
966
987
  .join('; ');
967
- const linkedButNotMentioned = sortedExtraLinked.join('; ');
988
+ const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked);
968
989
  const validationStatus: 'Pass' | 'Fail' =
969
990
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
970
991
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
@@ -2735,6 +2756,28 @@ export default class ResultDataProvider {
2735
2756
  return `SR${match[1]}`;
2736
2757
  }
2737
2758
 
2759
+ private formatRequirementCodesGroupedByFamily(codes: Iterable<string>): string {
2760
+ const byBaseKey = new Map<string, Set<string>>();
2761
+ for (const rawCode of codes || []) {
2762
+ const normalizedCode = this.normalizeMewpRequirementCodeWithSuffix(String(rawCode || ''));
2763
+ if (!normalizedCode) continue;
2764
+ const baseKey = this.toRequirementKey(normalizedCode) || normalizedCode;
2765
+ if (!byBaseKey.has(baseKey)) byBaseKey.set(baseKey, new Set<string>());
2766
+ byBaseKey.get(baseKey)!.add(normalizedCode);
2767
+ }
2768
+
2769
+ if (byBaseKey.size === 0) return '';
2770
+
2771
+ return [...byBaseKey.entries()]
2772
+ .sort((a, b) => a[0].localeCompare(b[0]))
2773
+ .map(([baseKey, members]) => {
2774
+ const sortedMembers = [...members].sort((a, b) => a.localeCompare(b));
2775
+ if (sortedMembers.length <= 1) return sortedMembers[0];
2776
+ return `${baseKey}: ${sortedMembers.join(', ')}`;
2777
+ })
2778
+ .join('; ');
2779
+ }
2780
+
2738
2781
  private toMewpComparableText(value: any): string {
2739
2782
  if (value === null || value === undefined) return '';
2740
2783
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
@@ -1986,6 +1986,158 @@ describe('ResultDataProvider', () => {
1986
1986
  );
1987
1987
  });
1988
1988
 
1989
+ it('should pass when a base SR mention is fully covered by its only linked child', async () => {
1990
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1991
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1992
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
1993
+ {
1994
+ testPointsItems: [{ testCaseId: 402, testCaseName: 'TC 402 - Single child covered' }],
1995
+ testCasesItems: [
1996
+ {
1997
+ workItem: {
1998
+ id: 402,
1999
+ workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-402\"></steps>' }],
2000
+ },
2001
+ },
2002
+ ],
2003
+ },
2004
+ ]);
2005
+ jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
2006
+ {
2007
+ workItemId: 9101,
2008
+ requirementId: 'SR0054',
2009
+ baseKey: 'SR0054',
2010
+ title: 'SR0054 parent',
2011
+ responsibility: 'ESUK',
2012
+ linkedTestCaseIds: [],
2013
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2014
+ },
2015
+ {
2016
+ workItemId: 9102,
2017
+ requirementId: 'SR0054-1',
2018
+ baseKey: 'SR0054',
2019
+ title: 'SR0054 child 1',
2020
+ responsibility: 'ESUK',
2021
+ linkedTestCaseIds: [],
2022
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2023
+ },
2024
+ ]);
2025
+ jest.spyOn(resultDataProvider as any, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(
2026
+ new Map([
2027
+ [
2028
+ 402,
2029
+ {
2030
+ baseKeys: new Set(['SR0054']),
2031
+ fullCodes: new Set(['SR0054-1']),
2032
+ },
2033
+ ],
2034
+ ])
2035
+ );
2036
+ jest.spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
2037
+ {
2038
+ stepId: '3',
2039
+ stepPosition: '3',
2040
+ action: 'Validate family root',
2041
+ expected: 'SR0054',
2042
+ isSharedStepTitle: false,
2043
+ },
2044
+ ]);
2045
+
2046
+ const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
2047
+ '123',
2048
+ mockProjectName,
2049
+ [1]
2050
+ );
2051
+
2052
+ expect(result.rows).toHaveLength(1);
2053
+ expect(result.rows[0]).toEqual(
2054
+ expect.objectContaining({
2055
+ 'Test Case ID': 402,
2056
+ 'Mentioned but Not Linked': '',
2057
+ 'Linked but Not Mentioned': '',
2058
+ 'Validation Status': 'Pass',
2059
+ })
2060
+ );
2061
+ });
2062
+
2063
+ it('should group linked-but-not-mentioned requirements by SR family', async () => {
2064
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
2065
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
2066
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
2067
+ {
2068
+ testPointsItems: [{ testCaseId: 403, testCaseName: 'TC 403 - Linked only' }],
2069
+ testCasesItems: [
2070
+ {
2071
+ workItem: {
2072
+ id: 403,
2073
+ workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-403\"></steps>' }],
2074
+ },
2075
+ },
2076
+ ],
2077
+ },
2078
+ ]);
2079
+ jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
2080
+ {
2081
+ workItemId: 9201,
2082
+ requirementId: 'SR0054-1',
2083
+ baseKey: 'SR0054',
2084
+ title: 'SR0054 child 1',
2085
+ responsibility: 'ESUK',
2086
+ linkedTestCaseIds: [],
2087
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2088
+ },
2089
+ {
2090
+ workItemId: 9202,
2091
+ requirementId: 'SR0054-2',
2092
+ baseKey: 'SR0054',
2093
+ title: 'SR0054 child 2',
2094
+ responsibility: 'ESUK',
2095
+ linkedTestCaseIds: [],
2096
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2097
+ },
2098
+ {
2099
+ workItemId: 9203,
2100
+ requirementId: 'SR0100-1',
2101
+ baseKey: 'SR0100',
2102
+ title: 'SR0100 child 1',
2103
+ responsibility: 'ESUK',
2104
+ linkedTestCaseIds: [],
2105
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2106
+ },
2107
+ ]);
2108
+ jest.spyOn(resultDataProvider as any, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(
2109
+ new Map([
2110
+ [
2111
+ 403,
2112
+ {
2113
+ baseKeys: new Set(['SR0054', 'SR0100']),
2114
+ fullCodes: new Set(['SR0054-1', 'SR0054-2', 'SR0100-1']),
2115
+ },
2116
+ ],
2117
+ ])
2118
+ );
2119
+ jest.spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
2120
+ {
2121
+ stepId: '1',
2122
+ stepPosition: '1',
2123
+ action: 'Action only',
2124
+ expected: '',
2125
+ isSharedStepTitle: false,
2126
+ },
2127
+ ]);
2128
+
2129
+ const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
2130
+ '123',
2131
+ mockProjectName,
2132
+ [1]
2133
+ );
2134
+
2135
+ expect(result.rows).toHaveLength(1);
2136
+ expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).toBe('');
2137
+ expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).toContain('SR0054: SR0054-1, SR0054-2');
2138
+ expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).toContain('SR0100-1');
2139
+ });
2140
+
1989
2141
  it('should report only base SR when an entire mentioned family is uncovered', async () => {
1990
2142
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1991
2143
  jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
@@ -2393,7 +2545,7 @@ describe('ResultDataProvider', () => {
2393
2545
  expect(byTestCase.get(201)).toEqual(
2394
2546
  expect.objectContaining({
2395
2547
  'Test Case Title': 'TC 201 - Mixed discrepancies',
2396
- 'Mentioned but Not Linked': 'Step 1: SR0095-3, SR0511-1, SR0511-2',
2548
+ 'Mentioned but Not Linked': 'Step 1: SR0095-3; SR0511: SR0511-1, SR0511-2',
2397
2549
  'Linked but Not Mentioned': 'SR8888',
2398
2550
  'Validation Status': 'Fail',
2399
2551
  })