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 }) => ( - ), Group: ({ children }) =>
{children}
, - Loader: ({ size, color }) => , + Loader: ({ size, color }) => ( + + ), Modal: ({ children, opened, onClose, title }) => opened ? (
{title}
- + {children}
) : null, MultiSelect: ({ label, placeholder, ...props }) => (
- +
), SegmentedControl: ({ value, onChange, data, disabled }) => ( @@ -115,7 +137,9 @@ vi.mock('@mantine/core', () => ({ {rightSection} @@ -124,7 +148,11 @@ vi.mock('@mantine/core', () => ({ TextInput: ({ label, placeholder, ...props }) => (
- +
), })); @@ -136,8 +164,10 @@ vi.mock('@mantine/dates', () => ({ onChange(e.target.value ? new Date(e.target.value) : null)} + value={value ? (value.toISOString?.() ?? value) : ''} + onChange={(e) => + onChange(e.target.value ? new Date(e.target.value) : null) + } /> ), @@ -175,7 +205,10 @@ const setupStoreMock = () => { const mockFetchRecurringRules = vi.fn().mockResolvedValue(undefined); vi.mocked(useChannelsStore).mockImplementation((sel) => - sel({ fetchRecordings: mockFetchRecordings, fetchRecurringRules: mockFetchRecurringRules }) + sel({ + fetchRecordings: mockFetchRecordings, + fetchRecurringRules: mockFetchRecurringRules, + }) ); return { mockFetchRecordings, mockFetchRecurringRules }; @@ -223,7 +256,9 @@ describe('RecordingModal', () => { it('renders the scheduling conflicts alert', () => { render(); expect(screen.getByTestId('alert')).toBeInTheDocument(); - expect(screen.getByTestId('alert-title')).toHaveTextContent('Scheduling Conflicts'); + expect(screen.getByTestId('alert-title')).toHaveTextContent( + 'Scheduling Conflicts' + ); }); }); @@ -232,13 +267,19 @@ describe('RecordingModal', () => { describe('mode switching', () => { it('defaults to "single" mode', () => { render(); - expect(screen.getByTestId('mode-single')).toHaveAttribute('data-active', 'true'); + expect(screen.getByTestId('mode-single')).toHaveAttribute( + 'data-active', + 'true' + ); }); it('switches to recurring mode when Recurring button clicked', () => { render(); fireEvent.click(screen.getByTestId('mode-recurring')); - expect(screen.getByTestId('mode-recurring')).toHaveAttribute('data-active', 'true'); + expect(screen.getByTestId('mode-recurring')).toHaveAttribute( + 'data-active', + 'true' + ); }); it('shows DateTimePicker fields in single mode', () => { @@ -256,7 +297,9 @@ describe('RecordingModal', () => { }); it('disables mode toggle when editing an existing recording', () => { - render(); + render( + + ); expect(screen.getByTestId('mode-single')).toBeDisabled(); expect(screen.getByTestId('mode-recurring')).toBeDisabled(); }); @@ -296,7 +339,9 @@ describe('RecordingModal', () => { }); it('calls sortedChannelOptions with [] when getChannelsSummary rejects', async () => { - vi.mocked(RecordingUtils.getChannelsSummary).mockRejectedValue(new Error('fail')); + vi.mocked(RecordingUtils.getChannelsSummary).mockRejectedValue( + new Error('fail') + ); render(); await waitFor(() => { expect(RecordingUtils.sortedChannelOptions).toHaveBeenCalledWith( @@ -336,7 +381,10 @@ describe('RecordingModal', () => { fireEvent.submit(screen.getByText('Schedule Recording').closest('form')); await waitFor(() => { expect(showNotification).toHaveBeenCalledWith( - expect.objectContaining({ title: 'Recording scheduled', color: 'green' }) + expect.objectContaining({ + title: 'Recording scheduled', + color: 'green', + }) ); }); }); @@ -360,7 +408,9 @@ describe('RecordingModal', () => { }); it('does not call showNotification when createRecording throws', async () => { - vi.mocked(RecordingUtils.createRecording).mockRejectedValue(new Error('fail')); + vi.mocked(RecordingUtils.createRecording).mockRejectedValue( + new Error('fail') + ); render(); fireEvent.submit(screen.getByText('Schedule Recording').closest('form')); await waitFor(() => { @@ -402,7 +452,10 @@ describe('RecordingModal', () => { fireEvent.submit(screen.getByText('Schedule Recording').closest('form')); await waitFor(() => { expect(showNotification).toHaveBeenCalledWith( - expect.objectContaining({ title: 'Recording updated', color: 'green' }) + expect.objectContaining({ + title: 'Recording updated', + color: 'green', + }) ); }); }); @@ -446,7 +499,10 @@ describe('RecordingModal', () => { fireEvent.submit(screen.getByText('Save Rule').closest('form')); await waitFor(() => { expect(showNotification).toHaveBeenCalledWith( - expect.objectContaining({ title: 'Recurring rule saved', color: 'green' }) + expect.objectContaining({ + title: 'Recurring rule saved', + color: 'green', + }) ); }); }); @@ -462,7 +518,9 @@ describe('RecordingModal', () => { }); it('does not show notification when createRecurringRule throws', async () => { - vi.mocked(RecordingUtils.createRecurringRule).mockRejectedValue(new Error('fail')); + vi.mocked(RecordingUtils.createRecurringRule).mockRejectedValue( + new Error('fail') + ); render(); fireEvent.click(screen.getByTestId('mode-recurring')); fireEvent.submit(screen.getByText('Save Rule').closest('form')); @@ -478,19 +536,34 @@ describe('RecordingModal', () => { it('calls getSingleFormDefaults with recording and channel when opening with existing recording', () => { const recording = makeRecording(); const channel = makeChannel(); - render(); - expect(RecordingUtils.getSingleFormDefaults).toHaveBeenCalledWith(recording, channel); + render( + + ); + expect(RecordingUtils.getSingleFormDefaults).toHaveBeenCalledWith( + recording, + channel + ); }); it('calls getSingleFormDefaults with null when opening for new recording', () => { render(); - expect(RecordingUtils.getSingleFormDefaults).toHaveBeenCalledWith(null, null); + expect(RecordingUtils.getSingleFormDefaults).toHaveBeenCalledWith( + null, + null + ); }); it('calls getRecurringFormDefaults with channel on open', () => { const channel = makeChannel(); render(); - expect(RecordingUtils.getRecurringFormDefaults).toHaveBeenCalledWith(channel); + expect(RecordingUtils.getRecurringFormDefaults).toHaveBeenCalledWith( + channel + ); }); }); @@ -504,4 +577,4 @@ describe('RecordingModal', () => { expect(onClose).toHaveBeenCalled(); }); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/components/forms/__tests__/RecordingDetailsModal.test.jsx b/frontend/src/components/forms/__tests__/RecordingDetailsModal.test.jsx index 91216ac1..9f07a5a8 100644 --- a/frontend/src/components/forms/__tests__/RecordingDetailsModal.test.jsx +++ b/frontend/src/components/forms/__tests__/RecordingDetailsModal.test.jsx @@ -59,7 +59,9 @@ vi.mock('@mantine/core', () => ({ ), 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 }) => ( {alt} ), - 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, + }) => (
), @@ -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 }) => ( - + ), })); @@ -66,7 +84,10 @@ vi.mock('../CronBuilder', () => ({ opened ? (
{currentValue} - - + + {initialRule ? initialRule.title : 'new'} + + +
) : 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} +
+ )}