@elisra-devops/docgen-data-provider 1.72.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.
@@ -29,6 +29,17 @@ const pLimit = require('p-limit');
29
29
  * Instantiate the class with the organization URL and token, and use the provided methods to fetch and process test data.
30
30
  */
31
31
  export default class ResultDataProvider {
32
+ private static readonly MEWP_L2_COVERAGE_COLUMNS = [
33
+ 'Customer Requirement',
34
+ 'Requirement ID',
35
+ 'Requirement Title',
36
+ 'Responsibility',
37
+ 'SAPWBS / Responsibility',
38
+ 'Number of passed steps',
39
+ 'Number of failed steps',
40
+ 'Number of steps not run',
41
+ ];
42
+
32
43
  orgUrl: string = '';
33
44
  token: string = '';
34
45
  private limit = pLimit(10);
@@ -375,6 +386,121 @@ export default class ResultDataProvider {
375
386
  }
376
387
  }
377
388
 
389
+ /**
390
+ * Builds MEWP L2 requirement coverage rows for audit reporting.
391
+ * Rows are one Requirement-TestCase pair; uncovered requirements are emitted with empty test-case columns.
392
+ */
393
+ public async getMewpL2CoverageFlatResults(
394
+ testPlanId: string,
395
+ projectName: string,
396
+ selectedSuiteIds: number[] | undefined
397
+ ) {
398
+ const defaultPayload = {
399
+ sheetName: `MEWP L2 Coverage - Plan ${testPlanId}`,
400
+ columnOrder: [...ResultDataProvider.MEWP_L2_COVERAGE_COLUMNS],
401
+ rows: [] as any[],
402
+ };
403
+
404
+ try {
405
+ const planName = await this.fetchTestPlanName(testPlanId, projectName);
406
+ const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
407
+ const testData = await this.fetchTestData(suites, projectName, testPlanId, false);
408
+
409
+ const requirements = await this.fetchMewpL2Requirements(projectName);
410
+ if (requirements.length === 0) {
411
+ return {
412
+ ...defaultPayload,
413
+ sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
414
+ };
415
+ }
416
+
417
+ const requirementIndex = new Map<
418
+ string,
419
+ { passed: number; failed: number; notRun: number }
420
+ >();
421
+ const requirementKeys = new Set<string>();
422
+ requirements.forEach((requirement) => {
423
+ const key = this.toRequirementKey(requirement.requirementId);
424
+ if (!key) return;
425
+ requirementKeys.add(key);
426
+ if (!requirementIndex.has(key)) {
427
+ requirementIndex.set(key, { passed: 0, failed: 0, notRun: 0 });
428
+ }
429
+ });
430
+
431
+ const parsedDefinitionStepsByTestCase = new Map<number, TestSteps[]>();
432
+ const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
433
+
434
+ const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
435
+ for (const runResult of runResults) {
436
+ const testCaseId = Number(runResult?.testCaseId);
437
+ const actionResults = Array.isArray(runResult?.iteration?.actionResults)
438
+ ? runResult.iteration.actionResults
439
+ : [];
440
+ const hasExecutedRun =
441
+ Number(runResult?.lastRunId || 0) > 0 && Number(runResult?.lastResultId || 0) > 0;
442
+
443
+ if (actionResults.length > 0) {
444
+ for (const actionResult of actionResults) {
445
+ if (actionResult?.isSharedStepTitle) continue;
446
+ const stepStatus = this.classifyRequirementStepOutcome(actionResult?.outcome);
447
+ this.accumulateRequirementCountsFromStepText(
448
+ `${String(actionResult?.action || '')} ${String(actionResult?.expected || '')}`,
449
+ stepStatus,
450
+ requirementKeys,
451
+ requirementIndex
452
+ );
453
+ }
454
+ continue;
455
+ }
456
+
457
+ // Do not force "not run" from definition steps when a run exists:
458
+ // some runs may have missing/unmapped actionResults.
459
+ if (hasExecutedRun) {
460
+ continue;
461
+ }
462
+
463
+ if (!Number.isFinite(testCaseId)) continue;
464
+ if (!parsedDefinitionStepsByTestCase.has(testCaseId)) {
465
+ const stepsXml = testCaseStepsXmlMap.get(testCaseId) || '';
466
+ const parsed =
467
+ stepsXml && String(stepsXml).trim() !== ''
468
+ ? await this.testStepParserHelper.parseTestSteps(stepsXml, new Map<number, number>())
469
+ : [];
470
+ parsedDefinitionStepsByTestCase.set(testCaseId, parsed);
471
+ }
472
+
473
+ const definitionSteps = parsedDefinitionStepsByTestCase.get(testCaseId) || [];
474
+ for (const step of definitionSteps) {
475
+ if (step?.isSharedStepTitle) continue;
476
+ this.accumulateRequirementCountsFromStepText(
477
+ `${String(step?.action || '')} ${String(step?.expected || '')}`,
478
+ 'notRun',
479
+ requirementKeys,
480
+ requirementIndex
481
+ );
482
+ }
483
+ }
484
+
485
+ const rows: any[] = requirements.map((requirement) => {
486
+ const key = this.toRequirementKey(requirement.requirementId);
487
+ const summary = key && requirementIndex.has(key)
488
+ ? requirementIndex.get(key)!
489
+ : { passed: 0, failed: 0, notRun: 0 };
490
+ return this.createMewpCoverageRow(requirement, summary);
491
+ });
492
+
493
+ return {
494
+ sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
495
+ columnOrder: [...ResultDataProvider.MEWP_L2_COVERAGE_COLUMNS],
496
+ rows,
497
+ };
498
+ } catch (error: any) {
499
+ logger.error(`Error during getMewpL2CoverageFlatResults: ${error.message}`);
500
+ return defaultPayload;
501
+ }
502
+ }
503
+
378
504
  /**
379
505
  * Mapping each attachment to a proper URL for downloading it
380
506
  * @param runResults Array of run results
@@ -413,6 +539,344 @@ export default class ResultDataProvider {
413
539
  });
414
540
  }
415
541
 
542
+ private buildMewpCoverageSheetName(planName: string, testPlanId: string): string {
543
+ const suffix = String(planName || '').trim() || `Plan ${testPlanId}`;
544
+ return `MEWP L2 Coverage - ${suffix}`;
545
+ }
546
+
547
+ private createMewpCoverageRow(
548
+ requirement: {
549
+ requirementId: string;
550
+ title: string;
551
+ responsibility: string;
552
+ },
553
+ stepSummary: { passed: number; failed: number; notRun: number }
554
+ ) {
555
+ const requirementId = String(requirement.requirementId || '').trim();
556
+ const requirementTitle = String(requirement.title || '').trim();
557
+ const customerRequirement = [requirementId, requirementTitle].filter(Boolean).join(' - ');
558
+ const responsibility = String(requirement.responsibility || '').trim();
559
+
560
+ return {
561
+ 'Customer Requirement': customerRequirement || requirementId || requirementTitle,
562
+ 'Requirement ID': requirementId,
563
+ 'Requirement Title': requirementTitle,
564
+ Responsibility: responsibility,
565
+ 'SAPWBS / Responsibility': responsibility,
566
+ 'Number of passed steps': Number.isFinite(stepSummary?.passed) ? stepSummary.passed : 0,
567
+ 'Number of failed steps': Number.isFinite(stepSummary?.failed) ? stepSummary.failed : 0,
568
+ 'Number of steps not run': Number.isFinite(stepSummary?.notRun) ? stepSummary.notRun : 0,
569
+ };
570
+ }
571
+
572
+ private buildTestCaseStepsXmlMap(testData: any[]): Map<number, string> {
573
+ const map = new Map<number, string>();
574
+ for (const suite of testData || []) {
575
+ const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
576
+ for (const testCase of testCasesItems) {
577
+ const id = Number(testCase?.workItem?.id);
578
+ if (!Number.isFinite(id)) continue;
579
+ if (map.has(id)) continue;
580
+ const fields = testCase?.workItem?.workItemFields;
581
+ const stepsXml = this.extractStepsXmlFromWorkItemFields(fields);
582
+ if (stepsXml) {
583
+ map.set(id, stepsXml);
584
+ }
585
+ }
586
+ }
587
+ return map;
588
+ }
589
+
590
+ private extractStepsXmlFromWorkItemFields(workItemFields: any): string {
591
+ if (!Array.isArray(workItemFields)) return '';
592
+ const isStepsKey = (name: string) => {
593
+ const normalized = String(name || '').toLowerCase().trim();
594
+ return normalized === 'steps' || normalized === 'microsoft.vsts.tcm.steps';
595
+ };
596
+
597
+ for (const field of workItemFields) {
598
+ const keyCandidates = [field?.key, field?.name, field?.referenceName, field?.id];
599
+ const hasStepsKey = keyCandidates.some((candidate) => isStepsKey(String(candidate || '')));
600
+ if (!hasStepsKey) continue;
601
+ const value = String(field?.value || '').trim();
602
+ if (value) return value;
603
+ }
604
+
605
+ return '';
606
+ }
607
+
608
+ private classifyRequirementStepOutcome(outcome: any): 'passed' | 'failed' | 'notRun' {
609
+ const normalized = String(outcome || '')
610
+ .trim()
611
+ .toLowerCase();
612
+ if (normalized === 'passed') return 'passed';
613
+ if (normalized === 'failed') return 'failed';
614
+ return 'notRun';
615
+ }
616
+
617
+ private accumulateRequirementCountsFromStepText(
618
+ stepText: string,
619
+ status: 'passed' | 'failed' | 'notRun',
620
+ requirementKeys: Set<string>,
621
+ counters: Map<string, { passed: number; failed: number; notRun: number }>
622
+ ) {
623
+ const codes = this.extractRequirementCodesFromText(stepText);
624
+ for (const code of codes) {
625
+ if (requirementKeys.size > 0 && !requirementKeys.has(code)) continue;
626
+ if (!counters.has(code)) {
627
+ counters.set(code, { passed: 0, failed: 0, notRun: 0 });
628
+ }
629
+ const counter = counters.get(code)!;
630
+ if (status === 'passed') counter.passed += 1;
631
+ else if (status === 'failed') counter.failed += 1;
632
+ else counter.notRun += 1;
633
+ }
634
+ }
635
+
636
+ private extractRequirementCodesFromText(text: string): Set<string> {
637
+ const out = new Set<string>();
638
+ const source = this.normalizeRequirementStepText(text);
639
+ // Supports SR<ID> patterns even when HTML formatting breaks the token,
640
+ // e.g. "S<b>R</b> 0 0 1" or "S R 0 0 1".
641
+ const regex = /S[\s\u00A0]*R(?:[\s\u00A0\-_]*\d){1,12}/gi;
642
+ let match: RegExpExecArray | null = null;
643
+ while ((match = regex.exec(source)) !== null) {
644
+ const digitsOnly = String(match[0] || '').replace(/\D/g, '');
645
+ const digits = Number.parseInt(digitsOnly, 10);
646
+ if (Number.isFinite(digits)) {
647
+ out.add(`SR${digits}`);
648
+ }
649
+ }
650
+ return out;
651
+ }
652
+
653
+ private normalizeRequirementStepText(text: string): string {
654
+ const raw = String(text || '');
655
+ if (!raw) return '';
656
+
657
+ return raw
658
+ .replace(/&nbsp;|&#160;|&#xA0;/gi, ' ')
659
+ .replace(/&lt;/gi, '<')
660
+ .replace(/&gt;/gi, '>')
661
+ .replace(/&amp;/gi, '&')
662
+ .replace(/&quot;/gi, '"')
663
+ .replace(/&#39;|&apos;/gi, "'")
664
+ .replace(/<[^>]*>/g, ' ')
665
+ .replace(/[\u200B-\u200D\uFEFF]/g, '')
666
+ .replace(/\s+/g, ' ');
667
+ }
668
+
669
+ private toRequirementKey(requirementId: string): string {
670
+ const normalized = this.normalizeMewpRequirementCode(requirementId);
671
+ if (!normalized) return '';
672
+ const digits = Number.parseInt(normalized.replace(/^SR/i, ''), 10);
673
+ if (!Number.isFinite(digits)) return '';
674
+ return `SR${digits}`;
675
+ }
676
+
677
+ private async fetchMewpL2Requirements(projectName: string): Promise<
678
+ Array<{
679
+ workItemId: number;
680
+ requirementId: string;
681
+ title: string;
682
+ responsibility: string;
683
+ linkedTestCaseIds: number[];
684
+ }>
685
+ > {
686
+ const workItemTypeNames = await this.fetchMewpRequirementTypeNames(projectName);
687
+ if (workItemTypeNames.length === 0) {
688
+ return [];
689
+ }
690
+
691
+ const quotedTypeNames = workItemTypeNames
692
+ .map((name) => `'${String(name).replace(/'/g, "''")}'`)
693
+ .join(', ');
694
+ const wiql = `SELECT [System.Id]
695
+ FROM WorkItems
696
+ WHERE [System.TeamProject] = @project
697
+ AND [System.WorkItemType] IN (${quotedTypeNames})
698
+ ORDER BY [System.Id]`;
699
+ const wiqlUrl = `${this.orgUrl}${projectName}/_apis/wit/wiql?api-version=7.1-preview.2`;
700
+ const wiqlResponse = await TFSServices.postRequest(wiqlUrl, this.token, 'Post', { query: wiql }, null);
701
+ const workItemRefs = Array.isArray(wiqlResponse?.data?.workItems) ? wiqlResponse.data.workItems : [];
702
+ const requirementIds = workItemRefs
703
+ .map((item: any) => Number(item?.id))
704
+ .filter((id: number) => Number.isFinite(id));
705
+
706
+ if (requirementIds.length === 0) {
707
+ return [];
708
+ }
709
+
710
+ const workItems = await this.fetchWorkItemsByIds(projectName, requirementIds, true);
711
+ const requirements = workItems.map((wi: any) => {
712
+ const fields = wi?.fields || {};
713
+ return {
714
+ workItemId: Number(wi?.id || 0),
715
+ requirementId: this.extractMewpRequirementIdentifier(fields, Number(wi?.id || 0)),
716
+ title: String(fields['System.Title'] || ''),
717
+ responsibility: this.deriveMewpResponsibility(fields),
718
+ linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
719
+ };
720
+ });
721
+
722
+ return requirements.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
723
+ }
724
+
725
+ private async fetchMewpRequirementTypeNames(projectName: string): Promise<string[]> {
726
+ try {
727
+ const url = `${this.orgUrl}${projectName}/_apis/wit/workitemtypes?api-version=7.1-preview.2`;
728
+ const result = await TFSServices.getItemContent(url, this.token);
729
+ const values = Array.isArray(result?.value) ? result.value : [];
730
+ const matched = values
731
+ .map((item: any) => String(item?.name || ''))
732
+ .filter((name: string) => /requirement/i.test(name) || /^epic$/i.test(name));
733
+ const unique = Array.from(new Set<string>(matched));
734
+ if (unique.length > 0) {
735
+ return unique;
736
+ }
737
+ } catch (error: any) {
738
+ logger.debug(`Could not fetch MEWP work item types, using defaults: ${error?.message || error}`);
739
+ }
740
+
741
+ return ['Requirement', 'Epic'];
742
+ }
743
+
744
+ private async fetchWorkItemsByIds(
745
+ projectName: string,
746
+ workItemIds: number[],
747
+ includeRelations: boolean
748
+ ): Promise<any[]> {
749
+ const ids = [...new Set(workItemIds.filter((id) => Number.isFinite(id)))];
750
+ if (ids.length === 0) return [];
751
+
752
+ const CHUNK_SIZE = 200;
753
+ const allItems: any[] = [];
754
+
755
+ for (let i = 0; i < ids.length; i += CHUNK_SIZE) {
756
+ const chunk = ids.slice(i, i + CHUNK_SIZE);
757
+ const idsQuery = chunk.join(',');
758
+ const expandParam = includeRelations ? '&$expand=relations' : '';
759
+ const url = `${this.orgUrl}${projectName}/_apis/wit/workitems?ids=${idsQuery}${expandParam}&api-version=7.1-preview.3`;
760
+ const response = await TFSServices.getItemContent(url, this.token);
761
+ const values = Array.isArray(response?.value) ? response.value : [];
762
+ allItems.push(...values);
763
+ }
764
+
765
+ return allItems;
766
+ }
767
+
768
+ private extractLinkedTestCaseIdsFromRequirement(relations: any[]): number[] {
769
+ const out = new Set<number>();
770
+ for (const relation of Array.isArray(relations) ? relations : []) {
771
+ const rel = String(relation?.rel || '')
772
+ .trim()
773
+ .toLowerCase();
774
+ const isRequirementToTestLink = rel.includes('testedby') || rel.includes('.tests');
775
+ if (!isRequirementToTestLink) continue;
776
+
777
+ const url = String(relation?.url || '');
778
+ const match = /\/workItems\/(\d+)/i.exec(url);
779
+ if (!match) continue;
780
+ const id = Number(match[1]);
781
+ if (Number.isFinite(id)) out.add(id);
782
+ }
783
+ return [...out].sort((a, b) => a - b);
784
+ }
785
+
786
+ private extractMewpRequirementIdentifier(fields: Record<string, any>, fallbackWorkItemId: number): string {
787
+ const entries = Object.entries(fields || {});
788
+
789
+ // First pass: only trusted identifier-like fields.
790
+ const strictHints = [
791
+ 'customerid',
792
+ 'customer id',
793
+ 'customerrequirementid',
794
+ 'requirementid',
795
+ 'externalid',
796
+ 'srid',
797
+ 'sapwbsid',
798
+ ];
799
+ for (const [key, value] of entries) {
800
+ const normalizedKey = String(key || '').toLowerCase();
801
+ if (!strictHints.some((hint) => normalizedKey.includes(hint))) continue;
802
+
803
+ const valueAsString = this.toMewpComparableText(value);
804
+ if (!valueAsString) continue;
805
+ const normalized = this.normalizeMewpRequirementCode(valueAsString);
806
+ if (normalized) return normalized;
807
+ }
808
+
809
+ // Second pass: weaker hints, but still key-based only.
810
+ const looseHints = ['customer', 'requirement', 'external', 'sapwbs', 'sr'];
811
+ for (const [key, value] of entries) {
812
+ const normalizedKey = String(key || '').toLowerCase();
813
+ if (!looseHints.some((hint) => normalizedKey.includes(hint))) continue;
814
+
815
+ const valueAsString = this.toMewpComparableText(value);
816
+ if (!valueAsString) continue;
817
+ const normalized = this.normalizeMewpRequirementCode(valueAsString);
818
+ if (normalized) return normalized;
819
+ }
820
+
821
+ // Optional fallback from title only (avoid scanning all fields and accidental SR matches).
822
+ const title = this.toMewpComparableText(fields?.['System.Title']);
823
+ const titleCode = this.normalizeMewpRequirementCode(title);
824
+ if (titleCode) return titleCode;
825
+
826
+ return String(fallbackWorkItemId || '');
827
+ }
828
+
829
+ private deriveMewpResponsibility(fields: Record<string, any>): string {
830
+ const areaPath = this.toMewpComparableText(fields?.['System.AreaPath']);
831
+ const fromAreaPath = this.resolveMewpResponsibility(areaPath);
832
+ if (fromAreaPath) return fromAreaPath;
833
+
834
+ const keyHints = ['sapwbs', 'responsibility', 'owner'];
835
+ for (const [key, value] of Object.entries(fields || {})) {
836
+ const normalizedKey = String(key || '').toLowerCase();
837
+ if (!keyHints.some((hint) => normalizedKey.includes(hint))) continue;
838
+ const resolved = this.resolveMewpResponsibility(this.toMewpComparableText(value));
839
+ if (resolved) return resolved;
840
+ }
841
+
842
+ return '';
843
+ }
844
+
845
+ private resolveMewpResponsibility(value: string): string {
846
+ const text = String(value || '')
847
+ .trim()
848
+ .toLowerCase();
849
+ if (!text) return '';
850
+
851
+ if (/(^|[^a-z0-9])esuk([^a-z0-9]|$)/i.test(text)) return 'ESUK';
852
+ if (/(^|[^a-z0-9])il([^a-z0-9]|$)/i.test(text)) return 'IL';
853
+ return '';
854
+ }
855
+
856
+ private normalizeMewpRequirementCode(value: string): string {
857
+ const text = String(value || '').trim();
858
+ if (!text) return '';
859
+ const match = /\bSR[\s\-_]*([0-9]+)\b/i.exec(text);
860
+ if (!match) return '';
861
+ return `SR${match[1]}`;
862
+ }
863
+
864
+ private toMewpComparableText(value: any): string {
865
+ if (value === null || value === undefined) return '';
866
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
867
+ return String(value).trim();
868
+ }
869
+ if (typeof value === 'object') {
870
+ const displayName = (value as any).displayName;
871
+ if (displayName) return String(displayName).trim();
872
+ const name = (value as any).name;
873
+ if (name) return String(name).trim();
874
+ const uniqueName = (value as any).uniqueName;
875
+ if (uniqueName) return String(uniqueName).trim();
876
+ }
877
+ return String(value).trim();
878
+ }
879
+
416
880
  private async fetchTestPlanName(testPlanId: string, teamProject: string): Promise<string> {
417
881
  try {
418
882
  const url = `${this.orgUrl}${teamProject}/_apis/testplan/Plans/${testPlanId}?api-version=5.1`;
@@ -1880,13 +2344,22 @@ export default class ResultDataProvider {
1880
2344
  ...additionalArgs
1881
2345
  );
1882
2346
 
1883
- const iteration =
2347
+ let iteration =
1884
2348
  resultData.iterationDetails?.length > 0
1885
2349
  ? resultData.iterationDetails[resultData.iterationDetails.length - 1]
1886
2350
  : undefined;
1887
2351
 
2352
+ if (resultData.stepsResultXml && !iteration) {
2353
+ iteration = { actionResults: [] };
2354
+ if (!Array.isArray(resultData.iterationDetails)) {
2355
+ resultData.iterationDetails = [];
2356
+ }
2357
+ resultData.iterationDetails.push(iteration);
2358
+ }
2359
+
1888
2360
  if (resultData.stepsResultXml && iteration) {
1889
- const actionResultsWithSharedModels = iteration.actionResults.filter(
2361
+ const actionResults = Array.isArray(iteration.actionResults) ? iteration.actionResults : [];
2362
+ const actionResultsWithSharedModels = actionResults.filter(
1890
2363
  (result: any) => result.sharedStepModel
1891
2364
  );
1892
2365
 
@@ -1910,7 +2383,7 @@ export default class ResultDataProvider {
1910
2383
  stepMap.set(step.stepId.toString(), step);
1911
2384
  }
1912
2385
 
1913
- for (const actionResult of iteration.actionResults) {
2386
+ for (const actionResult of actionResults) {
1914
2387
  const step = stepMap.get(actionResult.stepIdentifier);
1915
2388
  if (step) {
1916
2389
  actionResult.stepPosition = step.stepPosition;
@@ -1919,10 +2392,28 @@ export default class ResultDataProvider {
1919
2392
  actionResult.isSharedStepTitle = step.isSharedStepTitle;
1920
2393
  }
1921
2394
  }
1922
- //Sort by step position
1923
- iteration.actionResults = iteration.actionResults
1924
- .filter((result: any) => result.stepPosition)
1925
- .sort((a: any, b: any) => this.compareActionResults(a.stepPosition, b.stepPosition));
2395
+
2396
+ if (actionResults.length > 0) {
2397
+ // Sort mapped action results by logical step position.
2398
+ iteration.actionResults = actionResults
2399
+ .filter((result: any) => result.stepPosition)
2400
+ .sort((a: any, b: any) => this.compareActionResults(a.stepPosition, b.stepPosition));
2401
+ } else {
2402
+ // Fallback for runs that have no action results: emit test definition steps as Not Run.
2403
+ iteration.actionResults = stepsList
2404
+ .filter((step: any) => step?.stepPosition)
2405
+ .map((step: any) => ({
2406
+ stepIdentifier: String(step?.stepId ?? step?.stepPosition ?? ''),
2407
+ stepPosition: step.stepPosition,
2408
+ action: step.action,
2409
+ expected: step.expected,
2410
+ isSharedStepTitle: step.isSharedStepTitle,
2411
+ outcome: 'Unspecified',
2412
+ errorMessage: '',
2413
+ actionPath: String(step?.stepPosition ?? ''),
2414
+ }))
2415
+ .sort((a: any, b: any) => this.compareActionResults(a.stepPosition, b.stepPosition));
2416
+ }
1926
2417
  }
1927
2418
 
1928
2419
  return resultData?.testCase