@elisra-devops/docgen-data-provider 1.75.0 → 1.77.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.
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tfs_1 = require("../../helpers/tfs");
4
4
  const ResultDataProvider_1 = require("../../modules/ResultDataProvider");
5
5
  const logger_1 = require("../../utils/logger");
6
+ const axios_1 = require("axios");
7
+ const XLSX = require("xlsx");
6
8
  // Mock dependencies
7
9
  jest.mock('../../helpers/tfs');
8
10
  jest.mock('../../utils/logger');
@@ -22,6 +24,12 @@ describe('ResultDataProvider', () => {
22
24
  const mockToken = 'mock-token';
23
25
  const mockProjectName = 'test-project';
24
26
  const mockTestPlanId = '12345';
27
+ const buildWorkbookBuffer = (rows) => {
28
+ const worksheet = XLSX.utils.aoa_to_sheet(rows);
29
+ const workbook = XLSX.utils.book_new();
30
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
31
+ return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
32
+ };
25
33
  beforeEach(() => {
26
34
  jest.clearAllMocks();
27
35
  resultDataProvider = new ResultDataProvider_1.default(mockOrgUrl, mockToken);
@@ -880,6 +888,8 @@ describe('ResultDataProvider', () => {
880
888
  {
881
889
  workItemId: 5001,
882
890
  requirementId: 'SR1001',
891
+ baseKey: 'SR1001',
892
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
883
893
  title: 'Covered requirement',
884
894
  responsibility: 'ESUK',
885
895
  linkedTestCaseIds: [101],
@@ -887,6 +897,8 @@ describe('ResultDataProvider', () => {
887
897
  {
888
898
  workItemId: 5002,
889
899
  requirementId: 'SR1002',
900
+ baseKey: 'SR1002',
901
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
890
902
  title: 'Referenced from non-linked step text',
891
903
  responsibility: 'IL',
892
904
  linkedTestCaseIds: [],
@@ -894,6 +906,8 @@ describe('ResultDataProvider', () => {
894
906
  {
895
907
  workItemId: 5003,
896
908
  requirementId: 'SR1003',
909
+ baseKey: 'SR1003',
910
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
897
911
  title: 'Not covered by any test case',
898
912
  responsibility: 'IL',
899
913
  linkedTestCaseIds: [],
@@ -907,11 +921,11 @@ describe('ResultDataProvider', () => {
907
921
  actionResults: [
908
922
  {
909
923
  action: 'Validate <b>S</b><b>R</b> 1 0 0 1 happy path',
910
- expected: '',
924
+ expected: '<b>S</b><b>R</b> 1 0 0 1',
911
925
  outcome: 'Passed',
912
926
  },
913
- { action: 'Validate SR1001 failed flow', expected: '&nbsp;', outcome: 'Failed' },
914
- { action: '', expected: 'Pending S R 1 0 0 1 scenario', outcome: 'Unspecified' },
927
+ { action: 'Validate SR1001 failed flow', expected: 'SR1001', outcome: 'Failed' },
928
+ { action: '', expected: 'S R 1 0 0 1', outcome: 'Unspecified' },
915
929
  ],
916
930
  },
917
931
  },
@@ -928,48 +942,59 @@ describe('ResultDataProvider', () => {
928
942
  stepId: '1',
929
943
  stepPosition: '1',
930
944
  action: 'Definition contains SR1002',
931
- expected: '',
945
+ expected: 'SR1002',
932
946
  isSharedStepTitle: false,
933
947
  },
934
948
  ]);
935
949
  const result = await resultDataProvider.getMewpL2CoverageFlatResults('123', mockProjectName, [1]);
936
950
  expect(result).toEqual(expect.objectContaining({
937
951
  sheetName: expect.stringContaining('MEWP L2 Coverage'),
938
- columnOrder: expect.arrayContaining(['Customer ID', 'Test case id', 'Number of not run tests']),
952
+ columnOrder: expect.arrayContaining(['L2 REQ ID', 'L2 REQ Title', 'L2 Run Status']),
939
953
  }));
940
- const covered = result.rows.find((row) => row['Customer ID'] === 'SR1001' && row['Test case id'] === 101);
941
- const inferredByStepText = result.rows.find((row) => row['Customer ID'] === 'SR1002' && row['Test case id'] === 102);
942
- const uncovered = result.rows.find((row) => row['Customer ID'] === 'SR1003' &&
943
- (row['Test case id'] === '' || row['Test case id'] === undefined || row['Test case id'] === null));
954
+ const covered = result.rows.find((row) => row['L2 REQ ID'] === 'SR1001');
955
+ const inferredByStepText = result.rows.find((row) => row['L2 REQ ID'] === 'SR1002');
956
+ const uncovered = result.rows.find((row) => row['L2 REQ ID'] === 'SR1003');
944
957
  expect(covered).toEqual(expect.objectContaining({
945
- 'Title (Customer name)': 'Covered requirement',
946
- 'Responsibility - SAPWBS (ESUK/IL)': 'ESUK',
947
- 'Test case title': 'TC 101',
948
- 'Number of passed steps': 1,
949
- 'Number of failed steps': 1,
950
- 'Number of not run tests': 1,
958
+ 'L2 REQ Title': 'Covered requirement',
959
+ 'L2 SubSystem': '',
960
+ 'L2 Run Status': 'Fail',
961
+ 'Bug ID': '',
962
+ 'L3 REQ ID': '',
963
+ 'L4 REQ ID': '',
951
964
  }));
952
965
  expect(inferredByStepText).toEqual(expect.objectContaining({
953
- 'Title (Customer name)': 'Referenced from non-linked step text',
954
- 'Responsibility - SAPWBS (ESUK/IL)': 'IL',
955
- 'Test case title': 'TC 102',
956
- 'Number of passed steps': 0,
957
- 'Number of failed steps': 0,
958
- 'Number of not run tests': 1,
966
+ 'L2 REQ Title': 'Referenced from non-linked step text',
967
+ 'L2 SubSystem': '',
968
+ 'L2 Run Status': 'Not Run',
959
969
  }));
960
970
  expect(uncovered).toEqual(expect.objectContaining({
961
- 'Title (Customer name)': 'Not covered by any test case',
962
- 'Responsibility - SAPWBS (ESUK/IL)': 'IL',
963
- 'Test case title': '',
964
- 'Number of passed steps': 0,
965
- 'Number of failed steps': 0,
966
- 'Number of not run tests': 0,
971
+ 'L2 REQ Title': 'Not covered by any test case',
972
+ 'L2 SubSystem': '',
973
+ 'L2 Run Status': 'Not Run',
967
974
  }));
968
975
  });
969
976
  it('should extract SR ids from HTML/spacing and return unique ids per step text', () => {
970
- 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';
977
+ const text = '<b>S</b><b>R</b> 0 0 0 1; SR0002; S R 0 0 0 3; SR0002; &lt;b&gt;SR&lt;/b&gt;0004';
971
978
  const codes = resultDataProvider.extractRequirementCodesFromText(text);
972
- expect([...codes].sort()).toEqual(['SR1', 'SR2', 'SR3', 'SR4']);
979
+ expect([...codes].sort()).toEqual(['SR0001', 'SR0002', 'SR0003', 'SR0004']);
980
+ });
981
+ it('should keep only clean SR tokens and ignore noisy version/VVRM fragments', () => {
982
+ const extract = (text) => [...resultDataProvider.extractRequirementCodesFromText(text)].sort();
983
+ expect(extract('SR12413; SR24513; SR25135 VVRM2425')).toEqual(['SR12413', 'SR24513']);
984
+ expect(extract('SR12413; SR12412; SR12413-V3.24')).toEqual(['SR12412', 'SR12413']);
985
+ expect(extract('SR12413; SR12412; SR12413 V3.24')).toEqual(['SR12412', 'SR12413']);
986
+ expect(extract('SR12413, SR12412, SR12413-V3.24')).toEqual(['SR12412', 'SR12413']);
987
+ const extractWithSuffix = (text) => [
988
+ ...resultDataProvider.extractRequirementCodesFromExpectedText(text, true),
989
+ ].sort();
990
+ expect(extractWithSuffix('SR0095-2,3; SR0100-1,2,3,4')).toEqual([
991
+ 'SR0095-2',
992
+ 'SR0095-3',
993
+ 'SR0100-1',
994
+ 'SR0100-2',
995
+ 'SR0100-3',
996
+ 'SR0100-4',
997
+ ]);
973
998
  });
974
999
  it('should not backfill definition steps as not-run when a real run exists but has no action results', async () => {
975
1000
  jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
@@ -991,6 +1016,8 @@ describe('ResultDataProvider', () => {
991
1016
  {
992
1017
  workItemId: 7001,
993
1018
  requirementId: 'SR2001',
1019
+ baseKey: 'SR2001',
1020
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
994
1021
  title: 'Has run but no actions',
995
1022
  responsibility: 'ESUK',
996
1023
  linkedTestCaseIds: [101],
@@ -1018,12 +1045,10 @@ describe('ResultDataProvider', () => {
1018
1045
  },
1019
1046
  ]);
1020
1047
  const result = await resultDataProvider.getMewpL2CoverageFlatResults('123', mockProjectName, [1]);
1021
- const row = result.rows.find((item) => item['Customer ID'] === 'SR2001' && item['Test case id'] === 101);
1048
+ const row = result.rows.find((item) => item['L2 REQ ID'] === 'SR2001');
1022
1049
  expect(parseSpy).not.toHaveBeenCalled();
1023
1050
  expect(row).toEqual(expect.objectContaining({
1024
- 'Number of passed steps': 0,
1025
- 'Number of failed steps': 0,
1026
- 'Number of not run tests': 0,
1051
+ 'L2 Run Status': 'Not Run',
1027
1052
  }));
1028
1053
  });
1029
1054
  it('should not infer requirement id from unrelated SR text in non-identifier fields', () => {
@@ -1041,6 +1066,1233 @@ describe('ResultDataProvider', () => {
1041
1066
  });
1042
1067
  expect(responsibility).toBe('IL');
1043
1068
  });
1069
+ it('should derive responsibility from AreaPath suffix ATP/ATP\\\\ESUK when SAPWBS is empty', () => {
1070
+ const esuk = resultDataProvider.deriveMewpResponsibility({
1071
+ 'Custom.SAPWBS': '',
1072
+ 'System.AreaPath': 'MEWP\\Customer Requirements\\Level 2\\ATP\\ESUK',
1073
+ });
1074
+ const il = resultDataProvider.deriveMewpResponsibility({
1075
+ 'Custom.SAPWBS': '',
1076
+ 'System.AreaPath': 'MEWP\\Customer Requirements\\Level 2\\ATP',
1077
+ });
1078
+ expect(esuk).toBe('ESUK');
1079
+ expect(il).toBe('IL');
1080
+ });
1081
+ it('should emit additive rows for bugs and L3/L4 links without bug duplication', () => {
1082
+ const requirements = [
1083
+ {
1084
+ requirementId: 'SR5303',
1085
+ baseKey: 'SR5303',
1086
+ title: 'Req 5303',
1087
+ subSystem: 'Power',
1088
+ responsibility: 'ESUK',
1089
+ linkedTestCaseIds: [101],
1090
+ },
1091
+ ];
1092
+ const requirementIndex = new Map([
1093
+ [
1094
+ 'SR5303',
1095
+ new Map([
1096
+ [
1097
+ 101,
1098
+ {
1099
+ passed: 0,
1100
+ failed: 1,
1101
+ notRun: 0,
1102
+ },
1103
+ ],
1104
+ ]),
1105
+ ],
1106
+ ]);
1107
+ const observedTestCaseIdsByRequirement = new Map([
1108
+ ['SR5303', new Set([101])],
1109
+ ]);
1110
+ const linkedRequirementsByTestCase = new Map([
1111
+ [
1112
+ 101,
1113
+ {
1114
+ baseKeys: new Set(['SR5303']),
1115
+ fullCodes: new Set(['SR5303']),
1116
+ bugIds: new Set([10003, 20003]),
1117
+ },
1118
+ ],
1119
+ ]);
1120
+ const externalBugsByTestCase = new Map([
1121
+ [
1122
+ 101,
1123
+ [
1124
+ { id: 10003, title: 'Bug 10003', responsibility: 'Elisra', requirementBaseKey: 'SR5303' },
1125
+ { id: 20003, title: 'Bug 20003', responsibility: 'ESUK', requirementBaseKey: 'SR5303' },
1126
+ ],
1127
+ ],
1128
+ ]);
1129
+ const l3l4ByBaseKey = new Map([
1130
+ [
1131
+ 'SR5303',
1132
+ [
1133
+ { id: '9003', title: 'L3 9003', level: 'L3' },
1134
+ { id: '9103', title: 'L4 9103', level: 'L4' },
1135
+ ],
1136
+ ],
1137
+ ]);
1138
+ const rows = resultDataProvider.buildMewpCoverageRows(requirements, requirementIndex, observedTestCaseIdsByRequirement, linkedRequirementsByTestCase, l3l4ByBaseKey, externalBugsByTestCase);
1139
+ expect(rows).toHaveLength(4);
1140
+ expect(rows.map((row) => row['Bug ID'])).toEqual([10003, 20003, '', '']);
1141
+ expect(rows[2]).toEqual(expect.objectContaining({
1142
+ 'L3 REQ ID': '9003',
1143
+ 'L4 REQ ID': '',
1144
+ }));
1145
+ expect(rows[3]).toEqual(expect.objectContaining({
1146
+ 'L3 REQ ID': '',
1147
+ 'L4 REQ ID': '9103',
1148
+ }));
1149
+ });
1150
+ it('should not emit bug rows from ADO-linked bug ids when external bugs source is empty', () => {
1151
+ const requirements = [
1152
+ {
1153
+ requirementId: 'SR5304',
1154
+ baseKey: 'SR5304',
1155
+ title: 'Req 5304',
1156
+ subSystem: 'Power',
1157
+ responsibility: 'ESUK',
1158
+ linkedTestCaseIds: [101],
1159
+ },
1160
+ ];
1161
+ const requirementIndex = new Map([
1162
+ [
1163
+ 'SR5304',
1164
+ new Map([
1165
+ [
1166
+ 101,
1167
+ {
1168
+ passed: 0,
1169
+ failed: 1,
1170
+ notRun: 0,
1171
+ },
1172
+ ],
1173
+ ]),
1174
+ ],
1175
+ ]);
1176
+ const observedTestCaseIdsByRequirement = new Map([
1177
+ ['SR5304', new Set([101])],
1178
+ ]);
1179
+ const linkedRequirementsByTestCase = new Map([
1180
+ [
1181
+ 101,
1182
+ {
1183
+ baseKeys: new Set(['SR5304']),
1184
+ fullCodes: new Set(['SR5304']),
1185
+ bugIds: new Set([55555]), // must be ignored in MEWP coverage mode
1186
+ },
1187
+ ],
1188
+ ]);
1189
+ const rows = resultDataProvider.buildMewpCoverageRows(requirements, requirementIndex, observedTestCaseIdsByRequirement, linkedRequirementsByTestCase, new Map(), new Map());
1190
+ expect(rows).toHaveLength(1);
1191
+ expect(rows[0]['L2 Run Status']).toBe('Fail');
1192
+ expect(rows[0]['Bug ID']).toBe('');
1193
+ expect(rows[0]['Bug Title']).toBe('');
1194
+ expect(rows[0]['Bug Responsibility']).toBe('');
1195
+ });
1196
+ it('should fallback bug responsibility from requirement when external bug row has unknown responsibility', () => {
1197
+ const requirements = [
1198
+ {
1199
+ requirementId: 'SR5305',
1200
+ baseKey: 'SR5305',
1201
+ title: 'Req 5305',
1202
+ subSystem: 'Auth',
1203
+ responsibility: 'IL',
1204
+ linkedTestCaseIds: [202],
1205
+ },
1206
+ ];
1207
+ const requirementIndex = new Map([
1208
+ [
1209
+ 'SR5305',
1210
+ new Map([
1211
+ [
1212
+ 202,
1213
+ {
1214
+ passed: 0,
1215
+ failed: 1,
1216
+ notRun: 0,
1217
+ },
1218
+ ],
1219
+ ]),
1220
+ ],
1221
+ ]);
1222
+ const observedTestCaseIdsByRequirement = new Map([
1223
+ ['SR5305', new Set([202])],
1224
+ ]);
1225
+ const linkedRequirementsByTestCase = new Map([
1226
+ [
1227
+ 202,
1228
+ {
1229
+ baseKeys: new Set(['SR5305']),
1230
+ fullCodes: new Set(['SR5305']),
1231
+ bugIds: new Set(),
1232
+ },
1233
+ ],
1234
+ ]);
1235
+ const externalBugsByTestCase = new Map([
1236
+ [
1237
+ 202,
1238
+ [
1239
+ {
1240
+ id: 99001,
1241
+ title: 'External bug without SAPWBS',
1242
+ responsibility: 'Unknown',
1243
+ requirementBaseKey: 'SR5305',
1244
+ },
1245
+ ],
1246
+ ],
1247
+ ]);
1248
+ const rows = resultDataProvider.buildMewpCoverageRows(requirements, requirementIndex, observedTestCaseIdsByRequirement, linkedRequirementsByTestCase, new Map(), externalBugsByTestCase);
1249
+ expect(rows).toHaveLength(1);
1250
+ expect(rows[0]['Bug ID']).toBe(99001);
1251
+ expect(rows[0]['Bug Responsibility']).toBe('Elisra');
1252
+ });
1253
+ });
1254
+ describe('getMewpInternalValidationFlatResults', () => {
1255
+ it('should skip test cases with no in-scope expected requirements and no linked requirements', async () => {
1256
+ jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1257
+ jest.spyOn(resultDataProvider, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1258
+ jest.spyOn(resultDataProvider, 'fetchTestData').mockResolvedValueOnce([
1259
+ {
1260
+ testPointsItems: [
1261
+ { testCaseId: 101, testCaseName: 'TC 101' },
1262
+ { testCaseId: 102, testCaseName: 'TC 102' },
1263
+ ],
1264
+ testCasesItems: [
1265
+ {
1266
+ workItem: {
1267
+ id: 101,
1268
+ workItemFields: [{ key: 'Steps', value: '<steps></steps>' }],
1269
+ },
1270
+ },
1271
+ {
1272
+ workItem: {
1273
+ id: 102,
1274
+ workItemFields: [{ key: 'Steps', value: '<steps></steps>' }],
1275
+ },
1276
+ },
1277
+ ],
1278
+ },
1279
+ ]);
1280
+ jest.spyOn(resultDataProvider, 'fetchMewpL2Requirements').mockResolvedValueOnce([
1281
+ {
1282
+ workItemId: 5001,
1283
+ requirementId: 'SR0001',
1284
+ baseKey: 'SR0001',
1285
+ title: 'Req 1',
1286
+ responsibility: 'ESUK',
1287
+ linkedTestCaseIds: [101],
1288
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1289
+ },
1290
+ ]);
1291
+ jest.spyOn(resultDataProvider, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(new Map([[101, { baseKeys: new Set(['SR0001']), fullCodes: new Set(['SR0001']) }]]));
1292
+ jest
1293
+ .spyOn(resultDataProvider.testStepParserHelper, 'parseTestSteps')
1294
+ .mockResolvedValueOnce([
1295
+ {
1296
+ stepId: '1',
1297
+ stepPosition: '1',
1298
+ action: '',
1299
+ expected: 'SR0001',
1300
+ isSharedStepTitle: false,
1301
+ },
1302
+ ])
1303
+ .mockResolvedValueOnce([
1304
+ {
1305
+ stepId: '1',
1306
+ stepPosition: '1',
1307
+ action: '',
1308
+ expected: '',
1309
+ isSharedStepTitle: false,
1310
+ },
1311
+ ]);
1312
+ const result = await resultDataProvider.getMewpInternalValidationFlatResults('123', mockProjectName, [1]);
1313
+ expect(result.rows).toHaveLength(2);
1314
+ expect(result.rows).toEqual(expect.arrayContaining([
1315
+ expect.objectContaining({
1316
+ 'Test Case ID': 101,
1317
+ 'Validation Status': 'Pass',
1318
+ 'Mentioned but Not Linked': '',
1319
+ 'Linked but Not Mentioned': '',
1320
+ }),
1321
+ expect.objectContaining({
1322
+ 'Test Case ID': 102,
1323
+ 'Validation Status': 'Pass',
1324
+ 'Mentioned but Not Linked': '',
1325
+ 'Linked but Not Mentioned': '',
1326
+ }),
1327
+ ]));
1328
+ });
1329
+ it('should ignore non-L2 SR mentions and still report only real L2 discrepancies', async () => {
1330
+ jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1331
+ jest.spyOn(resultDataProvider, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1332
+ jest.spyOn(resultDataProvider, 'fetchTestData').mockResolvedValueOnce([
1333
+ {
1334
+ testPointsItems: [{ testCaseId: 101, testCaseName: 'TC 101' }],
1335
+ testCasesItems: [
1336
+ {
1337
+ workItem: {
1338
+ id: 101,
1339
+ workItemFields: [{ key: 'Steps', value: '<steps></steps>' }],
1340
+ },
1341
+ },
1342
+ ],
1343
+ },
1344
+ ]);
1345
+ jest.spyOn(resultDataProvider, 'fetchMewpL2Requirements').mockResolvedValueOnce([
1346
+ {
1347
+ workItemId: 5001,
1348
+ requirementId: 'SR0001',
1349
+ baseKey: 'SR0001',
1350
+ title: 'Req 1',
1351
+ responsibility: 'ESUK',
1352
+ linkedTestCaseIds: [101],
1353
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1354
+ },
1355
+ ]);
1356
+ jest.spyOn(resultDataProvider, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(new Map());
1357
+ jest.spyOn(resultDataProvider.testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
1358
+ {
1359
+ stepId: '1',
1360
+ stepPosition: '1',
1361
+ action: '',
1362
+ expected: 'SR0001; SR9999',
1363
+ isSharedStepTitle: false,
1364
+ },
1365
+ ]);
1366
+ const result = await resultDataProvider.getMewpInternalValidationFlatResults('123', mockProjectName, [1]);
1367
+ expect(result.rows).toHaveLength(1);
1368
+ expect(result.rows[0]).toEqual(expect.objectContaining({
1369
+ 'Test Case ID': 101,
1370
+ 'Mentioned but Not Linked': 'Step 1: SR0001',
1371
+ 'Linked but Not Mentioned': '',
1372
+ 'Validation Status': 'Fail',
1373
+ }));
1374
+ });
1375
+ it('should emit Direction A rows with step context for missing sibling links', async () => {
1376
+ jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1377
+ jest.spyOn(resultDataProvider, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1378
+ jest.spyOn(resultDataProvider, 'fetchTestData').mockResolvedValueOnce([
1379
+ {
1380
+ testPointsItems: [{ testCaseId: 101, testCaseName: 'TC 101' }],
1381
+ testCasesItems: [
1382
+ {
1383
+ workItem: {
1384
+ id: 101,
1385
+ workItemFields: [{ key: 'Steps', value: '<steps></steps>' }],
1386
+ },
1387
+ },
1388
+ ],
1389
+ },
1390
+ ]);
1391
+ jest.spyOn(resultDataProvider, 'fetchMewpL2Requirements').mockResolvedValueOnce([
1392
+ {
1393
+ workItemId: 5001,
1394
+ requirementId: 'SR0001',
1395
+ baseKey: 'SR0001',
1396
+ title: 'Req parent',
1397
+ responsibility: 'ESUK',
1398
+ linkedTestCaseIds: [101],
1399
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1400
+ },
1401
+ {
1402
+ workItemId: 5002,
1403
+ requirementId: 'SR0001-1',
1404
+ baseKey: 'SR0001',
1405
+ title: 'Req child',
1406
+ responsibility: 'ESUK',
1407
+ linkedTestCaseIds: [],
1408
+ areaPath: 'MEWP\\Customer Requirements\\Level 2\\MOP',
1409
+ },
1410
+ ]);
1411
+ jest.spyOn(resultDataProvider, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(new Map([[101, { baseKeys: new Set(['SR0001']), fullCodes: new Set(['SR0001']) }]]));
1412
+ jest.spyOn(resultDataProvider.testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
1413
+ {
1414
+ stepId: '2',
1415
+ stepPosition: '2',
1416
+ action: '',
1417
+ expected: 'SR0001; SR0002',
1418
+ isSharedStepTitle: false,
1419
+ },
1420
+ ]);
1421
+ const result = await resultDataProvider.getMewpInternalValidationFlatResults('123', mockProjectName, [1]);
1422
+ expect(result.rows).toEqual(expect.arrayContaining([
1423
+ expect.objectContaining({
1424
+ 'Test Case ID': 101,
1425
+ 'Mentioned but Not Linked': expect.stringContaining('Step 2: SR0001-1'),
1426
+ 'Validation Status': 'Fail',
1427
+ }),
1428
+ ]));
1429
+ });
1430
+ it('should not duplicate Direction A discrepancy when same requirement is repeated in multiple steps', async () => {
1431
+ jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1432
+ jest.spyOn(resultDataProvider, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1433
+ jest.spyOn(resultDataProvider, 'fetchTestData').mockResolvedValueOnce([
1434
+ {
1435
+ testPointsItems: [{ testCaseId: 101, testCaseName: 'TC 101' }],
1436
+ testCasesItems: [
1437
+ {
1438
+ workItem: {
1439
+ id: 101,
1440
+ workItemFields: [{ key: 'Steps', value: '<steps></steps>' }],
1441
+ },
1442
+ },
1443
+ ],
1444
+ },
1445
+ ]);
1446
+ jest.spyOn(resultDataProvider, 'fetchMewpL2Requirements').mockResolvedValueOnce([
1447
+ {
1448
+ workItemId: 5001,
1449
+ requirementId: 'SR0001',
1450
+ baseKey: 'SR0001',
1451
+ title: 'Req 1',
1452
+ responsibility: 'ESUK',
1453
+ linkedTestCaseIds: [],
1454
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1455
+ },
1456
+ ]);
1457
+ jest
1458
+ .spyOn(resultDataProvider, 'buildLinkedRequirementsByTestCase')
1459
+ .mockResolvedValueOnce(new Map());
1460
+ jest.spyOn(resultDataProvider.testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
1461
+ {
1462
+ stepId: '1',
1463
+ stepPosition: '1',
1464
+ action: '',
1465
+ expected: 'SR0001',
1466
+ isSharedStepTitle: false,
1467
+ },
1468
+ {
1469
+ stepId: '2',
1470
+ stepPosition: '2',
1471
+ action: '',
1472
+ expected: 'SR0001',
1473
+ isSharedStepTitle: false,
1474
+ },
1475
+ ]);
1476
+ const result = await resultDataProvider.getMewpInternalValidationFlatResults('123', mockProjectName, [1]);
1477
+ expect(result.rows).toHaveLength(1);
1478
+ expect(result.rows[0]).toEqual(expect.objectContaining({
1479
+ 'Test Case ID': 101,
1480
+ 'Mentioned but Not Linked': 'Step 1: SR0001',
1481
+ 'Linked but Not Mentioned': '',
1482
+ 'Validation Status': 'Fail',
1483
+ }));
1484
+ });
1485
+ it('should produce one detailed row per test case with correct bidirectional discrepancies', async () => {
1486
+ const mockDetailedStepsByTestCase = new Map([
1487
+ [
1488
+ 201,
1489
+ [
1490
+ {
1491
+ stepId: '1',
1492
+ stepPosition: '1',
1493
+ action: 'Validate parent SR0511 and SR0095 siblings',
1494
+ expected: '<b>sr0511</b>; SR0095-2,3; VVRM-05',
1495
+ isSharedStepTitle: false,
1496
+ },
1497
+ {
1498
+ stepId: '2',
1499
+ stepPosition: '2',
1500
+ action: 'Noisy requirement-like token should be ignored',
1501
+ expected: 'SR0511-V3.24',
1502
+ isSharedStepTitle: false,
1503
+ },
1504
+ {
1505
+ stepId: '3',
1506
+ stepPosition: '3',
1507
+ action: 'Regression note',
1508
+ expected: 'Verification note only',
1509
+ isSharedStepTitle: false,
1510
+ },
1511
+ ],
1512
+ ],
1513
+ [
1514
+ 202,
1515
+ [
1516
+ {
1517
+ stepId: '1',
1518
+ stepPosition: '1',
1519
+ action: 'Linked SR0200 exists but is not cleanly mentioned in expected result',
1520
+ expected: 'VVRM-22; SR0200 V3.1',
1521
+ isSharedStepTitle: false,
1522
+ },
1523
+ {
1524
+ stepId: '2',
1525
+ stepPosition: '2',
1526
+ action: 'Execution notes',
1527
+ expected: 'Notes without SR requirement token',
1528
+ isSharedStepTitle: false,
1529
+ },
1530
+ ],
1531
+ ],
1532
+ [
1533
+ 203,
1534
+ [
1535
+ {
1536
+ stepId: '1',
1537
+ stepPosition: '1',
1538
+ action: 'Primary requirement validation for SR0100-1',
1539
+ expected: '<i>SR0100-1</i>',
1540
+ isSharedStepTitle: false,
1541
+ },
1542
+ {
1543
+ stepId: '3',
1544
+ stepPosition: '3',
1545
+ action: 'Repeated mention should not create duplicate mismatch',
1546
+ expected: 'SR0100-1; SR0100-1',
1547
+ isSharedStepTitle: false,
1548
+ },
1549
+ ],
1550
+ ],
1551
+ ]);
1552
+ const mockLinkedRequirementsByTestCase = new Map([
1553
+ [
1554
+ 201,
1555
+ {
1556
+ baseKeys: new Set(['SR0511', 'SR0095', 'SR8888']),
1557
+ fullCodes: new Set(['SR0511', 'SR0095-2', 'SR8888']),
1558
+ },
1559
+ ],
1560
+ [
1561
+ 202,
1562
+ {
1563
+ baseKeys: new Set(['SR0200']),
1564
+ fullCodes: new Set(['SR0200']),
1565
+ },
1566
+ ],
1567
+ [
1568
+ 203,
1569
+ {
1570
+ baseKeys: new Set(['SR0100']),
1571
+ fullCodes: new Set(['SR0100-1']),
1572
+ },
1573
+ ],
1574
+ ]);
1575
+ jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1576
+ jest.spyOn(resultDataProvider, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1577
+ jest.spyOn(resultDataProvider, 'fetchTestData').mockResolvedValueOnce([
1578
+ {
1579
+ testPointsItems: [
1580
+ { testCaseId: 201, testCaseName: 'TC 201 - Mixed discrepancies' },
1581
+ { testCaseId: 202, testCaseName: 'TC 202 - Link only' },
1582
+ { testCaseId: 203, testCaseName: 'TC 203 - Fully valid' },
1583
+ ],
1584
+ testCasesItems: [
1585
+ {
1586
+ workItem: {
1587
+ id: 201,
1588
+ workItemFields: [{ key: 'Steps', value: '<steps id="mock-steps-tc-201"></steps>' }],
1589
+ },
1590
+ },
1591
+ {
1592
+ workItem: {
1593
+ id: 202,
1594
+ workItemFields: [{ key: 'Steps', value: '<steps id="mock-steps-tc-202"></steps>' }],
1595
+ },
1596
+ },
1597
+ {
1598
+ workItem: {
1599
+ id: 203,
1600
+ workItemFields: [{ key: 'Steps', value: '<steps id="mock-steps-tc-203"></steps>' }],
1601
+ },
1602
+ },
1603
+ ],
1604
+ },
1605
+ ]);
1606
+ jest.spyOn(resultDataProvider, 'fetchMewpL2Requirements').mockResolvedValueOnce([
1607
+ {
1608
+ workItemId: 6001,
1609
+ requirementId: 'SR0511',
1610
+ baseKey: 'SR0511',
1611
+ title: 'Parent 0511',
1612
+ responsibility: 'ESUK',
1613
+ linkedTestCaseIds: [201],
1614
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1615
+ },
1616
+ {
1617
+ workItemId: 6002,
1618
+ requirementId: 'SR0511-1',
1619
+ baseKey: 'SR0511',
1620
+ title: 'Child 0511-1',
1621
+ responsibility: 'ESUK',
1622
+ linkedTestCaseIds: [],
1623
+ areaPath: 'MEWP\\Customer Requirements\\Level 2\\MOP',
1624
+ },
1625
+ {
1626
+ workItemId: 6003,
1627
+ requirementId: 'SR0511-2',
1628
+ baseKey: 'SR0511',
1629
+ title: 'Child 0511-2',
1630
+ responsibility: 'ESUK',
1631
+ linkedTestCaseIds: [],
1632
+ areaPath: 'MEWP\\Customer Requirements\\Level 2\\MOP',
1633
+ },
1634
+ {
1635
+ workItemId: 6004,
1636
+ requirementId: 'SR0095-2',
1637
+ baseKey: 'SR0095',
1638
+ title: 'SR0095 child 2',
1639
+ responsibility: 'ESUK',
1640
+ linkedTestCaseIds: [],
1641
+ areaPath: 'MEWP\\Customer Requirements\\Level 2\\MOP',
1642
+ },
1643
+ {
1644
+ workItemId: 6005,
1645
+ requirementId: 'SR0095-3',
1646
+ baseKey: 'SR0095',
1647
+ title: 'SR0095 child 3',
1648
+ responsibility: 'ESUK',
1649
+ linkedTestCaseIds: [],
1650
+ areaPath: 'MEWP\\Customer Requirements\\Level 2\\MOP',
1651
+ },
1652
+ {
1653
+ workItemId: 6006,
1654
+ requirementId: 'SR0200',
1655
+ baseKey: 'SR0200',
1656
+ title: 'SR0200 standalone',
1657
+ responsibility: 'ESUK',
1658
+ linkedTestCaseIds: [202],
1659
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1660
+ },
1661
+ {
1662
+ workItemId: 6007,
1663
+ requirementId: 'SR0100-1',
1664
+ baseKey: 'SR0100',
1665
+ title: 'SR0100 child 1',
1666
+ responsibility: 'ESUK',
1667
+ linkedTestCaseIds: [203],
1668
+ areaPath: 'MEWP\\Customer Requirements\\Level 2\\MOP',
1669
+ },
1670
+ ]);
1671
+ jest
1672
+ .spyOn(resultDataProvider, 'buildLinkedRequirementsByTestCase')
1673
+ .mockResolvedValueOnce(mockLinkedRequirementsByTestCase);
1674
+ jest
1675
+ .spyOn(resultDataProvider.testStepParserHelper, 'parseTestSteps')
1676
+ .mockImplementation(async (...args) => {
1677
+ const stepsXml = String((args === null || args === void 0 ? void 0 : args[0]) || '');
1678
+ const testCaseMatch = /mock-steps-tc-(\d+)/i.exec(stepsXml);
1679
+ const testCaseId = Number((testCaseMatch === null || testCaseMatch === void 0 ? void 0 : testCaseMatch[1]) || 0);
1680
+ return mockDetailedStepsByTestCase.get(testCaseId) || [];
1681
+ });
1682
+ const result = await resultDataProvider.getMewpInternalValidationFlatResults('123', mockProjectName, [1]);
1683
+ expect(result.rows).toHaveLength(3);
1684
+ const byTestCase = new Map(result.rows.map((row) => [row['Test Case ID'], row]));
1685
+ expect(new Set(result.rows.map((row) => row['Test Case ID']))).toEqual(new Set([201, 202, 203]));
1686
+ expect(byTestCase.get(201)).toEqual(expect.objectContaining({
1687
+ 'Test Case Title': 'TC 201 - Mixed discrepancies',
1688
+ 'Mentioned but Not Linked': 'Step 1: SR0095-3, SR0511-1, SR0511-2',
1689
+ 'Linked but Not Mentioned': 'SR8888',
1690
+ 'Validation Status': 'Fail',
1691
+ }));
1692
+ expect(String(byTestCase.get(201)['Mentioned but Not Linked'] || '')).not.toContain('VVRM');
1693
+ expect(byTestCase.get(202)).toEqual(expect.objectContaining({
1694
+ 'Test Case Title': 'TC 202 - Link only',
1695
+ 'Mentioned but Not Linked': '',
1696
+ 'Linked but Not Mentioned': 'SR0200',
1697
+ 'Validation Status': 'Fail',
1698
+ }));
1699
+ expect(byTestCase.get(203)).toEqual(expect.objectContaining({
1700
+ 'Test Case Title': 'TC 203 - Fully valid',
1701
+ 'Mentioned but Not Linked': '',
1702
+ 'Linked but Not Mentioned': '',
1703
+ 'Validation Status': 'Pass',
1704
+ }));
1705
+ expect(resultDataProvider.testStepParserHelper.parseTestSteps).toHaveBeenCalledTimes(3);
1706
+ const parseCalls = resultDataProvider.testStepParserHelper.parseTestSteps.mock.calls;
1707
+ const parsedCaseIds = parseCalls
1708
+ .map(([xml]) => {
1709
+ const match = /mock-steps-tc-(\d+)/i.exec(String(xml || ''));
1710
+ return Number((match === null || match === void 0 ? void 0 : match[1]) || 0);
1711
+ })
1712
+ .filter((id) => Number.isFinite(id) && id > 0);
1713
+ expect(new Set(parsedCaseIds)).toEqual(new Set([201, 202, 203]));
1714
+ });
1715
+ it('should parse TC-0042 mixed expected text and keep only valid SR requirement codes', async () => {
1716
+ jest.spyOn(resultDataProvider, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1717
+ jest.spyOn(resultDataProvider, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1718
+ jest.spyOn(resultDataProvider, 'fetchTestData').mockResolvedValueOnce([
1719
+ {
1720
+ testPointsItems: [{ testCaseId: 42, testCaseName: 'TC-0042' }],
1721
+ testCasesItems: [
1722
+ {
1723
+ workItem: {
1724
+ id: 42,
1725
+ workItemFields: [{ key: 'Steps', value: '<steps id="mock-steps-tc-0042"></steps>' }],
1726
+ },
1727
+ },
1728
+ ],
1729
+ },
1730
+ ]);
1731
+ jest.spyOn(resultDataProvider, 'fetchMewpL2Requirements').mockResolvedValueOnce([
1732
+ {
1733
+ workItemId: 7001,
1734
+ requirementId: 'SR0036',
1735
+ baseKey: 'SR0036',
1736
+ title: 'SR0036',
1737
+ responsibility: 'ESUK',
1738
+ linkedTestCaseIds: [],
1739
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1740
+ },
1741
+ {
1742
+ workItemId: 7002,
1743
+ requirementId: 'SR0215',
1744
+ baseKey: 'SR0215',
1745
+ title: 'SR0215',
1746
+ responsibility: 'ESUK',
1747
+ linkedTestCaseIds: [],
1748
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1749
+ },
1750
+ {
1751
+ workItemId: 7003,
1752
+ requirementId: 'SR0539',
1753
+ baseKey: 'SR0539',
1754
+ title: 'SR0539',
1755
+ responsibility: 'ESUK',
1756
+ linkedTestCaseIds: [],
1757
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1758
+ },
1759
+ {
1760
+ workItemId: 7004,
1761
+ requirementId: 'SR0348',
1762
+ baseKey: 'SR0348',
1763
+ title: 'SR0348',
1764
+ responsibility: 'ESUK',
1765
+ linkedTestCaseIds: [],
1766
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1767
+ },
1768
+ {
1769
+ workItemId: 7005,
1770
+ requirementId: 'SR0027',
1771
+ baseKey: 'SR0027',
1772
+ title: 'SR0027',
1773
+ responsibility: 'ESUK',
1774
+ linkedTestCaseIds: [],
1775
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1776
+ },
1777
+ {
1778
+ workItemId: 7006,
1779
+ requirementId: 'SR0041',
1780
+ baseKey: 'SR0041',
1781
+ title: 'SR0041',
1782
+ responsibility: 'ESUK',
1783
+ linkedTestCaseIds: [],
1784
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1785
+ },
1786
+ {
1787
+ workItemId: 7007,
1788
+ requirementId: 'SR0550',
1789
+ baseKey: 'SR0550',
1790
+ title: 'SR0550',
1791
+ responsibility: 'ESUK',
1792
+ linkedTestCaseIds: [],
1793
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1794
+ },
1795
+ {
1796
+ workItemId: 7008,
1797
+ requirementId: 'SR0550-2',
1798
+ baseKey: 'SR0550',
1799
+ title: 'SR0550-2',
1800
+ responsibility: 'ESUK',
1801
+ linkedTestCaseIds: [],
1802
+ areaPath: 'MEWP\\Customer Requirements\\Level 2\\MOP',
1803
+ },
1804
+ {
1805
+ workItemId: 7009,
1806
+ requirementId: 'SR0817',
1807
+ baseKey: 'SR0817',
1808
+ title: 'SR0817',
1809
+ responsibility: 'ESUK',
1810
+ linkedTestCaseIds: [],
1811
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1812
+ },
1813
+ {
1814
+ workItemId: 7010,
1815
+ requirementId: 'SR0818',
1816
+ baseKey: 'SR0818',
1817
+ title: 'SR0818',
1818
+ responsibility: 'ESUK',
1819
+ linkedTestCaseIds: [],
1820
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1821
+ },
1822
+ {
1823
+ workItemId: 7011,
1824
+ requirementId: 'SR0859',
1825
+ baseKey: 'SR0859',
1826
+ title: 'SR0859',
1827
+ responsibility: 'ESUK',
1828
+ linkedTestCaseIds: [],
1829
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
1830
+ },
1831
+ ]);
1832
+ jest
1833
+ .spyOn(resultDataProvider, 'buildLinkedRequirementsByTestCase')
1834
+ .mockResolvedValueOnce(new Map([
1835
+ [
1836
+ 42,
1837
+ {
1838
+ baseKeys: new Set([
1839
+ 'SR0036',
1840
+ 'SR0215',
1841
+ 'SR0539',
1842
+ 'SR0348',
1843
+ 'SR0041',
1844
+ 'SR0550',
1845
+ 'SR0817',
1846
+ 'SR0818',
1847
+ 'SR0859',
1848
+ ]),
1849
+ fullCodes: new Set([
1850
+ 'SR0036',
1851
+ 'SR0215',
1852
+ 'SR0539',
1853
+ 'SR0348',
1854
+ 'SR0041',
1855
+ 'SR0550',
1856
+ 'SR0550-2',
1857
+ 'SR0817',
1858
+ 'SR0818',
1859
+ 'SR0859',
1860
+ ]),
1861
+ },
1862
+ ],
1863
+ ]));
1864
+ jest.spyOn(resultDataProvider.testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
1865
+ {
1866
+ stepId: '1',
1867
+ stepPosition: '1',
1868
+ action: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
1869
+ expected: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. (SR0036; SR0215; VVRM-1; SR0817-V3.2; SR0818-V3.3; SR0859-V3.4)',
1870
+ isSharedStepTitle: false,
1871
+ },
1872
+ {
1873
+ stepId: '2',
1874
+ stepPosition: '2',
1875
+ action: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
1876
+ expected: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. (SR0539; SR0348; VVRM-1)',
1877
+ isSharedStepTitle: false,
1878
+ },
1879
+ {
1880
+ stepId: '3',
1881
+ stepPosition: '3',
1882
+ action: 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.',
1883
+ expected: 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. (SR0027, SR0036; SR0041; VVRM-2)',
1884
+ isSharedStepTitle: false,
1885
+ },
1886
+ {
1887
+ stepId: '4',
1888
+ stepPosition: '4',
1889
+ action: 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
1890
+ expected: 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. (SR0550-2)',
1891
+ isSharedStepTitle: false,
1892
+ },
1893
+ {
1894
+ stepId: '5',
1895
+ stepPosition: '5',
1896
+ action: 'Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit.',
1897
+ expected: 'Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit. (VVRM-1)',
1898
+ isSharedStepTitle: false,
1899
+ },
1900
+ {
1901
+ stepId: '6',
1902
+ stepPosition: '6',
1903
+ action: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet consectetur adipisci velit.',
1904
+ expected: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet consectetur adipisci velit. (SR0041; SR0550; VVRM-3)',
1905
+ isSharedStepTitle: false,
1906
+ },
1907
+ ]);
1908
+ const result = await resultDataProvider.getMewpInternalValidationFlatResults('123', mockProjectName, [1]);
1909
+ expect(result.rows).toHaveLength(1);
1910
+ expect(result.rows[0]).toEqual(expect.objectContaining({
1911
+ 'Test Case ID': 42,
1912
+ 'Test Case Title': 'TC-0042',
1913
+ 'Mentioned but Not Linked': 'Step 3: SR0027',
1914
+ 'Linked but Not Mentioned': 'SR0817; SR0818; SR0859',
1915
+ 'Validation Status': 'Fail',
1916
+ }));
1917
+ expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).not.toContain('VVRM');
1918
+ expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).not.toContain('VVRM');
1919
+ });
1920
+ });
1921
+ describe('MEWP rel fallback scoping', () => {
1922
+ it('should fallback to previous Rel run when latest selected Rel has no run for a test case', async () => {
1923
+ const suites = [
1924
+ { testSuiteId: 10, suiteName: 'Rel10 / Validation' },
1925
+ { testSuiteId: 11, suiteName: 'Rel11 / Validation' },
1926
+ ];
1927
+ const allSuites = [
1928
+ { testSuiteId: 10, suiteName: 'Rel10 / Validation' },
1929
+ { testSuiteId: 11, suiteName: 'Rel11 / Validation' },
1930
+ ];
1931
+ const rawTestData = [
1932
+ {
1933
+ suiteName: 'Rel10 / Validation',
1934
+ testPointsItems: [
1935
+ { testCaseId: 501, lastRunId: 100, lastResultId: 200 },
1936
+ { testCaseId: 502, lastRunId: 300, lastResultId: 400 },
1937
+ ],
1938
+ testCasesItems: [{ workItem: { id: 501 } }, { workItem: { id: 502 } }],
1939
+ },
1940
+ {
1941
+ suiteName: 'Rel11 / Validation',
1942
+ testPointsItems: [
1943
+ { testCaseId: 501, lastRunId: 0, lastResultId: 0 },
1944
+ { testCaseId: 502, lastRunId: 500, lastResultId: 600 },
1945
+ ],
1946
+ testCasesItems: [{ workItem: { id: 501 } }, { workItem: { id: 502 } }],
1947
+ },
1948
+ ];
1949
+ const fetchSuitesSpy = jest
1950
+ .spyOn(resultDataProvider, 'fetchTestSuites')
1951
+ .mockResolvedValueOnce(suites)
1952
+ .mockResolvedValueOnce(allSuites);
1953
+ const fetchDataSpy = jest
1954
+ .spyOn(resultDataProvider, 'fetchTestData')
1955
+ .mockResolvedValueOnce(rawTestData);
1956
+ const scoped = await resultDataProvider.fetchMewpScopedTestData('123', mockProjectName, [11], true);
1957
+ expect(fetchSuitesSpy).toHaveBeenCalledTimes(2);
1958
+ expect(fetchDataSpy).toHaveBeenCalledTimes(1);
1959
+ expect(scoped).toHaveLength(1);
1960
+ const selectedPoints = scoped[0].testPointsItems;
1961
+ const tc501 = selectedPoints.find((item) => item.testCaseId === 501);
1962
+ const tc502 = selectedPoints.find((item) => item.testCaseId === 502);
1963
+ expect(tc501).toEqual(expect.objectContaining({ lastRunId: 100, lastResultId: 200 }));
1964
+ expect(tc502).toEqual(expect.objectContaining({ lastRunId: 500, lastResultId: 600 }));
1965
+ });
1966
+ });
1967
+ describe('MEWP external ingestion validation/parsing', () => {
1968
+ const validBugsSource = {
1969
+ name: 'bugs.xlsx',
1970
+ url: 'https://minio.local/mewp-external-ingestion/MEWP/mewp-external-ingestion/bugs/bugs.xlsx',
1971
+ sourceType: 'mewpExternalIngestion',
1972
+ };
1973
+ const validL3L4Source = {
1974
+ name: 'l3l4.xlsx',
1975
+ url: 'https://minio.local/mewp-external-ingestion/MEWP/mewp-external-ingestion/l3l4/l3l4.xlsx',
1976
+ sourceType: 'mewpExternalIngestion',
1977
+ };
1978
+ it('should validate external files when required columns exist in A3 header row', async () => {
1979
+ const bugsBuffer = buildWorkbookBuffer([
1980
+ ['', '', '', '', ''],
1981
+ ['', '', '', '', ''],
1982
+ ['Elisra_SortIndex', 'SR', 'TargetWorkItemId', 'Title', 'TargetState'],
1983
+ ['101', 'SR0001', '9001', 'Bug one', 'Active'],
1984
+ ]);
1985
+ const l3l4Buffer = buildWorkbookBuffer([
1986
+ ['', '', '', '', '', '', '', ''],
1987
+ ['', '', '', '', '', '', '', ''],
1988
+ [
1989
+ 'SR',
1990
+ 'AREA 34',
1991
+ 'TargetWorkItemId Level 3',
1992
+ 'TargetTitleLevel3',
1993
+ 'TargetStateLevel 3',
1994
+ 'TargetWorkItemIdLevel 4',
1995
+ 'TargetTitleLevel4',
1996
+ 'TargetStateLevel 4',
1997
+ ],
1998
+ ['SR0001', 'Level 3', '7001', 'Req L3', 'Active', '', '', ''],
1999
+ ]);
2000
+ const axiosSpy = jest
2001
+ .spyOn(axios_1.default, 'get')
2002
+ .mockResolvedValueOnce({ data: bugsBuffer, headers: {} })
2003
+ .mockResolvedValueOnce({ data: l3l4Buffer, headers: {} });
2004
+ const result = await resultDataProvider.validateMewpExternalFiles({
2005
+ externalBugsFile: validBugsSource,
2006
+ externalL3L4File: validL3L4Source,
2007
+ });
2008
+ expect(axiosSpy).toHaveBeenCalledTimes(2);
2009
+ expect(result.valid).toBe(true);
2010
+ expect(result.bugs).toEqual(expect.objectContaining({
2011
+ valid: true,
2012
+ headerRow: 'A3',
2013
+ matchedRequiredColumns: 5,
2014
+ }));
2015
+ expect(result.l3l4).toEqual(expect.objectContaining({
2016
+ valid: true,
2017
+ headerRow: 'A3',
2018
+ matchedRequiredColumns: 8,
2019
+ }));
2020
+ });
2021
+ it('should accept A1 fallback header row for backward compatibility', async () => {
2022
+ const bugsBuffer = buildWorkbookBuffer([
2023
+ ['Elisra_SortIndex', 'SR', 'TargetWorkItemId', 'Title', 'TargetState'],
2024
+ ['101', 'SR0001', '9001', 'Bug one', 'Active'],
2025
+ ]);
2026
+ jest.spyOn(axios_1.default, 'get').mockResolvedValueOnce({ data: bugsBuffer, headers: {} });
2027
+ const result = await resultDataProvider.validateMewpExternalFiles({
2028
+ externalBugsFile: validBugsSource,
2029
+ });
2030
+ expect(result.valid).toBe(true);
2031
+ expect(result.bugs).toEqual(expect.objectContaining({
2032
+ valid: true,
2033
+ headerRow: 'A1',
2034
+ }));
2035
+ });
2036
+ it('should fail validation when required columns are missing', async () => {
2037
+ const invalidBuffer = buildWorkbookBuffer([
2038
+ ['', '', ''],
2039
+ ['', '', ''],
2040
+ ['Elisra_SortIndex', 'TargetWorkItemId', 'TargetState'],
2041
+ ['101', '9001', 'Active'],
2042
+ ]);
2043
+ jest.spyOn(axios_1.default, 'get').mockResolvedValueOnce({ data: invalidBuffer, headers: {} });
2044
+ const result = await resultDataProvider.validateMewpExternalFiles({
2045
+ externalBugsFile: validBugsSource,
2046
+ });
2047
+ expect(result.valid).toBe(false);
2048
+ expect(result.bugs).toEqual(expect.objectContaining({
2049
+ valid: false,
2050
+ matchedRequiredColumns: 3,
2051
+ missingRequiredColumns: expect.arrayContaining(['SR', 'Title']),
2052
+ }));
2053
+ });
2054
+ it('should reject files from non-dedicated bucket/object path', async () => {
2055
+ var _a;
2056
+ const result = await resultDataProvider.validateMewpExternalFiles({
2057
+ externalBugsFile: {
2058
+ name: 'bugs.xlsx',
2059
+ url: 'https://minio.local/mewp-external-ingestion/MEWP/other-prefix/bugs.xlsx',
2060
+ sourceType: 'mewpExternalIngestion',
2061
+ },
2062
+ });
2063
+ expect(result.valid).toBe(false);
2064
+ expect(((_a = result.bugs) === null || _a === void 0 ? void 0 : _a.message) || '').toContain('Invalid object path');
2065
+ });
2066
+ it('should filter external bugs by SR/state and dedupe by bug+requirement', async () => {
2067
+ const mewpExternalTableUtils = resultDataProvider.mewpExternalTableUtils;
2068
+ jest.spyOn(mewpExternalTableUtils, 'loadExternalTableRows').mockResolvedValueOnce([
2069
+ {
2070
+ Elisra_SortIndex: '101',
2071
+ SR: 'SR0001',
2072
+ TargetWorkItemId: '9001',
2073
+ Title: 'Bug one',
2074
+ TargetState: 'Active',
2075
+ SAPWBS: 'ESUK',
2076
+ },
2077
+ {
2078
+ Elisra_SortIndex: '101',
2079
+ SR: 'SR0001',
2080
+ TargetWorkItemId: '9001',
2081
+ Title: 'Bug one duplicate',
2082
+ TargetState: 'Active',
2083
+ },
2084
+ {
2085
+ Elisra_SortIndex: '101',
2086
+ SR: '',
2087
+ TargetWorkItemId: '9002',
2088
+ Title: 'Missing SR',
2089
+ TargetState: 'Active',
2090
+ },
2091
+ {
2092
+ Elisra_SortIndex: '101',
2093
+ SR: 'SR0002',
2094
+ TargetWorkItemId: '9003',
2095
+ Title: 'Closed bug',
2096
+ TargetState: 'Closed',
2097
+ },
2098
+ ]);
2099
+ const map = await resultDataProvider.loadExternalBugsByTestCase(validBugsSource);
2100
+ const bugs = map.get(101) || [];
2101
+ expect(bugs).toHaveLength(1);
2102
+ expect(bugs[0]).toEqual(expect.objectContaining({
2103
+ id: 9001,
2104
+ requirementBaseKey: 'SR0001',
2105
+ responsibility: 'ESUK',
2106
+ }));
2107
+ });
2108
+ it('should require Elisra_SortIndex and ignore rows that only provide WorkItemId', async () => {
2109
+ const mewpExternalTableUtils = resultDataProvider.mewpExternalTableUtils;
2110
+ jest.spyOn(mewpExternalTableUtils, 'loadExternalTableRows').mockResolvedValueOnce([
2111
+ {
2112
+ WorkItemId: '101',
2113
+ SR: 'SR0001',
2114
+ TargetWorkItemId: '9010',
2115
+ Title: 'Bug without Elisra_SortIndex',
2116
+ TargetState: 'Active',
2117
+ },
2118
+ ]);
2119
+ const map = await resultDataProvider.loadExternalBugsByTestCase(validBugsSource);
2120
+ expect(map.size).toBe(0);
2121
+ });
2122
+ it('should parse external L3/L4 file using AREA 34 semantics and terminal-state filtering', async () => {
2123
+ const mewpExternalTableUtils = resultDataProvider.mewpExternalTableUtils;
2124
+ jest.spyOn(mewpExternalTableUtils, 'loadExternalTableRows').mockResolvedValueOnce([
2125
+ {
2126
+ SR: 'SR0001',
2127
+ 'AREA 34': 'Level 4',
2128
+ 'TargetWorkItemId Level 3': '7001',
2129
+ TargetTitleLevel3: 'L4 From Level3 Column',
2130
+ 'TargetStateLevel 3': 'Active',
2131
+ },
2132
+ {
2133
+ SR: 'SR0001',
2134
+ 'AREA 34': 'Level 3',
2135
+ 'TargetWorkItemId Level 3': '7002',
2136
+ TargetTitleLevel3: 'L3 Requirement',
2137
+ 'TargetStateLevel 3': 'Active',
2138
+ 'TargetWorkItemIdLevel 4': '7003',
2139
+ TargetTitleLevel4: 'L4 Requirement',
2140
+ 'TargetStateLevel 4': 'Closed',
2141
+ },
2142
+ ]);
2143
+ const map = await resultDataProvider.loadExternalL3L4ByBaseKey(validL3L4Source);
2144
+ expect(map.get('SR0001')).toEqual([
2145
+ { id: '7002', title: 'L3 Requirement', level: 'L3' },
2146
+ { id: '7001', title: 'L4 From Level3 Column', level: 'L4' },
2147
+ ]);
2148
+ });
2149
+ it('should exclude external open L3/L4 rows when SAPWBS resolves to ESUK', async () => {
2150
+ const mewpExternalTableUtils = resultDataProvider.mewpExternalTableUtils;
2151
+ jest.spyOn(mewpExternalTableUtils, 'loadExternalTableRows').mockResolvedValueOnce([
2152
+ {
2153
+ SR: 'SR0001',
2154
+ 'AREA 34': 'Level 3',
2155
+ 'TargetWorkItemId Level 3': '7101',
2156
+ TargetTitleLevel3: 'L3 ESUK',
2157
+ 'TargetStateLevel 3': 'Active',
2158
+ 'TargetSapWbsLevel 3': 'ESUK',
2159
+ 'TargetWorkItemIdLevel 4': '7201',
2160
+ TargetTitleLevel4: 'L4 IL',
2161
+ 'TargetStateLevel 4': 'Active',
2162
+ 'TargetSapWbsLevel 4': 'IL',
2163
+ },
2164
+ {
2165
+ SR: 'SR0001',
2166
+ 'AREA 34': 'Level 3',
2167
+ 'TargetWorkItemId Level 3': '7102',
2168
+ TargetTitleLevel3: 'L3 IL',
2169
+ 'TargetStateLevel 3': 'Active',
2170
+ 'TargetSapWbsLevel 3': 'IL',
2171
+ },
2172
+ ]);
2173
+ const map = await resultDataProvider.loadExternalL3L4ByBaseKey(validL3L4Source);
2174
+ expect(map.get('SR0001')).toEqual([
2175
+ { id: '7102', title: 'L3 IL', level: 'L3' },
2176
+ { id: '7201', title: 'L4 IL', level: 'L4' },
2177
+ ]);
2178
+ });
2179
+ it('should fallback L3/L4 SAPWBS exclusion from SR-mapped requirement when row SAPWBS is empty', async () => {
2180
+ const mewpExternalTableUtils = resultDataProvider.mewpExternalTableUtils;
2181
+ jest.spyOn(mewpExternalTableUtils, 'loadExternalTableRows').mockResolvedValueOnce([
2182
+ {
2183
+ SR: 'SR0001',
2184
+ 'AREA 34': 'Level 3',
2185
+ 'TargetWorkItemId Level 3': '7301',
2186
+ TargetTitleLevel3: 'L3 From ESUK Requirement',
2187
+ 'TargetStateLevel 3': 'Active',
2188
+ 'TargetSapWbsLevel 3': '',
2189
+ },
2190
+ {
2191
+ SR: 'SR0002',
2192
+ 'AREA 34': 'Level 3',
2193
+ 'TargetWorkItemId Level 3': '7302',
2194
+ TargetTitleLevel3: 'L3 From IL Requirement',
2195
+ 'TargetStateLevel 3': 'Active',
2196
+ 'TargetSapWbsLevel 3': '',
2197
+ },
2198
+ ]);
2199
+ const map = await resultDataProvider.loadExternalL3L4ByBaseKey(validL3L4Source, new Map([
2200
+ ['SR0001', 'ESUK'],
2201
+ ['SR0002', 'IL'],
2202
+ ]));
2203
+ expect(map.has('SR0001')).toBe(false);
2204
+ expect(map.get('SR0002')).toEqual([{ id: '7302', title: 'L3 From IL Requirement', level: 'L3' }]);
2205
+ });
2206
+ it('should resolve bug responsibility from AreaPath when SAPWBS is empty', () => {
2207
+ const fromEsukAreaPath = resultDataProvider.resolveBugResponsibility({
2208
+ 'Custom.SAPWBS': '',
2209
+ 'System.AreaPath': 'MEWP\\Customer Requirements\\Level 2\\ATP\\ESUK',
2210
+ });
2211
+ const fromIlAreaPath = resultDataProvider.resolveBugResponsibility({
2212
+ 'Custom.SAPWBS': '',
2213
+ 'System.AreaPath': 'MEWP\\Customer Requirements\\Level 2\\ATP',
2214
+ });
2215
+ const unknown = resultDataProvider.resolveBugResponsibility({
2216
+ 'Custom.SAPWBS': '',
2217
+ 'System.AreaPath': 'MEWP\\Other\\Area',
2218
+ });
2219
+ expect(fromEsukAreaPath).toBe('ESUK');
2220
+ expect(fromIlAreaPath).toBe('Elisra');
2221
+ expect(unknown).toBe('Unknown');
2222
+ });
2223
+ it('should handle 1000 external bug rows and keep only in-scope parsed items', async () => {
2224
+ const rows = [];
2225
+ for (let i = 1; i <= 1000; i++) {
2226
+ rows.push({
2227
+ Elisra_SortIndex: String(4000 + (i % 20)),
2228
+ SR: `SR${String(6000 + (i % 50)).padStart(4, '0')}`,
2229
+ TargetWorkItemId: String(900000 + i),
2230
+ Title: `Bug ${i}`,
2231
+ TargetState: i % 10 === 0 ? 'Closed' : 'Active',
2232
+ SAPWBS: i % 2 === 0 ? 'ESUK' : 'IL',
2233
+ });
2234
+ }
2235
+ const mewpExternalTableUtils = resultDataProvider.mewpExternalTableUtils;
2236
+ jest.spyOn(mewpExternalTableUtils, 'loadExternalTableRows').mockResolvedValueOnce(rows);
2237
+ const startedAt = Date.now();
2238
+ const map = await resultDataProvider.loadExternalBugsByTestCase({
2239
+ name: 'bulk-bugs.xlsx',
2240
+ url: 'https://minio.local/mewp-external-ingestion/MEWP/mewp-external-ingestion/bugs/bulk-bugs.xlsx',
2241
+ sourceType: 'mewpExternalIngestion',
2242
+ });
2243
+ const elapsedMs = Date.now() - startedAt;
2244
+ const totalParsed = [...map.values()].reduce((sum, items) => sum + ((items === null || items === void 0 ? void 0 : items.length) || 0), 0);
2245
+ expect(totalParsed).toBe(900); // every 10th row is closed and filtered out
2246
+ expect(elapsedMs).toBeLessThan(5000);
2247
+ });
2248
+ it('should handle 1000 external L3/L4 rows and map all active links', async () => {
2249
+ const rows = [];
2250
+ for (let i = 1; i <= 1000; i++) {
2251
+ rows.push({
2252
+ SR: `SR${String(7000 + (i % 25)).padStart(4, '0')}`,
2253
+ 'AREA 34': i % 3 === 0 ? 'Level 4' : 'Level 3',
2254
+ 'TargetWorkItemId Level 3': String(800000 + i),
2255
+ TargetTitleLevel3: `L3/L4 Title ${i}`,
2256
+ 'TargetStateLevel 3': i % 11 === 0 ? 'Resolved' : 'Active',
2257
+ 'TargetWorkItemIdLevel 4': String(810000 + i),
2258
+ TargetTitleLevel4: `L4 Title ${i}`,
2259
+ 'TargetStateLevel 4': i % 13 === 0 ? 'Closed' : 'Active',
2260
+ });
2261
+ }
2262
+ const mewpExternalTableUtils = resultDataProvider.mewpExternalTableUtils;
2263
+ jest.spyOn(mewpExternalTableUtils, 'loadExternalTableRows').mockResolvedValueOnce(rows);
2264
+ const startedAt = Date.now();
2265
+ const map = await resultDataProvider.loadExternalL3L4ByBaseKey({
2266
+ name: 'bulk-l3l4.xlsx',
2267
+ url: 'https://minio.local/mewp-external-ingestion/MEWP/mewp-external-ingestion/l3l4/bulk-l3l4.xlsx',
2268
+ sourceType: 'mewpExternalIngestion',
2269
+ });
2270
+ const elapsedMs = Date.now() - startedAt;
2271
+ const totalLinks = [...map.values()].reduce((sum, items) => sum + ((items === null || items === void 0 ? void 0 : items.length) || 0), 0);
2272
+ expect(totalLinks).toBeGreaterThan(700);
2273
+ expect(elapsedMs).toBeLessThan(5000);
2274
+ });
2275
+ });
2276
+ describe('MEWP high-volume requirement token parsing', () => {
2277
+ it('should parse 1000 expected-result requirement tokens with noisy fragments', () => {
2278
+ const tokens = [];
2279
+ for (let i = 1; i <= 1000; i++) {
2280
+ const code = `SR${String(10000 + i)}`;
2281
+ tokens.push(code);
2282
+ if (i % 100 === 0)
2283
+ tokens.push(`${code} V3.24`);
2284
+ if (i % 125 === 0)
2285
+ tokens.push(`${code} VVRM2425`);
2286
+ }
2287
+ const sourceText = tokens.join('; ');
2288
+ const startedAt = Date.now();
2289
+ const codes = resultDataProvider.extractRequirementCodesFromText(sourceText);
2290
+ const elapsedMs = Date.now() - startedAt;
2291
+ expect(codes.size).toBe(1000);
2292
+ expect(codes.has('SR10001')).toBe(true);
2293
+ expect(codes.has('SR11000')).toBe(true);
2294
+ expect(elapsedMs).toBeLessThan(3000);
2295
+ });
1044
2296
  });
1045
2297
  describe('fetchResultDataForTestReporter (runResultField switch)', () => {
1046
2298
  it('should populate requested runResultField values including testCaseResult URL branches', async () => {