@elisra-devops/docgen-data-provider 1.85.0 → 1.86.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/bin/modules/ResultDataProvider.js +55 -26
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +152 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/ResultDataProvider.ts +59 -24
- package/src/tests/modules/ResultDataProvider.test.ts +177 -0
package/package.json
CHANGED
|
@@ -848,18 +848,6 @@ export default class ResultDataProvider {
|
|
|
848
848
|
[...mentionedL2Only].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
849
849
|
);
|
|
850
850
|
|
|
851
|
-
const expectedFamilyCodes = new Set<string>();
|
|
852
|
-
for (const baseKey of mentionedBaseKeys) {
|
|
853
|
-
const familyCodes = requirementFamilies.get(baseKey);
|
|
854
|
-
if (familyCodes?.size) {
|
|
855
|
-
familyCodes.forEach((code) => expectedFamilyCodes.add(code));
|
|
856
|
-
} else {
|
|
857
|
-
for (const code of mentionedL2Only) {
|
|
858
|
-
if (this.toRequirementKey(code) === baseKey) expectedFamilyCodes.add(code);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
851
|
const linkedFullCodesRaw = linkedRequirementsByTestCase.get(testCaseId)?.fullCodes || new Set<string>();
|
|
864
852
|
const linkedFullCodes =
|
|
865
853
|
scopedRequirementKeys?.size && linkedFullCodesRaw.size > 0
|
|
@@ -873,14 +861,48 @@ export default class ResultDataProvider {
|
|
|
873
861
|
[...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
874
862
|
);
|
|
875
863
|
|
|
876
|
-
const
|
|
864
|
+
const mentionedCodesByBase = new Map<string, Set<string>>();
|
|
865
|
+
for (const code of mentionedL2Only) {
|
|
877
866
|
const baseKey = this.toRequirementKey(code);
|
|
878
|
-
if (!baseKey)
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
867
|
+
if (!baseKey) continue;
|
|
868
|
+
if (!mentionedCodesByBase.has(baseKey)) mentionedCodesByBase.set(baseKey, new Set<string>());
|
|
869
|
+
mentionedCodesByBase.get(baseKey)!.add(code);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Context-based direction A logic:
|
|
873
|
+
// 1) If no member of family is linked -> report only base SR (Step X: SR0054).
|
|
874
|
+
// 2) If family is partially linked -> report only specific missing members.
|
|
875
|
+
// 3) If family fully linked -> report nothing for that family.
|
|
876
|
+
const missingBaseWhenFamilyUncovered = new Set<string>();
|
|
877
|
+
const missingSpecificMentionedNoFamily = new Set<string>();
|
|
878
|
+
const missingFamilyMembers = new Set<string>();
|
|
879
|
+
for (const [baseKey, mentionedCodes] of mentionedCodesByBase.entries()) {
|
|
880
|
+
const familyCodes = requirementFamilies.get(baseKey);
|
|
881
|
+
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);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Fallback path when family data is unavailable for this base key.
|
|
896
|
+
for (const code of mentionedCodes) {
|
|
897
|
+
const hasSpecificSuffix = /-\d+$/.test(code);
|
|
898
|
+
if (hasSpecificSuffix) {
|
|
899
|
+
if (!linkedFullCodes.has(code)) missingSpecificMentionedNoFamily.add(code);
|
|
900
|
+
} else if (!linkedBaseKeys.has(baseKey)) {
|
|
901
|
+
missingBaseWhenFamilyUncovered.add(baseKey);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
884
906
|
// Direction B is family-based: if any member of a family is mentioned in Expected Result,
|
|
885
907
|
// linked members of that same family are not considered "linked but not mentioned".
|
|
886
908
|
const extraLinked = [...linkedFullCodes].filter((code) => {
|
|
@@ -899,13 +921,22 @@ export default class ResultDataProvider {
|
|
|
899
921
|
mentionedButNotLinkedByStep.get(normalizedStepRef)!.add(normalizedRequirementId);
|
|
900
922
|
};
|
|
901
923
|
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
924
|
+
const sortedMissingSpecificMentionedNoFamily = [...missingSpecificMentionedNoFamily].sort((a, b) =>
|
|
925
|
+
a.localeCompare(b)
|
|
926
|
+
);
|
|
927
|
+
const sortedMissingBaseWhenFamilyUncovered = [...missingBaseWhenFamilyUncovered].sort((a, b) =>
|
|
928
|
+
a.localeCompare(b)
|
|
929
|
+
);
|
|
930
|
+
const sortedMissingFamilyMembers = [...missingFamilyMembers].sort((a, b) => a.localeCompare(b));
|
|
931
|
+
for (const code of sortedMissingSpecificMentionedNoFamily) {
|
|
905
932
|
const stepRef = mentionedCodeFirstStep.get(code) || 'Step ?';
|
|
906
933
|
appendMentionedButNotLinked(code, stepRef);
|
|
907
934
|
}
|
|
908
|
-
for (const
|
|
935
|
+
for (const baseKey of sortedMissingBaseWhenFamilyUncovered) {
|
|
936
|
+
const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
|
|
937
|
+
appendMentionedButNotLinked(baseKey, stepRef);
|
|
938
|
+
}
|
|
939
|
+
for (const code of sortedMissingFamilyMembers) {
|
|
909
940
|
const baseKey = this.toRequirementKey(code);
|
|
910
941
|
const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
|
|
911
942
|
appendMentionedButNotLinked(code, stepRef);
|
|
@@ -941,7 +972,11 @@ export default class ResultDataProvider {
|
|
|
941
972
|
`MEWP internal validation parse diagnostics: ` +
|
|
942
973
|
`testCaseId=${testCaseId} parsedSteps=${executableSteps.length} ` +
|
|
943
974
|
`stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
|
|
944
|
-
`linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
|
|
975
|
+
`linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
|
|
976
|
+
sortedMissingSpecificMentionedNoFamily.length +
|
|
977
|
+
sortedMissingBaseWhenFamilyUncovered.length +
|
|
978
|
+
sortedMissingFamilyMembers.length
|
|
979
|
+
} ` +
|
|
945
980
|
`linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
|
|
946
981
|
`customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
|
|
947
982
|
);
|
|
@@ -1986,6 +1986,74 @@ describe('ResultDataProvider', () => {
|
|
|
1986
1986
|
);
|
|
1987
1987
|
});
|
|
1988
1988
|
|
|
1989
|
+
it('should report only base SR when an entire mentioned family is uncovered', 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: 401, testCaseName: 'TC 401 - Family uncovered' }],
|
|
1995
|
+
testCasesItems: [
|
|
1996
|
+
{
|
|
1997
|
+
workItem: {
|
|
1998
|
+
id: 401,
|
|
1999
|
+
workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-401\"></steps>' }],
|
|
2000
|
+
},
|
|
2001
|
+
},
|
|
2002
|
+
],
|
|
2003
|
+
},
|
|
2004
|
+
]);
|
|
2005
|
+
jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
|
|
2006
|
+
{
|
|
2007
|
+
workItemId: 9001,
|
|
2008
|
+
requirementId: 'SR0054-1',
|
|
2009
|
+
baseKey: 'SR0054',
|
|
2010
|
+
title: 'SR0054 child 1',
|
|
2011
|
+
responsibility: 'ESUK',
|
|
2012
|
+
linkedTestCaseIds: [],
|
|
2013
|
+
areaPath: 'MEWP\\Customer Requirements\\Level 2',
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
workItemId: 9002,
|
|
2017
|
+
requirementId: 'SR0054-2',
|
|
2018
|
+
baseKey: 'SR0054',
|
|
2019
|
+
title: 'SR0054 child 2',
|
|
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([[401, { baseKeys: new Set<string>(), fullCodes: new Set<string>() }]])
|
|
2027
|
+
);
|
|
2028
|
+
jest.spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
|
|
2029
|
+
{
|
|
2030
|
+
stepId: '3',
|
|
2031
|
+
stepPosition: '3',
|
|
2032
|
+
action: 'Validate family root',
|
|
2033
|
+
expected: 'SR0054',
|
|
2034
|
+
isSharedStepTitle: false,
|
|
2035
|
+
},
|
|
2036
|
+
]);
|
|
2037
|
+
|
|
2038
|
+
const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
|
|
2039
|
+
'123',
|
|
2040
|
+
mockProjectName,
|
|
2041
|
+
[1]
|
|
2042
|
+
);
|
|
2043
|
+
|
|
2044
|
+
expect(result.rows).toHaveLength(1);
|
|
2045
|
+
expect(result.rows[0]).toEqual(
|
|
2046
|
+
expect.objectContaining({
|
|
2047
|
+
'Test Case ID': 401,
|
|
2048
|
+
'Mentioned but Not Linked': 'Step 3: SR0054',
|
|
2049
|
+
'Linked but Not Mentioned': '',
|
|
2050
|
+
'Validation Status': 'Fail',
|
|
2051
|
+
})
|
|
2052
|
+
);
|
|
2053
|
+
expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).not.toContain('SR0054-1');
|
|
2054
|
+
expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).not.toContain('SR0054-2');
|
|
2055
|
+
});
|
|
2056
|
+
|
|
1989
2057
|
it('should not duplicate Direction A discrepancy when same requirement is repeated in multiple steps', async () => {
|
|
1990
2058
|
jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
|
|
1991
2059
|
jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
|
|
@@ -2662,6 +2730,115 @@ describe('ResultDataProvider', () => {
|
|
|
2662
2730
|
});
|
|
2663
2731
|
});
|
|
2664
2732
|
|
|
2733
|
+
describe('buildLinkedRequirementsByTestCase', () => {
|
|
2734
|
+
it('should map linked requirements only for supported test-case requirement relation types', async () => {
|
|
2735
|
+
const requirements = [
|
|
2736
|
+
{
|
|
2737
|
+
workItemId: 7001,
|
|
2738
|
+
requirementId: 'SR0054-1',
|
|
2739
|
+
baseKey: 'SR0054',
|
|
2740
|
+
linkedTestCaseIds: [],
|
|
2741
|
+
},
|
|
2742
|
+
];
|
|
2743
|
+
const testData = [
|
|
2744
|
+
{
|
|
2745
|
+
testCasesItems: [{ workItem: { id: 1001 } }],
|
|
2746
|
+
testPointsItems: [],
|
|
2747
|
+
},
|
|
2748
|
+
];
|
|
2749
|
+
|
|
2750
|
+
const fetchByIdsSpy = jest
|
|
2751
|
+
.spyOn(resultDataProvider as any, 'fetchWorkItemsByIds')
|
|
2752
|
+
.mockImplementation(async (...args: any[]) => {
|
|
2753
|
+
const ids = Array.isArray(args?.[1]) ? args[1] : [];
|
|
2754
|
+
const includeRelations = !!args?.[2];
|
|
2755
|
+
if (includeRelations) {
|
|
2756
|
+
return [
|
|
2757
|
+
{
|
|
2758
|
+
id: 1001,
|
|
2759
|
+
relations: [
|
|
2760
|
+
{
|
|
2761
|
+
rel: 'Microsoft.VSTS.Common.TestedBy-Reverse',
|
|
2762
|
+
url: 'https://dev.azure.com/org/project/_apis/wit/workItems/7001',
|
|
2763
|
+
},
|
|
2764
|
+
{
|
|
2765
|
+
rel: 'System.LinkTypes.Related',
|
|
2766
|
+
url: 'https://dev.azure.com/org/project/_apis/wit/workItems/7001',
|
|
2767
|
+
},
|
|
2768
|
+
],
|
|
2769
|
+
},
|
|
2770
|
+
];
|
|
2771
|
+
}
|
|
2772
|
+
return ids.map((id) => ({
|
|
2773
|
+
id,
|
|
2774
|
+
fields: {
|
|
2775
|
+
'System.WorkItemType': id === 7001 ? 'Requirement' : 'Test Case',
|
|
2776
|
+
},
|
|
2777
|
+
}));
|
|
2778
|
+
});
|
|
2779
|
+
|
|
2780
|
+
const linked = await (resultDataProvider as any).buildLinkedRequirementsByTestCase(
|
|
2781
|
+
requirements,
|
|
2782
|
+
testData,
|
|
2783
|
+
mockProjectName
|
|
2784
|
+
);
|
|
2785
|
+
|
|
2786
|
+
expect(fetchByIdsSpy).toHaveBeenCalled();
|
|
2787
|
+
expect(linked.get(1001)?.baseKeys?.has('SR0054')).toBe(true);
|
|
2788
|
+
expect(linked.get(1001)?.fullCodes?.has('SR0054-1')).toBe(true);
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
it('should ignore unsupported relation types when linking test case to requirements', async () => {
|
|
2792
|
+
const requirements = [
|
|
2793
|
+
{
|
|
2794
|
+
workItemId: 7002,
|
|
2795
|
+
requirementId: 'SR0099-1',
|
|
2796
|
+
baseKey: 'SR0099',
|
|
2797
|
+
linkedTestCaseIds: [],
|
|
2798
|
+
},
|
|
2799
|
+
];
|
|
2800
|
+
const testData = [
|
|
2801
|
+
{
|
|
2802
|
+
testCasesItems: [{ workItem: { id: 1002 } }],
|
|
2803
|
+
testPointsItems: [],
|
|
2804
|
+
},
|
|
2805
|
+
];
|
|
2806
|
+
|
|
2807
|
+
jest.spyOn(resultDataProvider as any, 'fetchWorkItemsByIds').mockImplementation(async (...args: any[]) => {
|
|
2808
|
+
const ids = Array.isArray(args?.[1]) ? args[1] : [];
|
|
2809
|
+
const includeRelations = !!args?.[2];
|
|
2810
|
+
if (includeRelations) {
|
|
2811
|
+
return [
|
|
2812
|
+
{
|
|
2813
|
+
id: 1002,
|
|
2814
|
+
relations: [
|
|
2815
|
+
{
|
|
2816
|
+
rel: 'System.LinkTypes.Related',
|
|
2817
|
+
url: 'https://dev.azure.com/org/project/_apis/wit/workItems/7002',
|
|
2818
|
+
},
|
|
2819
|
+
],
|
|
2820
|
+
},
|
|
2821
|
+
];
|
|
2822
|
+
}
|
|
2823
|
+
return ids.map((id) => ({
|
|
2824
|
+
id,
|
|
2825
|
+
fields: {
|
|
2826
|
+
'System.WorkItemType': id === 7002 ? 'Requirement' : 'Test Case',
|
|
2827
|
+
},
|
|
2828
|
+
}));
|
|
2829
|
+
});
|
|
2830
|
+
|
|
2831
|
+
const linked = await (resultDataProvider as any).buildLinkedRequirementsByTestCase(
|
|
2832
|
+
requirements,
|
|
2833
|
+
testData,
|
|
2834
|
+
mockProjectName
|
|
2835
|
+
);
|
|
2836
|
+
|
|
2837
|
+
expect(linked.get(1002)?.baseKeys?.has('SR0099')).toBe(false);
|
|
2838
|
+
expect(linked.get(1002)?.fullCodes?.has('SR0099-1')).toBe(false);
|
|
2839
|
+
});
|
|
2840
|
+
});
|
|
2841
|
+
|
|
2665
2842
|
describe('MEWP rel fallback scoping', () => {
|
|
2666
2843
|
it('should fallback to previous Rel run when latest selected Rel has no run for a test case', async () => {
|
|
2667
2844
|
const suites = [
|