@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/bin/modules/ResultDataProvider.d.ts +23 -0
- package/bin/modules/ResultDataProvider.js +128 -38
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +260 -2
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/ResultDataProvider.ts +206 -53
- package/src/tests/modules/ResultDataProvider.test.ts +366 -2
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(
|
|
@@ -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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|