From 9348c20cf10351667fd51ef3531835e201a58e83 Mon Sep 17 00:00:00 2001 From: ardey Date: Mon, 17 Aug 2026 11:44:58 +0530 Subject: [PATCH] OCPBUGS-111090: fix(build): correct duration display for builds over 60 hours The duration formatter only read the first three decomposed values from a while-loop, which mis-reported durations longer than 60 hours. Compute hours, minutes, and seconds directly and add unit tests. Fixes https://redhat.atlassian.net/browse/OCPBUGS-111090 Closes #16913 Co-authored-by: Cursor --- .../utils/__tests__/build-utils.spec.ts | 33 +++++++++++++++ .../public/components/utils/build-utils.ts | 40 +++++++++---------- 2 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 frontend/public/components/utils/__tests__/build-utils.spec.ts diff --git a/frontend/public/components/utils/__tests__/build-utils.spec.ts b/frontend/public/components/utils/__tests__/build-utils.spec.ts new file mode 100644 index 00000000000..2fd49fc350b --- /dev/null +++ b/frontend/public/components/utils/__tests__/build-utils.spec.ts @@ -0,0 +1,33 @@ +import { displayDurationInWords } from '../build-utils'; + +jest.mock('i18next', () => ({ + t: (key: string) => key.replace('public~', ''), +})); + +describe('displayDurationInWords', () => { + it('returns - when start is missing', () => { + expect(displayDurationInWords('', '2024-01-01T00:00:00Z')).toEqual('-'); + }); + + it('formats sub-hour durations', () => { + expect( + displayDurationInWords('2024-01-01T00:00:00Z', '2024-01-01T00:01:05Z'), + ).toEqual('1 minute 5 seconds'); + }); + + it('formats multi-hour durations', () => { + expect( + displayDurationInWords('2024-01-01T00:00:00Z', '2024-01-01T01:02:03Z'), + ).toEqual('1 hour 2 minutes 3 seconds'); + }); + + it('formats durations longer than 60 hours', () => { + expect( + displayDurationInWords('2024-01-01T00:00:00Z', '2024-01-05T04:00:00Z'), + ).toEqual('100 hours'); + }); + + it('returns - for invalid timestamps', () => { + expect(displayDurationInWords('invalid', '2024-01-01T00:00:00Z')).toEqual('-'); + }); +}); diff --git a/frontend/public/components/utils/build-utils.ts b/frontend/public/components/utils/build-utils.ts index f675a142596..6d4b5055a3b 100644 --- a/frontend/public/components/utils/build-utils.ts +++ b/frontend/public/components/utils/build-utils.ts @@ -6,30 +6,30 @@ export const displayDurationInWords = (start: string, stop: string): string => { } const startTime = new Date(start).getTime(); const stopTime = stop ? new Date(stop).getTime() : new Date().getTime(); - let duration = Math.round((stopTime - startTime) / 1000); - const time = []; - let durationInWords = ''; - while (duration >= 60) { - time.push(duration % 60); - duration = Math.floor(duration / 60); + const duration = Math.max(0, Math.round((stopTime - startTime) / 1000)); + if (!Number.isFinite(duration)) { + return '-'; } - time.push(duration); - if (time[2]) { - durationInWords += `${time[2]} ${ - time[2] > 1 ? i18next.t('public~hours') : i18next.t('public~hour') - } `; + const seconds = duration % 60; + const minutes = Math.floor(duration / 60) % 60; + const hours = Math.floor(duration / 3600); + const durationInWords = []; + if (hours) { + durationInWords.push( + `${hours} ${hours > 1 ? i18next.t('public~hours') : i18next.t('public~hour')}`, + ); } - if (time[1]) { - durationInWords += `${time[1]} ${ - time[1] > 1 ? i18next.t('public~minutes') : i18next.t('public~minute') - } `; + if (minutes) { + durationInWords.push( + `${minutes} ${minutes > 1 ? i18next.t('public~minutes') : i18next.t('public~minute')}`, + ); } - if (time[0]) { - durationInWords += `${time[0]} ${ - time[0] > 1 ? i18next.t('public~seconds') : i18next.t('public~second') - } `; + if (seconds || !durationInWords.length) { + durationInWords.push( + `${seconds} ${seconds === 1 ? i18next.t('public~second') : i18next.t('public~seconds')}`, + ); } - return durationInWords.trim(); + return durationInWords.join(' '); }; export enum BuildStrategyType {