@elisra-devops/docgen-data-provider 1.71.0 → 1.73.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.d.ts +28 -0
- package/bin/modules/ResultDataProvider.js +449 -8
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +334 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/ResultDataProvider.ts +500 -8
- package/src/tests/modules/ResultDataProvider.test.ts +424 -0
|
@@ -22,6 +22,7 @@ import { OpenPcrRequest } from '../models/tfs-data';
|
|
|
22
22
|
* Instantiate the class with the organization URL and token, and use the provided methods to fetch and process test data.
|
|
23
23
|
*/
|
|
24
24
|
export default class ResultDataProvider {
|
|
25
|
+
private static readonly MEWP_L2_COVERAGE_COLUMNS;
|
|
25
26
|
orgUrl: string;
|
|
26
27
|
token: string;
|
|
27
28
|
private limit;
|
|
@@ -60,11 +61,38 @@ export default class ResultDataProvider {
|
|
|
60
61
|
planName: string;
|
|
61
62
|
rows: any[];
|
|
62
63
|
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Builds MEWP L2 requirement coverage rows for audit reporting.
|
|
66
|
+
* Rows are one Requirement-TestCase pair; uncovered requirements are emitted with empty test-case columns.
|
|
67
|
+
*/
|
|
68
|
+
getMewpL2CoverageFlatResults(testPlanId: string, projectName: string, selectedSuiteIds: number[] | undefined): Promise<{
|
|
69
|
+
sheetName: string;
|
|
70
|
+
columnOrder: string[];
|
|
71
|
+
rows: any[];
|
|
72
|
+
}>;
|
|
63
73
|
/**
|
|
64
74
|
* Mapping each attachment to a proper URL for downloading it
|
|
65
75
|
* @param runResults Array of run results
|
|
66
76
|
*/
|
|
67
77
|
mapAttachmentsUrl(runResults: any[], project: string): any[];
|
|
78
|
+
private buildMewpCoverageSheetName;
|
|
79
|
+
private createMewpCoverageRow;
|
|
80
|
+
private buildTestCaseStepsXmlMap;
|
|
81
|
+
private extractStepsXmlFromWorkItemFields;
|
|
82
|
+
private classifyRequirementStepOutcome;
|
|
83
|
+
private accumulateRequirementCountsFromStepText;
|
|
84
|
+
private extractRequirementCodesFromText;
|
|
85
|
+
private normalizeRequirementStepText;
|
|
86
|
+
private toRequirementKey;
|
|
87
|
+
private fetchMewpL2Requirements;
|
|
88
|
+
private fetchMewpRequirementTypeNames;
|
|
89
|
+
private fetchWorkItemsByIds;
|
|
90
|
+
private extractLinkedTestCaseIdsFromRequirement;
|
|
91
|
+
private extractMewpRequirementIdentifier;
|
|
92
|
+
private deriveMewpResponsibility;
|
|
93
|
+
private resolveMewpResponsibility;
|
|
94
|
+
private normalizeMewpRequirementCode;
|
|
95
|
+
private toMewpComparableText;
|
|
68
96
|
private fetchTestPlanName;
|
|
69
97
|
/**
|
|
70
98
|
* Fetches test suites for a given test plan and project, optionally filtering by selected suite IDs.
|
|
@@ -279,6 +279,93 @@ class ResultDataProvider {
|
|
|
279
279
|
return { planId: testPlanId, planName: '', rows: [] };
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Builds MEWP L2 requirement coverage rows for audit reporting.
|
|
284
|
+
* Rows are one Requirement-TestCase pair; uncovered requirements are emitted with empty test-case columns.
|
|
285
|
+
*/
|
|
286
|
+
async getMewpL2CoverageFlatResults(testPlanId, projectName, selectedSuiteIds) {
|
|
287
|
+
var _a;
|
|
288
|
+
const defaultPayload = {
|
|
289
|
+
sheetName: `MEWP L2 Coverage - Plan ${testPlanId}`,
|
|
290
|
+
columnOrder: [...ResultDataProvider.MEWP_L2_COVERAGE_COLUMNS],
|
|
291
|
+
rows: [],
|
|
292
|
+
};
|
|
293
|
+
try {
|
|
294
|
+
const planName = await this.fetchTestPlanName(testPlanId, projectName);
|
|
295
|
+
const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
|
|
296
|
+
const testData = await this.fetchTestData(suites, projectName, testPlanId, false);
|
|
297
|
+
const requirements = await this.fetchMewpL2Requirements(projectName);
|
|
298
|
+
if (requirements.length === 0) {
|
|
299
|
+
return Object.assign(Object.assign({}, defaultPayload), { sheetName: this.buildMewpCoverageSheetName(planName, testPlanId) });
|
|
300
|
+
}
|
|
301
|
+
const requirementIndex = new Map();
|
|
302
|
+
const requirementKeys = new Set();
|
|
303
|
+
requirements.forEach((requirement) => {
|
|
304
|
+
const key = this.toRequirementKey(requirement.requirementId);
|
|
305
|
+
if (!key)
|
|
306
|
+
return;
|
|
307
|
+
requirementKeys.add(key);
|
|
308
|
+
if (!requirementIndex.has(key)) {
|
|
309
|
+
requirementIndex.set(key, { passed: 0, failed: 0, notRun: 0 });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
const parsedDefinitionStepsByTestCase = new Map();
|
|
313
|
+
const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
|
|
314
|
+
const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
|
|
315
|
+
for (const runResult of runResults) {
|
|
316
|
+
const testCaseId = Number(runResult === null || runResult === void 0 ? void 0 : runResult.testCaseId);
|
|
317
|
+
const actionResults = Array.isArray((_a = runResult === null || runResult === void 0 ? void 0 : runResult.iteration) === null || _a === void 0 ? void 0 : _a.actionResults)
|
|
318
|
+
? runResult.iteration.actionResults
|
|
319
|
+
: [];
|
|
320
|
+
const hasExecutedRun = Number((runResult === null || runResult === void 0 ? void 0 : runResult.lastRunId) || 0) > 0 && Number((runResult === null || runResult === void 0 ? void 0 : runResult.lastResultId) || 0) > 0;
|
|
321
|
+
if (actionResults.length > 0) {
|
|
322
|
+
for (const actionResult of actionResults) {
|
|
323
|
+
if (actionResult === null || actionResult === void 0 ? void 0 : actionResult.isSharedStepTitle)
|
|
324
|
+
continue;
|
|
325
|
+
const stepStatus = this.classifyRequirementStepOutcome(actionResult === null || actionResult === void 0 ? void 0 : actionResult.outcome);
|
|
326
|
+
this.accumulateRequirementCountsFromStepText(`${String((actionResult === null || actionResult === void 0 ? void 0 : actionResult.action) || '')} ${String((actionResult === null || actionResult === void 0 ? void 0 : actionResult.expected) || '')}`, stepStatus, requirementKeys, requirementIndex);
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
// Do not force "not run" from definition steps when a run exists:
|
|
331
|
+
// some runs may have missing/unmapped actionResults.
|
|
332
|
+
if (hasExecutedRun) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (!Number.isFinite(testCaseId))
|
|
336
|
+
continue;
|
|
337
|
+
if (!parsedDefinitionStepsByTestCase.has(testCaseId)) {
|
|
338
|
+
const stepsXml = testCaseStepsXmlMap.get(testCaseId) || '';
|
|
339
|
+
const parsed = stepsXml && String(stepsXml).trim() !== ''
|
|
340
|
+
? await this.testStepParserHelper.parseTestSteps(stepsXml, new Map())
|
|
341
|
+
: [];
|
|
342
|
+
parsedDefinitionStepsByTestCase.set(testCaseId, parsed);
|
|
343
|
+
}
|
|
344
|
+
const definitionSteps = parsedDefinitionStepsByTestCase.get(testCaseId) || [];
|
|
345
|
+
for (const step of definitionSteps) {
|
|
346
|
+
if (step === null || step === void 0 ? void 0 : step.isSharedStepTitle)
|
|
347
|
+
continue;
|
|
348
|
+
this.accumulateRequirementCountsFromStepText(`${String((step === null || step === void 0 ? void 0 : step.action) || '')} ${String((step === null || step === void 0 ? void 0 : step.expected) || '')}`, 'notRun', requirementKeys, requirementIndex);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const rows = requirements.map((requirement) => {
|
|
352
|
+
const key = this.toRequirementKey(requirement.requirementId);
|
|
353
|
+
const summary = key && requirementIndex.has(key)
|
|
354
|
+
? requirementIndex.get(key)
|
|
355
|
+
: { passed: 0, failed: 0, notRun: 0 };
|
|
356
|
+
return this.createMewpCoverageRow(requirement, summary);
|
|
357
|
+
});
|
|
358
|
+
return {
|
|
359
|
+
sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
|
|
360
|
+
columnOrder: [...ResultDataProvider.MEWP_L2_COVERAGE_COLUMNS],
|
|
361
|
+
rows,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
logger_1.default.error(`Error during getMewpL2CoverageFlatResults: ${error.message}`);
|
|
366
|
+
return defaultPayload;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
282
369
|
/**
|
|
283
370
|
* Mapping each attachment to a proper URL for downloading it
|
|
284
371
|
* @param runResults Array of run results
|
|
@@ -306,6 +393,320 @@ class ResultDataProvider {
|
|
|
306
393
|
return Object.assign({}, restResult);
|
|
307
394
|
});
|
|
308
395
|
}
|
|
396
|
+
buildMewpCoverageSheetName(planName, testPlanId) {
|
|
397
|
+
const suffix = String(planName || '').trim() || `Plan ${testPlanId}`;
|
|
398
|
+
return `MEWP L2 Coverage - ${suffix}`;
|
|
399
|
+
}
|
|
400
|
+
createMewpCoverageRow(requirement, stepSummary) {
|
|
401
|
+
const requirementId = String(requirement.requirementId || '').trim();
|
|
402
|
+
const requirementTitle = String(requirement.title || '').trim();
|
|
403
|
+
const customerRequirement = [requirementId, requirementTitle].filter(Boolean).join(' - ');
|
|
404
|
+
const responsibility = String(requirement.responsibility || '').trim();
|
|
405
|
+
return {
|
|
406
|
+
'Customer Requirement': customerRequirement || requirementId || requirementTitle,
|
|
407
|
+
'Requirement ID': requirementId,
|
|
408
|
+
'Requirement Title': requirementTitle,
|
|
409
|
+
Responsibility: responsibility,
|
|
410
|
+
'SAPWBS / Responsibility': responsibility,
|
|
411
|
+
'Number of passed steps': Number.isFinite(stepSummary === null || stepSummary === void 0 ? void 0 : stepSummary.passed) ? stepSummary.passed : 0,
|
|
412
|
+
'Number of failed steps': Number.isFinite(stepSummary === null || stepSummary === void 0 ? void 0 : stepSummary.failed) ? stepSummary.failed : 0,
|
|
413
|
+
'Number of steps not run': Number.isFinite(stepSummary === null || stepSummary === void 0 ? void 0 : stepSummary.notRun) ? stepSummary.notRun : 0,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
buildTestCaseStepsXmlMap(testData) {
|
|
417
|
+
var _a, _b;
|
|
418
|
+
const map = new Map();
|
|
419
|
+
for (const suite of testData || []) {
|
|
420
|
+
const testCasesItems = Array.isArray(suite === null || suite === void 0 ? void 0 : suite.testCasesItems) ? suite.testCasesItems : [];
|
|
421
|
+
for (const testCase of testCasesItems) {
|
|
422
|
+
const id = Number((_a = testCase === null || testCase === void 0 ? void 0 : testCase.workItem) === null || _a === void 0 ? void 0 : _a.id);
|
|
423
|
+
if (!Number.isFinite(id))
|
|
424
|
+
continue;
|
|
425
|
+
if (map.has(id))
|
|
426
|
+
continue;
|
|
427
|
+
const fields = (_b = testCase === null || testCase === void 0 ? void 0 : testCase.workItem) === null || _b === void 0 ? void 0 : _b.workItemFields;
|
|
428
|
+
const stepsXml = this.extractStepsXmlFromWorkItemFields(fields);
|
|
429
|
+
if (stepsXml) {
|
|
430
|
+
map.set(id, stepsXml);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return map;
|
|
435
|
+
}
|
|
436
|
+
extractStepsXmlFromWorkItemFields(workItemFields) {
|
|
437
|
+
if (!Array.isArray(workItemFields))
|
|
438
|
+
return '';
|
|
439
|
+
const isStepsKey = (name) => {
|
|
440
|
+
const normalized = String(name || '').toLowerCase().trim();
|
|
441
|
+
return normalized === 'steps' || normalized === 'microsoft.vsts.tcm.steps';
|
|
442
|
+
};
|
|
443
|
+
for (const field of workItemFields) {
|
|
444
|
+
const keyCandidates = [field === null || field === void 0 ? void 0 : field.key, field === null || field === void 0 ? void 0 : field.name, field === null || field === void 0 ? void 0 : field.referenceName, field === null || field === void 0 ? void 0 : field.id];
|
|
445
|
+
const hasStepsKey = keyCandidates.some((candidate) => isStepsKey(String(candidate || '')));
|
|
446
|
+
if (!hasStepsKey)
|
|
447
|
+
continue;
|
|
448
|
+
const value = String((field === null || field === void 0 ? void 0 : field.value) || '').trim();
|
|
449
|
+
if (value)
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
return '';
|
|
453
|
+
}
|
|
454
|
+
classifyRequirementStepOutcome(outcome) {
|
|
455
|
+
const normalized = String(outcome || '')
|
|
456
|
+
.trim()
|
|
457
|
+
.toLowerCase();
|
|
458
|
+
if (normalized === 'passed')
|
|
459
|
+
return 'passed';
|
|
460
|
+
if (normalized === 'failed')
|
|
461
|
+
return 'failed';
|
|
462
|
+
return 'notRun';
|
|
463
|
+
}
|
|
464
|
+
accumulateRequirementCountsFromStepText(stepText, status, requirementKeys, counters) {
|
|
465
|
+
const codes = this.extractRequirementCodesFromText(stepText);
|
|
466
|
+
for (const code of codes) {
|
|
467
|
+
if (requirementKeys.size > 0 && !requirementKeys.has(code))
|
|
468
|
+
continue;
|
|
469
|
+
if (!counters.has(code)) {
|
|
470
|
+
counters.set(code, { passed: 0, failed: 0, notRun: 0 });
|
|
471
|
+
}
|
|
472
|
+
const counter = counters.get(code);
|
|
473
|
+
if (status === 'passed')
|
|
474
|
+
counter.passed += 1;
|
|
475
|
+
else if (status === 'failed')
|
|
476
|
+
counter.failed += 1;
|
|
477
|
+
else
|
|
478
|
+
counter.notRun += 1;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
extractRequirementCodesFromText(text) {
|
|
482
|
+
const out = new Set();
|
|
483
|
+
const source = this.normalizeRequirementStepText(text);
|
|
484
|
+
// Supports SR<ID> patterns even when HTML formatting breaks the token,
|
|
485
|
+
// e.g. "S<b>R</b> 0 0 1" or "S R 0 0 1".
|
|
486
|
+
const regex = /S[\s\u00A0]*R(?:[\s\u00A0\-_]*\d){1,12}/gi;
|
|
487
|
+
let match = null;
|
|
488
|
+
while ((match = regex.exec(source)) !== null) {
|
|
489
|
+
const digitsOnly = String(match[0] || '').replace(/\D/g, '');
|
|
490
|
+
const digits = Number.parseInt(digitsOnly, 10);
|
|
491
|
+
if (Number.isFinite(digits)) {
|
|
492
|
+
out.add(`SR${digits}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
normalizeRequirementStepText(text) {
|
|
498
|
+
const raw = String(text || '');
|
|
499
|
+
if (!raw)
|
|
500
|
+
return '';
|
|
501
|
+
return raw
|
|
502
|
+
.replace(/ | | /gi, ' ')
|
|
503
|
+
.replace(/</gi, '<')
|
|
504
|
+
.replace(/>/gi, '>')
|
|
505
|
+
.replace(/&/gi, '&')
|
|
506
|
+
.replace(/"/gi, '"')
|
|
507
|
+
.replace(/'|'/gi, "'")
|
|
508
|
+
.replace(/<[^>]*>/g, ' ')
|
|
509
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
510
|
+
.replace(/\s+/g, ' ');
|
|
511
|
+
}
|
|
512
|
+
toRequirementKey(requirementId) {
|
|
513
|
+
const normalized = this.normalizeMewpRequirementCode(requirementId);
|
|
514
|
+
if (!normalized)
|
|
515
|
+
return '';
|
|
516
|
+
const digits = Number.parseInt(normalized.replace(/^SR/i, ''), 10);
|
|
517
|
+
if (!Number.isFinite(digits))
|
|
518
|
+
return '';
|
|
519
|
+
return `SR${digits}`;
|
|
520
|
+
}
|
|
521
|
+
async fetchMewpL2Requirements(projectName) {
|
|
522
|
+
var _a;
|
|
523
|
+
const workItemTypeNames = await this.fetchMewpRequirementTypeNames(projectName);
|
|
524
|
+
if (workItemTypeNames.length === 0) {
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
const quotedTypeNames = workItemTypeNames
|
|
528
|
+
.map((name) => `'${String(name).replace(/'/g, "''")}'`)
|
|
529
|
+
.join(', ');
|
|
530
|
+
const wiql = `SELECT [System.Id]
|
|
531
|
+
FROM WorkItems
|
|
532
|
+
WHERE [System.TeamProject] = @project
|
|
533
|
+
AND [System.WorkItemType] IN (${quotedTypeNames})
|
|
534
|
+
ORDER BY [System.Id]`;
|
|
535
|
+
const wiqlUrl = `${this.orgUrl}${projectName}/_apis/wit/wiql?api-version=7.1-preview.2`;
|
|
536
|
+
const wiqlResponse = await tfs_1.TFSServices.postRequest(wiqlUrl, this.token, 'Post', { query: wiql }, null);
|
|
537
|
+
const workItemRefs = Array.isArray((_a = wiqlResponse === null || wiqlResponse === void 0 ? void 0 : wiqlResponse.data) === null || _a === void 0 ? void 0 : _a.workItems) ? wiqlResponse.data.workItems : [];
|
|
538
|
+
const requirementIds = workItemRefs
|
|
539
|
+
.map((item) => Number(item === null || item === void 0 ? void 0 : item.id))
|
|
540
|
+
.filter((id) => Number.isFinite(id));
|
|
541
|
+
if (requirementIds.length === 0) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
const workItems = await this.fetchWorkItemsByIds(projectName, requirementIds, true);
|
|
545
|
+
const requirements = workItems.map((wi) => {
|
|
546
|
+
const fields = (wi === null || wi === void 0 ? void 0 : wi.fields) || {};
|
|
547
|
+
return {
|
|
548
|
+
workItemId: Number((wi === null || wi === void 0 ? void 0 : wi.id) || 0),
|
|
549
|
+
requirementId: this.extractMewpRequirementIdentifier(fields, Number((wi === null || wi === void 0 ? void 0 : wi.id) || 0)),
|
|
550
|
+
title: String(fields['System.Title'] || ''),
|
|
551
|
+
responsibility: this.deriveMewpResponsibility(fields),
|
|
552
|
+
linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement((wi === null || wi === void 0 ? void 0 : wi.relations) || []),
|
|
553
|
+
};
|
|
554
|
+
});
|
|
555
|
+
return requirements.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
|
|
556
|
+
}
|
|
557
|
+
async fetchMewpRequirementTypeNames(projectName) {
|
|
558
|
+
try {
|
|
559
|
+
const url = `${this.orgUrl}${projectName}/_apis/wit/workitemtypes?api-version=7.1-preview.2`;
|
|
560
|
+
const result = await tfs_1.TFSServices.getItemContent(url, this.token);
|
|
561
|
+
const values = Array.isArray(result === null || result === void 0 ? void 0 : result.value) ? result.value : [];
|
|
562
|
+
const matched = values
|
|
563
|
+
.map((item) => String((item === null || item === void 0 ? void 0 : item.name) || ''))
|
|
564
|
+
.filter((name) => /requirement/i.test(name) || /^epic$/i.test(name));
|
|
565
|
+
const unique = Array.from(new Set(matched));
|
|
566
|
+
if (unique.length > 0) {
|
|
567
|
+
return unique;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
logger_1.default.debug(`Could not fetch MEWP work item types, using defaults: ${(error === null || error === void 0 ? void 0 : error.message) || error}`);
|
|
572
|
+
}
|
|
573
|
+
return ['Requirement', 'Epic'];
|
|
574
|
+
}
|
|
575
|
+
async fetchWorkItemsByIds(projectName, workItemIds, includeRelations) {
|
|
576
|
+
const ids = [...new Set(workItemIds.filter((id) => Number.isFinite(id)))];
|
|
577
|
+
if (ids.length === 0)
|
|
578
|
+
return [];
|
|
579
|
+
const CHUNK_SIZE = 200;
|
|
580
|
+
const allItems = [];
|
|
581
|
+
for (let i = 0; i < ids.length; i += CHUNK_SIZE) {
|
|
582
|
+
const chunk = ids.slice(i, i + CHUNK_SIZE);
|
|
583
|
+
const idsQuery = chunk.join(',');
|
|
584
|
+
const expandParam = includeRelations ? '&$expand=relations' : '';
|
|
585
|
+
const url = `${this.orgUrl}${projectName}/_apis/wit/workitems?ids=${idsQuery}${expandParam}&api-version=7.1-preview.3`;
|
|
586
|
+
const response = await tfs_1.TFSServices.getItemContent(url, this.token);
|
|
587
|
+
const values = Array.isArray(response === null || response === void 0 ? void 0 : response.value) ? response.value : [];
|
|
588
|
+
allItems.push(...values);
|
|
589
|
+
}
|
|
590
|
+
return allItems;
|
|
591
|
+
}
|
|
592
|
+
extractLinkedTestCaseIdsFromRequirement(relations) {
|
|
593
|
+
const out = new Set();
|
|
594
|
+
for (const relation of Array.isArray(relations) ? relations : []) {
|
|
595
|
+
const rel = String((relation === null || relation === void 0 ? void 0 : relation.rel) || '')
|
|
596
|
+
.trim()
|
|
597
|
+
.toLowerCase();
|
|
598
|
+
const isRequirementToTestLink = rel.includes('testedby') || rel.includes('.tests');
|
|
599
|
+
if (!isRequirementToTestLink)
|
|
600
|
+
continue;
|
|
601
|
+
const url = String((relation === null || relation === void 0 ? void 0 : relation.url) || '');
|
|
602
|
+
const match = /\/workItems\/(\d+)/i.exec(url);
|
|
603
|
+
if (!match)
|
|
604
|
+
continue;
|
|
605
|
+
const id = Number(match[1]);
|
|
606
|
+
if (Number.isFinite(id))
|
|
607
|
+
out.add(id);
|
|
608
|
+
}
|
|
609
|
+
return [...out].sort((a, b) => a - b);
|
|
610
|
+
}
|
|
611
|
+
extractMewpRequirementIdentifier(fields, fallbackWorkItemId) {
|
|
612
|
+
const entries = Object.entries(fields || {});
|
|
613
|
+
// First pass: only trusted identifier-like fields.
|
|
614
|
+
const strictHints = [
|
|
615
|
+
'customerid',
|
|
616
|
+
'customer id',
|
|
617
|
+
'customerrequirementid',
|
|
618
|
+
'requirementid',
|
|
619
|
+
'externalid',
|
|
620
|
+
'srid',
|
|
621
|
+
'sapwbsid',
|
|
622
|
+
];
|
|
623
|
+
for (const [key, value] of entries) {
|
|
624
|
+
const normalizedKey = String(key || '').toLowerCase();
|
|
625
|
+
if (!strictHints.some((hint) => normalizedKey.includes(hint)))
|
|
626
|
+
continue;
|
|
627
|
+
const valueAsString = this.toMewpComparableText(value);
|
|
628
|
+
if (!valueAsString)
|
|
629
|
+
continue;
|
|
630
|
+
const normalized = this.normalizeMewpRequirementCode(valueAsString);
|
|
631
|
+
if (normalized)
|
|
632
|
+
return normalized;
|
|
633
|
+
}
|
|
634
|
+
// Second pass: weaker hints, but still key-based only.
|
|
635
|
+
const looseHints = ['customer', 'requirement', 'external', 'sapwbs', 'sr'];
|
|
636
|
+
for (const [key, value] of entries) {
|
|
637
|
+
const normalizedKey = String(key || '').toLowerCase();
|
|
638
|
+
if (!looseHints.some((hint) => normalizedKey.includes(hint)))
|
|
639
|
+
continue;
|
|
640
|
+
const valueAsString = this.toMewpComparableText(value);
|
|
641
|
+
if (!valueAsString)
|
|
642
|
+
continue;
|
|
643
|
+
const normalized = this.normalizeMewpRequirementCode(valueAsString);
|
|
644
|
+
if (normalized)
|
|
645
|
+
return normalized;
|
|
646
|
+
}
|
|
647
|
+
// Optional fallback from title only (avoid scanning all fields and accidental SR matches).
|
|
648
|
+
const title = this.toMewpComparableText(fields === null || fields === void 0 ? void 0 : fields['System.Title']);
|
|
649
|
+
const titleCode = this.normalizeMewpRequirementCode(title);
|
|
650
|
+
if (titleCode)
|
|
651
|
+
return titleCode;
|
|
652
|
+
return String(fallbackWorkItemId || '');
|
|
653
|
+
}
|
|
654
|
+
deriveMewpResponsibility(fields) {
|
|
655
|
+
const areaPath = this.toMewpComparableText(fields === null || fields === void 0 ? void 0 : fields['System.AreaPath']);
|
|
656
|
+
const fromAreaPath = this.resolveMewpResponsibility(areaPath);
|
|
657
|
+
if (fromAreaPath)
|
|
658
|
+
return fromAreaPath;
|
|
659
|
+
const keyHints = ['sapwbs', 'responsibility', 'owner'];
|
|
660
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
661
|
+
const normalizedKey = String(key || '').toLowerCase();
|
|
662
|
+
if (!keyHints.some((hint) => normalizedKey.includes(hint)))
|
|
663
|
+
continue;
|
|
664
|
+
const resolved = this.resolveMewpResponsibility(this.toMewpComparableText(value));
|
|
665
|
+
if (resolved)
|
|
666
|
+
return resolved;
|
|
667
|
+
}
|
|
668
|
+
return '';
|
|
669
|
+
}
|
|
670
|
+
resolveMewpResponsibility(value) {
|
|
671
|
+
const text = String(value || '')
|
|
672
|
+
.trim()
|
|
673
|
+
.toLowerCase();
|
|
674
|
+
if (!text)
|
|
675
|
+
return '';
|
|
676
|
+
if (/(^|[^a-z0-9])esuk([^a-z0-9]|$)/i.test(text))
|
|
677
|
+
return 'ESUK';
|
|
678
|
+
if (/(^|[^a-z0-9])il([^a-z0-9]|$)/i.test(text))
|
|
679
|
+
return 'IL';
|
|
680
|
+
return '';
|
|
681
|
+
}
|
|
682
|
+
normalizeMewpRequirementCode(value) {
|
|
683
|
+
const text = String(value || '').trim();
|
|
684
|
+
if (!text)
|
|
685
|
+
return '';
|
|
686
|
+
const match = /\bSR[\s\-_]*([0-9]+)\b/i.exec(text);
|
|
687
|
+
if (!match)
|
|
688
|
+
return '';
|
|
689
|
+
return `SR${match[1]}`;
|
|
690
|
+
}
|
|
691
|
+
toMewpComparableText(value) {
|
|
692
|
+
if (value === null || value === undefined)
|
|
693
|
+
return '';
|
|
694
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
695
|
+
return String(value).trim();
|
|
696
|
+
}
|
|
697
|
+
if (typeof value === 'object') {
|
|
698
|
+
const displayName = value.displayName;
|
|
699
|
+
if (displayName)
|
|
700
|
+
return String(displayName).trim();
|
|
701
|
+
const name = value.name;
|
|
702
|
+
if (name)
|
|
703
|
+
return String(name).trim();
|
|
704
|
+
const uniqueName = value.uniqueName;
|
|
705
|
+
if (uniqueName)
|
|
706
|
+
return String(uniqueName).trim();
|
|
707
|
+
}
|
|
708
|
+
return String(value).trim();
|
|
709
|
+
}
|
|
309
710
|
async fetchTestPlanName(testPlanId, teamProject) {
|
|
310
711
|
try {
|
|
311
712
|
const url = `${this.orgUrl}${teamProject}/_apis/testplan/Plans/${testPlanId}?api-version=5.1`;
|
|
@@ -1444,11 +1845,19 @@ class ResultDataProvider {
|
|
|
1444
1845
|
try {
|
|
1445
1846
|
const { lastRunId, lastResultId } = point;
|
|
1446
1847
|
const resultData = await fetchResultMethod(projectName, (lastRunId === null || lastRunId === void 0 ? void 0 : lastRunId.toString()) || '0', (lastResultId === null || lastResultId === void 0 ? void 0 : lastResultId.toString()) || '0', ...additionalArgs);
|
|
1447
|
-
|
|
1848
|
+
let iteration = ((_a = resultData.iterationDetails) === null || _a === void 0 ? void 0 : _a.length) > 0
|
|
1448
1849
|
? resultData.iterationDetails[resultData.iterationDetails.length - 1]
|
|
1449
1850
|
: undefined;
|
|
1851
|
+
if (resultData.stepsResultXml && !iteration) {
|
|
1852
|
+
iteration = { actionResults: [] };
|
|
1853
|
+
if (!Array.isArray(resultData.iterationDetails)) {
|
|
1854
|
+
resultData.iterationDetails = [];
|
|
1855
|
+
}
|
|
1856
|
+
resultData.iterationDetails.push(iteration);
|
|
1857
|
+
}
|
|
1450
1858
|
if (resultData.stepsResultXml && iteration) {
|
|
1451
|
-
const
|
|
1859
|
+
const actionResults = Array.isArray(iteration.actionResults) ? iteration.actionResults : [];
|
|
1860
|
+
const actionResultsWithSharedModels = actionResults.filter((result) => result.sharedStepModel);
|
|
1452
1861
|
const sharedStepIdToRevisionLookupMap = new Map();
|
|
1453
1862
|
if ((actionResultsWithSharedModels === null || actionResultsWithSharedModels === void 0 ? void 0 : actionResultsWithSharedModels.length) > 0) {
|
|
1454
1863
|
actionResultsWithSharedModels.forEach((actionResult) => {
|
|
@@ -1462,7 +1871,7 @@ class ResultDataProvider {
|
|
|
1462
1871
|
for (const step of stepsList) {
|
|
1463
1872
|
stepMap.set(step.stepId.toString(), step);
|
|
1464
1873
|
}
|
|
1465
|
-
for (const actionResult of
|
|
1874
|
+
for (const actionResult of actionResults) {
|
|
1466
1875
|
const step = stepMap.get(actionResult.stepIdentifier);
|
|
1467
1876
|
if (step) {
|
|
1468
1877
|
actionResult.stepPosition = step.stepPosition;
|
|
@@ -1471,10 +1880,31 @@ class ResultDataProvider {
|
|
|
1471
1880
|
actionResult.isSharedStepTitle = step.isSharedStepTitle;
|
|
1472
1881
|
}
|
|
1473
1882
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
.
|
|
1477
|
-
|
|
1883
|
+
if (actionResults.length > 0) {
|
|
1884
|
+
// Sort mapped action results by logical step position.
|
|
1885
|
+
iteration.actionResults = actionResults
|
|
1886
|
+
.filter((result) => result.stepPosition)
|
|
1887
|
+
.sort((a, b) => this.compareActionResults(a.stepPosition, b.stepPosition));
|
|
1888
|
+
}
|
|
1889
|
+
else {
|
|
1890
|
+
// Fallback for runs that have no action results: emit test definition steps as Not Run.
|
|
1891
|
+
iteration.actionResults = stepsList
|
|
1892
|
+
.filter((step) => step === null || step === void 0 ? void 0 : step.stepPosition)
|
|
1893
|
+
.map((step) => {
|
|
1894
|
+
var _a, _b, _c;
|
|
1895
|
+
return ({
|
|
1896
|
+
stepIdentifier: String((_b = (_a = step === null || step === void 0 ? void 0 : step.stepId) !== null && _a !== void 0 ? _a : step === null || step === void 0 ? void 0 : step.stepPosition) !== null && _b !== void 0 ? _b : ''),
|
|
1897
|
+
stepPosition: step.stepPosition,
|
|
1898
|
+
action: step.action,
|
|
1899
|
+
expected: step.expected,
|
|
1900
|
+
isSharedStepTitle: step.isSharedStepTitle,
|
|
1901
|
+
outcome: 'Unspecified',
|
|
1902
|
+
errorMessage: '',
|
|
1903
|
+
actionPath: String((_c = step === null || step === void 0 ? void 0 : step.stepPosition) !== null && _c !== void 0 ? _c : ''),
|
|
1904
|
+
});
|
|
1905
|
+
})
|
|
1906
|
+
.sort((a, b) => this.compareActionResults(a.stepPosition, b.stepPosition));
|
|
1907
|
+
}
|
|
1478
1908
|
}
|
|
1479
1909
|
return (resultData === null || resultData === void 0 ? void 0 : resultData.testCase)
|
|
1480
1910
|
? createResponseObject(resultData, testSuiteId, point, ...additionalArgs)
|
|
@@ -1936,6 +2366,7 @@ class ResultDataProvider {
|
|
|
1936
2366
|
parentSuiteName,
|
|
1937
2367
|
suitePath,
|
|
1938
2368
|
testCaseId: point === null || point === void 0 ? void 0 : point.testCaseId,
|
|
2369
|
+
testCaseName: point === null || point === void 0 ? void 0 : point.testCaseName,
|
|
1939
2370
|
customFields,
|
|
1940
2371
|
pointOutcome: point === null || point === void 0 ? void 0 : point.outcome,
|
|
1941
2372
|
testCaseResultMessage: (_g = (_f = fetchedTestCase === null || fetchedTestCase === void 0 ? void 0 : fetchedTestCase.testCaseResult) === null || _f === void 0 ? void 0 : _f.resultMessage) !== null && _g !== void 0 ? _g : '',
|
|
@@ -1946,7 +2377,7 @@ class ResultDataProvider {
|
|
|
1946
2377
|
testPointId: point === null || point === void 0 ? void 0 : point.testPointId,
|
|
1947
2378
|
tester: (_s = (_p = fetchedTestCase === null || fetchedTestCase === void 0 ? void 0 : fetchedTestCase.runBy) !== null && _p !== void 0 ? _p : (_r = (_q = point === null || point === void 0 ? void 0 : point.lastResultDetails) === null || _q === void 0 ? void 0 : _q.runBy) === null || _r === void 0 ? void 0 : _r.displayName) !== null && _s !== void 0 ? _s : '',
|
|
1948
2379
|
stepOutcome: actionResult === null || actionResult === void 0 ? void 0 : actionResult.outcome,
|
|
1949
|
-
stepStepIdentifier: (_t =
|
|
2380
|
+
stepStepIdentifier: (_t = actionResult === null || actionResult === void 0 ? void 0 : actionResult.stepPosition) !== null && _t !== void 0 ? _t : '',
|
|
1950
2381
|
};
|
|
1951
2382
|
},
|
|
1952
2383
|
});
|
|
@@ -2109,5 +2540,15 @@ class ResultDataProvider {
|
|
|
2109
2540
|
return customFields;
|
|
2110
2541
|
}
|
|
2111
2542
|
}
|
|
2543
|
+
ResultDataProvider.MEWP_L2_COVERAGE_COLUMNS = [
|
|
2544
|
+
'Customer Requirement',
|
|
2545
|
+
'Requirement ID',
|
|
2546
|
+
'Requirement Title',
|
|
2547
|
+
'Responsibility',
|
|
2548
|
+
'SAPWBS / Responsibility',
|
|
2549
|
+
'Number of passed steps',
|
|
2550
|
+
'Number of failed steps',
|
|
2551
|
+
'Number of steps not run',
|
|
2552
|
+
];
|
|
2112
2553
|
exports.default = ResultDataProvider;
|
|
2113
2554
|
//# sourceMappingURL=ResultDataProvider.js.map
|