-
{
- const tab = e.target.closest('[data-tab-value]');
- if (tab) onChange?.(tab.dataset.tabValue);
- }}>
+
{
+ const tab = e.target.closest('[data-tab-value]');
+ if (tab) onChange?.(tab.dataset.tabValue);
+ }}
+ >
{children}
),
TabsList: ({ children }) =>
{children}
,
TabsTab: ({ children, value }) => (
-
+
),
TabsPanel: ({ children, value }) => (
-
{children}
+
+ {children}
+
+ ),
+ Table: ({ children, ...props }) => (
+
+ ),
+ TableThead: ({ children }) => (
+
{children}
+ ),
+ TableTbody: ({ children }) => (
+
{children}
),
- Table: ({ children, ...props }) =>
,
- TableThead: ({ children }) =>
{children},
- TableTbody: ({ children }) =>
{children},
TableTr: ({ children, onClick, ...props }) => (
-
{children}
+
+ {children}
+
+ ),
+ TableTh: ({ children, ...props }) => (
+
+ {children}
+ |
+ ),
+ TableTd: ({ children, ...props }) => (
+
+ {children}
+ |
),
- TableTh: ({ children, ...props }) =>
{children} | ,
- TableTd: ({ children, ...props }) =>
{children} | ,
Divider: (props) =>
,
};
});
@@ -168,7 +239,7 @@ describe('SeriesModal', () => {
m3u_account: { name: 'Provider 2' },
stream_name: 'Test Series 720p',
quality_info: null,
- }
+ },
];
beforeEach(() => {
@@ -187,9 +258,15 @@ describe('SeriesModal', () => {
environment: { env_mode: 'prod' },
};
- useVODStore.mockImplementation((selector) => selector ? selector(mockVODStore) : mockVODStore);
- useVideoStore.mockImplementation((selector) => selector ? selector(mockVideoStore) : mockVideoStore);
- useSettingsStore.mockImplementation((selector) => selector ? selector(mockSettingsStore) : mockSettingsStore);
+ useVODStore.mockImplementation((selector) =>
+ selector ? selector(mockVODStore) : mockVODStore
+ );
+ useVideoStore.mockImplementation((selector) =>
+ selector ? selector(mockVideoStore) : mockVideoStore
+ );
+ useSettingsStore.mockImplementation((selector) =>
+ selector ? selector(mockSettingsStore) : mockSettingsStore
+ );
copyToClipboard.mockResolvedValue(undefined);
});
@@ -325,23 +402,37 @@ describe('SeriesModal', () => {
it('should display IMDB link when imdb_id exists', async () => {
render(
-
+
);
await waitFor(() => {
const link = screen.getByText(/IMDB/i).closest('a');
- expect(link).toHaveAttribute('href', 'https://www.imdb.com/title/tt1234567');
+ expect(link).toHaveAttribute(
+ 'href',
+ 'https://www.imdb.com/title/tt1234567'
+ );
});
});
it('should display TMDB link when tmdb_id exists', async () => {
render(
-
+
);
await waitFor(() => {
const link = screen.getByText(/TMDB/i).closest('a');
- expect(link).toHaveAttribute('href', 'https://www.themoviedb.org/tv/12345');
+ expect(link).toHaveAttribute(
+ 'href',
+ 'https://www.themoviedb.org/tv/12345'
+ );
});
});
});
@@ -446,7 +537,12 @@ describe('SeriesModal', () => {
});
it('should sort episodes by episode number', async () => {
- const episode2 = { ...mockEpisode, id: 2, episode_number: 2, name: 'Second Episode' };
+ const episode2 = {
+ ...mockEpisode,
+ id: 2,
+ episode_number: 2,
+ name: 'Second Episode',
+ };
mockVODStore.fetchSeriesInfo.mockResolvedValue({
...mockDetailedSeries,
episodesList: [episode2, mockEpisode],
@@ -592,7 +688,12 @@ describe('SeriesModal', () => {
describe('Season Tabs', () => {
it('should create tabs for each season', async () => {
- const season2Episode = { ...mockEpisode, id: 2, season_number: 2, episode_num: 1 };
+ const season2Episode = {
+ ...mockEpisode,
+ id: 2,
+ season_number: 2,
+ episode_num: 1,
+ };
mockVODStore.fetchSeriesInfo.mockResolvedValue({
...mockDetailedSeries,
episodesList: [mockEpisode, season2Episode],
@@ -670,7 +771,6 @@ describe('SeriesModal', () => {
);
-
await waitFor(() => {
expect(screen.getByText(/Provider 1 - 1080p/)).toBeInTheDocument();
});
@@ -797,7 +897,7 @@ describe('SeriesModal', () => {
);
await waitFor(() => {
- expect(screen.getByText('Test Account')).toBeInTheDocument()
+ expect(screen.getByText('Test Account')).toBeInTheDocument();
});
});
});
diff --git a/frontend/src/components/cards/__tests__/RecordingCard.test.jsx b/frontend/src/components/cards/__tests__/RecordingCard.test.jsx
index 9566f2b5..24e8dc46 100644
--- a/frontend/src/components/cards/__tests__/RecordingCard.test.jsx
+++ b/frontend/src/components/cards/__tests__/RecordingCard.test.jsx
@@ -109,13 +109,12 @@ vi.mock('@mantine/core', async () => ({
{children}
),
- Tooltip: ({ children, label }) => (
-
{children}
- ),
+ Tooltip: ({ children, label }) =>
{children}
,
}));
// ── lucide-react ───────────────────────────────────────────────────────────────
vi.mock('lucide-react', () => ({
+ ListOrdered: () =>
,
AlertTriangle: () =>
,
Plus: () =>
,
Square: () =>
,
@@ -140,8 +139,13 @@ vi.mock('../../../images/logo.png', () => ({ default: 'default-logo.png' }));
import useChannelsStore from '../../../store/channels.jsx';
import useSettingsStore from '../../../store/settings.jsx';
import useVideoStore from '../../../store/useVideoStore.jsx';
-import { useDateTimeFormat, useTimeHelpers, format, isAfter, isBefore }
- from '../../../utils/dateTimeUtils.js';
+import {
+ useDateTimeFormat,
+ useTimeHelpers,
+ format,
+ isAfter,
+ isBefore,
+} from '../../../utils/dateTimeUtils.js';
import { notifications } from '@mantine/notifications';
import * as RecordingCardUtils from '../../../utils/cards/RecordingCardUtils.js';
import dayjs from 'dayjs';
@@ -187,7 +191,11 @@ const makeChannel = () => ({
});
/** Wire up all store/utility mocks with sensible defaults */
-const setupMocks = ({ now = NOW, recording = makeRecording(), channel = makeChannel() } = {}) => {
+const setupMocks = ({
+ now = NOW,
+ recording = makeRecording(),
+ channel = makeChannel(),
+} = {}) => {
const nowMoment = makeMoment(now);
const startMoment = makeMoment(recording.start_time);
const endMoment = makeMoment(recording.end_time);
@@ -226,9 +234,13 @@ const setupMocks = ({ now = NOW, recording = makeRecording(), channel = makeChan
vi.mocked(RecordingCardUtils.getPosterUrl).mockReturnValue('/poster.jpg');
vi.mocked(RecordingCardUtils.getChannelLogoUrl).mockReturnValue('/logo.png');
- vi.mocked(RecordingCardUtils.getRecordingUrl).mockReturnValue('/recordings/test.ts');
+ vi.mocked(RecordingCardUtils.getRecordingUrl).mockReturnValue(
+ '/recordings/test.ts'
+ );
vi.mocked(RecordingCardUtils.getSeasonLabel).mockReturnValue('');
- vi.mocked(RecordingCardUtils.getSeriesInfo).mockReturnValue({ seriesId: 's1' });
+ vi.mocked(RecordingCardUtils.getSeriesInfo).mockReturnValue({
+ seriesId: 's1',
+ });
vi.mocked(RecordingCardUtils.getShowVideoUrl).mockReturnValue('/live/ch-1');
return { mockShowVideo, mockFetchRecordings };
@@ -237,10 +249,18 @@ const setupMocks = ({ now = NOW, recording = makeRecording(), channel = makeChan
describe('RecordingCard', () => {
beforeEach(() => {
vi.clearAllMocks();
- vi.mocked(RecordingCardUtils.stopRecordingById).mockResolvedValue(undefined);
- vi.mocked(RecordingCardUtils.deleteRecordingById).mockResolvedValue(undefined);
- vi.mocked(RecordingCardUtils.deleteSeriesAndRule).mockResolvedValue(undefined);
- vi.mocked(RecordingCardUtils.extendRecordingById).mockResolvedValue(undefined);
+ vi.mocked(RecordingCardUtils.stopRecordingById).mockResolvedValue(
+ undefined
+ );
+ vi.mocked(RecordingCardUtils.deleteRecordingById).mockResolvedValue(
+ undefined
+ );
+ vi.mocked(RecordingCardUtils.deleteSeriesAndRule).mockResolvedValue(
+ undefined
+ );
+ vi.mocked(RecordingCardUtils.extendRecordingById).mockResolvedValue(
+ undefined
+ );
vi.mocked(RecordingCardUtils.runComSkip).mockResolvedValue(undefined);
vi.mocked(RecordingCardUtils.removeRecording).mockReturnValue(undefined);
});
@@ -250,17 +270,23 @@ describe('RecordingCard', () => {
describe('rendering', () => {
it('renders the recording title', () => {
setupMocks();
- render(
);
+ render(
+
+ );
expect(screen.getByText('Test Show')).toBeInTheDocument();
});
it('renders "Custom Recording" when no program title', () => {
setupMocks({
- recording: makeRecording({ custom_properties: { status: 'completed', program: {} } }),
+ recording: makeRecording({
+ custom_properties: { status: 'completed', program: {} },
+ }),
});
render(
);
@@ -269,7 +295,9 @@ describe('RecordingCard', () => {
it('renders channel info', () => {
setupMocks();
- render(
);
+ render(
+
+ );
expect(screen.getByText('501 • HBO')).toBeInTheDocument();
});
@@ -281,27 +309,35 @@ describe('RecordingCard', () => {
it('shows description via RecordingSynopsis for non-series completed recording', () => {
setupMocks();
- render(
);
+ render(
+
+ );
expect(screen.getByTestId('recording-synopsis')).toBeInTheDocument();
expect(screen.getByText('A test description')).toBeInTheDocument();
});
it('shows sub_title when present', () => {
setupMocks();
- render(
);
+ render(
+
+ );
expect(screen.getByText('Pilot')).toBeInTheDocument();
});
it('shows season/episode label when getSeasonLabel returns a value', () => {
setupMocks();
vi.mocked(RecordingCardUtils.getSeasonLabel).mockReturnValue('S01E02');
- render(
);
+ render(
+
+ );
expect(screen.getByText('S01E02')).toBeInTheDocument();
});
it('renders the poster image', () => {
setupMocks();
- render(
);
+ render(
+
+ );
const img = screen.getByAltText('Test Show');
expect(img).toHaveAttribute('src', '/poster.jpg');
});
@@ -312,7 +348,9 @@ describe('RecordingCard', () => {
describe('status badge', () => {
it('shows "Completed" badge for a completed recording', () => {
setupMocks();
- render(
);
+ render(
+
+ );
expect(screen.getByText('Completed')).toBeInTheDocument();
});
@@ -320,7 +358,10 @@ describe('RecordingCard', () => {
const recording = makeRecording({
start_time: PAST,
end_time: FUTURE,
- custom_properties: { status: 'recording', program: { title: 'Live Show' } },
+ custom_properties: {
+ status: 'recording',
+ program: { title: 'Live Show' },
+ },
});
setupMocks({ recording });
render(
);
@@ -331,7 +372,10 @@ describe('RecordingCard', () => {
const recording = makeRecording({
start_time: FUTURE,
end_time: FUTURE,
- custom_properties: { status: 'scheduled', program: { title: 'Future Show' } },
+ custom_properties: {
+ status: 'scheduled',
+ program: { title: 'Future Show' },
+ },
});
setupMocks({ recording });
render(
);
@@ -373,8 +417,7 @@ describe('RecordingCard', () => {
// ── Series group ───────────────────────────────────────────────────────────
describe('series group', () => {
- const makeSeriesRecording = () =>
- makeRecording({ _group_count: 3 });
+ const makeSeriesRecording = () => makeRecording({ _group_count: 3 });
it('shows "Series" badge when _group_count > 1', () => {
const recording = makeSeriesRecording();
@@ -394,7 +437,9 @@ describe('RecordingCard', () => {
const recording = makeSeriesRecording();
setupMocks({ recording });
render(
);
- expect(screen.queryByTestId('recording-synopsis')).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('recording-synopsis')
+ ).not.toBeInTheDocument();
});
});
@@ -500,7 +545,9 @@ describe('RecordingCard', () => {
it('does not show "Watch Live" for a completed recording', () => {
setupMocks();
- render(
);
+ render(
+
+ );
expect(screen.queryByText('Watch Live')).not.toBeInTheDocument();
});
@@ -523,7 +570,9 @@ describe('RecordingCard', () => {
it('shows "Watch" button for a completed recording', () => {
setupMocks();
- render(
);
+ render(
+
+ );
expect(screen.getByText('Watch')).toBeInTheDocument();
});
@@ -531,7 +580,10 @@ describe('RecordingCard', () => {
const recording = makeRecording({
start_time: FUTURE,
end_time: FUTURE,
- custom_properties: { status: 'scheduled', program: { title: 'Future' } },
+ custom_properties: {
+ status: 'scheduled',
+ program: { title: 'Future' },
+ },
});
setupMocks({ recording });
render(
);
@@ -540,7 +592,9 @@ describe('RecordingCard', () => {
it('calls showVideo with vod params when Watch is clicked', () => {
const { mockShowVideo } = setupMocks();
- render(
);
+ render(
+
+ );
fireEvent.click(screen.getByText('Watch'));
expect(mockShowVideo).toHaveBeenCalledWith(
'/recordings/test.ts',
@@ -552,7 +606,9 @@ describe('RecordingCard', () => {
it('does not call showVideo when Watch is clicked but no file url', () => {
const { mockShowVideo } = setupMocks();
vi.mocked(RecordingCardUtils.getRecordingUrl).mockReturnValue(null);
- render(
);
+ render(
+
+ );
fireEvent.click(screen.getByText('Watch'));
expect(mockShowVideo).not.toHaveBeenCalled();
});
@@ -575,7 +631,9 @@ describe('RecordingCard', () => {
describe('"Remove commercials" button', () => {
it('shows "Remove commercials" for a completed recording without comskip', () => {
setupMocks();
- render(
);
+ render(
+
+ );
expect(screen.getByText('Remove commercials')).toBeInTheDocument();
});
@@ -597,7 +655,10 @@ describe('RecordingCard', () => {
const recording = makeRecording({
start_time: FUTURE,
end_time: FUTURE,
- custom_properties: { status: 'scheduled', program: { title: 'Future' } },
+ custom_properties: {
+ status: 'scheduled',
+ program: { title: 'Future' },
+ },
});
setupMocks({ recording });
render(
);
@@ -606,20 +667,31 @@ describe('RecordingCard', () => {
it('calls runComSkip and shows notification on success', async () => {
setupMocks();
- render(
);
+ render(
+
+ );
fireEvent.click(screen.getByText('Remove commercials'));
await waitFor(() => {
- expect(RecordingCardUtils.runComSkip).toHaveBeenCalledWith(makeRecording());
+ expect(RecordingCardUtils.runComSkip).toHaveBeenCalledWith(
+ makeRecording()
+ );
expect(notifications.show).toHaveBeenCalledWith(
- expect.objectContaining({ title: 'Removing commercials', color: 'blue.5' })
+ expect.objectContaining({
+ title: 'Removing commercials',
+ color: 'blue.5',
+ })
);
});
});
it('does not show notification when runComSkip throws', async () => {
- vi.mocked(RecordingCardUtils.runComSkip).mockRejectedValue(new Error('fail'));
+ vi.mocked(RecordingCardUtils.runComSkip).mockRejectedValue(
+ new Error('fail')
+ );
setupMocks();
- render(
);
+ render(
+
+ );
fireEvent.click(screen.getByText('Remove commercials'));
await waitFor(() => {
expect(notifications.show).not.toHaveBeenCalled();
@@ -634,7 +706,11 @@ describe('RecordingCard', () => {
makeRecording({
start_time: PAST,
end_time: FUTURE,
- custom_properties: { status: 'recording', program: { title: 'Live Show' }, file_url: '/f.ts' },
+ custom_properties: {
+ status: 'recording',
+ program: { title: 'Live Show' },
+ file_url: '/f.ts',
+ },
});
it('shows extend menu for in-progress recording', () => {
@@ -650,9 +726,15 @@ describe('RecordingCard', () => {
render(
);
fireEvent.click(screen.getByText('+15 minutes'));
await waitFor(() => {
- expect(RecordingCardUtils.extendRecordingById).toHaveBeenCalledWith('rec-1', 15);
+ expect(RecordingCardUtils.extendRecordingById).toHaveBeenCalledWith(
+ 'rec-1',
+ 15
+ );
expect(notifications.show).toHaveBeenCalledWith(
- expect.objectContaining({ title: 'Recording extended', color: 'teal' })
+ expect.objectContaining({
+ title: 'Recording extended',
+ color: 'teal',
+ })
);
});
});
@@ -663,7 +745,10 @@ describe('RecordingCard', () => {
render(
);
fireEvent.click(screen.getByText('+30 minutes'));
await waitFor(() => {
- expect(RecordingCardUtils.extendRecordingById).toHaveBeenCalledWith('rec-1', 30);
+ expect(RecordingCardUtils.extendRecordingById).toHaveBeenCalledWith(
+ 'rec-1',
+ 30
+ );
});
});
@@ -673,12 +758,17 @@ describe('RecordingCard', () => {
render(
);
fireEvent.click(screen.getByText('+1 hour'));
await waitFor(() => {
- expect(RecordingCardUtils.extendRecordingById).toHaveBeenCalledWith('rec-1', 60);
+ expect(RecordingCardUtils.extendRecordingById).toHaveBeenCalledWith(
+ 'rec-1',
+ 60
+ );
});
});
it('shows error notification when extendRecordingById throws', async () => {
- vi.mocked(RecordingCardUtils.extendRecordingById).mockRejectedValue(new Error('Network error'));
+ vi.mocked(RecordingCardUtils.extendRecordingById).mockRejectedValue(
+ new Error('Network error')
+ );
const recording = makeInProgress();
setupMocks({ recording });
render(
);
@@ -698,7 +788,11 @@ describe('RecordingCard', () => {
makeRecording({
start_time: PAST,
end_time: FUTURE,
- custom_properties: { status: 'recording', program: { title: 'Live Show' }, file_url: '/f.ts' },
+ custom_properties: {
+ status: 'recording',
+ program: { title: 'Live Show' },
+ file_url: '/f.ts',
+ },
});
it('shows stop modal when stop button is clicked', () => {
@@ -711,7 +805,9 @@ describe('RecordingCard', () => {
fireEvent.click(stopButton);
expect(screen.getByTestId('modal')).toBeInTheDocument();
- expect(screen.getByTestId('modal-title')).toHaveTextContent('Stop Recording');
+ expect(screen.getByTestId('modal-title')).toHaveTextContent(
+ 'Stop Recording'
+ );
});
it('closes stop modal when Go Back is clicked', () => {
@@ -736,7 +832,9 @@ describe('RecordingCard', () => {
fireEvent.click(screen.getAllByText('Stop Recording')[1]);
await waitFor(() => {
- expect(RecordingCardUtils.stopRecordingById).toHaveBeenCalledWith('rec-1');
+ expect(RecordingCardUtils.stopRecordingById).toHaveBeenCalledWith(
+ 'rec-1'
+ );
expect(mockFetchRecordings).toHaveBeenCalled();
});
});
@@ -761,48 +859,71 @@ describe('RecordingCard', () => {
describe('delete recording', () => {
it('shows delete modal for a completed non-series recording', () => {
setupMocks();
- render(
);
+ render(
+
+ );
- const deleteButton = screen.getByTestId('icon-square-x').closest('button');
+ const deleteButton = screen
+ .getByTestId('icon-square-x')
+ .closest('button');
fireEvent.click(deleteButton);
expect(screen.getByTestId('modal')).toBeInTheDocument();
- expect(screen.getByTestId('modal-title')).toHaveTextContent('Delete Recording');
+ expect(screen.getByTestId('modal-title')).toHaveTextContent(
+ 'Delete Recording'
+ );
});
it('shows "Cancel Recording" title for upcoming recording delete', () => {
const recording = makeRecording({
start_time: FUTURE,
end_time: FUTURE,
- custom_properties: { status: 'scheduled', program: { title: 'Future Show' } },
+ custom_properties: {
+ status: 'scheduled',
+ program: { title: 'Future Show' },
+ },
});
setupMocks({ recording });
render(
);
- const deleteButton = screen.getByTestId('icon-square-x').closest('button');
+ const deleteButton = screen
+ .getByTestId('icon-square-x')
+ .closest('button');
fireEvent.click(deleteButton);
- expect(screen.getByTestId('modal-title')).toHaveTextContent('Cancel Recording');
+ expect(screen.getByTestId('modal-title')).toHaveTextContent(
+ 'Cancel Recording'
+ );
});
it('calls removeRecording when delete is confirmed', async () => {
setupMocks();
- render(
);
+ render(
+
+ );
- const deleteButton = screen.getByTestId('icon-square-x').closest('button');
+ const deleteButton = screen
+ .getByTestId('icon-square-x')
+ .closest('button');
fireEvent.click(deleteButton);
fireEvent.click(screen.getByText('Delete'));
await waitFor(() => {
- expect(RecordingCardUtils.removeRecording).toHaveBeenCalledWith('rec-1');
+ expect(RecordingCardUtils.removeRecording).toHaveBeenCalledWith(
+ 'rec-1'
+ );
});
});
it('closes delete modal after confirming', async () => {
setupMocks();
- render(
);
+ render(
+
+ );
- const deleteButton = screen.getByTestId('icon-square-x').closest('button');
+ const deleteButton = screen
+ .getByTestId('icon-square-x')
+ .closest('button');
fireEvent.click(deleteButton);
fireEvent.click(screen.getByText('Delete'));
@@ -813,9 +934,13 @@ describe('RecordingCard', () => {
it('closes delete modal on Go Back click', () => {
setupMocks();
- render(
);
+ render(
+
+ );
- const deleteButton = screen.getByTestId('icon-square-x').closest('button');
+ const deleteButton = screen
+ .getByTestId('icon-square-x')
+ .closest('button');
fireEvent.click(deleteButton);
fireEvent.click(screen.getByText('Go Back'));
@@ -842,10 +967,14 @@ describe('RecordingCard', () => {
setupMocks({ recording });
render(
);
- const deleteButton = screen.getByTestId('icon-square-x').closest('button');
+ const deleteButton = screen
+ .getByTestId('icon-square-x')
+ .closest('button');
fireEvent.click(deleteButton);
- expect(screen.getByTestId('modal-title')).toHaveTextContent('Cancel Series');
+ expect(screen.getByTestId('modal-title')).toHaveTextContent(
+ 'Cancel Series'
+ );
});
it('calls deleteRecordingById when "Only this upcoming" is clicked', async () => {
@@ -853,12 +982,16 @@ describe('RecordingCard', () => {
const { mockFetchRecordings } = setupMocks({ recording });
render(
);
- const deleteButton = screen.getByTestId('icon-square-x').closest('button');
+ const deleteButton = screen
+ .getByTestId('icon-square-x')
+ .closest('button');
fireEvent.click(deleteButton);
fireEvent.click(screen.getByText('Only this upcoming'));
await waitFor(() => {
- expect(RecordingCardUtils.deleteRecordingById).toHaveBeenCalledWith('rec-1');
+ expect(RecordingCardUtils.deleteRecordingById).toHaveBeenCalledWith(
+ 'rec-1'
+ );
expect(mockFetchRecordings).toHaveBeenCalled();
});
});
@@ -868,7 +1001,9 @@ describe('RecordingCard', () => {
const { mockFetchRecordings } = setupMocks({ recording });
render(
);
- const deleteButton = screen.getByTestId('icon-square-x').closest('button');
+ const deleteButton = screen
+ .getByTestId('icon-square-x')
+ .closest('button');
fireEvent.click(deleteButton);
fireEvent.click(screen.getByText('Entire series + rule'));
@@ -883,7 +1018,9 @@ describe('RecordingCard', () => {
setupMocks({ recording });
render(
);
- const deleteButton = screen.getByTestId('icon-square-x').closest('button');
+ const deleteButton = screen
+ .getByTestId('icon-square-x')
+ .closest('button');
fireEvent.click(deleteButton);
fireEvent.click(screen.getByText('Only this upcoming'));
@@ -918,13 +1055,18 @@ describe('RecordingCard', () => {
_group_count: 3,
start_time: FUTURE,
end_time: FUTURE,
- custom_properties: { status: 'scheduled', program: { title: 'Series' } },
+ custom_properties: {
+ status: 'scheduled',
+ program: { title: 'Series' },
+ },
});
const { mockFetchRecordings } = setupMocks({ recording });
mockFetchRecordings.mockRejectedValue(new Error('network'));
render(
);
- const deleteButton = screen.getByTestId('icon-square-x').closest('button');
+ const deleteButton = screen
+ .getByTestId('icon-square-x')
+ .closest('button');
fireEvent.click(deleteButton);
await expect(
@@ -932,4 +1074,4 @@ describe('RecordingCard', () => {
).resolves.not.toThrow();
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/components/cards/__tests__/SeriesCard.test.jsx b/frontend/src/components/cards/__tests__/SeriesCard.test.jsx
index fb95c882..49b8524f 100644
--- a/frontend/src/components/cards/__tests__/SeriesCard.test.jsx
+++ b/frontend/src/components/cards/__tests__/SeriesCard.test.jsx
@@ -30,7 +30,13 @@ vi.mock('@mantine/core', async () => ({
),
Stack: ({ children, gap }) =>
{children}
,
Text: ({ children, size, fw, c, lineClamp, style }) => (
-
+
{children}
),
@@ -38,6 +44,7 @@ vi.mock('@mantine/core', async () => ({
// ── lucide-react ───────────────────────────────────────────────────────────────
vi.mock('lucide-react', () => ({
+ ListOrdered: () => ,
Calendar: () => ,
Play: () => ,
Star: () => ,
@@ -78,7 +85,12 @@ describe('SeriesCard', () => {
});
it('renders a fallback image when poster_url is missing', () => {
- render();
+ render(
+
+ );
const img = screen.getByRole('img');
expect(img).toBeInTheDocument();
});
@@ -105,7 +117,12 @@ describe('SeriesCard', () => {
it('renders play icon', () => {
//this only renders when logo.url is missing, but we want to test that the icon itself renders correctly
- render();
+ render(
+
+ );
expect(screen.getByTestId('icon-play')).toBeInTheDocument();
});
@@ -119,27 +136,52 @@ describe('SeriesCard', () => {
describe('optional fields', () => {
it('does not crash when year is missing', () => {
- render();
+ render(
+
+ );
expect(screen.getByTestId('series-card')).toBeInTheDocument();
});
it('does not crash when rating is missing', () => {
- render();
+ render(
+
+ );
expect(screen.getByTestId('series-card')).toBeInTheDocument();
});
it('does not crash when genre is missing', () => {
- render();
+ render(
+
+ );
expect(screen.getByTestId('series-card')).toBeInTheDocument();
});
it('does not crash when description is missing', () => {
- render();
+ render(
+
+ );
expect(screen.getByTestId('series-card')).toBeInTheDocument();
});
it('does not crash when seasons is missing', () => {
- render();
+ render(
+
+ );
expect(screen.getByTestId('series-card')).toBeInTheDocument();
});
@@ -161,4 +203,4 @@ describe('SeriesCard', () => {
expect(onClick).toHaveBeenCalledWith(series);
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/components/cards/__tests__/StreamConnectionCard.test.jsx b/frontend/src/components/cards/__tests__/StreamConnectionCard.test.jsx
index babcf1e6..2f006244 100644
--- a/frontend/src/components/cards/__tests__/StreamConnectionCard.test.jsx
+++ b/frontend/src/components/cards/__tests__/StreamConnectionCard.test.jsx
@@ -95,7 +95,12 @@ vi.mock('@mantine/core', () => ({
Center: ({ children }) => {children}
,
Group: ({ children }) => {children}
,
Progress: ({ value, size, color }) => (
-
+
),
Select: ({ value, onChange, label, data, disabled, placeholder }) => (
),
- Tooltip: ({ children, label }) => (
-
{children}
- ),
+ Tooltip: ({ children, label }) =>
{children}
,
useMantineTheme: vi.fn(() => ({
tailwind: { green: { 5: '#22c55e' } },
})),
@@ -139,6 +142,22 @@ vi.mock('@mantine/core', () => ({
// ── lucide-react ──────────────────────────────────────────────────────────────
vi.mock('lucide-react', () => ({
+ // navigation.js icons (all must be present for the auth→navigation import chain)
+ ListOrdered: () =>
,
+ Play: () =>
,
+ Database: () =>
,
+ LayoutGrid: () =>
,
+ Settings: () =>
,
+ ChartLine: () =>
,
+ Video: () =>
,
+ PlugZap: () =>
,
+ User: () =>
,
+ FileImage: () =>
,
+ Webhook: () =>
,
+ Logs: () =>
,
+ Blocks: () =>
,
+ MonitorCog: () =>
,
+ // StreamConnectionCard-specific icons
ChevronDown: () =>
,
ChevronRight: () =>
,
CirclePlay: () =>
,
@@ -149,7 +168,6 @@ vi.mock('lucide-react', () => ({
SquareX: () =>
,
Timer: () =>
,
Users: () =>
,
- Video: () =>
,
}));
// ── Imports after mocks ───────────────────────────────────────────────────────
@@ -264,13 +282,17 @@ describe('StreamConnectionCard', () => {
describe('route guard', () => {
it('renders nothing when pathname is not /stats', () => {
setupLocation('/dashboard');
- const { container } = render(
);
+ const { container } = render(
+
+ );
expect(container.firstChild).toBeNull();
});
it('renders nothing when pathname is /channels', () => {
setupLocation('/channels');
- const { container } = render(
);
+ const { container } = render(
+
+ );
expect(container.firstChild).toBeNull();
});
@@ -337,7 +359,9 @@ describe('StreamConnectionCard', () => {
vi.mocked(getStreamsByIds).mockResolvedValue([]);
render(
);
expect(screen.getByText('Unnamed Channel')).toBeInTheDocument();
@@ -465,7 +489,9 @@ describe('StreamConnectionCard', () => {
describe('current program', () => {
it('does not render program section when currentProgram is null', () => {
- render(
);
+ render(
+
+ );
expect(screen.queryByText('Now Playing:')).not.toBeInTheDocument();
});
@@ -493,7 +519,9 @@ describe('StreamConnectionCard', () => {
{...defaultProps({ currentProgram: makeCurrentProgram() })}
/>
);
- expect(screen.queryByText('Daily news broadcast.')).not.toBeInTheDocument();
+ expect(
+ screen.queryByText('Daily news broadcast.')
+ ).not.toBeInTheDocument();
});
it('expands program description when chevron button is clicked', () => {
@@ -502,7 +530,9 @@ describe('StreamConnectionCard', () => {
{...defaultProps({ currentProgram: makeCurrentProgram() })}
/>
);
- const chevronBtn = screen.getByTestId('icon-chevron-right').closest('button');
+ const chevronBtn = screen
+ .getByTestId('icon-chevron-right')
+ .closest('button');
fireEvent.click(chevronBtn);
expect(screen.getByText('Daily news broadcast.')).toBeInTheDocument();
});
@@ -513,12 +543,18 @@ describe('StreamConnectionCard', () => {
{...defaultProps({ currentProgram: makeCurrentProgram() })}
/>
);
- const chevronBtn = screen.getByTestId('icon-chevron-right').closest('button');
+ const chevronBtn = screen
+ .getByTestId('icon-chevron-right')
+ .closest('button');
fireEvent.click(chevronBtn);
expect(screen.getByText('Daily news broadcast.')).toBeInTheDocument();
- const chevronDownBtn = screen.getByTestId('icon-chevron-down').closest('button');
+ const chevronDownBtn = screen
+ .getByTestId('icon-chevron-down')
+ .closest('button');
fireEvent.click(chevronDownBtn);
- expect(screen.queryByText('Daily news broadcast.')).not.toBeInTheDocument();
+ expect(
+ screen.queryByText('Daily news broadcast.')
+ ).not.toBeInTheDocument();
});
it('renders program progress when expanded and times are present', () => {
@@ -527,7 +563,9 @@ describe('StreamConnectionCard', () => {
{...defaultProps({ currentProgram: makeCurrentProgram() })}
/>
);
- const chevronBtn = screen.getByTestId('icon-chevron-right').closest('button');
+ const chevronBtn = screen
+ .getByTestId('icon-chevron-right')
+ .closest('button');
fireEvent.click(chevronBtn);
expect(screen.getByTestId('progress')).toBeInTheDocument();
});
@@ -536,11 +574,16 @@ describe('StreamConnectionCard', () => {
render(
);
- const chevronBtn = screen.getByTestId('icon-chevron-right').closest('button');
+ const chevronBtn = screen
+ .getByTestId('icon-chevron-right')
+ .closest('button');
fireEvent.click(chevronBtn);
expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
});
@@ -570,9 +613,8 @@ describe('StreamConnectionCard', () => {
{ id: 11, name: 'Stream B', url: 'http://b.com', m3u_profile: null },
]);
// Provide options via getStreamOptions mock
- const { getStreamOptions } = await import(
- '../../../utils/cards/StreamConnectionCardUtils.js'
- );
+ const { getStreamOptions } =
+ await import('../../../utils/cards/StreamConnectionCardUtils.js');
vi.mocked(getStreamOptions).mockReturnValue([
{ value: '10', label: 'Stream A' },
{ value: '11', label: 'Stream B' },
@@ -585,7 +627,12 @@ describe('StreamConnectionCard', () => {
it('sets activeStreamId when a matching stream is found by URL', async () => {
vi.mocked(getChannelStreams).mockResolvedValue([
- { id: 42, name: 'Stream A', url: 'http://stream.example.com/ch1', m3u_profile: null },
+ {
+ id: 42,
+ name: 'Stream A',
+ url: 'http://stream.example.com/ch1',
+ m3u_profile: null,
+ },
]);
vi.mocked(getMatchingStreamByUrl).mockReturnValue({
id: 42,
@@ -614,11 +661,15 @@ describe('StreamConnectionCard', () => {
describe('stream switching', () => {
beforeEach(async () => {
vi.mocked(getChannelStreams).mockResolvedValue([
- { id: 10, name: 'Stream A', url: 'http://a.com', m3u_profile: { name: 'M3U A' } },
+ {
+ id: 10,
+ name: 'Stream A',
+ url: 'http://a.com',
+ m3u_profile: { name: 'M3U A' },
+ },
]);
- const { getStreamOptions } = await import(
- '../../../utils/cards/StreamConnectionCardUtils.js'
- );
+ const { getStreamOptions } =
+ await import('../../../utils/cards/StreamConnectionCardUtils.js');
vi.mocked(getStreamOptions).mockReturnValue([
{ value: '10', label: 'Stream A' },
]);
@@ -633,7 +684,9 @@ describe('StreamConnectionCard', () => {
vi.mocked(switchStream).mockResolvedValue({});
render(
);
await waitFor(() => screen.getByTestId('select'));
- fireEvent.change(screen.getByTestId('select'), { target: { value: '10' } });
+ fireEvent.change(screen.getByTestId('select'), {
+ target: { value: '10' },
+ });
await waitFor(() => {
expect(switchStream).toHaveBeenCalledWith(
expect.objectContaining({ channel_id: 'ch-uuid-1' }),
@@ -646,7 +699,9 @@ describe('StreamConnectionCard', () => {
vi.mocked(switchStream).mockResolvedValue({});
render(
);
await waitFor(() => screen.getByTestId('select'));
- fireEvent.change(screen.getByTestId('select'), { target: { value: '10' } });
+ fireEvent.change(screen.getByTestId('select'), {
+ target: { value: '10' },
+ });
await waitFor(() => {
expect(showNotification).toHaveBeenCalledWith(
expect.objectContaining({ color: 'blue.5' })
@@ -658,7 +713,9 @@ describe('StreamConnectionCard', () => {
vi.mocked(switchStream).mockRejectedValue(new Error('Switch failed'));
render(
);
await waitFor(() => screen.getByTestId('select'));
- fireEvent.change(screen.getByTestId('select'), { target: { value: '10' } });
+ fireEvent.change(screen.getByTestId('select'), {
+ target: { value: '10' },
+ });
await waitFor(() => {
expect(showNotification).toHaveBeenCalledWith(
expect.objectContaining({ color: 'red.5' })
@@ -672,7 +729,9 @@ describe('StreamConnectionCard', () => {
});
render(
);
await waitFor(() => screen.getByTestId('select'));
- fireEvent.change(screen.getByTestId('select'), { target: { value: '10' } });
+ fireEvent.change(screen.getByTestId('select'), {
+ target: { value: '10' },
+ });
await waitFor(() => {
expect(screen.getByText('Updated M3U')).toBeInTheDocument();
});
@@ -686,7 +745,9 @@ describe('StreamConnectionCard', () => {
vi.mocked(getChannelStreams).mockResolvedValue([]);
render(
);
await waitFor(() => {
- expect(screen.queryByTestId('icon-circle-play')).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('icon-circle-play')
+ ).not.toBeInTheDocument();
});
});
@@ -694,10 +755,11 @@ describe('StreamConnectionCard', () => {
vi.mocked(getChannelStreams).mockResolvedValue([
{ id: 10, name: 'Stream A', url: 'http://a.com', m3u_profile: null },
]);
- const { getStreamOptions } = await import(
- '../../../utils/cards/StreamConnectionCardUtils.js'
- );
- vi.mocked(getStreamOptions).mockReturnValue([{ value: '10', label: 'Stream A' }]);
+ const { getStreamOptions } =
+ await import('../../../utils/cards/StreamConnectionCardUtils.js');
+ vi.mocked(getStreamOptions).mockReturnValue([
+ { value: '10', label: 'Stream A' },
+ ]);
render(
);
await waitFor(() => {
expect(screen.getByTestId('icon-circle-play')).toBeInTheDocument();
@@ -712,10 +774,11 @@ describe('StreamConnectionCard', () => {
vi.mocked(getChannelStreams).mockResolvedValue([
{ id: 10, name: 'Stream A', url: 'http://a.com', m3u_profile: null },
]);
- const { getStreamOptions } = await import(
- '../../../utils/cards/StreamConnectionCardUtils.js'
- );
- vi.mocked(getStreamOptions).mockReturnValue([{ value: '10', label: 'Stream A' }]);
+ const { getStreamOptions } =
+ await import('../../../utils/cards/StreamConnectionCardUtils.js');
+ vi.mocked(getStreamOptions).mockReturnValue([
+ { value: '10', label: 'Stream A' },
+ ]);
render(
);
await waitFor(() => screen.getByTestId('icon-circle-play'));
@@ -738,10 +801,11 @@ describe('StreamConnectionCard', () => {
vi.mocked(getChannelStreams).mockResolvedValue([
{ id: 10, name: 'Stream A', url: 'http://a.com', m3u_profile: null },
]);
- const { getStreamOptions } = await import(
- '../../../utils/cards/StreamConnectionCardUtils.js'
- );
- vi.mocked(getStreamOptions).mockReturnValue([{ value: '10', label: 'Stream A' }]);
+ const { getStreamOptions } =
+ await import('../../../utils/cards/StreamConnectionCardUtils.js');
+ vi.mocked(getStreamOptions).mockReturnValue([
+ { value: '10', label: 'Stream A' },
+ ]);
render(
@@ -837,4 +901,4 @@ describe('StreamConnectionCard', () => {
expect(screen.getAllByText('0 Kbps').length).toBeGreaterThan(0);
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/components/cards/__tests__/VODCard.test.jsx b/frontend/src/components/cards/__tests__/VODCard.test.jsx
index 05e98b1e..679d8145 100644
--- a/frontend/src/components/cards/__tests__/VODCard.test.jsx
+++ b/frontend/src/components/cards/__tests__/VODCard.test.jsx
@@ -10,12 +10,22 @@ vi.mock('../../../utils/cards/VODCardUtils.js', () => ({
// ── Mantine core ──────────────────────────────────────────────────────────────
vi.mock('@mantine/core', () => ({
ActionIcon: ({ children, onClick, variant, size }) => (
-
),
- CardSection: ({ children }) =>