mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 17:16:26 +00:00
Syntax formatting
This commit is contained in:
parent
067a0ce0a9
commit
48734727ad
27 changed files with 1546 additions and 596 deletions
|
|
@ -16,7 +16,15 @@ import {
|
|||
Text,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { AlertTriangle, CheckCircle, Clock, Info, RefreshCw, Users, XCircle, } from 'lucide-react';
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Info,
|
||||
RefreshCw,
|
||||
Users,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import usePlaylistsStore from '../../store/playlists';
|
||||
import { showNotification } from '../../utils/notificationUtils.js';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,27 +1,17 @@
|
|||
import React from 'react';
|
||||
import API from '../../api';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Text,
|
||||
Group,
|
||||
Flex,
|
||||
NumberInput,
|
||||
} from '@mantine/core';
|
||||
import { Button, Modal, Text, Group, Flex, NumberInput } from '@mantine/core';
|
||||
import { ListOrdered } from 'lucide-react';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '../../utils/notificationUtils.js';
|
||||
|
||||
const assignChannelNumbers = (channelIds, starting_number) => {
|
||||
return API.assignChannelNumbers(
|
||||
channelIds,
|
||||
starting_number
|
||||
);
|
||||
}
|
||||
return API.assignChannelNumbers(channelIds, starting_number);
|
||||
};
|
||||
|
||||
const requeryChannels = () => {
|
||||
API.requeryChannels();
|
||||
}
|
||||
};
|
||||
|
||||
const AssignChannelNumbers = ({ channelIds, isOpen, onClose }) => {
|
||||
const form = useForm({
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@ import { ListOrdered, SquarePlus, X, Zap } from 'lucide-react';
|
|||
import useEPGsStore from '../../store/epgs';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { USER_LEVEL_LABELS, USER_LEVELS } from '../../constants';
|
||||
import { showNotification, updateNotification, } from '../../utils/notificationUtils.js';
|
||||
import {
|
||||
showNotification,
|
||||
updateNotification,
|
||||
} from '../../utils/notificationUtils.js';
|
||||
import {
|
||||
addChannel,
|
||||
createLogo,
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ const updateChannelGroup = (channelGroup, values) => {
|
|||
id: channelGroup.id,
|
||||
...values,
|
||||
});
|
||||
}
|
||||
};
|
||||
const addChannelGroup = (values) => {
|
||||
return API.addChannelGroup(values);
|
||||
}
|
||||
};
|
||||
|
||||
const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => {
|
||||
const canEditChannelGroup = useChannelsStore((s) => s.canEditChannelGroup);
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ const ConnectionForm = ({ connection = null, isOpen, onClose }) => {
|
|||
const next = [...headers];
|
||||
next[idx] = { ...next[idx], key: newValue };
|
||||
setHeaders(next);
|
||||
}
|
||||
};
|
||||
|
||||
const onHeaderValueChange = (idx, newValue) => {
|
||||
const next = [...headers];
|
||||
|
|
@ -191,14 +191,12 @@ const ConnectionForm = ({ connection = null, isOpen, onClose }) => {
|
|||
value: newValue,
|
||||
};
|
||||
setHeaders(next);
|
||||
}
|
||||
};
|
||||
|
||||
const onHeaderRemove = (idx) => {
|
||||
const next = headers.filter((_, i) => i !== idx);
|
||||
setHeaders(
|
||||
next.length ? next : [{ key: '', value: '' }]
|
||||
);
|
||||
}
|
||||
setHeaders(next.length ? next : [{ key: '', value: '' }]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal opened={isOpen} size="lg" onClose={handleClose} title="Connection">
|
||||
|
|
@ -257,8 +255,12 @@ const ConnectionForm = ({ connection = null, isOpen, onClose }) => {
|
|||
<HeaderRow
|
||||
key={idx}
|
||||
h={h}
|
||||
onKeyChange={(e) => onHeaderKeyChange(idx, e.target.value)}
|
||||
onValueChange={(e) => onHeaderValueChange(idx, e.target.value)}
|
||||
onKeyChange={(e) =>
|
||||
onHeaderKeyChange(idx, e.target.value)
|
||||
}
|
||||
onValueChange={(e) =>
|
||||
onHeaderValueChange(idx, e.target.value)
|
||||
}
|
||||
onRemove={() => onHeaderRemove(idx)}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@ const CronPartInput = ({ field, cron, onChange }) => (
|
|||
label={field.label}
|
||||
placeholder={field.placeholder}
|
||||
value={cron.split(' ')[field.index] || '*'}
|
||||
onChange={(e) => onChange(updateCronPart(cron, field.index, e.currentTarget.value))}
|
||||
onChange={(e) =>
|
||||
onChange(updateCronPart(cron, field.index, e.currentTarget.value))
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -114,11 +114,17 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
|
|||
});
|
||||
|
||||
const patternValidation = useMemo(() => {
|
||||
const title = matchPattern(titlePattern, sampleTitle, 'Title pattern error');
|
||||
const time = matchPattern(timePattern, sampleTitle, 'Time pattern error');
|
||||
const date = matchPattern(datePattern, sampleTitle, 'Date pattern error');
|
||||
const title = matchPattern(
|
||||
titlePattern,
|
||||
sampleTitle,
|
||||
'Title pattern error'
|
||||
);
|
||||
const time = matchPattern(timePattern, sampleTitle, 'Time pattern error');
|
||||
const date = matchPattern(datePattern, sampleTitle, 'Date pattern error');
|
||||
|
||||
const errors = [title.error, time.error, date.error].filter(Boolean).join('; ');
|
||||
const errors = [title.error, time.error, date.error]
|
||||
.filter(Boolean)
|
||||
.join('; ');
|
||||
|
||||
const timePlaceholders = buildTimePlaceholders(
|
||||
time.groups,
|
||||
|
|
@ -138,40 +144,48 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
|
|||
const hasMatch = title.matched || time.matched;
|
||||
|
||||
return {
|
||||
titleMatch: title.matched,
|
||||
timeMatch: time.matched,
|
||||
dateMatch: date.matched,
|
||||
titleMatch: title.matched,
|
||||
timeMatch: time.matched,
|
||||
dateMatch: date.matched,
|
||||
titleGroups: title.groups,
|
||||
timeGroups: time.groups,
|
||||
dateGroups: date.groups,
|
||||
timeGroups: time.groups,
|
||||
dateGroups: date.groups,
|
||||
calculatedPlaceholders: time.matched ? timePlaceholders : {},
|
||||
error: errors || null,
|
||||
...applyTemplates(
|
||||
{ titleTemplate, subtitleTemplate, descriptionTemplate, upcomingTitleTemplate,
|
||||
upcomingDescriptionTemplate, endedTitleTemplate, endedDescriptionTemplate,
|
||||
channelLogoUrl, programPosterUrl },
|
||||
{
|
||||
titleTemplate,
|
||||
subtitleTemplate,
|
||||
descriptionTemplate,
|
||||
upcomingTitleTemplate,
|
||||
upcomingDescriptionTemplate,
|
||||
endedTitleTemplate,
|
||||
endedDescriptionTemplate,
|
||||
channelLogoUrl,
|
||||
programPosterUrl,
|
||||
},
|
||||
allGroups,
|
||||
hasMatch
|
||||
),
|
||||
};
|
||||
}, [
|
||||
titlePattern,
|
||||
timePattern,
|
||||
datePattern,
|
||||
sampleTitle,
|
||||
titleTemplate,
|
||||
subtitleTemplate,
|
||||
descriptionTemplate,
|
||||
upcomingTitleTemplate,
|
||||
upcomingDescriptionTemplate,
|
||||
endedTitleTemplate,
|
||||
endedDescriptionTemplate,
|
||||
channelLogoUrl,
|
||||
programPosterUrl,
|
||||
form.values.custom_properties?.timezone,
|
||||
form.values.custom_properties?.output_timezone,
|
||||
form.values.custom_properties?.program_duration,
|
||||
]);
|
||||
titlePattern,
|
||||
timePattern,
|
||||
datePattern,
|
||||
sampleTitle,
|
||||
titleTemplate,
|
||||
subtitleTemplate,
|
||||
descriptionTemplate,
|
||||
upcomingTitleTemplate,
|
||||
upcomingDescriptionTemplate,
|
||||
endedTitleTemplate,
|
||||
endedDescriptionTemplate,
|
||||
channelLogoUrl,
|
||||
programPosterUrl,
|
||||
form.values.custom_properties?.timezone,
|
||||
form.values.custom_properties?.output_timezone,
|
||||
form.values.custom_properties?.program_duration,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (epg) {
|
||||
|
|
|
|||
|
|
@ -59,9 +59,7 @@ vi.mock('@mantine/core', () => ({
|
|||
TableTd: ({ children }) => <td>{children}</td>,
|
||||
TableTr: ({ children }) => <tr>{children}</tr>,
|
||||
Text: ({ children }) => <span>{children}</span>,
|
||||
Tooltip: ({ children, label }) => (
|
||||
<div data-tooltip={label}>{children}</div>
|
||||
),
|
||||
Tooltip: ({ children, label }) => <div data-tooltip={label}>{children}</div>,
|
||||
}));
|
||||
|
||||
// ── lucide-react ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -109,13 +107,11 @@ const makeProfile = (overrides = {}) => ({
|
|||
});
|
||||
|
||||
const setupMocks = ({
|
||||
profiles = {},
|
||||
timeRemaining = '10 days 5 hours',
|
||||
formattedTimestamp = 'Nov 14, 2023',
|
||||
} = {}) => {
|
||||
vi.mocked(usePlaylistsStore).mockImplementation((sel) =>
|
||||
sel({ profiles })
|
||||
);
|
||||
profiles = {},
|
||||
timeRemaining = '10 days 5 hours',
|
||||
formattedTimestamp = 'Nov 14, 2023',
|
||||
} = {}) => {
|
||||
vi.mocked(usePlaylistsStore).mockImplementation((sel) => sel({ profiles }));
|
||||
vi.mocked(getTimeRemaining).mockReturnValue(timeRemaining);
|
||||
vi.mocked(formatTimestamp).mockReturnValue(formattedTimestamp);
|
||||
vi.mocked(refreshAccountInfo).mockResolvedValue({ success: true });
|
||||
|
|
@ -397,7 +393,10 @@ describe('AccountInfoModal', () => {
|
|||
it('disables the refresh button while refreshing', async () => {
|
||||
setupMocks();
|
||||
vi.mocked(refreshAccountInfo).mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 200))
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
setTimeout(() => resolve({ success: true }), 200)
|
||||
)
|
||||
);
|
||||
render(<AccountInfoModal {...defaultProps()} />);
|
||||
fireEvent.click(screen.getByTestId('action-icon'));
|
||||
|
|
@ -409,7 +408,9 @@ describe('AccountInfoModal', () => {
|
|||
|
||||
it('re-enables the refresh button after refresh fails', async () => {
|
||||
setupMocks();
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
vi.mocked(refreshAccountInfo).mockRejectedValue(new Error('fail'));
|
||||
render(<AccountInfoModal {...defaultProps()} />);
|
||||
fireEvent.click(screen.getByTestId('action-icon'));
|
||||
|
|
@ -443,4 +444,4 @@ describe('AccountInfoModal', () => {
|
|||
expect(screen.getByTestId('modal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,7 +26,15 @@ vi.mock('@mantine/form', () => ({
|
|||
|
||||
// ── Mantine core ───────────────────────────────────────────────────────────────
|
||||
vi.mock('@mantine/core', () => ({
|
||||
Button: ({ children, onClick, disabled, loading, variant, color, leftSection }) => (
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
loading,
|
||||
variant,
|
||||
color,
|
||||
leftSection,
|
||||
}) => (
|
||||
<button
|
||||
data-testid="button"
|
||||
onClick={onClick}
|
||||
|
|
@ -252,7 +260,9 @@ describe('AssignChannelNumbers', () => {
|
|||
|
||||
it('shows error notification when assignChannelNumbers throws', async () => {
|
||||
setupMocks();
|
||||
vi.mocked(API.assignChannelNumbers).mockRejectedValue(new Error('Server error'));
|
||||
vi.mocked(API.assignChannelNumbers).mockRejectedValue(
|
||||
new Error('Server error')
|
||||
);
|
||||
render(<AssignChannelNumbers {...defaultProps()} />);
|
||||
fireEvent.click(screen.getByTestId('button'));
|
||||
await waitFor(() => {
|
||||
|
|
@ -314,7 +324,9 @@ describe('AssignChannelNumbers', () => {
|
|||
describe('channelIds prop', () => {
|
||||
it('passes a single channelId correctly', async () => {
|
||||
setupMocks();
|
||||
render(<AssignChannelNumbers {...defaultProps({ channelIds: ['ch-99'] })} />);
|
||||
render(
|
||||
<AssignChannelNumbers {...defaultProps({ channelIds: ['ch-99'] })} />
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('button'));
|
||||
await waitFor(() => {
|
||||
expect(API.assignChannelNumbers).toHaveBeenCalledWith(['ch-99'], 1);
|
||||
|
|
@ -331,4 +343,4 @@ describe('AssignChannelNumbers', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -65,7 +65,12 @@ vi.mock('../Logo', () => ({
|
|||
|
||||
vi.mock('../../LazyLogo', () => ({
|
||||
default: ({ logoId, alt, style }) => (
|
||||
<img data-testid="lazy-logo" data-logo-id={logoId} alt={alt} style={style} />
|
||||
<img
|
||||
data-testid="lazy-logo"
|
||||
data-logo-id={logoId}
|
||||
alt={alt}
|
||||
style={style}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
|
|
@ -119,7 +124,17 @@ vi.mock('@mantine/core', async () => ({
|
|||
</button>
|
||||
),
|
||||
Box: ({ children, style }) => <div style={style}>{children}</div>,
|
||||
Button: ({ children, onClick, disabled, loading, type, variant, color, leftSection, title }) => (
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
loading,
|
||||
type,
|
||||
variant,
|
||||
color,
|
||||
leftSection,
|
||||
title,
|
||||
}) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
|
|
@ -138,7 +153,14 @@ vi.mock('@mantine/core', async () => ({
|
|||
<hr data-orientation={orientation} data-size={size} />
|
||||
),
|
||||
Flex: ({ children, gap, justify, align, mih }) => (
|
||||
<div style={{ gap, justifyContent: justify, alignItems: align, minHeight: mih }}>
|
||||
<div
|
||||
style={{
|
||||
gap,
|
||||
justifyContent: justify,
|
||||
alignItems: align,
|
||||
minHeight: mih,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
|
|
@ -165,7 +187,9 @@ vi.mock('@mantine/core', async () => ({
|
|||
name={name}
|
||||
type="number"
|
||||
value={value ?? ''}
|
||||
onChange={(e) => onChange(e.target.value === '' ? undefined : Number(e.target.value))}
|
||||
onChange={(e) =>
|
||||
onChange(e.target.value === '' ? undefined : Number(e.target.value))
|
||||
}
|
||||
data-testid={`number-input-${name}`}
|
||||
/>
|
||||
{error && <span data-testid={`error-${name}`}>{error}</span>}
|
||||
|
|
@ -181,7 +205,9 @@ vi.mock('@mantine/core', async () => ({
|
|||
{children}
|
||||
</div>
|
||||
),
|
||||
PopoverTarget: ({ children }) => <div data-testid="popover-target">{children}</div>,
|
||||
PopoverTarget: ({ children }) => (
|
||||
<div data-testid="popover-target">{children}</div>
|
||||
),
|
||||
ScrollArea: ({ children, style }) => <div style={style}>{children}</div>,
|
||||
Select: ({ label, value, onChange, data, id, name, error }) => (
|
||||
<div>
|
||||
|
|
@ -222,7 +248,20 @@ vi.mock('@mantine/core', async () => ({
|
|||
{children}
|
||||
</span>
|
||||
),
|
||||
TextInput: ({ id, name, label, readOnly, value, onClick, onChange, error, autoFocus, placeholder, rightSection, ...rest }) => (
|
||||
TextInput: ({
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
readOnly,
|
||||
value,
|
||||
onClick,
|
||||
onChange,
|
||||
error,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
rightSection,
|
||||
...rest
|
||||
}) => (
|
||||
<div>
|
||||
<label htmlFor={id}>{label}</label>
|
||||
<input
|
||||
|
|
@ -242,9 +281,7 @@ vi.mock('@mantine/core', async () => ({
|
|||
{error && <span data-testid={`error-${name}`}>{error}</span>}
|
||||
</div>
|
||||
),
|
||||
Tooltip: ({ children, label }) => (
|
||||
<div data-tooltip={label}>{children}</div>
|
||||
),
|
||||
Tooltip: ({ children, label }) => <div data-tooltip={label}>{children}</div>,
|
||||
UnstyledButton: ({ children, onClick }) => (
|
||||
<button data-testid="unstyled-button" onClick={onClick}>
|
||||
{children}
|
||||
|
|
@ -264,7 +301,10 @@ import useLogosStore from '../../../store/logos';
|
|||
import { useChannelLogoSelection } from '../../../hooks/useSmartLogos';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as ChannelUtils from '../../../utils/forms/ChannelUtils.js';
|
||||
import { showNotification, updateNotification } from '../../../utils/notificationUtils.js';
|
||||
import {
|
||||
showNotification,
|
||||
updateNotification,
|
||||
} from '../../../utils/notificationUtils.js';
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -284,7 +324,10 @@ const makeFormMethods = (overrides = {}) => {
|
|||
};
|
||||
|
||||
const register = vi.fn((name) => ({ name, ref: vi.fn() }));
|
||||
const handleSubmit = vi.fn((fn) => (e) => { e?.preventDefault?.(); return fn(watchValues); });
|
||||
const handleSubmit = vi.fn((fn) => (e) => {
|
||||
e?.preventDefault?.();
|
||||
return fn(watchValues);
|
||||
});
|
||||
const setValue = vi.fn();
|
||||
const watch = vi.fn((key) => (key ? watchValues[key] : watchValues));
|
||||
const reset = vi.fn();
|
||||
|
|
@ -326,18 +369,52 @@ const makeEpgs = () => ({
|
|||
});
|
||||
|
||||
const makeTvgs = () => [
|
||||
{ id: 'tvg-1', name: 'ESPN', tvg_id: 'espn.us', epg_source: 10, icon_url: 'http://example.com/espn.png' },
|
||||
{ id: 'tvg-2', name: 'CNN', tvg_id: 'cnn.us', epg_source: 11, icon_url: 'http://example.com/cnn.png' },
|
||||
{
|
||||
id: 'tvg-1',
|
||||
name: 'ESPN',
|
||||
tvg_id: 'espn.us',
|
||||
epg_source: 10,
|
||||
icon_url: 'http://example.com/espn.png',
|
||||
},
|
||||
{
|
||||
id: 'tvg-2',
|
||||
name: 'CNN',
|
||||
tvg_id: 'cnn.us',
|
||||
epg_source: 11,
|
||||
icon_url: 'http://example.com/cnn.png',
|
||||
},
|
||||
];
|
||||
|
||||
const makeTvgsById = () => ({
|
||||
'tvg-1': { id: 'tvg-1', name: 'ESPN', tvg_id: 'espn.us', epg_source: 10, icon_url: 'http://example.com/espn.png' },
|
||||
'tvg-2': { id: 'tvg-2', name: 'CNN', tvg_id: 'cnn.us', epg_source: 11, icon_url: 'http://example.com/cnn.png' },
|
||||
'tvg-1': {
|
||||
id: 'tvg-1',
|
||||
name: 'ESPN',
|
||||
tvg_id: 'espn.us',
|
||||
epg_source: 10,
|
||||
icon_url: 'http://example.com/espn.png',
|
||||
},
|
||||
'tvg-2': {
|
||||
id: 'tvg-2',
|
||||
name: 'CNN',
|
||||
tvg_id: 'cnn.us',
|
||||
epg_source: 11,
|
||||
icon_url: 'http://example.com/cnn.png',
|
||||
},
|
||||
});
|
||||
|
||||
const makeLogos = () => ({
|
||||
42: { id: 42, name: 'ESPN Logo', url: 'http://example.com/espn.png', cache_url: '/cache/espn.png' },
|
||||
43: { id: 43, name: 'CNN Logo', url: 'http://example.com/cnn.png', cache_url: '/cache/cnn.png' },
|
||||
42: {
|
||||
id: 42,
|
||||
name: 'ESPN Logo',
|
||||
url: 'http://example.com/espn.png',
|
||||
cache_url: '/cache/espn.png',
|
||||
},
|
||||
43: {
|
||||
id: 43,
|
||||
name: 'CNN Logo',
|
||||
url: 'http://example.com/cnn.png',
|
||||
cache_url: '/cache/cnn.png',
|
||||
},
|
||||
});
|
||||
|
||||
const makeChannelLogos = () => ({
|
||||
|
|
@ -384,9 +461,7 @@ const setupMocks = ({ formOverrides = {}, channel = null } = {}) => {
|
|||
sel({ epgs, tvgs, tvgsById })
|
||||
);
|
||||
|
||||
vi.mocked(useLogosStore).mockImplementation((sel) =>
|
||||
sel({ logos })
|
||||
);
|
||||
vi.mocked(useLogosStore).mockImplementation((sel) => sel({ logos }));
|
||||
|
||||
const mockEnsureLogosLoaded = vi.fn();
|
||||
vi.mocked(useChannelLogoSelection).mockReturnValue({
|
||||
|
|
@ -396,7 +471,9 @@ const setupMocks = ({ formOverrides = {}, channel = null } = {}) => {
|
|||
});
|
||||
|
||||
// Same object reference every call — prevents useMemo from recalculating
|
||||
vi.mocked(ChannelUtils.getChannelFormDefaultValues).mockReturnValue(stableDefaults);
|
||||
vi.mocked(ChannelUtils.getChannelFormDefaultValues).mockReturnValue(
|
||||
stableDefaults
|
||||
);
|
||||
vi.mocked(ChannelUtils.getFormattedValues).mockImplementation((v) => v);
|
||||
|
||||
return { formMethods, mockEnsureLogosLoaded };
|
||||
|
|
@ -460,7 +537,9 @@ describe('ChannelForm', () => {
|
|||
it('renders Channel Group text input', () => {
|
||||
setupMocks();
|
||||
render(<ChannelForm {...defaultProps()} />);
|
||||
expect(screen.getByTestId('text-input-channel_group_id')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('text-input-channel_group_id')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders stream profile select with options', () => {
|
||||
|
|
@ -493,7 +572,9 @@ describe('ChannelForm', () => {
|
|||
it('renders the channel number input', () => {
|
||||
setupMocks();
|
||||
render(<ChannelForm {...defaultProps()} />);
|
||||
expect(screen.getByTestId('number-input-channel_number')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('number-input-channel_number')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders TVG-ID text input', () => {
|
||||
|
|
@ -505,7 +586,9 @@ describe('ChannelForm', () => {
|
|||
it('renders Gracenote StationId text input', () => {
|
||||
setupMocks();
|
||||
render(<ChannelForm {...defaultProps()} />);
|
||||
expect(screen.getByTestId('text-input-tvc_guide_stationid')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('text-input-tvc_guide_stationid')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders EPG text input', () => {
|
||||
|
|
@ -622,7 +705,9 @@ describe('ChannelForm', () => {
|
|||
|
||||
it('shows error notification when matchChannelEpg throws', async () => {
|
||||
const channel = makeChannel();
|
||||
vi.mocked(ChannelUtils.matchChannelEpg).mockRejectedValue(new Error('Network'));
|
||||
vi.mocked(ChannelUtils.matchChannelEpg).mockRejectedValue(
|
||||
new Error('Network')
|
||||
);
|
||||
setupMocks({ channel });
|
||||
render(<ChannelForm {...defaultProps({ channel })} />);
|
||||
fireEvent.click(screen.getByText('Auto Match'));
|
||||
|
|
@ -644,7 +729,10 @@ describe('ChannelForm', () => {
|
|||
render(<ChannelForm {...defaultProps({ channel })} />);
|
||||
fireEvent.click(screen.getByText('Auto Match'));
|
||||
await waitFor(() => {
|
||||
expect(formMethods.setValue).toHaveBeenCalledWith('epg_data_id', 'tvg-1');
|
||||
expect(formMethods.setValue).toHaveBeenCalledWith(
|
||||
'epg_data_id',
|
||||
'tvg-1'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -731,7 +819,6 @@ describe('ChannelForm', () => {
|
|||
expect.objectContaining({ color: 'green' })
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('creates a new logo when no matching logo exists', async () => {
|
||||
|
|
@ -745,8 +832,8 @@ describe('ChannelForm', () => {
|
|||
},
|
||||
});
|
||||
// tvg-2 has icon_url 'http://example.com/cnn.png' — but make allLogos not contain it
|
||||
vi.mocked(useLogosStore).mockImplementation((sel) =>
|
||||
sel({ logos: {} }) // empty logos so no match
|
||||
vi.mocked(useLogosStore).mockImplementation(
|
||||
(sel) => sel({ logos: {} }) // empty logos so no match
|
||||
);
|
||||
render(<ChannelForm {...defaultProps()} />);
|
||||
fireEvent.click(screen.getByText('Use EPG Logo'));
|
||||
|
|
@ -761,9 +848,7 @@ describe('ChannelForm', () => {
|
|||
setupMocks({
|
||||
formOverrides: { watchValues: { epg_data_id: 'tvg-2' } },
|
||||
});
|
||||
vi.mocked(useLogosStore).mockImplementation((sel) =>
|
||||
sel({ logos: {} })
|
||||
);
|
||||
vi.mocked(useLogosStore).mockImplementation((sel) => sel({ logos: {} }));
|
||||
render(<ChannelForm {...defaultProps()} />);
|
||||
fireEvent.click(screen.getByText('Use EPG Logo'));
|
||||
await waitFor(() => {
|
||||
|
|
@ -844,7 +929,9 @@ describe('ChannelForm', () => {
|
|||
fireEvent.click(screen.getByTestId('icon-square-plus').closest('button'));
|
||||
fireEvent.click(screen.getByTestId('channel-group-close'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('channel-group-form')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('channel-group-form')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -854,8 +941,13 @@ describe('ChannelForm', () => {
|
|||
fireEvent.click(screen.getByTestId('icon-square-plus').closest('button'));
|
||||
fireEvent.click(screen.getByTestId('channel-group-save'));
|
||||
await waitFor(() => {
|
||||
expect(formMethods.setValue).toHaveBeenCalledWith('channel_group_id', '99');
|
||||
expect(screen.queryByTestId('channel-group-form')).not.toBeInTheDocument();
|
||||
expect(formMethods.setValue).toHaveBeenCalledWith(
|
||||
'channel_group_id',
|
||||
'99'
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('channel-group-form')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -923,7 +1015,9 @@ describe('ChannelForm', () => {
|
|||
});
|
||||
|
||||
it('still calls onClose when submission throws', async () => {
|
||||
vi.mocked(ChannelUtils.addChannel).mockRejectedValue(new Error('Server error'));
|
||||
vi.mocked(ChannelUtils.addChannel).mockRejectedValue(
|
||||
new Error('Server error')
|
||||
);
|
||||
setupMocks();
|
||||
const onClose = vi.fn();
|
||||
render(<ChannelForm {...defaultProps({ onClose })} />);
|
||||
|
|
@ -986,4 +1080,4 @@ describe('ChannelForm', () => {
|
|||
expect(epgInput).toHaveValue('EPG Source 1 - ESPN');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -57,12 +57,25 @@ vi.mock('../ChannelGroup', () => ({
|
|||
}));
|
||||
|
||||
vi.mock('../../ConfirmationDialog', () => ({
|
||||
default: ({ opened, onConfirm, onClose, title, confirmLabel, cancelLabel, loading, message }) =>
|
||||
default: ({
|
||||
opened,
|
||||
onConfirm,
|
||||
onClose,
|
||||
title,
|
||||
confirmLabel,
|
||||
cancelLabel,
|
||||
loading,
|
||||
message,
|
||||
}) =>
|
||||
opened ? (
|
||||
<div data-testid="confirmation-dialog">
|
||||
<div data-testid="dialog-title">{title}</div>
|
||||
<div data-testid="dialog-message">{message}</div>
|
||||
<button data-testid="dialog-confirm" onClick={onConfirm} disabled={loading}>
|
||||
<button
|
||||
data-testid="dialog-confirm"
|
||||
onClick={onConfirm}
|
||||
disabled={loading}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
<button data-testid="dialog-cancel" onClick={onClose}>
|
||||
|
|
@ -73,7 +86,9 @@ vi.mock('../../ConfirmationDialog', () => ({
|
|||
}));
|
||||
|
||||
vi.mock('../../LazyLogo', () => ({
|
||||
default: ({ src, alt }) => <img src={src} alt={alt} data-testid="lazy-logo" />,
|
||||
default: ({ src, alt }) => (
|
||||
<img src={src} alt={alt} data-testid="lazy-logo" />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../../images/logo.png', () => ({ default: 'default-logo.png' }));
|
||||
|
|
@ -104,14 +119,20 @@ vi.mock('@mantine/core', async () => ({
|
|||
opened ? (
|
||||
<div data-testid="modal">
|
||||
<div data-testid="modal-title">{title}</div>
|
||||
<button data-testid="modal-close" onClick={onClose}>×</button>
|
||||
<button data-testid="modal-close" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
Paper: ({ children, style }) => <div style={style}>{children}</div>,
|
||||
Popover: ({ children }) => <div data-testid="popover">{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>
|
||||
),
|
||||
PopoverTarget: ({ children }) => (
|
||||
<div data-testid="popover-target">{children}</div>
|
||||
),
|
||||
ScrollArea: ({ children, h }) => <div style={{ height: h }}>{children}</div>,
|
||||
Select: ({ label, data, value, onChange, disabled }) => (
|
||||
<select
|
||||
|
|
@ -124,13 +145,19 @@ vi.mock('@mantine/core', async () => ({
|
|||
{(data ?? []).map((opt) => {
|
||||
const val = typeof opt === 'string' ? opt : opt.value;
|
||||
const lbl = typeof opt === 'string' ? opt : opt.label;
|
||||
return <option key={val} value={val}>{lbl}</option>;
|
||||
return (
|
||||
<option key={val} value={val}>
|
||||
{lbl}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
),
|
||||
Stack: ({ children }) => <div>{children}</div>,
|
||||
Text: ({ children, size, c }) => (
|
||||
<span data-size={size} data-color={c}>{children}</span>
|
||||
<span data-size={size} data-color={c}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
TextInput: ({ label, placeholder, value, onChange, disabled }) => (
|
||||
<input
|
||||
|
|
@ -144,7 +171,9 @@ vi.mock('@mantine/core', async () => ({
|
|||
),
|
||||
Tooltip: ({ children, label }) => <div data-tooltip={label}>{children}</div>,
|
||||
UnstyledButton: ({ children, onClick, style }) => (
|
||||
<button onClick={onClick} style={style}>{children}</button>
|
||||
<button onClick={onClick} style={style}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
useMantineTheme: () => ({ tailwind: { green: { 5: '#38a169' } } }),
|
||||
}));
|
||||
|
|
@ -246,7 +275,8 @@ const setupMocks = (overrides = {}) => {
|
|||
|
||||
vi.mocked(useWarningsStore).mockImplementation((sel) =>
|
||||
sel({
|
||||
isWarningSuppressed: overrides.isWarningSuppressed ?? vi.fn().mockReturnValue(false),
|
||||
isWarningSuppressed:
|
||||
overrides.isWarningSuppressed ?? vi.fn().mockReturnValue(false),
|
||||
suppressWarning: vi.fn(),
|
||||
})
|
||||
);
|
||||
|
|
@ -269,14 +299,24 @@ const setupMocks = (overrides = {}) => {
|
|||
});
|
||||
|
||||
vi.mocked(ChannelBatchUtils.computeRegexPreview).mockReturnValue([]);
|
||||
vi.mocked(ChannelBatchUtils.buildSubmitValues).mockReturnValue({ stream_profile_id: '1' });
|
||||
vi.mocked(ChannelBatchUtils.buildSubmitValues).mockReturnValue({
|
||||
stream_profile_id: '1',
|
||||
});
|
||||
vi.mocked(ChannelBatchUtils.buildEpgAssociations).mockResolvedValue(null);
|
||||
vi.mocked(ChannelBatchUtils.updateChannels).mockResolvedValue(undefined);
|
||||
vi.mocked(ChannelBatchUtils.bulkRegexRenameChannels).mockResolvedValue(undefined);
|
||||
vi.mocked(ChannelBatchUtils.bulkRegexRenameChannels).mockResolvedValue(
|
||||
undefined
|
||||
);
|
||||
vi.mocked(ChannelBatchUtils.batchSetEPG).mockResolvedValue(undefined);
|
||||
vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockResolvedValue(undefined);
|
||||
vi.mocked(ChannelBatchUtils.setChannelLogosFromEpg).mockResolvedValue(undefined);
|
||||
vi.mocked(ChannelBatchUtils.setChannelTvgIdsFromEpg).mockResolvedValue(undefined);
|
||||
vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockResolvedValue(
|
||||
undefined
|
||||
);
|
||||
vi.mocked(ChannelBatchUtils.setChannelLogosFromEpg).mockResolvedValue(
|
||||
undefined
|
||||
);
|
||||
vi.mocked(ChannelBatchUtils.setChannelTvgIdsFromEpg).mockResolvedValue(
|
||||
undefined
|
||||
);
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(null);
|
||||
vi.mocked(ChannelBatchUtils.getLogoChange).mockReturnValue(null);
|
||||
vi.mocked(ChannelBatchUtils.getStreamProfileChange).mockReturnValue(null);
|
||||
|
|
@ -353,12 +393,16 @@ describe('ChannelBatchForm', () => {
|
|||
expect(showNotification).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ color: 'orange' })
|
||||
);
|
||||
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('confirmation-dialog')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens confirmation dialog when at least one change is present', () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
|
|
@ -371,11 +415,13 @@ describe('ChannelBatchForm', () => {
|
|||
|
||||
describe('warning suppression', () => {
|
||||
it('skips confirmation dialog and calls onSubmit directly when batch-update warning is suppressed', async () => {
|
||||
const isWarningSuppressed = vi.fn().mockImplementation((key) =>
|
||||
key === 'batch-update-channels'
|
||||
);
|
||||
const isWarningSuppressed = vi
|
||||
.fn()
|
||||
.mockImplementation((key) => key === 'batch-update-channels');
|
||||
setupMocks({ isWarningSuppressed });
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
const onClose = vi.fn();
|
||||
|
||||
renderForm({ onClose });
|
||||
|
|
@ -384,7 +430,9 @@ describe('ChannelBatchForm', () => {
|
|||
await waitFor(() => {
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('confirmation-dialog')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -393,27 +441,33 @@ describe('ChannelBatchForm', () => {
|
|||
describe('confirmation dialog', () => {
|
||||
it('closes confirmation dialog on cancel', () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
expect(screen.getByTestId('confirmation-dialog')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('dialog-cancel'));
|
||||
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('confirmation-dialog')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls updateChannels on confirm', async () => {
|
||||
setupMocks({
|
||||
formValues: {
|
||||
stream_profile_id: '1', // survives: not '-1' or '0', so kept as-is after parseInt...
|
||||
user_level: '-1', // deleted
|
||||
is_adult: '-1', // deleted
|
||||
channel_group: '', // deleted (channel_group key is removed)
|
||||
logo: '', // deleted
|
||||
stream_profile_id: '1', // survives: not '-1' or '0', so kept as-is after parseInt...
|
||||
user_level: '-1', // deleted
|
||||
is_adult: '-1', // deleted
|
||||
channel_group: '', // deleted (channel_group key is removed)
|
||||
logo: '', // deleted
|
||||
},
|
||||
});
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
|
|
@ -429,7 +483,9 @@ describe('ChannelBatchForm', () => {
|
|||
|
||||
it('calls requeryChannels after successful submit', async () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
|
|
@ -442,7 +498,9 @@ describe('ChannelBatchForm', () => {
|
|||
|
||||
it('calls onClose after successful submit', async () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
const onClose = vi.fn();
|
||||
|
||||
renderForm({ onClose });
|
||||
|
|
@ -477,7 +535,7 @@ describe('ChannelBatchForm', () => {
|
|||
expect(ChannelBatchUtils.bulkRegexRenameChannels).toHaveBeenCalledWith(
|
||||
CHANNEL_IDS,
|
||||
'foo',
|
||||
'', // regexReplace default is ''
|
||||
'', // regexReplace default is ''
|
||||
'g'
|
||||
);
|
||||
});
|
||||
|
|
@ -504,20 +562,24 @@ describe('ChannelBatchForm', () => {
|
|||
|
||||
fireEvent.click(screen.getByText('Set Names from EPG'));
|
||||
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent('Confirm Set Names from EPG');
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
||||
'Confirm Set Names from EPG'
|
||||
);
|
||||
});
|
||||
|
||||
it('skips dialog and executes directly when warning is suppressed', async () => {
|
||||
const isWarningSuppressed = vi.fn().mockImplementation((key) =>
|
||||
key === 'batch-set-names-from-epg'
|
||||
);
|
||||
const isWarningSuppressed = vi
|
||||
.fn()
|
||||
.mockImplementation((key) => key === 'batch-set-names-from-epg');
|
||||
setupMocks({ isWarningSuppressed });
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Set Names from EPG'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ChannelBatchUtils.setChannelNamesFromEpg).toHaveBeenCalledWith(CHANNEL_IDS);
|
||||
expect(ChannelBatchUtils.setChannelNamesFromEpg).toHaveBeenCalledWith(
|
||||
CHANNEL_IDS
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -529,7 +591,9 @@ describe('ChannelBatchForm', () => {
|
|||
fireEvent.click(screen.getByTestId('dialog-confirm'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ChannelBatchUtils.setChannelNamesFromEpg).toHaveBeenCalledWith(CHANNEL_IDS);
|
||||
expect(ChannelBatchUtils.setChannelNamesFromEpg).toHaveBeenCalledWith(
|
||||
CHANNEL_IDS
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -549,7 +613,9 @@ describe('ChannelBatchForm', () => {
|
|||
|
||||
it('shows error notification when setChannelNamesFromEpg rejects', async () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockRejectedValue(new Error('fail'));
|
||||
vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockRejectedValue(
|
||||
new Error('fail')
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Set Names from EPG'));
|
||||
|
|
@ -583,7 +649,9 @@ describe('ChannelBatchForm', () => {
|
|||
|
||||
fireEvent.click(screen.getByText('Set Logos from EPG'));
|
||||
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent('Confirm Set Logos from EPG');
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
||||
'Confirm Set Logos from EPG'
|
||||
);
|
||||
});
|
||||
|
||||
it('calls setChannelLogosFromEpg on confirm', async () => {
|
||||
|
|
@ -594,13 +662,17 @@ describe('ChannelBatchForm', () => {
|
|||
fireEvent.click(screen.getByTestId('dialog-confirm'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ChannelBatchUtils.setChannelLogosFromEpg).toHaveBeenCalledWith(CHANNEL_IDS);
|
||||
expect(ChannelBatchUtils.setChannelLogosFromEpg).toHaveBeenCalledWith(
|
||||
CHANNEL_IDS
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error notification when setChannelLogosFromEpg rejects', async () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.setChannelLogosFromEpg).mockRejectedValue(new Error('fail'));
|
||||
vi.mocked(ChannelBatchUtils.setChannelLogosFromEpg).mockRejectedValue(
|
||||
new Error('fail')
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Set Logos from EPG'));
|
||||
|
|
@ -634,7 +706,9 @@ describe('ChannelBatchForm', () => {
|
|||
|
||||
fireEvent.click(screen.getByText('Set TVG-IDs from EPG'));
|
||||
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent('Confirm Set TVG-IDs from EPG');
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
||||
'Confirm Set TVG-IDs from EPG'
|
||||
);
|
||||
});
|
||||
|
||||
it('calls setChannelTvgIdsFromEpg on confirm', async () => {
|
||||
|
|
@ -645,13 +719,17 @@ describe('ChannelBatchForm', () => {
|
|||
fireEvent.click(screen.getByTestId('dialog-confirm'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ChannelBatchUtils.setChannelTvgIdsFromEpg).toHaveBeenCalledWith(CHANNEL_IDS);
|
||||
expect(ChannelBatchUtils.setChannelTvgIdsFromEpg).toHaveBeenCalledWith(
|
||||
CHANNEL_IDS
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error notification when setChannelTvgIdsFromEpg rejects', async () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.setChannelTvgIdsFromEpg).mockRejectedValue(new Error('fail'));
|
||||
vi.mocked(ChannelBatchUtils.setChannelTvgIdsFromEpg).mockRejectedValue(
|
||||
new Error('fail')
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Set TVG-IDs from EPG'));
|
||||
|
|
@ -672,7 +750,9 @@ describe('ChannelBatchForm', () => {
|
|||
setupMocks();
|
||||
renderForm();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('icon-square-plus')[0].closest('button'));
|
||||
fireEvent.click(
|
||||
screen.getAllByTestId('icon-square-plus')[0].closest('button')
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('channel-group-form')).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -681,20 +761,28 @@ describe('ChannelBatchForm', () => {
|
|||
setupMocks();
|
||||
renderForm();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('icon-square-plus')[0].closest('button'));
|
||||
fireEvent.click(
|
||||
screen.getAllByTestId('icon-square-plus')[0].closest('button')
|
||||
);
|
||||
fireEvent.click(screen.getByText('Save Group'));
|
||||
|
||||
expect(screen.queryByTestId('channel-group-form')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('channel-group-form')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes channel group modal without updating selection when cancelled', () => {
|
||||
setupMocks();
|
||||
renderForm();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('icon-square-plus')[0].closest('button'));
|
||||
fireEvent.click(
|
||||
screen.getAllByTestId('icon-square-plus')[0].closest('button')
|
||||
);
|
||||
fireEvent.click(screen.getByText('Cancel Group'));
|
||||
|
||||
expect(screen.queryByTestId('channel-group-form')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('channel-group-form')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -745,7 +833,9 @@ describe('ChannelBatchForm', () => {
|
|||
describe('error resilience', () => {
|
||||
it('does not throw when requeryChannels rejects after submit', async () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
vi.mocked(requeryChannels).mockRejectedValue(new Error('network'));
|
||||
|
||||
renderForm();
|
||||
|
|
@ -758,7 +848,9 @@ describe('ChannelBatchForm', () => {
|
|||
|
||||
it('does not throw when setChannelNamesFromEpg rejects and no channels', async () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockRejectedValue(new Error('fail'));
|
||||
vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockRejectedValue(
|
||||
new Error('fail')
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Set Names from EPG'));
|
||||
|
|
@ -769,12 +861,14 @@ describe('ChannelBatchForm', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── Batch update confirmation message ──────────────────────────────────────
|
||||
// ── Batch update confirmation message ──────────────────────────────────────
|
||||
|
||||
describe('batch update confirmation message', () => {
|
||||
it('displays the channel count in the confirmation message', () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
|
|
@ -784,19 +878,29 @@ describe('ChannelBatchForm', () => {
|
|||
|
||||
it('displays a single change line in the confirmation message', () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.getStreamProfileChange).mockReturnValue('• Stream Profile: HD Profile');
|
||||
vi.mocked(ChannelBatchUtils.getStreamProfileChange).mockReturnValue(
|
||||
'• Stream Profile: HD Profile'
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
|
||||
expect(screen.getByTestId('dialog-message')).toHaveTextContent('• Stream Profile: HD Profile');
|
||||
expect(screen.getByTestId('dialog-message')).toHaveTextContent(
|
||||
'• Stream Profile: HD Profile'
|
||||
);
|
||||
});
|
||||
|
||||
it('displays multiple change lines when several fields are changed', () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getStreamProfileChange).mockReturnValue('• Stream Profile: HD Profile');
|
||||
vi.mocked(ChannelBatchUtils.getUserLevelChange).mockReturnValue('• User Level: Admin');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
vi.mocked(ChannelBatchUtils.getStreamProfileChange).mockReturnValue(
|
||||
'• Stream Profile: HD Profile'
|
||||
);
|
||||
vi.mocked(ChannelBatchUtils.getUserLevelChange).mockReturnValue(
|
||||
'• User Level: Admin'
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
|
|
@ -825,22 +929,30 @@ describe('ChannelBatchForm', () => {
|
|||
|
||||
it('uses "Apply Changes" as the confirm button label', () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
|
||||
expect(screen.getByTestId('dialog-confirm')).toHaveTextContent('Apply Changes');
|
||||
expect(screen.getByTestId('dialog-confirm')).toHaveTextContent(
|
||||
'Apply Changes'
|
||||
);
|
||||
});
|
||||
|
||||
it('shows "Confirm Batch Update" as the dialog title', () => {
|
||||
setupMocks();
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports');
|
||||
vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
|
||||
renderForm();
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent('Confirm Batch Update');
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
||||
'Confirm Batch Update'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -155,7 +155,9 @@ describe('ChannelGroup', () => {
|
|||
|
||||
renderForm();
|
||||
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent('Channel Group');
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent(
|
||||
'Channel Group'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -227,7 +229,10 @@ describe('ChannelGroup', () => {
|
|||
|
||||
it('resets the form after successful add', async () => {
|
||||
setupMocks();
|
||||
vi.mocked(API.addChannelGroup).mockResolvedValue({ id: 99, name: 'NewGroup' });
|
||||
vi.mocked(API.addChannelGroup).mockResolvedValue({
|
||||
id: 99,
|
||||
name: 'NewGroup',
|
||||
});
|
||||
|
||||
const form = makeFormMock('NewGroup');
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
|
|
@ -317,7 +322,10 @@ describe('ChannelGroup', () => {
|
|||
|
||||
renderForm({ channelGroup: makeGroup() });
|
||||
|
||||
expect(screen.getByTestId('alert')).toHaveAttribute('data-color', 'yellow');
|
||||
expect(screen.getByTestId('alert')).toHaveAttribute(
|
||||
'data-color',
|
||||
'yellow'
|
||||
);
|
||||
});
|
||||
|
||||
it('disables the name input for non-editable group', () => {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,9 @@ vi.mock('@mantine/core', () => ({
|
|||
opened ? (
|
||||
<div data-testid="modal">
|
||||
<div data-testid="modal-title">{title}</div>
|
||||
<button data-testid="modal-close" onClick={onClose}>×</button>
|
||||
<button data-testid="modal-close" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
|
|
@ -106,11 +108,7 @@ vi.mock('@mantine/core', () => ({
|
|||
<div data-testid={`tabs-panel-${value}`}>{children}</div>
|
||||
),
|
||||
TabsTab: ({ children, value, onClick }) => (
|
||||
<button
|
||||
data-testid={`tab-${value}`}
|
||||
onClick={onClick}
|
||||
role="tab"
|
||||
>
|
||||
<button data-testid={`tab-${value}`} onClick={onClick} role="tab">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
|
|
@ -134,7 +132,13 @@ vi.mock('@mantine/core', () => ({
|
|||
placeholder={placeholder}
|
||||
data-testid={`input-${label?.toLowerCase().replace(/\s+/g, '-')}`}
|
||||
/>
|
||||
{error && <span data-testid={`error-${label?.toLowerCase().replace(/\s+/g, '-')}`}>{error}</span>}
|
||||
{error && (
|
||||
<span
|
||||
data-testid={`error-${label?.toLowerCase().replace(/\s+/g, '-')}`}
|
||||
>
|
||||
{error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
|
@ -152,7 +156,11 @@ const makeConnection = (overrides = {}) => ({
|
|||
},
|
||||
subscriptions: [
|
||||
{ event: 'channel_added', enabled: true, payload_template: null },
|
||||
{ event: 'recording_started', enabled: true, payload_template: '{"id": "{{id}}"}' },
|
||||
{
|
||||
event: 'recording_started',
|
||||
enabled: true,
|
||||
payload_template: '{"id": "{{id}}"}',
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
});
|
||||
|
|
@ -212,12 +220,23 @@ const renderForm = (props = {}) => {
|
|||
describe('ConnectionForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(ConnectionUtils.createConnectIntegration).mockResolvedValue({ id: 99 });
|
||||
vi.mocked(ConnectionUtils.updateConnectIntegration).mockResolvedValue(undefined);
|
||||
vi.mocked(ConnectionUtils.setConnectSubscriptions).mockResolvedValue(undefined);
|
||||
vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({ url: 'https://x.com' });
|
||||
vi.mocked(ConnectionUtils.createConnectIntegration).mockResolvedValue({
|
||||
id: 99,
|
||||
});
|
||||
vi.mocked(ConnectionUtils.updateConnectIntegration).mockResolvedValue(
|
||||
undefined
|
||||
);
|
||||
vi.mocked(ConnectionUtils.setConnectSubscriptions).mockResolvedValue(
|
||||
undefined
|
||||
);
|
||||
vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(ConnectionUtils.buildSubscriptions).mockReturnValue([]);
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: '' });
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({
|
||||
fieldErrors: {},
|
||||
apiError: '',
|
||||
});
|
||||
});
|
||||
|
||||
// ── Visibility ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -253,7 +272,11 @@ describe('ConnectionForm', () => {
|
|||
});
|
||||
|
||||
it('calls createConnectIntegration on submit', async () => {
|
||||
const form = makeFormMock({ name: 'New Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'New Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
|
||||
renderForm({ connection: null });
|
||||
|
|
@ -269,7 +292,11 @@ describe('ConnectionForm', () => {
|
|||
});
|
||||
|
||||
it('does not call updateConnectIntegration when creating', async () => {
|
||||
const form = makeFormMock({ name: 'New Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'New Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
|
||||
renderForm({ connection: null });
|
||||
|
|
@ -284,9 +311,15 @@ describe('ConnectionForm', () => {
|
|||
|
||||
it('calls setConnectSubscriptions with newly created connection', async () => {
|
||||
const created = { id: 99, name: 'New Hook' };
|
||||
vi.mocked(ConnectionUtils.createConnectIntegration).mockResolvedValue(created);
|
||||
vi.mocked(ConnectionUtils.createConnectIntegration).mockResolvedValue(
|
||||
created
|
||||
);
|
||||
|
||||
const form = makeFormMock({ name: 'New Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'New Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
|
||||
renderForm({ connection: null });
|
||||
|
|
@ -301,7 +334,11 @@ describe('ConnectionForm', () => {
|
|||
});
|
||||
|
||||
it('calls onClose after successful create', async () => {
|
||||
const form = makeFormMock({ name: 'New Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'New Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
|
||||
const onClose = vi.fn();
|
||||
|
|
@ -357,7 +394,11 @@ describe('ConnectionForm', () => {
|
|||
|
||||
it('does not call createConnectIntegration when editing', async () => {
|
||||
const connection = makeConnection();
|
||||
const form = makeFormMock({ name: 'My Webhook', type: 'webhook', url: 'https://example.com/hook' });
|
||||
const form = makeFormMock({
|
||||
name: 'My Webhook',
|
||||
type: 'webhook',
|
||||
url: 'https://example.com/hook',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
|
||||
renderForm({ connection });
|
||||
|
|
@ -371,7 +412,11 @@ describe('ConnectionForm', () => {
|
|||
|
||||
it('calls setConnectSubscriptions with existing connection on update', async () => {
|
||||
const connection = makeConnection();
|
||||
const form = makeFormMock({ name: 'My Webhook', type: 'webhook', url: 'https://example.com/hook' });
|
||||
const form = makeFormMock({
|
||||
name: 'My Webhook',
|
||||
type: 'webhook',
|
||||
url: 'https://example.com/hook',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
|
||||
renderForm({ connection });
|
||||
|
|
@ -387,7 +432,11 @@ describe('ConnectionForm', () => {
|
|||
|
||||
it('calls onClose after successful update', async () => {
|
||||
const connection = makeConnection();
|
||||
const form = makeFormMock({ name: 'My Webhook', type: 'webhook', url: 'https://example.com/hook' });
|
||||
const form = makeFormMock({
|
||||
name: 'My Webhook',
|
||||
type: 'webhook',
|
||||
url: 'https://example.com/hook',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
|
||||
const onClose = vi.fn();
|
||||
|
|
@ -446,7 +495,9 @@ describe('ConnectionForm', () => {
|
|||
script_path: '/usr/local/bin/notify.sh',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({ path: '/usr/local/bin/notify.sh' });
|
||||
vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({
|
||||
path: '/usr/local/bin/notify.sh',
|
||||
});
|
||||
|
||||
renderForm({ connection: null });
|
||||
fireEvent.submit(screen.getByTestId('modal').querySelector('form'));
|
||||
|
|
@ -485,7 +536,9 @@ describe('ConnectionForm', () => {
|
|||
url: 'https://example.com/webhook',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({ url: 'https://example.com/webhook' });
|
||||
vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({
|
||||
url: 'https://example.com/webhook',
|
||||
});
|
||||
|
||||
renderForm({ connection: null });
|
||||
fireEvent.submit(screen.getByTestId('modal').querySelector('form'));
|
||||
|
|
@ -507,7 +560,9 @@ describe('ConnectionForm', () => {
|
|||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({
|
||||
url: 'https://example.com/hook', headers: { 'X-Token': 'abc123' } });
|
||||
url: 'https://example.com/hook',
|
||||
headers: { 'X-Token': 'abc123' },
|
||||
});
|
||||
|
||||
renderForm({ connection });
|
||||
fireEvent.submit(screen.getByTestId('modal').querySelector('form'));
|
||||
|
|
@ -522,7 +577,9 @@ describe('ConnectionForm', () => {
|
|||
});
|
||||
|
||||
it('omits headers from webhook config when all header rows are empty', async () => {
|
||||
const connection = makeConnection({ config: { url: 'https://example.com/hook', headers: {} } });
|
||||
const connection = makeConnection({
|
||||
config: { url: 'https://example.com/hook', headers: {} },
|
||||
});
|
||||
const form = makeFormMock({
|
||||
name: 'My Webhook',
|
||||
type: 'webhook',
|
||||
|
|
@ -547,15 +604,22 @@ describe('ConnectionForm', () => {
|
|||
|
||||
describe('subscriptions', () => {
|
||||
it('passes subscription list with enabled flags to setConnectSubscriptions', async () => {
|
||||
const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.buildSubscriptions).mockReturnValue([
|
||||
{ event: 'channel_added', enabled: true, payload_template: null },
|
||||
{ event: 'channel_removed', enabled: false, payload_template: null },
|
||||
{ event: 'recording_started', enabled: true, payload_template: '{"id": "{{id}}"}' },
|
||||
{
|
||||
event: 'recording_started',
|
||||
enabled: true,
|
||||
payload_template: '{"id": "{{id}}"}',
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
renderForm({ connection: null });
|
||||
|
||||
// Toggle channel_added on
|
||||
|
|
@ -567,8 +631,14 @@ describe('ConnectionForm', () => {
|
|||
expect.any(Object),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ event: 'channel_added', enabled: true }),
|
||||
expect.objectContaining({ event: 'channel_removed', enabled: false }),
|
||||
expect.objectContaining({ event: 'recording_started', enabled: true }),
|
||||
expect.objectContaining({
|
||||
event: 'channel_removed',
|
||||
enabled: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
event: 'recording_started',
|
||||
enabled: true,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
|
@ -576,12 +646,20 @@ describe('ConnectionForm', () => {
|
|||
|
||||
it('toggles event off when checkbox is clicked twice', async () => {
|
||||
const connection = makeConnection();
|
||||
const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.buildSubscriptions).mockReturnValue([
|
||||
{ event: 'channel_added', enabled: false, payload_template: null },
|
||||
{ event: 'channel_removed', enabled: false, payload_template: null },
|
||||
{ event: 'recording_started', enabled: true, payload_template: '{"id": "{{id}}"}' },
|
||||
{
|
||||
event: 'recording_started',
|
||||
enabled: true,
|
||||
payload_template: '{"id": "{{id}}"}',
|
||||
},
|
||||
]);
|
||||
|
||||
renderForm({ connection });
|
||||
|
|
@ -595,8 +673,14 @@ describe('ConnectionForm', () => {
|
|||
expect.any(Object),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ event: 'channel_added', enabled: false }),
|
||||
expect.objectContaining({ event: 'channel_removed', enabled: false }),
|
||||
expect.objectContaining({ event: 'recording_started', enabled: true }),
|
||||
expect.objectContaining({
|
||||
event: 'channel_removed',
|
||||
enabled: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
event: 'recording_started',
|
||||
enabled: true,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
|
@ -604,17 +688,26 @@ describe('ConnectionForm', () => {
|
|||
|
||||
it('includes payload_template in subscription when set', async () => {
|
||||
const connection = makeConnection();
|
||||
const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.buildSubscriptions).mockReturnValue([
|
||||
{ event: 'recording_started', enabled: true, payload_template: '{"id": "{{id}}"}' },
|
||||
{
|
||||
event: 'recording_started',
|
||||
enabled: true,
|
||||
payload_template: '{"id": "{{id}}"}',
|
||||
},
|
||||
]);
|
||||
|
||||
renderForm({ connection });
|
||||
fireEvent.submit(screen.getByTestId('modal').querySelector('form'));
|
||||
|
||||
await waitFor(() => {
|
||||
const subs = vi.mocked(ConnectionUtils.setConnectSubscriptions).mock.calls[0][1];
|
||||
const subs = vi.mocked(ConnectionUtils.setConnectSubscriptions).mock
|
||||
.calls[0][1];
|
||||
const recordingSub = subs.find((s) => s.event === 'recording_started');
|
||||
expect(recordingSub).toBeDefined();
|
||||
expect(recordingSub.payload_template).toBe('{"id": "{{id}}"}');
|
||||
|
|
@ -622,14 +715,19 @@ describe('ConnectionForm', () => {
|
|||
});
|
||||
|
||||
it('sends null payload_template when not set for an event', async () => {
|
||||
const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
|
||||
renderForm({ connection: null });
|
||||
fireEvent.submit(screen.getByTestId('modal').querySelector('form'));
|
||||
|
||||
await waitFor(() => {
|
||||
const subs = vi.mocked(ConnectionUtils.setConnectSubscriptions).mock.calls[0][1];
|
||||
const subs = vi.mocked(ConnectionUtils.setConnectSubscriptions).mock
|
||||
.calls[0][1];
|
||||
subs.forEach((s) => expect(s.payload_template).toBeNull());
|
||||
});
|
||||
});
|
||||
|
|
@ -642,9 +740,16 @@ describe('ConnectionForm', () => {
|
|||
vi.mocked(ConnectionUtils.createConnectIntegration).mockRejectedValue(
|
||||
new Error('Server error')
|
||||
);
|
||||
const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: 'Server error' });
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({
|
||||
fieldErrors: {},
|
||||
apiError: 'Server error',
|
||||
});
|
||||
|
||||
renderForm({ connection: null });
|
||||
fireEvent.submit(screen.getByTestId('modal').querySelector('form'));
|
||||
|
|
@ -659,9 +764,16 @@ describe('ConnectionForm', () => {
|
|||
new Error('Update failed')
|
||||
);
|
||||
const connection = makeConnection();
|
||||
const form = makeFormMock({ name: 'My Webhook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'My Webhook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: 'Update failed' });
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({
|
||||
fieldErrors: {},
|
||||
apiError: 'Update failed',
|
||||
});
|
||||
|
||||
renderForm({ connection });
|
||||
fireEvent.submit(screen.getByTestId('modal').querySelector('form'));
|
||||
|
|
@ -675,9 +787,16 @@ describe('ConnectionForm', () => {
|
|||
vi.mocked(ConnectionUtils.createConnectIntegration).mockRejectedValue(
|
||||
new Error('fail')
|
||||
);
|
||||
const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: 'fail' });
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({
|
||||
fieldErrors: {},
|
||||
apiError: 'fail',
|
||||
});
|
||||
|
||||
const onClose = vi.fn();
|
||||
renderForm({ connection: null, onClose });
|
||||
|
|
@ -694,19 +813,30 @@ describe('ConnectionForm', () => {
|
|||
.mockRejectedValueOnce(new Error('First error'))
|
||||
.mockResolvedValueOnce({ id: 99 });
|
||||
|
||||
const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: 'First error' });
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({
|
||||
fieldErrors: {},
|
||||
apiError: 'First error',
|
||||
});
|
||||
|
||||
renderForm({ connection: null });
|
||||
|
||||
// First submit — fails
|
||||
fireEvent.submit(screen.getByTestId('modal').querySelector('form'));
|
||||
await waitFor(() => expect(screen.queryByText('First error')).toBeInTheDocument());
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText('First error')).toBeInTheDocument()
|
||||
);
|
||||
|
||||
// Second submit — succeeds
|
||||
fireEvent.submit(screen.getByTestId('modal').querySelector('form'));
|
||||
await waitFor(() => expect(screen.queryByText('First error')).not.toBeInTheDocument());
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText('First error')).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -727,18 +857,29 @@ describe('ConnectionForm', () => {
|
|||
vi.mocked(ConnectionUtils.createConnectIntegration).mockRejectedValue(
|
||||
new Error('fail')
|
||||
);
|
||||
const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' });
|
||||
const form = makeFormMock({
|
||||
name: 'Hook',
|
||||
type: 'webhook',
|
||||
url: 'https://x.com',
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue(form);
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: 'fail' });
|
||||
vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({
|
||||
fieldErrors: {},
|
||||
apiError: 'fail',
|
||||
});
|
||||
|
||||
renderForm({ connection: null });
|
||||
fireEvent.submit(screen.getByTestId('modal').querySelector('form'));
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('fail')).toBeInTheDocument());
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText('fail')).toBeInTheDocument()
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('modal-close'));
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('fail')).not.toBeInTheDocument());
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText('fail')).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,27 +7,35 @@ vi.mock('../../../utils/forms/CronBuilderUtils.js', () => ({
|
|||
buildCron: vi.fn(),
|
||||
parseCronPreset: vi.fn(),
|
||||
CRON_FIELDS: [
|
||||
{ index: 0, label: 'Minute (0-59)', placeholder: '*, 0, */15' },
|
||||
{ index: 1, label: 'Hour (0-23)', placeholder: '*, 0, 9-17' },
|
||||
{ index: 2, label: 'Day of Month (1-31)', placeholder: '*, 1, 1-15' },
|
||||
{ index: 3, label: 'Month (1-12)', placeholder: '*, 1, 1-6' },
|
||||
{ index: 4, label: 'Day of Week (0-6, Sun-Sat)', placeholder: '*, 0, 1-5' },
|
||||
{ index: 0, label: 'Minute (0-59)', placeholder: '*, 0, */15' },
|
||||
{ index: 1, label: 'Hour (0-23)', placeholder: '*, 0, 9-17' },
|
||||
{ index: 2, label: 'Day of Month (1-31)', placeholder: '*, 1, 1-15' },
|
||||
{ index: 3, label: 'Month (1-12)', placeholder: '*, 1, 1-6' },
|
||||
{ index: 4, label: 'Day of Week (0-6, Sun-Sat)', placeholder: '*, 0, 1-5' },
|
||||
],
|
||||
DAYS_OF_WEEK: [
|
||||
{ value: '*', label: 'Every day' },
|
||||
{ value: '1', label: 'Monday' },
|
||||
{ value: '2', label: 'Tuesday' },
|
||||
{ value: '*', label: 'Every day' },
|
||||
{ value: '1', label: 'Monday' },
|
||||
{ value: '2', label: 'Tuesday' },
|
||||
],
|
||||
FREQUENCY_OPTIONS: [
|
||||
{ value: 'hourly', label: 'Hourly' },
|
||||
{ value: 'daily', label: 'Daily' },
|
||||
{ value: 'weekly', label: 'Weekly' },
|
||||
{ value: 'hourly', label: 'Hourly' },
|
||||
{ value: 'daily', label: 'Daily' },
|
||||
{ value: 'weekly', label: 'Weekly' },
|
||||
{ value: 'monthly', label: 'Monthly' },
|
||||
],
|
||||
PRESETS: [
|
||||
{ label: 'Every Hour', description: 'Runs every hour', value: '0 * * * *' },
|
||||
{ label: 'Every Day 3am', description: 'Runs every day at 3 AM', value: '0 3 * * *' },
|
||||
{ label: 'Every Sunday', description: 'Runs every Sunday at 3AM', value: '0 3 * * 0' },
|
||||
{ label: 'Every Hour', description: 'Runs every hour', value: '0 * * * *' },
|
||||
{
|
||||
label: 'Every Day 3am',
|
||||
description: 'Runs every day at 3 AM',
|
||||
value: '0 3 * * *',
|
||||
},
|
||||
{
|
||||
label: 'Every Sunday',
|
||||
description: 'Runs every Sunday at 3AM',
|
||||
value: '0 3 * * 0',
|
||||
},
|
||||
],
|
||||
updateCronPart: vi.fn(),
|
||||
}));
|
||||
|
|
@ -35,7 +43,12 @@ vi.mock('../../../utils/forms/CronBuilderUtils.js', () => ({
|
|||
// ── Mantine core ───────────────────────────────────────────────────────────────
|
||||
vi.mock('@mantine/core', async () => ({
|
||||
Badge: ({ children, size, variant, color }) => (
|
||||
<span data-testid="badge" data-size={size} data-variant={variant} data-color={color}>
|
||||
<span
|
||||
data-testid="badge"
|
||||
data-size={size}
|
||||
data-variant={variant}
|
||||
data-color={color}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
|
|
@ -51,7 +64,9 @@ vi.mock('@mantine/core', async () => ({
|
|||
opened ? (
|
||||
<div data-testid="modal">
|
||||
<div data-testid="modal-title">{title}</div>
|
||||
<button data-testid="modal-close" onClick={onClose}>×</button>
|
||||
<button data-testid="modal-close" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
|
|
@ -122,7 +137,7 @@ vi.mock('@mantine/core', async () => ({
|
|||
// ── lucide-react ───────────────────────────────────────────────────────────────
|
||||
vi.mock('lucide-react', () => ({
|
||||
Calendar: () => <svg data-testid="icon-calendar" />,
|
||||
Clock: () => <svg data-testid="icon-clock" />,
|
||||
Clock: () => <svg data-testid="icon-clock" />,
|
||||
}));
|
||||
|
||||
// ── Imports after mocks ────────────────────────────────────────────────────────
|
||||
|
|
@ -241,7 +256,9 @@ describe('CronBuilder', () => {
|
|||
it('calls parseCronPreset with the preset value on click', () => {
|
||||
renderBuilder();
|
||||
fireEvent.click(screen.getByText('Every Hour'));
|
||||
expect(CronBuilderUtils.parseCronPreset).toHaveBeenCalledWith('0 * * * *');
|
||||
expect(CronBuilderUtils.parseCronPreset).toHaveBeenCalledWith(
|
||||
'0 * * * *'
|
||||
);
|
||||
});
|
||||
|
||||
it('updates state from parseCronPreset result on preset click', () => {
|
||||
|
|
@ -256,7 +273,11 @@ describe('CronBuilder', () => {
|
|||
fireEvent.click(screen.getByText('Every Hour'));
|
||||
// buildCron should be called with the updated frequency
|
||||
expect(CronBuilderUtils.buildCron).toHaveBeenCalledWith(
|
||||
'hourly', 0, 0, '*', 1
|
||||
'hourly',
|
||||
0,
|
||||
0,
|
||||
'*',
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -268,56 +289,81 @@ describe('CronBuilder', () => {
|
|||
|
||||
it('renders the Frequency select', () => {
|
||||
renderBuilder();
|
||||
expect(within(getSimplePanel()).getByLabelText('Frequency')).toBeInTheDocument();
|
||||
expect(
|
||||
within(getSimplePanel()).getByLabelText('Frequency')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Minute input', () => {
|
||||
renderBuilder();
|
||||
expect(within(getSimplePanel()).getByLabelText('Minute (0-59)')).toBeInTheDocument();
|
||||
expect(
|
||||
within(getSimplePanel()).getByLabelText('Minute (0-59)')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Hour input when frequency is not hourly', () => {
|
||||
renderBuilder();
|
||||
expect(within(getSimplePanel()).getByLabelText('Hour (0-23)')).toBeInTheDocument();
|
||||
expect(
|
||||
within(getSimplePanel()).getByLabelText('Hour (0-23)')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render Hour input when frequency is hourly', () => {
|
||||
renderBuilder();
|
||||
const freqSelect = within(getSimplePanel()).getByLabelText('Frequency');
|
||||
fireEvent.change(freqSelect, { target: { value: 'hourly' } });
|
||||
expect(within(getSimplePanel()).queryByLabelText('Hour (0-23)')).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(getSimplePanel()).queryByLabelText('Hour (0-23)')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Day of Week select when frequency is weekly', () => {
|
||||
renderBuilder();
|
||||
const freqSelect = within(getSimplePanel()).getByLabelText('Frequency');
|
||||
fireEvent.change(freqSelect, { target: { value: 'weekly' } });
|
||||
expect(within(getSimplePanel()).getByLabelText('Day of Week')).toBeInTheDocument();
|
||||
expect(
|
||||
within(getSimplePanel()).getByLabelText('Day of Week')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render Day of Week for daily frequency', () => {
|
||||
renderBuilder();
|
||||
expect(within(getSimplePanel()).queryByLabelText('Day of Week')).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(getSimplePanel()).queryByLabelText('Day of Week')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Day of Month input when frequency is monthly', () => {
|
||||
renderBuilder();
|
||||
const freqSelect = within(getSimplePanel()).getByLabelText('Frequency');
|
||||
fireEvent.change(freqSelect, { target: { value: 'monthly' } });
|
||||
expect(within(getSimplePanel()).getByLabelText('Day of Month (1-31)')).toBeInTheDocument();
|
||||
expect(
|
||||
within(getSimplePanel()).getByLabelText('Day of Month (1-31)')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render Day of Month for daily frequency', () => {
|
||||
renderBuilder();
|
||||
expect(within(getSimplePanel()).queryByLabelText('Day of Month (1-31)')).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(getSimplePanel()).queryByLabelText('Day of Month (1-31)')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls buildCron when minute changes', () => {
|
||||
renderBuilder();
|
||||
fireEvent.change(within(getSimplePanel()).getByLabelText('Minute (0-59)'), {
|
||||
target: { value: 30 },
|
||||
});
|
||||
expect(CronBuilderUtils.buildCron).toHaveBeenCalledWith('daily', 30, 3, '*', 1);
|
||||
fireEvent.change(
|
||||
within(getSimplePanel()).getByLabelText('Minute (0-59)'),
|
||||
{
|
||||
target: { value: 30 },
|
||||
}
|
||||
);
|
||||
expect(CronBuilderUtils.buildCron).toHaveBeenCalledWith(
|
||||
'daily',
|
||||
30,
|
||||
3,
|
||||
'*',
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
it('calls buildCron when hour changes', () => {
|
||||
|
|
@ -325,7 +371,13 @@ describe('CronBuilder', () => {
|
|||
fireEvent.change(within(getSimplePanel()).getByLabelText('Hour (0-23)'), {
|
||||
target: { value: 8 },
|
||||
});
|
||||
expect(CronBuilderUtils.buildCron).toHaveBeenCalledWith('daily', 0, 8, '*', 1);
|
||||
expect(CronBuilderUtils.buildCron).toHaveBeenCalledWith(
|
||||
'daily',
|
||||
0,
|
||||
8,
|
||||
'*',
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -336,31 +388,47 @@ describe('CronBuilder', () => {
|
|||
|
||||
it('renders all 5 cron field inputs', () => {
|
||||
renderBuilder();
|
||||
expect(within(getAdvancedPanel()).getByLabelText('Minute (0-59)')).toBeInTheDocument();
|
||||
expect(within(getAdvancedPanel()).getByLabelText('Hour (0-23)')).toBeInTheDocument();
|
||||
expect(within(getAdvancedPanel()).getByLabelText('Day of Month (1-31)')).toBeInTheDocument();
|
||||
expect(within(getAdvancedPanel()).getByLabelText('Month (1-12)')).toBeInTheDocument();
|
||||
expect(within(getAdvancedPanel()).getByLabelText('Day of Week (0-6, Sun-Sat)')).toBeInTheDocument();
|
||||
expect(
|
||||
within(getAdvancedPanel()).getByLabelText('Minute (0-59)')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(getAdvancedPanel()).getByLabelText('Hour (0-23)')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(getAdvancedPanel()).getByLabelText('Day of Month (1-31)')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(getAdvancedPanel()).getByLabelText('Month (1-12)')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(getAdvancedPanel()).getByLabelText('Day of Week (0-6, Sun-Sat)')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders descriptive helper text', () => {
|
||||
renderBuilder();
|
||||
expect(screen.getByText(/Build advanced cron expressions/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Build advanced cron expressions/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls updateCronPart when an advanced field changes', () => {
|
||||
renderBuilder();
|
||||
const minuteInput = within(getAdvancedPanel()).getByLabelText('Minute (0-59)');
|
||||
const minuteInput =
|
||||
within(getAdvancedPanel()).getByLabelText('Minute (0-59)');
|
||||
fireEvent.change(minuteInput, { target: { value: '30' } });
|
||||
expect(CronBuilderUtils.updateCronPart).toHaveBeenCalledWith(
|
||||
'* * * * *', 0, '30'
|
||||
'* * * * *',
|
||||
0,
|
||||
'30'
|
||||
);
|
||||
});
|
||||
|
||||
it('initializes advanced fields from currentValue when opened', () => {
|
||||
renderBuilder({ currentValue: '5 4 * * 1' });
|
||||
// The minute field (index 0) should display '5'
|
||||
const minuteInput = within(getAdvancedPanel()).getByLabelText('Minute (0-59)');
|
||||
const minuteInput =
|
||||
within(getAdvancedPanel()).getByLabelText('Minute (0-59)');
|
||||
expect(minuteInput).toHaveValue('5');
|
||||
});
|
||||
});
|
||||
|
|
@ -416,7 +484,8 @@ describe('CronBuilder', () => {
|
|||
// Switch to advanced mode by simulating tab onChange
|
||||
// The Tabs mock renders but doesn't fire onChange on tab clicks,
|
||||
// so we directly test that advanced panel input changes manualCron
|
||||
const minuteInput = within(getAdvancedPanel()).getByLabelText('Minute (0-59)');
|
||||
const minuteInput =
|
||||
within(getAdvancedPanel()).getByLabelText('Minute (0-59)');
|
||||
// The advanced panel's minute input value should be '5'
|
||||
expect(minuteInput).toHaveValue('5');
|
||||
});
|
||||
|
|
@ -429,13 +498,15 @@ describe('CronBuilder', () => {
|
|||
|
||||
it('sets manualCron to currentValue when opened', () => {
|
||||
renderBuilder({ currentValue: '*/15 * * * *' });
|
||||
const minuteInput = within(getAdvancedPanel()).getByLabelText('Minute (0-59)');
|
||||
const minuteInput =
|
||||
within(getAdvancedPanel()).getByLabelText('Minute (0-59)');
|
||||
expect(minuteInput).toHaveValue('*/15');
|
||||
});
|
||||
|
||||
it('keeps default manualCron when currentValue is empty', () => {
|
||||
renderBuilder({ currentValue: '' });
|
||||
const minuteInput = within(getAdvancedPanel()).getByLabelText('Minute (0-59)');
|
||||
const minuteInput =
|
||||
within(getAdvancedPanel()).getByLabelText('Minute (0-59)');
|
||||
expect(minuteInput).toHaveValue('*');
|
||||
});
|
||||
|
||||
|
|
@ -444,4 +515,4 @@ describe('CronBuilder', () => {
|
|||
expect(screen.queryByTestId('modal')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,58 +27,58 @@ vi.mock('../../../utils/forms/DummyEpgUtils.js', () => ({
|
|||
const fill = (tpl) =>
|
||||
tpl
|
||||
? Object.entries(allGroups).reduce(
|
||||
(s, [k, v]) => s.replaceAll(`{${k}}`, v),
|
||||
tpl
|
||||
)
|
||||
(s, [k, v]) => s.replaceAll(`{${k}}`, v),
|
||||
tpl
|
||||
)
|
||||
: '';
|
||||
return {
|
||||
formattedTitle: fill(templates.titleTemplate),
|
||||
formattedSubtitle: fill(templates.subtitleTemplate),
|
||||
formattedDescription: fill(templates.descriptionTemplate),
|
||||
formattedUpcomingTitle: fill(templates.upcomingTitleTemplate),
|
||||
formattedUpcomingDescription: fill(templates.upcomingDescriptionTemplate),
|
||||
formattedEndedTitle: fill(templates.endedTitleTemplate),
|
||||
formattedEndedDescription: fill(templates.endedDescriptionTemplate),
|
||||
formattedChannelLogoUrl: fill(templates.channelLogoUrl),
|
||||
formattedProgramPosterUrl: fill(templates.programPosterUrl),
|
||||
formattedTitle: fill(templates.titleTemplate),
|
||||
formattedSubtitle: fill(templates.subtitleTemplate),
|
||||
formattedDescription: fill(templates.descriptionTemplate),
|
||||
formattedUpcomingTitle: fill(templates.upcomingTitleTemplate),
|
||||
formattedUpcomingDescription: fill(templates.upcomingDescriptionTemplate),
|
||||
formattedEndedTitle: fill(templates.endedTitleTemplate),
|
||||
formattedEndedDescription: fill(templates.endedDescriptionTemplate),
|
||||
formattedChannelLogoUrl: fill(templates.channelLogoUrl),
|
||||
formattedProgramPosterUrl: fill(templates.programPosterUrl),
|
||||
};
|
||||
}),
|
||||
buildCustomProperties: vi.fn((custom = {}) => ({
|
||||
title_pattern: custom.title_pattern || '',
|
||||
time_pattern: custom.time_pattern || '',
|
||||
date_pattern: custom.date_pattern || '',
|
||||
timezone: custom.timezone || 'US/Eastern',
|
||||
output_timezone: custom.output_timezone || '',
|
||||
program_duration: custom.program_duration || 180,
|
||||
sample_title: custom.sample_title || '',
|
||||
title_template: custom.title_template || '',
|
||||
subtitle_template: custom.subtitle_template || '',
|
||||
description_template: custom.description_template || '',
|
||||
upcoming_title_template: custom.upcoming_title_template || '',
|
||||
upcoming_description_template:custom.upcoming_description_template|| '',
|
||||
ended_title_template: custom.ended_title_template || '',
|
||||
ended_description_template: custom.ended_description_template || '',
|
||||
fallback_title_template: custom.fallback_title_template || '',
|
||||
fallback_description_template:custom.fallback_description_template|| '',
|
||||
channel_logo_url: custom.channel_logo_url || '',
|
||||
program_poster_url: custom.program_poster_url || '',
|
||||
name_source: custom.name_source || 'channel',
|
||||
stream_index: custom.stream_index || 1,
|
||||
category: custom.category || '',
|
||||
include_date: custom.include_date ?? true,
|
||||
include_live: custom.include_live ?? false,
|
||||
include_new: custom.include_new ?? false,
|
||||
title_pattern: custom.title_pattern || '',
|
||||
time_pattern: custom.time_pattern || '',
|
||||
date_pattern: custom.date_pattern || '',
|
||||
timezone: custom.timezone || 'US/Eastern',
|
||||
output_timezone: custom.output_timezone || '',
|
||||
program_duration: custom.program_duration || 180,
|
||||
sample_title: custom.sample_title || '',
|
||||
title_template: custom.title_template || '',
|
||||
subtitle_template: custom.subtitle_template || '',
|
||||
description_template: custom.description_template || '',
|
||||
upcoming_title_template: custom.upcoming_title_template || '',
|
||||
upcoming_description_template: custom.upcoming_description_template || '',
|
||||
ended_title_template: custom.ended_title_template || '',
|
||||
ended_description_template: custom.ended_description_template || '',
|
||||
fallback_title_template: custom.fallback_title_template || '',
|
||||
fallback_description_template: custom.fallback_description_template || '',
|
||||
channel_logo_url: custom.channel_logo_url || '',
|
||||
program_poster_url: custom.program_poster_url || '',
|
||||
name_source: custom.name_source || 'channel',
|
||||
stream_index: custom.stream_index || 1,
|
||||
category: custom.category || '',
|
||||
include_date: custom.include_date ?? true,
|
||||
include_live: custom.include_live ?? false,
|
||||
include_new: custom.include_new ?? false,
|
||||
})),
|
||||
buildTimePlaceholders: vi.fn((timeGroups) => {
|
||||
if (!timeGroups || !timeGroups.hour) return {};
|
||||
const hour = parseInt(timeGroups.hour, 10);
|
||||
const hour = parseInt(timeGroups.hour, 10);
|
||||
const minute = String(timeGroups.minute ?? '00').padStart(2, '0');
|
||||
const ampm = timeGroups.ampm ? ` ${timeGroups.ampm}` : '';
|
||||
const ampm = timeGroups.ampm ? ` ${timeGroups.ampm}` : '';
|
||||
return {
|
||||
starttime: `${hour}:${minute}${ampm}`,
|
||||
starttime: `${hour}:${minute}${ampm}`,
|
||||
starttime24: `${String(hour).padStart(2, '0')}:${minute}`,
|
||||
endtime: `${(hour + 3) % 24}:${minute}${ampm}`,
|
||||
endtime24: `${String((hour + 3) % 24).padStart(2, '0')}:${minute}`,
|
||||
endtime: `${(hour + 3) % 24}:${minute}${ampm}`,
|
||||
endtime24: `${String((hour + 3) % 24).padStart(2, '0')}:${minute}`,
|
||||
};
|
||||
}),
|
||||
getDummyEpgFormInitialValues: vi.fn(() => ({
|
||||
|
|
@ -86,30 +86,30 @@ vi.mock('../../../utils/forms/DummyEpgUtils.js', () => ({
|
|||
is_active: true,
|
||||
source_type: 'dummy',
|
||||
custom_properties: {
|
||||
title_pattern: '',
|
||||
time_pattern: '',
|
||||
date_pattern: '',
|
||||
timezone: 'US/Eastern',
|
||||
output_timezone: '',
|
||||
program_duration: 180,
|
||||
sample_title: '',
|
||||
title_template: '',
|
||||
subtitle_template: '',
|
||||
description_template: '',
|
||||
upcoming_title_template: '',
|
||||
upcoming_description_template:'',
|
||||
ended_title_template: '',
|
||||
ended_description_template: '',
|
||||
fallback_title_template: '',
|
||||
fallback_description_template:'',
|
||||
channel_logo_url: '',
|
||||
program_poster_url: '',
|
||||
name_source: 'channel',
|
||||
stream_index: 1,
|
||||
category: '',
|
||||
include_date: true,
|
||||
include_live: false,
|
||||
include_new: false,
|
||||
title_pattern: '',
|
||||
time_pattern: '',
|
||||
date_pattern: '',
|
||||
timezone: 'US/Eastern',
|
||||
output_timezone: '',
|
||||
program_duration: 180,
|
||||
sample_title: '',
|
||||
title_template: '',
|
||||
subtitle_template: '',
|
||||
description_template: '',
|
||||
upcoming_title_template: '',
|
||||
upcoming_description_template: '',
|
||||
ended_title_template: '',
|
||||
ended_description_template: '',
|
||||
fallback_title_template: '',
|
||||
fallback_description_template: '',
|
||||
channel_logo_url: '',
|
||||
program_poster_url: '',
|
||||
name_source: 'channel',
|
||||
stream_index: 1,
|
||||
category: '',
|
||||
include_date: true,
|
||||
include_live: false,
|
||||
include_new: false,
|
||||
},
|
||||
})),
|
||||
getTimezones: vi.fn().mockResolvedValue({
|
||||
|
|
@ -118,19 +118,23 @@ vi.mock('../../../utils/forms/DummyEpgUtils.js', () => ({
|
|||
matchPattern: vi.fn((pattern, sample, errorLabel = 'Pattern error') => {
|
||||
if (!pattern) return { matched: false, groups: {}, error: null };
|
||||
try {
|
||||
const regex = new RegExp(pattern, 'u');
|
||||
const regex = new RegExp(pattern, 'u');
|
||||
const result = regex.exec(sample ?? '');
|
||||
if (!result) return { matched: false, groups: {}, error: null };
|
||||
const groups = result.groups ?? {};
|
||||
return { matched: true, groups, error: null };
|
||||
} catch {
|
||||
return { matched: false, groups: {}, error: `${errorLabel}: invalid regex` };
|
||||
return {
|
||||
matched: false,
|
||||
groups: {},
|
||||
error: `${errorLabel}: invalid regex`,
|
||||
};
|
||||
}
|
||||
}),
|
||||
updateEPG: vi.fn(),
|
||||
validateCustomNameSource: vi.fn(() => null),
|
||||
validateCustomStreamIndex: vi.fn(() => null),
|
||||
validateCustomTitlePattern: vi.fn(() => null),
|
||||
validateCustomNameSource: vi.fn(() => null),
|
||||
validateCustomStreamIndex: vi.fn(() => null),
|
||||
validateCustomTitlePattern: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
// ── Mantine notifications ──────────────────────────────────────────────────────
|
||||
|
|
@ -141,13 +145,25 @@ vi.mock('@mantine/notifications', () => ({
|
|||
// ── Mantine core ───────────────────────────────────────────────────────────────
|
||||
vi.mock('@mantine/core', async () => ({
|
||||
Accordion: ({ children }) => <div data-testid="accordion">{children}</div>,
|
||||
AccordionControl: ({ children }) => <button data-testid="accordion-control">{children}</button>,
|
||||
AccordionItem: ({ children, value }) => <div data-testid={`accordion-item-${value}`}>{children}</div>,
|
||||
AccordionPanel: ({ children }) => <div data-testid="accordion-panel">{children}</div>,
|
||||
ActionIcon: ({ children, onClick }) => (
|
||||
<button data-testid="action-icon" onClick={onClick}>{children}</button>
|
||||
AccordionControl: ({ children }) => (
|
||||
<button data-testid="accordion-control">{children}</button>
|
||||
),
|
||||
AccordionItem: ({ children, value }) => (
|
||||
<div data-testid={`accordion-item-${value}`}>{children}</div>
|
||||
),
|
||||
AccordionPanel: ({ children }) => (
|
||||
<div data-testid="accordion-panel">{children}</div>
|
||||
),
|
||||
ActionIcon: ({ children, onClick }) => (
|
||||
<button data-testid="action-icon" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Box: ({ children, mt, style }) => (
|
||||
<div style={style} data-mt={mt}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Box: ({ children, mt, style }) => <div style={style} data-mt={mt}>{children}</div>,
|
||||
Button: ({ children, onClick, disabled, loading, type, color, variant }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
|
|
@ -177,7 +193,9 @@ vi.mock('@mantine/core', async () => ({
|
|||
opened ? (
|
||||
<div data-testid="modal">
|
||||
<div data-testid="modal-title">{title}</div>
|
||||
<button data-testid="modal-close" onClick={onClose}>×</button>
|
||||
<button data-testid="modal-close" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
|
|
@ -196,7 +214,9 @@ vi.mock('@mantine/core', async () => ({
|
|||
),
|
||||
Paper: ({ children }) => <div data-testid="paper">{children}</div>,
|
||||
Popover: ({ children }) => <div>{children}</div>,
|
||||
PopoverDropdown: ({ children }) => <div data-testid="popover-dropdown">{children}</div>,
|
||||
PopoverDropdown: ({ children }) => (
|
||||
<div data-testid="popover-dropdown">{children}</div>
|
||||
),
|
||||
PopoverTarget: ({ children }) => <div>{children}</div>,
|
||||
Select: ({ label, value, onChange, data, placeholder }) => (
|
||||
<label>
|
||||
|
|
@ -210,14 +230,20 @@ vi.mock('@mantine/core', async () => ({
|
|||
{(data || []).map((opt) => {
|
||||
const val = typeof opt === 'string' ? opt : opt.value;
|
||||
const lab = typeof opt === 'string' ? opt : opt.label;
|
||||
return <option key={val} value={val}>{lab}</option>;
|
||||
return (
|
||||
<option key={val} value={val}>
|
||||
{lab}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
),
|
||||
Stack: ({ children }) => <div>{children}</div>,
|
||||
Text: ({ children, size, c, fw, style }) => (
|
||||
<span data-size={size} data-color={c} data-fw={fw} style={style}>{children}</span>
|
||||
<span data-size={size} data-color={c} data-fw={fw} style={style}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
Textarea: ({ label, value, onChange, placeholder }) => (
|
||||
<label>
|
||||
|
|
@ -324,7 +350,10 @@ const defaultProps = (overrides = {}) => ({
|
|||
describe('DummyEPGForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(DummyEpgUtils.addEPG).mockResolvedValue({ id: 2, name: 'New EPG' });
|
||||
vi.mocked(DummyEpgUtils.addEPG).mockResolvedValue({
|
||||
id: 2,
|
||||
name: 'New EPG',
|
||||
});
|
||||
vi.mocked(DummyEpgUtils.updateEPG).mockResolvedValue({});
|
||||
vi.mocked(DummyEpgUtils.getTimezones).mockResolvedValue({
|
||||
timezones: ['US/Eastern', 'US/Pacific', 'UTC'],
|
||||
|
|
@ -347,12 +376,16 @@ describe('DummyEPGForm', () => {
|
|||
|
||||
it('shows "Add Dummy EPG" title when no epg prop', () => {
|
||||
render(<DummyEPGForm {...defaultProps()} />);
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent('Create Dummy EPG');
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent(
|
||||
'Create Dummy EPG'
|
||||
);
|
||||
});
|
||||
|
||||
it('shows "Edit Dummy EPG" title when epg prop is provided', () => {
|
||||
render(<DummyEPGForm {...defaultProps({ epg: makeEPG() })} />);
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent('Edit Dummy EPG');
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent(
|
||||
'Edit Dummy EPG'
|
||||
);
|
||||
});
|
||||
|
||||
it('pre-fills form name when editing an existing EPG', () => {
|
||||
|
|
@ -395,12 +428,16 @@ describe('DummyEPGForm', () => {
|
|||
it('renders timezone options after loading', async () => {
|
||||
render(<DummyEPGForm {...defaultProps()} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('option', { name: 'US/Eastern' }).length).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole('option', { name: 'US/Eastern' }).length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows fallback timezones and warning notification when getTimezones rejects', async () => {
|
||||
vi.mocked(DummyEpgUtils.getTimezones).mockRejectedValueOnce(new Error('Network error'));
|
||||
vi.mocked(DummyEpgUtils.getTimezones).mockRejectedValueOnce(
|
||||
new Error('Network error')
|
||||
);
|
||||
render(<DummyEPGForm {...defaultProps()} />);
|
||||
await waitFor(() => {
|
||||
expect(showNotification).toHaveBeenCalledWith(
|
||||
|
|
@ -409,7 +446,9 @@ describe('DummyEPGForm', () => {
|
|||
});
|
||||
// Fallback options should still be rendered
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('option', { name: 'UTC' }).length).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole('option', { name: 'UTC' }).length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -420,13 +459,17 @@ describe('DummyEPGForm', () => {
|
|||
it('shows the import select when dummyEpgs are available', () => {
|
||||
setupMocks({ dummyEpgs: [makeTemplate()] });
|
||||
render(<DummyEPGForm {...defaultProps()} />);
|
||||
expect(screen.getByPlaceholderText(/Select a template/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByPlaceholderText(/Select a template/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show import select when dummyEpgs is empty', () => {
|
||||
setupMocks({ dummyEpgs: [] });
|
||||
render(<DummyEPGForm {...defaultProps()} />);
|
||||
expect(screen.queryByPlaceholderText(/Select a template/i)).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByPlaceholderText(/Select a template/i)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls buildCustomProperties and applyCustomState on template import', async () => {
|
||||
|
|
@ -465,7 +508,9 @@ describe('DummyEPGForm', () => {
|
|||
fireEvent.change(select, { target: { value: '99' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue('My Template (Copy)')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByDisplayValue('My Template (Copy)')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -551,7 +596,9 @@ describe('DummyEPGForm', () => {
|
|||
});
|
||||
render(<DummyEPGForm {...defaultProps({ epg })} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('(no template provided)')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('(no template provided)')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -559,8 +606,8 @@ describe('DummyEPGForm', () => {
|
|||
const epg = makeEPG({
|
||||
custom_properties: {
|
||||
title_pattern: '',
|
||||
date_pattern: '(?<month>\\w+)\\s+(?<day>\\d+)',
|
||||
sample_title: 'Show Oct 17',
|
||||
date_pattern: '(?<month>\\w+)\\s+(?<day>\\d+)',
|
||||
sample_title: 'Show Oct 17',
|
||||
},
|
||||
});
|
||||
render(<DummyEPGForm {...defaultProps({ epg })} />);
|
||||
|
|
@ -573,14 +620,16 @@ describe('DummyEPGForm', () => {
|
|||
const epg = makeEPG({
|
||||
custom_properties: {
|
||||
title_pattern: '(Show.+)',
|
||||
time_pattern: '(?<hour>\\d+):(?<minute>\\d+)\\s*(?<ampm>AM|PM)',
|
||||
sample_title: 'Show @ 9:00 PM',
|
||||
timezone: 'US/Eastern',
|
||||
time_pattern: '(?<hour>\\d+):(?<minute>\\d+)\\s*(?<ampm>AM|PM)',
|
||||
sample_title: 'Show @ 9:00 PM',
|
||||
timezone: 'US/Eastern',
|
||||
},
|
||||
});
|
||||
render(<DummyEPGForm {...defaultProps({ epg })} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/available time placeholders/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/available time placeholders/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -638,7 +687,9 @@ describe('DummyEPGForm', () => {
|
|||
});
|
||||
|
||||
it('shows error notification when addEPG rejects', async () => {
|
||||
vi.mocked(DummyEpgUtils.addEPG).mockRejectedValueOnce(new Error('Server error'));
|
||||
vi.mocked(DummyEpgUtils.addEPG).mockRejectedValueOnce(
|
||||
new Error('Server error')
|
||||
);
|
||||
render(<DummyEPGForm {...defaultProps()} />);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('My Sports EPG');
|
||||
|
|
@ -653,7 +704,9 @@ describe('DummyEPGForm', () => {
|
|||
});
|
||||
|
||||
it('shows error notification when updateEPG rejects', async () => {
|
||||
vi.mocked(DummyEpgUtils.updateEPG).mockRejectedValueOnce(new Error('Update failed'));
|
||||
vi.mocked(DummyEpgUtils.updateEPG).mockRejectedValueOnce(
|
||||
new Error('Update failed')
|
||||
);
|
||||
render(<DummyEPGForm {...defaultProps({ epg: makeEPG() })} />);
|
||||
fireEvent.submit(document.querySelector('form'));
|
||||
|
||||
|
|
@ -687,7 +740,9 @@ describe('DummyEPGForm', () => {
|
|||
fireEvent.submit(document.querySelector('form'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(showNotification).toHaveBeenCalledWith(expect.objectContaining({ color: 'red' }));
|
||||
expect(showNotification).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ color: 'red' })
|
||||
);
|
||||
});
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -743,4 +798,4 @@ describe('DummyEPGForm', () => {
|
|||
expect(screen.getByDisplayValue('180')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ vi.mock('@mantine/form', () => ({
|
|||
// Return the live object — no spread — so setFieldValue mutations are visible
|
||||
getValues: vi.fn(() => values),
|
||||
setValues: vi.fn((v) => Object.assign(values, v)),
|
||||
setFieldValue: vi.fn((field, value) => { values[field] = value; }),
|
||||
setFieldValue: vi.fn((field, value) => {
|
||||
values[field] = value;
|
||||
}),
|
||||
reset: vi.fn(),
|
||||
submitting: false,
|
||||
onSubmit: vi.fn((handler) => (e) => {
|
||||
|
|
@ -48,12 +50,23 @@ vi.mock('@mantine/form', () => ({
|
|||
|
||||
// ── ScheduleInput mock ─────────────────────────────────────────────────────────
|
||||
vi.mock('../ScheduleInput', () => ({
|
||||
default: ({ scheduleType, onScheduleTypeChange, onIntervalChange, onCronChange }) => (
|
||||
default: ({
|
||||
scheduleType,
|
||||
onScheduleTypeChange,
|
||||
onIntervalChange,
|
||||
onCronChange,
|
||||
}) => (
|
||||
<div data-testid="schedule-input">
|
||||
<button data-testid="set-interval" onClick={() => onScheduleTypeChange('interval')}>
|
||||
<button
|
||||
data-testid="set-interval"
|
||||
onClick={() => onScheduleTypeChange('interval')}
|
||||
>
|
||||
Set Interval
|
||||
</button>
|
||||
<button data-testid="set-cron" onClick={() => onScheduleTypeChange('cron')}>
|
||||
<button
|
||||
data-testid="set-cron"
|
||||
onClick={() => onScheduleTypeChange('cron')}
|
||||
>
|
||||
Set Cron
|
||||
</button>
|
||||
<input
|
||||
|
|
@ -117,7 +130,11 @@ vi.mock('@mantine/core', async () => ({
|
|||
{(data ?? []).map((opt) => {
|
||||
const val = typeof opt === 'string' ? opt : opt.value;
|
||||
const lbl = typeof opt === 'string' ? opt : opt.label;
|
||||
return <option key={val} value={val}>{lbl}</option>;
|
||||
return (
|
||||
<option key={val} value={val}>
|
||||
{lbl}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
),
|
||||
|
|
@ -139,7 +156,16 @@ vi.mock('@mantine/core', async () => ({
|
|||
{children}
|
||||
</span>
|
||||
),
|
||||
TextInput: ({ label, value, onChange, placeholder, error, id, name, required }) => (
|
||||
TextInput: ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
error,
|
||||
id,
|
||||
name,
|
||||
required,
|
||||
}) => (
|
||||
<div>
|
||||
<label htmlFor={id || name}>{label}</label>
|
||||
<input
|
||||
|
|
@ -149,7 +175,10 @@ vi.mock('@mantine/core', async () => ({
|
|||
placeholder={placeholder}
|
||||
required={required}
|
||||
onChange={(e) =>
|
||||
onChange?.({ target: { value: e.target.value }, currentTarget: { value: e.target.value } })
|
||||
onChange?.({
|
||||
target: { value: e.target.value },
|
||||
currentTarget: { value: e.target.value },
|
||||
})
|
||||
}
|
||||
/>
|
||||
{error && <span data-testid="input-error">{error}</span>}
|
||||
|
|
@ -243,7 +272,9 @@ describe('EPG', () => {
|
|||
|
||||
it('renders a cancel/close button', () => {
|
||||
render(<EPG {...defaultProps()} />);
|
||||
expect(screen.getByRole('button', { name: /cancel|close/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /cancel|close/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -257,11 +288,20 @@ describe('EPG', () => {
|
|||
key: vi.fn(),
|
||||
getValues: vi.fn(() => ({ ...epg, cron_expression: '' })),
|
||||
setValues: mockSetValues,
|
||||
setFieldValue: vi.fn((field, value) => { epg[field] = value; }),
|
||||
setFieldValue: vi.fn((field, value) => {
|
||||
epg[field] = value;
|
||||
}),
|
||||
reset: vi.fn(),
|
||||
submitting: false,
|
||||
onSubmit: vi.fn((h) => (e) => { e?.preventDefault?.(); h({ ...epg }); }),
|
||||
getInputProps: vi.fn((field) => ({ value: epg[field] ?? '', onChange: vi.fn(), error: null })),
|
||||
onSubmit: vi.fn((h) => (e) => {
|
||||
e?.preventDefault?.();
|
||||
h({ ...epg });
|
||||
}),
|
||||
getInputProps: vi.fn((field) => ({
|
||||
value: epg[field] ?? '',
|
||||
onChange: vi.fn(),
|
||||
error: null,
|
||||
})),
|
||||
});
|
||||
|
||||
render(<EPG {...defaultProps({ epg })} />);
|
||||
|
|
@ -279,8 +319,15 @@ describe('EPG', () => {
|
|||
setFieldValue: vi.fn(() => {}),
|
||||
reset: vi.fn(),
|
||||
submitting: false,
|
||||
onSubmit: vi.fn((h) => (e) => { e?.preventDefault?.(); h({}); }),
|
||||
getInputProps: vi.fn(() => ({ value: '', onChange: vi.fn(), error: null })),
|
||||
onSubmit: vi.fn((h) => (e) => {
|
||||
e?.preventDefault?.();
|
||||
h({});
|
||||
}),
|
||||
getInputProps: vi.fn(() => ({
|
||||
value: '',
|
||||
onChange: vi.fn(),
|
||||
error: null,
|
||||
})),
|
||||
});
|
||||
|
||||
render(<EPG {...defaultProps({ epg: null })} />);
|
||||
|
|
@ -288,15 +335,22 @@ describe('EPG', () => {
|
|||
});
|
||||
|
||||
it('sets scheduleType to "cron" when epg has a cron_expression', () => {
|
||||
const epg = makeEPG({ cron_expression: '0 */6 * * *', refresh_interval: 0 });
|
||||
const epg = makeEPG({
|
||||
cron_expression: '0 */6 * * *',
|
||||
refresh_interval: 0,
|
||||
});
|
||||
render(<EPG {...defaultProps({ epg })} />);
|
||||
expect(screen.getByTestId('schedule-type-value')).toHaveTextContent('cron');
|
||||
expect(screen.getByTestId('schedule-type-value')).toHaveTextContent(
|
||||
'cron'
|
||||
);
|
||||
});
|
||||
|
||||
it('sets scheduleType to "interval" when epg has no cron_expression', () => {
|
||||
const epg = makeEPG({ cron_expression: '', refresh_interval: 12 });
|
||||
render(<EPG {...defaultProps({ epg })} />);
|
||||
expect(screen.getByTestId('schedule-type-value')).toHaveTextContent('interval');
|
||||
expect(screen.getByTestId('schedule-type-value')).toHaveTextContent(
|
||||
'interval'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -331,20 +385,26 @@ describe('EPG', () => {
|
|||
describe('schedule type toggling', () => {
|
||||
it('scheduleType starts as "interval" for new EPG', () => {
|
||||
render(<EPG {...defaultProps()} />);
|
||||
expect(screen.getByTestId('schedule-type-value')).toHaveTextContent('interval');
|
||||
expect(screen.getByTestId('schedule-type-value')).toHaveTextContent(
|
||||
'interval'
|
||||
);
|
||||
});
|
||||
|
||||
it('updates scheduleType to "cron" when ScheduleInput fires onScheduleTypeChange', () => {
|
||||
render(<EPG {...defaultProps()} />);
|
||||
fireEvent.click(screen.getByTestId('set-cron'));
|
||||
expect(screen.getByTestId('schedule-type-value')).toHaveTextContent('cron');
|
||||
expect(screen.getByTestId('schedule-type-value')).toHaveTextContent(
|
||||
'cron'
|
||||
);
|
||||
});
|
||||
|
||||
it('updates scheduleType back to "interval" from cron', () => {
|
||||
render(<EPG {...defaultProps()} />);
|
||||
fireEvent.click(screen.getByTestId('set-cron'));
|
||||
fireEvent.click(screen.getByTestId('set-interval'));
|
||||
expect(screen.getByTestId('schedule-type-value')).toHaveTextContent('interval');
|
||||
expect(screen.getByTestId('schedule-type-value')).toHaveTextContent(
|
||||
'interval'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -415,16 +475,28 @@ describe('EPG', () => {
|
|||
});
|
||||
|
||||
it('clears refresh_interval when schedule type is cron on submit', async () => {
|
||||
const epg = makeEPG({ cron_expression: '0 * * * *', refresh_interval: 24 });
|
||||
const epg = makeEPG({
|
||||
cron_expression: '0 * * * *',
|
||||
refresh_interval: 24,
|
||||
});
|
||||
vi.mocked(useForm).mockReturnValue({
|
||||
key: vi.fn(),
|
||||
getValues: vi.fn(() => ({ ...epg })),
|
||||
setValues: vi.fn((v) => Object.assign(epg, v)),
|
||||
setFieldValue: vi.fn((field, value) => { epg[field] = value; }),
|
||||
setFieldValue: vi.fn((field, value) => {
|
||||
epg[field] = value;
|
||||
}),
|
||||
reset: vi.fn(),
|
||||
submitting: false,
|
||||
onSubmit: vi.fn((h) => (e) => { e?.preventDefault?.(); h({ ...epg }); }),
|
||||
getInputProps: vi.fn((field) => ({ value: epg[field] ?? '', onChange: vi.fn(), error: null })),
|
||||
onSubmit: vi.fn((h) => (e) => {
|
||||
e?.preventDefault?.();
|
||||
h({ ...epg });
|
||||
}),
|
||||
getInputProps: vi.fn((field) => ({
|
||||
value: epg[field] ?? '',
|
||||
onChange: vi.fn(),
|
||||
error: null,
|
||||
})),
|
||||
});
|
||||
render(<EPG {...defaultProps({ epg })} />);
|
||||
fireEvent.click(screen.getByText('Update EPG Source'));
|
||||
|
|
@ -442,11 +514,20 @@ describe('EPG', () => {
|
|||
key: vi.fn(),
|
||||
getValues: vi.fn(() => ({ ...epg })),
|
||||
setValues: vi.fn((v) => Object.assign(epg, v)),
|
||||
setFieldValue: vi.fn((field, value) => { epg[field] = value; }),
|
||||
setFieldValue: vi.fn((field, value) => {
|
||||
epg[field] = value;
|
||||
}),
|
||||
reset: vi.fn(),
|
||||
submitting: false,
|
||||
onSubmit: vi.fn((h) => (e) => { e?.preventDefault?.(); h({ ...epg }); }),
|
||||
getInputProps: vi.fn((field) => ({ value: epg[field] ?? '', onChange: vi.fn(), error: null })),
|
||||
onSubmit: vi.fn((h) => (e) => {
|
||||
e?.preventDefault?.();
|
||||
h({ ...epg });
|
||||
}),
|
||||
getInputProps: vi.fn((field) => ({
|
||||
value: epg[field] ?? '',
|
||||
onChange: vi.fn(),
|
||||
error: null,
|
||||
})),
|
||||
});
|
||||
render(<EPG {...defaultProps({ epg })} />);
|
||||
fireEvent.click(screen.getByText('Update EPG Source'));
|
||||
|
|
@ -497,8 +578,15 @@ describe('EPG', () => {
|
|||
setFieldValue: vi.fn(() => {}),
|
||||
reset: mockReset,
|
||||
submitting: false,
|
||||
onSubmit: vi.fn((h) => (e) => { e?.preventDefault?.(); h({}); }),
|
||||
getInputProps: vi.fn(() => ({ value: '', onChange: vi.fn(), error: null })),
|
||||
onSubmit: vi.fn((h) => (e) => {
|
||||
e?.preventDefault?.();
|
||||
h({});
|
||||
}),
|
||||
getInputProps: vi.fn(() => ({
|
||||
value: '',
|
||||
onChange: vi.fn(),
|
||||
error: null,
|
||||
})),
|
||||
});
|
||||
render(<EPG {...defaultProps()} />);
|
||||
fireEvent.click(screen.getByText('Create EPG Source'));
|
||||
|
|
@ -513,7 +601,9 @@ describe('EPG', () => {
|
|||
describe('active checkbox', () => {
|
||||
it('renders the Active checkbox', () => {
|
||||
render(<EPG {...defaultProps()} />);
|
||||
expect(screen.getByTestId('checkbox-enable-this-epg-source')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('checkbox-enable-this-epg-source')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -614,8 +614,18 @@ describe('dateTimeUtils', () => {
|
|||
|
||||
it('should contain all lowercase month names', () => {
|
||||
expect(dateTimeUtils.MONTH_NAMES).toEqual([
|
||||
'january', 'february', 'march', 'april', 'may', 'june',
|
||||
'july', 'august', 'september', 'october', 'november', 'december',
|
||||
'january',
|
||||
'february',
|
||||
'march',
|
||||
'april',
|
||||
'may',
|
||||
'june',
|
||||
'july',
|
||||
'august',
|
||||
'september',
|
||||
'october',
|
||||
'november',
|
||||
'december',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -635,8 +645,18 @@ describe('dateTimeUtils', () => {
|
|||
|
||||
it('should contain all lowercase abbreviated month names', () => {
|
||||
expect(dateTimeUtils.MONTH_ABBR).toEqual([
|
||||
'jan', 'feb', 'mar', 'apr', 'may', 'jun',
|
||||
'jul', 'aug', 'sep', 'oct', 'nov', 'dec',
|
||||
'jan',
|
||||
'feb',
|
||||
'mar',
|
||||
'apr',
|
||||
'may',
|
||||
'jun',
|
||||
'jul',
|
||||
'aug',
|
||||
'sep',
|
||||
'oct',
|
||||
'nov',
|
||||
'dec',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -183,4 +183,4 @@ export const buildEpgAssociations = async (
|
|||
|
||||
if (!epgData) return null;
|
||||
return channelIds.map((id) => ({ channel_id: id, epg_data_id: epgData.id }));
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -94,4 +94,4 @@ export const handleEpgUpdate = async (
|
|||
streams: channelStreams.map((stream) => stream.id),
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,4 +72,4 @@ export const parseApiError = (error) => {
|
|||
(Object.keys(fieldErrors).length === 0 ? JSON.stringify(body) : '');
|
||||
|
||||
return { fieldErrors, apiError };
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -263,4 +263,4 @@ export const applyTemplates = (templateValues, groups, hasMatch) => {
|
|||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// ── API mock ───────────────────────────────────────────────────────────────────
|
||||
vi.mock('../../../api.js', () => ({ default: { refreshAccountInfo: vi.fn() } }));
|
||||
vi.mock('../../../api.js', () => ({
|
||||
default: { refreshAccountInfo: vi.fn() },
|
||||
}));
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Imports after mocks
|
||||
|
|
@ -118,7 +120,9 @@ describe('AccountInfoModalUtils', () => {
|
|||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-01-10T00:00:00Z'));
|
||||
// Jan 1, 2024 Unix timestamp
|
||||
const past = String(Math.floor(new Date('2024-01-01T00:00:00Z').getTime() / 1000));
|
||||
const past = String(
|
||||
Math.floor(new Date('2024-01-01T00:00:00Z').getTime() / 1000)
|
||||
);
|
||||
expect(getTimeRemaining(past)).toBe('Expired');
|
||||
});
|
||||
|
||||
|
|
@ -211,7 +215,9 @@ describe('AccountInfoModalUtils', () => {
|
|||
it('propagates rejection from API.refreshAccountInfo', async () => {
|
||||
const error = new Error('network error');
|
||||
vi.mocked(API.refreshAccountInfo).mockRejectedValue(error);
|
||||
await expect(refreshAccountInfo({ id: 'profile-123' })).rejects.toThrow('network error');
|
||||
await expect(refreshAccountInfo({ id: 'profile-123' })).rejects.toThrow(
|
||||
'network error'
|
||||
);
|
||||
});
|
||||
|
||||
it('calls API.refreshAccountInfo exactly once', async () => {
|
||||
|
|
@ -227,4 +233,4 @@ describe('AccountInfoModalUtils', () => {
|
|||
expect(API.refreshAccountInfo).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ describe('ChannelBatchUtils', () => {
|
|||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// ── getChannelGroupChange ──────────────────────────────────────────────────────
|
||||
// ── getChannelGroupChange ──────────────────────────────────────────────────────
|
||||
|
||||
describe('getChannelGroupChange', () => {
|
||||
const channelGroups = {
|
||||
|
|
@ -58,15 +58,19 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
|
||||
it('returns the group name when a valid group is selected', () => {
|
||||
expect(getChannelGroupChange('1', channelGroups)).toBe('• Channel Group: Sports');
|
||||
expect(getChannelGroupChange('1', channelGroups)).toBe(
|
||||
'• Channel Group: Sports'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "Unknown" when group id is not found in channelGroups', () => {
|
||||
expect(getChannelGroupChange('99', channelGroups)).toBe('• Channel Group: Unknown');
|
||||
expect(getChannelGroupChange('99', channelGroups)).toBe(
|
||||
'• Channel Group: Unknown'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── getLogoChange ─────────────────────────────────────────────────────────────
|
||||
// ── getLogoChange ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('getLogoChange', () => {
|
||||
const channelLogos = {
|
||||
|
|
@ -97,7 +101,7 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── getStreamProfileChange ────────────────────────────────────────────────────
|
||||
// ── getStreamProfileChange ────────────────────────────────────────────────────
|
||||
|
||||
describe('getStreamProfileChange', () => {
|
||||
const streamProfiles = [
|
||||
|
|
@ -116,23 +120,31 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
|
||||
it('returns "Use Default" message when streamProfileId is "0"', () => {
|
||||
expect(getStreamProfileChange('0', streamProfiles)).toBe('• Stream Profile: Use Default');
|
||||
expect(getStreamProfileChange('0', streamProfiles)).toBe(
|
||||
'• Stream Profile: Use Default'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the profile name when a valid profile is selected', () => {
|
||||
expect(getStreamProfileChange('1', streamProfiles)).toBe('• Stream Profile: HD Profile');
|
||||
expect(getStreamProfileChange('1', streamProfiles)).toBe(
|
||||
'• Stream Profile: HD Profile'
|
||||
);
|
||||
});
|
||||
|
||||
it('matches profile id using string coercion', () => {
|
||||
expect(getStreamProfileChange(2, streamProfiles)).toBe('• Stream Profile: SD Profile');
|
||||
expect(getStreamProfileChange(2, streamProfiles)).toBe(
|
||||
'• Stream Profile: SD Profile'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns "Selected Profile" fallback when profile is not found', () => {
|
||||
expect(getStreamProfileChange('99', streamProfiles)).toBe('• Stream Profile: Selected Profile');
|
||||
expect(getStreamProfileChange('99', streamProfiles)).toBe(
|
||||
'• Stream Profile: Selected Profile'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── getUserLevelChange ────────────────────────────────────────────────────────
|
||||
// ── getUserLevelChange ────────────────────────────────────────────────────────
|
||||
|
||||
describe('getUserLevelChange', () => {
|
||||
const userLevelLabels = {
|
||||
|
|
@ -151,15 +163,19 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
|
||||
it('returns the label when a valid user level is selected', () => {
|
||||
expect(getUserLevelChange('1', userLevelLabels)).toBe('• User Level: Admin');
|
||||
expect(getUserLevelChange('1', userLevelLabels)).toBe(
|
||||
'• User Level: Admin'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the raw userLevel value when label is not found', () => {
|
||||
expect(getUserLevelChange('99', userLevelLabels)).toBe('• User Level: 99');
|
||||
expect(getUserLevelChange('99', userLevelLabels)).toBe(
|
||||
'• User Level: 99'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── getMatureContentChange ────────────────────────────────────────────────────
|
||||
// ── getMatureContentChange ────────────────────────────────────────────────────
|
||||
|
||||
describe('getMatureContentChange', () => {
|
||||
it('returns null when isAdult is falsy', () => {
|
||||
|
|
@ -181,7 +197,7 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── getRegexNameChange ────────────────────────────────────────────────────────
|
||||
// ── getRegexNameChange ────────────────────────────────────────────────────────
|
||||
|
||||
describe('getRegexNameChange', () => {
|
||||
it('returns null when regexFind is falsy', () => {
|
||||
|
|
@ -210,7 +226,7 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── getEpgChange ──────────────────────────────────────────────────────────────
|
||||
// ── getEpgChange ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('getEpgChange', () => {
|
||||
const epgs = {
|
||||
|
|
@ -225,7 +241,9 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
|
||||
it('returns clear assignment message when selectedDummyEpgId is "clear"', () => {
|
||||
expect(getEpgChange('clear', epgs)).toBe('• EPG: Clear Assignment (use default dummy)');
|
||||
expect(getEpgChange('clear', epgs)).toBe(
|
||||
'• EPG: Clear Assignment (use default dummy)'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the EPG name when a valid EPG is selected', () => {
|
||||
|
|
@ -237,7 +255,7 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── API wrappers ──────────────────────────────────────────────────────────────
|
||||
// ── API wrappers ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('updateChannels', () => {
|
||||
it('calls API.updateChannels with channelIds and values', () => {
|
||||
|
|
@ -257,17 +275,32 @@ describe('ChannelBatchUtils', () => {
|
|||
describe('bulkRegexRenameChannels', () => {
|
||||
it('calls API.bulkRegexRenameChannels with all arguments', () => {
|
||||
bulkRegexRenameChannels([1, 2], 'find', 'replace', 'g');
|
||||
expect(API.bulkRegexRenameChannels).toHaveBeenCalledWith([1, 2], 'find', 'replace', 'g');
|
||||
expect(API.bulkRegexRenameChannels).toHaveBeenCalledWith(
|
||||
[1, 2],
|
||||
'find',
|
||||
'replace',
|
||||
'g'
|
||||
);
|
||||
});
|
||||
|
||||
it('passes empty string when regexReplace is null', () => {
|
||||
bulkRegexRenameChannels([1], 'find', null, 'g');
|
||||
expect(API.bulkRegexRenameChannels).toHaveBeenCalledWith([1], 'find', '', 'g');
|
||||
expect(API.bulkRegexRenameChannels).toHaveBeenCalledWith(
|
||||
[1],
|
||||
'find',
|
||||
'',
|
||||
'g'
|
||||
);
|
||||
});
|
||||
|
||||
it('passes empty string when regexReplace is undefined', () => {
|
||||
bulkRegexRenameChannels([1], 'find', undefined, 'g');
|
||||
expect(API.bulkRegexRenameChannels).toHaveBeenCalledWith([1], 'find', '', 'g');
|
||||
expect(API.bulkRegexRenameChannels).toHaveBeenCalledWith(
|
||||
[1],
|
||||
'find',
|
||||
'',
|
||||
'g'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -307,7 +340,7 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── computeRegexPreview ───────────────────────────────────────────────────────
|
||||
// ── computeRegexPreview ───────────────────────────────────────────────────────
|
||||
|
||||
describe('computeRegexPreview', () => {
|
||||
const nameById = {
|
||||
|
|
@ -343,7 +376,12 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
|
||||
it('only includes ids that exist in nameById', () => {
|
||||
const result = computeRegexPreview([1, 99, 2], nameById, 'HBO', 'Cinemax');
|
||||
const result = computeRegexPreview(
|
||||
[1, 99, 2],
|
||||
nameById,
|
||||
'HBO',
|
||||
'Cinemax'
|
||||
);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
|
|
@ -352,7 +390,13 @@ describe('ChannelBatchUtils', () => {
|
|||
Array.from({ length: 30 }, (_, i) => [i + 1, `HBO ${i + 1}`])
|
||||
);
|
||||
const ids = Array.from({ length: 30 }, (_, i) => i + 1);
|
||||
const result = computeRegexPreview(ids, largeNameById, 'HBO', 'Cinemax', 5);
|
||||
const result = computeRegexPreview(
|
||||
ids,
|
||||
largeNameById,
|
||||
'HBO',
|
||||
'Cinemax',
|
||||
5
|
||||
);
|
||||
expect(result).toHaveLength(5);
|
||||
});
|
||||
|
||||
|
|
@ -377,7 +421,7 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── buildSubmitValues ─────────────────────────────────────────────────────────
|
||||
// ── buildSubmitValues ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('buildSubmitValues', () => {
|
||||
const baseFormValues = {
|
||||
|
|
@ -395,47 +439,83 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
|
||||
it('removes stream_profile_id when it is "-1"', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, stream_profile_id: '-1' }, null, null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, stream_profile_id: '-1' },
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(result).not.toHaveProperty('stream_profile_id');
|
||||
});
|
||||
|
||||
it('removes stream_profile_id when it is falsy', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, stream_profile_id: '' }, null, null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, stream_profile_id: '' },
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(result).not.toHaveProperty('stream_profile_id');
|
||||
});
|
||||
|
||||
it('converts stream_profile_id "0" to null (use default)', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, stream_profile_id: '0' }, null, null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, stream_profile_id: '0' },
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(result.stream_profile_id).toBeNull();
|
||||
});
|
||||
|
||||
it('preserves a valid stream_profile_id string', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, stream_profile_id: '3' }, null, null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, stream_profile_id: '3' },
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(result.stream_profile_id).toBe('3');
|
||||
});
|
||||
|
||||
it('removes user_level when it is "-1"', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, user_level: '-1' }, null, null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, user_level: '-1' },
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(result).not.toHaveProperty('user_level');
|
||||
});
|
||||
|
||||
it('preserves a valid user_level', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, user_level: '2' }, null, null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, user_level: '2' },
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(result.user_level).toBe('2');
|
||||
});
|
||||
|
||||
it('removes is_adult when it is "-1"', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, is_adult: '-1' }, null, null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, is_adult: '-1' },
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(result).not.toHaveProperty('is_adult');
|
||||
});
|
||||
|
||||
it('converts is_adult "true" to boolean true', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, is_adult: 'true' }, null, null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, is_adult: 'true' },
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(result.is_adult).toBe(true);
|
||||
});
|
||||
|
||||
it('converts is_adult "false" to boolean false', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, is_adult: 'false' }, null, null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, is_adult: 'false' },
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(result.is_adult).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -445,12 +525,20 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
|
||||
it('removes channel_group_id when selectedChannelGroup is null', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, channel_group_id: 99 }, null, null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, channel_group_id: 99 },
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(result).not.toHaveProperty('channel_group_id');
|
||||
});
|
||||
|
||||
it('removes channel_group_id when selectedChannelGroup is "-1"', () => {
|
||||
const result = buildSubmitValues({ ...baseFormValues, channel_group_id: 99 }, '-1', null);
|
||||
const result = buildSubmitValues(
|
||||
{ ...baseFormValues, channel_group_id: 99 },
|
||||
'-1',
|
||||
null
|
||||
);
|
||||
expect(result).not.toHaveProperty('channel_group_id');
|
||||
});
|
||||
|
||||
|
|
@ -481,7 +569,7 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── buildEpgAssociations ──────────────────────────────────────────────────────
|
||||
// ── buildEpgAssociations ──────────────────────────────────────────────────────
|
||||
|
||||
describe('buildEpgAssociations', () => {
|
||||
const channelIds = [1, 2, 3];
|
||||
|
|
@ -491,18 +579,27 @@ describe('ChannelBatchUtils', () => {
|
|||
11: { name: 'Empty EPG', epg_data_count: 0 },
|
||||
};
|
||||
|
||||
const tvgs = [
|
||||
{ id: 42, epg_source: 10 },
|
||||
];
|
||||
const tvgs = [{ id: 42, epg_source: 10 }];
|
||||
|
||||
it('returns null when selectedDummyEpgId is falsy', async () => {
|
||||
await expect(buildEpgAssociations(null, channelIds, epgs, tvgs)).resolves.toBeNull();
|
||||
await expect(buildEpgAssociations('', channelIds, epgs, tvgs)).resolves.toBeNull();
|
||||
await expect(buildEpgAssociations(undefined, channelIds, epgs, tvgs)).resolves.toBeNull();
|
||||
await expect(
|
||||
buildEpgAssociations(null, channelIds, epgs, tvgs)
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
buildEpgAssociations('', channelIds, epgs, tvgs)
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
buildEpgAssociations(undefined, channelIds, epgs, tvgs)
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('returns clear associations when selectedDummyEpgId is "clear"', async () => {
|
||||
const result = await buildEpgAssociations('clear', channelIds, epgs, tvgs);
|
||||
const result = await buildEpgAssociations(
|
||||
'clear',
|
||||
channelIds,
|
||||
epgs,
|
||||
tvgs
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{ channel_id: 1, epg_data_id: null },
|
||||
{ channel_id: 2, epg_data_id: null },
|
||||
|
|
@ -531,9 +628,7 @@ describe('ChannelBatchUtils', () => {
|
|||
});
|
||||
|
||||
it('calls getEpgData when tvgs does not contain a matching epg_source', async () => {
|
||||
vi.mocked(API.getEPGData).mockResolvedValue([
|
||||
{ id: 55, epg_source: 10 },
|
||||
]);
|
||||
vi.mocked(API.getEPGData).mockResolvedValue([{ id: 55, epg_source: 10 }]);
|
||||
const result = await buildEpgAssociations('10', channelIds, epgs, []);
|
||||
expect(API.getEPGData).toHaveBeenCalled();
|
||||
expect(result).toEqual([
|
||||
|
|
|
|||
|
|
@ -238,48 +238,85 @@ describe('ChannelUtils', () => {
|
|||
|
||||
describe('getFormattedValues', () => {
|
||||
it('converts "0" stream_profile_id to null', () => {
|
||||
const result = getFormattedValues({ stream_profile_id: '0', tvg_id: 'x', tvc_guide_stationid: 'y' });
|
||||
const result = getFormattedValues({
|
||||
stream_profile_id: '0',
|
||||
tvg_id: 'x',
|
||||
tvc_guide_stationid: 'y',
|
||||
});
|
||||
expect(result.stream_profile_id).toBeNull();
|
||||
});
|
||||
|
||||
it('converts empty string stream_profile_id to null', () => {
|
||||
const result = getFormattedValues({ stream_profile_id: '', tvg_id: 'x', tvc_guide_stationid: 'y' });
|
||||
const result = getFormattedValues({
|
||||
stream_profile_id: '',
|
||||
tvg_id: 'x',
|
||||
tvc_guide_stationid: 'y',
|
||||
});
|
||||
expect(result.stream_profile_id).toBeNull();
|
||||
});
|
||||
|
||||
it('preserves non-zero stream_profile_id', () => {
|
||||
const result = getFormattedValues({ stream_profile_id: '5', tvg_id: 'x', tvc_guide_stationid: 'y' });
|
||||
const result = getFormattedValues({
|
||||
stream_profile_id: '5',
|
||||
tvg_id: 'x',
|
||||
tvc_guide_stationid: 'y',
|
||||
});
|
||||
expect(result.stream_profile_id).toBe('5');
|
||||
});
|
||||
|
||||
it('converts empty tvg_id to null', () => {
|
||||
const result = getFormattedValues({ stream_profile_id: '1', tvg_id: '', tvc_guide_stationid: 'y' });
|
||||
const result = getFormattedValues({
|
||||
stream_profile_id: '1',
|
||||
tvg_id: '',
|
||||
tvc_guide_stationid: 'y',
|
||||
});
|
||||
expect(result.tvg_id).toBeNull();
|
||||
});
|
||||
|
||||
it('preserves non-empty tvg_id', () => {
|
||||
const result = getFormattedValues({ stream_profile_id: '1', tvg_id: 'hbo.us', tvc_guide_stationid: 'y' });
|
||||
const result = getFormattedValues({
|
||||
stream_profile_id: '1',
|
||||
tvg_id: 'hbo.us',
|
||||
tvc_guide_stationid: 'y',
|
||||
});
|
||||
expect(result.tvg_id).toBe('hbo.us');
|
||||
});
|
||||
|
||||
it('converts empty tvc_guide_stationid to null', () => {
|
||||
const result = getFormattedValues({ stream_profile_id: '1', tvg_id: 'x', tvc_guide_stationid: '' });
|
||||
const result = getFormattedValues({
|
||||
stream_profile_id: '1',
|
||||
tvg_id: 'x',
|
||||
tvc_guide_stationid: '',
|
||||
});
|
||||
expect(result.tvc_guide_stationid).toBeNull();
|
||||
});
|
||||
|
||||
it('preserves non-empty tvc_guide_stationid', () => {
|
||||
const result = getFormattedValues({ stream_profile_id: '1', tvg_id: 'x', tvc_guide_stationid: 'hbo-station' });
|
||||
const result = getFormattedValues({
|
||||
stream_profile_id: '1',
|
||||
tvg_id: 'x',
|
||||
tvc_guide_stationid: 'hbo-station',
|
||||
});
|
||||
expect(result.tvc_guide_stationid).toBe('hbo-station');
|
||||
});
|
||||
|
||||
it('does not mutate the original values object', () => {
|
||||
const values = { stream_profile_id: '0', tvg_id: '', tvc_guide_stationid: '' };
|
||||
const values = {
|
||||
stream_profile_id: '0',
|
||||
tvg_id: '',
|
||||
tvc_guide_stationid: '',
|
||||
};
|
||||
getFormattedValues(values);
|
||||
expect(values.stream_profile_id).toBe('0');
|
||||
});
|
||||
|
||||
it('passes through unrelated fields unchanged', () => {
|
||||
const result = getFormattedValues({ stream_profile_id: '1', tvg_id: 'x', tvc_guide_stationid: 'y', name: 'HBO' });
|
||||
const result = getFormattedValues({
|
||||
stream_profile_id: '1',
|
||||
tvg_id: 'x',
|
||||
tvc_guide_stationid: 'y',
|
||||
name: 'HBO',
|
||||
});
|
||||
expect(result.name).toBe('HBO');
|
||||
});
|
||||
});
|
||||
|
|
@ -314,7 +351,12 @@ describe('ChannelUtils', () => {
|
|||
it('calls API.setChannelEPG with channel id and new epg_data_id', async () => {
|
||||
const channel = makeChannel({ epg_data_id: 'epg-old' });
|
||||
const values = makeValues({ epg_data_id: 'epg-new' });
|
||||
await handleEpgUpdate(channel, values, makeFormattedValues({ epg_data_id: 'epg-new' }), makeChannelStreams());
|
||||
await handleEpgUpdate(
|
||||
channel,
|
||||
values,
|
||||
makeFormattedValues({ epg_data_id: 'epg-new' }),
|
||||
makeChannelStreams()
|
||||
);
|
||||
expect(API.setChannelEPG).toHaveBeenCalledWith('ch-1', 'epg-new');
|
||||
});
|
||||
|
||||
|
|
@ -338,7 +380,12 @@ describe('ChannelUtils', () => {
|
|||
const channel = makeChannel({ epg_data_id: 'epg-old' });
|
||||
const values = makeValues({ epg_data_id: 'epg-new' });
|
||||
// Only epg_data_id in formatted — after stripping it, nothing remains
|
||||
await handleEpgUpdate(channel, values, { epg_data_id: 'epg-new' }, makeChannelStreams());
|
||||
await handleEpgUpdate(
|
||||
channel,
|
||||
values,
|
||||
{ epg_data_id: 'epg-new' },
|
||||
makeChannelStreams()
|
||||
);
|
||||
expect(API.updateChannel).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -347,7 +394,12 @@ describe('ChannelUtils', () => {
|
|||
it('does not call API.setChannelEPG', async () => {
|
||||
const channel = makeChannel({ epg_data_id: 'epg-1' });
|
||||
const values = makeValues({ epg_data_id: 'epg-1' });
|
||||
await handleEpgUpdate(channel, values, makeFormattedValues({ epg_data_id: 'epg-1' }), makeChannelStreams());
|
||||
await handleEpgUpdate(
|
||||
channel,
|
||||
values,
|
||||
makeFormattedValues({ epg_data_id: 'epg-1' }),
|
||||
makeChannelStreams()
|
||||
);
|
||||
expect(API.setChannelEPG).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -366,7 +418,12 @@ describe('ChannelUtils', () => {
|
|||
it('handles empty channel streams array', async () => {
|
||||
const channel = makeChannel({ epg_data_id: 'epg-1' });
|
||||
const values = makeValues({ epg_data_id: 'epg-1' });
|
||||
await handleEpgUpdate(channel, values, makeFormattedValues({ epg_data_id: 'epg-1' }), []);
|
||||
await handleEpgUpdate(
|
||||
channel,
|
||||
values,
|
||||
makeFormattedValues({ epg_data_id: 'epg-1' }),
|
||||
[]
|
||||
);
|
||||
expect(API.updateChannel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ streams: [] })
|
||||
);
|
||||
|
|
@ -378,7 +435,12 @@ describe('ChannelUtils', () => {
|
|||
const channel = makeChannel({ epg_data_id: 'epg-old' });
|
||||
const values = makeValues({ epg_data_id: 'epg-new' });
|
||||
await expect(
|
||||
handleEpgUpdate(channel, values, makeFormattedValues({ epg_data_id: 'epg-new' }), makeChannelStreams())
|
||||
handleEpgUpdate(
|
||||
channel,
|
||||
values,
|
||||
makeFormattedValues({ epg_data_id: 'epg-new' }),
|
||||
makeChannelStreams()
|
||||
)
|
||||
).rejects.toThrow('EPG error');
|
||||
});
|
||||
|
||||
|
|
@ -387,8 +449,13 @@ describe('ChannelUtils', () => {
|
|||
const channel = makeChannel({ epg_data_id: 'epg-1' });
|
||||
const values = makeValues({ epg_data_id: 'epg-1' });
|
||||
await expect(
|
||||
handleEpgUpdate(channel, values, makeFormattedValues({ epg_data_id: 'epg-1' }), makeChannelStreams())
|
||||
handleEpgUpdate(
|
||||
channel,
|
||||
values,
|
||||
makeFormattedValues({ epg_data_id: 'epg-1' }),
|
||||
makeChannelStreams()
|
||||
)
|
||||
).rejects.toThrow('Update error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -73,7 +73,9 @@ describe('ConnectionUtils', () => {
|
|||
it('calls API.createConnectIntegration with the correct payload', async () => {
|
||||
const values = makeValues();
|
||||
const config = { url: 'https://example.com/hook' };
|
||||
vi.mocked(API.createConnectIntegration).mockResolvedValue({ id: 'new-1' });
|
||||
vi.mocked(API.createConnectIntegration).mockResolvedValue({
|
||||
id: 'new-1',
|
||||
});
|
||||
|
||||
await createConnectIntegration(values, config);
|
||||
|
||||
|
|
@ -86,14 +88,20 @@ describe('ConnectionUtils', () => {
|
|||
});
|
||||
|
||||
it('returns the API response', async () => {
|
||||
vi.mocked(API.createConnectIntegration).mockResolvedValue({ id: 'new-1' });
|
||||
vi.mocked(API.createConnectIntegration).mockResolvedValue({
|
||||
id: 'new-1',
|
||||
});
|
||||
const result = await createConnectIntegration(makeValues(), {});
|
||||
expect(result).toEqual({ id: 'new-1' });
|
||||
});
|
||||
|
||||
it('propagates API errors', async () => {
|
||||
vi.mocked(API.createConnectIntegration).mockRejectedValue(new Error('Network error'));
|
||||
await expect(createConnectIntegration(makeValues(), {})).rejects.toThrow('Network error');
|
||||
vi.mocked(API.createConnectIntegration).mockRejectedValue(
|
||||
new Error('Network error')
|
||||
);
|
||||
await expect(createConnectIntegration(makeValues(), {})).rejects.toThrow(
|
||||
'Network error'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -104,7 +112,9 @@ describe('ConnectionUtils', () => {
|
|||
const connection = makeConnection();
|
||||
const values = makeValues();
|
||||
const config = { url: 'https://example.com/hook' };
|
||||
vi.mocked(API.updateConnectIntegration).mockResolvedValue({ id: 'conn-1' });
|
||||
vi.mocked(API.updateConnectIntegration).mockResolvedValue({
|
||||
id: 'conn-1',
|
||||
});
|
||||
|
||||
await updateConnectIntegration(connection, values, config);
|
||||
|
||||
|
|
@ -117,13 +127,22 @@ describe('ConnectionUtils', () => {
|
|||
});
|
||||
|
||||
it('returns the API response', async () => {
|
||||
vi.mocked(API.updateConnectIntegration).mockResolvedValue({ id: 'conn-1', name: 'Updated' });
|
||||
const result = await updateConnectIntegration(makeConnection(), makeValues(), {});
|
||||
vi.mocked(API.updateConnectIntegration).mockResolvedValue({
|
||||
id: 'conn-1',
|
||||
name: 'Updated',
|
||||
});
|
||||
const result = await updateConnectIntegration(
|
||||
makeConnection(),
|
||||
makeValues(),
|
||||
{}
|
||||
);
|
||||
expect(result).toEqual({ id: 'conn-1', name: 'Updated' });
|
||||
});
|
||||
|
||||
it('propagates API errors', async () => {
|
||||
vi.mocked(API.updateConnectIntegration).mockRejectedValue(new Error('Server error'));
|
||||
vi.mocked(API.updateConnectIntegration).mockRejectedValue(
|
||||
new Error('Server error')
|
||||
);
|
||||
await expect(
|
||||
updateConnectIntegration(makeConnection(), makeValues(), {})
|
||||
).rejects.toThrow('Server error');
|
||||
|
|
@ -135,7 +154,9 @@ describe('ConnectionUtils', () => {
|
|||
describe('setConnectSubscriptions', () => {
|
||||
it('calls API.setConnectSubscriptions with connection.id and subs', async () => {
|
||||
const connection = makeConnection();
|
||||
const subs = [{ event: 'channel_added', enabled: true, payload_template: null }];
|
||||
const subs = [
|
||||
{ event: 'channel_added', enabled: true, payload_template: null },
|
||||
];
|
||||
vi.mocked(API.setConnectSubscriptions).mockResolvedValue(undefined);
|
||||
|
||||
await setConnectSubscriptions(connection, subs);
|
||||
|
|
@ -144,8 +165,12 @@ describe('ConnectionUtils', () => {
|
|||
});
|
||||
|
||||
it('propagates API errors', async () => {
|
||||
vi.mocked(API.setConnectSubscriptions).mockRejectedValue(new Error('Failed'));
|
||||
await expect(setConnectSubscriptions(makeConnection(), [])).rejects.toThrow('Failed');
|
||||
vi.mocked(API.setConnectSubscriptions).mockRejectedValue(
|
||||
new Error('Failed')
|
||||
);
|
||||
await expect(
|
||||
setConnectSubscriptions(makeConnection(), [])
|
||||
).rejects.toThrow('Failed');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -154,12 +179,20 @@ describe('ConnectionUtils', () => {
|
|||
describe('buildConfig', () => {
|
||||
describe('webhook type', () => {
|
||||
it('returns config with url only when no headers are provided', () => {
|
||||
const values = makeValues({ type: 'webhook', url: 'https://example.com/hook' });
|
||||
expect(buildConfig(values, [])).toEqual({ url: 'https://example.com/hook' });
|
||||
const values = makeValues({
|
||||
type: 'webhook',
|
||||
url: 'https://example.com/hook',
|
||||
});
|
||||
expect(buildConfig(values, [])).toEqual({
|
||||
url: 'https://example.com/hook',
|
||||
});
|
||||
});
|
||||
|
||||
it('includes headers when non-empty key/value pairs are present', () => {
|
||||
const values = makeValues({ type: 'webhook', url: 'https://example.com/hook' });
|
||||
const values = makeValues({
|
||||
type: 'webhook',
|
||||
url: 'https://example.com/hook',
|
||||
});
|
||||
const headers = [{ key: 'Authorization', value: 'Bearer token' }];
|
||||
expect(buildConfig(values, headers)).toEqual({
|
||||
url: 'https://example.com/hook',
|
||||
|
|
@ -168,7 +201,10 @@ describe('ConnectionUtils', () => {
|
|||
});
|
||||
|
||||
it('omits headers with blank keys', () => {
|
||||
const values = makeValues({ type: 'webhook', url: 'https://example.com/hook' });
|
||||
const values = makeValues({
|
||||
type: 'webhook',
|
||||
url: 'https://example.com/hook',
|
||||
});
|
||||
const headers = [
|
||||
{ key: '', value: 'should-be-ignored' },
|
||||
{ key: ' ', value: 'also-ignored' },
|
||||
|
|
@ -181,14 +217,20 @@ describe('ConnectionUtils', () => {
|
|||
});
|
||||
|
||||
it('omits headers property entirely when all keys are blank', () => {
|
||||
const values = makeValues({ type: 'webhook', url: 'https://example.com/hook' });
|
||||
const values = makeValues({
|
||||
type: 'webhook',
|
||||
url: 'https://example.com/hook',
|
||||
});
|
||||
const headers = [{ key: '', value: 'ignored' }];
|
||||
const config = buildConfig(values, headers);
|
||||
expect(config).not.toHaveProperty('headers');
|
||||
});
|
||||
|
||||
it('supports multiple headers', () => {
|
||||
const values = makeValues({ type: 'webhook', url: 'https://example.com/hook' });
|
||||
const values = makeValues({
|
||||
type: 'webhook',
|
||||
url: 'https://example.com/hook',
|
||||
});
|
||||
const headers = [
|
||||
{ key: 'X-One', value: '1' },
|
||||
{ key: 'X-Two', value: '2' },
|
||||
|
|
@ -202,12 +244,20 @@ describe('ConnectionUtils', () => {
|
|||
|
||||
describe('script type', () => {
|
||||
it('returns config with path from script_path', () => {
|
||||
const values = makeValues({ type: 'script', script_path: '/usr/local/bin/notify.sh' });
|
||||
expect(buildConfig(values, [])).toEqual({ path: '/usr/local/bin/notify.sh' });
|
||||
const values = makeValues({
|
||||
type: 'script',
|
||||
script_path: '/usr/local/bin/notify.sh',
|
||||
});
|
||||
expect(buildConfig(values, [])).toEqual({
|
||||
path: '/usr/local/bin/notify.sh',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores headers for script type', () => {
|
||||
const values = makeValues({ type: 'script', script_path: '/usr/bin/run.sh' });
|
||||
const values = makeValues({
|
||||
type: 'script',
|
||||
script_path: '/usr/bin/run.sh',
|
||||
});
|
||||
const headers = [{ key: 'Authorization', value: 'Bearer token' }];
|
||||
const config = buildConfig(values, headers);
|
||||
expect(config).not.toHaveProperty('headers');
|
||||
|
|
@ -262,20 +312,31 @@ describe('ConnectionUtils', () => {
|
|||
describe('parseApiError', () => {
|
||||
it('returns message when error has no body', () => {
|
||||
const error = { message: 'Network failure' };
|
||||
expect(parseApiError(error)).toEqual({ fieldErrors: {}, apiError: 'Network failure' });
|
||||
expect(parseApiError(error)).toEqual({
|
||||
fieldErrors: {},
|
||||
apiError: 'Network failure',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns "Unknown error" when error has no body and no message', () => {
|
||||
expect(parseApiError({})).toEqual({ fieldErrors: {}, apiError: 'Unknown error' });
|
||||
expect(parseApiError({})).toEqual({
|
||||
fieldErrors: {},
|
||||
apiError: 'Unknown error',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns message when body is a string (not an object)', () => {
|
||||
const error = { body: 'Bad Request', message: 'HTTP 400' };
|
||||
expect(parseApiError(error)).toEqual({ fieldErrors: {}, apiError: 'HTTP 400' });
|
||||
expect(parseApiError(error)).toEqual({
|
||||
fieldErrors: {},
|
||||
apiError: 'HTTP 400',
|
||||
});
|
||||
});
|
||||
|
||||
it('extracts known field errors from body', () => {
|
||||
const error = { body: { name: 'This field is required.', type: 'Invalid choice.' } };
|
||||
const error = {
|
||||
body: { name: 'This field is required.', type: 'Invalid choice.' },
|
||||
};
|
||||
const { fieldErrors } = parseApiError(error);
|
||||
expect(fieldErrors).toEqual({
|
||||
name: 'This field is required.',
|
||||
|
|
@ -302,7 +363,9 @@ describe('ConnectionUtils', () => {
|
|||
});
|
||||
|
||||
it('prefers non_field_errors over detail', () => {
|
||||
const error = { body: { non_field_errors: 'NF error', detail: 'Detail error' } };
|
||||
const error = {
|
||||
body: { non_field_errors: 'NF error', detail: 'Detail error' },
|
||||
};
|
||||
const { apiError } = parseApiError(error);
|
||||
expect(apiError).toBe('NF error');
|
||||
});
|
||||
|
|
@ -320,11 +383,17 @@ describe('ConnectionUtils', () => {
|
|||
});
|
||||
|
||||
it('handles null error gracefully', () => {
|
||||
expect(parseApiError(null)).toEqual({ fieldErrors: {}, apiError: 'Unknown error' });
|
||||
expect(parseApiError(null)).toEqual({
|
||||
fieldErrors: {},
|
||||
apiError: 'Unknown error',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles undefined error gracefully', () => {
|
||||
expect(parseApiError(undefined)).toEqual({ fieldErrors: {}, apiError: 'Unknown error' });
|
||||
expect(parseApiError(undefined)).toEqual({
|
||||
fieldErrors: {},
|
||||
apiError: 'Unknown error',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ describe('CronBuilderUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── DAYS_OF_WEEK ───────────────────────────────────────────────────────────────
|
||||
// ── DAYS_OF_WEEK ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('DAYS_OF_WEEK', () => {
|
||||
it('contains 8 entries (wildcard + 7 days)', () => {
|
||||
|
|
@ -53,11 +53,19 @@ describe('CronBuilderUtils', () => {
|
|||
|
||||
it('numeric entries run 0-6', () => {
|
||||
const numeric = DAYS_OF_WEEK.filter((d) => d.value !== '*');
|
||||
expect(numeric.map((d) => d.value)).toEqual(['0', '1', '2', '3', '4', '5', '6']);
|
||||
expect(numeric.map((d) => d.value)).toEqual([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── FREQUENCY_OPTIONS ──────────────────────────────────────────────────────────
|
||||
// ── FREQUENCY_OPTIONS ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('FREQUENCY_OPTIONS', () => {
|
||||
it('contains exactly 4 options', () => {
|
||||
|
|
@ -77,7 +85,7 @@ describe('CronBuilderUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── CRON_FIELDS ────────────────────────────────────────────────────────────────
|
||||
// ── CRON_FIELDS ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('CRON_FIELDS', () => {
|
||||
it('contains exactly 5 fields', () => {
|
||||
|
|
@ -97,7 +105,7 @@ describe('CronBuilderUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── buildCron ──────────────────────────────────────────────────────────────────
|
||||
// ── buildCron ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('buildCron', () => {
|
||||
describe('hourly', () => {
|
||||
|
|
@ -171,7 +179,7 @@ describe('CronBuilderUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── parseCronPreset ────────────────────────────────────────────────────────────
|
||||
// ── parseCronPreset ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('parseCronPreset', () => {
|
||||
describe('hourly detection', () => {
|
||||
|
|
@ -279,7 +287,7 @@ describe('CronBuilderUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── updateCronPart ─────────────────────────────────────────────────────────────
|
||||
// ── updateCronPart ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('updateCronPart', () => {
|
||||
describe('valid 5-part cron', () => {
|
||||
|
|
@ -342,7 +350,9 @@ describe('CronBuilderUtils', () => {
|
|||
});
|
||||
|
||||
it('accepts comma-separated values like 0,15,30,45', () => {
|
||||
expect(updateCronPart('* * * * *', 0, '0,15,30,45')).toBe('0,15,30,45 * * * *');
|
||||
expect(updateCronPart('* * * * *', 0, '0,15,30,45')).toBe(
|
||||
'0,15,30,45 * * * *'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,27 +3,52 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|||
// ── Module mocks (must be before imports) ─────────────────────────────────────
|
||||
vi.mock('../../../api.js', () => ({
|
||||
default: {
|
||||
getTimezones: vi.fn(),
|
||||
addEPG: vi.fn(),
|
||||
updateEPG: vi.fn(),
|
||||
getTimezones: vi.fn(),
|
||||
addEPG: vi.fn(),
|
||||
updateEPG: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../dateTimeUtils.js', () => ({
|
||||
format: vi.fn(),
|
||||
getDay: vi.fn(),
|
||||
getHour: vi.fn(),
|
||||
getMinute: vi.fn(),
|
||||
getMonth: vi.fn(),
|
||||
getNow: vi.fn(),
|
||||
getYear: vi.fn(),
|
||||
MONTH_ABBR: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
|
||||
MONTH_NAMES: ['January','February','March','April','May','June',
|
||||
'July','August','September','October','November','December'],
|
||||
setHour: vi.fn((dt) => dt),
|
||||
setMinute: vi.fn((dt) => dt),
|
||||
setSecond: vi.fn((dt) => dt),
|
||||
setTz: vi.fn(),
|
||||
format: vi.fn(),
|
||||
getDay: vi.fn(),
|
||||
getHour: vi.fn(),
|
||||
getMinute: vi.fn(),
|
||||
getMonth: vi.fn(),
|
||||
getNow: vi.fn(),
|
||||
getYear: vi.fn(),
|
||||
MONTH_ABBR: [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
],
|
||||
MONTH_NAMES: [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
],
|
||||
setHour: vi.fn((dt) => dt),
|
||||
setMinute: vi.fn((dt) => dt),
|
||||
setSecond: vi.fn((dt) => dt),
|
||||
setTz: vi.fn(),
|
||||
}));
|
||||
|
||||
// ── Imports after mocks ────────────────────────────────────────────────────────
|
||||
|
|
@ -55,30 +80,30 @@ const makeEPG = (overrides = {}) => ({
|
|||
});
|
||||
|
||||
const makeCustom = (overrides = {}) => ({
|
||||
title_pattern: '(?<title>.+)',
|
||||
time_pattern: '(?<hour>\\d+):(?<minute>\\d+)',
|
||||
date_pattern: '',
|
||||
timezone: 'US/Eastern',
|
||||
output_timezone: '',
|
||||
program_duration: 180,
|
||||
sample_title: 'Test Show 9:00 PM',
|
||||
title_template: '{title}',
|
||||
subtitle_template: '',
|
||||
description_template: '',
|
||||
upcoming_title_template: '',
|
||||
title_pattern: '(?<title>.+)',
|
||||
time_pattern: '(?<hour>\\d+):(?<minute>\\d+)',
|
||||
date_pattern: '',
|
||||
timezone: 'US/Eastern',
|
||||
output_timezone: '',
|
||||
program_duration: 180,
|
||||
sample_title: 'Test Show 9:00 PM',
|
||||
title_template: '{title}',
|
||||
subtitle_template: '',
|
||||
description_template: '',
|
||||
upcoming_title_template: '',
|
||||
upcoming_description_template: '',
|
||||
ended_title_template: '',
|
||||
ended_description_template: '',
|
||||
fallback_title_template: '',
|
||||
ended_title_template: '',
|
||||
ended_description_template: '',
|
||||
fallback_title_template: '',
|
||||
fallback_description_template: '',
|
||||
channel_logo_url: '',
|
||||
program_poster_url: '',
|
||||
name_source: 'channel',
|
||||
stream_index: 1,
|
||||
category: '',
|
||||
include_date: true,
|
||||
include_live: false,
|
||||
include_new: false,
|
||||
channel_logo_url: '',
|
||||
program_poster_url: '',
|
||||
name_source: 'channel',
|
||||
stream_index: 1,
|
||||
category: '',
|
||||
include_date: true,
|
||||
include_live: false,
|
||||
include_new: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
|
@ -86,7 +111,6 @@ const makeCustom = (overrides = {}) => ({
|
|||
// Tests
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
describe('DummyEpgUtils', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
|
@ -94,7 +118,9 @@ describe('DummyEpgUtils', () => {
|
|||
// ── getTimezones ───────────────────────────────────────────────────────────
|
||||
describe('getTimezones', () => {
|
||||
it('calls API.getTimezones and returns its result', async () => {
|
||||
vi.mocked(API.getTimezones).mockResolvedValue({ timezones: ['UTC', 'US/Eastern'] });
|
||||
vi.mocked(API.getTimezones).mockResolvedValue({
|
||||
timezones: ['UTC', 'US/Eastern'],
|
||||
});
|
||||
const result = await getTimezones();
|
||||
expect(API.getTimezones).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({ timezones: ['UTC', 'US/Eastern'] });
|
||||
|
|
@ -125,13 +151,19 @@ describe('DummyEpgUtils', () => {
|
|||
// ── updateEPG ──────────────────────────────────────────────────────────────
|
||||
describe('updateEPG', () => {
|
||||
it('calls API.updateEPG with values merged with epg.id', async () => {
|
||||
const epg = makeEPG({ id: 42 });
|
||||
const epg = makeEPG({ id: 42 });
|
||||
const values = { name: 'Updated EPG' };
|
||||
vi.mocked(API.updateEPG).mockResolvedValue({ id: 42, name: 'Updated EPG' });
|
||||
vi.mocked(API.updateEPG).mockResolvedValue({
|
||||
id: 42,
|
||||
name: 'Updated EPG',
|
||||
});
|
||||
|
||||
const result = await updateEPG(values, epg);
|
||||
|
||||
expect(API.updateEPG).toHaveBeenCalledWith({ name: 'Updated EPG', id: 42 });
|
||||
expect(API.updateEPG).toHaveBeenCalledWith({
|
||||
name: 'Updated EPG',
|
||||
id: 42,
|
||||
});
|
||||
expect(result).toMatchObject({ id: 42 });
|
||||
});
|
||||
|
||||
|
|
@ -139,7 +171,9 @@ describe('DummyEpgUtils', () => {
|
|||
const epg = makeEPG({ id: 99 });
|
||||
vi.mocked(API.updateEPG).mockResolvedValue({});
|
||||
await updateEPG({ name: 'X' }, epg);
|
||||
expect(API.updateEPG).toHaveBeenCalledWith(expect.objectContaining({ id: 99 }));
|
||||
expect(API.updateEPG).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 99 })
|
||||
);
|
||||
});
|
||||
|
||||
it('propagates rejection from API.updateEPG', async () => {
|
||||
|
|
@ -153,8 +187,8 @@ describe('DummyEpgUtils', () => {
|
|||
it('returns an object with name, is_active, source_type, and custom_properties', () => {
|
||||
const result = getDummyEpgFormInitialValues();
|
||||
expect(result).toMatchObject({
|
||||
name: expect.any(String),
|
||||
is_active: expect.any(Boolean),
|
||||
name: expect.any(String),
|
||||
is_active: expect.any(Boolean),
|
||||
source_type: 'dummy',
|
||||
});
|
||||
expect(result.custom_properties).toBeDefined();
|
||||
|
|
@ -165,7 +199,9 @@ describe('DummyEpgUtils', () => {
|
|||
});
|
||||
|
||||
it('sets include_date to true by default', () => {
|
||||
expect(getDummyEpgFormInitialValues().custom_properties.include_date).toBe(true);
|
||||
expect(
|
||||
getDummyEpgFormInitialValues().custom_properties.include_date
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('sets include_live and include_new to false by default', () => {
|
||||
|
|
@ -175,15 +211,21 @@ describe('DummyEpgUtils', () => {
|
|||
});
|
||||
|
||||
it('defaults name_source to "channel"', () => {
|
||||
expect(getDummyEpgFormInitialValues().custom_properties.name_source).toBe('channel');
|
||||
expect(getDummyEpgFormInitialValues().custom_properties.name_source).toBe(
|
||||
'channel'
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults program_duration to 180', () => {
|
||||
expect(getDummyEpgFormInitialValues().custom_properties.program_duration).toBe(180);
|
||||
expect(
|
||||
getDummyEpgFormInitialValues().custom_properties.program_duration
|
||||
).toBe(180);
|
||||
});
|
||||
|
||||
it('defaults stream_index to 1', () => {
|
||||
expect(getDummyEpgFormInitialValues().custom_properties.stream_index).toBe(1);
|
||||
expect(
|
||||
getDummyEpgFormInitialValues().custom_properties.stream_index
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -201,7 +243,10 @@ describe('DummyEpgUtils', () => {
|
|||
});
|
||||
|
||||
it('preserves provided values over defaults', () => {
|
||||
const custom = makeCustom({ timezone: 'US/Pacific', program_duration: 60 });
|
||||
const custom = makeCustom({
|
||||
timezone: 'US/Pacific',
|
||||
program_duration: 60,
|
||||
});
|
||||
const result = buildCustomProperties(custom);
|
||||
expect(result.timezone).toBe('US/Pacific');
|
||||
expect(result.program_duration).toBe(60);
|
||||
|
|
@ -224,17 +269,17 @@ describe('DummyEpgUtils', () => {
|
|||
|
||||
it('maps all template fields', () => {
|
||||
const custom = makeCustom({
|
||||
title_template: 'T:{title}',
|
||||
subtitle_template: 'S:{title}',
|
||||
description_template: 'D:{title}',
|
||||
upcoming_title_template: 'U:{title}',
|
||||
title_template: 'T:{title}',
|
||||
subtitle_template: 'S:{title}',
|
||||
description_template: 'D:{title}',
|
||||
upcoming_title_template: 'U:{title}',
|
||||
upcoming_description_template: 'UD:{title}',
|
||||
ended_title_template: 'E:{title}',
|
||||
ended_description_template: 'ED:{title}',
|
||||
fallback_title_template: 'FT:{title}',
|
||||
ended_title_template: 'E:{title}',
|
||||
ended_description_template: 'ED:{title}',
|
||||
fallback_title_template: 'FT:{title}',
|
||||
fallback_description_template: 'FD:{title}',
|
||||
channel_logo_url: 'http://logo',
|
||||
program_poster_url: 'http://poster',
|
||||
channel_logo_url: 'http://logo',
|
||||
program_poster_url: 'http://poster',
|
||||
});
|
||||
const result = buildCustomProperties(custom);
|
||||
expect(result.title_template).toBe('T:{title}');
|
||||
|
|
@ -288,7 +333,9 @@ describe('DummyEpgUtils', () => {
|
|||
});
|
||||
|
||||
it('returns null for a complex valid regex with named groups', () => {
|
||||
expect(validateCustomTitlePattern('(?<title>[A-Z].+)\\s(?<time>\\d+:\\d+)')).toBeNull();
|
||||
expect(
|
||||
validateCustomTitlePattern('(?<title>[A-Z].+)\\s(?<time>\\d+:\\d+)')
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -398,7 +445,11 @@ describe('DummyEpgUtils', () => {
|
|||
'Test Show 9:00'
|
||||
);
|
||||
expect(result.matched).toBe(true);
|
||||
expect(result.groups).toMatchObject({ title: 'Test Show', hour: '9', minute: '00' });
|
||||
expect(result.groups).toMatchObject({
|
||||
title: 'Test Show',
|
||||
hour: '9',
|
||||
minute: '00',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -425,7 +476,10 @@ describe('DummyEpgUtils', () => {
|
|||
});
|
||||
|
||||
it('adds normalized keys for multiple groups', () => {
|
||||
const result = addNormalizedGroups({ title: 'Show', subtitle: 'Épilogue' });
|
||||
const result = addNormalizedGroups({
|
||||
title: 'Show',
|
||||
subtitle: 'Épilogue',
|
||||
});
|
||||
expect(result.title_normalize).toBeDefined();
|
||||
expect(result.subtitle_normalize).toBeDefined();
|
||||
});
|
||||
|
|
@ -434,15 +488,15 @@ describe('DummyEpgUtils', () => {
|
|||
// ── applyTemplates ─────────────────────────────────────────────────────────
|
||||
describe('applyTemplates', () => {
|
||||
const templates = {
|
||||
titleTemplate: '{title}',
|
||||
subtitleTemplate: '{subtitle}',
|
||||
descriptionTemplate: '{title} - {subtitle}',
|
||||
upcomingTitleTemplate: 'Upcoming: {title}',
|
||||
titleTemplate: '{title}',
|
||||
subtitleTemplate: '{subtitle}',
|
||||
descriptionTemplate: '{title} - {subtitle}',
|
||||
upcomingTitleTemplate: 'Upcoming: {title}',
|
||||
upcomingDescriptionTemplate: 'Details for {title}',
|
||||
endedTitleTemplate: 'Ended: {title}',
|
||||
endedDescriptionTemplate: 'Summary of {title}',
|
||||
channelLogoUrl: 'http://logo/{title}',
|
||||
programPosterUrl: 'http://poster/{title}',
|
||||
endedTitleTemplate: 'Ended: {title}',
|
||||
endedDescriptionTemplate: 'Summary of {title}',
|
||||
channelLogoUrl: 'http://logo/{title}',
|
||||
programPosterUrl: 'http://poster/{title}',
|
||||
};
|
||||
|
||||
const groups = { title: 'Test Show', subtitle: 'Pilot' };
|
||||
|
|
@ -479,7 +533,9 @@ describe('DummyEpgUtils', () => {
|
|||
|
||||
it('fills programPosterUrl', () => {
|
||||
const result = applyTemplates(templates, groups, true);
|
||||
expect(result.formattedProgramPosterUrl).toBe('http://poster/Test%20Show');
|
||||
expect(result.formattedProgramPosterUrl).toBe(
|
||||
'http://poster/Test%20Show'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns empty string for template that has no matching placeholder', () => {
|
||||
|
|
@ -510,10 +566,10 @@ describe('DummyEpgUtils', () => {
|
|||
describe('buildTimePlaceholders', () => {
|
||||
beforeEach(() => {
|
||||
// Provide realistic return values from dateTimeUtils
|
||||
vi.mocked(dateTimeUtils.getHour).mockReturnValue(21); // 9 PM
|
||||
vi.mocked(dateTimeUtils.getHour).mockReturnValue(21); // 9 PM
|
||||
vi.mocked(dateTimeUtils.getMinute).mockReturnValue(0);
|
||||
vi.mocked(dateTimeUtils.getDay).mockReturnValue(15);
|
||||
vi.mocked(dateTimeUtils.getMonth).mockReturnValue(5); // June (0-indexed)
|
||||
vi.mocked(dateTimeUtils.getMonth).mockReturnValue(5); // June (0-indexed)
|
||||
vi.mocked(dateTimeUtils.getYear).mockReturnValue(2024);
|
||||
vi.mocked(dateTimeUtils.format).mockImplementation((dt, fmt) => fmt);
|
||||
vi.mocked(dateTimeUtils.setHour).mockImplementation((dt) => dt);
|
||||
|
|
@ -525,20 +581,38 @@ describe('DummyEpgUtils', () => {
|
|||
|
||||
it('returns an object when given valid time groups', () => {
|
||||
const timeGroups = { hour: '9', minute: '00', ampm: 'PM' };
|
||||
const result = buildTimePlaceholders(timeGroups, {}, 'US/Eastern', '', 180);
|
||||
const result = buildTimePlaceholders(
|
||||
timeGroups,
|
||||
{},
|
||||
'US/Eastern',
|
||||
'',
|
||||
180
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('object');
|
||||
});
|
||||
|
||||
it('returns starttime key when time groups have hour and minute', () => {
|
||||
const timeGroups = { hour: '9', minute: '00', ampm: 'PM' };
|
||||
const result = buildTimePlaceholders(timeGroups, {}, 'US/Eastern', '', 180);
|
||||
const result = buildTimePlaceholders(
|
||||
timeGroups,
|
||||
{},
|
||||
'US/Eastern',
|
||||
'',
|
||||
180
|
||||
);
|
||||
expect(result).toHaveProperty('starttime');
|
||||
});
|
||||
|
||||
it('returns endtime key calculated from duration', () => {
|
||||
const timeGroups = { hour: '9', minute: '00', ampm: 'PM' };
|
||||
const result = buildTimePlaceholders(timeGroups, {}, 'US/Eastern', '', 180);
|
||||
const result = buildTimePlaceholders(
|
||||
timeGroups,
|
||||
{},
|
||||
'US/Eastern',
|
||||
'',
|
||||
180
|
||||
);
|
||||
expect(result).toHaveProperty('endtime');
|
||||
});
|
||||
|
||||
|
|
@ -555,8 +629,14 @@ describe('DummyEpgUtils', () => {
|
|||
it('incorporates date groups when provided', () => {
|
||||
const timeGroups = { hour: '9', minute: '00' };
|
||||
const dateGroups = { year: '2024', month: '06', day: '15' };
|
||||
const result = buildTimePlaceholders(timeGroups, dateGroups, 'US/Eastern', '', 180);
|
||||
const result = buildTimePlaceholders(
|
||||
timeGroups,
|
||||
dateGroups,
|
||||
'US/Eastern',
|
||||
'',
|
||||
180
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue