fix(frontend): update getShowVideoUrl tests to expect output_format and output_profile

This commit is contained in:
nemes 2026-05-29 17:32:24 +02:00
parent 98d7acd3c0
commit c717df50f9

View file

@ -1,7 +1,14 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
removeRecording, getPosterUrl, getShowVideoUrl, runComSkip,
deleteRecordingById, deleteSeriesAndRule, getRecordingUrl, getSeasonLabel, getSeriesInfo,
removeRecording,
getPosterUrl,
getShowVideoUrl,
runComSkip,
deleteRecordingById,
deleteSeriesAndRule,
getRecordingUrl,
getSeasonLabel,
getSeriesInfo,
} from '../RecordingCardUtils';
import API from '../../../api';
import useChannelsStore from '../../../store/channels';
@ -10,50 +17,174 @@ vi.mock('../../../api');
vi.mock('../../../store/channels');
describe('RecordingCardUtils', () => {
beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); });
describe('getShowVideoUrl', () => {
it('returns proxy URL with mpegts output format', () => {
expect(getShowVideoUrl({ uuid: 'channel-123' }, 'production'))
.toBe('/proxy/ts/stream/channel-123?output_format=mpegts');
});
it('includes output_profile when set in player prefs', () => {
localStorage.setItem('dispatcharr-player-prefs', JSON.stringify({ webPlayerOutputProfileId: 5 }));
expect(getShowVideoUrl({ uuid: 'channel-123' }, 'production'))
.toBe('/proxy/ts/stream/channel-123?output_format=mpegts&output_profile=5');
});
it('prepends dev server URL in dev mode with output params', () => {
expect(getShowVideoUrl({ uuid: 'channel-123' }, 'dev')).toMatch(/^https?:\/\/.*:5656\/proxy\/ts\/stream\/channel-123\?output_format=mpegts$/);
});
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
});
describe('removeRecording', () => {
let mockRemoveRecording, mockFetchRecordings;
let mockRemoveRecording;
let mockFetchRecordings;
beforeEach(() => {
mockRemoveRecording = vi.fn(); mockFetchRecordings = vi.fn();
useChannelsStore.getState = vi.fn(() => ({ removeRecording: mockRemoveRecording, fetchRecordings: mockFetchRecordings }));
mockRemoveRecording = vi.fn();
mockFetchRecordings = vi.fn();
useChannelsStore.getState = vi.fn(() => ({
removeRecording: mockRemoveRecording,
fetchRecordings: mockFetchRecordings,
}));
});
it('optimistically removes recording from store', () => {
API.deleteRecording.mockResolvedValue();
removeRecording('recording-1');
expect(mockRemoveRecording).toHaveBeenCalledWith('recording-1');
});
it('calls API to delete recording', () => {
API.deleteRecording.mockResolvedValue();
removeRecording('recording-1');
expect(API.deleteRecording).toHaveBeenCalledWith('recording-1');
});
it('handles optimistic removal error', () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation();
mockRemoveRecording.mockImplementation(() => {
throw new Error('Store error');
});
API.deleteRecording.mockResolvedValue();
removeRecording('recording-1');
expect(consoleError).toHaveBeenCalledWith(
'Failed to optimistically remove recording',
expect.any(Error)
);
consoleError.mockRestore();
});
it('refetches recordings when API delete fails', async () => {
API.deleteRecording.mockRejectedValue(new Error('Delete failed'));
removeRecording('recording-1');
await vi.waitFor(() => { expect(mockFetchRecordings).toHaveBeenCalled(); });
await vi.waitFor(() => {
expect(mockFetchRecordings).toHaveBeenCalled();
});
});
it('handles fetch error after failed delete', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation();
API.deleteRecording.mockRejectedValue(new Error('Delete failed'));
mockFetchRecordings.mockImplementation(() => {
throw new Error('Fetch error');
});
removeRecording('recording-1');
await vi.waitFor(() => {
expect(consoleError).toHaveBeenCalledWith(
'Failed to refresh recordings after delete',
expect.any(Error)
);
});
consoleError.mockRestore();
});
});
describe('getPosterUrl', () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it('returns logo URL when posterLogoId is provided', () => {
vi.stubEnv('DEV', false);
const result = getPosterUrl('logo-123', {}, '');
expect(result).toBe('/api/channels/logos/logo-123/cache/');
});
it('returns custom poster_url when no posterLogoId', () => {
vi.stubEnv('DEV', false);
const customProps = { poster_url: '/custom/poster.jpg' };
const result = getPosterUrl(null, customProps, '');
expect(result).toBe('/custom/poster.jpg');
});
it('returns posterUrl when no posterLogoId or custom poster_url', () => {
vi.stubEnv('DEV', false);
const result = getPosterUrl(null, {}, '/fallback/poster.jpg');
expect(result).toBe('/fallback/poster.jpg');
});
it('returns default logo when no parameters provided', () => {
vi.stubEnv('DEV', false);
const result = getPosterUrl(null, {}, '');
// Falls back to the imported default Dispatcharr logo asset
expect(result).toBeTruthy();
expect(result).toContain('logo');
});
it('prepends dev server URL in dev mode for relative paths', () => {
vi.stubEnv('DEV', true);
const result = getPosterUrl(null, {}, '/poster.jpg');
expect(result).toMatch(/^https?:\/\/.*:5656\/poster\.jpg$/);
});
it('does not prepend dev URL for absolute URLs', () => {
vi.stubEnv('DEV', true);
const result = getPosterUrl(null, {}, 'https://example.com/poster.jpg');
expect(result).toBe('https://example.com/poster.jpg');
});
});
describe('getShowVideoUrl', () => {
it('returns proxy URL with mpegts output format for channel', () => {
const channel = { uuid: 'channel-123' };
const result = getShowVideoUrl(channel, 'production');
expect(result).toBe('/proxy/ts/stream/channel-123?output_format=mpegts');
});
it('includes output_profile when set in player prefs', () => {
localStorage.setItem(
'dispatcharr-player-prefs',
JSON.stringify({ webPlayerOutputProfileId: 5 })
);
const channel = { uuid: 'channel-123' };
const result = getShowVideoUrl(channel, 'production');
expect(result).toBe(
'/proxy/ts/stream/channel-123?output_format=mpegts&output_profile=5'
);
});
it('prepends dev server URL in dev mode with output params', () => {
const channel = { uuid: 'channel-123' };
const result = getShowVideoUrl(channel, 'dev');
expect(result).toMatch(
/^https?:\/\/.*:5656\/proxy\/ts\/stream\/channel-123\?output_format=mpegts$/
);
});
});
describe('runComSkip', () => {
it('calls API runComskip with recording id', async () => {
API.runComskip.mockResolvedValue();
await runComSkip({ id: 'recording-1' });
const recording = { id: 'recording-1' };
await runComSkip(recording);
expect(API.runComskip).toHaveBeenCalledWith('recording-1');
});
});
@ -61,34 +192,231 @@ describe('RecordingCardUtils', () => {
describe('deleteRecordingById', () => {
it('calls API deleteRecording with id', async () => {
API.deleteRecording.mockResolvedValue();
await deleteRecordingById('recording-1');
expect(API.deleteRecording).toHaveBeenCalledWith('recording-1');
});
});
describe('deleteSeriesAndRule', () => {
it('removes series recordings and deletes series rule', async () => {
API.bulkRemoveSeriesRecordings.mockResolvedValue();
API.deleteSeriesRule.mockResolvedValue();
const seriesInfo = { tvg_id: 'series-123', title: 'Test Series' };
await deleteSeriesAndRule(seriesInfo);
expect(API.bulkRemoveSeriesRecordings).toHaveBeenCalledWith({
tvg_id: 'series-123',
title: 'Test Series',
scope: 'title',
});
expect(API.deleteSeriesRule).toHaveBeenCalledWith(
'series-123',
'Test Series'
);
});
it('works for title-only rules with no tvg_id', async () => {
API.bulkRemoveSeriesRecordings.mockResolvedValue();
API.deleteSeriesRule.mockResolvedValue();
const seriesInfo = { tvg_id: undefined, title: 'Title-Only Show' };
await deleteSeriesAndRule(seriesInfo);
expect(API.bulkRemoveSeriesRecordings).toHaveBeenCalledWith({
tvg_id: '',
title: 'Title-Only Show',
scope: 'title',
});
expect(API.deleteSeriesRule).toHaveBeenCalledWith(
undefined,
'Title-Only Show'
);
});
it('handles bulk remove error gracefully', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation();
API.bulkRemoveSeriesRecordings.mockRejectedValue(
new Error('Bulk remove failed')
);
API.deleteSeriesRule.mockResolvedValue();
const seriesInfo = { tvg_id: 'series-123', title: 'Test Series' };
await deleteSeriesAndRule(seriesInfo);
expect(consoleError).toHaveBeenCalledWith(
'Failed to remove series recordings',
expect.any(Error)
);
expect(API.deleteSeriesRule).toHaveBeenCalled();
consoleError.mockRestore();
});
it('handles delete rule error gracefully', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation();
API.bulkRemoveSeriesRecordings.mockResolvedValue();
API.deleteSeriesRule.mockRejectedValue(new Error('Delete rule failed'));
const seriesInfo = { tvg_id: 'series-123', title: 'Test Series' };
await deleteSeriesAndRule(seriesInfo);
expect(consoleError).toHaveBeenCalledWith(
'Failed to delete series rule',
expect.any(Error)
);
consoleError.mockRestore();
});
});
describe('getRecordingUrl', () => {
it('returns file_url when available', () => {
expect(getRecordingUrl({ file_url: '/recordings/file.mp4' }, 'production')).toBe('/recordings/file.mp4');
const customProps = { file_url: '/recordings/file.mp4' };
const result = getRecordingUrl(customProps, 'production');
expect(result).toBe('/recordings/file.mp4');
});
it('returns undefined when no file URL available', () => {
expect(getRecordingUrl({}, 'production')).toBeUndefined();
it('returns output_file_url when file_url is not available', () => {
const customProps = { output_file_url: '/output/file.mp4' };
const result = getRecordingUrl(customProps, 'production');
expect(result).toBe('/output/file.mp4');
});
it('prefers file_url over output_file_url', () => {
const customProps = {
file_url: '/recordings/file.mp4',
output_file_url: '/output/file.mp4',
};
const result = getRecordingUrl(customProps, 'production');
expect(result).toBe('/recordings/file.mp4');
});
it('prepends dev server URL in dev mode for relative paths', () => {
const customProps = { file_url: '/recordings/file.mp4' };
const result = getRecordingUrl(customProps, 'dev');
expect(result).toMatch(/^https?:\/\/.*:5656\/recordings\/file\.mp4$/);
});
it('does not prepend dev URL for absolute URLs', () => {
const customProps = { file_url: 'https://example.com/file.mp4' };
const result = getRecordingUrl(customProps, 'dev');
expect(result).toBe('https://example.com/file.mp4');
});
it('returns undefined when no file URL is available', () => {
const result = getRecordingUrl({}, 'production');
expect(result).toBeUndefined();
});
it('handles null customProps', () => {
expect(getRecordingUrl(null, 'production')).toBeUndefined();
const result = getRecordingUrl(null, 'production');
expect(result).toBeUndefined();
});
});
describe('getSeasonLabel', () => {
it('returns formatted label', () => { expect(getSeasonLabel(1, 5, null)).toBe('S01E05'); });
it('returns onscreen when season missing', () => { expect(getSeasonLabel(null, 5, 'Episode 5')).toBe('Episode 5'); });
it('returns null when nothing provided', () => { expect(getSeasonLabel(null, null, null)).toBeNull(); });
it('returns formatted season and episode label', () => {
const result = getSeasonLabel(1, 5, null);
expect(result).toBe('S01E05');
});
it('pads single digit season and episode numbers', () => {
const result = getSeasonLabel(2, 3, null);
expect(result).toBe('S02E03');
});
it('handles multi-digit season and episode numbers', () => {
const result = getSeasonLabel(12, 34, null);
expect(result).toBe('S12E34');
});
it('returns onscreen value when season or episode is missing', () => {
const result = getSeasonLabel(null, 5, 'Episode 5');
expect(result).toBe('Episode 5');
});
it('returns onscreen value when only episode is missing', () => {
const result = getSeasonLabel(1, null, 'Special');
expect(result).toBe('Special');
});
it('returns null when no season, episode, or onscreen provided', () => {
const result = getSeasonLabel(null, null, null);
expect(result).toBeNull();
});
it('returns formatted label even when onscreen is provided', () => {
const result = getSeasonLabel(1, 5, 'Episode 5');
expect(result).toBe('S01E05');
});
});
describe('getSeriesInfo', () => {
it('extracts tvg_id and title', () => {
expect(getSeriesInfo({ program: { tvg_id: 'series-123', title: 'Test Series' } }))
.toEqual({ tvg_id: 'series-123', title: 'Test Series' });
it('extracts tvg_id and title from program', () => {
const customProps = {
program: { tvg_id: 'series-123', title: 'Test Series' },
};
const result = getSeriesInfo(customProps);
expect(result).toEqual({
tvg_id: 'series-123',
title: 'Test Series',
});
});
it('handles missing program object', () => {
const customProps = {};
const result = getSeriesInfo(customProps);
expect(result).toEqual({
tvg_id: undefined,
title: undefined,
});
});
it('handles null customProps', () => {
const result = getSeriesInfo(null);
expect(result).toEqual({
tvg_id: undefined,
title: undefined,
});
});
it('handles undefined customProps', () => {
const result = getSeriesInfo(undefined);
expect(result).toEqual({
tvg_id: undefined,
title: undefined,
});
});
it('handles partial program data', () => {
const customProps = {
program: { tvg_id: 'series-123' },
};
const result = getSeriesInfo(customProps);
expect(result).toEqual({
tvg_id: 'series-123',
title: undefined,
});
});
it('handles null', () => { expect(getSeriesInfo(null)).toEqual({ tvg_id: undefined, title: undefined }); });
});
});