@elisra-devops/docgen-data-provider 1.95.0 → 1.97.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elisra-devops/docgen-data-provider",
3
- "version": "1.95.0",
3
+ "version": "1.97.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3362,6 +3362,11 @@ export default class ResultDataProvider {
3362
3362
  const parsedAsOf = new Date(String(asOfTimestamp || '').trim());
3363
3363
  if (!Number.isFinite(id) || id <= 0 || Number.isNaN(parsedAsOf.getTime())) return null;
3364
3364
 
3365
+ logger.debug(
3366
+ `[RunlessResolver] Fetching work item ${id} by asOf (raw="${String(
3367
+ asOfTimestamp || ''
3368
+ )}", normalized="${parsedAsOf.toISOString()}", expandAll=${String(expandAll)})`
3369
+ );
3365
3370
  const query: string[] = [`asOf=${encodeURIComponent(parsedAsOf.toISOString())}`];
3366
3371
  if (expandAll) {
3367
3372
  query.push(`$expand=all`);
@@ -3395,6 +3400,29 @@ export default class ResultDataProvider {
3395
3400
  }
3396
3401
  }
3397
3402
 
3403
+ /**
3404
+ * Returns true when the snapshot includes a non-empty test steps XML payload.
3405
+ */
3406
+ private hasStepsInWorkItemSnapshot(workItemData: any): boolean {
3407
+ const stepsXml = this.extractStepsXmlFromFieldsMap(workItemData?.fields || {});
3408
+ return String(stepsXml || '').trim() !== '';
3409
+ }
3410
+
3411
+ private logRunlessSnapshotDecision(testCaseId: number, source: string, snapshot: any | null): void {
3412
+ if (!snapshot) {
3413
+ logger.debug(`[RunlessResolver] TC ${testCaseId}: source=${source}, snapshot=none`);
3414
+ return;
3415
+ }
3416
+
3417
+ const stepsXml = this.extractStepsXmlFromFieldsMap(snapshot?.fields || {});
3418
+ const stepsLength = String(stepsXml || '').trim().length;
3419
+ logger.debug(
3420
+ `[RunlessResolver] TC ${testCaseId}: source=${source}, rev=${String(
3421
+ snapshot?.rev ?? ''
3422
+ )}, hasSteps=${String(stepsLength > 0)}, stepsLength=${String(stepsLength)}`
3423
+ );
3424
+ }
3425
+
3398
3426
  /**
3399
3427
  * Resolves runless test case data using ordered fallbacks:
3400
3428
  * 1) point-based `asOf` snapshot, 2) explicit revision, 3) suite payload snapshot, 4) latest WI.
@@ -3407,6 +3435,8 @@ export default class ResultDataProvider {
3407
3435
  fallbackSnapshot: any,
3408
3436
  expandAll: boolean
3409
3437
  ): Promise<any | null> {
3438
+ let bestSnapshotWithoutSteps: any | null = null;
3439
+
3410
3440
  if (pointAsOfTimestamp) {
3411
3441
  const asOfSnapshot = await this.fetchWorkItemByAsOf(
3412
3442
  projectName,
@@ -3414,7 +3444,13 @@ export default class ResultDataProvider {
3414
3444
  pointAsOfTimestamp,
3415
3445
  expandAll
3416
3446
  );
3417
- if (asOfSnapshot) return asOfSnapshot;
3447
+ this.logRunlessSnapshotDecision(testCaseId, 'asOf', asOfSnapshot);
3448
+ if (asOfSnapshot) {
3449
+ if (this.hasStepsInWorkItemSnapshot(asOfSnapshot)) return asOfSnapshot;
3450
+ bestSnapshotWithoutSteps = asOfSnapshot;
3451
+ }
3452
+ } else {
3453
+ logger.debug(`[RunlessResolver] TC ${testCaseId}: asOf timestamp is empty, skipping asOf fetch`);
3418
3454
  }
3419
3455
 
3420
3456
  const revisionSnapshot = await this.fetchWorkItemByRevision(
@@ -3423,11 +3459,26 @@ export default class ResultDataProvider {
3423
3459
  suiteTestCaseRevision,
3424
3460
  expandAll
3425
3461
  );
3426
- if (revisionSnapshot) return revisionSnapshot;
3462
+ this.logRunlessSnapshotDecision(testCaseId, `revision:${String(suiteTestCaseRevision)}`, revisionSnapshot);
3463
+ if (revisionSnapshot) {
3464
+ if (this.hasStepsInWorkItemSnapshot(revisionSnapshot)) return revisionSnapshot;
3465
+ bestSnapshotWithoutSteps = bestSnapshotWithoutSteps || revisionSnapshot;
3466
+ }
3427
3467
 
3428
- if (fallbackSnapshot) return fallbackSnapshot;
3468
+ this.logRunlessSnapshotDecision(testCaseId, 'suiteSnapshot', fallbackSnapshot);
3469
+ if (fallbackSnapshot) {
3470
+ if (this.hasStepsInWorkItemSnapshot(fallbackSnapshot)) return fallbackSnapshot;
3471
+ bestSnapshotWithoutSteps = bestSnapshotWithoutSteps || fallbackSnapshot;
3472
+ }
3429
3473
 
3430
- return this.fetchWorkItemLatest(projectName, testCaseId, expandAll);
3474
+ const latestSnapshot = await this.fetchWorkItemLatest(projectName, testCaseId, expandAll);
3475
+ this.logRunlessSnapshotDecision(testCaseId, 'latest', latestSnapshot);
3476
+ if (latestSnapshot) {
3477
+ if (this.hasStepsInWorkItemSnapshot(latestSnapshot)) return latestSnapshot;
3478
+ bestSnapshotWithoutSteps = bestSnapshotWithoutSteps || latestSnapshot;
3479
+ }
3480
+
3481
+ return bestSnapshotWithoutSteps;
3431
3482
  }
3432
3483
 
3433
3484
  /**
@@ -3473,6 +3524,13 @@ export default class ResultDataProvider {
3473
3524
  const pointAsOfTimestamp = useRunlessAsOf
3474
3525
  ? String(point?.pointAsOfTimestamp || '').trim()
3475
3526
  : '';
3527
+ logger.debug(
3528
+ `[RunlessResolver] Start TC ${String(testCaseId)}: useRunlessAsOf=${String(
3529
+ useRunlessAsOf
3530
+ )}, pointAsOfTimestamp="${pointAsOfTimestamp}", suiteRevision=${String(
3531
+ suiteTestCaseRevision
3532
+ )}, pointOutcome="${String(point?.outcome || '')}"`
3533
+ );
3476
3534
  const fallbackSnapshot = this.buildWorkItemSnapshotFromSuiteTestCase(
3477
3535
  suiteTestCaseItem,
3478
3536
  testCaseId,
@@ -4441,15 +4499,30 @@ export default class ResultDataProvider {
4441
4499
  includeNotRunTestCases: boolean
4442
4500
  ): Record<string, any> {
4443
4501
  return iterations.reduce((map, iterationItem) => {
4444
- if (
4445
- (isTestReporter && iterationItem.lastRunId && iterationItem.lastResultId) ||
4446
- iterationItem.iteration
4447
- ) {
4502
+ const hasRunIdentifiers =
4503
+ iterationItem?.lastRunId !== undefined &&
4504
+ iterationItem?.lastRunId !== null &&
4505
+ String(iterationItem?.lastRunId).trim() !== '' &&
4506
+ iterationItem?.lastResultId !== undefined &&
4507
+ iterationItem?.lastResultId !== null &&
4508
+ String(iterationItem?.lastResultId).trim() !== '';
4509
+
4510
+ if (hasRunIdentifiers) {
4448
4511
  const key = `${iterationItem.lastRunId}-${iterationItem.lastResultId}-${iterationItem.testCaseId}`;
4449
4512
  map[key] = iterationItem;
4450
4513
  } else if (includeNotRunTestCases) {
4451
4514
  const key = `${iterationItem.testCaseId}`;
4452
4515
  map[key] = iterationItem;
4516
+ if (isTestReporter && iterationItem?.iteration) {
4517
+ logger.debug(
4518
+ `[RunlessResolver] createIterationsMap: mapped runless testCaseId=${String(
4519
+ iterationItem?.testCaseId
4520
+ )} to case-only key`
4521
+ );
4522
+ }
4523
+ } else if (iterationItem?.iteration && !isTestReporter) {
4524
+ const key = `${iterationItem.lastRunId}-${iterationItem.lastResultId}-${iterationItem.testCaseId}`;
4525
+ map[key] = iterationItem;
4453
4526
  }
4454
4527
  return map;
4455
4528
  }, {} as Record<string, any>);
@@ -4012,6 +4012,70 @@ describe('ResultDataProvider', () => {
4012
4012
  expect(res).toEqual(expect.objectContaining({ testCaseRevision: 6 }));
4013
4013
  });
4014
4014
 
4015
+ it('should fallback from asOf snapshot without steps to revision snapshot with steps', async () => {
4016
+ (TFSServices.getItemContent as jest.Mock).mockReset();
4017
+ const point = {
4018
+ testCaseId: '777',
4019
+ testCaseName: 'TC 777',
4020
+ outcome: 'Not Run',
4021
+ pointAsOfTimestamp: '2025-03-01T00:00:00Z',
4022
+ suiteTestCase: {
4023
+ workItem: {
4024
+ id: 777,
4025
+ workItemFields: [{ key: 'System.Rev', value: '21' }],
4026
+ },
4027
+ },
4028
+ testSuite: { id: '1', name: 'Suite' },
4029
+ };
4030
+
4031
+ (TFSServices.getItemContent as jest.Mock)
4032
+ .mockResolvedValueOnce({
4033
+ id: 777,
4034
+ rev: 18,
4035
+ fields: {
4036
+ 'System.State': 'Design',
4037
+ 'System.CreatedDate': '2024-01-01T00:00:00',
4038
+ 'Microsoft.VSTS.TCM.Priority': 1,
4039
+ 'System.Title': 'TC 777',
4040
+ },
4041
+ relations: [],
4042
+ })
4043
+ .mockResolvedValueOnce({
4044
+ id: 777,
4045
+ rev: 21,
4046
+ fields: {
4047
+ 'System.State': 'Design',
4048
+ 'System.CreatedDate': '2024-01-01T00:00:00',
4049
+ 'Microsoft.VSTS.TCM.Priority': 1,
4050
+ 'System.Title': 'TC 777',
4051
+ 'Microsoft.VSTS.TCM.Steps': '<steps></steps>',
4052
+ },
4053
+ relations: [],
4054
+ });
4055
+
4056
+ const res = await (resultDataProvider as any).fetchResultDataBasedOnWiBase(
4057
+ mockProjectName,
4058
+ '0',
4059
+ '0',
4060
+ true,
4061
+ [],
4062
+ false,
4063
+ point,
4064
+ false,
4065
+ true
4066
+ );
4067
+
4068
+ const calledUrls = (TFSServices.getItemContent as jest.Mock).mock.calls.map((args: any[]) => String(args[0]));
4069
+ expect(calledUrls.some((url: string) => url.includes('/_apis/wit/workItems/777?asOf='))).toBe(true);
4070
+ expect(calledUrls.some((url: string) => url.includes('/_apis/wit/workItems/777/revisions/21'))).toBe(true);
4071
+ expect(res).toEqual(
4072
+ expect.objectContaining({
4073
+ testCaseRevision: 21,
4074
+ stepsResultXml: '<steps></steps>',
4075
+ })
4076
+ );
4077
+ });
4078
+
4015
4079
  it('should fallback to suite revision when asOf fetch fails', async () => {
4016
4080
  (TFSServices.getItemContent as jest.Mock).mockReset();
4017
4081
  const point = {
@@ -5064,6 +5128,17 @@ describe('ResultDataProvider', () => {
5064
5128
  // Assert
5065
5129
  expect(result['1']).toBeDefined();
5066
5130
  });
5131
+
5132
+ it('should map runless test reporter item with iteration by testCaseId key', () => {
5133
+ const iterations = [
5134
+ { testCaseId: 217897, lastRunId: undefined, lastResultId: undefined, iteration: { actionResults: [] } },
5135
+ ];
5136
+
5137
+ const result = (resultDataProvider as any).createIterationsMap(iterations, true, true);
5138
+
5139
+ expect(result['217897']).toBeDefined();
5140
+ expect(result['undefined-undefined-217897']).toBeUndefined();
5141
+ });
5067
5142
  });
5068
5143
 
5069
5144
  describe('alignStepsWithIterationsBase', () => {