@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/bin/modules/ResultDataProvider.d.ts +27 -0
- package/bin/modules/ResultDataProvider.js +144 -30
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +162 -1
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/ResultDataProvider.ts +242 -51
- package/src/tests/modules/ResultDataProvider.test.ts +225 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
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
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
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
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
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
|
|
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
|
-
|
|
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',
|