mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
tests: Update frontend tests
This commit is contained in:
parent
dd97b642c7
commit
7ece48bdf2
8 changed files with 1006 additions and 280 deletions
|
|
@ -57,6 +57,7 @@ vi.mock('@mantine/core', async () => {
|
|||
|
||||
// Mock lucide-react icons
|
||||
vi.mock('lucide-react', () => ({
|
||||
ListOrdered: () => <div data-testid="icon-list-ordered" />,
|
||||
CircleCheck: () => <div data-testid="circle-check-icon" />,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -33,8 +33,12 @@ vi.mock('@mantine/core', async () => {
|
|||
{children}
|
||||
</div>
|
||||
),
|
||||
PopoverTarget: ({ children }) => <div data-testid="popover-target">{children}</div>,
|
||||
PopoverDropdown: ({ children }) => <div data-testid="popover-dropdown">{children}</div>,
|
||||
PopoverTarget: ({ children }) => (
|
||||
<div data-testid="popover-target">{children}</div>
|
||||
),
|
||||
PopoverDropdown: ({ children }) => (
|
||||
<div data-testid="popover-dropdown">{children}</div>
|
||||
),
|
||||
Indicator: ({ children, label, disabled, processing }) => (
|
||||
<div
|
||||
data-testid="indicator"
|
||||
|
|
@ -46,18 +50,53 @@ vi.mock('@mantine/core', async () => {
|
|||
</div>
|
||||
),
|
||||
ActionIcon: ({ children, onClick, 'aria-label': ariaLabel, ...props }) => (
|
||||
<button onClick={onClick} aria-label={ariaLabel} data-testid={`action-icon-${ariaLabel}`} {...props}>
|
||||
<button
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={`action-icon-${ariaLabel}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
ScrollAreaAutosize: ({ children }) => <div data-testid="scroll-area">{children}</div>,
|
||||
Badge: ({ children, ...props }) => <span data-testid="badge" {...props}>{children}</span>,
|
||||
Card: ({ children, ...props }) => <div data-testid="notification-card" {...props}>{children}</div>,
|
||||
ThemeIcon: ({ children, ...props }) => <div data-testid="theme-icon" {...props}>{children}</div>,
|
||||
Group: ({ children, ...props }) => <div data-testid="group" {...props}>{children}</div>,
|
||||
Stack: ({ children, ...props }) => <div data-testid="stack" {...props}>{children}</div>,
|
||||
Box: ({ children, ...props }) => <div data-testid="box" {...props}>{children}</div>,
|
||||
Text: ({ children, ...props }) => <span data-testid="text" {...props}>{children}</span>,
|
||||
ScrollAreaAutosize: ({ children }) => (
|
||||
<div data-testid="scroll-area">{children}</div>
|
||||
),
|
||||
Badge: ({ children, ...props }) => (
|
||||
<span data-testid="badge" {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
Card: ({ children, ...props }) => (
|
||||
<div data-testid="notification-card" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
ThemeIcon: ({ children, ...props }) => (
|
||||
<div data-testid="theme-icon" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Group: ({ children, ...props }) => (
|
||||
<div data-testid="group" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Stack: ({ children, ...props }) => (
|
||||
<div data-testid="stack" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Box: ({ children, ...props }) => (
|
||||
<div data-testid="box" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Text: ({ children, ...props }) => (
|
||||
<span data-testid="text" {...props}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
Button: ({ children, onClick, ...props }) => (
|
||||
<button onClick={onClick} data-testid="button" {...props}>
|
||||
{children}
|
||||
|
|
@ -84,14 +123,19 @@ vi.mock('@mantine/core', async () => {
|
|||
|
||||
// Mock lucide-react icons
|
||||
vi.mock('lucide-react', () => ({
|
||||
ListOrdered: () => <span data-testid="icon-list-ordered">ListOrdered</span>,
|
||||
Bell: () => <span data-testid="bell-icon">Bell</span>,
|
||||
Check: () => <span data-testid="check-icon">Check</span>,
|
||||
CheckCheck: () => <span data-testid="checkcheck-icon">CheckCheck</span>,
|
||||
Download: () => <span data-testid="download-icon">Download</span>,
|
||||
ExternalLink: () => <span data-testid="external-link-icon">ExternalLink</span>,
|
||||
ExternalLink: () => (
|
||||
<span data-testid="external-link-icon">ExternalLink</span>
|
||||
),
|
||||
Info: () => <span data-testid="info-icon">Info</span>,
|
||||
Settings: () => <span data-testid="settings-icon">Settings</span>,
|
||||
AlertTriangle: () => <span data-testid="alert-triangle-icon">AlertTriangle</span>,
|
||||
AlertTriangle: () => (
|
||||
<span data-testid="alert-triangle-icon">AlertTriangle</span>
|
||||
),
|
||||
Megaphone: () => <span data-testid="megaphone-icon">Megaphone</span>,
|
||||
X: () => <span data-testid="x-icon">X</span>,
|
||||
Eye: () => <span data-testid="eye-icon">Eye</span>,
|
||||
|
|
@ -279,8 +323,12 @@ describe('NotificationCenter', () => {
|
|||
});
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
NotificationUtils.getNotifications.mockRejectedValue(new Error('Network error'));
|
||||
const consoleError = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
NotificationUtils.getNotifications.mockRejectedValue(
|
||||
new Error('Network error')
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
|
|
@ -311,7 +359,7 @@ describe('NotificationCenter', () => {
|
|||
fireEvent.click(screen.getByLabelText('Notifications'));
|
||||
|
||||
const eyeButtons = screen.getAllByTestId(/action-icon-/);
|
||||
const toggleButton = eyeButtons.find(btn =>
|
||||
const toggleButton = eyeButtons.find((btn) =>
|
||||
btn.querySelector('[data-testid="eye-icon"]')
|
||||
);
|
||||
fireEvent.click(toggleButton);
|
||||
|
|
@ -376,7 +424,10 @@ describe('NotificationCenter', () => {
|
|||
fireEvent.click(xIcons[0].closest('button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(NotificationUtils.dismissNotification).toHaveBeenCalledWith(1, 'dismissed');
|
||||
expect(NotificationUtils.dismissNotification).toHaveBeenCalledWith(
|
||||
1,
|
||||
'dismissed'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -445,7 +496,10 @@ describe('NotificationCenter', () => {
|
|||
fireEvent.click(applyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(NotificationUtils.dismissNotification).toHaveBeenCalledWith(2, 'applied');
|
||||
expect(NotificationUtils.dismissNotification).toHaveBeenCalledWith(
|
||||
2,
|
||||
'applied'
|
||||
);
|
||||
expect(onSettingAction).toHaveBeenCalledWith(mockNotifications[1]);
|
||||
});
|
||||
});
|
||||
|
|
@ -458,7 +512,10 @@ describe('NotificationCenter', () => {
|
|||
fireEvent.click(ignoreButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(NotificationUtils.dismissNotification).toHaveBeenCalledWith(2, 'dismissed');
|
||||
expect(NotificationUtils.dismissNotification).toHaveBeenCalledWith(
|
||||
2,
|
||||
'dismissed'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -520,7 +577,7 @@ describe('NotificationCenter', () => {
|
|||
fireEvent.click(screen.getByLabelText('Notifications'));
|
||||
|
||||
const eyeButtons = screen.getAllByTestId(/action-icon-/);
|
||||
const toggleButton = eyeButtons.find(btn =>
|
||||
const toggleButton = eyeButtons.find((btn) =>
|
||||
btn.querySelector('[data-testid="eye-icon"]')
|
||||
);
|
||||
fireEvent.click(toggleButton);
|
||||
|
|
@ -599,7 +656,7 @@ describe('NotificationCenter', () => {
|
|||
fireEvent.click(screen.getByLabelText('Notifications'));
|
||||
|
||||
const eyeButtons = screen.getAllByTestId(/action-icon-/);
|
||||
const toggleButton = eyeButtons.find(btn =>
|
||||
const toggleButton = eyeButtons.find((btn) =>
|
||||
btn.querySelector('[data-testid="eye-icon"]')
|
||||
);
|
||||
fireEvent.click(toggleButton);
|
||||
|
|
@ -610,8 +667,12 @@ describe('NotificationCenter', () => {
|
|||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle dismiss notification errors', async () => {
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
NotificationUtils.dismissNotification.mockRejectedValue(new Error('API error'));
|
||||
const consoleError = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
NotificationUtils.dismissNotification.mockRejectedValue(
|
||||
new Error('API error')
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
fireEvent.click(screen.getByLabelText('Notifications'));
|
||||
|
|
@ -630,8 +691,12 @@ describe('NotificationCenter', () => {
|
|||
});
|
||||
|
||||
it('should handle dismiss all notifications errors', async () => {
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
NotificationUtils.dismissAllNotifications.mockRejectedValue(new Error('API error'));
|
||||
const consoleError = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
NotificationUtils.dismissAllNotifications.mockRejectedValue(
|
||||
new Error('API error')
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
fireEvent.click(screen.getByLabelText('Notifications'));
|
||||
|
|
@ -655,7 +720,9 @@ describe('NotificationCenter', () => {
|
|||
renderComponent();
|
||||
fireEvent.click(screen.getByLabelText('Notifications'));
|
||||
|
||||
const expectedDate = new Date('2024-01-01T10:00:00Z').toLocaleDateString();
|
||||
const expectedDate = new Date(
|
||||
'2024-01-01T10:00:00Z'
|
||||
).toLocaleDateString();
|
||||
expect(screen.getByText(expectedDate)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ vi.mock('../../utils', () => ({
|
|||
|
||||
// Mock lucide-react icons
|
||||
vi.mock('lucide-react', () => ({
|
||||
ListOrdered: () => <div data-testid="icon-list-ordered" />,
|
||||
Play: () => <div data-testid="play-icon" />,
|
||||
Copy: () => <div data-testid="copy-icon" />,
|
||||
}));
|
||||
|
|
@ -40,27 +41,60 @@ vi.mock('@mantine/core', async () => {
|
|||
if (!opened) return null;
|
||||
return (
|
||||
<div data-testid="modal" data-title={title} data-size={size}>
|
||||
<button onClick={onClose} data-testid="modal-close">Close</button>
|
||||
<button onClick={onClose} data-testid="modal-close">
|
||||
Close
|
||||
</button>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
Box: ({ children, ...props }) => <div data-testid="box" {...props}>{children}</div>,
|
||||
Box: ({ children, ...props }) => (
|
||||
<div data-testid="box" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Button: ({ children, onClick, disabled, ...props }) => (
|
||||
<button onClick={onClick} disabled={disabled} data-testid="button" {...props}>
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
data-testid="button"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Flex: ({ children, ...props }) => <div data-testid="flex" {...props}>{children}</div>,
|
||||
Group: ({ children, ...props }) => <div data-testid="group" {...props}>{children}</div>,
|
||||
Flex: ({ children, ...props }) => (
|
||||
<div data-testid="flex" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Group: ({ children, ...props }) => (
|
||||
<div data-testid="group" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Image: ({ src, alt, ...props }) => (
|
||||
<img src={src} alt={alt} data-testid="image" {...props} />
|
||||
),
|
||||
Text: ({ children, ...props }) => <div data-testid="text" {...props}>{children}</div>,
|
||||
Title: ({ children, order, ...props }) => (
|
||||
<div data-testid="title" data-order={order} {...props}>{children}</div>
|
||||
Text: ({ children, ...props }) => (
|
||||
<div data-testid="text" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Select: ({ value, onChange, data, label, placeholder, disabled, ...props }) => (
|
||||
Title: ({ children, order, ...props }) => (
|
||||
<div data-testid="title" data-order={order} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Select: ({
|
||||
value,
|
||||
onChange,
|
||||
data,
|
||||
label,
|
||||
placeholder,
|
||||
disabled,
|
||||
...props
|
||||
}) => (
|
||||
<div data-testid="select" data-label={label}>
|
||||
<select
|
||||
value={value || ''}
|
||||
|
|
@ -77,39 +111,76 @@ vi.mock('@mantine/core', async () => {
|
|||
</select>
|
||||
</div>
|
||||
),
|
||||
Badge: ({ children, ...props }) => <a data-testid="badge" {...props}>{children}</a>,
|
||||
Badge: ({ children, ...props }) => (
|
||||
<a data-testid="badge" {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
Loader: (props) => <div data-testid="loader" {...props} />,
|
||||
Stack: ({ children, ...props }) => <div data-testid="stack" {...props}>{children}</div>,
|
||||
Stack: ({ children, ...props }) => (
|
||||
<div data-testid="stack" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
ActionIcon: ({ children, onClick, disabled, ...props }) => (
|
||||
<button onClick={onClick} disabled={disabled} data-testid="action-icon" {...props}>
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
data-testid="action-icon"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Tabs: ({ children, value, onChange, ...props }) => (
|
||||
<div data-testid="tabs" data-value={value} {...props}>
|
||||
<div onClick={(e) => {
|
||||
const tab = e.target.closest('[data-tab-value]');
|
||||
if (tab) onChange?.(tab.dataset.tabValue);
|
||||
}}>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
const tab = e.target.closest('[data-tab-value]');
|
||||
if (tab) onChange?.(tab.dataset.tabValue);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
TabsList: ({ children }) => <div data-testid="tabs-list">{children}</div>,
|
||||
TabsTab: ({ children, value }) => (
|
||||
<button data-testid="tabs-tab" data-tab-value={value}>{children}</button>
|
||||
<button data-testid="tabs-tab" data-tab-value={value}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
TabsPanel: ({ children, value }) => (
|
||||
<div data-testid="tabs-panel" data-value={value}>{children}</div>
|
||||
<div data-testid="tabs-panel" data-value={value}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Table: ({ children, ...props }) => (
|
||||
<table data-testid="table" {...props}>
|
||||
{children}
|
||||
</table>
|
||||
),
|
||||
TableThead: ({ children }) => (
|
||||
<thead data-testid="table-thead">{children}</thead>
|
||||
),
|
||||
TableTbody: ({ children }) => (
|
||||
<tbody data-testid="table-tbody">{children}</tbody>
|
||||
),
|
||||
Table: ({ children, ...props }) => <table data-testid="table" {...props}>{children}</table>,
|
||||
TableThead: ({ children }) => <thead data-testid="table-thead">{children}</thead>,
|
||||
TableTbody: ({ children }) => <tbody data-testid="table-tbody">{children}</tbody>,
|
||||
TableTr: ({ children, onClick, ...props }) => (
|
||||
<tr onClick={onClick} data-testid="table-tr" {...props}>{children}</tr>
|
||||
<tr onClick={onClick} data-testid="table-tr" {...props}>
|
||||
{children}
|
||||
</tr>
|
||||
),
|
||||
TableTh: ({ children, ...props }) => (
|
||||
<th data-testid="table-th" {...props}>
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
TableTd: ({ children, ...props }) => (
|
||||
<td data-testid="table-td" {...props}>
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
TableTh: ({ children, ...props }) => <th data-testid="table-th" {...props}>{children}</th>,
|
||||
TableTd: ({ children, ...props }) => <td data-testid="table-td" {...props}>{children}</td>,
|
||||
Divider: (props) => <hr data-testid="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(
|
||||
<SeriesModal series={mockDetailedSeries} opened={true} onClose={vi.fn()} />
|
||||
<SeriesModal
|
||||
series={mockDetailedSeries}
|
||||
opened={true}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<SeriesModal series={mockDetailedSeries} opened={true} onClose={vi.fn()} />
|
||||
<SeriesModal
|
||||
series={mockDetailedSeries}
|
||||
opened={true}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
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', () => {
|
|||
<SeriesModal series={mockSeries} opened={true} onClose={vi.fn()} />
|
||||
);
|
||||
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -109,13 +109,12 @@ vi.mock('@mantine/core', async () => ({
|
|||
{children}
|
||||
</span>
|
||||
),
|
||||
Tooltip: ({ children, label }) => (
|
||||
<div data-tooltip={label}>{children}</div>
|
||||
),
|
||||
Tooltip: ({ children, label }) => <div data-tooltip={label}>{children}</div>,
|
||||
}));
|
||||
|
||||
// ── lucide-react ───────────────────────────────────────────────────────────────
|
||||
vi.mock('lucide-react', () => ({
|
||||
ListOrdered: () => <svg data-testid="icon-list-ordered" />,
|
||||
AlertTriangle: () => <svg data-testid="icon-alert-triangle" />,
|
||||
Plus: () => <svg data-testid="icon-plus" />,
|
||||
Square: () => <svg data-testid="icon-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(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
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(
|
||||
<RecordingCard
|
||||
recording={makeRecording({ custom_properties: { status: 'completed', program: {} } })}
|
||||
recording={makeRecording({
|
||||
custom_properties: { status: 'completed', program: {} },
|
||||
})}
|
||||
channel={makeChannel()}
|
||||
/>
|
||||
);
|
||||
|
|
@ -269,7 +295,9 @@ describe('RecordingCard', () => {
|
|||
|
||||
it('renders channel info', () => {
|
||||
setupMocks();
|
||||
render(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
expect(screen.getByText('501 • HBO')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -281,27 +309,35 @@ describe('RecordingCard', () => {
|
|||
|
||||
it('shows description via RecordingSynopsis for non-series completed recording', () => {
|
||||
setupMocks();
|
||||
render(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
expect(screen.getByTestId('recording-synopsis')).toBeInTheDocument();
|
||||
expect(screen.getByText('A test description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows sub_title when present', () => {
|
||||
setupMocks();
|
||||
render(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
expect(screen.getByText('Pilot')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows season/episode label when getSeasonLabel returns a value', () => {
|
||||
setupMocks();
|
||||
vi.mocked(RecordingCardUtils.getSeasonLabel).mockReturnValue('S01E02');
|
||||
render(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
expect(screen.getByText('S01E02')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the poster image', () => {
|
||||
setupMocks();
|
||||
render(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
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(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
|
|
@ -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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
|
|
@ -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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
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(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
expect(screen.queryByText('Watch Live')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -523,7 +570,9 @@ describe('RecordingCard', () => {
|
|||
|
||||
it('shows "Watch" button for a completed recording', () => {
|
||||
setupMocks();
|
||||
render(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
|
|
@ -540,7 +592,9 @@ describe('RecordingCard', () => {
|
|||
|
||||
it('calls showVideo with vod params when Watch is clicked', () => {
|
||||
const { mockShowVideo } = setupMocks();
|
||||
render(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
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(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
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(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
|
|
@ -606,20 +667,31 @@ describe('RecordingCard', () => {
|
|||
|
||||
it('calls runComSkip and shows notification on success', async () => {
|
||||
setupMocks();
|
||||
render(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
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(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
|
|
@ -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(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
|
||||
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(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
|
||||
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(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
|
||||
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(<RecordingCard recording={makeRecording()} channel={makeChannel()} />);
|
||||
render(
|
||||
<RecordingCard recording={makeRecording()} channel={makeChannel()} />
|
||||
);
|
||||
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
|
||||
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(<RecordingCard recording={recording} channel={makeChannel()} />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,7 +30,13 @@ vi.mock('@mantine/core', async () => ({
|
|||
),
|
||||
Stack: ({ children, gap }) => <div data-testid="stack">{children}</div>,
|
||||
Text: ({ children, size, fw, c, lineClamp, style }) => (
|
||||
<span data-testid="text" data-size={size} data-fw={fw} data-color={c} style={style}>
|
||||
<span
|
||||
data-testid="text"
|
||||
data-size={size}
|
||||
data-fw={fw}
|
||||
data-color={c}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
|
|
@ -38,6 +44,7 @@ vi.mock('@mantine/core', async () => ({
|
|||
|
||||
// ── lucide-react ───────────────────────────────────────────────────────────────
|
||||
vi.mock('lucide-react', () => ({
|
||||
ListOrdered: () => <svg data-testid="icon-list-ordered" />,
|
||||
Calendar: () => <svg data-testid="icon-calendar" />,
|
||||
Play: () => <svg data-testid="icon-play" />,
|
||||
Star: () => <svg data-testid="icon-star" />,
|
||||
|
|
@ -78,7 +85,12 @@ describe('SeriesCard', () => {
|
|||
});
|
||||
|
||||
it('renders a fallback image when poster_url is missing', () => {
|
||||
render(<SeriesCard series={makeSeries({ poster_url: null })} onClick={vi.fn()} />);
|
||||
render(
|
||||
<SeriesCard
|
||||
series={makeSeries({ poster_url: null })}
|
||||
onClick={vi.fn()}
|
||||
/>
|
||||
);
|
||||
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(<SeriesCard series={makeSeries({ logo: { url: null } })} onClick={vi.fn()} />);
|
||||
render(
|
||||
<SeriesCard
|
||||
series={makeSeries({ logo: { url: null } })}
|
||||
onClick={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('icon-play')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -119,27 +136,52 @@ describe('SeriesCard', () => {
|
|||
|
||||
describe('optional fields', () => {
|
||||
it('does not crash when year is missing', () => {
|
||||
render(<SeriesCard series={makeSeries({ year: undefined })} onClick={vi.fn()} />);
|
||||
render(
|
||||
<SeriesCard
|
||||
series={makeSeries({ year: undefined })}
|
||||
onClick={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('series-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not crash when rating is missing', () => {
|
||||
render(<SeriesCard series={makeSeries({ rating: undefined })} onClick={vi.fn()} />);
|
||||
render(
|
||||
<SeriesCard
|
||||
series={makeSeries({ rating: undefined })}
|
||||
onClick={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('series-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not crash when genre is missing', () => {
|
||||
render(<SeriesCard series={makeSeries({ genre: undefined })} onClick={vi.fn()} />);
|
||||
render(
|
||||
<SeriesCard
|
||||
series={makeSeries({ genre: undefined })}
|
||||
onClick={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('series-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not crash when description is missing', () => {
|
||||
render(<SeriesCard series={makeSeries({ description: undefined })} onClick={vi.fn()} />);
|
||||
render(
|
||||
<SeriesCard
|
||||
series={makeSeries({ description: undefined })}
|
||||
onClick={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('series-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not crash when seasons is missing', () => {
|
||||
render(<SeriesCard series={makeSeries({ seasons: undefined })} onClick={vi.fn()} />);
|
||||
render(
|
||||
<SeriesCard
|
||||
series={makeSeries({ seasons: undefined })}
|
||||
onClick={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('series-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -161,4 +203,4 @@ describe('SeriesCard', () => {
|
|||
expect(onClick).toHaveBeenCalledWith(series);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -95,7 +95,12 @@ vi.mock('@mantine/core', () => ({
|
|||
Center: ({ children }) => <div data-testid="center">{children}</div>,
|
||||
Group: ({ children }) => <div data-testid="group">{children}</div>,
|
||||
Progress: ({ value, size, color }) => (
|
||||
<div data-testid="progress" data-value={value} data-size={size} data-color={color} />
|
||||
<div
|
||||
data-testid="progress"
|
||||
data-value={value}
|
||||
data-size={size}
|
||||
data-color={color}
|
||||
/>
|
||||
),
|
||||
Select: ({ value, onChange, label, data, disabled, placeholder }) => (
|
||||
<select
|
||||
|
|
@ -129,9 +134,7 @@ vi.mock('@mantine/core', () => ({
|
|||
{children}
|
||||
</span>
|
||||
),
|
||||
Tooltip: ({ children, label }) => (
|
||||
<div data-tooltip={label}>{children}</div>
|
||||
),
|
||||
Tooltip: ({ children, label }) => <div data-tooltip={label}>{children}</div>,
|
||||
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: () => <svg data-testid="icon-list-ordered" />,
|
||||
Play: () => <svg data-testid="icon-play" />,
|
||||
Database: () => <svg data-testid="icon-database" />,
|
||||
LayoutGrid: () => <svg data-testid="icon-layout-grid" />,
|
||||
Settings: () => <svg data-testid="icon-settings" />,
|
||||
ChartLine: () => <svg data-testid="icon-chart-line" />,
|
||||
Video: () => <svg data-testid="icon-video" />,
|
||||
PlugZap: () => <svg data-testid="icon-plug-zap" />,
|
||||
User: () => <svg data-testid="icon-user" />,
|
||||
FileImage: () => <svg data-testid="icon-file-image" />,
|
||||
Webhook: () => <svg data-testid="icon-webhook" />,
|
||||
Logs: () => <svg data-testid="icon-logs" />,
|
||||
Blocks: () => <svg data-testid="icon-blocks" />,
|
||||
MonitorCog: () => <svg data-testid="icon-monitor-cog" />,
|
||||
// StreamConnectionCard-specific icons
|
||||
ChevronDown: () => <svg data-testid="icon-chevron-down" />,
|
||||
ChevronRight: () => <svg data-testid="icon-chevron-right" />,
|
||||
CirclePlay: () => <svg data-testid="icon-circle-play" />,
|
||||
|
|
@ -149,7 +168,6 @@ vi.mock('lucide-react', () => ({
|
|||
SquareX: () => <svg data-testid="icon-square-x" />,
|
||||
Timer: () => <svg data-testid="icon-timer" />,
|
||||
Users: () => <svg data-testid="icon-users" />,
|
||||
Video: () => <svg data-testid="icon-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(<StreamConnectionCard {...defaultProps()} />);
|
||||
const { container } = render(
|
||||
<StreamConnectionCard {...defaultProps()} />
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders nothing when pathname is /channels', () => {
|
||||
setupLocation('/channels');
|
||||
const { container } = render(<StreamConnectionCard {...defaultProps()} />);
|
||||
const { container } = render(
|
||||
<StreamConnectionCard {...defaultProps()} />
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
|
|
@ -337,7 +359,9 @@ describe('StreamConnectionCard', () => {
|
|||
vi.mocked(getStreamsByIds).mockResolvedValue([]);
|
||||
render(
|
||||
<StreamConnectionCard
|
||||
{...defaultProps({ channel: makeChannel({ name: '', stream_id: null }) })}
|
||||
{...defaultProps({
|
||||
channel: makeChannel({ name: '', stream_id: null }),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
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(<StreamConnectionCard {...defaultProps({ currentProgram: null })} />);
|
||||
render(
|
||||
<StreamConnectionCard {...defaultProps({ currentProgram: null })} />
|
||||
);
|
||||
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(
|
||||
<StreamConnectionCard
|
||||
{...defaultProps({
|
||||
currentProgram: makeCurrentProgram({ start_time: null, end_time: null }),
|
||||
currentProgram: makeCurrentProgram({
|
||||
start_time: null,
|
||||
end_time: null,
|
||||
}),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
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(<StreamConnectionCard {...defaultProps()} />);
|
||||
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(<StreamConnectionCard {...defaultProps()} />);
|
||||
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(<StreamConnectionCard {...defaultProps()} />);
|
||||
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(<StreamConnectionCard {...defaultProps()} />);
|
||||
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(<StreamConnectionCard {...defaultProps()} />);
|
||||
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(<StreamConnectionCard {...defaultProps()} />);
|
||||
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(<StreamConnectionCard {...defaultProps()} />);
|
||||
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(
|
||||
<StreamConnectionCard {...defaultProps({ channelsByUUID: {} })} />
|
||||
|
|
@ -837,4 +901,4 @@ describe('StreamConnectionCard', () => {
|
|||
expect(screen.getAllByText('0 Kbps').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,12 +10,22 @@ vi.mock('../../../utils/cards/VODCardUtils.js', () => ({
|
|||
// ── Mantine core ──────────────────────────────────────────────────────────────
|
||||
vi.mock('@mantine/core', () => ({
|
||||
ActionIcon: ({ children, onClick, variant, size }) => (
|
||||
<button data-testid="action-icon" data-variant={variant} data-size={size} onClick={onClick}>
|
||||
<button
|
||||
data-testid="action-icon"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Badge: ({ children, color, variant, size }) => (
|
||||
<span data-testid="badge" data-color={color} data-variant={variant} data-size={size}>
|
||||
<span
|
||||
data-testid="badge"
|
||||
data-color={color}
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
|
|
@ -37,14 +47,27 @@ vi.mock('@mantine/core', () => ({
|
|||
{children}
|
||||
</div>
|
||||
),
|
||||
CardSection: ({ children }) => <div data-testid="card-section">{children}</div>,
|
||||
CardSection: ({ children }) => (
|
||||
<div data-testid="card-section">{children}</div>
|
||||
),
|
||||
Group: ({ children, justify, gap, wrap }) => (
|
||||
<div data-testid="group" data-justify={justify} data-gap={gap} data-wrap={wrap}>
|
||||
<div
|
||||
data-testid="group"
|
||||
data-justify={justify}
|
||||
data-gap={gap}
|
||||
data-wrap={wrap}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Image: ({ src, alt, height, fallbackSrc, fit }) => (
|
||||
<img src={src} alt={alt} data-height={height} data-fallback={fallbackSrc} data-fit={fit} />
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
data-height={height}
|
||||
data-fallback={fallbackSrc}
|
||||
data-fit={fit}
|
||||
/>
|
||||
),
|
||||
Stack: ({ children, spacing, gap, p }) => (
|
||||
<div data-testid="stack" data-spacing={spacing} data-gap={gap} data-p={p}>
|
||||
|
|
@ -68,6 +91,7 @@ vi.mock('@mantine/core', () => ({
|
|||
|
||||
// ── lucide-react ──────────────────────────────────────────────────────────────
|
||||
vi.mock('lucide-react', () => ({
|
||||
ListOrdered: () => <svg data-testid="icon-list-ordered" />,
|
||||
Calendar: () => <svg data-testid="icon-calendar" />,
|
||||
Clock: () => <svg data-testid="icon-clock" />,
|
||||
Play: () => <svg data-testid="icon-play" />,
|
||||
|
|
@ -75,7 +99,10 @@ vi.mock('lucide-react', () => ({
|
|||
}));
|
||||
|
||||
// ── Imports after mocks ───────────────────────────────────────────────────────
|
||||
import { formatDuration, getSeasonLabel } from '../../../utils/cards/VODCardUtils.js';
|
||||
import {
|
||||
formatDuration,
|
||||
getSeasonLabel,
|
||||
} from '../../../utils/cards/VODCardUtils.js';
|
||||
import VODCard from '../VODCard';
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
|
@ -113,7 +140,9 @@ const makeEpisode = (overrides = {}) => ({
|
|||
describe('VODCard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(formatDuration).mockImplementation((mins) => (mins ? `${mins}m` : null));
|
||||
vi.mocked(formatDuration).mockImplementation((mins) =>
|
||||
mins ? `${mins}m` : null
|
||||
);
|
||||
vi.mocked(getSeasonLabel).mockReturnValue('S01E02');
|
||||
});
|
||||
|
||||
|
|
@ -193,7 +222,9 @@ describe('VODCard', () => {
|
|||
|
||||
it('renders the season label via getSeasonLabel', () => {
|
||||
render(<VODCard vod={makeEpisode()} onClick={vi.fn()} />);
|
||||
expect(getSeasonLabel).toHaveBeenCalledWith(expect.objectContaining({ type: 'episode' }));
|
||||
expect(getSeasonLabel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'episode' })
|
||||
);
|
||||
expect(screen.getByText(/S01E02/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -230,7 +261,9 @@ describe('VODCard', () => {
|
|||
});
|
||||
|
||||
it('does not render an img tag when logo.url is empty string', () => {
|
||||
render(<VODCard vod={makeMovie({ logo: { url: '' } })} onClick={vi.fn()} />);
|
||||
render(
|
||||
<VODCard vod={makeMovie({ logo: { url: '' } })} onClick={vi.fn()} />
|
||||
);
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -257,8 +290,9 @@ describe('VODCard', () => {
|
|||
it('does not render genre when absent', () => {
|
||||
render(<VODCard vod={makeMovie({ genre: null })} onClick={vi.fn()} />);
|
||||
const badges = screen.queryAllByTestId('badge');
|
||||
const dimmedBadges = badges.filter((badge) =>
|
||||
badge.getAttribute('data-color') === 'dimmed');
|
||||
const dimmedBadges = badges.filter(
|
||||
(badge) => badge.getAttribute('data-color') === 'dimmed'
|
||||
);
|
||||
expect(dimmedBadges.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -291,4 +325,4 @@ describe('VODCard', () => {
|
|||
expect(onClick).toHaveBeenCalledWith(vod);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ vi.mock('../../../utils/dateTimeUtils.js', () => ({
|
|||
vi.mock('../../../utils/cards/VodConnectionCardUtils.js', () => ({
|
||||
calculateConnectionDuration: vi.fn(() => '5m 30s'),
|
||||
calculateConnectionStartTime: vi.fn(() => 'Jan 1 2024 10:00 AM'),
|
||||
calculateProgress: vi.fn(() => ({ totalTime: 0, currentTime: 0, percentage: 0 })),
|
||||
calculateProgress: vi.fn(() => ({
|
||||
totalTime: 0,
|
||||
currentTime: 0,
|
||||
percentage: 0,
|
||||
})),
|
||||
formatDuration: vi.fn((secs) => (secs ? `${secs}s` : null)),
|
||||
formatTime: vi.fn((secs) => `${secs}s`),
|
||||
getEpisodeDisplayTitle: vi.fn(() => 'S01E02 — Pilot'),
|
||||
|
|
@ -28,42 +32,82 @@ vi.mock('../../../images/logo.png', () => ({ default: 'default-logo.png' }));
|
|||
// ── Mantine core ──────────────────────────────────────────────────────────────
|
||||
vi.mock('@mantine/core', () => ({
|
||||
ActionIcon: ({ children, onClick, color, variant }) => (
|
||||
<button data-testid="action-icon" data-color={color} data-variant={variant} onClick={onClick}>
|
||||
<button
|
||||
data-testid="action-icon"
|
||||
data-color={color}
|
||||
data-variant={variant}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Badge: ({ children, color, variant, size }) => (
|
||||
<span data-testid="badge" data-color={color} data-variant={variant} data-size={size}>
|
||||
<span
|
||||
data-testid="badge"
|
||||
data-color={color}
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
Box: ({ children, style }) => <div data-testid="box" style={style}>{children}</div>,
|
||||
Card: ({ children, shadow, style }) => ( // remove padding, radius, withBorder
|
||||
Box: ({ children, style }) => (
|
||||
<div data-testid="box" style={style}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Card: (
|
||||
{ children, shadow, style } // remove padding, radius, withBorder
|
||||
) => (
|
||||
<div data-testid="vod-connection-card" data-shadow={shadow} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Center: ({ children }) => <div data-testid="center">{children}</div>,
|
||||
Flex: ({ children, justify }) => ( // remove align
|
||||
<div data-testid="flex" data-justify={justify}>{children}</div>
|
||||
Flex: (
|
||||
{ children, justify } // remove align
|
||||
) => (
|
||||
<div data-testid="flex" data-justify={justify}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Group: ({ children, justify, onClick, style }) => ( // remove gap, align, p
|
||||
<div data-testid="group" data-justify={justify} onClick={onClick} style={style}>
|
||||
Group: (
|
||||
{ children, justify, onClick, style } // remove gap, align, p
|
||||
) => (
|
||||
<div
|
||||
data-testid="group"
|
||||
data-justify={justify}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Progress: ({ value, size, color }) => (
|
||||
<div data-testid="progress" data-value={value} data-size={size} data-color={color} />
|
||||
<div
|
||||
data-testid="progress"
|
||||
data-value={value}
|
||||
data-size={size}
|
||||
data-color={color}
|
||||
/>
|
||||
),
|
||||
Stack: ({ children, gap, pos, mt }) => (
|
||||
<div data-testid="stack" data-gap={gap} data-pos={pos} data-mt={mt}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Text: ({ children, size, c, fw, color }) => ( // remove ff, ta
|
||||
<span data-testid="text" data-size={size} data-c={c} data-fw={fw} data-color={color}>
|
||||
{children}
|
||||
</span>
|
||||
Text: (
|
||||
{ children, size, c, fw, color } // remove ff, ta
|
||||
) => (
|
||||
<span
|
||||
data-testid="text"
|
||||
data-size={size}
|
||||
data-c={c}
|
||||
data-fw={fw}
|
||||
data-color={color}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
Tooltip: ({ children, label }) => (
|
||||
<div data-testid="tooltip" data-label={label}>
|
||||
|
|
@ -74,6 +118,7 @@ vi.mock('@mantine/core', () => ({
|
|||
|
||||
// ── lucide-react ──────────────────────────────────────────────────────────────
|
||||
vi.mock('lucide-react', () => ({
|
||||
ListOrdered: () => <svg data-testid="icon-list-ordered" />,
|
||||
ChevronDown: ({ size, style }) => (
|
||||
<svg data-testid="icon-chevron-down" data-size={size} style={style} />
|
||||
),
|
||||
|
|
@ -100,12 +145,13 @@ import VodConnectionCard from '../VodConnectionCard';
|
|||
|
||||
const makeConnection = (overrides = {}) => ({
|
||||
client_ip: '192.168.1.100',
|
||||
client_id: 'client-abc-123', // needed for stopVODClient
|
||||
client_id: 'client-abc-123', // needed for stopVODClient
|
||||
user_agent: 'Plex/1.0',
|
||||
connected_at: '2024-01-01T10:00:00Z',
|
||||
duration: 330,
|
||||
bytes_sent: 1048576,
|
||||
m3u_profile: { // M3U profile lives on connection
|
||||
m3u_profile: {
|
||||
// M3U profile lives on connection
|
||||
account_name: 'Main Account',
|
||||
profile_name: 'Main M3U',
|
||||
},
|
||||
|
|
@ -161,14 +207,22 @@ describe('VodConnectionCard', () => {
|
|||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(useDateTimeFormat).mockReturnValue({ fullDateTimeFormat: 'MM/DD/YYYY h:mm A' });
|
||||
vi.mocked(calculateProgress).mockReturnValue({ totalTime: 0, currentTime: 0, percentage: 0 });
|
||||
vi.mocked(useDateTimeFormat).mockReturnValue({
|
||||
fullDateTimeFormat: 'MM/DD/YYYY h:mm A',
|
||||
});
|
||||
vi.mocked(calculateProgress).mockReturnValue({
|
||||
totalTime: 0,
|
||||
currentTime: 0,
|
||||
percentage: 0,
|
||||
});
|
||||
vi.mocked(getMovieDisplayTitle).mockReturnValue('Test Movie (2022)');
|
||||
vi.mocked(getMovieSubtitle).mockReturnValue(['120m', 'Action']);
|
||||
vi.mocked(getEpisodeDisplayTitle).mockReturnValue('S01E02 — Pilot');
|
||||
vi.mocked(getEpisodeSubtitle).mockReturnValue(['Test Series', 'Season 1']);
|
||||
vi.mocked(calculateConnectionDuration).mockReturnValue('5m 30s');
|
||||
vi.mocked(calculateConnectionStartTime).mockReturnValue('Jan 1 2024 10:00 AM');
|
||||
vi.mocked(calculateConnectionStartTime).mockReturnValue(
|
||||
'Jan 1 2024 10:00 AM'
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -179,39 +233,69 @@ describe('VodConnectionCard', () => {
|
|||
|
||||
describe('rendering', () => {
|
||||
it('renders the card element', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('vod-connection-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the poster image when logo_url is present', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
expect(screen.getByRole('img')).toHaveAttribute('src', 'http://example.com/poster.jpg');
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole('img')).toHaveAttribute(
|
||||
'src',
|
||||
'http://example.com/poster.jpg'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the default logo when logo_url is absent', () => {
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent({ content_metadata: { logo_url: null } })}
|
||||
vodContent={makeMovieContent({
|
||||
content_metadata: { logo_url: null },
|
||||
})}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole('img')).toHaveAttribute('src', 'default-logo.png');
|
||||
expect(screen.getByRole('img')).toHaveAttribute(
|
||||
'src',
|
||||
'default-logo.png'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the stop client button', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('icon-square-x')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the client IP in the connection section', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('192.168.1.100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "Unknown IP" when client_ip is absent', () => {
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent({ individual_connection: makeConnection({ client_ip: null }) })}
|
||||
vodContent={makeMovieContent({
|
||||
individual_connection: makeConnection({ client_ip: null }),
|
||||
})}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
|
@ -219,14 +303,22 @@ describe('VodConnectionCard', () => {
|
|||
});
|
||||
|
||||
it('renders "Hide Details" / "Show Details" toggle text', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Show Details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render client section when no connection is present', () => {
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent({ individual_connection: null, connections: null })}
|
||||
vodContent={makeMovieContent({
|
||||
individual_connection: null,
|
||||
connections: null,
|
||||
})}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
|
@ -239,29 +331,50 @@ describe('VodConnectionCard', () => {
|
|||
describe('movie content', () => {
|
||||
it('calls getMovieDisplayTitle with vodContent', () => {
|
||||
const vodContent = makeMovieContent();
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
expect(getMovieDisplayTitle).toHaveBeenCalledWith(vodContent);
|
||||
});
|
||||
|
||||
it('renders the movie display title', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Test Movie (2022)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls getMovieSubtitle with metadata', () => {
|
||||
const vodContent = makeMovieContent();
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
expect(getMovieSubtitle).toHaveBeenCalledWith(vodContent.content_metadata);
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
expect(getMovieSubtitle).toHaveBeenCalledWith(
|
||||
vodContent.content_metadata
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the movie subtitle parts', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/120m/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Action/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not call getEpisodeDisplayTitle for a movie', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(getEpisodeDisplayTitle).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -271,29 +384,52 @@ describe('VodConnectionCard', () => {
|
|||
describe('episode content', () => {
|
||||
it('calls getEpisodeDisplayTitle with vodContent', () => {
|
||||
const vodContent = makeEpisodeContent();
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
expect(getEpisodeDisplayTitle).toHaveBeenCalledWith(vodContent.content_metadata);
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
expect(getEpisodeDisplayTitle).toHaveBeenCalledWith(
|
||||
vodContent.content_metadata
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the episode display title', () => {
|
||||
render(<VodConnectionCard vodContent={makeEpisodeContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeEpisodeContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('S01E02 — Pilot')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls getEpisodeSubtitle with metadata', () => {
|
||||
const vodContent = makeEpisodeContent();
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
expect(getEpisodeSubtitle).toHaveBeenCalledWith(vodContent.content_metadata);
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
expect(getEpisodeSubtitle).toHaveBeenCalledWith(
|
||||
vodContent.content_metadata
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the episode subtitle parts', () => {
|
||||
render(<VodConnectionCard vodContent={makeEpisodeContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeEpisodeContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/Test Series/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Season 1/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not call getMovieDisplayTitle for an episode', () => {
|
||||
render(<VodConnectionCard vodContent={makeEpisodeContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeEpisodeContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(getMovieDisplayTitle).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -302,12 +438,22 @@ describe('VodConnectionCard', () => {
|
|||
|
||||
describe('unknown content type', () => {
|
||||
it('renders content_name as fallback title', () => {
|
||||
render(<VodConnectionCard vodContent={makeUnknownContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeUnknownContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Some Stream')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not call getMovieDisplayTitle or getEpisodeDisplayTitle', () => {
|
||||
render(<VodConnectionCard vodContent={makeUnknownContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeUnknownContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(getMovieDisplayTitle).not.toHaveBeenCalled();
|
||||
expect(getEpisodeDisplayTitle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -315,7 +461,12 @@ describe('VodConnectionCard', () => {
|
|||
it('renders no subtitle when subtitle parts are empty', () => {
|
||||
vi.mocked(getMovieSubtitle).mockReturnValue([]);
|
||||
vi.mocked(getEpisodeSubtitle).mockReturnValue([]);
|
||||
render(<VodConnectionCard vodContent={makeUnknownContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeUnknownContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
// No " • " separator rendered for empty subtitle
|
||||
expect(screen.queryByText(' • ')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -326,7 +477,9 @@ describe('VodConnectionCard', () => {
|
|||
describe('connection source fallback', () => {
|
||||
it('uses individual_connection when present', () => {
|
||||
const vodContent = makeMovieContent();
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
expect(screen.getByText('192.168.1.100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -336,7 +489,9 @@ describe('VodConnectionCard', () => {
|
|||
individual_connection: null,
|
||||
connections: [connection],
|
||||
});
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
expect(screen.getByText('10.0.0.1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -345,7 +500,12 @@ describe('VodConnectionCard', () => {
|
|||
|
||||
describe('M3U profile', () => {
|
||||
it('renders M3U profile name when present', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Main M3U')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -353,7 +513,9 @@ describe('VodConnectionCard', () => {
|
|||
const vodContent = makeMovieContent({
|
||||
individual_connection: makeConnection({ m3u_profile: null }),
|
||||
});
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
expect(screen.queryByText('Main M3U')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -364,14 +526,24 @@ describe('VodConnectionCard', () => {
|
|||
it('calls stopVODClient with the connection when stop button is clicked', () => {
|
||||
const stopVODClient = vi.fn();
|
||||
const vodContent = makeMovieContent();
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={stopVODClient} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={vodContent}
|
||||
stopVODClient={stopVODClient}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('icon-square-x').closest('button'));
|
||||
expect(stopVODClient).toHaveBeenCalledWith('client-abc-123');
|
||||
});
|
||||
|
||||
it('calls stopVODClient once per click', () => {
|
||||
const stopVODClient = vi.fn();
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={stopVODClient} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={stopVODClient}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('icon-square-x').closest('button'));
|
||||
fireEvent.click(screen.getByTestId('icon-square-x').closest('button'));
|
||||
expect(stopVODClient).toHaveBeenCalledTimes(2);
|
||||
|
|
@ -382,30 +554,58 @@ describe('VodConnectionCard', () => {
|
|||
|
||||
describe('client expand/collapse', () => {
|
||||
it('starts collapsed (Show Details visible)', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Show Details')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Hide Details')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('expands to show client details on header click', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
const header = screen.getByText('Show Details').closest('[data-testid="group"]');
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
const header = screen
|
||||
.getByText('Show Details')
|
||||
.closest('[data-testid="group"]');
|
||||
fireEvent.click(header);
|
||||
expect(screen.getByText('Hide Details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows user agent in expanded details', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
const header = screen.getByText('Show Details').closest('[data-testid="group"]');
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
const header = screen
|
||||
.getByText('Show Details')
|
||||
.closest('[data-testid="group"]');
|
||||
fireEvent.click(header);
|
||||
expect(screen.getByText('Plex/1.0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('collapses back when header is clicked again', () => {
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
const header = screen.getByText('Show Details').closest('[data-testid="group"]');
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
const header = screen
|
||||
.getByText('Show Details')
|
||||
.closest('[data-testid="group"]');
|
||||
fireEvent.click(header);
|
||||
fireEvent.click(screen.getByText('Hide Details').closest('[data-testid="group"]'));
|
||||
fireEvent.click(
|
||||
screen.getByText('Hide Details').closest('[data-testid="group"]')
|
||||
);
|
||||
expect(screen.getByText('Show Details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -413,8 +613,12 @@ describe('VodConnectionCard', () => {
|
|||
const vodContent = makeMovieContent({
|
||||
individual_connection: makeConnection({ user_agent: 'Unknown' }),
|
||||
});
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
const header = screen.getByText('Show Details').closest('[data-testid="group"]');
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
const header = screen
|
||||
.getByText('Show Details')
|
||||
.closest('[data-testid="group"]');
|
||||
fireEvent.click(header);
|
||||
expect(screen.queryByText('Unknown')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -423,8 +627,12 @@ describe('VodConnectionCard', () => {
|
|||
const vodContent = makeMovieContent({
|
||||
individual_connection: makeConnection({ user_agent: null }),
|
||||
});
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
const header = screen.getByText('Show Details').closest('[data-testid="group"]');
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
const header = screen
|
||||
.getByText('Show Details')
|
||||
.closest('[data-testid="group"]');
|
||||
fireEvent.click(header);
|
||||
// "Plex/1.0" should not appear
|
||||
expect(screen.queryByText('Plex/1.0')).not.toBeInTheDocument();
|
||||
|
|
@ -434,8 +642,12 @@ describe('VodConnectionCard', () => {
|
|||
const vodContent = makeMovieContent({
|
||||
individual_connection: makeConnection({ bytes_sent: 0 }),
|
||||
});
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
const header = screen.getByText('Show Details').closest('[data-testid="group"]');
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
const header = screen
|
||||
.getByText('Show Details')
|
||||
.closest('[data-testid="group"]');
|
||||
fireEvent.click(header);
|
||||
expect(screen.queryByText('Data Sent:')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -444,8 +656,12 @@ describe('VodConnectionCard', () => {
|
|||
const vodContent = makeMovieContent({
|
||||
individual_connection: makeConnection({ duration: 0 }),
|
||||
});
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
const header = screen.getByText('Show Details').closest('[data-testid="group"]');
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
const header = screen
|
||||
.getByText('Show Details')
|
||||
.closest('[data-testid="group"]');
|
||||
fireEvent.click(header);
|
||||
expect(screen.queryByText('Watch Duration:')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -455,26 +671,58 @@ describe('VodConnectionCard', () => {
|
|||
|
||||
describe('ConnectionProgress', () => {
|
||||
it('does not render progress bar when totalTime is 0', () => {
|
||||
vi.mocked(calculateProgress).mockReturnValue({ totalTime: 0, currentTime: 0, percentage: 0 });
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
vi.mocked(calculateProgress).mockReturnValue({
|
||||
totalTime: 0,
|
||||
currentTime: 0,
|
||||
percentage: 0,
|
||||
});
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders progress bar when totalTime > 0', () => {
|
||||
vi.mocked(calculateProgress).mockReturnValue({ totalTime: 7200, currentTime: 3600, percentage: 50 });
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
vi.mocked(calculateProgress).mockReturnValue({
|
||||
totalTime: 7200,
|
||||
currentTime: 3600,
|
||||
percentage: 50,
|
||||
});
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('progress')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('passes correct percentage value to Progress component', () => {
|
||||
vi.mocked(calculateProgress).mockReturnValue({ totalTime: 7200, currentTime: 3600, percentage: 50 });
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
expect(screen.getByTestId('progress')).toHaveAttribute('data-value', '50');
|
||||
vi.mocked(calculateProgress).mockReturnValue({
|
||||
totalTime: 7200,
|
||||
currentTime: 3600,
|
||||
percentage: 50,
|
||||
});
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('progress')).toHaveAttribute(
|
||||
'data-value',
|
||||
'50'
|
||||
);
|
||||
});
|
||||
|
||||
it('calls calculateProgress with connection and duration_secs', () => {
|
||||
const vodContent = makeMovieContent();
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
expect(calculateProgress).toHaveBeenCalledWith(
|
||||
vodContent.individual_connection,
|
||||
vodContent.content_metadata.duration_secs
|
||||
|
|
@ -484,7 +732,10 @@ describe('VodConnectionCard', () => {
|
|||
it('does not render ConnectionProgress when no connection', () => {
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent({ individual_connection: null, connections: null })}
|
||||
vodContent={makeMovieContent({
|
||||
individual_connection: null,
|
||||
connections: null,
|
||||
})}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
|
@ -496,11 +747,22 @@ describe('VodConnectionCard', () => {
|
|||
|
||||
describe('progress update timer', () => {
|
||||
it('sets up a 1-second interval to trigger re-renders', () => {
|
||||
vi.mocked(calculateProgress).mockReturnValue({ totalTime: 7200, currentTime: 0, percentage: 0 });
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
vi.mocked(calculateProgress).mockReturnValue({
|
||||
totalTime: 7200,
|
||||
currentTime: 0,
|
||||
percentage: 0,
|
||||
});
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const callsBefore = vi.mocked(calculateProgress).mock.calls.length;
|
||||
act(() => { vi.advanceTimersByTime(1000); });
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
const callsAfter = vi.mocked(calculateProgress).mock.calls.length;
|
||||
|
||||
expect(callsAfter).toBeGreaterThan(callsBefore);
|
||||
|
|
@ -509,7 +771,10 @@ describe('VodConnectionCard', () => {
|
|||
it('clears the interval on unmount', () => {
|
||||
const clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval');
|
||||
const { unmount } = render(
|
||||
<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
unmount();
|
||||
expect(clearIntervalSpy).toHaveBeenCalled();
|
||||
|
|
@ -521,23 +786,34 @@ describe('VodConnectionCard', () => {
|
|||
describe('connection duration and start time', () => {
|
||||
it('calls calculateConnectionDuration with the connection', () => {
|
||||
const vodContent = makeMovieContent();
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
expect(calculateConnectionDuration).toHaveBeenCalledWith(vodContent.individual_connection);
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
expect(calculateConnectionDuration).toHaveBeenCalledWith(
|
||||
vodContent.individual_connection
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the duration returned by calculateConnectionDuration', () => {
|
||||
vi.mocked(calculateConnectionDuration).mockReturnValue('12m 45s');
|
||||
render(<VodConnectionCard vodContent={makeMovieContent()} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard
|
||||
vodContent={makeMovieContent()}
|
||||
stopVODClient={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('12m 45s')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls calculateConnectionStartTime with connection and fullDateTimeFormat', () => {
|
||||
const vodContent = makeMovieContent();
|
||||
render(<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />);
|
||||
render(
|
||||
<VodConnectionCard vodContent={vodContent} stopVODClient={vi.fn()} />
|
||||
);
|
||||
expect(calculateConnectionStartTime).toHaveBeenCalledWith(
|
||||
vodContent.individual_connection,
|
||||
'MM/DD/YYYY h:mm A'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue