From 302b6f28d686db0bcc2119f16a73310b1c148fc7 Mon Sep 17 00:00:00 2001 From: Malik Glossop Date: Fri, 31 Jul 2026 10:55:51 +0200 Subject: [PATCH] Merge pull request #35989 from overleaf/worktree-mg-ai-limit-countdown-sub-minute Round AI usage limit countdown up so it never renders empty GitOrigin-RevId: f277de79c4962f534603497201a8f4a43fb01125 --- services/web/frontend/js/shared/utils/time.ts | 6 +- .../test/frontend/shared/utils/time.test.ts | 64 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 services/web/test/frontend/shared/utils/time.test.ts diff --git a/services/web/frontend/js/shared/utils/time.ts b/services/web/frontend/js/shared/utils/time.ts index 4ce1350659..77a9855287 100644 --- a/services/web/frontend/js/shared/utils/time.ts +++ b/services/web/frontend/js/shared/utils/time.ts @@ -4,8 +4,10 @@ export function formatSecondsToHoursAndMinutes( t: TFunction, seconds: number ): string { - const hrs = Math.floor(seconds / 3600) - const mins = Math.floor((seconds % 3600) / 60) + // round up: the caller tells the user how long to wait, so never understate + const totalMinutes = Math.ceil(seconds / 60) + const hrs = Math.floor(totalMinutes / 60) + const mins = totalMinutes % 60 const parts = [] diff --git a/services/web/test/frontend/shared/utils/time.test.ts b/services/web/test/frontend/shared/utils/time.test.ts new file mode 100644 index 0000000000..175ec54b9e --- /dev/null +++ b/services/web/test/frontend/shared/utils/time.test.ts @@ -0,0 +1,64 @@ +import { expect } from 'chai' +import { TFunction } from 'i18next' +import { formatSecondsToHoursAndMinutes } from '@/shared/utils/time' + +// stands in for i18next, echoing the key and its count +const t = ((key: string, options?: { count?: number }) => + options?.count === undefined + ? key + : `${options.count} ${key}`) as unknown as TFunction + +const ONE_HOUR_IN_SECONDS = 3600 +const ONE_MINUTE_IN_SECONDS = 60 + +describe('formatSecondsToHoursAndMinutes', function () { + it('joins hours and minutes - 3h 25m', function () { + expect( + formatSecondsToHoursAndMinutes( + t, + 3 * ONE_HOUR_IN_SECONDS + 25 * ONE_MINUTE_IN_SECONDS + ) + ).to.equal('3 time_hour time_and 25 time_minute') + }) + + it('omits minutes for a whole number of hours - 2h', function () { + expect(formatSecondsToHoursAndMinutes(t, 2 * ONE_HOUR_IN_SECONDS)).to.equal( + '2 time_hour' + ) + }) + + it('omits hours for under an hour - 25m', function () { + expect( + formatSecondsToHoursAndMinutes(t, 25 * ONE_MINUTE_IN_SECONDS) + ).to.equal('25 time_minute') + }) + + it('rounds a part-minute up - 25m 30s', function () { + expect( + formatSecondsToHoursAndMinutes(t, 25 * ONE_MINUTE_IN_SECONDS + 30) + ).to.equal('26 time_minute') + }) + + it('rounds up to a minute for under a minute - 30s', function () { + expect(formatSecondsToHoursAndMinutes(t, 30)).to.equal('1 time_minute') + }) + + it('rounds up to a minute for a fraction of a second - 0.5s', function () { + expect(formatSecondsToHoursAndMinutes(t, 0.5)).to.equal('1 time_minute') + }) + + it('carries a rounded-up minute into the hours - 20h 59m 30s', function () { + expect( + formatSecondsToHoursAndMinutes( + t, + 20 * ONE_HOUR_IN_SECONDS + 59 * ONE_MINUTE_IN_SECONDS + 30 + ) + ).to.equal('21 time_hour') + }) + + it('adds a minute just past a whole hour - 1h 1s', function () { + expect(formatSecondsToHoursAndMinutes(t, ONE_HOUR_IN_SECONDS + 1)).to.equal( + '1 time_hour time_and 1 time_minute' + ) + }) +})