From 14ab2a0317fd3f225aef41b5eb6b86106a5ab2fa Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 30 Jun 2026 16:48:36 -0500 Subject: [PATCH] Enhance VodConnectionCard to use human-readable duration format and update tests accordingly. Refactor formatDuration function to support 'human' precision and add related unit tests for various duration scenarios. --- .../components/cards/VodConnectionCard.jsx | 5 +- .../__tests__/VodConnectionCard.test.jsx | 74 ++++++++++++++++++- .../src/utils/__tests__/dateTimeUtils.test.js | 43 ++++++++++- frontend/src/utils/dateTimeUtils.js | 32 +++++--- 4 files changed, 138 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/cards/VodConnectionCard.jsx b/frontend/src/components/cards/VodConnectionCard.jsx index 8efe4e55..0ecb2031 100644 --- a/frontend/src/components/cards/VodConnectionCard.jsx +++ b/frontend/src/components/cards/VodConnectionCard.jsx @@ -372,7 +372,10 @@ const VodConnectionCard = ({ vodContent, stopVODClient }) => { {metadata.duration_secs && ( - {formatDuration(metadata.duration_secs, { zeroValue: 'Unknown', precision: 'hm' })} + {formatDuration(metadata.duration_secs, { + zeroValue: 'Unknown', + precision: 'human', + })} )} diff --git a/frontend/src/components/cards/__tests__/VodConnectionCard.test.jsx b/frontend/src/components/cards/__tests__/VodConnectionCard.test.jsx index 6013ff31..1419dc1b 100644 --- a/frontend/src/components/cards/__tests__/VodConnectionCard.test.jsx +++ b/frontend/src/components/cards/__tests__/VodConnectionCard.test.jsx @@ -147,7 +147,10 @@ vi.mock('lucide-react', () => ({ })); // ── Imports after mocks ─────────────────────────────────────────────────────── -import { useDateTimeFormat } from '../../../utils/dateTimeUtils.js'; +import { + formatDuration, + useDateTimeFormat, +} from '../../../utils/dateTimeUtils.js'; import { calculateProgress, getEpisodeDisplayTitle, @@ -397,6 +400,75 @@ describe('VodConnectionCard', () => { }); }); + // ── Content duration badge ───────────────────────────────────────────────── + + describe('content duration badge', () => { + it('requests human-readable formatting for the content duration badge', () => { + render( + + ); + + expect(formatDuration).toHaveBeenCalledWith(2700, { + zeroValue: 'Unknown', + precision: 'human', + }); + }); + + it('renders the human-readable duration returned by formatDuration', () => { + vi.mocked(formatDuration).mockImplementation((seconds, options = {}) => { + if (options?.precision === 'human') { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + } + return `${seconds}s`; + }); + + render( + + ); + + expect(screen.getByText('45m')).toBeInTheDocument(); + }); + + it('shows hours and minutes for long-form content', () => { + vi.mocked(formatDuration).mockImplementation((seconds, options = {}) => { + if (options?.precision === 'human') { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + } + return `${seconds}s`; + }); + + render( + + ); + + expect(screen.getByText('2h 0m')).toBeInTheDocument(); + }); + + it('does not render the duration badge when duration_secs is missing', () => { + render( + + ); + + expect(formatDuration).not.toHaveBeenCalled(); + }); + }); + // ── Episode rendering ────────────────────────────────────────────────────── describe('episode content', () => { diff --git a/frontend/src/utils/__tests__/dateTimeUtils.test.js b/frontend/src/utils/__tests__/dateTimeUtils.test.js index 43ecaaf6..3bc642af 100644 --- a/frontend/src/utils/__tests__/dateTimeUtils.test.js +++ b/frontend/src/utils/__tests__/dateTimeUtils.test.js @@ -73,11 +73,21 @@ describe('dateTimeUtils', () => { it('should handle strict parsing', () => { // With strict=true, a date that doesn't match the format should be invalid - const invalid = dateTimeUtils.initializeTime('15-01-2024', 'YYYY-MM-DD', null, true); + const invalid = dateTimeUtils.initializeTime( + '15-01-2024', + 'YYYY-MM-DD', + null, + true + ); expect(invalid.isValid()).toBe(false); // With strict=true and a matching format, it should be valid - const valid = dateTimeUtils.initializeTime('2024-01-15', 'YYYY-MM-DD', null, true); + const valid = dateTimeUtils.initializeTime( + '2024-01-15', + 'YYYY-MM-DD', + null, + true + ); expect(valid.isValid()).toBe(true); }); }); @@ -873,6 +883,35 @@ describe('dateTimeUtils', () => { }); }); + describe('precision: human', () => { + it('should format sub-hour content as minutes with suffix', () => { + expect(dateTimeUtils.formatDuration(2700, { precision: 'human' })).toBe( + '45m' + ); + }); + + it('should floor partial minutes for short content', () => { + expect(dateTimeUtils.formatDuration(90, { precision: 'human' })).toBe( + '1m' + ); + }); + + it('should format hour-plus content as hours and minutes', () => { + expect(dateTimeUtils.formatDuration(5400, { precision: 'human' })).toBe( + '1h 30m' + ); + }); + + it('should return custom zeroValue when seconds is 0', () => { + expect( + dateTimeUtils.formatDuration(0, { + precision: 'human', + zeroValue: 'Unknown', + }) + ).toBe('Unknown'); + }); + }); + describe('precision: m', () => { it('should return total minutes as integer', () => { expect(dateTimeUtils.formatDuration(90, { precision: 'm' })).toBe('1'); diff --git a/frontend/src/utils/dateTimeUtils.js b/frontend/src/utils/dateTimeUtils.js index e198329c..2b613a32 100644 --- a/frontend/src/utils/dateTimeUtils.js +++ b/frontend/src/utils/dateTimeUtils.js @@ -18,7 +18,12 @@ export const convertToMs = (dateTime) => dayjs(dateTime).valueOf(); export const convertToSec = (dateTime) => dayjs(dateTime).unix(); -export const initializeTime = (dateTime, format = null, locale = null, strict = false) => { +export const initializeTime = ( + dateTime, + format = null, + locale = null, + strict = false +) => { if (format && locale) { return dayjs(dateTime, format, locale, strict); } else if (format) { @@ -26,7 +31,7 @@ export const initializeTime = (dateTime, format = null, locale = null, strict = } else { return dayjs(dateTime); } -} +}; export const startOfDay = (dateTime) => dayjs(dateTime).startOf('day'); @@ -90,7 +95,8 @@ export const setMinute = (dateTime, value) => dayjs(dateTime).minute(value); export const setSecond = (dateTime, value) => dayjs(dateTime).second(value); -export const setMillisecond = (dateTime, value) => dayjs(dateTime).millisecond(value); +export const setMillisecond = (dateTime, value) => + dayjs(dateTime).millisecond(value); export const getMonth = (dateTime) => dayjs(dateTime).month(); @@ -376,12 +382,16 @@ export const MONTH_ABBR = [ /** * @param {number} seconds * @param {object} [options] - * @param {'hms'|'hm'|'m'} [options.precision='hms'] - Segments to include + * @param {'hms'|'hm'|'m'|'human'} [options.precision='hms'] - Segments to include * @param {boolean} [options.alwaysShowHours=false] - Always include hours segment * @param {string|null} [options.zeroValue=null] - Return this when seconds is 0/falsy */ export const formatDuration = (seconds, options = {}) => { - const { precision = 'hms', alwaysShowHours = false, zeroValue = null } = options; + const { + precision = 'hms', + alwaysShowHours = false, + zeroValue = null, + } = options; if (!seconds || seconds === 0) return zeroValue ?? '0:00'; @@ -395,16 +405,14 @@ export const formatDuration = (seconds, options = {}) => { const hh = h.toString().padStart(2, '0'); switch (precision) { + case 'human': + return h > 0 ? `${h}h ${m}m` : `${m}m`; case 'm': return `${Math.floor(abs / 60)}`; case 'hm': - return (alwaysShowHours || h > 0) - ? `${hh}:${mm}` - : `${m}`; + return alwaysShowHours || h > 0 ? `${hh}:${mm}` : `${m}`; case 'hms': default: - return (alwaysShowHours || h > 0) - ? `${hh}:${mm}:${ss}` - : `${m}:${ss}`; + return alwaysShowHours || h > 0 ? `${hh}:${mm}:${ss}` : `${m}:${ss}`; } -}; \ No newline at end of file +};