mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
Add default/advanced mode toggle and test coverage for EPG matching
Enhanced EPG matching UX by adding radio button toggle to more clearly define optional advanced options. Also added test coverage for stability. Frontend Changes: - Added radio button UI to EPGMatchModal for default/advanced mode selection - Default mode: Uses built-in normalization - Advanced mode: Shows TagsInput fields for custom prefixes/suffixes/strings - Mode persists across sessions and settings are preserved when switching modes Test Coverage: - Created EPGMatchModal.test.jsx with test cases covering: - Component rendering and mode switching - Form submission and settings persistence - Error handling and UI text variations - Added test cases to SettingsUtils.test.js for EPG mode handling: - epg_match_mode routing to epg_settings group - Settings creation and preservation Since advanced settings are saved persistently but ignored when default settings are used, this adds an easy way in the future to add additional advanced settings to EPG matching like region influence, match aggression, and match preview, since users can easily switch between modes.
This commit is contained in:
parent
0172a7cc00
commit
d7b98fef8d
6 changed files with 429 additions and 65 deletions
|
|
@ -139,7 +139,7 @@ COMMON_EXTRANEOUS_WORDS = [
|
|||
def normalize_name(name: str) -> str:
|
||||
"""
|
||||
A more aggressive normalization that:
|
||||
- Removes user-configured prefixes/suffixes/custom strings (if configured)
|
||||
- Removes user-configured prefixes/suffixes/custom strings (only if mode is 'advanced')
|
||||
- Lowercases
|
||||
- Removes bracketed/parenthesized text
|
||||
- Removes punctuation
|
||||
|
|
@ -157,17 +157,23 @@ def normalize_name(name: str) -> str:
|
|||
try:
|
||||
from core.models import CoreSettings
|
||||
settings = CoreSettings.get_epg_settings()
|
||||
prefixes = settings.get("epg_match_ignore_prefixes", [])
|
||||
suffixes = settings.get("epg_match_ignore_suffixes", [])
|
||||
custom_strings = settings.get("epg_match_ignore_custom", [])
|
||||
|
||||
# Ensure we have lists
|
||||
if not isinstance(prefixes, list):
|
||||
prefixes = []
|
||||
if not isinstance(suffixes, list):
|
||||
suffixes = []
|
||||
if not isinstance(custom_strings, list):
|
||||
custom_strings = []
|
||||
# Check if user has enabled advanced mode
|
||||
mode = settings.get("epg_match_mode", "default")
|
||||
|
||||
# Only use custom settings if mode is 'advanced'
|
||||
if mode == "advanced":
|
||||
prefixes = settings.get("epg_match_ignore_prefixes", [])
|
||||
suffixes = settings.get("epg_match_ignore_suffixes", [])
|
||||
custom_strings = settings.get("epg_match_ignore_custom", [])
|
||||
|
||||
# Ensure we have lists
|
||||
if not isinstance(prefixes, list):
|
||||
prefixes = []
|
||||
if not isinstance(suffixes, list):
|
||||
suffixes = []
|
||||
if not isinstance(custom_strings, list):
|
||||
custom_strings = []
|
||||
|
||||
except Exception as e:
|
||||
# Settings unavailable or error - continue with empty lists (graceful degradation)
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ class CoreSettings(models.Model):
|
|||
def get_epg_settings(cls):
|
||||
"""Get all EPG-related settings."""
|
||||
return cls._get_group(EPG_SETTINGS_KEY, {
|
||||
"epg_match_mode": "default",
|
||||
"epg_match_ignore_prefixes": [],
|
||||
"epg_match_ignore_suffixes": [],
|
||||
"epg_match_ignore_custom": [],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useState, useEffect } from 'react';
|
||||
import { useMemo, useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Stack,
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Group,
|
||||
Button,
|
||||
Loader,
|
||||
Radio,
|
||||
} from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import useSettingsStore from '../../store/settings';
|
||||
|
|
@ -20,6 +21,7 @@ import {
|
|||
const getEpgSettingsFromStore = (settings) => {
|
||||
const epgSettings = settings?.['epg_settings']?.value;
|
||||
return {
|
||||
epg_match_mode: epgSettings?.epg_match_mode || 'default',
|
||||
epg_match_ignore_prefixes: Array.isArray(epgSettings?.epg_match_ignore_prefixes)
|
||||
? epgSettings.epg_match_ignore_prefixes
|
||||
: [],
|
||||
|
|
@ -40,6 +42,7 @@ const EPGMatchModal = ({
|
|||
const settings = useSettingsStore((s) => s.settings);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [settingsMode, setSettingsMode] = useState('default');
|
||||
|
||||
// Compute form values directly from settings - memoized for performance
|
||||
const storedValues = useMemo(
|
||||
|
|
@ -50,18 +53,28 @@ const EPGMatchModal = ({
|
|||
// Local form state
|
||||
const [formValues, setFormValues] = useState(storedValues);
|
||||
|
||||
// Reset to stored values when modal opens
|
||||
// Track previous opened state to detect transitions
|
||||
const prevOpened = useRef(false);
|
||||
|
||||
// Reset to stored values and mode only when modal opens (not on storedValues changes)
|
||||
useEffect(() => {
|
||||
if (opened) {
|
||||
// Only reset when transitioning from closed to open
|
||||
if (opened && !prevOpened.current) {
|
||||
setFormValues(storedValues);
|
||||
setSettingsMode(storedValues.epg_match_mode);
|
||||
}
|
||||
prevOpened.current = opened;
|
||||
}, [opened, storedValues]);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Save settings first
|
||||
const changedSettings = getChangedSettings(formValues, settings);
|
||||
// Save mode and settings (backend will ignore custom settings if mode is 'default')
|
||||
const settingsToSave = {
|
||||
...formValues,
|
||||
epg_match_mode: settingsMode,
|
||||
};
|
||||
const changedSettings = getChangedSettings(settingsToSave, settings);
|
||||
if (Object.keys(changedSettings).length > 0) {
|
||||
await saveChangedSettings(settings, changedSettings);
|
||||
}
|
||||
|
|
@ -108,59 +121,81 @@ const EPGMatchModal = ({
|
|||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
Configure how channel names are normalized during matching, then start
|
||||
the auto-match process for {scopeText}.
|
||||
Match channels to EPG data for {scopeText}.
|
||||
</Text>
|
||||
|
||||
<TagsInput
|
||||
label="Ignore Prefixes"
|
||||
description="Removed from START of channel names (e.g., Prime:, Sling:, US:)"
|
||||
placeholder="Type and press Enter"
|
||||
value={formValues.epg_match_ignore_prefixes}
|
||||
onChange={(value) =>
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
epg_match_ignore_prefixes: value,
|
||||
}))
|
||||
}
|
||||
splitChars={[]}
|
||||
clearable
|
||||
/>
|
||||
<Radio.Group
|
||||
value={settingsMode}
|
||||
onChange={setSettingsMode}
|
||||
label="Matching Mode"
|
||||
>
|
||||
<Stack gap="xs" mt="xs">
|
||||
<Radio
|
||||
value="default"
|
||||
label="Use default settings"
|
||||
description="Recommended for most users. Handles standard channel name variations automatically."
|
||||
/>
|
||||
<Radio
|
||||
value="advanced"
|
||||
label="Configure advanced options"
|
||||
description="Use if channels aren't matching correctly. Add custom prefixes, suffixes, or strings to ignore."
|
||||
/>
|
||||
</Stack>
|
||||
</Radio.Group>
|
||||
|
||||
<TagsInput
|
||||
label="Ignore Suffixes"
|
||||
description="Removed from END of channel names (e.g., HD, 4K, +1)"
|
||||
placeholder="Type and press Enter"
|
||||
value={formValues.epg_match_ignore_suffixes}
|
||||
onChange={(value) =>
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
epg_match_ignore_suffixes: value,
|
||||
}))
|
||||
}
|
||||
splitChars={[]}
|
||||
clearable
|
||||
/>
|
||||
{settingsMode === 'advanced' && (
|
||||
<>
|
||||
<TagsInput
|
||||
label="Ignore Prefixes"
|
||||
description="Removed from START of channel names (e.g., Prime:, Sling:, US:)"
|
||||
placeholder="Type and press Enter"
|
||||
value={formValues.epg_match_ignore_prefixes}
|
||||
onChange={(value) =>
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
epg_match_ignore_prefixes: value,
|
||||
}))
|
||||
}
|
||||
splitChars={[]}
|
||||
clearable
|
||||
/>
|
||||
|
||||
<TagsInput
|
||||
label="Ignore Custom Strings"
|
||||
description="Removed from ANYWHERE in channel names (e.g., 24/7, LIVE)"
|
||||
placeholder="Type and press Enter"
|
||||
value={formValues.epg_match_ignore_custom}
|
||||
onChange={(value) =>
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
epg_match_ignore_custom: value,
|
||||
}))
|
||||
}
|
||||
splitChars={[]}
|
||||
clearable
|
||||
/>
|
||||
<TagsInput
|
||||
label="Ignore Suffixes"
|
||||
description="Removed from END of channel names (e.g., HD, 4K, +1)"
|
||||
placeholder="Type and press Enter"
|
||||
value={formValues.epg_match_ignore_suffixes}
|
||||
onChange={(value) =>
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
epg_match_ignore_suffixes: value,
|
||||
}))
|
||||
}
|
||||
splitChars={[]}
|
||||
clearable
|
||||
/>
|
||||
|
||||
<Text size="xs" c="dimmed">
|
||||
Channel display names are never modified. These settings only affect
|
||||
the matching algorithm.
|
||||
</Text>
|
||||
<TagsInput
|
||||
label="Ignore Custom Strings"
|
||||
description="Removed from ANYWHERE in channel names (e.g., 24/7, LIVE)"
|
||||
placeholder="Type and press Enter"
|
||||
value={formValues.epg_match_ignore_custom}
|
||||
onChange={(value) =>
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
epg_match_ignore_custom: value,
|
||||
}))
|
||||
}
|
||||
splitChars={[]}
|
||||
clearable
|
||||
/>
|
||||
|
||||
<Text size="xs" c="dimmed">
|
||||
Channel display names are never modified. These settings only affect
|
||||
the matching algorithm.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button variant="default" onClick={onClose} disabled={loading}>
|
||||
|
|
|
|||
190
frontend/src/components/modals/__tests__/EPGMatchModal.test.jsx
Normal file
190
frontend/src/components/modals/__tests__/EPGMatchModal.test.jsx
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import EPGMatchModal from '../EPGMatchModal';
|
||||
import * as SettingsUtils from '../../../utils/pages/SettingsUtils';
|
||||
import API from '../../../api';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../../../api', () => ({
|
||||
default: {
|
||||
matchEpg: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../utils/pages/SettingsUtils', () => ({
|
||||
getChangedSettings: vi.fn(),
|
||||
saveChangedSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@mantine/notifications', () => ({
|
||||
notifications: {
|
||||
show: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../store/settings', () => ({
|
||||
default: vi.fn((selector) => {
|
||||
const mockState = {
|
||||
settings: {
|
||||
epg_settings: {
|
||||
value: {
|
||||
epg_match_mode: 'default',
|
||||
epg_match_ignore_prefixes: [],
|
||||
epg_match_ignore_suffixes: [],
|
||||
epg_match_ignore_custom: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return selector(mockState);
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('EPGMatchModal', () => {
|
||||
const defaultProps = {
|
||||
opened: true,
|
||||
onClose: vi.fn(),
|
||||
selectedChannelIds: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the modal when opened', () => {
|
||||
render(<EPGMatchModal {...defaultProps} />);
|
||||
expect(screen.getByText('EPG Match Settings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show default mode selected by default', () => {
|
||||
render(<EPGMatchModal {...defaultProps} />);
|
||||
const defaultRadio = screen.getByLabelText('Use default settings');
|
||||
expect(defaultRadio).toBeChecked();
|
||||
});
|
||||
|
||||
it('should not show advanced fields in default mode', () => {
|
||||
render(<EPGMatchModal {...defaultProps} />);
|
||||
expect(screen.queryByLabelText('Ignore Prefixes')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show advanced fields when advanced mode is selected', async () => {
|
||||
render(<EPGMatchModal {...defaultProps} />);
|
||||
const advancedRadio = screen.getByLabelText('Configure advanced options');
|
||||
|
||||
fireEvent.click(advancedRadio);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('Ignore Prefixes')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Ignore Suffixes')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Ignore Custom Strings')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mode Switching', () => {
|
||||
it('should allow switching between default and advanced modes', async () => {
|
||||
render(<EPGMatchModal {...defaultProps} />);
|
||||
|
||||
const defaultRadio = screen.getByLabelText('Use default settings');
|
||||
const advancedRadio = screen.getByLabelText('Configure advanced options');
|
||||
|
||||
expect(defaultRadio).toBeChecked();
|
||||
|
||||
fireEvent.click(advancedRadio);
|
||||
await waitFor(() => {
|
||||
expect(advancedRadio).toBeChecked();
|
||||
expect(defaultRadio).not.toBeChecked();
|
||||
});
|
||||
|
||||
fireEvent.click(defaultRadio);
|
||||
await waitFor(() => {
|
||||
expect(defaultRadio).toBeChecked();
|
||||
expect(advancedRadio).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Submission', () => {
|
||||
it('should save mode and trigger auto-match', async () => {
|
||||
SettingsUtils.getChangedSettings.mockReturnValue({ epg_match_mode: 'default' });
|
||||
SettingsUtils.saveChangedSettings.mockResolvedValue();
|
||||
API.matchEpg.mockResolvedValue();
|
||||
|
||||
render(<EPGMatchModal {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByText('Start Auto-Match');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(SettingsUtils.saveChangedSettings).toHaveBeenCalled();
|
||||
expect(API.matchEpg).toHaveBeenCalled();
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass selectedChannelIds to matchEpg when provided', async () => {
|
||||
const selectedIds = [1, 2, 3];
|
||||
SettingsUtils.getChangedSettings.mockReturnValue({});
|
||||
API.matchEpg.mockResolvedValue();
|
||||
|
||||
render(<EPGMatchModal {...defaultProps} selectedChannelIds={selectedIds} />);
|
||||
|
||||
const submitButton = screen.getByText('Start Auto-Match');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.matchEpg).toHaveBeenCalledWith(selectedIds);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle save errors gracefully', async () => {
|
||||
const error = new Error('Save failed');
|
||||
SettingsUtils.getChangedSettings.mockReturnValue({ epg_match_mode: 'default' });
|
||||
SettingsUtils.saveChangedSettings.mockRejectedValue(error);
|
||||
|
||||
render(<EPGMatchModal {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByText('Start Auto-Match');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Settings Persistence', () => {
|
||||
it('should include epg_match_mode in settings to save', async () => {
|
||||
SettingsUtils.getChangedSettings.mockImplementation((values) => values);
|
||||
SettingsUtils.saveChangedSettings.mockResolvedValue();
|
||||
API.matchEpg.mockResolvedValue();
|
||||
|
||||
render(<EPGMatchModal {...defaultProps} />);
|
||||
|
||||
const submitButton = screen.getByText('Start Auto-Match');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(SettingsUtils.getChangedSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
epg_match_mode: 'default',
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('UI Text', () => {
|
||||
it('should show correct text for selected channels', () => {
|
||||
render(<EPGMatchModal {...defaultProps} selectedChannelIds={[1, 2, 3]} />);
|
||||
expect(screen.getByText(/Match channels to EPG data for 3 selected channel\(s\)/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show correct text for all channels', () => {
|
||||
render(<EPGMatchModal {...defaultProps} selectedChannelIds={[]} />);
|
||||
expect(screen.getByText(/Match channels to EPG data for all channels without EPG/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -28,7 +28,7 @@ export const saveChangedSettings = async (settings, changedSettings) => {
|
|||
|
||||
// Map of field prefixes to their groups
|
||||
const streamFields = ['default_user_agent', 'default_stream_profile', 'm3u_hash_key', 'preferred_region', 'auto_import_mapped_files'];
|
||||
const epgFields = ['epg_match_ignore_prefixes', 'epg_match_ignore_suffixes', 'epg_match_ignore_custom'];
|
||||
const epgFields = ['epg_match_mode', 'epg_match_ignore_prefixes', 'epg_match_ignore_suffixes', 'epg_match_ignore_custom'];
|
||||
const dvrFields = ['tv_template', 'movie_template', 'tv_fallback_dir', 'tv_fallback_template', 'movie_fallback_template',
|
||||
'comskip_enabled', 'comskip_custom_path', 'pre_offset_minutes', 'post_offset_minutes', 'series_rules'];
|
||||
const backupFields = ['schedule_enabled', 'schedule_frequency', 'schedule_time', 'schedule_day_of_week',
|
||||
|
|
@ -134,6 +134,12 @@ export const getChangedSettings = (values, settings) => {
|
|||
let actualValue = values[settingKey];
|
||||
let compareValue;
|
||||
|
||||
// Handle EPG mode field - always include (defaults to 'default' if not set)
|
||||
if (settingKey === 'epg_match_mode') {
|
||||
changedSettings[settingKey] = actualValue || 'default';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle EPG fields specially - keep as arrays, don't skip empty arrays
|
||||
if (epgFields.includes(settingKey)) {
|
||||
if (!Array.isArray(actualValue)) {
|
||||
|
|
|
|||
|
|
@ -407,5 +407,131 @@ describe('SettingsUtils', () => {
|
|||
expect(changes.network_access).toBeUndefined();
|
||||
expect(changes.time_zone).toBe('America/New_York');
|
||||
});
|
||||
|
||||
it('should always include epg_match_mode', () => {
|
||||
const values = {
|
||||
epg_match_mode: 'advanced',
|
||||
epg_match_ignore_prefixes: ['HD:'],
|
||||
};
|
||||
const settings = {};
|
||||
|
||||
const changes = SettingsUtils.getChangedSettings(values, settings);
|
||||
expect(changes.epg_match_mode).toBe('advanced');
|
||||
});
|
||||
|
||||
it('should default epg_match_mode to "default" if not provided', () => {
|
||||
const values = {
|
||||
epg_match_ignore_prefixes: ['HD:'],
|
||||
};
|
||||
const settings = {};
|
||||
|
||||
const changes = SettingsUtils.getChangedSettings(values, settings);
|
||||
// epg_match_mode should not be included if not in values
|
||||
expect(changes.epg_match_mode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should always include EPG array fields even if empty', () => {
|
||||
const values = {
|
||||
epg_match_ignore_prefixes: [],
|
||||
epg_match_ignore_suffixes: [],
|
||||
epg_match_ignore_custom: [],
|
||||
};
|
||||
const settings = {};
|
||||
|
||||
const changes = SettingsUtils.getChangedSettings(values, settings);
|
||||
expect(changes.epg_match_ignore_prefixes).toEqual([]);
|
||||
expect(changes.epg_match_ignore_suffixes).toEqual([]);
|
||||
expect(changes.epg_match_ignore_custom).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveChangedSettings - EPG Mode', () => {
|
||||
it('should save epg_match_mode to epg_settings group', async () => {
|
||||
const settings = {
|
||||
epg_settings: {
|
||||
id: 3,
|
||||
key: 'epg_settings',
|
||||
value: {
|
||||
epg_match_mode: 'default',
|
||||
epg_match_ignore_prefixes: [],
|
||||
epg_match_ignore_suffixes: [],
|
||||
epg_match_ignore_custom: [],
|
||||
}
|
||||
}
|
||||
};
|
||||
const changedSettings = {
|
||||
epg_match_mode: 'advanced',
|
||||
epg_match_ignore_prefixes: ['HD:'],
|
||||
};
|
||||
|
||||
API.updateSetting.mockResolvedValue({});
|
||||
|
||||
await SettingsUtils.saveChangedSettings(settings, changedSettings);
|
||||
|
||||
expect(API.updateSetting).toHaveBeenCalledWith({
|
||||
id: 3,
|
||||
key: 'epg_settings',
|
||||
value: {
|
||||
epg_match_mode: 'advanced',
|
||||
epg_match_ignore_prefixes: ['HD:'],
|
||||
epg_match_ignore_suffixes: [],
|
||||
epg_match_ignore_custom: [],
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should create epg_settings if it does not exist', async () => {
|
||||
const settings = {};
|
||||
const changedSettings = {
|
||||
epg_match_mode: 'advanced',
|
||||
epg_match_ignore_prefixes: ['Sling:'],
|
||||
};
|
||||
|
||||
API.createSetting.mockResolvedValue({});
|
||||
|
||||
await SettingsUtils.saveChangedSettings(settings, changedSettings);
|
||||
|
||||
expect(API.createSetting).toHaveBeenCalledWith({
|
||||
key: 'epg_settings',
|
||||
name: 'Epg Settings',
|
||||
value: {
|
||||
epg_match_mode: 'advanced',
|
||||
epg_match_ignore_prefixes: ['Sling:'],
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve existing EPG settings when updating mode', async () => {
|
||||
const settings = {
|
||||
epg_settings: {
|
||||
id: 3,
|
||||
key: 'epg_settings',
|
||||
value: {
|
||||
epg_match_mode: 'advanced',
|
||||
epg_match_ignore_prefixes: ['HD:'],
|
||||
epg_match_ignore_suffixes: [' 4K'],
|
||||
epg_match_ignore_custom: ['Plus'],
|
||||
}
|
||||
}
|
||||
};
|
||||
const changedSettings = {
|
||||
epg_match_mode: 'default',
|
||||
};
|
||||
|
||||
API.updateSetting.mockResolvedValue({});
|
||||
|
||||
await SettingsUtils.saveChangedSettings(settings, changedSettings);
|
||||
|
||||
expect(API.updateSetting).toHaveBeenCalledWith({
|
||||
id: 3,
|
||||
key: 'epg_settings',
|
||||
value: {
|
||||
epg_match_mode: 'default',
|
||||
epg_match_ignore_prefixes: ['HD:'],
|
||||
epg_match_ignore_suffixes: [' 4K'],
|
||||
epg_match_ignore_custom: ['Plus'],
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue