From 284ba87fe7c96e85ca3b84ae2494fe6a56412224 Mon Sep 17 00:00:00 2001
From: Nick Sandstrom <32273437+nick4810@users.noreply.github.com>
Date: Mon, 25 May 2026 18:15:09 -0700
Subject: [PATCH] Minor formatting changes
---
frontend/src/components/forms/User.jsx | 2 +-
frontend/src/components/forms/UserAgent.jsx | 8 +-
.../__tests__/ProgramRecordingModal.test.jsx | 65 +++--
.../forms/__tests__/Recording.test.jsx | 127 +++++++---
.../__tests__/RecordingDetailsModal.test.jsx | 84 +++++--
.../__tests__/RecurringRuleModal.test.jsx | 116 +++++----
.../forms/__tests__/ScheduleInput.test.jsx | 96 ++++++--
.../__tests__/SeriesRecordingModal.test.jsx | 72 ++++--
.../__tests__/SeriesRuleEditorModal.test.jsx | 110 ++++++---
.../forms/__tests__/Stream.test.jsx | 12 +-
.../forms/__tests__/StreamProfile.test.jsx | 66 ++++-
.../forms/__tests__/SuperuserForm.test.jsx | 5 +-
.../components/forms/__tests__/User.test.jsx | 228 ++++++++++++++----
.../forms/__tests__/UserAgent.test.jsx | 64 ++++-
.../__tests__/VODCategoryFilter.test.jsx | 215 +++++++++++++----
.../utils/forms/SeriesRuleEditorModalUtils.js | 4 +-
frontend/src/utils/forms/UserUtils.js | 14 +-
.../RecordingDetailsModalUtils.test.js | 29 ++-
.../forms/__tests__/RecordingUtils.test.js | 73 +++---
.../SeriesRuleEditorModalUtils.test.js | 84 ++++---
.../__tests__/StreamProfileUtils.test.js | 6 +-
.../utils/forms/__tests__/StreamUtils.test.js | 6 +-
.../forms/__tests__/UserAgentUtils.test.js | 12 +-
.../utils/forms/__tests__/UserUtils.test.js | 169 ++++++++++---
24 files changed, 1217 insertions(+), 450 deletions(-)
diff --git a/frontend/src/components/forms/User.jsx b/frontend/src/components/forms/User.jsx
index 34a43c30..3ca08e1a 100644
--- a/frontend/src/components/forms/User.jsx
+++ b/frontend/src/components/forms/User.jsx
@@ -23,7 +23,7 @@ import { Copy, Key, RotateCcwKey, X } from 'lucide-react';
import { useForm } from '@mantine/form';
import useChannelsStore from '../../store/channels';
import useOutputProfilesStore from '../../store/outputProfiles';
-import { USER_LEVEL_LABELS, USER_LEVELS, } from '../../constants';
+import { USER_LEVEL_LABELS, USER_LEVELS } from '../../constants';
import useAuthStore from '../../store/auth';
import { copyToClipboard } from '../../utils';
import {
diff --git a/frontend/src/components/forms/UserAgent.jsx b/frontend/src/components/forms/UserAgent.jsx
index b6c237b1..1742e553 100644
--- a/frontend/src/components/forms/UserAgent.jsx
+++ b/frontend/src/components/forms/UserAgent.jsx
@@ -1,8 +1,12 @@
// Modal.js
import React, { useEffect, useMemo } from 'react';
import { useForm } from 'react-hook-form';
-import { Button, Checkbox, Flex, Modal, Space, TextInput, } from '@mantine/core';
-import { addUserAgent, getResolver, updateUserAgent } from '../../utils/forms/UserAgentUtils.js';
+import { Button, Checkbox, Flex, Modal, Space, TextInput } from '@mantine/core';
+import {
+ addUserAgent,
+ getResolver,
+ updateUserAgent,
+} from '../../utils/forms/UserAgentUtils.js';
const UserAgent = ({ userAgent = null, isOpen, onClose }) => {
const defaultValues = useMemo(
diff --git a/frontend/src/components/forms/__tests__/ProgramRecordingModal.test.jsx b/frontend/src/components/forms/__tests__/ProgramRecordingModal.test.jsx
index e47b15fd..f49776a8 100644
--- a/frontend/src/components/forms/__tests__/ProgramRecordingModal.test.jsx
+++ b/frontend/src/components/forms/__tests__/ProgramRecordingModal.test.jsx
@@ -27,7 +27,9 @@ vi.mock('@mantine/core', async () => ({
opened ? (
{title}
-
+
{children}
) : null,
@@ -43,13 +45,18 @@ vi.mock('@mantine/core', async () => ({
),
Anchor: ({ children, onClick }) => (
- {children}
+
+ {children}
+
),
}));
// ── Imports after mocks ────────────────────────────────────────────────────────
import ProgramRecordingModal from '../ProgramRecordingModal.jsx';
-import { deleteRecordingById, deleteSeriesAndRule } from '../../../utils/cards/RecordingCardUtils.js';
+import {
+ deleteRecordingById,
+ deleteSeriesAndRule,
+} from '../../../utils/cards/RecordingCardUtils.js';
import { deleteSeriesRuleByTvgId } from '../../../utils/guideUtils.js';
// ── Helpers ────────────────────────────────────────────────────────────────────
@@ -114,11 +121,7 @@ describe('ProgramRecordingModal', () => {
describe('handleRemoveRecording', () => {
it('calls deleteRecordingById with the recording id', async () => {
const recording = makeRecording();
- render(
-
- );
+ render();
fireEvent.click(screen.getByText('Remove this recording'));
await waitFor(() => {
expect(deleteRecordingById).toHaveBeenCalledWith('rec-1');
@@ -129,9 +132,7 @@ describe('ProgramRecordingModal', () => {
const onClose = vi.fn();
const recording = makeRecording();
render(
-
+
);
fireEvent.click(screen.getByText('Remove this recording'));
await waitFor(() => {
@@ -144,9 +145,7 @@ describe('ProgramRecordingModal', () => {
const onClose = vi.fn();
const recording = makeRecording();
render(
-
+
);
fireEvent.click(screen.getByText('Remove this recording'));
await waitFor(() => {
@@ -202,7 +201,10 @@ describe('ProgramRecordingModal', () => {
);
fireEvent.click(screen.getByText(/Remove series rule/i));
await waitFor(() => {
- expect(deleteSeriesRuleByTvgId).toHaveBeenCalledWith('tvg-3', 'Rule Show');
+ expect(deleteSeriesRuleByTvgId).toHaveBeenCalledWith(
+ 'tvg-3',
+ 'Rule Show'
+ );
});
});
@@ -210,7 +212,10 @@ describe('ProgramRecordingModal', () => {
const onExistingRuleModeChange = vi.fn();
render(
);
fireEvent.click(screen.getByText(/Remove series rule/i));
@@ -245,14 +250,18 @@ describe('ProgramRecordingModal', () => {
it('calls onRecordSeriesAll when "Record All" is clicked', () => {
const onRecordSeriesAll = vi.fn();
- render();
+ render(
+
+ );
fireEvent.click(screen.getByText('Every episode'));
expect(onRecordSeriesAll).toHaveBeenCalled();
});
it('calls onRecordSeriesNew when "Record New" is clicked', () => {
const onRecordSeriesNew = vi.fn();
- render();
+ render(
+
+ );
fireEvent.click(screen.getByText('New episodes only'));
expect(onRecordSeriesNew).toHaveBeenCalled();
});
@@ -264,22 +273,32 @@ describe('ProgramRecordingModal', () => {
it('opens SeriesRuleEditorModal when "Edit Rule" is clicked', () => {
render(
);
fireEvent.click(screen.getByText(/Customize rule/i));
- expect(screen.getByTestId('series-rule-editor-modal')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('series-rule-editor-modal')
+ ).toBeInTheDocument();
});
it('closes SeriesRuleEditorModal when its onClose is called', () => {
render(
);
fireEvent.click(screen.getByText(/Customize rule/i));
fireEvent.click(screen.getByTestId('series-rule-editor-close'));
- expect(screen.queryByTestId('series-rule-editor-modal')).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('series-rule-editor-modal')
+ ).not.toBeInTheDocument();
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/components/forms/__tests__/Recording.test.jsx b/frontend/src/components/forms/__tests__/Recording.test.jsx
index 9c3771bc..ae9da25f 100644
--- a/frontend/src/components/forms/__tests__/Recording.test.jsx
+++ b/frontend/src/components/forms/__tests__/Recording.test.jsx
@@ -55,11 +55,20 @@ vi.mock('@mantine/form', () => ({
return {
values,
key: vi.fn((k) => k),
- getInputProps: vi.fn((k) => ({ name: k, value: values[k] ?? '', onChange: vi.fn() })),
- onSubmit: vi.fn((handler) => (e) => { e?.preventDefault?.(); return handler(values); }),
+ getInputProps: vi.fn((k) => ({
+ name: k,
+ value: values[k] ?? '',
+ onChange: vi.fn(),
+ })),
+ onSubmit: vi.fn((handler) => (e) => {
+ e?.preventDefault?.();
+ return handler(values);
+ }),
reset: vi.fn(),
setValues: vi.fn((newVals) => Object.assign(values, newVals)),
- setFieldValue: vi.fn((k, v) => { values[k] = v; }),
+ setFieldValue: vi.fn((k, v) => {
+ values[k] = v;
+ }),
validateField: vi.fn(),
};
}),
@@ -74,24 +83,37 @@ vi.mock('@mantine/core', () => ({
),
Button: ({ children, onClick, loading, type }) => (
-
),
Badge: ({ children, color }) => (
- {children}
+
+ {children}
+
),
Button: ({ children, onClick, disabled, loading, size, variant, color }) => (
({
),
Card: ({ children, onClick, style }) => (
- {children}
+
+ {children}
+
),
Flex: ({ children }) => {children}
,
Group: ({ children }) => {children}
,
Image: ({ src, alt, fallbackSrc }) => (
),
- Modal: ({ children, opened, onClose, title, size }) =>
+ Modal: ({ children, opened, onClose, title }) =>
opened ? (
{title}
-
×
+
+ ×
+
{children}
) : null,
Stack: ({ children }) => {children}
,
Text: ({ children, size, c, fw, style }) => (
- {children}
+
+ {children}
+
),
Textarea: ({ label, value, onChange, placeholder, ...props }) => (
@@ -207,7 +215,9 @@ const setupMocks = ({ recording = makeRecording(), now = NOW } = {}) => {
vi.mocked(RecordingCardUtils.getPosterUrl).mockReturnValue('/poster.jpg');
vi.mocked(RecordingCardUtils.getChannelLogoUrl).mockReturnValue('/logo.png');
- vi.mocked(RecordingCardUtils.getRecordingUrl).mockReturnValue('/recordings/test.ts');
+ vi.mocked(RecordingCardUtils.getRecordingUrl).mockReturnValue(
+ '/recordings/test.ts'
+ );
vi.mocked(RecordingCardUtils.getSeasonLabel).mockReturnValue('');
vi.mocked(RecordingCardUtils.getShowVideoUrl).mockReturnValue('/live/ch-1');
@@ -234,10 +244,16 @@ describe('RecordingDetailsModal', () => {
beforeEach(() => {
vi.clearAllMocks();
setupMocks();
- vi.mocked(RecordingDetailsModalUtils.updateRecordingMetadata).mockResolvedValue(undefined);
- vi.mocked(RecordingDetailsModalUtils.refreshArtwork).mockResolvedValue(undefined);
+ vi.mocked(
+ RecordingDetailsModalUtils.updateRecordingMetadata
+ ).mockResolvedValue(undefined);
+ vi.mocked(RecordingDetailsModalUtils.refreshArtwork).mockResolvedValue(
+ undefined
+ );
vi.mocked(RecordingCardUtils.runComSkip).mockResolvedValue(undefined);
- vi.mocked(RecordingCardUtils.deleteRecordingById).mockResolvedValue(undefined);
+ vi.mocked(RecordingCardUtils.deleteRecordingById).mockResolvedValue(
+ undefined
+ );
});
// ── Visibility ───────────────────────────────────────────────────────────────
@@ -294,14 +310,19 @@ describe('RecordingDetailsModal', () => {
it('renders the poster image', () => {
render();
- expect(screen.getByAltText('Test Show')).toHaveAttribute('src', '/poster.jpg');
+ expect(screen.getByAltText('Test Show')).toHaveAttribute(
+ 'src',
+ '/poster.jpg'
+ );
});
it('renders the season/episode label when present', () => {
const seriesRecording = makeRecording({ _group_count: 2 });
const mockEpisode = makeRecording({ id: 'ep-1', _group_count: 1 });
- vi.mocked(RecordingDetailsModalUtils.getUpcomingEpisodes).mockReturnValue([mockEpisode]);
+ vi.mocked(RecordingDetailsModalUtils.getUpcomingEpisodes).mockReturnValue(
+ [mockEpisode]
+ );
vi.mocked(RecordingCardUtils.getSeasonLabel).mockReturnValue('S01E02');
render(
@@ -412,7 +433,10 @@ describe('RecordingDetailsModal', () => {
describe('Edit button', () => {
it('renders the Edit button', () => {
- const futureRecording = makeRecording({ start_time: FUTURE, end_time: FUTURE });
+ const futureRecording = makeRecording({
+ start_time: FUTURE,
+ end_time: FUTURE,
+ });
setupMocks({ recording: futureRecording });
render(
{
});
it('calls onEdit when Edit is clicked', () => {
- const futureRecording = makeRecording({ start_time: FUTURE, end_time: FUTURE });
+ const futureRecording = makeRecording({
+ start_time: FUTURE,
+ end_time: FUTURE,
+ });
setupMocks({ recording: futureRecording });
const onEdit = vi.fn();
render(
@@ -445,27 +472,37 @@ describe('RecordingDetailsModal', () => {
it('shows edit inputs when pencil icon is clicked', () => {
render();
fireEvent.click(screen.getByTestId('icon-pencil').closest('button'));
- expect(screen.getByTestId('textinput-Recording title')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('textinput-Recording title')
+ ).toBeInTheDocument();
});
it('pre-fills title input with current title', () => {
render();
fireEvent.click(screen.getByTestId('icon-pencil').closest('button'));
- expect(screen.getByTestId('textinput-Recording title')).toHaveValue('Test Show');
+ expect(screen.getByTestId('textinput-Recording title')).toHaveValue(
+ 'Test Show'
+ );
});
it('pre-fills description input with current description', () => {
render();
fireEvent.click(screen.getByTestId('icon-pencil').closest('button'));
- expect(screen.getByTestId('textarea-Description (optional)')).toHaveValue('A test description');
+ expect(screen.getByTestId('textarea-Description (optional)')).toHaveValue(
+ 'A test description'
+ );
});
it('cancels editing when X icon is clicked', () => {
render();
fireEvent.click(screen.getByTestId('icon-pencil').closest('button'));
- expect(screen.getByTestId('textinput-Recording title')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('textinput-Recording title')
+ ).toBeInTheDocument();
fireEvent.click(screen.getByTestId('icon-x').closest('button'));
- expect(screen.queryByTestId('textinput-Recording title')).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('textinput-Recording title')
+ ).not.toBeInTheDocument();
});
it('calls updateRecordingMetadata with new title and description on save', async () => {
@@ -753,18 +790,17 @@ describe('RecordingDetailsModal', () => {
target: { value: 'Custom' },
});
fireEvent.click(screen.getByTestId('icon-check').closest('button'));
- await waitFor(() => expect(screen.getByText('Custom')).toBeInTheDocument());
+ await waitFor(() =>
+ expect(screen.getByText('Custom')).toBeInTheDocument()
+ );
// Switch to a different recording
const newRecording = makeRecording({ id: 'rec-2' });
setupMocks({ recording: newRecording });
rerender(
-
+
);
expect(screen.getByText(/Test Show/i)).toBeInTheDocument();
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/components/forms/__tests__/RecurringRuleModal.test.jsx b/frontend/src/components/forms/__tests__/RecurringRuleModal.test.jsx
index 6bfc8927..308a415b 100644
--- a/frontend/src/components/forms/__tests__/RecurringRuleModal.test.jsx
+++ b/frontend/src/components/forms/__tests__/RecurringRuleModal.test.jsx
@@ -59,12 +59,10 @@ vi.mock('@mantine/form', async () => {
setValuesState((prev) => ({ ...prev, [k]: v }));
}),
})),
- onSubmit: vi.fn(
- (handler) => (e) => {
- e?.preventDefault?.();
- return handler(values);
- }
- ),
+ onSubmit: vi.fn((handler) => (e) => {
+ e?.preventDefault?.();
+ return handler(values);
+ }),
reset: vi.fn(() => setValuesState({ ...initialValues })),
setValues: vi.fn((newVals) =>
setValuesState((prev) => ({ ...prev, ...newVals }))
@@ -84,7 +82,16 @@ vi.mock('@mantine/core', () => ({
{children}
),
- Button: ({ children, onClick, loading, type, variant, color, size, disabled }) => (
+ Button: ({
+ children,
+ onClick,
+ loading,
+ type,
+ variant,
+ color,
+ size,
+ disabled,
+ }) => (
({
onChange(e.target.value ? new Date(e.target.value) : null)}
+ value={value ? (value.toISOString?.() ?? String(value)) : ''}
+ onChange={(e) =>
+ onChange(e.target.value ? new Date(e.target.value) : null)
+ }
/>
),
@@ -286,7 +295,9 @@ const setupMocks = ({ rule = makeRule(), occurrences = [] } = {}) => {
enabled: rule?.enabled ?? true,
});
- vi.mocked(RecordingUtils.getChannelsSummary).mockResolvedValue([makeChannel()]);
+ vi.mocked(RecordingUtils.getChannelsSummary).mockResolvedValue([
+ makeChannel(),
+ ]);
vi.mocked(RecurringRuleModalUtils.getFormDefaults).mockReturnValue({
channel_id: rule?.channel_id ?? '',
@@ -299,10 +310,18 @@ const setupMocks = ({ rule = makeRule(), occurrences = [] } = {}) => {
enabled: rule?.enabled ?? true,
});
- vi.mocked(RecurringRuleModalUtils.getUpcomingOccurrences).mockReturnValue(occurrences);
- vi.mocked(RecurringRuleModalUtils.updateRecurringRule).mockResolvedValue(undefined);
- vi.mocked(RecurringRuleModalUtils.updateRecurringRuleEnabled).mockResolvedValue(undefined);
- vi.mocked(RecurringRuleModalUtils.deleteRecurringRuleById).mockResolvedValue(undefined);
+ vi.mocked(RecurringRuleModalUtils.getUpcomingOccurrences).mockReturnValue(
+ occurrences
+ );
+ vi.mocked(RecurringRuleModalUtils.updateRecurringRule).mockResolvedValue(
+ undefined
+ );
+ vi.mocked(
+ RecurringRuleModalUtils.updateRecurringRuleEnabled
+ ).mockResolvedValue(undefined);
+ vi.mocked(RecurringRuleModalUtils.deleteRecurringRuleById).mockResolvedValue(
+ undefined
+ );
vi.mocked(deleteRecordingById).mockResolvedValue(undefined);
return { mockFetchRecurringRules };
@@ -339,13 +358,17 @@ describe('RecurringRuleModal', () => {
it('uses rule name as modal title', () => {
render();
- expect(screen.getByTestId('modal-title')).toHaveTextContent('Morning News');
+ expect(screen.getByTestId('modal-title')).toHaveTextContent(
+ 'Morning News'
+ );
});
it('falls back to "Recurring Rule" when rule has no name', () => {
setupMocks({ rule: makeRule({ name: '' }) });
render();
- expect(screen.getByTestId('modal-title')).toHaveTextContent('Recurring Rule');
+ expect(screen.getByTestId('modal-title')).toHaveTextContent(
+ 'Recurring Rule'
+ );
});
it('calls onClose when modal close button is clicked', () => {
@@ -366,16 +389,15 @@ describe('RecurringRuleModal', () => {
it('renders fallback message when rule does not exist', () => {
render();
expect(
- screen.getByText('The recurring rule for this recording no longer exists.')
+ screen.getByText(
+ 'The recurring rule for this recording no longer exists.'
+ )
).toBeInTheDocument();
});
it('shows Delete Recording button when sourceRecording is provided', () => {
render(
-
+
);
expect(screen.getByText('Delete Recording')).toBeInTheDocument();
});
@@ -407,7 +429,10 @@ describe('RecurringRuleModal', () => {
fireEvent.click(screen.getByText('Delete Recording'));
await waitFor(() => {
expect(showNotification).toHaveBeenCalledWith(
- expect.objectContaining({ title: 'Recording deleted', color: 'green' })
+ expect.objectContaining({
+ title: 'Recording deleted',
+ color: 'green',
+ })
);
});
});
@@ -571,10 +596,9 @@ describe('RecurringRuleModal', () => {
render();
fireEvent.submit(screen.getByText('Save changes').closest('form'));
await waitFor(() => {
- expect(RecurringRuleModalUtils.updateRecurringRule).toHaveBeenCalledWith(
- 'rule-1',
- expect.any(Object)
- );
+ expect(
+ RecurringRuleModalUtils.updateRecurringRule
+ ).toHaveBeenCalledWith('rule-1', expect.any(Object));
});
});
@@ -623,12 +647,17 @@ describe('RecurringRuleModal', () => {
it('Save changes button shows loading state while saving', async () => {
let resolve;
vi.mocked(RecurringRuleModalUtils.updateRecurringRule).mockReturnValue(
- new Promise((r) => { resolve = r; })
+ new Promise((r) => {
+ resolve = r;
+ })
);
render();
fireEvent.submit(screen.getByText('Save changes').closest('form'));
await waitFor(() => {
- expect(screen.getByText('Save changes')).toHaveAttribute('data-loading', 'true');
+ expect(screen.getByText('Save changes')).toHaveAttribute(
+ 'data-loading',
+ 'true'
+ );
});
resolve();
});
@@ -646,9 +675,9 @@ describe('RecurringRuleModal', () => {
render();
fireEvent.click(screen.getByText('Delete rule'));
await waitFor(() => {
- expect(RecurringRuleModalUtils.deleteRecurringRuleById).toHaveBeenCalledWith(
- 'rule-1'
- );
+ expect(
+ RecurringRuleModalUtils.deleteRecurringRuleById
+ ).toHaveBeenCalledWith('rule-1');
});
});
@@ -684,9 +713,9 @@ describe('RecurringRuleModal', () => {
});
it('does not show notification when deleteRecurringRuleById throws', async () => {
- vi.mocked(RecurringRuleModalUtils.deleteRecurringRuleById).mockRejectedValue(
- new Error('fail')
- );
+ vi.mocked(
+ RecurringRuleModalUtils.deleteRecurringRuleById
+ ).mockRejectedValue(new Error('fail'));
render();
fireEvent.click(screen.getByText('Delete rule'));
await waitFor(() => {
@@ -805,7 +834,9 @@ describe('RecurringRuleModal', () => {
it('calls getUpcomingOccurrences with recordings, userNow, ruleId, toUserTime', () => {
render();
- expect(RecurringRuleModalUtils.getUpcomingOccurrences).toHaveBeenCalledWith(
+ expect(
+ RecurringRuleModalUtils.getUpcomingOccurrences
+ ).toHaveBeenCalledWith(
expect.any(Array),
expect.any(Function),
'rule-1',
@@ -838,10 +869,7 @@ describe('RecurringRuleModal', () => {
const occ = makeOccurrence();
setupMocks({ occurrences: [occ] });
render(
-
+
);
expect(() => fireEvent.click(screen.getByText('Edit'))).not.toThrow();
});
@@ -888,13 +916,18 @@ describe('RecurringRuleModal', () => {
it('shows loading state on Cancel button while deleting occurrence', async () => {
let resolve;
vi.mocked(deleteRecordingById).mockReturnValue(
- new Promise((r) => { resolve = r; })
+ new Promise((r) => {
+ resolve = r;
+ })
);
setupMocks({ occurrences: [makeOccurrence()] });
render();
fireEvent.click(screen.getByText('Cancel'));
await waitFor(() => {
- expect(screen.getByText('Cancel')).toHaveAttribute('data-loading', 'true');
+ expect(screen.getByText('Cancel')).toHaveAttribute(
+ 'data-loading',
+ 'true'
+ );
});
resolve();
});
@@ -934,4 +967,3 @@ describe('RecurringRuleModal', () => {
});
});
});
-
diff --git a/frontend/src/components/forms/__tests__/ScheduleInput.test.jsx b/frontend/src/components/forms/__tests__/ScheduleInput.test.jsx
index 85dc55d9..1fcd3216 100644
--- a/frontend/src/components/forms/__tests__/ScheduleInput.test.jsx
+++ b/frontend/src/components/forms/__tests__/ScheduleInput.test.jsx
@@ -4,7 +4,15 @@ import ScheduleInput from '../ScheduleInput';
// ── Mantine core ───────────────────────────────────────────────────────────────
vi.mock('@mantine/core', () => ({
- TextInput: ({ label, placeholder, description, value, onChange, error, disabled }) => (
+ TextInput: ({
+ label,
+ placeholder,
+ description,
+ value,
+ onChange,
+ error,
+ disabled,
+ }) => (
({
min={min}
disabled={disabled}
/>
- {description && {description}}
+ {description && (
+ {description}
+ )}
),
Anchor: ({ children, onClick }) => (
-
+
{children}
),
@@ -44,9 +58,13 @@ vi.mock('@mantine/core', () => ({
Group: ({ children }) => {children}
,
Popover: ({ children }) => {children}
,
PopoverTarget: ({ children }) => {children}
,
- PopoverDropdown: ({ children }) => {children}
,
+ PopoverDropdown: ({ children }) => (
+ {children}
+ ),
ActionIcon: ({ children, onClick }) => (
- {children}
+
+ {children}
+
),
}));
@@ -66,7 +84,10 @@ vi.mock('../CronBuilder', () => ({
opened ? (
{currentValue}
-
onApply('0 3 * * *')}>
+ onApply('0 3 * * *')}
+ >
Apply
@@ -122,7 +143,12 @@ describe('ScheduleInput', () => {
});
it('renders custom intervalLabel', () => {
- render();
+ render(
+
+ );
expect(screen.getByText('My Custom Label')).toBeInTheDocument();
});
@@ -144,7 +170,9 @@ describe('ScheduleInput', () => {
it('shows "Use cron schedule" link in interval mode', () => {
render();
- expect(screen.getByTestId('anchor-use-cron-schedule')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('anchor-use-cron-schedule')
+ ).toBeInTheDocument();
});
it('calls onScheduleTypeChange("cron") when "Use cron schedule" is clicked', () => {
@@ -156,7 +184,9 @@ describe('ScheduleInput', () => {
it('does not show cron switch link when disabled', () => {
render();
- expect(screen.queryByTestId('anchor-use-cron-schedule')).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('anchor-use-cron-schedule')
+ ).not.toBeInTheDocument();
});
it('disables the NumberInput when disabled prop is true', () => {
@@ -166,7 +196,10 @@ describe('ScheduleInput', () => {
it('renders custom switchToCronLabel', () => {
render(
-
+
);
expect(screen.getByText('Use custom cron schedule')).toBeInTheDocument();
});
@@ -182,7 +215,9 @@ describe('ScheduleInput', () => {
);
expect(screen.getByTestId('custom-child')).toBeInTheDocument();
- expect(screen.queryByTestId('numberinput-interval')).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('numberinput-interval')
+ ).not.toBeInTheDocument();
});
it('shows cron toggle link below children', () => {
@@ -191,7 +226,9 @@ describe('ScheduleInput', () => {
Child
);
- expect(screen.getByTestId('anchor-use-cron-schedule')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('anchor-use-cron-schedule')
+ ).toBeInTheDocument();
});
it('does not show cron link when disabled with children', () => {
@@ -200,7 +237,9 @@ describe('ScheduleInput', () => {
Child
);
- expect(screen.queryByTestId('anchor-use-cron-schedule')).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('anchor-use-cron-schedule')
+ ).not.toBeInTheDocument();
});
});
@@ -219,12 +258,16 @@ describe('ScheduleInput', () => {
it('shows "Use interval schedule" link in cron mode', () => {
render();
- expect(screen.getByTestId('anchor-use-interval-schedule')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('anchor-use-interval-schedule')
+ ).toBeInTheDocument();
});
it('shows "Open Cron Builder" link', () => {
render();
- expect(screen.getByTestId('anchor-open-cron-builder')).toBeInTheDocument();
+ expect(
+ screen.getByTestId('anchor-open-cron-builder')
+ ).toBeInTheDocument();
});
it('calls onScheduleTypeChange("interval") and onCronChange("") when switching to interval', () => {
@@ -251,13 +294,20 @@ describe('ScheduleInput', () => {
it('does not show cron links when disabled', () => {
render();
- expect(screen.queryByTestId('anchor-use-interval-schedule')).not.toBeInTheDocument();
- expect(screen.queryByTestId('anchor-open-cron-builder')).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('anchor-use-interval-schedule')
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('anchor-open-cron-builder')
+ ).not.toBeInTheDocument();
});
it('renders custom switchToIntervalLabel', () => {
render(
-
+
);
expect(screen.getByText('Use simple schedule')).toBeInTheDocument();
});
@@ -279,7 +329,9 @@ describe('ScheduleInput', () => {
});
render();
await waitFor(() => {
- expect(screen.getByTestId('cron-error')).toHaveTextContent('Invalid cron expression');
+ expect(screen.getByTestId('cron-error')).toHaveTextContent(
+ 'Invalid cron expression'
+ );
});
});
@@ -325,7 +377,9 @@ describe('ScheduleInput', () => {
it('passes current cronValue to CronBuilder', () => {
render();
fireEvent.click(screen.getByTestId('anchor-open-cron-builder'));
- expect(screen.getByTestId('cron-builder-value')).toHaveTextContent('0 4 * * *');
+ expect(screen.getByTestId('cron-builder-value')).toHaveTextContent(
+ '0 4 * * *'
+ );
});
it('closes CronBuilder when onClose is triggered', () => {
@@ -370,4 +424,4 @@ describe('ScheduleInput', () => {
expect(screen.getByTestId('numberinput-interval')).toHaveValue(0);
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/components/forms/__tests__/SeriesRecordingModal.test.jsx b/frontend/src/components/forms/__tests__/SeriesRecordingModal.test.jsx
index 5e7f3c09..a5ca3bb0 100644
--- a/frontend/src/components/forms/__tests__/SeriesRecordingModal.test.jsx
+++ b/frontend/src/components/forms/__tests__/SeriesRecordingModal.test.jsx
@@ -63,9 +63,15 @@ vi.mock('../SeriesRuleEditorModal', () => ({
default: ({ opened, onClose, initialRule, onSaved }) =>
opened ? (
- {initialRule ? initialRule.title : 'new'}
- Close
- Save
+
+ {initialRule ? initialRule.title : 'new'}
+
+
+ Close
+
+
+ Save
+
) : null,
}));
@@ -73,7 +79,10 @@ vi.mock('../SeriesRuleEditorModal', () => ({
// ──────────────────────────────────────────────────────────────────────────────
import useChannelsStore from '../../../store/channels.jsx';
import { deleteSeriesAndRule } from '../../../utils/cards/RecordingCardUtils.js';
-import { evaluateSeriesRulesByTvgId, fetchRules } from '../../../utils/guideUtils.js';
+import {
+ evaluateSeriesRulesByTvgId,
+ fetchRules,
+} from '../../../utils/guideUtils.js';
import { showNotification } from '../../../utils/notificationUtils.js';
const makeRule = (overrides = {}) => ({
@@ -126,7 +135,9 @@ describe('SeriesRecordingModal', () => {
it('renders modal title "Series Recording Rules"', () => {
render();
- expect(screen.getByTestId('modal-title')).toHaveTextContent('Series Recording Rules');
+ expect(screen.getByTestId('modal-title')).toHaveTextContent(
+ 'Series Recording Rules'
+ );
});
it('calls onClose when modal close button is clicked', () => {
@@ -142,17 +153,23 @@ describe('SeriesRecordingModal', () => {
describe('empty state', () => {
it('shows "No series rules configured" when rules is empty', () => {
render();
- expect(screen.getByText('No series rules configured')).toBeInTheDocument();
+ expect(
+ screen.getByText('No series rules configured')
+ ).toBeInTheDocument();
});
it('shows "No series rules configured" when rules is null', () => {
render();
- expect(screen.getByText('No series rules configured')).toBeInTheDocument();
+ expect(
+ screen.getByText('No series rules configured')
+ ).toBeInTheDocument();
});
it('does not show "No series rules configured" when rules are present', () => {
render();
- expect(screen.queryByText('No series rules configured')).not.toBeInTheDocument();
+ expect(
+ screen.queryByText('No series rules configured')
+ ).not.toBeInTheDocument();
});
});
@@ -167,7 +184,9 @@ describe('SeriesRecordingModal', () => {
it('falls back to tvg_id when rule has no title', () => {
render(
);
expect(screen.getByText('tvg-fallback')).toBeInTheDocument();
@@ -214,19 +233,29 @@ describe('SeriesRecordingModal', () => {
describe('rule summary', () => {
it('shows "New episodes" for mode new', () => {
- render();
+ render(
+
+ );
expect(screen.getByText(/New episodes/)).toBeInTheDocument();
});
it('shows "Every episode" for mode all', () => {
- render();
+ render(
+
+ );
expect(screen.getByText(/Every episode/)).toBeInTheDocument();
});
it('shows exact title label in summary', () => {
render(
);
expect(screen.getByText(/Exact title: "Test Show"/)).toBeInTheDocument();
@@ -235,7 +264,9 @@ describe('SeriesRecordingModal', () => {
it('shows contains label in summary', () => {
render(
);
expect(screen.getByText(/Title contains: "Test"/)).toBeInTheDocument();
@@ -244,7 +275,9 @@ describe('SeriesRecordingModal', () => {
it('shows regex label in summary', () => {
render(
);
expect(screen.getByText(/Title regex: "\^Test"/)).toBeInTheDocument();
@@ -289,7 +322,10 @@ describe('SeriesRecordingModal', () => {
fireEvent.click(screen.getByText('Evaluate Now'));
await waitFor(() => {
expect(showNotification).toHaveBeenCalledWith(
- expect.objectContaining({ title: 'Evaluated', message: 'Checked for episodes' })
+ expect.objectContaining({
+ title: 'Evaluated',
+ message: 'Checked for episodes',
+ })
);
});
});
@@ -377,7 +413,9 @@ describe('SeriesRecordingModal', () => {
render();
fireEvent.click(screen.getByText('Add rule'));
fireEvent.click(screen.getByTestId('editor-close'));
- expect(screen.queryByTestId('series-rule-editor')).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('series-rule-editor')
+ ).not.toBeInTheDocument();
});
});
@@ -414,4 +452,4 @@ describe('SeriesRecordingModal', () => {
});
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/components/forms/__tests__/SeriesRuleEditorModal.test.jsx b/frontend/src/components/forms/__tests__/SeriesRuleEditorModal.test.jsx
index 59a98bd9..2fffd170 100644
--- a/frontend/src/components/forms/__tests__/SeriesRuleEditorModal.test.jsx
+++ b/frontend/src/components/forms/__tests__/SeriesRuleEditorModal.test.jsx
@@ -90,14 +90,7 @@ vi.mock('@mantine/core', () => ({
))}
),
- Select: ({
- label,
- placeholder,
- data,
- value,
- onChange,
- description,
- }) => (
+ Select: ({ label, placeholder, data, value, onChange, description }) => (
{description && (
@@ -156,7 +149,10 @@ import useEPGsStore from '../../../store/epgs.jsx';
import { useDebounce } from '../../../utils.js';
import { showNotification } from '../../../utils/notificationUtils.js';
import { getChannelsSummary } from '../../../utils/forms/RecordingUtils.js';
-import { createSeriesRule, evaluateSeriesRulesByTvgId } from '../../../utils/guideUtils.js';
+import {
+ createSeriesRule,
+ evaluateSeriesRulesByTvgId,
+} from '../../../utils/guideUtils.js';
import {
getTvgOptions,
previewSeriesRule,
@@ -216,7 +212,11 @@ const setupMocks = () => {
vi.mocked(getTvgOptions).mockReturnValue(mockTvgOptions);
vi.mocked(getChannelsSummary).mockResolvedValue(mockChannels);
- vi.mocked(previewSeriesRule).mockResolvedValue({ matches: [], total: 0, epg_found: true });
+ vi.mocked(previewSeriesRule).mockResolvedValue({
+ matches: [],
+ total: 0,
+ epg_found: true,
+ });
vi.mocked(createSeriesRule).mockResolvedValue({ id: 'rule-1' });
vi.mocked(evaluateSeriesRulesByTvgId).mockResolvedValue(undefined);
vi.mocked(useDebounce).mockImplementation((val) => val);
@@ -260,7 +260,9 @@ describe('SeriesRuleEditorModal', () => {
it('shows "New Series Rule" title when initialRule is null', () => {
render();
- expect(screen.getByTestId('modal-title')).toHaveTextContent('New Series Rule');
+ expect(screen.getByTestId('modal-title')).toHaveTextContent(
+ 'New Series Rule'
+ );
});
it('shows "Edit Series Rule" title when initialRule is provided', () => {
@@ -270,7 +272,9 @@ describe('SeriesRuleEditorModal', () => {
initialRule={{ tvg_id: 'tvg-1', mode: 'all', title: 'Test' }}
/>
);
- expect(screen.getByTestId('modal-title')).toHaveTextContent('Edit Series Rule');
+ expect(screen.getByTestId('modal-title')).toHaveTextContent(
+ 'Edit Series Rule'
+ );
});
it('renders the title input', () => {
@@ -325,21 +329,29 @@ describe('SeriesRuleEditorModal', () => {
};
it('pre-fills title from initialRule', () => {
- render();
+ render(
+
+ );
expect(screen.getByTestId('input-title')).toHaveValue('Pre-filled Title');
});
it('pre-fills description from initialRule', () => {
- render();
- expect(screen.getByTestId('textarea-description')).toHaveValue('Pre-filled description');
+ render(
+
+ );
+ expect(screen.getByTestId('textarea-description')).toHaveValue(
+ 'Pre-filled description'
+ );
});
it('uses default mode "all" when initialRule has no mode', () => {
- render();
- // "All" segmented button should be active (data-active=true)
- const allBtn = screen.getAllByText('All').find(
- (el) => el.closest('[data-testid="segmented-control"]')
+ render(
+
);
+ // "All" segmented button should be active (data-active=true)
+ const allBtn = screen
+ .getAllByText('All')
+ .find((el) => el.closest('[data-testid="segmented-control"]'));
expect(allBtn).toBeTruthy();
});
@@ -348,10 +360,18 @@ describe('SeriesRuleEditorModal', () => {
);
rerender(
-
+
);
rerender(
-
+
);
expect(screen.getByTestId('input-title')).toHaveValue('');
expect(screen.getByTestId('textarea-description')).toHaveValue('');
@@ -374,7 +394,9 @@ describe('SeriesRuleEditorModal', () => {
});
it('handles getChannelsSummary rejection gracefully', async () => {
- vi.mocked(getChannelsSummary).mockRejectedValue(new Error('Network error'));
+ vi.mocked(getChannelsSummary).mockRejectedValue(
+ new Error('Network error')
+ );
render();
// Should not throw; channel options just empty
await waitFor(() => {
@@ -439,7 +461,11 @@ describe('SeriesRuleEditorModal', () => {
});
it('renders preview badge with match count', async () => {
- vi.mocked(previewSeriesRule).mockResolvedValue({ matches: [{ id: 'p1', title: 'X' }], total: 5, epg_found: true });
+ vi.mocked(previewSeriesRule).mockResolvedValue({
+ matches: [{ id: 'p1', title: 'X' }],
+ total: 5,
+ epg_found: true,
+ });
render();
fireEvent.change(screen.getByTestId('input-title'), {
target: { value: 'X' },
@@ -450,7 +476,9 @@ describe('SeriesRuleEditorModal', () => {
});
it('shows preview error alert when previewSeriesRule rejects', async () => {
- vi.mocked(previewSeriesRule).mockRejectedValue(new Error('Preview failed'));
+ vi.mocked(previewSeriesRule).mockRejectedValue(
+ new Error('Preview failed')
+ );
render();
fireEvent.change(screen.getByTestId('input-title'), {
target: { value: 'Boom' },
@@ -467,9 +495,16 @@ describe('SeriesRuleEditorModal', () => {
total: 0,
epg_found: false,
});
- render();
+ render(
+
+ );
await waitFor(() => {
- expect(screen.getByText(/No EPG channel matches this tvg_id/)).toBeInTheDocument();
+ expect(
+ screen.getByText(/No EPG channel matches this tvg_id/)
+ ).toBeInTheDocument();
});
});
@@ -485,7 +520,9 @@ describe('SeriesRuleEditorModal', () => {
target: { value: 'X' },
});
await waitFor(() => {
- expect(screen.getByText(/This rule matches many programs/)).toBeInTheDocument();
+ expect(
+ screen.getByText(/This rule matches many programs/)
+ ).toBeInTheDocument();
});
});
@@ -603,7 +640,9 @@ describe('SeriesRuleEditorModal', () => {
});
it('does not crash when evaluateSeriesRulesByTvgId throws', async () => {
- vi.mocked(evaluateSeriesRulesByTvgId).mockRejectedValue(new Error('eval fail'));
+ vi.mocked(evaluateSeriesRulesByTvgId).mockRejectedValue(
+ new Error('eval fail')
+ );
renderWithTitle();
await expect(
waitFor(() => {
@@ -626,7 +665,12 @@ describe('SeriesRuleEditorModal', () => {
it('includes channel_id in payload when channelId is set', async () => {
vi.mocked(getChannelsSummary).mockResolvedValue(mockChannels);
- render();
+ render(
+
+ );
fireEvent.click(screen.getByText('Save rule'));
await waitFor(() => {
expect(createSeriesRule).toHaveBeenCalledWith(
@@ -660,7 +704,11 @@ describe('SeriesRuleEditorModal', () => {
describe('SegmentedControl', () => {
it('changes title mode when SegmentedControl is clicked', () => {
render();
- fireEvent.click(screen.getAllByText('Contains').find((el) => el.closest('[data-testid="segmented-control"]')));
+ fireEvent.click(
+ screen
+ .getAllByText('Contains')
+ .find((el) => el.closest('[data-testid="segmented-control"]'))
+ );
// No error thrown; state updated without crash
});
@@ -679,4 +727,4 @@ describe('SeriesRuleEditorModal', () => {
});
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/components/forms/__tests__/Stream.test.jsx b/frontend/src/components/forms/__tests__/Stream.test.jsx
index 18fe3b75..622fd46d 100644
--- a/frontend/src/components/forms/__tests__/Stream.test.jsx
+++ b/frontend/src/components/forms/__tests__/Stream.test.jsx
@@ -257,7 +257,9 @@ describe('Stream', () => {
describe('default values', () => {
it('pre-fills name from stream prop', () => {
render();
- expect(screen.getByTestId('input-stream-name')).toHaveValue('Test Stream');
+ expect(screen.getByTestId('input-stream-name')).toHaveValue(
+ 'Test Stream'
+ );
});
it('pre-fills url from stream prop', () => {
@@ -289,7 +291,9 @@ describe('Stream', () => {
it('renders empty group when stream has no channel_group', () => {
render(
-
+
);
expect(screen.getByTestId('select-group')).toHaveValue('');
});
@@ -317,7 +321,9 @@ describe('Stream', () => {
target: { value: 'http://new.stream/live' },
});
- fireEvent.submit(screen.getByRole('button', { name: 'Submit' }).closest('form'));
+ fireEvent.submit(
+ screen.getByRole('button', { name: 'Submit' }).closest('form')
+ );
await waitFor(() => {
expect(StreamUtils.addStream).toHaveBeenCalledWith(
diff --git a/frontend/src/components/forms/__tests__/StreamProfile.test.jsx b/frontend/src/components/forms/__tests__/StreamProfile.test.jsx
index 50237950..922a0281 100644
--- a/frontend/src/components/forms/__tests__/StreamProfile.test.jsx
+++ b/frontend/src/components/forms/__tests__/StreamProfile.test.jsx
@@ -25,7 +25,8 @@ vi.mock('../../../utils/forms/StreamProfileUtils.js', () => ({
],
COMMAND_EXAMPLES: {
ffmpeg: '-user_agent {userAgent} -i {streamUrl} -c copy -f mpegts pipe:1',
- streamlink: '{streamUrl} --http-header User-Agent={userAgent} best --stdout',
+ streamlink:
+ '{streamUrl} --http-header User-Agent={userAgent} best --stdout',
},
}));
@@ -99,7 +100,9 @@ vi.mock('@mantine/core', () => ({
data-testid="checkbox-is-active"
type="checkbox"
checked={checked ?? false}
- onChange={(e) => onChange({ currentTarget: { checked: e.target.checked } })}
+ onChange={(e) =>
+ onChange({ currentTarget: { checked: e.target.checked } })
+ }
/>
),
@@ -114,7 +117,17 @@ vi.mock('@mantine/core', () => ({
{children}
) : null,
- Select: ({ label, value, onChange, data, placeholder, error, clearable, disabled, description }) => (
+ Select: ({
+ label,
+ value,
+ onChange,
+ data,
+ placeholder,
+ error,
+ clearable,
+ disabled,
+ description,
+ }) => (
{description &&
{description}
}
@@ -135,10 +148,24 @@ vi.mock('@mantine/core', () => ({
),
Stack: ({ children }) => {children}
,
- Textarea: ({ label, name, value, onChange, error, placeholder, description, disabled, ...rest }) => (
+ Textarea: ({
+ label,
+ name,
+ value,
+ onChange,
+ error,
+ placeholder,
+ description,
+ disabled,
+ ...rest
+ }) => (
- {description &&
{description}
}
+ {description && (
+
+ {description}
+
+ )}
),
- NumberInput: ({ label }) => ,
+ NumberInput: ({ label }) => (
+
+
+
+
+ ),
PasswordInput: ({ label, disabled }) => (
-
+
+
+
+
),
Select: ({ label, disabled }) => (
-
+
+
+
+
),
Stack: ({ children }) => {children}
,
- Switch: ({ label }) => ,
+ Switch: ({ label }) => (
+
+
+
+
+ ),
Tabs: ({ children }) => {children}
,
TabsList: ({ children }) => {children}
,
- TabsPanel: ({ children, value }) => {children}
,
- TabsTab: ({ children, value }) => {children},
- TagsInput: ({ label }) => ,
+ TabsPanel: ({ children, value }) => (
+ {children}
+ ),
+ TabsTab: ({ children, value }) => (
+ {children}
+ ),
+ TagsInput: ({ label }) => (
+
+
+
+
+ ),
Text: ({ children }) => {children},
TextInput: ({ label, value, disabled, rightSection }) => (
-
+
{rightSection}
),
@@ -155,9 +191,10 @@ const setupMocks = ({
// Reset form state
mockForm.getValues.mockReturnValue({ user_level: '3' });
- mockForm.onSubmit.mockImplementation(
- (fn) => (e) => { e?.preventDefault?.(); return fn(); }
- );
+ mockForm.onSubmit.mockImplementation((fn) => (e) => {
+ e?.preventDefault?.();
+ return fn();
+ });
return { mockSetUser };
};
@@ -169,7 +206,9 @@ describe('User', () => {
vi.clearAllMocks();
vi.mocked(UserUtils.createUser).mockResolvedValue({});
vi.mocked(UserUtils.updateUser).mockResolvedValue({});
- vi.mocked(UserUtils.generateApiKey).mockResolvedValue({ key: 'new-api-key' });
+ vi.mocked(UserUtils.generateApiKey).mockResolvedValue({
+ key: 'new-api-key',
+ });
vi.mocked(UserUtils.revokeApiKey).mockResolvedValue({ success: true });
vi.mocked(UserUtils.formValuesToPayload).mockReturnValue({ user_level: 3 });
vi.mocked(UserUtils.getFormInitialValues).mockReturnValue({});
@@ -214,9 +253,7 @@ describe('User', () => {
it('shows Permissions tab when admin edits another user', () => {
setupMocks({ authUser: makeAdminUser() });
- render(
-
- );
+ render();
expect(screen.getByTestId('tab-permissions')).toBeInTheDocument();
});
@@ -230,7 +267,11 @@ describe('User', () => {
it('hides Permissions tab for non-admin user', () => {
setupMocks({ authUser: makeRegularUser() });
render(
-
+
);
expect(screen.queryByTestId('tab-permissions')).not.toBeInTheDocument();
});
@@ -242,7 +283,11 @@ describe('User', () => {
it('shows Output Format Override for admin', () => {
setupMocks({ authUser: makeAdminUser() });
render(
-
+
);
expect(screen.getByText('Output Format Override')).toBeInTheDocument();
});
@@ -250,15 +295,25 @@ describe('User', () => {
it('hides Output Format Override for non-admin', () => {
setupMocks({ authUser: makeRegularUser() });
render(
-
+
);
- expect(screen.queryByText('Output Format Override')).not.toBeInTheDocument();
+ expect(
+ screen.queryByText('Output Format Override')
+ ).not.toBeInTheDocument();
});
it('shows Output Profile Override for admin', () => {
setupMocks({ authUser: makeAdminUser() });
render(
-
+
);
expect(screen.getByText('Output Profile Override')).toBeInTheDocument();
});
@@ -266,7 +321,11 @@ describe('User', () => {
it('shows Allowed IPs for admin', () => {
setupMocks({ authUser: makeAdminUser() });
render(
-
+
);
expect(screen.getByText('Allowed IPs')).toBeInTheDocument();
});
@@ -274,7 +333,11 @@ describe('User', () => {
it('hides Allowed IPs for non-admin', () => {
setupMocks({ authUser: makeRegularUser() });
render(
-
+
);
expect(screen.queryByText('Allowed IPs')).not.toBeInTheDocument();
});
@@ -286,7 +349,11 @@ describe('User', () => {
it('shows "Generate API Key" when no key exists', () => {
setupMocks({ authUser: makeAdminUser() });
render(
-
+
);
expect(screen.getByText('Generate API Key')).toBeInTheDocument();
});
@@ -305,10 +372,16 @@ describe('User', () => {
it('calls generateApiKey and switches button to "Regenerate API Key"', async () => {
setupMocks({ authUser: makeAdminUser() });
- vi.mocked(UserUtils.generateApiKey).mockResolvedValue({ key: 'brand-new-key' });
+ vi.mocked(UserUtils.generateApiKey).mockResolvedValue({
+ key: 'brand-new-key',
+ });
render(
-
+
);
expect(screen.getByText('Generate API Key')).toBeInTheDocument();
@@ -322,10 +395,16 @@ describe('User', () => {
it('also sets the key when response contains raw_key instead of key', async () => {
setupMocks({ authUser: makeAdminUser() });
- vi.mocked(UserUtils.generateApiKey).mockResolvedValue({ raw_key: 'raw-value' });
+ vi.mocked(UserUtils.generateApiKey).mockResolvedValue({
+ raw_key: 'raw-value',
+ });
render(
-
+
);
fireEvent.click(screen.getByText('Generate API Key'));
@@ -339,7 +418,11 @@ describe('User', () => {
// non-admin editing someone else
setupMocks({ authUser: makeRegularUser({ id: 5 }) });
render(
-
+
);
// No button visible since canGenerateKey is false
expect(screen.queryByText('Generate API Key')).not.toBeInTheDocument();
@@ -351,7 +434,11 @@ describe('User', () => {
vi.mocked(UserUtils.generateApiKey).mockResolvedValue({});
render(
-
+
);
fireEvent.click(screen.getByText('Generate API Key'));
@@ -367,7 +454,11 @@ describe('User', () => {
vi.mocked(UserUtils.generateApiKey).mockResolvedValue({ key: 'show-me' });
render(
-
+
);
fireEvent.click(screen.getByText('Generate API Key'));
@@ -397,7 +488,11 @@ describe('User', () => {
it('hides "Revoke API Key" when no key exists', () => {
setupMocks({ authUser: makeAdminUser() });
render(
-
+
);
expect(screen.queryByText('Revoke API Key')).not.toBeInTheDocument();
});
@@ -455,7 +550,7 @@ describe('User', () => {
});
});
- it('does not update auth store when revoking another user\'s key', async () => {
+ it("does not update auth store when revoking another user's key", async () => {
const admin = makeAdminUser({ id: 1 });
const { mockSetUser } = setupMocks({ authUser: admin });
@@ -518,7 +613,11 @@ describe('User', () => {
vi.mocked(UserUtils.updateUser).mockResolvedValue({ id: 2 });
render(
-
+
);
fireEvent.click(screen.getByText('Save'));
@@ -569,7 +668,11 @@ describe('User', () => {
vi.mocked(UserUtils.updateUser).mockResolvedValue({ id: 2 });
render(
-
+
);
fireEvent.click(screen.getByText('Save'));
@@ -585,7 +688,9 @@ describe('User', () => {
describe('useEffect on user prop', () => {
it('calls userToFormValues and sets form values when user has an id', () => {
const user = makeRegularUser({ id: 2 });
- vi.mocked(UserUtils.userToFormValues).mockReturnValue({ username: 'user1' });
+ vi.mocked(UserUtils.userToFormValues).mockReturnValue({
+ username: 'user1',
+ });
setupMocks({ authUser: makeAdminUser() });
render();
@@ -607,7 +712,11 @@ describe('User', () => {
it('calls setValues with a generated xc_password when rotate icon is clicked', () => {
setupMocks({ authUser: makeAdminUser() });
render(
-
+
);
const rotateButton = screen
@@ -623,7 +732,11 @@ describe('User', () => {
it('disables the XC password rotate button for non-admin', () => {
setupMocks({ authUser: makeRegularUser() });
render(
-
+
);
const rotateButton = screen
@@ -641,7 +754,11 @@ describe('User', () => {
it('excludes "All" (0) when other profiles are selected alongside it', () => {
setupMocks({ authUser: makeAdminUser(), profiles });
render(
-
+
);
const multiSelect = screen.getByTestId('multiselect-Channel Profiles');
@@ -656,31 +773,38 @@ describe('User', () => {
it('sets only ["0"] when "All" is newly selected', () => {
setupMocks({ authUser: makeAdminUser(), profiles });
render(
-
+
);
const multiSelect = screen.getByTestId('multiselect-Channel Profiles');
fireEvent.change(multiSelect, { target: { value: '0' } });
- expect(mockForm.setFieldValue).toHaveBeenCalledWith(
- 'channel_profiles',
- ['0']
- );
+ expect(mockForm.setFieldValue).toHaveBeenCalledWith('channel_profiles', [
+ '0',
+ ]);
});
it('allows multiple non-all profiles together', () => {
setupMocks({ authUser: makeAdminUser(), profiles });
render(
-
+
);
const multiSelect = screen.getByTestId('multiselect-Channel Profiles');
fireEvent.change(multiSelect, { target: { value: '1,2' } });
- expect(mockForm.setFieldValue).toHaveBeenCalledWith(
- 'channel_profiles',
- ['1', '2']
- );
+ expect(mockForm.setFieldValue).toHaveBeenCalledWith('channel_profiles', [
+ '1',
+ '2',
+ ]);
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/components/forms/__tests__/UserAgent.test.jsx b/frontend/src/components/forms/__tests__/UserAgent.test.jsx
index 2292b8cd..a7c50390 100644
--- a/frontend/src/components/forms/__tests__/UserAgent.test.jsx
+++ b/frontend/src/components/forms/__tests__/UserAgent.test.jsx
@@ -31,7 +31,9 @@ vi.mock('@mantine/core', () => ({
opened ? (
{title}
-
×
+
+ ×
+
{children}
) : null,
@@ -105,23 +107,37 @@ describe('UserAgent', () => {
it('pre-fills fields from userAgent prop', () => {
render(
-
+
);
expect(screen.getByTestId('input-Name')).toHaveValue('Chrome');
expect(screen.getByTestId('input-User-Agent')).toHaveValue('Mozilla/5.0');
- expect(screen.getByTestId('input-Description')).toHaveValue('Chrome browser');
+ expect(screen.getByTestId('input-Description')).toHaveValue(
+ 'Chrome browser'
+ );
});
it('checks Is Active checkbox when userAgent.is_active is true', () => {
render(
-
+
);
expect(screen.getByTestId('checkbox-is-active')).toBeChecked();
});
it('unchecks Is Active checkbox when userAgent.is_active is false', () => {
render(
-
+
);
expect(screen.getByTestId('checkbox-is-active')).not.toBeChecked();
});
@@ -137,7 +153,11 @@ describe('UserAgent', () => {
describe('field interactions', () => {
it('toggles Is Active checkbox', () => {
render(
-
+
);
const checkbox = screen.getByTestId('checkbox-is-active');
fireEvent.click(checkbox);
@@ -158,7 +178,11 @@ describe('UserAgent', () => {
it('calls updateUserAgent with the id when editing an existing userAgent', async () => {
render(
-
+
);
fireEvent.click(screen.getByText('Submit'));
await waitFor(() => {
@@ -171,7 +195,11 @@ describe('UserAgent', () => {
it('does not call addUserAgent when updating', async () => {
render(
-
+
);
fireEvent.click(screen.getByText('Submit'));
await waitFor(() => {
@@ -214,7 +242,11 @@ describe('UserAgent', () => {
describe('form reset on userAgent change', () => {
it('resets form values when userAgent prop changes', () => {
const { rerender } = render(
-
+
);
expect(screen.getByTestId('input-Name')).toHaveValue('Chrome');
@@ -222,7 +254,11 @@ describe('UserAgent', () => {
);
expect(screen.getByTestId('input-Name')).toHaveValue('Firefox');
@@ -230,10 +266,14 @@ describe('UserAgent', () => {
it('resets to empty when userAgent prop is removed', () => {
const { rerender } = render(
-
+
);
rerender();
expect(screen.getByTestId('input-Name')).toHaveValue('');
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/components/forms/__tests__/VODCategoryFilter.test.jsx b/frontend/src/components/forms/__tests__/VODCategoryFilter.test.jsx
index de87d5c0..31cd5e2b 100644
--- a/frontend/src/components/forms/__tests__/VODCategoryFilter.test.jsx
+++ b/frontend/src/components/forms/__tests__/VODCategoryFilter.test.jsx
@@ -7,14 +7,19 @@ vi.mock('../../../store/useVODStore', () => ({ default: vi.fn() }));
// ── lucide-react ───────────────────────────────────────────────────────────────
vi.mock('lucide-react', () => ({
CircleCheck: () => ,
- CircleX: () => ,
+ CircleX: () => ,
}));
// ── Mantine core ───────────────────────────────────────────────────────────────
vi.mock('@mantine/core', () => ({
- Box: ({ children }) => {children}
,
- Button: ({ children, onClick, disabled, color, variant }) => (
-
+ Box: ({ children }) => {children}
,
+ Button: ({ children, onClick, disabled, color, variant }) => (
+
{children}
),
@@ -24,17 +29,19 @@ vi.mock('@mantine/core', () => ({
type="checkbox"
aria-label={label}
checked={checked}
- onChange={(e) => onChange?.({ currentTarget: { checked: e.target.checked } })}
+ onChange={(e) =>
+ onChange?.({ currentTarget: { checked: e.target.checked } })
+ }
disabled={disabled}
/>
{label}
),
- Flex: ({ children }) => {children}
,
- Group: ({ children }) => {children}
,
+ Flex: ({ children }) => {children}
,
+ Group: ({ children }) => {children}
,
SimpleGrid: ({ children }) => {children}
,
- Stack: ({ children }) => {children}
,
- Text: ({ children }) => {children},
+ Stack: ({ children }) => {children}
,
+ Text: ({ children }) => {children},
TextInput: ({ label, value, onChange, placeholder }) => (
[
m3u_accounts: [{ m3u_account: 10, enabled: true }],
category_type: 'movies',
},
- { id: 2, name: 'Comedy', m3u_accounts: [{ m3u_account: 10, enabled: false }], category_type: 'movies' },
- { id: 3, name: 'Drama', m3u_accounts: [{ m3u_account: 10, enabled: true }], category_type: 'movies' },
- { id: 4, name: 'News', m3u_accounts: [{ m3u_account: 10, enabled: true }], category_type: 'series' },
+ {
+ id: 2,
+ name: 'Comedy',
+ m3u_accounts: [{ m3u_account: 10, enabled: false }],
+ category_type: 'movies',
+ },
+ {
+ id: 3,
+ name: 'Drama',
+ m3u_accounts: [{ m3u_account: 10, enabled: true }],
+ category_type: 'movies',
+ },
+ {
+ id: 4,
+ name: 'News',
+ m3u_accounts: [{ m3u_account: 10, enabled: true }],
+ category_type: 'series',
+ },
];
-const makePlaylist = (overrides = {}) => ({ id: 10, name: 'My Playlist', ...overrides });
+const makePlaylist = (overrides = {}) => ({
+ id: 10,
+ name: 'My Playlist',
+ ...overrides,
+});
const categoriesToDict = (arr) =>
arr.reduce((acc, cat) => ({ ...acc, [cat.id]: cat }), {});
const setupMocks = ({ categories = makeCategories() } = {}) => {
- const dict = Array.isArray(categories) ? categoriesToDict(categories) : categories;
+ const dict = Array.isArray(categories)
+ ? categoriesToDict(categories)
+ : categories;
vi.mocked(useVODStore).mockImplementation((sel) => sel({ categories: dict }));
};
@@ -120,13 +148,19 @@ describe('VODCategoryFilter', () => {
it('renders a button for each category matching the type and playlist', () => {
render();
- expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Comedy' })).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Action' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Comedy' })
+ ).toBeInTheDocument();
});
it('does not render categories of a different type', () => {
render();
- expect(screen.queryByRole('checkbox', { name: 'News' })).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('checkbox', { name: 'News' })
+ ).not.toBeInTheDocument();
});
it('renders the text filter input', () => {
@@ -147,7 +181,9 @@ describe('VODCategoryFilter', () => {
it('renders the Auto-enable new groups checkbox', () => {
render();
- expect(screen.getByLabelText(/automatically enable new/i)).toBeInTheDocument();
+ expect(
+ screen.getByLabelText(/automatically enable new/i)
+ ).toBeInTheDocument();
});
});
@@ -158,15 +194,25 @@ describe('VODCategoryFilter', () => {
render();
const input = screen.getByPlaceholderText(/filter/i);
fireEvent.change(input, { target: { value: 'act' } });
- expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: 'Comedy' })).not.toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Action' })
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Comedy' })
+ ).not.toBeInTheDocument();
});
it('is case-insensitive', () => {
render();
- fireEvent.change(screen.getByPlaceholderText(/filter/i), { target: { value: 'COMEDY' } });
- expect(screen.getByRole('button', { name: 'Comedy' })).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: 'Action' })).not.toBeInTheDocument();
+ fireEvent.change(screen.getByPlaceholderText(/filter/i), {
+ target: { value: 'COMEDY' },
+ });
+ expect(
+ screen.getByRole('button', { name: 'Comedy' })
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Action' })
+ ).not.toBeInTheDocument();
});
it('shows all categories when filter is cleared', () => {
@@ -174,8 +220,12 @@ describe('VODCategoryFilter', () => {
const input = screen.getByPlaceholderText(/filter/i);
fireEvent.change(input, { target: { value: 'act' } });
fireEvent.change(input, { target: { value: '' } });
- expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Comedy' })).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Action' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Comedy' })
+ ).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Drama' })).toBeInTheDocument();
});
@@ -184,7 +234,9 @@ describe('VODCategoryFilter', () => {
fireEvent.change(screen.getByPlaceholderText(/filter/i), {
target: { value: 'zzznomatch' },
});
- expect(screen.queryByRole('button', { name: 'Action' })).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Action' })
+ ).not.toBeInTheDocument();
});
});
@@ -193,34 +245,58 @@ describe('VODCategoryFilter', () => {
describe('status filter', () => {
it('shows only enabled categories when "Enabled" segment is selected', () => {
const props = defaultProps({
- categoryStates: [ { id: 1, name: 'Action', enabled: true }, { id: 2, name: 'Comedy', enabled: false }, { id: 3, name: 'Drama', enabled: true } ],
+ categoryStates: [
+ { id: 1, name: 'Action', enabled: true },
+ { id: 2, name: 'Comedy', enabled: false },
+ { id: 3, name: 'Drama', enabled: true },
+ ],
});
render();
fireEvent.click(screen.getByTestId('segment-enabled'));
- expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: 'Comedy' })).not.toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Action' })
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Comedy' })
+ ).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Drama' })).toBeInTheDocument();
});
it('shows only disabled categories when "Disabled" segment is selected', () => {
const props = defaultProps({
- categoryStates: [ { id: 1, name: 'Action', enabled: true }, { id: 2, name: 'Comedy', enabled: false }, { id: 3, name: 'Drama', enabled: true } ],
+ categoryStates: [
+ { id: 1, name: 'Action', enabled: true },
+ { id: 2, name: 'Comedy', enabled: false },
+ { id: 3, name: 'Drama', enabled: true },
+ ],
});
render();
fireEvent.click(screen.getByTestId('segment-disabled'));
- expect(screen.queryByRole('button', { name: 'Action' })).not.toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Comedy' })).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Action' })
+ ).not.toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Comedy' })
+ ).toBeInTheDocument();
});
it('shows all categories when "All" segment is active', () => {
const props = defaultProps({
- categoryStates: [ { id: 1, name: 'Action', enabled: true }, { id: 2, name: 'Comedy', enabled: false }, { id: 3, name: 'Drama', enabled: true } ],
+ categoryStates: [
+ { id: 1, name: 'Action', enabled: true },
+ { id: 2, name: 'Comedy', enabled: false },
+ { id: 3, name: 'Drama', enabled: true },
+ ],
});
render();
fireEvent.click(screen.getByTestId('segment-disabled'));
fireEvent.click(screen.getByTestId('segment-all'));
- expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Comedy' })).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Action' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Comedy' })
+ ).toBeInTheDocument();
});
});
@@ -230,11 +306,17 @@ describe('VODCategoryFilter', () => {
it('applies both text and status filters simultaneously', () => {
const props = defaultProps();
render();
- fireEvent.change(screen.getByPlaceholderText(/filter/i), { target: { value: 'o' } });
+ fireEvent.change(screen.getByPlaceholderText(/filter/i), {
+ target: { value: 'o' },
+ });
fireEvent.click(screen.getByTestId('segment-disabled'));
// "Comedy" matches "o" AND is disabled; "Action" matches "o" but is enabled
- expect(screen.getByRole('button', { name: 'Comedy' })).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: 'Action' })).not.toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Comedy' })
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Action' })
+ ).not.toBeInTheDocument();
});
});
@@ -247,7 +329,11 @@ describe('VODCategoryFilter', () => {
);
@@ -264,11 +350,17 @@ describe('VODCategoryFilter', () => {
);
- fireEvent.change(screen.getByPlaceholderText(/filter/i), { target: { value: 'act' } });
+ fireEvent.change(screen.getByPlaceholderText(/filter/i), {
+ target: { value: 'act' },
+ });
fireEvent.click(screen.getByText('Select Visible'));
const called = setCategoryStates.mock.calls.at(-1)[0];
expect(called.find((s) => s.id === 1).enabled).toBe(true);
@@ -295,11 +387,17 @@ describe('VODCategoryFilter', () => {
);
- fireEvent.change(screen.getByPlaceholderText(/filter/i), { target: { value: 'comedy' } });
+ fireEvent.change(screen.getByPlaceholderText(/filter/i), {
+ target: { value: 'comedy' },
+ });
fireEvent.click(screen.getByText('Deselect Visible'));
const called = setCategoryStates.mock.calls.at(-1)[0];
expect(called.find((s) => s.id === 2).enabled).toBe(false);
@@ -315,7 +413,10 @@ describe('VODCategoryFilter', () => {
const setAutoEnableNewGroups = vi.fn();
render(
);
fireEvent.click(screen.getByLabelText(/automatically enable new/i));
@@ -326,7 +427,10 @@ describe('VODCategoryFilter', () => {
const setAutoEnableNewGroups = vi.fn();
render(
);
fireEvent.click(screen.getByLabelText(/automatically enable new/i));
@@ -340,23 +444,34 @@ describe('VODCategoryFilter', () => {
it('renders no category buttons when categories list is empty', () => {
setupMocks({ categories: [] });
render();
- expect(screen.queryByRole('button', { name: 'Action' })).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Action' })
+ ).not.toBeInTheDocument();
});
it('renders no category buttons when categoryStates is empty', () => {
render();
- expect(screen.queryByRole('button', { name: 'Action' })).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Action' })
+ ).not.toBeInTheDocument();
});
it('renders categories only for the matching playlist id', () => {
setupMocks({
categories: [
...makeCategories(),
- { id: 99, name: 'Foreign', m3u_accounts: [{ m3u_account: 99, enabled: true }], category_type: 'movies' },
+ {
+ id: 99,
+ name: 'Foreign',
+ m3u_accounts: [{ m3u_account: 99, enabled: true }],
+ category_type: 'movies',
+ },
],
});
render();
- expect(screen.queryByRole('button', { name: 'Foreign' })).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Foreign' })
+ ).not.toBeInTheDocument();
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/utils/forms/SeriesRuleEditorModalUtils.js b/frontend/src/utils/forms/SeriesRuleEditorModalUtils.js
index dbcd545e..0eddc9c1 100644
--- a/frontend/src/utils/forms/SeriesRuleEditorModalUtils.js
+++ b/frontend/src/utils/forms/SeriesRuleEditorModalUtils.js
@@ -25,7 +25,7 @@ export function formatRange(start, end) {
const s = new Date(start);
const e = new Date(end);
- if(isNaN(s) || isNaN(e)) throw new Error('Invalid date');
+ if (isNaN(s) || isNaN(e)) throw new Error('Invalid date');
const sameDay = s.toDateString() === e.toDateString();
const dateStr = s.toLocaleDateString();
@@ -81,4 +81,4 @@ export const getChannelOptions = (allChannels, tvgsById, tvgId) => {
}
}
return [...matching, ...others];
-};
\ No newline at end of file
+};
diff --git a/frontend/src/utils/forms/UserUtils.js b/frontend/src/utils/forms/UserUtils.js
index 256d84bb..3627d6ca 100644
--- a/frontend/src/utils/forms/UserUtils.js
+++ b/frontend/src/utils/forms/UserUtils.js
@@ -1,6 +1,6 @@
-import {NETWORK_ACCESS_OPTIONS, USER_LEVELS} from "../../constants.js";
+import { NETWORK_ACCESS_OPTIONS, USER_LEVELS } from '../../constants.js';
import { IPV4_CIDR_REGEX, IPV6_CIDR_REGEX } from '../networkUtils.js';
-import API from "../../api.js";
+import API from '../../api.js';
const isValidNetworkEntry = (entry) =>
entry.match(IPV4_CIDR_REGEX) ||
@@ -123,14 +123,14 @@ export const getFormInitialValues = () => {
epg_prev_days: 0,
allowed_ips: [],
};
-}
+};
export const getFormValidators = (user) => {
return (values) => ({
username: !values.username
? 'Username is required'
: values.user_level == USER_LEVELS.STREAMER &&
- !values.username.match(/^[a-z0-9]+$/i)
+ !values.username.match(/^[a-z0-9]+$/i)
? 'Streamer username must be alphanumeric'
: null,
password:
@@ -141,10 +141,8 @@ export const getFormValidators = (user) => {
values.xc_password && !values.xc_password.match(/^[a-z0-9]+$/i)
? 'XC password must be alphanumeric'
: null,
- allowed_ips: (values.allowed_ips || []).some(
- (t) => !isValidNetworkEntry(t)
- )
+ allowed_ips: (values.allowed_ips || []).some((t) => !isValidNetworkEntry(t))
? 'Invalid IP address or CIDR range'
: null,
});
-}
\ No newline at end of file
+};
diff --git a/frontend/src/utils/forms/__tests__/RecordingDetailsModalUtils.test.js b/frontend/src/utils/forms/__tests__/RecordingDetailsModalUtils.test.js
index 303b891b..7ad80f63 100644
--- a/frontend/src/utils/forms/__tests__/RecordingDetailsModalUtils.test.js
+++ b/frontend/src/utils/forms/__tests__/RecordingDetailsModalUtils.test.js
@@ -11,7 +11,6 @@ vi.mock('../../../api.js', () => ({
},
}));
-
describe('RecordingDetailsModalUtils', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -653,7 +652,9 @@ describe('RecordingDetailsModalUtils', () => {
it('should propagate API errors', async () => {
API.getChannel.mockRejectedValue(new Error('Not found'));
- await expect(RecordingDetailsModalUtils.getChannel(99)).rejects.toThrow('Not found');
+ await expect(RecordingDetailsModalUtils.getChannel(99)).rejects.toThrow(
+ 'Not found'
+ );
});
});
@@ -680,7 +681,11 @@ describe('RecordingDetailsModalUtils', () => {
API.updateRecordingMetadata.mockResolvedValue({});
const recording = { id: 5 };
- await RecordingDetailsModalUtils.updateRecordingMetadata(recording, '', 'Some description');
+ await RecordingDetailsModalUtils.updateRecordingMetadata(
+ recording,
+ '',
+ 'Some description'
+ );
expect(API.updateRecordingMetadata).toHaveBeenCalledWith(5, {
title: 'Custom Recording',
@@ -692,7 +697,11 @@ describe('RecordingDetailsModalUtils', () => {
API.updateRecordingMetadata.mockResolvedValue({});
const recording = { id: 5 };
- await RecordingDetailsModalUtils.updateRecordingMetadata(recording, null, 'desc');
+ await RecordingDetailsModalUtils.updateRecordingMetadata(
+ recording,
+ null,
+ 'desc'
+ );
expect(API.updateRecordingMetadata).toHaveBeenCalledWith(5, {
title: 'Custom Recording',
@@ -704,7 +713,11 @@ describe('RecordingDetailsModalUtils', () => {
API.updateRecordingMetadata.mockRejectedValue(new Error('Server error'));
await expect(
- RecordingDetailsModalUtils.updateRecordingMetadata({ id: 1 }, 'Title', 'Desc')
+ RecordingDetailsModalUtils.updateRecordingMetadata(
+ { id: 1 },
+ 'Title',
+ 'Desc'
+ )
).rejects.toThrow('Server error');
});
});
@@ -723,9 +736,9 @@ describe('RecordingDetailsModalUtils', () => {
it('should propagate API errors', async () => {
API.refreshArtwork.mockRejectedValue(new Error('Artwork fetch failed'));
- await expect(RecordingDetailsModalUtils.refreshArtwork(7)).rejects.toThrow(
- 'Artwork fetch failed'
- );
+ await expect(
+ RecordingDetailsModalUtils.refreshArtwork(7)
+ ).rejects.toThrow('Artwork fetch failed');
});
});
});
diff --git a/frontend/src/utils/forms/__tests__/RecordingUtils.test.js b/frontend/src/utils/forms/__tests__/RecordingUtils.test.js
index be7cf1a2..6e3ae901 100644
--- a/frontend/src/utils/forms/__tests__/RecordingUtils.test.js
+++ b/frontend/src/utils/forms/__tests__/RecordingUtils.test.js
@@ -85,7 +85,7 @@ describe('RecordingUtils', () => {
});
});
-// ─── toIsoIfDate ──────────────────────────────────────────────────────────────
+ // ─── toIsoIfDate ──────────────────────────────────────────────────────────────
describe('toIsoIfDate', () => {
it('returns ISO string for a valid Date', () => {
@@ -107,7 +107,7 @@ describe('RecordingUtils', () => {
});
});
-// ─── toDateString ─────────────────────────────────────────────────────────────
+ // ─── toDateString ─────────────────────────────────────────────────────────────
describe('toDateString', () => {
it('formats a Date to YYYY-MM-DD', () => {
@@ -129,7 +129,7 @@ describe('RecordingUtils', () => {
});
});
-// ─── createRoundedDate ────────────────────────────────────────────────────────
+ // ─── createRoundedDate ────────────────────────────────────────────────────────
describe('createRoundedDate', () => {
it('returns a Date', () => {
@@ -141,7 +141,7 @@ describe('RecordingUtils', () => {
});
});
-// ─── timeChange ───────────────────────────────────────────────────────────────
+ // ─── timeChange ───────────────────────────────────────────────────────────────
describe('timeChange', () => {
it('calls setter with string directly', () => {
@@ -163,7 +163,7 @@ describe('RecordingUtils', () => {
});
});
-// ─── API wrappers ─────────────────────────────────────────────────────────────
+ // ─── API wrappers ─────────────────────────────────────────────────────────────
describe('getChannelsSummary', () => {
it('calls API.getChannelsSummary', () => {
@@ -197,13 +197,13 @@ describe('RecordingUtils', () => {
});
});
-// ─── sortedChannelOptions ─────────────────────────────────────────────────────
+ // ─── sortedChannelOptions ─────────────────────────────────────────────────────
describe('sortedChannelOptions', () => {
const channels = [
- { id: 3, name: 'CNN', channel_number: 20 },
- { id: 1, name: 'ESPN', channel_number: 5 },
- { id: 2, name: 'HBO', channel_number: 10 },
+ { id: 3, name: 'CNN', channel_number: 20 },
+ { id: 1, name: 'ESPN', channel_number: 5 },
+ { id: 2, name: 'HBO', channel_number: 10 },
];
it('sorts channels by channel_number ascending', () => {
@@ -213,7 +213,9 @@ describe('RecordingUtils', () => {
});
it('maps each channel to { value, label }', () => {
- const result = sortedChannelOptions([{ id: 7, name: 'FOX', channel_number: 3 }]);
+ const result = sortedChannelOptions([
+ { id: 7, name: 'FOX', channel_number: 3 },
+ ]);
expect(result[0]).toEqual({ value: '7', label: 'FOX' });
});
@@ -269,22 +271,25 @@ describe('RecordingUtils', () => {
});
});
-// ─── numberedChannelLabel ─────────────────────────────────────────────────────
+ // ─── numberedChannelLabel ─────────────────────────────────────────────────────
describe('numberedChannelLabel', () => {
it('returns "number - name" when both are present', () => {
- expect(numberedChannelLabel({ id: 1, name: 'HBO', channel_number: 501 }))
- .toBe('501 - HBO');
+ expect(
+ numberedChannelLabel({ id: 1, name: 'HBO', channel_number: 501 })
+ ).toBe('501 - HBO');
});
it('returns name only when channel_number is falsy', () => {
- expect(numberedChannelLabel({ id: 1, name: 'HBO', channel_number: null }))
- .toBe('HBO');
+ expect(
+ numberedChannelLabel({ id: 1, name: 'HBO', channel_number: null })
+ ).toBe('HBO');
});
it('falls back to "Channel id" when name is missing and number present', () => {
- expect(numberedChannelLabel({ id: 5, channel_number: 10 }))
- .toBe('10 - Channel 5');
+ expect(numberedChannelLabel({ id: 5, channel_number: 10 })).toBe(
+ '10 - Channel 5'
+ );
});
it('falls back to "Channel id" when both name and number are missing', () => {
@@ -292,7 +297,7 @@ describe('RecordingUtils', () => {
});
});
-// ─── getSingleFormDefaults ────────────────────────────────────────────────────
+ // ─── getSingleFormDefaults ────────────────────────────────────────────────────
describe('getSingleFormDefaults', () => {
it('returns defaults with no arguments', () => {
@@ -320,7 +325,7 @@ describe('RecordingUtils', () => {
});
});
-// ─── getRecurringFormDefaults ─────────────────────────────────────────────────
+ // ─── getRecurringFormDefaults ─────────────────────────────────────────────────
describe('getRecurringFormDefaults', () => {
it('returns defaults with no arguments', () => {
@@ -341,7 +346,7 @@ describe('RecordingUtils', () => {
});
});
-// ─── buildSinglePayload ───────────────────────────────────────────────────────
+ // ─── buildSinglePayload ───────────────────────────────────────────────────────
describe('buildSinglePayload', () => {
it('builds payload from values', () => {
@@ -357,7 +362,7 @@ describe('RecordingUtils', () => {
});
});
-// ─── buildRecurringPayload ────────────────────────────────────────────────────
+ // ─── buildRecurringPayload ────────────────────────────────────────────────────
describe('buildRecurringPayload', () => {
it('builds payload from values', () => {
@@ -396,7 +401,7 @@ describe('RecordingUtils', () => {
});
});
-// ─── singleFormValidators ─────────────────────────────────────────────────────
+ // ─── singleFormValidators ─────────────────────────────────────────────────────
describe('singleFormValidators', () => {
describe('end_time', () => {
@@ -424,12 +429,14 @@ describe('RecordingUtils', () => {
it('returns null when end is after start', () => {
const start = new Date('2024-06-15T09:00:00Z');
const end = new Date('2024-06-15T10:00:00Z');
- expect(singleFormValidators.end_time(end, { start_time: start })).toBeNull();
+ expect(
+ singleFormValidators.end_time(end, { start_time: start })
+ ).toBeNull();
});
});
});
-// ─── recurringFormValidators ──────────────────────────────────────────────────
+ // ─── recurringFormValidators ──────────────────────────────────────────────────
describe('recurringFormValidators', () => {
describe('days_of_week', () => {
@@ -458,9 +465,11 @@ describe('RecordingUtils', () => {
describe('end_time', () => {
it('returns error when value is falsy', () => {
- expect(recurringFormValidators.end_time(null, { start_time: '2024-06-15T10:00:00' })).toBe(
- 'Select an end time'
- );
+ expect(
+ recurringFormValidators.end_time(null, {
+ start_time: '2024-06-15T10:00:00',
+ })
+ ).toBe('Select an end time');
});
it('returns error when end equals start (diff === 0)', () => {
@@ -488,15 +497,17 @@ describe('RecordingUtils', () => {
it('returns error when end_date is before start_date', () => {
const start = new Date('2024-06-15');
const end = new Date('2024-06-01');
- expect(recurringFormValidators.end_date(end, { start_date: start })).toBe(
- 'End date cannot be before start date'
- );
+ expect(
+ recurringFormValidators.end_date(end, { start_date: start })
+ ).toBe('End date cannot be before start date');
});
it('returns null when end_date is on or after start_date', () => {
const start = new Date('2024-06-15');
const end = new Date('2024-06-15');
- expect(recurringFormValidators.end_date(end, { start_date: start })).toBeNull();
+ expect(
+ recurringFormValidators.end_date(end, { start_date: start })
+ ).toBeNull();
});
});
});
diff --git a/frontend/src/utils/forms/__tests__/SeriesRuleEditorModalUtils.test.js b/frontend/src/utils/forms/__tests__/SeriesRuleEditorModalUtils.test.js
index 1731e08d..0c234085 100644
--- a/frontend/src/utils/forms/__tests__/SeriesRuleEditorModalUtils.test.js
+++ b/frontend/src/utils/forms/__tests__/SeriesRuleEditorModalUtils.test.js
@@ -8,11 +8,10 @@ vi.mock('../../../api.js', () => ({
}));
vi.mock('../RecordingUtils.js', () => ({
- numberedChannelLabel: vi.fn(
- (item) =>
- item.channel_number
- ? `${item.channel_number} - ${item.name || `Channel ${item.id}`}`
- : item.name || `Channel ${item.id}`
+ numberedChannelLabel: vi.fn((item) =>
+ item.channel_number
+ ? `${item.channel_number} - ${item.name || `Channel ${item.id}`}`
+ : item.name || `Channel ${item.id}`
),
sortedChannelOptions: vi.fn((channels) =>
(Array.isArray(channels) ? channels : []).map((c) => ({
@@ -33,7 +32,10 @@ import {
getChannelOptions,
previewSeriesRule,
} from '../SeriesRuleEditorModalUtils.js';
-import { sortedChannelOptions, numberedChannelLabel } from '../RecordingUtils.js';
+import {
+ sortedChannelOptions,
+ numberedChannelLabel,
+} from '../RecordingUtils.js';
// ─────────────────────────────────────────────────────────────────────────────
@@ -112,7 +114,7 @@ describe('SeriesRuleEditorModalUtils', () => {
describe('formatRange', () => {
it('formats same-day range as "date startTime - endTime"', () => {
const start = '2024-06-01T10:00:00';
- const end = '2024-06-01T11:30:00';
+ const end = '2024-06-01T11:30:00';
const result = formatRange(start, end);
// Must include a dash separating start and end times (not "->")
expect(result).toMatch(/-(?!>)/);
@@ -121,7 +123,7 @@ describe('SeriesRuleEditorModalUtils', () => {
it('formats cross-day range with "->" separator', () => {
const start = '2024-06-01T23:00:00';
- const end = '2024-06-02T01:00:00';
+ const end = '2024-06-02T01:00:00';
const result = formatRange(start, end);
expect(result).toMatch(/->/);
});
@@ -133,7 +135,7 @@ describe('SeriesRuleEditorModalUtils', () => {
it('includes a date string component in the output', () => {
const start = '2024-06-01T10:00:00';
- const end = '2024-06-01T11:00:00';
+ const end = '2024-06-01T11:00:00';
const result = formatRange(start, end);
// toLocaleDateString produces something non-empty
expect(result.length).toBeGreaterThan(5);
@@ -141,7 +143,7 @@ describe('SeriesRuleEditorModalUtils', () => {
it('handles ISO strings with timezone offsets', () => {
const start = '2024-06-01T10:00:00Z';
- const end = '2024-06-01T11:00:00Z';
+ const end = '2024-06-01T11:00:00Z';
// Should not throw
expect(() => formatRange(start, end)).not.toThrow();
});
@@ -227,15 +229,15 @@ describe('SeriesRuleEditorModalUtils', () => {
describe('getChannelOptions', () => {
const makeChannels = () => [
- { id: 1, name: 'ESPN', channel_number: 5, epg_data_id: 'epg-1' },
- { id: 2, name: 'HBO', channel_number: 10, epg_data_id: 'epg-2' },
- { id: 3, name: 'CNN', channel_number: 20, epg_data_id: 'epg-3' },
+ { id: 1, name: 'ESPN', channel_number: 5, epg_data_id: 'epg-1' },
+ { id: 2, name: 'HBO', channel_number: 10, epg_data_id: 'epg-2' },
+ { id: 3, name: 'CNN', channel_number: 20, epg_data_id: 'epg-3' },
];
const makeTvgsById = () => ({
'epg-1': { tvg_id: 'tvg-espn' },
- 'epg-2': { tvg_id: 'tvg-hbo' },
- 'epg-3': { tvg_id: 'tvg-cnn' },
+ 'epg-2': { tvg_id: 'tvg-hbo' },
+ 'epg-3': { tvg_id: 'tvg-cnn' },
});
it('calls sortedChannelOptions with allChannels and numberedChannelLabel', () => {
@@ -262,9 +264,9 @@ describe('SeriesRuleEditorModalUtils', () => {
it('places matching channels before non-matching when tvgId set', () => {
// Override sortedChannelOptions to return deterministic output
vi.mocked(sortedChannelOptions).mockReturnValueOnce([
- { value: '1', label: 'ESPN' },
- { value: '2', label: 'HBO' },
- { value: '3', label: 'CNN' },
+ { value: '1', label: 'ESPN' },
+ { value: '2', label: 'HBO' },
+ { value: '3', label: 'CNN' },
]);
const channels = makeChannels();
@@ -273,15 +275,15 @@ describe('SeriesRuleEditorModalUtils', () => {
// tvgId matches ESPN (epg-1 → tvg-espn)
const result = getChannelOptions(channels, tvgsById, 'tvg-espn');
- expect(result[0].value).toBe('1'); // ESPN first (matching)
- expect(result[1].value).toBe('2'); // HBO second (non-matching)
- expect(result[2].value).toBe('3'); // CNN third (non-matching)
+ expect(result[0].value).toBe('1'); // ESPN first (matching)
+ expect(result[1].value).toBe('2'); // HBO second (non-matching)
+ expect(result[2].value).toBe('3'); // CNN third (non-matching)
});
it('returns all in sorted order when no channel matches tvgId', () => {
vi.mocked(sortedChannelOptions).mockReturnValueOnce([
{ value: '1', label: 'ESPN' },
- { value: '2', label: 'HBO' },
+ { value: '2', label: 'HBO' },
]);
const channels = makeChannels();
@@ -292,7 +294,9 @@ describe('SeriesRuleEditorModalUtils', () => {
});
it('handles channel with no epg_data_id (cTvg is null)', () => {
- const channels = [{ id: 9, name: 'NoEPG', channel_number: 1, epg_data_id: null }];
+ const channels = [
+ { id: 9, name: 'NoEPG', channel_number: 1, epg_data_id: null },
+ ];
vi.mocked(sortedChannelOptions).mockReturnValueOnce([
{ value: '9', label: 'NoEPG' },
]);
@@ -305,7 +309,14 @@ describe('SeriesRuleEditorModalUtils', () => {
});
it('handles missing epg_data_id entry in tvgsById', () => {
- const channels = [{ id: 5, name: 'Unknown', channel_number: 1, epg_data_id: 'epg-missing' }];
+ const channels = [
+ {
+ id: 5,
+ name: 'Unknown',
+ channel_number: 1,
+ epg_data_id: 'epg-missing',
+ },
+ ];
vi.mocked(sortedChannelOptions).mockReturnValueOnce([
{ value: '5', label: 'Unknown' },
]);
@@ -333,20 +344,20 @@ describe('SeriesRuleEditorModalUtils', () => {
it('places multiple matching channels before non-matching', () => {
vi.mocked(sortedChannelOptions).mockReturnValueOnce([
- { value: '1', label: 'ESPN' },
+ { value: '1', label: 'ESPN' },
{ value: '2', label: 'ESPN2' },
- { value: '3', label: 'CNN' },
+ { value: '3', label: 'CNN' },
]);
const channels = [
- { id: 1, name: 'ESPN', channel_number: 5, epg_data_id: 'epg-1' },
- { id: 2, name: 'ESPN2', channel_number: 6, epg_data_id: 'epg-4' },
- { id: 3, name: 'CNN', channel_number: 20, epg_data_id: 'epg-3' },
+ { id: 1, name: 'ESPN', channel_number: 5, epg_data_id: 'epg-1' },
+ { id: 2, name: 'ESPN2', channel_number: 6, epg_data_id: 'epg-4' },
+ { id: 3, name: 'CNN', channel_number: 20, epg_data_id: 'epg-3' },
];
const tvgsById = {
'epg-1': { tvg_id: 'tvg-espn' },
'epg-4': { tvg_id: 'tvg-espn' }, // both ESPN channels share tvg_id
- 'epg-3': { tvg_id: 'tvg-cnn' },
+ 'epg-3': { tvg_id: 'tvg-cnn' },
};
const result = getChannelOptions(channels, tvgsById, 'tvg-espn');
@@ -385,9 +396,13 @@ describe('SeriesRuleEditorModalUtils', () => {
});
it('propagates rejection from API.previewSeriesRule', async () => {
- vi.mocked(API.previewSeriesRule).mockRejectedValue(new Error('Network error'));
+ vi.mocked(API.previewSeriesRule).mockRejectedValue(
+ new Error('Network error')
+ );
const controller = new AbortController();
- await expect(previewSeriesRule({}, controller)).rejects.toThrow('Network error');
+ await expect(previewSeriesRule({}, controller)).rejects.toThrow(
+ 'Network error'
+ );
});
it('propagates AbortError when signal is aborted', async () => {
@@ -397,7 +412,9 @@ describe('SeriesRuleEditorModalUtils', () => {
const controller = new AbortController();
controller.abort();
- await expect(previewSeriesRule({}, controller)).rejects.toThrow('Aborted');
+ await expect(previewSeriesRule({}, controller)).rejects.toThrow(
+ 'Aborted'
+ );
});
it('passes through all fields of the preview key', async () => {
@@ -422,4 +439,3 @@ describe('SeriesRuleEditorModalUtils', () => {
});
});
});
-
diff --git a/frontend/src/utils/forms/__tests__/StreamProfileUtils.test.js b/frontend/src/utils/forms/__tests__/StreamProfileUtils.test.js
index 3ed9357e..100f7817 100644
--- a/frontend/src/utils/forms/__tests__/StreamProfileUtils.test.js
+++ b/frontend/src/utils/forms/__tests__/StreamProfileUtils.test.js
@@ -54,7 +54,9 @@ describe('StreamProfileUtils', () => {
describe('COMMAND_EXAMPLES', () => {
it('has an entry for each non-custom built-in command', () => {
- const nonCustom = BUILT_IN_COMMANDS.filter((c) => c.value !== '__custom__');
+ const nonCustom = BUILT_IN_COMMANDS.filter(
+ (c) => c.value !== '__custom__'
+ );
for (const cmd of nonCustom) {
expect(COMMAND_EXAMPLES).toHaveProperty(cmd.value);
expect(typeof COMMAND_EXAMPLES[cmd.value]).toBe('string');
@@ -194,4 +196,4 @@ describe('StreamProfileUtils', () => {
expect(API.addStreamProfile).toHaveBeenCalledWith({});
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/utils/forms/__tests__/StreamUtils.test.js b/frontend/src/utils/forms/__tests__/StreamUtils.test.js
index 4eeded36..1cb25805 100644
--- a/frontend/src/utils/forms/__tests__/StreamUtils.test.js
+++ b/frontend/src/utils/forms/__tests__/StreamUtils.test.js
@@ -32,7 +32,7 @@ describe('StreamUtils', () => {
});
});
-// ─── updateStream ─────────────────────────────────────────────────────────────
+ // ─── updateStream ─────────────────────────────────────────────────────────────
describe('updateStream', () => {
beforeEach(() => {
@@ -63,7 +63,7 @@ describe('StreamUtils', () => {
});
});
-// ─── addStream ────────────────────────────────────────────────────────────────
+ // ─── addStream ────────────────────────────────────────────────────────────────
describe('addStream', () => {
beforeEach(() => {
@@ -92,4 +92,4 @@ describe('StreamUtils', () => {
expect(API.addStream).toHaveBeenCalledWith({});
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/utils/forms/__tests__/UserAgentUtils.test.js b/frontend/src/utils/forms/__tests__/UserAgentUtils.test.js
index ed6163b2..2e0bed4f 100644
--- a/frontend/src/utils/forms/__tests__/UserAgentUtils.test.js
+++ b/frontend/src/utils/forms/__tests__/UserAgentUtils.test.js
@@ -13,7 +13,11 @@ vi.mock('@hookform/resolvers/yup', () => ({
import API from '../../../api.js';
import { yupResolver } from '@hookform/resolvers/yup';
-import { getResolver, updateUserAgent, addUserAgent } from '../UserAgentUtils.js';
+import {
+ getResolver,
+ updateUserAgent,
+ addUserAgent,
+} from '../UserAgentUtils.js';
describe('UserAgentUtils', () => {
// ─── getResolver ──────────────────────────────────────────────────────────────
@@ -32,7 +36,7 @@ describe('UserAgentUtils', () => {
});
});
-// ─── updateUserAgent ──────────────────────────────────────────────────────────
+ // ─── updateUserAgent ──────────────────────────────────────────────────────────
describe('updateUserAgent', () => {
beforeEach(() => {
@@ -63,7 +67,7 @@ describe('UserAgentUtils', () => {
});
});
-// ─── addUserAgent ─────────────────────────────────────────────────────────────
+ // ─── addUserAgent ─────────────────────────────────────────────────────────────
describe('addUserAgent', () => {
beforeEach(() => {
@@ -92,4 +96,4 @@ describe('UserAgentUtils', () => {
expect(API.addUserAgent).toHaveBeenCalledWith({});
});
});
-});
\ No newline at end of file
+});
diff --git a/frontend/src/utils/forms/__tests__/UserUtils.test.js b/frontend/src/utils/forms/__tests__/UserUtils.test.js
index 3618228a..5157ac96 100644
--- a/frontend/src/utils/forms/__tests__/UserUtils.test.js
+++ b/frontend/src/utils/forms/__tests__/UserUtils.test.js
@@ -47,7 +47,10 @@ describe('UserUtils', () => {
it('calls API.createUser with the provided values', () => {
API.createUser.mockResolvedValueOnce({ id: 1 });
createUser({ username: 'alice', password: 'pass' });
- expect(API.createUser).toHaveBeenCalledWith({ username: 'alice', password: 'pass' });
+ expect(API.createUser).toHaveBeenCalledWith({
+ username: 'alice',
+ password: 'pass',
+ });
});
it('returns the promise from API.createUser', () => {
@@ -57,7 +60,7 @@ describe('UserUtils', () => {
});
});
-// ─── updateUser ───────────────────────────────────────────────────────────────
+ // ─── updateUser ───────────────────────────────────────────────────────────────
describe('updateUser', () => {
beforeEach(() => vi.clearAllMocks());
@@ -65,19 +68,31 @@ describe('UserUtils', () => {
it('calls API.updateUser with selfEdit=false when isAdmin is true', () => {
API.updateUser.mockResolvedValueOnce({});
updateUser(1, { username: 'bob' }, true, { id: 1 });
- expect(API.updateUser).toHaveBeenCalledWith(1, { username: 'bob' }, false);
+ expect(API.updateUser).toHaveBeenCalledWith(
+ 1,
+ { username: 'bob' },
+ false
+ );
});
it('calls API.updateUser with selfEdit=true when not admin and userId matches authUser', () => {
API.updateUser.mockResolvedValueOnce({});
updateUser(5, { username: 'carol' }, false, { id: 5 });
- expect(API.updateUser).toHaveBeenCalledWith(5, { username: 'carol' }, true);
+ expect(API.updateUser).toHaveBeenCalledWith(
+ 5,
+ { username: 'carol' },
+ true
+ );
});
it('calls API.updateUser with selfEdit=false when not admin and userId does not match authUser', () => {
API.updateUser.mockResolvedValueOnce({});
updateUser(5, { username: 'dave' }, false, { id: 99 });
- expect(API.updateUser).toHaveBeenCalledWith(5, { username: 'dave' }, false);
+ expect(API.updateUser).toHaveBeenCalledWith(
+ 5,
+ { username: 'dave' },
+ false
+ );
});
it('returns the promise from API.updateUser', () => {
@@ -87,7 +102,7 @@ describe('UserUtils', () => {
});
});
-// ─── generateApiKey ───────────────────────────────────────────────────────────
+ // ─── generateApiKey ───────────────────────────────────────────────────────────
describe('generateApiKey', () => {
beforeEach(() => vi.clearAllMocks());
@@ -105,7 +120,7 @@ describe('UserUtils', () => {
});
});
-// ─── revokeApiKey ─────────────────────────────────────────────────────────────
+ // ─── revokeApiKey ─────────────────────────────────────────────────────────────
describe('revokeApiKey', () => {
beforeEach(() => vi.clearAllMocks());
@@ -123,7 +138,7 @@ describe('UserUtils', () => {
});
});
-// ─── userToFormValues ─────────────────────────────────────────────────────────
+ // ─── userToFormValues ─────────────────────────────────────────────────────────
describe('userToFormValues', () => {
const makeUser = (overrides = {}) => ({
@@ -149,7 +164,9 @@ describe('UserUtils', () => {
});
it('defaults first_name and last_name to empty string when absent', () => {
- const result = userToFormValues(makeUser({ first_name: null, last_name: null }));
+ const result = userToFormValues(
+ makeUser({ first_name: null, last_name: null })
+ );
expect(result.first_name).toBe('');
expect(result.last_name).toBe('');
});
@@ -160,7 +177,9 @@ describe('UserUtils', () => {
});
it('uses channel_profiles when non-empty', () => {
- const result = userToFormValues(makeUser({ channel_profiles: ['3', '4'] }));
+ const result = userToFormValues(
+ makeUser({ channel_profiles: ['3', '4'] })
+ );
expect(result.channel_profiles).toEqual(['3', '4']);
});
@@ -217,7 +236,7 @@ describe('UserUtils', () => {
});
});
-// ─── formValuesToPayload ──────────────────────────────────────────────────────
+ // ─── formValuesToPayload ──────────────────────────────────────────────────────
describe('formValuesToPayload', () => {
const makeValues = (overrides = {}) => ({
@@ -253,7 +272,10 @@ describe('UserUtils', () => {
});
it('sets output_profile to null when empty string', () => {
- const result = formValuesToPayload(makeValues({ output_profile: '' }), null);
+ const result = formValuesToPayload(
+ makeValues({ output_profile: '' }),
+ null
+ );
expect(result.custom_properties.output_profile).toBeNull();
});
@@ -272,11 +294,20 @@ describe('UserUtils', () => {
});
it('populates allowed_networks for all NETWORK_KEYS when allowed_ips present', () => {
- const result = formValuesToPayload(makeValues({ allowed_ips: ['192.168.1.0/24', '10.0.0.1/32'] }), null);
+ const result = formValuesToPayload(
+ makeValues({ allowed_ips: ['192.168.1.0/24', '10.0.0.1/32'] }),
+ null
+ );
expect(result.allowed_ips).toBeUndefined();
- expect(result.custom_properties.allowed_networks.m3u).toBe('192.168.1.0/24,10.0.0.1/32');
- expect(result.custom_properties.allowed_networks.xc).toBe('192.168.1.0/24,10.0.0.1/32');
- expect(result.custom_properties.allowed_networks.mpegts).toBe('192.168.1.0/24,10.0.0.1/32');
+ expect(result.custom_properties.allowed_networks.m3u).toBe(
+ '192.168.1.0/24,10.0.0.1/32'
+ );
+ expect(result.custom_properties.allowed_networks.xc).toBe(
+ '192.168.1.0/24,10.0.0.1/32'
+ );
+ expect(result.custom_properties.allowed_networks.mpegts).toBe(
+ '192.168.1.0/24,10.0.0.1/32'
+ );
});
it('sets empty allowed_networks when allowed_ips is empty', () => {
@@ -285,17 +316,25 @@ describe('UserUtils', () => {
});
it('sets channel_profiles to empty array when it includes "0"', () => {
- const result = formValuesToPayload(makeValues({ channel_profiles: ['0'] }), null);
+ const result = formValuesToPayload(
+ makeValues({ channel_profiles: ['0'] }),
+ null
+ );
expect(result.channel_profiles).toEqual([]);
});
it('preserves channel_profiles when it does not include "0"', () => {
- const result = formValuesToPayload(makeValues({ channel_profiles: ['1', '2'] }), null);
+ const result = formValuesToPayload(
+ makeValues({ channel_profiles: ['1', '2'] }),
+ null
+ );
expect(result.channel_profiles).toEqual(['1', '2']);
});
it('merges existing custom_properties from existingUser', () => {
- const existingUser = { custom_properties: { some_other_prop: 'keep_me' } };
+ const existingUser = {
+ custom_properties: { some_other_prop: 'keep_me' },
+ };
const result = formValuesToPayload(makeValues(), existingUser);
expect(result.custom_properties.some_other_prop).toBe('keep_me');
});
@@ -305,7 +344,7 @@ describe('UserUtils', () => {
});
});
-// ─── getFormInitialValues ─────────────────────────────────────────────────────
+ // ─── getFormInitialValues ─────────────────────────────────────────────────────
describe('getFormInitialValues', () => {
it('returns the expected default structure', () => {
@@ -336,97 +375,151 @@ describe('UserUtils', () => {
});
});
-// ─── getFormValidators ────────────────────────────────────────────────────────
+ // ─── getFormValidators ────────────────────────────────────────────────────────
describe('getFormValidators', () => {
const validate = (user, values) => getFormValidators(user)(values);
describe('username', () => {
it('returns error when username is empty', () => {
- const result = validate(null, { ...getFormInitialValues(), username: '' });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ username: '',
+ });
expect(result.username).toBe('Username is required');
});
it('returns null for a valid non-streamer username', () => {
- const result = validate(null, { ...getFormInitialValues(), username: 'alice', user_level: '0' });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ username: 'alice',
+ user_level: '0',
+ });
expect(result.username).toBeNull();
});
it('returns null for a valid alphanumeric streamer username', () => {
- const result = validate(null, { ...getFormInitialValues(), username: 'alice123', user_level: '2' });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ username: 'alice123',
+ user_level: '2',
+ });
expect(result.username).toBeNull();
});
it('returns error for streamer username with non-alphanumeric characters', () => {
- const result = validate(null, { ...getFormInitialValues(), username: 'alice_123', user_level: '2' });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ username: 'alice_123',
+ user_level: '2',
+ });
expect(result.username).toBe('Streamer username must be alphanumeric');
});
});
describe('password', () => {
it('returns error when creating a non-streamer user without a password', () => {
- const result = validate(null, { ...getFormInitialValues(), user_level: '0', password: '' });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ user_level: '0',
+ password: '',
+ });
expect(result.password).toBe('Password is required');
});
it('returns null when creating a streamer user without a password', () => {
- const result = validate(null, { ...getFormInitialValues(), user_level: '2', password: '' });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ user_level: '2',
+ password: '',
+ });
expect(result.password).toBeNull();
});
it('returns null when editing an existing user without a password', () => {
- const result = validate({ id: 1 }, { ...getFormInitialValues(), user_level: '0', password: '' });
+ const result = validate(
+ { id: 1 },
+ { ...getFormInitialValues(), user_level: '0', password: '' }
+ );
expect(result.password).toBeNull();
});
it('returns null when password is provided for new user', () => {
- const result = validate(null, { ...getFormInitialValues(), user_level: '0', password: 'secret' });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ user_level: '0',
+ password: 'secret',
+ });
expect(result.password).toBeNull();
});
});
describe('xc_password', () => {
it('returns null when xc_password is empty', () => {
- const result = validate(null, { ...getFormInitialValues(), xc_password: '' });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ xc_password: '',
+ });
expect(result.xc_password).toBeNull();
});
it('returns null for a valid alphanumeric xc_password', () => {
- const result = validate(null, { ...getFormInitialValues(), xc_password: 'abc123' });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ xc_password: 'abc123',
+ });
expect(result.xc_password).toBeNull();
});
it('returns error for xc_password with non-alphanumeric characters', () => {
- const result = validate(null, { ...getFormInitialValues(), xc_password: 'abc!@#' });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ xc_password: 'abc!@#',
+ });
expect(result.xc_password).toBe('XC password must be alphanumeric');
});
});
describe('allowed_ips', () => {
it('returns null for an empty allowed_ips array', () => {
- const result = validate(null, { ...getFormInitialValues(), allowed_ips: [] });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ allowed_ips: [],
+ });
expect(result.allowed_ips).toBeNull();
});
it('returns null for a valid IPv4 CIDR', () => {
- const result = validate(null, { ...getFormInitialValues(), allowed_ips: ['192.168.1.0/24'] });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ allowed_ips: ['192.168.1.0/24'],
+ });
expect(result.allowed_ips).toBeNull();
});
it('returns null for a bare IPv4 address (auto-appends /32)', () => {
- const result = validate(null, { ...getFormInitialValues(), allowed_ips: ['10.0.0.1'] });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ allowed_ips: ['10.0.0.1'],
+ });
expect(result.allowed_ips).toBeNull();
});
it('returns error for an invalid IP entry', () => {
- const result = validate(null, { ...getFormInitialValues(), allowed_ips: ['not-an-ip'] });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ allowed_ips: ['not-an-ip'],
+ });
expect(result.allowed_ips).toBe('Invalid IP address or CIDR range');
});
it('returns error when any entry in the list is invalid', () => {
- const result = validate(null, { ...getFormInitialValues(), allowed_ips: ['192.168.1.0/24', 'bad-entry'] });
+ const result = validate(null, {
+ ...getFormInitialValues(),
+ allowed_ips: ['192.168.1.0/24', 'bad-entry'],
+ });
expect(result.allowed_ips).toBe('Invalid IP address or CIDR range');
});
});
});
-});
\ No newline at end of file
+});