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
+};