@elisra-devops/docgen-data-provider 1.94.0 → 1.96.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.94.0",
3
+ "version": "1.96.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -410,7 +410,8 @@ export default class ResultDataProvider {
410
410
  projectName,
411
411
  selectedFields,
412
412
  false,
413
- includeAllHistory
413
+ includeAllHistory,
414
+ true
414
415
  );
415
416
 
416
417
  const rows = this.alignStepsWithIterationsFlatReport(
@@ -3086,40 +3087,108 @@ export default class ResultDataProvider {
3086
3087
  * Maps raw test point data to a simplified object.
3087
3088
  */
3088
3089
  private mapTestPoint(testPoint: any, projectName: string): any {
3089
- return {
3090
- testPointId: testPoint.id,
3091
- testCaseId: testPoint.testCaseReference.id,
3092
- testCaseName: testPoint.testCaseReference.name,
3093
- testCaseUrl: `${this.orgUrl}${projectName}/_workitems/edit/${testPoint.testCaseReference.id}`,
3094
- configurationName: testPoint.configuration?.name,
3095
- outcome: testPoint.results?.outcome || 'Not Run',
3096
- testSuite: testPoint.testSuite,
3097
- lastRunId: testPoint.results?.lastTestRunId,
3098
- lastResultId: testPoint.results?.lastResultId,
3099
- lastResultDetails: testPoint.results?.lastResultDetails,
3100
- };
3090
+ const pointAsOfTimestamp = this.resolvePointAsOfTimestamp(testPoint);
3091
+ return this.buildMappedTestPoint(
3092
+ {
3093
+ testPointId: testPoint.id,
3094
+ testCaseId: testPoint.testCaseReference.id,
3095
+ testCaseName: testPoint.testCaseReference.name,
3096
+ configurationName: testPoint.configuration?.name,
3097
+ outcome: testPoint.results?.outcome,
3098
+ testSuite: testPoint.testSuite,
3099
+ lastRunId: testPoint.results?.lastTestRunId,
3100
+ lastResultId: testPoint.results?.lastResultId,
3101
+ lastResultDetails: testPoint.results?.lastResultDetails,
3102
+ },
3103
+ projectName,
3104
+ pointAsOfTimestamp
3105
+ );
3101
3106
  }
3102
3107
 
3103
3108
  /**
3104
3109
  * Maps raw test point data to a simplified object.
3105
3110
  */
3106
3111
  private mapTestPointForCrossPlans(testPoint: any, projectName: string): any {
3107
- return {
3108
- testPointId: testPoint.id,
3109
- testCaseId: testPoint.testCase.id,
3110
- testCaseName: testPoint.testCase.name,
3111
- testCaseUrl: `${this.orgUrl}${projectName}/_workitems/edit/${testPoint.testCase.id}`,
3112
- testSuite: testPoint.testSuite,
3113
- configurationName: testPoint.configuration?.name,
3114
- outcome: testPoint.outcome || 'Not Run',
3115
- lastRunId: testPoint.lastTestRun?.id,
3116
- lastResultId: testPoint.lastResult?.id,
3117
- lastResultDetails: testPoint.lastResultDetails || {
3118
- duration: 0,
3119
- dateCompleted: '0000-00-00T00:00:00.000Z',
3120
- runBy: { displayName: 'No tester', id: '00000000-0000-0000-0000-000000000000' },
3112
+ const pointAsOfTimestamp = this.resolvePointAsOfTimestamp(testPoint);
3113
+ return this.buildMappedTestPoint(
3114
+ {
3115
+ testPointId: testPoint.id,
3116
+ testCaseId: testPoint.testCase.id,
3117
+ testCaseName: testPoint.testCase.name,
3118
+ configurationName: testPoint.configuration?.name,
3119
+ outcome: testPoint.outcome,
3120
+ testSuite: testPoint.testSuite,
3121
+ lastRunId: testPoint.lastTestRun?.id,
3122
+ lastResultId: testPoint.lastResult?.id,
3123
+ lastResultDetails: testPoint.lastResultDetails || {
3124
+ duration: 0,
3125
+ dateCompleted: '0000-00-00T00:00:00.000Z',
3126
+ runBy: { displayName: 'No tester', id: '00000000-0000-0000-0000-000000000000' },
3127
+ },
3121
3128
  },
3129
+ projectName,
3130
+ pointAsOfTimestamp
3131
+ );
3132
+ }
3133
+
3134
+ /**
3135
+ * Resolves a point timestamp that can be used as WIT `asOf` anchor.
3136
+ * Returns an ISO string when a valid date exists, otherwise an empty string.
3137
+ */
3138
+ private resolvePointAsOfTimestamp(testPoint: any): string {
3139
+ const candidates = [
3140
+ testPoint?.lastUpdatedDate,
3141
+ testPoint?.lastUpdatedOn,
3142
+ testPoint?.updatedDate,
3143
+ testPoint?.updatedOn,
3144
+ ];
3145
+
3146
+ for (const candidate of candidates) {
3147
+ const raw = String(candidate || '').trim();
3148
+ if (!raw) continue;
3149
+ const parsed = new Date(raw);
3150
+ if (!Number.isNaN(parsed.getTime())) {
3151
+ return parsed.toISOString();
3152
+ }
3153
+ }
3154
+
3155
+ return '';
3156
+ }
3157
+
3158
+ /**
3159
+ * Builds a normalized test point shape used by downstream result fetch flows.
3160
+ */
3161
+ private buildMappedTestPoint(
3162
+ pointData: {
3163
+ testPointId: any;
3164
+ testCaseId: any;
3165
+ testCaseName: any;
3166
+ configurationName?: any;
3167
+ outcome?: any;
3168
+ testSuite?: any;
3169
+ lastRunId?: any;
3170
+ lastResultId?: any;
3171
+ lastResultDetails?: any;
3172
+ },
3173
+ projectName: string,
3174
+ pointAsOfTimestamp: string
3175
+ ): any {
3176
+ const mappedPoint: any = {
3177
+ testPointId: pointData.testPointId,
3178
+ testCaseId: pointData.testCaseId,
3179
+ testCaseName: pointData.testCaseName,
3180
+ testCaseUrl: `${this.orgUrl}${projectName}/_workitems/edit/${pointData.testCaseId}`,
3181
+ configurationName: pointData.configurationName,
3182
+ outcome: pointData.outcome || 'Not Run',
3183
+ testSuite: pointData.testSuite,
3184
+ lastRunId: pointData.lastRunId,
3185
+ lastResultId: pointData.lastResultId,
3186
+ lastResultDetails: pointData.lastResultDetails,
3122
3187
  };
3188
+ if (pointAsOfTimestamp) {
3189
+ mappedPoint.pointAsOfTimestamp = pointAsOfTimestamp;
3190
+ }
3191
+ return mappedPoint;
3123
3192
  }
3124
3193
 
3125
3194
  // Helper method to get all test points for a test case
@@ -3186,6 +3255,9 @@ export default class ResultDataProvider {
3186
3255
  return fields;
3187
3256
  }
3188
3257
 
3258
+ /**
3259
+ * Reads a field value by reference name with case-insensitive key matching.
3260
+ */
3189
3261
  private getFieldValueByName(fields: Record<string, any>, fieldName: string): any {
3190
3262
  if (!fields || typeof fields !== 'object') return undefined;
3191
3263
  if (Object.prototype.hasOwnProperty.call(fields, fieldName)) {
@@ -3277,12 +3349,30 @@ export default class ResultDataProvider {
3277
3349
 
3278
3350
  const expandParam = expandAll ? '?$expand=all' : '';
3279
3351
  const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}/revisions/${rev}${expandParam}`;
3280
- try {
3281
- return await TFSServices.getItemContent(url, this.token);
3282
- } catch (error: any) {
3283
- logger.warn(`Failed to fetch work item ${id} by revision ${rev}: ${error?.message || error}`);
3284
- return null;
3352
+ return this.fetchWorkItemByUrl(url, `work item ${id} by revision ${rev}`);
3353
+ }
3354
+
3355
+ private async fetchWorkItemByAsOf(
3356
+ projectName: string,
3357
+ workItemId: number,
3358
+ asOfTimestamp: string,
3359
+ expandAll: boolean = false
3360
+ ): Promise<any | null> {
3361
+ const id = Number(workItemId || 0);
3362
+ const parsedAsOf = new Date(String(asOfTimestamp || '').trim());
3363
+ if (!Number.isFinite(id) || id <= 0 || Number.isNaN(parsedAsOf.getTime())) return null;
3364
+
3365
+ logger.debug(
3366
+ `[RunlessResolver] Fetching work item ${id} by asOf (raw="${String(
3367
+ asOfTimestamp || ''
3368
+ )}", normalized="${parsedAsOf.toISOString()}", expandAll=${String(expandAll)})`
3369
+ );
3370
+ const query: string[] = [`asOf=${encodeURIComponent(parsedAsOf.toISOString())}`];
3371
+ if (expandAll) {
3372
+ query.push(`$expand=all`);
3285
3373
  }
3374
+ const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}?${query.join('&')}`;
3375
+ return this.fetchWorkItemByUrl(url, `work item ${id} by asOf ${parsedAsOf.toISOString()}`);
3286
3376
  }
3287
3377
 
3288
3378
  private async fetchWorkItemLatest(
@@ -3295,14 +3385,102 @@ export default class ResultDataProvider {
3295
3385
 
3296
3386
  const expandParam = expandAll ? '?$expand=all' : '';
3297
3387
  const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}${expandParam}`;
3388
+ return this.fetchWorkItemByUrl(url, `latest work item ${id}`);
3389
+ }
3390
+
3391
+ /**
3392
+ * Executes a work-item GET by URL and applies consistent warning/error handling.
3393
+ */
3394
+ private async fetchWorkItemByUrl(url: string, failureContext: string): Promise<any | null> {
3298
3395
  try {
3299
3396
  return await TFSServices.getItemContent(url, this.token);
3300
3397
  } catch (error: any) {
3301
- logger.warn(`Failed to fetch latest work item ${id}: ${error?.message || error}`);
3398
+ logger.warn(`Failed to fetch ${failureContext}: ${error?.message || error}`);
3302
3399
  return null;
3303
3400
  }
3304
3401
  }
3305
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
+
3426
+ /**
3427
+ * Resolves runless test case data using ordered fallbacks:
3428
+ * 1) point-based `asOf` snapshot, 2) explicit revision, 3) suite payload snapshot, 4) latest WI.
3429
+ */
3430
+ private async resolveRunlessTestCaseData(
3431
+ projectName: string,
3432
+ testCaseId: number,
3433
+ suiteTestCaseRevision: number,
3434
+ pointAsOfTimestamp: string,
3435
+ fallbackSnapshot: any,
3436
+ expandAll: boolean
3437
+ ): Promise<any | null> {
3438
+ let bestSnapshotWithoutSteps: any | null = null;
3439
+
3440
+ if (pointAsOfTimestamp) {
3441
+ const asOfSnapshot = await this.fetchWorkItemByAsOf(
3442
+ projectName,
3443
+ testCaseId,
3444
+ pointAsOfTimestamp,
3445
+ expandAll
3446
+ );
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`);
3454
+ }
3455
+
3456
+ const revisionSnapshot = await this.fetchWorkItemByRevision(
3457
+ projectName,
3458
+ testCaseId,
3459
+ suiteTestCaseRevision,
3460
+ expandAll
3461
+ );
3462
+ this.logRunlessSnapshotDecision(testCaseId, `revision:${String(suiteTestCaseRevision)}`, revisionSnapshot);
3463
+ if (revisionSnapshot) {
3464
+ if (this.hasStepsInWorkItemSnapshot(revisionSnapshot)) return revisionSnapshot;
3465
+ bestSnapshotWithoutSteps = bestSnapshotWithoutSteps || revisionSnapshot;
3466
+ }
3467
+
3468
+ this.logRunlessSnapshotDecision(testCaseId, 'suiteSnapshot', fallbackSnapshot);
3469
+ if (fallbackSnapshot) {
3470
+ if (this.hasStepsInWorkItemSnapshot(fallbackSnapshot)) return fallbackSnapshot;
3471
+ bestSnapshotWithoutSteps = bestSnapshotWithoutSteps || fallbackSnapshot;
3472
+ }
3473
+
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;
3482
+ }
3483
+
3306
3484
  /**
3307
3485
  * Fetches result data based on the Work Item Test Reporter.
3308
3486
  *
@@ -3325,7 +3503,8 @@ export default class ResultDataProvider {
3325
3503
  selectedFields?: string[],
3326
3504
  isQueryMode?: boolean,
3327
3505
  point?: any,
3328
- includeAllHistory: boolean = false
3506
+ includeAllHistory: boolean = false,
3507
+ useRunlessAsOf: boolean = false
3329
3508
  ): Promise<any> {
3330
3509
  try {
3331
3510
  let filteredFields: any = {};
@@ -3342,23 +3521,29 @@ export default class ResultDataProvider {
3342
3521
  point?.testCaseId || suiteTestCaseItem?.workItem?.id || suiteTestCaseItem?.testCaseId || 0
3343
3522
  );
3344
3523
  const suiteTestCaseRevision = this.resolveSuiteTestCaseRevision(suiteTestCaseItem);
3524
+ const pointAsOfTimestamp = useRunlessAsOf
3525
+ ? String(point?.pointAsOfTimestamp || '').trim()
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
+ );
3345
3534
  const fallbackSnapshot = this.buildWorkItemSnapshotFromSuiteTestCase(
3346
3535
  suiteTestCaseItem,
3347
3536
  testCaseId,
3348
3537
  String(point?.testCaseName || '')
3349
3538
  );
3350
- let testCaseData = await this.fetchWorkItemByRevision(
3539
+ const testCaseData = await this.resolveRunlessTestCaseData(
3351
3540
  projectName,
3352
3541
  testCaseId,
3353
3542
  suiteTestCaseRevision,
3543
+ pointAsOfTimestamp,
3544
+ fallbackSnapshot,
3354
3545
  isTestReporter
3355
3546
  );
3356
- if (!testCaseData) {
3357
- testCaseData = fallbackSnapshot;
3358
- }
3359
- if (!testCaseData) {
3360
- testCaseData = await this.fetchWorkItemLatest(projectName, testCaseId, isTestReporter);
3361
- }
3362
3547
  if (!testCaseData) {
3363
3548
  logger.warn(`Could not resolve test case ${point.testCaseId} for runless point fallback.`);
3364
3549
  return null;
@@ -4992,7 +5177,8 @@ export default class ResultDataProvider {
4992
5177
  selectedFields?: string[],
4993
5178
  isQueryMode?: boolean,
4994
5179
  point?: any,
4995
- includeAllHistory: boolean = false
5180
+ includeAllHistory: boolean = false,
5181
+ useRunlessAsOf: boolean = false
4996
5182
  ): Promise<any> {
4997
5183
  return this.fetchResultDataBasedOnWiBase(
4998
5184
  projectName,
@@ -5002,7 +5188,8 @@ export default class ResultDataProvider {
5002
5188
  selectedFields,
5003
5189
  isQueryMode,
5004
5190
  point,
5005
- includeAllHistory
5191
+ includeAllHistory,
5192
+ useRunlessAsOf
5006
5193
  );
5007
5194
  }
5008
5195
 
@@ -5023,22 +5210,24 @@ export default class ResultDataProvider {
5023
5210
  projectName: string,
5024
5211
  selectedFields?: string[],
5025
5212
  isQueryMode?: boolean,
5026
- includeAllHistory: boolean = false
5213
+ includeAllHistory: boolean = false,
5214
+ useRunlessAsOf: boolean = false
5027
5215
  ): Promise<any[]> {
5028
5216
  return this.fetchAllResultDataBase(
5029
5217
  testData,
5030
5218
  projectName,
5031
5219
  true,
5032
- (projectName, testSuiteId, point, selectedFields, isQueryMode, includeAllHistory) =>
5220
+ (projectName, testSuiteId, point, selectedFields, isQueryMode, includeAllHistory, useRunlessAsOf) =>
5033
5221
  this.fetchResultDataForTestReporter(
5034
5222
  projectName,
5035
5223
  testSuiteId,
5036
5224
  point,
5037
5225
  selectedFields,
5038
5226
  isQueryMode,
5039
- includeAllHistory
5227
+ includeAllHistory,
5228
+ useRunlessAsOf
5040
5229
  ),
5041
- [selectedFields, isQueryMode, includeAllHistory]
5230
+ [selectedFields, isQueryMode, includeAllHistory, useRunlessAsOf]
5042
5231
  );
5043
5232
  }
5044
5233
 
@@ -5196,13 +5385,14 @@ export default class ResultDataProvider {
5196
5385
  point: any,
5197
5386
  selectedFields?: string[],
5198
5387
  isQueryMode?: boolean,
5199
- includeAllHistory: boolean = false
5388
+ includeAllHistory: boolean = false,
5389
+ useRunlessAsOf: boolean = false
5200
5390
  ) {
5201
5391
  return this.fetchResultDataBase(
5202
5392
  projectName,
5203
5393
  testSuiteId,
5204
5394
  point,
5205
- (project, runId, resultId, fields, isQueryMode, point, includeAllHistory) =>
5395
+ (project, runId, resultId, fields, isQueryMode, point, includeAllHistory, useRunlessAsOf) =>
5206
5396
  this.fetchResultDataBasedOnWiTestReporter(
5207
5397
  project,
5208
5398
  runId,
@@ -5210,7 +5400,8 @@ export default class ResultDataProvider {
5210
5400
  fields,
5211
5401
  isQueryMode,
5212
5402
  point,
5213
- includeAllHistory
5403
+ includeAllHistory,
5404
+ useRunlessAsOf
5214
5405
  ),
5215
5406
  (resultData, testSuiteId, point, selectedFields) => {
5216
5407
  const { lastRunId, lastResultId, configurationName, lastResultDetails } = point;
@@ -5330,7 +5521,7 @@ export default class ResultDataProvider {
5330
5521
  return null;
5331
5522
  }
5332
5523
  },
5333
- [selectedFields, isQueryMode, point, includeAllHistory]
5524
+ [selectedFields, isQueryMode, point, includeAllHistory, useRunlessAsOf]
5334
5525
  );
5335
5526
  }
5336
5527
 
@@ -333,6 +333,21 @@ describe('ResultDataProvider', () => {
333
333
  testCaseUrl: 'https://dev.azure.com/organization/test-project/_workitems/edit/1',
334
334
  });
335
335
  });
336
+
337
+ it('should include pointAsOfTimestamp when lastUpdatedDate is available', () => {
338
+ const testPoint = {
339
+ testCaseReference: { id: 1, name: 'Test Case 1' },
340
+ lastUpdatedDate: '2025-01-01T12:34:56Z',
341
+ };
342
+
343
+ const result = (resultDataProvider as any).mapTestPoint(testPoint, mockProjectName);
344
+
345
+ expect(result).toEqual(
346
+ expect.objectContaining({
347
+ pointAsOfTimestamp: '2025-01-01T12:34:56.000Z',
348
+ })
349
+ );
350
+ });
336
351
  });
337
352
 
338
353
  describe('calculateGroupResultSummary', () => {
@@ -3890,7 +3905,9 @@ describe('ResultDataProvider', () => {
3890
3905
  true,
3891
3906
  [],
3892
3907
  false,
3893
- point
3908
+ point,
3909
+ false,
3910
+ true
3894
3911
  );
3895
3912
 
3896
3913
  expect(TFSServices.getItemContent).toHaveBeenCalledWith(
@@ -3902,7 +3919,214 @@ describe('ResultDataProvider', () => {
3902
3919
  expect(res).toEqual(expect.objectContaining({ testCaseRevision: 9 }));
3903
3920
  });
3904
3921
 
3922
+ it('should ignore point asOf timestamp when runless asOf mode is disabled', async () => {
3923
+ const point = {
3924
+ testCaseId: '123',
3925
+ testCaseName: 'TC 123',
3926
+ outcome: 'passed',
3927
+ pointAsOfTimestamp: '2025-01-01T12:34:56Z',
3928
+ suiteTestCase: {
3929
+ workItem: {
3930
+ id: 123,
3931
+ rev: 9,
3932
+ workItemFields: [{ key: 'Microsoft.VSTS.TCM.Steps', value: '<steps></steps>' }],
3933
+ },
3934
+ },
3935
+ testSuite: { id: '1', name: 'Suite' },
3936
+ };
3937
+
3938
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
3939
+ id: 123,
3940
+ rev: 9,
3941
+ fields: {
3942
+ 'System.State': 'Active',
3943
+ 'System.CreatedDate': '2024-01-01T00:00:00',
3944
+ 'Microsoft.VSTS.TCM.Priority': 1,
3945
+ 'System.Title': 'Title 123',
3946
+ 'Microsoft.VSTS.TCM.Steps': '<steps></steps>',
3947
+ },
3948
+ relations: null,
3949
+ });
3950
+
3951
+ const res = await (resultDataProvider as any).fetchResultDataBasedOnWiBase(
3952
+ mockProjectName,
3953
+ '0',
3954
+ '0',
3955
+ true,
3956
+ [],
3957
+ false,
3958
+ point
3959
+ );
3960
+
3961
+ const calledUrls = (TFSServices.getItemContent as jest.Mock).mock.calls.map((args: any[]) => String(args[0]));
3962
+ expect(calledUrls.some((url: string) => url.includes('/_apis/wit/workItems/123?asOf='))).toBe(false);
3963
+ expect(calledUrls.some((url: string) => url.includes('/_apis/wit/workItems/123/revisions/9'))).toBe(true);
3964
+ expect(res).toEqual(expect.objectContaining({ testCaseRevision: 9 }));
3965
+ });
3966
+
3967
+ it('should fetch no-run test case by asOf timestamp when pointAsOfTimestamp is available', async () => {
3968
+ (TFSServices.getItemContent as jest.Mock).mockReset();
3969
+ const point = {
3970
+ testCaseId: '123',
3971
+ testCaseName: 'TC 123',
3972
+ outcome: 'passed',
3973
+ pointAsOfTimestamp: '2025-01-01T12:34:56Z',
3974
+ suiteTestCase: {
3975
+ workItem: {
3976
+ id: 123,
3977
+ rev: 9,
3978
+ workItemFields: [{ key: 'Microsoft.VSTS.TCM.Steps', value: '<steps></steps>' }],
3979
+ },
3980
+ },
3981
+ testSuite: { id: '1', name: 'Suite' },
3982
+ };
3983
+
3984
+ (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
3985
+ id: 123,
3986
+ rev: 6,
3987
+ fields: {
3988
+ 'System.State': 'Active',
3989
+ 'System.CreatedDate': '2024-01-01T00:00:00',
3990
+ 'Microsoft.VSTS.TCM.Priority': 1,
3991
+ 'System.Title': 'Title 123',
3992
+ 'Microsoft.VSTS.TCM.Steps': '<steps></steps>',
3993
+ },
3994
+ relations: null,
3995
+ });
3996
+
3997
+ const res = await (resultDataProvider as any).fetchResultDataBasedOnWiBase(
3998
+ mockProjectName,
3999
+ '0',
4000
+ '0',
4001
+ true,
4002
+ [],
4003
+ false,
4004
+ point,
4005
+ false,
4006
+ true
4007
+ );
4008
+
4009
+ const calledUrls = (TFSServices.getItemContent as jest.Mock).mock.calls.map((args: any[]) => String(args[0]));
4010
+ expect(calledUrls.some((url: string) => url.includes('/_apis/wit/workItems/123?asOf='))).toBe(true);
4011
+ expect(calledUrls.some((url: string) => url.includes('/revisions/9'))).toBe(false);
4012
+ expect(res).toEqual(expect.objectContaining({ testCaseRevision: 6 }));
4013
+ });
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
+
4079
+ it('should fallback to suite revision when asOf fetch fails', async () => {
4080
+ (TFSServices.getItemContent as jest.Mock).mockReset();
4081
+ const point = {
4082
+ testCaseId: '456',
4083
+ testCaseName: 'TC 456',
4084
+ outcome: 'Not Run',
4085
+ pointAsOfTimestamp: '2025-02-01T00:00:00Z',
4086
+ suiteTestCase: {
4087
+ workItem: {
4088
+ id: 456,
4089
+ workItemFields: [{ key: 'System.Rev', value: '11' }],
4090
+ },
4091
+ },
4092
+ testSuite: { id: '1', name: 'Suite' },
4093
+ };
4094
+
4095
+ (TFSServices.getItemContent as jest.Mock)
4096
+ .mockRejectedValueOnce(new Error('asOf failed'))
4097
+ .mockResolvedValueOnce({
4098
+ id: 456,
4099
+ rev: 11,
4100
+ fields: {
4101
+ 'System.State': 'Active',
4102
+ 'System.CreatedDate': '2024-01-01T00:00:00',
4103
+ 'Microsoft.VSTS.TCM.Priority': 1,
4104
+ 'System.Title': 'Title 456',
4105
+ 'Microsoft.VSTS.TCM.Steps': '<steps></steps>',
4106
+ },
4107
+ relations: [],
4108
+ });
4109
+
4110
+ const res = await (resultDataProvider as any).fetchResultDataBasedOnWiBase(
4111
+ mockProjectName,
4112
+ '0',
4113
+ '0',
4114
+ true,
4115
+ [],
4116
+ false,
4117
+ point,
4118
+ false,
4119
+ true
4120
+ );
4121
+
4122
+ const calledUrls = (TFSServices.getItemContent as jest.Mock).mock.calls.map((args: any[]) => String(args[0]));
4123
+ expect(calledUrls.some((url: string) => url.includes('/_apis/wit/workItems/456?asOf='))).toBe(true);
4124
+ expect(calledUrls.some((url: string) => url.includes('/_apis/wit/workItems/456/revisions/11'))).toBe(true);
4125
+ expect(res).toEqual(expect.objectContaining({ testCaseRevision: 11 }));
4126
+ });
4127
+
3905
4128
  it('should resolve no-run revision from System.Rev in suite test-case fields', async () => {
4129
+ (TFSServices.getItemContent as jest.Mock).mockReset();
3906
4130
  const point = {
3907
4131
  testCaseId: '321',
3908
4132
  testCaseName: 'TC 321',