@elisra-devops/docgen-data-provider 1.93.0 → 1.95.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.93.0",
3
+ "version": "1.95.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(
@@ -3049,7 +3050,7 @@ export default class ResultDataProvider {
3049
3050
  // Fetch detailed information for each test point and map to required format
3050
3051
  const detailedPoints = await Promise.all(
3051
3052
  latestPoints.map(async (point: any) => {
3052
- const url = `${point.url}?witFields=Microsoft.VSTS.TCM.Steps&includePointDetails=true`;
3053
+ const url = `${point.url}?witFields=Microsoft.VSTS.TCM.Steps,System.Rev&includePointDetails=true`;
3053
3054
  const detailedPoint = await TFSServices.getItemContent(url, this.token);
3054
3055
  return this.mapTestPointForCrossPlans(detailedPoint, projectName);
3055
3056
  // return this.mapTestPointForCrossPlans(detailedPoint, projectName);
@@ -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
@@ -3142,7 +3211,7 @@ export default class ResultDataProvider {
3142
3211
  testPlanId: string,
3143
3212
  suiteId: string
3144
3213
  ): Promise<any[]> {
3145
- const url = `${this.orgUrl}${projectName}/_apis/testplan/Plans/${testPlanId}/Suites/${suiteId}/TestCase?witFields=Microsoft.VSTS.TCM.Steps`;
3214
+ const url = `${this.orgUrl}${projectName}/_apis/testplan/Plans/${testPlanId}/Suites/${suiteId}/TestCase?witFields=Microsoft.VSTS.TCM.Steps,System.Rev`;
3146
3215
 
3147
3216
  const { value: testCases } = await TFSServices.getItemContent(url, this.token);
3148
3217
 
@@ -3186,8 +3255,31 @@ 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
+ */
3261
+ private getFieldValueByName(fields: Record<string, any>, fieldName: string): any {
3262
+ if (!fields || typeof fields !== 'object') return undefined;
3263
+ if (Object.prototype.hasOwnProperty.call(fields, fieldName)) {
3264
+ return fields[fieldName];
3265
+ }
3266
+
3267
+ const lookupName = String(fieldName || '').toLowerCase().trim();
3268
+ if (!lookupName) return undefined;
3269
+ const matchedKey = Object.keys(fields).find(
3270
+ (key) => String(key || '').toLowerCase().trim() === lookupName
3271
+ );
3272
+ return matchedKey ? fields[matchedKey] : undefined;
3273
+ }
3274
+
3189
3275
  private resolveSuiteTestCaseRevision(testCaseItem: any): number {
3276
+ const fieldsFromList = this.extractWorkItemFieldsMap(testCaseItem?.workItem?.workItemFields);
3277
+ const fieldsFromMap = testCaseItem?.workItem?.fields || {};
3278
+ const systemRevFromList = this.getFieldValueByName(fieldsFromList, 'System.Rev');
3279
+ const systemRevFromMap = this.getFieldValueByName(fieldsFromMap, 'System.Rev');
3190
3280
  const revisionCandidates = [
3281
+ systemRevFromList,
3282
+ systemRevFromMap,
3191
3283
  testCaseItem?.workItem?.rev,
3192
3284
  testCaseItem?.workItem?.revision,
3193
3285
  testCaseItem?.workItem?.version,
@@ -3257,12 +3349,25 @@ export default class ResultDataProvider {
3257
3349
 
3258
3350
  const expandParam = expandAll ? '?$expand=all' : '';
3259
3351
  const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}/revisions/${rev}${expandParam}`;
3260
- try {
3261
- return await TFSServices.getItemContent(url, this.token);
3262
- } catch (error: any) {
3263
- logger.warn(`Failed to fetch work item ${id} by revision ${rev}: ${error?.message || error}`);
3264
- 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
+ const query: string[] = [`asOf=${encodeURIComponent(parsedAsOf.toISOString())}`];
3366
+ if (expandAll) {
3367
+ query.push(`$expand=all`);
3265
3368
  }
3369
+ const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}?${query.join('&')}`;
3370
+ return this.fetchWorkItemByUrl(url, `work item ${id} by asOf ${parsedAsOf.toISOString()}`);
3266
3371
  }
3267
3372
 
3268
3373
  private async fetchWorkItemLatest(
@@ -3275,14 +3380,56 @@ export default class ResultDataProvider {
3275
3380
 
3276
3381
  const expandParam = expandAll ? '?$expand=all' : '';
3277
3382
  const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}${expandParam}`;
3383
+ return this.fetchWorkItemByUrl(url, `latest work item ${id}`);
3384
+ }
3385
+
3386
+ /**
3387
+ * Executes a work-item GET by URL and applies consistent warning/error handling.
3388
+ */
3389
+ private async fetchWorkItemByUrl(url: string, failureContext: string): Promise<any | null> {
3278
3390
  try {
3279
3391
  return await TFSServices.getItemContent(url, this.token);
3280
3392
  } catch (error: any) {
3281
- logger.warn(`Failed to fetch latest work item ${id}: ${error?.message || error}`);
3393
+ logger.warn(`Failed to fetch ${failureContext}: ${error?.message || error}`);
3282
3394
  return null;
3283
3395
  }
3284
3396
  }
3285
3397
 
3398
+ /**
3399
+ * Resolves runless test case data using ordered fallbacks:
3400
+ * 1) point-based `asOf` snapshot, 2) explicit revision, 3) suite payload snapshot, 4) latest WI.
3401
+ */
3402
+ private async resolveRunlessTestCaseData(
3403
+ projectName: string,
3404
+ testCaseId: number,
3405
+ suiteTestCaseRevision: number,
3406
+ pointAsOfTimestamp: string,
3407
+ fallbackSnapshot: any,
3408
+ expandAll: boolean
3409
+ ): Promise<any | null> {
3410
+ if (pointAsOfTimestamp) {
3411
+ const asOfSnapshot = await this.fetchWorkItemByAsOf(
3412
+ projectName,
3413
+ testCaseId,
3414
+ pointAsOfTimestamp,
3415
+ expandAll
3416
+ );
3417
+ if (asOfSnapshot) return asOfSnapshot;
3418
+ }
3419
+
3420
+ const revisionSnapshot = await this.fetchWorkItemByRevision(
3421
+ projectName,
3422
+ testCaseId,
3423
+ suiteTestCaseRevision,
3424
+ expandAll
3425
+ );
3426
+ if (revisionSnapshot) return revisionSnapshot;
3427
+
3428
+ if (fallbackSnapshot) return fallbackSnapshot;
3429
+
3430
+ return this.fetchWorkItemLatest(projectName, testCaseId, expandAll);
3431
+ }
3432
+
3286
3433
  /**
3287
3434
  * Fetches result data based on the Work Item Test Reporter.
3288
3435
  *
@@ -3305,7 +3452,8 @@ export default class ResultDataProvider {
3305
3452
  selectedFields?: string[],
3306
3453
  isQueryMode?: boolean,
3307
3454
  point?: any,
3308
- includeAllHistory: boolean = false
3455
+ includeAllHistory: boolean = false,
3456
+ useRunlessAsOf: boolean = false
3309
3457
  ): Promise<any> {
3310
3458
  try {
3311
3459
  let filteredFields: any = {};
@@ -3322,23 +3470,22 @@ export default class ResultDataProvider {
3322
3470
  point?.testCaseId || suiteTestCaseItem?.workItem?.id || suiteTestCaseItem?.testCaseId || 0
3323
3471
  );
3324
3472
  const suiteTestCaseRevision = this.resolveSuiteTestCaseRevision(suiteTestCaseItem);
3473
+ const pointAsOfTimestamp = useRunlessAsOf
3474
+ ? String(point?.pointAsOfTimestamp || '').trim()
3475
+ : '';
3325
3476
  const fallbackSnapshot = this.buildWorkItemSnapshotFromSuiteTestCase(
3326
3477
  suiteTestCaseItem,
3327
3478
  testCaseId,
3328
3479
  String(point?.testCaseName || '')
3329
3480
  );
3330
- let testCaseData = await this.fetchWorkItemByRevision(
3481
+ const testCaseData = await this.resolveRunlessTestCaseData(
3331
3482
  projectName,
3332
3483
  testCaseId,
3333
3484
  suiteTestCaseRevision,
3485
+ pointAsOfTimestamp,
3486
+ fallbackSnapshot,
3334
3487
  isTestReporter
3335
3488
  );
3336
- if (!testCaseData) {
3337
- testCaseData = fallbackSnapshot;
3338
- }
3339
- if (!testCaseData) {
3340
- testCaseData = await this.fetchWorkItemLatest(projectName, testCaseId, isTestReporter);
3341
- }
3342
3489
  if (!testCaseData) {
3343
3490
  logger.warn(`Could not resolve test case ${point.testCaseId} for runless point fallback.`);
3344
3491
  return null;
@@ -4972,7 +5119,8 @@ export default class ResultDataProvider {
4972
5119
  selectedFields?: string[],
4973
5120
  isQueryMode?: boolean,
4974
5121
  point?: any,
4975
- includeAllHistory: boolean = false
5122
+ includeAllHistory: boolean = false,
5123
+ useRunlessAsOf: boolean = false
4976
5124
  ): Promise<any> {
4977
5125
  return this.fetchResultDataBasedOnWiBase(
4978
5126
  projectName,
@@ -4982,7 +5130,8 @@ export default class ResultDataProvider {
4982
5130
  selectedFields,
4983
5131
  isQueryMode,
4984
5132
  point,
4985
- includeAllHistory
5133
+ includeAllHistory,
5134
+ useRunlessAsOf
4986
5135
  );
4987
5136
  }
4988
5137
 
@@ -5003,22 +5152,24 @@ export default class ResultDataProvider {
5003
5152
  projectName: string,
5004
5153
  selectedFields?: string[],
5005
5154
  isQueryMode?: boolean,
5006
- includeAllHistory: boolean = false
5155
+ includeAllHistory: boolean = false,
5156
+ useRunlessAsOf: boolean = false
5007
5157
  ): Promise<any[]> {
5008
5158
  return this.fetchAllResultDataBase(
5009
5159
  testData,
5010
5160
  projectName,
5011
5161
  true,
5012
- (projectName, testSuiteId, point, selectedFields, isQueryMode, includeAllHistory) =>
5162
+ (projectName, testSuiteId, point, selectedFields, isQueryMode, includeAllHistory, useRunlessAsOf) =>
5013
5163
  this.fetchResultDataForTestReporter(
5014
5164
  projectName,
5015
5165
  testSuiteId,
5016
5166
  point,
5017
5167
  selectedFields,
5018
5168
  isQueryMode,
5019
- includeAllHistory
5169
+ includeAllHistory,
5170
+ useRunlessAsOf
5020
5171
  ),
5021
- [selectedFields, isQueryMode, includeAllHistory]
5172
+ [selectedFields, isQueryMode, includeAllHistory, useRunlessAsOf]
5022
5173
  );
5023
5174
  }
5024
5175
 
@@ -5176,13 +5327,14 @@ export default class ResultDataProvider {
5176
5327
  point: any,
5177
5328
  selectedFields?: string[],
5178
5329
  isQueryMode?: boolean,
5179
- includeAllHistory: boolean = false
5330
+ includeAllHistory: boolean = false,
5331
+ useRunlessAsOf: boolean = false
5180
5332
  ) {
5181
5333
  return this.fetchResultDataBase(
5182
5334
  projectName,
5183
5335
  testSuiteId,
5184
5336
  point,
5185
- (project, runId, resultId, fields, isQueryMode, point, includeAllHistory) =>
5337
+ (project, runId, resultId, fields, isQueryMode, point, includeAllHistory, useRunlessAsOf) =>
5186
5338
  this.fetchResultDataBasedOnWiTestReporter(
5187
5339
  project,
5188
5340
  runId,
@@ -5190,7 +5342,8 @@ export default class ResultDataProvider {
5190
5342
  fields,
5191
5343
  isQueryMode,
5192
5344
  point,
5193
- includeAllHistory
5345
+ includeAllHistory,
5346
+ useRunlessAsOf
5194
5347
  ),
5195
5348
  (resultData, testSuiteId, point, selectedFields) => {
5196
5349
  const { lastRunId, lastResultId, configurationName, lastResultDetails } = point;
@@ -5310,7 +5463,7 @@ export default class ResultDataProvider {
5310
5463
  return null;
5311
5464
  }
5312
5465
  },
5313
- [selectedFields, isQueryMode, point, includeAllHistory]
5466
+ [selectedFields, isQueryMode, point, includeAllHistory, useRunlessAsOf]
5314
5467
  );
5315
5468
  }
5316
5469