@elisra-devops/docgen-data-provider 1.72.0 → 1.74.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.
@@ -856,6 +856,264 @@ describe('ResultDataProvider', () => {
856
856
  expect(instance.GetQueryResultsFromWiql).toHaveBeenCalledWith('https://example.com/wiql', true, expect.any(Map));
857
857
  });
858
858
  });
859
+ describe('getMewpL2CoverageFlatResults', () => {
860
+ it('should support query-mode requirement scope for MEWP coverage', async () => {
861
+ jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
862
+ jest.spyOn(resultDataProvider, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
863
+ jest.spyOn(resultDataProvider, 'fetchTestData').mockResolvedValueOnce([
864
+ {
865
+ testPointsItems: [{ testCaseId: 101, lastRunId: 10, lastResultId: 20, testCaseName: 'TC 101' }],
866
+ testCasesItems: [
867
+ {
868
+ workItem: {
869
+ id: 101,
870
+ workItemFields: [{ key: 'System.Title', value: 'TC 101' }],
871
+ },
872
+ },
873
+ ],
874
+ },
875
+ ]);
876
+ jest.spyOn(resultDataProvider, 'fetchMewpRequirementTypeNames').mockResolvedValueOnce([
877
+ 'Requirement',
878
+ ]);
879
+ jest.spyOn(resultDataProvider, 'fetchWorkItemsByIds').mockResolvedValueOnce([
880
+ {
881
+ id: 9001,
882
+ fields: {
883
+ 'System.WorkItemType': 'Requirement',
884
+ 'System.Title': 'Requirement from query',
885
+ 'Custom.CustomerId': 'SR3001',
886
+ 'System.AreaPath': 'MEWP\\IL',
887
+ },
888
+ relations: [
889
+ {
890
+ rel: 'Microsoft.VSTS.Common.TestedBy-Forward',
891
+ url: 'https://dev.azure.com/org/_apis/wit/workItems/101',
892
+ },
893
+ ],
894
+ },
895
+ ]);
896
+ jest.spyOn(resultDataProvider, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([
897
+ {
898
+ testCaseId: 101,
899
+ testCase: { id: 101, name: 'TC 101' },
900
+ iteration: {
901
+ actionResults: [{ action: 'Validate SR3001', expected: '', outcome: 'Passed' }],
902
+ },
903
+ },
904
+ ]);
905
+ const TicketsProviderMock = require('../../modules/TicketsDataProvider').default;
906
+ TicketsProviderMock.mockImplementationOnce(() => ({
907
+ GetQueryResultsFromWiql: jest.fn().mockResolvedValue({
908
+ fetchedWorkItems: [
909
+ {
910
+ id: 9001,
911
+ fields: {
912
+ 'System.WorkItemType': 'Requirement',
913
+ 'System.Title': 'Requirement from query',
914
+ 'Custom.CustomerId': 'SR3001',
915
+ 'System.AreaPath': 'MEWP\\IL',
916
+ },
917
+ },
918
+ ],
919
+ }),
920
+ }));
921
+ const result = await resultDataProvider.getMewpL2CoverageFlatResults('123', mockProjectName, [1], {
922
+ linkedQueryMode: 'query',
923
+ testAssociatedQuery: { wiql: { href: 'https://example.com/wiql' } },
924
+ });
925
+ const row = result.rows.find((item) => item['Customer ID'] === 'SR3001');
926
+ expect(row).toEqual(expect.objectContaining({
927
+ 'Title (Customer name)': 'Requirement from query',
928
+ 'Responsibility - SAPWBS (ESUK/IL)': 'IL',
929
+ 'Test case id': 101,
930
+ 'Test case title': 'TC 101',
931
+ 'Number of passed steps': 1,
932
+ 'Number of failed steps': 0,
933
+ 'Number of not run tests': 0,
934
+ }));
935
+ expect(TicketsProviderMock).toHaveBeenCalled();
936
+ const instance = TicketsProviderMock.mock.results[0].value;
937
+ expect(instance.GetQueryResultsFromWiql).toHaveBeenCalledWith('https://example.com/wiql', true, expect.any(Map));
938
+ });
939
+ it('should map SR ids from steps and output requirement-test-case coverage rows', async () => {
940
+ jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
941
+ jest.spyOn(resultDataProvider, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
942
+ jest.spyOn(resultDataProvider, 'fetchTestData').mockResolvedValueOnce([
943
+ {
944
+ testPointsItems: [
945
+ { testCaseId: 101, lastRunId: 11, lastResultId: 22, testCaseName: 'TC 101' },
946
+ { testCaseId: 102, lastRunId: 0, lastResultId: 0, testCaseName: 'TC 102' },
947
+ ],
948
+ testCasesItems: [
949
+ {
950
+ workItem: {
951
+ id: 102,
952
+ workItemFields: [{ key: 'Steps', value: '<steps></steps>' }],
953
+ },
954
+ },
955
+ ],
956
+ },
957
+ ]);
958
+ jest.spyOn(resultDataProvider, 'fetchMewpL2Requirements').mockResolvedValueOnce([
959
+ {
960
+ workItemId: 5001,
961
+ requirementId: 'SR1001',
962
+ title: 'Covered requirement',
963
+ responsibility: 'ESUK',
964
+ linkedTestCaseIds: [101],
965
+ },
966
+ {
967
+ workItemId: 5002,
968
+ requirementId: 'SR1002',
969
+ title: 'Referenced from non-linked step text',
970
+ responsibility: 'IL',
971
+ linkedTestCaseIds: [],
972
+ },
973
+ {
974
+ workItemId: 5003,
975
+ requirementId: 'SR1003',
976
+ title: 'Not covered by any test case',
977
+ responsibility: 'IL',
978
+ linkedTestCaseIds: [],
979
+ },
980
+ ]);
981
+ jest.spyOn(resultDataProvider, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([
982
+ {
983
+ testCaseId: 101,
984
+ testCase: { id: 101, name: 'TC 101' },
985
+ iteration: {
986
+ actionResults: [
987
+ {
988
+ action: 'Validate <b>S</b><b>R</b> 1 0 0 1 happy path',
989
+ expected: '',
990
+ outcome: 'Passed',
991
+ },
992
+ { action: 'Validate SR1001 failed flow', expected: '&nbsp;', outcome: 'Failed' },
993
+ { action: '', expected: 'Pending S R 1 0 0 1 scenario', outcome: 'Unspecified' },
994
+ ],
995
+ },
996
+ },
997
+ {
998
+ testCaseId: 102,
999
+ testCase: { id: 102, name: 'TC 102' },
1000
+ iteration: undefined,
1001
+ },
1002
+ ]);
1003
+ jest
1004
+ .spyOn(resultDataProvider.testStepParserHelper, 'parseTestSteps')
1005
+ .mockResolvedValueOnce([
1006
+ {
1007
+ stepId: '1',
1008
+ stepPosition: '1',
1009
+ action: 'Definition contains SR1002',
1010
+ expected: '',
1011
+ isSharedStepTitle: false,
1012
+ },
1013
+ ]);
1014
+ const result = await resultDataProvider.getMewpL2CoverageFlatResults('123', mockProjectName, [1]);
1015
+ expect(result).toEqual(expect.objectContaining({
1016
+ sheetName: expect.stringContaining('MEWP L2 Coverage'),
1017
+ columnOrder: expect.arrayContaining(['Customer ID', 'Test case id', 'Number of not run tests']),
1018
+ }));
1019
+ const covered = result.rows.find((row) => row['Customer ID'] === 'SR1001' && row['Test case id'] === 101);
1020
+ const inferredByStepText = result.rows.find((row) => row['Customer ID'] === 'SR1002' && row['Test case id'] === 102);
1021
+ const uncovered = result.rows.find((row) => row['Customer ID'] === 'SR1003' &&
1022
+ (row['Test case id'] === '' || row['Test case id'] === undefined || row['Test case id'] === null));
1023
+ expect(covered).toEqual(expect.objectContaining({
1024
+ 'Title (Customer name)': 'Covered requirement',
1025
+ 'Responsibility - SAPWBS (ESUK/IL)': 'ESUK',
1026
+ 'Test case title': 'TC 101',
1027
+ 'Number of passed steps': 1,
1028
+ 'Number of failed steps': 1,
1029
+ 'Number of not run tests': 1,
1030
+ }));
1031
+ expect(inferredByStepText).toEqual(expect.objectContaining({
1032
+ 'Title (Customer name)': 'Referenced from non-linked step text',
1033
+ 'Responsibility - SAPWBS (ESUK/IL)': 'IL',
1034
+ 'Test case title': 'TC 102',
1035
+ 'Number of passed steps': 0,
1036
+ 'Number of failed steps': 0,
1037
+ 'Number of not run tests': 1,
1038
+ }));
1039
+ expect(uncovered).toEqual(expect.objectContaining({
1040
+ 'Title (Customer name)': 'Not covered by any test case',
1041
+ 'Responsibility - SAPWBS (ESUK/IL)': 'IL',
1042
+ 'Test case title': '',
1043
+ 'Number of passed steps': 0,
1044
+ 'Number of failed steps': 0,
1045
+ 'Number of not run tests': 0,
1046
+ }));
1047
+ });
1048
+ it('should extract SR ids from HTML/spacing and return unique ids per step text', () => {
1049
+ const text = 'A: <b>S</b><b>R</b> 0 0 0 1; B: SR0002; C: S R 0 0 0 3; D: SR0002; E: &lt;b&gt;SR&lt;/b&gt;0004';
1050
+ const codes = resultDataProvider.extractRequirementCodesFromText(text);
1051
+ expect([...codes].sort()).toEqual(['SR1', 'SR2', 'SR3', 'SR4']);
1052
+ });
1053
+ it('should not backfill definition steps as not-run when a real run exists but has no action results', async () => {
1054
+ jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1055
+ jest.spyOn(resultDataProvider, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1056
+ jest.spyOn(resultDataProvider, 'fetchTestData').mockResolvedValueOnce([
1057
+ {
1058
+ testPointsItems: [{ testCaseId: 101, lastRunId: 88, lastResultId: 99, testCaseName: 'TC 101' }],
1059
+ testCasesItems: [
1060
+ {
1061
+ workItem: {
1062
+ id: 101,
1063
+ workItemFields: [{ key: 'Steps', value: '<steps></steps>' }],
1064
+ },
1065
+ },
1066
+ ],
1067
+ },
1068
+ ]);
1069
+ jest.spyOn(resultDataProvider, 'fetchMewpL2Requirements').mockResolvedValueOnce([
1070
+ {
1071
+ workItemId: 7001,
1072
+ requirementId: 'SR2001',
1073
+ title: 'Has run but no actions',
1074
+ responsibility: 'ESUK',
1075
+ linkedTestCaseIds: [101],
1076
+ },
1077
+ ]);
1078
+ jest.spyOn(resultDataProvider, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([
1079
+ {
1080
+ testCaseId: 101,
1081
+ lastRunId: 88,
1082
+ lastResultId: 99,
1083
+ iteration: {
1084
+ actionResults: [],
1085
+ },
1086
+ },
1087
+ ]);
1088
+ const parseSpy = jest
1089
+ .spyOn(resultDataProvider.testStepParserHelper, 'parseTestSteps')
1090
+ .mockResolvedValueOnce([
1091
+ {
1092
+ stepId: '1',
1093
+ stepPosition: '1',
1094
+ action: 'SR2001 from definition',
1095
+ expected: '',
1096
+ isSharedStepTitle: false,
1097
+ },
1098
+ ]);
1099
+ const result = await resultDataProvider.getMewpL2CoverageFlatResults('123', mockProjectName, [1]);
1100
+ const row = result.rows.find((item) => item['Customer ID'] === 'SR2001' && item['Test case id'] === 101);
1101
+ expect(parseSpy).not.toHaveBeenCalled();
1102
+ expect(row).toEqual(expect.objectContaining({
1103
+ 'Number of passed steps': 0,
1104
+ 'Number of failed steps': 0,
1105
+ 'Number of not run tests': 0,
1106
+ }));
1107
+ });
1108
+ it('should not infer requirement id from unrelated SR text in non-identifier fields', () => {
1109
+ const requirementId = resultDataProvider.extractMewpRequirementIdentifier({
1110
+ 'System.Description': 'random text with SR9999 that is unrelated',
1111
+ 'Custom.CustomerId': 'customer id unknown',
1112
+ 'System.Title': 'Requirement without explicit SR code',
1113
+ }, 4321);
1114
+ expect(requirementId).toBe('4321');
1115
+ });
1116
+ });
859
1117
  describe('fetchResultDataForTestReporter (runResultField switch)', () => {
860
1118
  it('should populate requested runResultField values including testCaseResult URL branches', async () => {
861
1119
  jest
@@ -1945,6 +2203,186 @@ describe('ResultDataProvider', () => {
1945
2203
  expect(actionResults[0]).toEqual(expect.objectContaining({ stepIdentifier: '1', action: 'A1' }));
1946
2204
  expect(actionResults[1]).toEqual(expect.objectContaining({ stepIdentifier: '2', action: 'A2', isSharedStepTitle: true }));
1947
2205
  });
2206
+ it('should fall back to parsed test steps when actionResults are missing for latest run', async () => {
2207
+ const point = { testCaseId: 1, lastRunId: 10, lastResultId: 20 };
2208
+ const fetchResultMethod = jest.fn().mockResolvedValue({
2209
+ testCase: { id: 1, name: 'TC' },
2210
+ stepsResultXml: '<steps></steps>',
2211
+ iterationDetails: [{}],
2212
+ });
2213
+ const createResponseObject = (resultData) => ({ iteration: resultData.iterationDetails[0] });
2214
+ const helper = resultDataProvider.testStepParserHelper;
2215
+ helper.parseTestSteps.mockResolvedValueOnce([
2216
+ { stepId: 172, stepPosition: '1', action: 'Step 1', expected: 'Expected 1', isSharedStepTitle: false },
2217
+ { stepId: 173, stepPosition: '2', action: 'Step 2', expected: 'Expected 2', isSharedStepTitle: false },
2218
+ ]);
2219
+ const res = await resultDataProvider.fetchResultDataBase(mockProjectName, 'suite1', point, fetchResultMethod, createResponseObject, []);
2220
+ const actionResults = res.iteration.actionResults;
2221
+ expect(actionResults).toHaveLength(2);
2222
+ expect(actionResults[0]).toEqual(expect.objectContaining({
2223
+ stepIdentifier: '172',
2224
+ stepPosition: '1',
2225
+ action: 'Step 1',
2226
+ expected: 'Expected 1',
2227
+ outcome: 'Unspecified',
2228
+ }));
2229
+ expect(actionResults[1]).toEqual(expect.objectContaining({
2230
+ stepIdentifier: '173',
2231
+ stepPosition: '2',
2232
+ action: 'Step 2',
2233
+ expected: 'Expected 2',
2234
+ outcome: 'Unspecified',
2235
+ }));
2236
+ });
2237
+ it('should create synthetic iteration and fall back to parsed test steps when iterationDetails are missing', async () => {
2238
+ const point = { testCaseId: 1, lastRunId: 10, lastResultId: 20 };
2239
+ const fetchResultMethod = jest.fn().mockResolvedValue({
2240
+ testCase: { id: 1, name: 'TC' },
2241
+ stepsResultXml: '<steps></steps>',
2242
+ iterationDetails: [],
2243
+ });
2244
+ const createResponseObject = (resultData) => ({ iteration: resultData.iterationDetails[0] });
2245
+ const helper = resultDataProvider.testStepParserHelper;
2246
+ helper.parseTestSteps.mockResolvedValueOnce([
2247
+ { stepId: 301, stepPosition: '1', action: 'S1', expected: 'E1', isSharedStepTitle: false },
2248
+ ]);
2249
+ const res = await resultDataProvider.fetchResultDataBase(mockProjectName, 'suite1', point, fetchResultMethod, createResponseObject, []);
2250
+ expect(res.iteration).toBeDefined();
2251
+ expect(res.iteration.actionResults).toHaveLength(1);
2252
+ expect(res.iteration.actionResults[0]).toEqual(expect.objectContaining({
2253
+ stepIdentifier: '301',
2254
+ stepPosition: '1',
2255
+ action: 'S1',
2256
+ expected: 'E1',
2257
+ outcome: 'Unspecified',
2258
+ }));
2259
+ });
2260
+ it('should fall back to parsed test steps when actionResults is an empty array', async () => {
2261
+ const point = { testCaseId: 1, lastRunId: 10, lastResultId: 20 };
2262
+ const fetchResultMethod = jest.fn().mockResolvedValue({
2263
+ testCase: { id: 1, name: 'TC' },
2264
+ stepsResultXml: '<steps></steps>',
2265
+ iterationDetails: [{ actionResults: [] }],
2266
+ });
2267
+ const createResponseObject = (resultData) => ({ iteration: resultData.iterationDetails[0] });
2268
+ const helper = resultDataProvider.testStepParserHelper;
2269
+ helper.parseTestSteps.mockResolvedValueOnce([
2270
+ { stepId: 11, stepPosition: '1', action: 'A1', expected: 'E1', isSharedStepTitle: false },
2271
+ { stepId: 22, stepPosition: '2', action: 'A2', expected: 'E2', isSharedStepTitle: false },
2272
+ ]);
2273
+ const res = await resultDataProvider.fetchResultDataBase(mockProjectName, 'suite1', point, fetchResultMethod, createResponseObject, []);
2274
+ expect(helper.parseTestSteps).toHaveBeenCalledTimes(1);
2275
+ expect(res.iteration.actionResults).toHaveLength(2);
2276
+ expect(res.iteration.actionResults[0]).toEqual(expect.objectContaining({
2277
+ stepIdentifier: '11',
2278
+ stepPosition: '1',
2279
+ action: 'A1',
2280
+ expected: 'E1',
2281
+ outcome: 'Unspecified',
2282
+ }));
2283
+ expect(res.iteration.actionResults[1]).toEqual(expect.objectContaining({
2284
+ stepIdentifier: '22',
2285
+ stepPosition: '2',
2286
+ action: 'A2',
2287
+ expected: 'E2',
2288
+ outcome: 'Unspecified',
2289
+ }));
2290
+ });
2291
+ });
2292
+ describe('getTestReporterFlatResults', () => {
2293
+ it('should return flat rows with logical step numbering for fallback-generated action results', async () => {
2294
+ jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan 12');
2295
+ jest.spyOn(resultDataProvider, 'fetchTestSuites').mockResolvedValueOnce([
2296
+ {
2297
+ testSuiteId: 200,
2298
+ suiteId: 200,
2299
+ suiteName: 'suite 2.1',
2300
+ parentSuiteId: 100,
2301
+ parentSuiteName: 'Rel2',
2302
+ suitePath: 'Root/Rel2/suite 2.1',
2303
+ testGroupName: 'suite 2.1',
2304
+ },
2305
+ ]);
2306
+ const testData = [
2307
+ {
2308
+ testSuiteId: 200,
2309
+ suiteId: 200,
2310
+ suiteName: 'suite 2.1',
2311
+ parentSuiteId: 100,
2312
+ parentSuiteName: 'Rel2',
2313
+ suitePath: 'Root/Rel2/suite 2.1',
2314
+ testGroupName: 'suite 2.1',
2315
+ testPointsItems: [
2316
+ {
2317
+ testCaseId: 17,
2318
+ testCaseName: 'TC 17',
2319
+ outcome: 'Unspecified',
2320
+ lastRunId: 99,
2321
+ lastResultId: 88,
2322
+ testPointId: 501,
2323
+ lastResultDetails: {
2324
+ dateCompleted: '2026-02-01T10:00:00.000Z',
2325
+ outcome: 'Unspecified',
2326
+ runBy: { displayName: 'tester user' },
2327
+ },
2328
+ },
2329
+ ],
2330
+ testCasesItems: [
2331
+ {
2332
+ workItem: {
2333
+ id: 17,
2334
+ workItemFields: [{ key: 'Steps', value: '<steps></steps>' }],
2335
+ },
2336
+ },
2337
+ ],
2338
+ },
2339
+ ];
2340
+ jest.spyOn(resultDataProvider, 'fetchTestData').mockResolvedValueOnce(testData);
2341
+ jest.spyOn(resultDataProvider, 'fetchAllResultDataTestReporter').mockResolvedValueOnce([
2342
+ {
2343
+ testCaseId: 17,
2344
+ lastRunId: 99,
2345
+ lastResultId: 88,
2346
+ executionDate: '2026-02-01T10:00:00.000Z',
2347
+ testCaseResult: { resultMessage: '' },
2348
+ customFields: { 'Custom.SubSystem': 'SYS' },
2349
+ runBy: 'tester user',
2350
+ iteration: {
2351
+ actionResults: [
2352
+ {
2353
+ stepIdentifier: '172',
2354
+ stepPosition: '1',
2355
+ actionPath: '1',
2356
+ action: 'fallback action',
2357
+ expected: 'fallback expected',
2358
+ outcome: 'Unspecified',
2359
+ errorMessage: '',
2360
+ isSharedStepTitle: false,
2361
+ },
2362
+ ],
2363
+ },
2364
+ },
2365
+ ]);
2366
+ const result = await resultDataProvider.getTestReporterFlatResults(mockTestPlanId, mockProjectName, undefined, [], false);
2367
+ expect(result.planId).toBe(mockTestPlanId);
2368
+ expect(result.planName).toBe('Plan 12');
2369
+ expect(result.rows).toHaveLength(1);
2370
+ expect(result.rows[0]).toEqual(expect.objectContaining({
2371
+ planId: mockTestPlanId,
2372
+ planName: 'Plan 12',
2373
+ suiteId: 200,
2374
+ suiteName: 'suite 2.1',
2375
+ parentSuiteId: 100,
2376
+ parentSuiteName: 'Rel2',
2377
+ suitePath: 'Root/Rel2/suite 2.1',
2378
+ testCaseId: 17,
2379
+ testCaseName: 'TC 17',
2380
+ testRunId: 99,
2381
+ testPointId: 501,
2382
+ stepOutcome: 'Unspecified',
2383
+ stepStepIdentifier: '1',
2384
+ }));
2385
+ });
1948
2386
  });
1949
2387
  describe('getCombinedResultsSummary - appendix branches', () => {
1950
2388
  it('should use mapAttachmentsUrl when stepAnalysis.generateRunAttachments is enabled and stepExecution.runAttachmentMode != planOnly', async () => {