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.

This commit is contained in:
SergeantPanda 2026-06-30 16:48:36 -05:00
parent 570adc2fe0
commit 14ab2a0317
4 changed files with 138 additions and 16 deletions

View file

@ -372,7 +372,10 @@ const VodConnectionCard = ({ vodContent, stopVODClient }) => {
{metadata.duration_secs && (
<Tooltip label="Content Duration">
<Badge size="sm" variant="light" color="blue">
{formatDuration(metadata.duration_secs, { zeroValue: 'Unknown', precision: 'hm' })}
{formatDuration(metadata.duration_secs, {
zeroValue: 'Unknown',
precision: 'human',
})}
</Badge>
</Tooltip>
)}

View file

@ -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(
<VodConnectionCard
vodContent={makeEpisodeContent()}
stopVODClient={vi.fn()}
/>
);
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(
<VodConnectionCard
vodContent={makeEpisodeContent()}
stopVODClient={vi.fn()}
/>
);
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(
<VodConnectionCard
vodContent={makeMovieContent()}
stopVODClient={vi.fn()}
/>
);
expect(screen.getByText('2h 0m')).toBeInTheDocument();
});
it('does not render the duration badge when duration_secs is missing', () => {
render(
<VodConnectionCard
vodContent={makeUnknownContent()}
stopVODClient={vi.fn()}
/>
);
expect(formatDuration).not.toHaveBeenCalled();
});
});
// Episode rendering
describe('episode content', () => {

View file

@ -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');

View file

@ -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}`;
}
};
};