From c717df50f91c92c9eb1bfdceef596b4d66403218 Mon Sep 17 00:00:00 2001 From: nemes Date: Fri, 29 May 2026 17:32:24 +0200 Subject: [PATCH] fix(frontend): update getShowVideoUrl tests to expect output_format and output_profile --- .../__tests__/RecordingCardUtils.test.js | 394 ++++++++++++++++-- 1 file changed, 361 insertions(+), 33 deletions(-) diff --git a/frontend/src/utils/cards/__tests__/RecordingCardUtils.test.js b/frontend/src/utils/cards/__tests__/RecordingCardUtils.test.js index d0b843ee..2e601120 100644 --- a/frontend/src/utils/cards/__tests__/RecordingCardUtils.test.js +++ b/frontend/src/utils/cards/__tests__/RecordingCardUtils.test.js @@ -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 }); }); }); });