Conforming to style guide

This commit is contained in:
Nick Sandstrom 2026-03-14 07:56:05 -07:00
parent e95285ec8c
commit e421c007cc
7 changed files with 339 additions and 136 deletions

View file

@ -125,13 +125,17 @@ const mockFormValues = {
const makeFormMock = (overrides = {}) => ({
getInputProps: vi.fn((field, opts) => {
if (opts?.type === 'checkbox') return { checked: mockFormValues[field] ?? false, onChange: vi.fn() };
if (opts?.type === 'checkbox')
return { checked: mockFormValues[field] ?? false, onChange: vi.fn() };
return { value: mockFormValues[field] ?? '', onChange: vi.fn() };
}),
setValues: vi.fn(),
setFieldValue: vi.fn(),
getValues: vi.fn(() => mockFormValues),
onSubmit: vi.fn((handler) => (e) => { e?.preventDefault?.(); return handler(); }),
onSubmit: vi.fn((handler) => (e) => {
e?.preventDefault?.();
return handler();
}),
...overrides,
});
@ -229,7 +233,9 @@ describe('DvrSettingsForm', () => {
expect(screen.getByTestId('tv_template')).toBeInTheDocument();
expect(screen.getByTestId('tv_fallback_template')).toBeInTheDocument();
expect(screen.getByTestId('movie_template')).toBeInTheDocument();
expect(screen.getByTestId('movie_fallback_template')).toBeInTheDocument();
expect(
screen.getByTestId('movie_fallback_template')
).toBeInTheDocument();
});
});
@ -250,7 +256,9 @@ describe('DvrSettingsForm', () => {
it('shows "No custom comskip.ini uploaded." when no config exists', async () => {
render(<DvrSettingsForm active={true} />);
await waitFor(() => {
expect(screen.getByText('No custom comskip.ini uploaded.')).toBeInTheDocument();
expect(
screen.getByText('No custom comskip.ini uploaded.')
).toBeInTheDocument();
});
});
@ -261,7 +269,9 @@ describe('DvrSettingsForm', () => {
});
render(<DvrSettingsForm active={true} />);
await waitFor(() => {
expect(screen.getByText('Using /app/docker/comskip.ini')).toBeInTheDocument();
expect(
screen.getByText('Using /app/docker/comskip.ini')
).toBeInTheDocument();
});
});
});
@ -306,7 +316,9 @@ describe('DvrSettingsForm', () => {
it('handles getComskipConfig error gracefully', async () => {
vi.mocked(getComskipConfig).mockRejectedValue(new Error('network error'));
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
render(<DvrSettingsForm active={true} />);
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith(
@ -357,8 +369,13 @@ describe('DvrSettingsForm', () => {
fireEvent.submit(screen.getByText('Save').closest('form'));
await waitFor(() => {
expect(getChangedSettings).toHaveBeenCalledWith(mockFormValues, makeSettings());
expect(saveChangedSettings).toHaveBeenCalledWith(makeSettings(), { pre_offset_minutes: 5 });
expect(getChangedSettings).toHaveBeenCalledWith(
mockFormValues,
makeSettings()
);
expect(saveChangedSettings).toHaveBeenCalledWith(makeSettings(), {
pre_offset_minutes: 5,
});
});
});
@ -373,8 +390,12 @@ describe('DvrSettingsForm', () => {
});
it('does not show success alert when saveChangedSettings throws', async () => {
vi.mocked(saveChangedSettings).mockRejectedValue(new Error('save failed'));
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
vi.mocked(saveChangedSettings).mockRejectedValue(
new Error('save failed')
);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
render(<DvrSettingsForm active={true} />);
fireEvent.submit(screen.getByText('Save').closest('form'));
@ -400,11 +421,17 @@ describe('DvrSettingsForm', () => {
});
it('calls uploadComskipIni with the selected file', async () => {
const mockFile = new File(['content'], 'comskip.ini', { type: 'text/plain' });
vi.mocked(uploadComskipIni).mockResolvedValue({ path: '/uploaded/comskip.ini' });
const mockFile = new File(['content'], 'comskip.ini', {
type: 'text/plain',
});
vi.mocked(uploadComskipIni).mockResolvedValue({
path: '/uploaded/comskip.ini',
});
render(<DvrSettingsForm active={true} />);
fireEvent.change(screen.getByTestId('file-input'), { target: { files: [mockFile] } });
fireEvent.change(screen.getByTestId('file-input'), {
target: { files: [mockFile] },
});
await waitFor(() => {
fireEvent.click(screen.getByText('Upload comskip.ini'));
@ -416,11 +443,17 @@ describe('DvrSettingsForm', () => {
});
it('shows success notification after successful upload', async () => {
const mockFile = new File(['content'], 'comskip.ini', { type: 'text/plain' });
vi.mocked(uploadComskipIni).mockResolvedValue({ path: '/uploaded/comskip.ini' });
const mockFile = new File(['content'], 'comskip.ini', {
type: 'text/plain',
});
vi.mocked(uploadComskipIni).mockResolvedValue({
path: '/uploaded/comskip.ini',
});
render(<DvrSettingsForm active={true} />);
fireEvent.change(screen.getByTestId('file-input'), { target: { files: [mockFile] } });
fireEvent.change(screen.getByTestId('file-input'), {
target: { files: [mockFile] },
});
await waitFor(() => {
fireEvent.click(screen.getByText('Upload comskip.ini'));
@ -428,17 +461,26 @@ describe('DvrSettingsForm', () => {
await waitFor(() => {
expect(showNotification).toHaveBeenCalledWith(
expect.objectContaining({ title: 'comskip.ini uploaded', color: 'green' })
expect.objectContaining({
title: 'comskip.ini uploaded',
color: 'green',
})
);
});
});
it('sets comskip_custom_path form field after successful upload', async () => {
const mockFile = new File(['content'], 'comskip.ini', { type: 'text/plain' });
vi.mocked(uploadComskipIni).mockResolvedValue({ path: '/uploaded/comskip.ini' });
const mockFile = new File(['content'], 'comskip.ini', {
type: 'text/plain',
});
vi.mocked(uploadComskipIni).mockResolvedValue({
path: '/uploaded/comskip.ini',
});
render(<DvrSettingsForm active={true} />);
fireEvent.change(screen.getByTestId('file-input'), { target: { files: [mockFile] } });
fireEvent.change(screen.getByTestId('file-input'), {
target: { files: [mockFile] },
});
await waitFor(() => {
fireEvent.click(screen.getByText('Upload comskip.ini'));
@ -453,12 +495,18 @@ describe('DvrSettingsForm', () => {
});
it('handles upload error gracefully', async () => {
const mockFile = new File(['content'], 'comskip.ini', { type: 'text/plain' });
const mockFile = new File(['content'], 'comskip.ini', {
type: 'text/plain',
});
vi.mocked(uploadComskipIni).mockRejectedValue(new Error('upload failed'));
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
render(<DvrSettingsForm active={true} />);
fireEvent.change(screen.getByTestId('file-input'), { target: { files: [mockFile] } });
fireEvent.change(screen.getByTestId('file-input'), {
target: { files: [mockFile] },
});
await waitFor(() => {
fireEvent.click(screen.getByText('Upload comskip.ini'));
@ -480,4 +528,4 @@ describe('DvrSettingsForm', () => {
expect(uploadComskipIni).not.toHaveBeenCalled();
});
});
});
});

View file

@ -64,7 +64,7 @@ vi.mock('@dnd-kit/modifiers', () => ({
// Mock Mantine components
vi.mock('@mantine/core', () => ({
Box: ({ children, ...props }) => <div {...props}>{children}</div>,
Button: ({ children, onClick, disabled, ...props }) => (
Button: ({ children, onClick, disabled }) => (
<button onClick={onClick} disabled={disabled} data-testid="reset-button">
{children}
</button>

View file

@ -36,8 +36,12 @@ vi.mock('../../../ConfirmationDialog.jsx', () => ({
<div data-testid="confirmation-dialog">
<div data-testid="confirm-title">{title}</div>
<div data-testid="confirm-message">{message}</div>
<button data-testid="confirm-ok" onClick={onConfirm}>Confirm</button>
<button data-testid="confirm-cancel" onClick={onClose}>Cancel</button>
<button data-testid="confirm-ok" onClick={onConfirm}>
Confirm
</button>
<button data-testid="confirm-cancel" onClick={onClose}>
Cancel
</button>
</div>
) : null,
}));
@ -85,7 +89,10 @@ vi.mock('@mantine/core', () => ({
//
import useSettingsStore from '../../../../store/settings.jsx';
import { useForm } from '@mantine/form';
import { checkSetting, updateSetting } from '../../../../utils/pages/SettingsUtils.js';
import {
checkSetting,
updateSetting,
} from '../../../../utils/pages/SettingsUtils.js';
import {
getNetworkAccessFormInitialValues,
getNetworkAccessFormValidation,
@ -139,10 +146,15 @@ const setupMocks = ({ settings = makeSettings(), formOverrides = {} } = {}) => {
const formMock = makeFormMock(formOverrides);
vi.mocked(useForm).mockReturnValue(formMock);
vi.mocked(getNetworkAccessFormInitialValues).mockReturnValue(mockInitialValues);
vi.mocked(getNetworkAccessFormInitialValues).mockReturnValue(
mockInitialValues
);
vi.mocked(getNetworkAccessFormValidation).mockReturnValue({});
vi.mocked(getNetworkAccessDefaults).mockReturnValue(mockInitialValues);
vi.mocked(checkSetting).mockResolvedValue({ client_ip: '192.168.1.1', UI: ['192.168.0.0/16'] });
vi.mocked(checkSetting).mockResolvedValue({
client_ip: '192.168.1.1',
UI: ['192.168.0.0/16'],
});
vi.mocked(updateSetting).mockResolvedValue(undefined);
vi.mocked(useSettingsStore).mockImplementation((sel) => sel({ settings }));
@ -180,7 +192,9 @@ describe('NetworkAccessForm', () => {
it('does not show confirmation dialog on initial render', () => {
render(<NetworkAccessForm active={true} />);
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
expect(
screen.queryByTestId('confirmation-dialog')
).not.toBeInTheDocument();
});
it('does not show error alert on initial render', () => {
@ -238,7 +252,10 @@ describe('NetworkAccessForm', () => {
vi.mocked(updateSetting).mockResolvedValue(undefined);
const { rerender } = render(<NetworkAccessForm active={true} />);
fireEvent.submit(screen.getByText('Save').closest('form') ?? screen.getByText('Save').closest('div'));
fireEvent.submit(
screen.getByText('Save').closest('form') ??
screen.getByText('Save').closest('div')
);
await waitFor(() => {
expect(screen.queryByTestId('confirmation-dialog')).not.toBeNull();
@ -270,7 +287,9 @@ describe('NetworkAccessForm', () => {
fireEvent.click(screen.getByTestId('confirm-cancel'));
await waitFor(() => {
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
expect(
screen.queryByTestId('confirmation-dialog')
).not.toBeInTheDocument();
});
});
@ -295,7 +314,9 @@ describe('NetworkAccessForm', () => {
await waitFor(() => {
expect(screen.getByTestId('alert')).toBeInTheDocument();
expect(screen.getByTestId('alert-title')).toHaveTextContent('Saved Successfully');
expect(screen.getByTestId('alert-title')).toHaveTextContent(
'Saved Successfully'
);
});
});
@ -312,8 +333,9 @@ describe('NetworkAccessForm', () => {
});
it('does not show success alert when updateSetting throws', async () => {
vi.mocked(updateSetting).mockRejectedValue(
{ body: { value: { m3u_custom_cidrs: 'Invalid CIDR' } } });
vi.mocked(updateSetting).mockRejectedValue({
body: { value: { m3u_custom_cidrs: 'Invalid CIDR' } },
});
render(<NetworkAccessForm active={true} />);
fireEvent.click(screen.getByText('Save'));
@ -344,7 +366,9 @@ describe('NetworkAccessForm', () => {
vi.mocked(checkSetting).mockResolvedValue(null);
render(<NetworkAccessForm active={true} />);
await waitFor(() => {
expect(screen.queryByText(/\d+\.\d+\.\d+\.\d+/)).not.toBeInTheDocument();
expect(
screen.queryByText(/\d+\.\d+\.\d+\.\d+/)
).not.toBeInTheDocument();
});
});
});
@ -352,9 +376,11 @@ describe('NetworkAccessForm', () => {
// Network access error
describe('network access error state', () => {
it('clears error state after successful save', async () => {
vi.mocked(checkSetting).mockResolvedValue(
{ error: true, message: 'Invalid CIDR', data: 'Error details' }
);
vi.mocked(checkSetting).mockResolvedValue({
error: true,
message: 'Invalid CIDR',
data: 'Error details',
});
render(<NetworkAccessForm active={true} />);
@ -362,8 +388,10 @@ describe('NetworkAccessForm', () => {
fireEvent.click(screen.getByText('Save'));
await waitFor(() => screen.getByTestId('alert'));
vi.mocked(checkSetting).mockResolvedValue(
{ client_ip: '192.168.1.1', UI: ['192.168.0.0/16'] });
vi.mocked(checkSetting).mockResolvedValue({
client_ip: '192.168.1.1',
UI: ['192.168.0.0/16'],
});
// Second save succeeds
fireEvent.click(screen.getByText('Save'));
@ -371,8 +399,10 @@ describe('NetworkAccessForm', () => {
fireEvent.click(screen.getByTestId('confirm-ok'));
await waitFor(() => {
expect(screen.getByTestId('alert-title')).toHaveTextContent('Saved Successfully');
expect(screen.getByTestId('alert-title')).toHaveTextContent(
'Saved Successfully'
);
});
});
});
});
});

View file

@ -5,8 +5,14 @@ import ProxySettingsForm from '../ProxySettingsForm';
// Constants mock
vi.mock('../../../../constants.js', () => ({
PROXY_SETTINGS_OPTIONS: {
buffering_timeout: { label: 'Buffering Timeout', description: 'Timeout in seconds' },
buffering_speed: { label: 'Buffering Speed', description: 'Speed multiplier' },
buffering_timeout: {
label: 'Buffering Timeout',
description: 'Timeout in seconds',
},
buffering_speed: {
label: 'Buffering Speed',
description: 'Speed multiplier',
},
redis_url: { label: 'Redis URL', description: 'Redis connection URL' },
},
}));
@ -133,7 +139,9 @@ const setupMocks = ({ settings = makeSettings(), formOverrides = {} } = {}) => {
const formMock = makeFormMock(formOverrides);
vi.mocked(useForm).mockReturnValue(formMock);
vi.mocked(getProxySettingsFormInitialValues).mockReturnValue(mockInitialValues);
vi.mocked(getProxySettingsFormInitialValues).mockReturnValue(
mockInitialValues
);
vi.mocked(getProxySettingDefaults).mockReturnValue(mockDefaults);
vi.mocked(updateSetting).mockResolvedValue({ success: true });
vi.mocked(useSettingsStore).mockImplementation((sel) => sel({ settings }));
@ -161,12 +169,16 @@ describe('ProxySettingsForm', () => {
it('renders NumberInput for buffering_timeout', () => {
render(<ProxySettingsForm active={true} />);
expect(screen.getByTestId('number-input-Buffering Timeout')).toBeInTheDocument();
expect(
screen.getByTestId('number-input-Buffering Timeout')
).toBeInTheDocument();
});
it('renders NumberInput for buffering_speed', () => {
render(<ProxySettingsForm active={true} />);
expect(screen.getByTestId('number-input-Buffering Speed')).toBeInTheDocument();
expect(
screen.getByTestId('number-input-Buffering Speed')
).toBeInTheDocument();
});
it('renders TextInput for redis_url', () => {
@ -216,7 +228,9 @@ describe('ProxySettingsForm', () => {
});
it('handles null settings gracefully', () => {
vi.mocked(useSettingsStore).mockImplementation((sel) => sel({ settings: null }));
vi.mocked(useSettingsStore).mockImplementation((sel) =>
sel({ settings: null })
);
expect(() => render(<ProxySettingsForm active={true} />)).not.toThrow();
});
});
@ -261,7 +275,9 @@ describe('ProxySettingsForm', () => {
await waitFor(() => {
expect(screen.getByTestId('alert')).toBeInTheDocument();
expect(screen.getByTestId('alert-title')).toHaveTextContent('Saved Successfully');
expect(screen.getByTestId('alert-title')).toHaveTextContent(
'Saved Successfully'
);
});
});
@ -344,4 +360,4 @@ describe('ProxySettingsForm', () => {
expect(formMock.getInputProps).toHaveBeenCalledWith('redis_url');
});
});
});
});

View file

@ -35,16 +35,16 @@ vi.mock('@mantine/form', () => ({ useForm: vi.fn() }));
// ConfirmationDialog mock
vi.mock('../../../ConfirmationDialog.jsx', () => ({
default: ({
opened,
onClose,
onConfirm,
title,
message,
confirmLabel,
cancelLabel,
actionKey,
onSuppressChange,
}) =>
opened,
onClose,
onConfirm,
title,
message,
confirmLabel,
cancelLabel,
actionKey,
onSuppressChange,
}) =>
opened ? (
<div data-testid="confirmation-dialog">
<div data-testid="confirm-title">{title}</div>
@ -98,7 +98,9 @@ vi.mock('@mantine/core', () => ({
aria-label={label}
multiple
onChange={(e) => {
const selected = Array.from(e.target.selectedOptions).map((o) => o.value);
const selected = Array.from(e.target.selectedOptions).map(
(o) => o.value
);
rest.onChange?.(selected);
}}
value={rest.value ?? []}
@ -212,12 +214,12 @@ const makeStreamProfiles = () => [
];
const setupMocks = ({
settings = makeSettings(),
formOverrides = {},
warningSuppressed = false,
userAgents = makeUserAgents(),
streamProfiles = makeStreamProfiles(),
} = {}) => {
settings = makeSettings(),
formOverrides = {},
warningSuppressed = false,
userAgents = makeUserAgents(),
streamProfiles = makeStreamProfiles(),
} = {}) => {
const formMock = makeFormMock(formOverrides);
vi.mocked(useForm).mockReturnValue(formMock);
@ -290,7 +292,9 @@ describe('StreamSettingsForm', () => {
it('renders the Auto-Import Mapped Files switch', () => {
render(<StreamSettingsForm active={true} />);
expect(screen.getByTestId('auto_import_mapped_files')).toBeInTheDocument();
expect(
screen.getByTestId('auto_import_mapped_files')
).toBeInTheDocument();
});
it('renders the M3U Hash Key multiselect', () => {
@ -323,7 +327,9 @@ describe('StreamSettingsForm', () => {
it('does not show confirmation dialog on initial render', () => {
render(<StreamSettingsForm active={true} />);
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
expect(
screen.queryByTestId('confirmation-dialog')
).not.toBeInTheDocument();
});
});
@ -429,13 +435,19 @@ describe('StreamSettingsForm', () => {
fireEvent.submit(screen.getByText('Save').closest('form'));
await waitFor(() => {
expect(screen.getByTestId('alert')).toBeInTheDocument();
expect(screen.getByTestId('alert-title')).toHaveTextContent('Saved Successfully');
expect(screen.getByTestId('alert-title')).toHaveTextContent(
'Saved Successfully'
);
});
});
it('does not show success alert when saveChangedSettings throws', async () => {
vi.mocked(saveChangedSettings).mockRejectedValue(new Error('save failed'));
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
vi.mocked(saveChangedSettings).mockRejectedValue(
new Error('save failed')
);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
render(<StreamSettingsForm active={true} />);
fireEvent.submit(screen.getByText('Save').closest('form'));
@ -449,12 +461,17 @@ describe('StreamSettingsForm', () => {
it('logs error when saveChangedSettings throws', async () => {
const error = new Error('save failed');
vi.mocked(saveChangedSettings).mockRejectedValue(error);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
render(<StreamSettingsForm active={true} />);
fireEvent.submit(screen.getByText('Save').closest('form'));
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith('Error saving settings:', error);
expect(consoleSpy).toHaveBeenCalledWith(
'Error saving settings:',
error
);
});
consoleSpy.mockRestore();
});
@ -466,7 +483,9 @@ describe('StreamSettingsForm', () => {
await waitFor(() => {
expect(saveChangedSettings).toHaveBeenCalled();
});
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
expect(
screen.queryByTestId('confirmation-dialog')
).not.toBeInTheDocument();
});
});
@ -501,7 +520,9 @@ describe('StreamSettingsForm', () => {
fireEvent.submit(screen.getByText('Save').closest('form'));
await waitFor(() => {
expect(screen.getByTestId('confirm-ok')).toHaveTextContent('Save and Rehash');
expect(screen.getByTestId('confirm-ok')).toHaveTextContent(
'Save and Rehash'
);
});
});
@ -526,9 +547,7 @@ describe('StreamSettingsForm', () => {
render(<StreamSettingsForm active={true} />);
fireEvent.submit(screen.getByText('Save').closest('form'));
await waitFor(() =>
screen.getByTestId('confirmation-dialog')
);
await waitFor(() => screen.getByTestId('confirmation-dialog'));
fireEvent.click(screen.getByTestId('confirm-ok'));
await waitFor(() => {
@ -546,7 +565,9 @@ describe('StreamSettingsForm', () => {
fireEvent.click(screen.getByTestId('confirm-ok'));
await waitFor(() => {
expect(screen.getByTestId('alert-title')).toHaveTextContent('Saved Successfully');
expect(screen.getByTestId('alert-title')).toHaveTextContent(
'Saved Successfully'
);
});
});
@ -560,7 +581,9 @@ describe('StreamSettingsForm', () => {
fireEvent.click(screen.getByTestId('confirm-ok'));
await waitFor(() => {
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
expect(
screen.queryByTestId('confirmation-dialog')
).not.toBeInTheDocument();
});
});
@ -574,7 +597,9 @@ describe('StreamSettingsForm', () => {
fireEvent.click(screen.getByTestId('confirm-cancel'));
await waitFor(() => {
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
expect(
screen.queryByTestId('confirmation-dialog')
).not.toBeInTheDocument();
});
expect(saveChangedSettings).not.toHaveBeenCalled();
});
@ -590,7 +615,9 @@ describe('StreamSettingsForm', () => {
await waitFor(() => {
expect(saveChangedSettings).toHaveBeenCalled();
});
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
expect(
screen.queryByTestId('confirmation-dialog')
).not.toBeInTheDocument();
});
});
@ -613,7 +640,9 @@ describe('StreamSettingsForm', () => {
fireEvent.click(screen.getByText('Rehash Streams'));
await waitFor(() => {
expect(screen.getByTestId('confirm-ok')).toHaveTextContent('Start Rehash');
expect(screen.getByTestId('confirm-ok')).toHaveTextContent(
'Start Rehash'
);
});
});
@ -636,7 +665,9 @@ describe('StreamSettingsForm', () => {
await waitFor(() => {
expect(rehashStreams).toHaveBeenCalled();
});
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
expect(
screen.queryByTestId('confirmation-dialog')
).not.toBeInTheDocument();
});
it('shows rehash success alert after rehash completes', async () => {
@ -654,7 +685,9 @@ describe('StreamSettingsForm', () => {
it('does not show rehash success alert when rehashStreams throws', async () => {
setupMocks({ warningSuppressed: true });
vi.mocked(rehashStreams).mockRejectedValue(new Error('fail'));
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
render(<StreamSettingsForm active={true} />);
fireEvent.click(screen.getByText('Rehash Streams'));
@ -669,12 +702,17 @@ describe('StreamSettingsForm', () => {
setupMocks({ warningSuppressed: true });
const error = new Error('network error');
vi.mocked(rehashStreams).mockRejectedValue(error);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
render(<StreamSettingsForm active={true} />);
fireEvent.click(screen.getByText('Rehash Streams'));
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith('Error rehashing streams:', error);
expect(consoleSpy).toHaveBeenCalledWith(
'Error rehashing streams:',
error
);
});
consoleSpy.mockRestore();
});
@ -686,7 +724,9 @@ describe('StreamSettingsForm', () => {
fireEvent.click(screen.getByTestId('confirm-cancel'));
await waitFor(() => {
expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
expect(
screen.queryByTestId('confirmation-dialog')
).not.toBeInTheDocument();
});
expect(rehashStreams).not.toHaveBeenCalled();
});
@ -710,7 +750,9 @@ describe('StreamSettingsForm', () => {
it('Rehash Streams button is disabled while rehashing', async () => {
let resolveRehash;
vi.mocked(rehashStreams).mockReturnValue(
new Promise((res) => { resolveRehash = res; })
new Promise((res) => {
resolveRehash = res;
})
);
setupMocks({ warningSuppressed: true });
render(<StreamSettingsForm active={true} />);
@ -736,7 +778,9 @@ describe('StreamSettingsForm', () => {
it('calls getInputProps for default_stream_profile', () => {
render(<StreamSettingsForm active={true} />);
expect(formMock.getInputProps).toHaveBeenCalledWith('default_stream_profile');
expect(formMock.getInputProps).toHaveBeenCalledWith(
'default_stream_profile'
);
});
it('calls getInputProps for preferred_region', () => {
@ -757,4 +801,4 @@ describe('StreamSettingsForm', () => {
expect(formMock.getInputProps).toHaveBeenCalledWith('m3u_hash_key');
});
});
});
});

View file

@ -53,7 +53,11 @@ vi.mock('@mantine/core', () => ({
// Imports after mocks
//
import useSettingsStore from '../../../../store/settings.jsx';
import { getChangedSettings, parseSettings, saveChangedSettings } from '../../../../utils/pages/SettingsUtils.js';
import {
getChangedSettings,
parseSettings,
saveChangedSettings,
} from '../../../../utils/pages/SettingsUtils.js';
import { getSystemSettingsFormInitialValues } from '../../../../utils/forms/settings/SystemSettingsFormUtils.js';
import { useForm } from '@mantine/form';
@ -83,7 +87,9 @@ const setupMocks = ({ settings = makeSettings() } = {}) => {
vi.mocked(getSystemSettingsFormInitialValues).mockReturnValue(formValues);
vi.mocked(useSettingsStore).mockImplementation((sel) => sel({ settings }));
vi.mocked(parseSettings).mockReturnValue(formValues);
vi.mocked(getChangedSettings).mockReturnValue({ max_system_events: settings?.max_system_events ?? 100 });
vi.mocked(getChangedSettings).mockReturnValue({
max_system_events: settings?.max_system_events ?? 100,
});
vi.mocked(saveChangedSettings).mockResolvedValue(undefined);
return { formMock };
@ -122,7 +128,9 @@ describe('SystemSettingsForm', () => {
setupMocks();
render(<SystemSettingsForm active={true} />);
expect(
screen.getByText('Number of events to retain (minimum: 10, maximum: 1000)')
screen.getByText(
'Number of events to retain (minimum: 10, maximum: 1000)'
)
).toBeInTheDocument();
});
@ -184,7 +192,9 @@ describe('SystemSettingsForm', () => {
const settings = makeSettings();
const { formMock } = setupMocks({ settings });
render(<SystemSettingsForm active={true} />);
expect(formMock.setValues).toHaveBeenCalledWith({ max_system_events: 100 });
expect(formMock.setValues).toHaveBeenCalledWith({
max_system_events: 100,
});
});
it('does not call parseSettings when settings is null', () => {
@ -197,8 +207,12 @@ describe('SystemSettingsForm', () => {
submitting: false,
};
vi.mocked(useForm).mockReturnValue(formMock);
vi.mocked(getSystemSettingsFormInitialValues).mockReturnValue({ max_system_events: 100 });
vi.mocked(useSettingsStore).mockImplementation((sel) => sel({ settings: null }));
vi.mocked(getSystemSettingsFormInitialValues).mockReturnValue({
max_system_events: 100,
});
vi.mocked(useSettingsStore).mockImplementation((sel) =>
sel({ settings: null })
);
vi.mocked(parseSettings).mockReturnValue({});
vi.mocked(saveChangedSettings).mockResolvedValue(undefined);
@ -213,8 +227,13 @@ describe('SystemSettingsForm', () => {
it('calls form.setFieldValue when NumberInput changes', () => {
const { formMock } = setupMocks();
render(<SystemSettingsForm active={true} />);
fireEvent.change(screen.getByTestId('number-input'), { target: { value: '200' } });
expect(formMock.setFieldValue).toHaveBeenCalledWith('max_system_events', 200);
fireEvent.change(screen.getByTestId('number-input'), {
target: { value: '200' },
});
expect(formMock.setFieldValue).toHaveBeenCalledWith(
'max_system_events',
200
);
});
});
@ -229,7 +248,10 @@ describe('SystemSettingsForm', () => {
fireEvent.click(screen.getByText('Save'));
await waitFor(() => {
expect(getChangedSettings).toHaveBeenCalledWith(formMock.getValues(), settings);
expect(getChangedSettings).toHaveBeenCalledWith(
formMock.getValues(),
settings
);
expect(saveChangedSettings).toHaveBeenCalled();
});
});
@ -247,9 +269,13 @@ describe('SystemSettingsForm', () => {
});
it('does not show success alert when saveChangedSettings throws', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
setupMocks();
vi.mocked(saveChangedSettings).mockRejectedValue(new Error('save failed'));
vi.mocked(saveChangedSettings).mockRejectedValue(
new Error('save failed')
);
render(<SystemSettingsForm active={true} />);
fireEvent.click(screen.getByText('Save'));
@ -263,7 +289,9 @@ describe('SystemSettingsForm', () => {
it('logs error when saveChangedSettings throws', async () => {
const error = new Error('save failed');
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
setupMocks();
vi.mocked(saveChangedSettings).mockRejectedValue(error);
@ -271,7 +299,10 @@ describe('SystemSettingsForm', () => {
fireEvent.click(screen.getByText('Save'));
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith('Error saving settings:', error);
expect(consoleSpy).toHaveBeenCalledWith(
'Error saving settings:',
error
);
});
consoleSpy.mockRestore();
});
@ -308,4 +339,4 @@ describe('SystemSettingsForm', () => {
expect(screen.getByTestId('alert')).toBeInTheDocument();
});
});
});
});

View file

@ -7,7 +7,9 @@ vi.mock('../../../../store/settings.jsx', () => ({ default: vi.fn() }));
// Hook mocks
vi.mock('../../../../hooks/useLocalStorage.jsx', () => ({ default: vi.fn() }));
vi.mock('../../../../hooks/useTablePreferences.jsx', () => ({ default: vi.fn() }));
vi.mock('../../../../hooks/useTablePreferences.jsx', () => ({
default: vi.fn(),
}));
// Utility mocks
vi.mock('../../../../utils/dateTimeUtils.js', () => ({
@ -62,7 +64,10 @@ vi.mock('@mantine/core', () => ({
import useSettingsStore from '../../../../store/settings.jsx';
import useLocalStorage from '../../../../hooks/useLocalStorage.jsx';
import useTablePreferences from '../../../../hooks/useTablePreferences.jsx';
import { buildTimeZoneOptions, getDefaultTimeZone } from '../../../../utils/dateTimeUtils.js';
import {
buildTimeZoneOptions,
getDefaultTimeZone,
} from '../../../../utils/dateTimeUtils.js';
import { showNotification } from '../../../../utils/notificationUtils.js';
import { saveTimeZoneSetting } from '../../../../utils/forms/settings/UiSettingsFormUtils.js';
@ -83,13 +88,13 @@ const TZ_OPTIONS = [
];
const setupMocks = ({
settings = makeSettings(),
timeFormat = '12h',
dateFormat = 'mdy',
timeZone = DEFAULT_TZ,
headerPinned = false,
tableSize = 'default',
} = {}) => {
settings = makeSettings(),
timeFormat = '12h',
dateFormat = 'mdy',
timeZone = DEFAULT_TZ,
headerPinned = false,
tableSize = 'default',
} = {}) => {
const setTimeFormat = vi.fn();
const setDateFormat = vi.fn();
const setTimeZone = vi.fn();
@ -102,13 +107,12 @@ const setupMocks = ({
vi.mocked(useSettingsStore).mockImplementation((sel) => sel({ settings }));
vi.mocked(useLocalStorage)
.mockImplementation((key, defaultVal) => {
if (key === 'time-format') return [timeFormat, setTimeFormat];
if (key === 'date-format') return [dateFormat, setDateFormat];
if (key === 'time-zone') return [timeZone, setTimeZone];
return [defaultVal, vi.fn()];
});
vi.mocked(useLocalStorage).mockImplementation((key, defaultVal) => {
if (key === 'time-format') return [timeFormat, setTimeFormat];
if (key === 'date-format') return [dateFormat, setDateFormat];
if (key === 'time-zone') return [timeZone, setTimeZone];
return [defaultVal, vi.fn()];
});
vi.mocked(useTablePreferences).mockReturnValue({
headerPinned,
@ -117,7 +121,13 @@ const setupMocks = ({
setTableSize,
});
return { setTimeFormat, setDateFormat, setTimeZone, setHeaderPinned, setTableSize };
return {
setTimeFormat,
setDateFormat,
setTimeZone,
setHeaderPinned,
setTableSize,
};
};
//
@ -164,7 +174,9 @@ describe('UiSettingsForm', () => {
it('renders the switch description', () => {
setupMocks();
render(<UiSettingsForm />);
expect(screen.getByText('Keep table headers visible when scrolling')).toBeInTheDocument();
expect(
screen.getByText('Keep table headers visible when scrolling')
).toBeInTheDocument();
});
it('renders Table Size select with initial value', () => {
@ -331,7 +343,10 @@ describe('UiSettingsForm', () => {
});
render(<UiSettingsForm />);
await waitFor(() => {
expect(saveTimeZoneSetting).toHaveBeenCalledWith(DEFAULT_TZ, makeSettings({ timeZone: null }));
expect(saveTimeZoneSetting).toHaveBeenCalledWith(
DEFAULT_TZ,
makeSettings({ timeZone: null })
);
});
});
@ -359,9 +374,13 @@ describe('UiSettingsForm', () => {
describe('error handling', () => {
it('shows error notification when saveTimeZoneSetting throws on tz change', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
setupMocks({ settings: makeSettings({ timeZone: null }), timeZone: '' });
vi.mocked(saveTimeZoneSetting).mockRejectedValue(new Error('network error'));
vi.mocked(saveTimeZoneSetting).mockRejectedValue(
new Error('network error')
);
render(<UiSettingsForm />);
fireEvent.change(screen.getByTestId('select-time-zone'), {
@ -370,7 +389,10 @@ describe('UiSettingsForm', () => {
await waitFor(() => {
expect(showNotification).toHaveBeenCalledWith(
expect.objectContaining({ color: 'red', title: 'Failed to update time zone' })
expect.objectContaining({
color: 'red',
title: 'Failed to update time zone',
})
);
});
consoleSpy.mockRestore();
@ -378,7 +400,9 @@ describe('UiSettingsForm', () => {
it('logs error when saveTimeZoneSetting throws on tz change', async () => {
const error = new Error('network error');
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
setupMocks({ settings: makeSettings({ timeZone: null }), timeZone: '' });
vi.mocked(saveTimeZoneSetting).mockRejectedValue(error);
@ -397,15 +421,25 @@ describe('UiSettingsForm', () => {
});
it('shows error notification when saveTimeZoneSetting throws during initial sync', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
setupMocks({ settings: makeSettings({ timeZone: null }), timeZone: DEFAULT_TZ });
vi.mocked(saveTimeZoneSetting).mockRejectedValue(new Error('sync failed'));
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
setupMocks({
settings: makeSettings({ timeZone: null }),
timeZone: DEFAULT_TZ,
});
vi.mocked(saveTimeZoneSetting).mockRejectedValue(
new Error('sync failed')
);
render(<UiSettingsForm />);
await waitFor(() => {
expect(showNotification).toHaveBeenCalledWith(
expect.objectContaining({ color: 'red', title: 'Failed to update time zone' })
expect.objectContaining({
color: 'red',
title: 'Failed to update time zone',
})
);
});
consoleSpy.mockRestore();
@ -421,4 +455,4 @@ describe('UiSettingsForm', () => {
expect(buildTimeZoneOptions).toHaveBeenCalledWith('America/Chicago');
});
});
});
});