From 48734727ad01fb2afdb8b4536d87abbc837ffad2 Mon Sep 17 00:00:00 2001 From: Nick Sandstrom <32273437+nick4810@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:28:37 -0700 Subject: [PATCH] Syntax formatting --- .../src/components/forms/AccountInfoModal.jsx | 10 +- .../components/forms/AssignChannelNumbers.jsx | 18 +- frontend/src/components/forms/Channel.jsx | 5 +- .../src/components/forms/ChannelGroup.jsx | 4 +- frontend/src/components/forms/Connection.jsx | 18 +- frontend/src/components/forms/CronBuilder.jsx | 4 +- frontend/src/components/forms/DummyEPG.jsx | 72 +++-- .../forms/__tests__/AccountInfoModal.test.jsx | 27 +- .../__tests__/AssignChannelNumbers.test.jsx | 20 +- .../forms/__tests__/Channel.test.jsx | 168 +++++++++--- .../forms/__tests__/ChannelBatch.test.jsx | 242 ++++++++++++----- .../forms/__tests__/ChannelGroup.test.jsx | 14 +- .../forms/__tests__/Connection.test.jsx | 247 +++++++++++++---- .../forms/__tests__/CronBuilder.test.jsx | 161 +++++++---- .../forms/__tests__/DummyEPG.test.jsx | 255 +++++++++++------- .../components/forms/__tests__/EPG.test.jsx | 150 ++++++++--- .../src/utils/__tests__/dateTimeUtils.test.js | 28 +- frontend/src/utils/forms/ChannelBatchUtils.js | 2 +- frontend/src/utils/forms/ChannelUtils.js | 2 +- frontend/src/utils/forms/ConnectionUtils.js | 2 +- frontend/src/utils/forms/DummyEpgUtils.js | 2 +- .../__tests__/AccountInfoModalUtils.test.js | 14 +- .../forms/__tests__/ChannelBatchUtils.test.js | 187 +++++++++---- .../forms/__tests__/ChannelUtils.test.js | 99 +++++-- .../forms/__tests__/ConnectionUtils.test.js | 125 +++++++-- .../forms/__tests__/CronBuilderUtils.test.js | 26 +- .../forms/__tests__/DummyEpgUtils.test.js | 240 +++++++++++------ 27 files changed, 1546 insertions(+), 596 deletions(-) diff --git a/frontend/src/components/forms/AccountInfoModal.jsx b/frontend/src/components/forms/AccountInfoModal.jsx index a85ecc34..a9e46147 100644 --- a/frontend/src/components/forms/AccountInfoModal.jsx +++ b/frontend/src/components/forms/AccountInfoModal.jsx @@ -16,7 +16,15 @@ import { Text, Tooltip, } from '@mantine/core'; -import { AlertTriangle, CheckCircle, Clock, Info, RefreshCw, Users, XCircle, } from 'lucide-react'; +import { + AlertTriangle, + CheckCircle, + Clock, + Info, + RefreshCw, + Users, + XCircle, +} from 'lucide-react'; import usePlaylistsStore from '../../store/playlists'; import { showNotification } from '../../utils/notificationUtils.js'; import { diff --git a/frontend/src/components/forms/AssignChannelNumbers.jsx b/frontend/src/components/forms/AssignChannelNumbers.jsx index 22861986..7d1f52fe 100644 --- a/frontend/src/components/forms/AssignChannelNumbers.jsx +++ b/frontend/src/components/forms/AssignChannelNumbers.jsx @@ -1,27 +1,17 @@ import React from 'react'; import API from '../../api'; -import { - Button, - Modal, - Text, - Group, - Flex, - NumberInput, -} from '@mantine/core'; +import { Button, Modal, Text, Group, Flex, NumberInput } from '@mantine/core'; import { ListOrdered } from 'lucide-react'; import { useForm } from '@mantine/form'; import { showNotification } from '../../utils/notificationUtils.js'; const assignChannelNumbers = (channelIds, starting_number) => { - return API.assignChannelNumbers( - channelIds, - starting_number - ); -} + return API.assignChannelNumbers(channelIds, starting_number); +}; const requeryChannels = () => { API.requeryChannels(); -} +}; const AssignChannelNumbers = ({ channelIds, isOpen, onClose }) => { const form = useForm({ diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx index 28826e7f..8ac62555 100644 --- a/frontend/src/components/forms/Channel.jsx +++ b/frontend/src/components/forms/Channel.jsx @@ -37,7 +37,10 @@ import { ListOrdered, SquarePlus, X, Zap } from 'lucide-react'; import useEPGsStore from '../../store/epgs'; import { FixedSizeList as List } from 'react-window'; import { USER_LEVEL_LABELS, USER_LEVELS } from '../../constants'; -import { showNotification, updateNotification, } from '../../utils/notificationUtils.js'; +import { + showNotification, + updateNotification, +} from '../../utils/notificationUtils.js'; import { addChannel, createLogo, diff --git a/frontend/src/components/forms/ChannelGroup.jsx b/frontend/src/components/forms/ChannelGroup.jsx index ebb4d9ee..5e5ddfd3 100644 --- a/frontend/src/components/forms/ChannelGroup.jsx +++ b/frontend/src/components/forms/ChannelGroup.jsx @@ -11,10 +11,10 @@ const updateChannelGroup = (channelGroup, values) => { id: channelGroup.id, ...values, }); -} +}; const addChannelGroup = (values) => { return API.addChannelGroup(values); -} +}; const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => { const canEditChannelGroup = useChannelsStore((s) => s.canEditChannelGroup); diff --git a/frontend/src/components/forms/Connection.jsx b/frontend/src/components/forms/Connection.jsx index 817789fd..ecbb0e0f 100644 --- a/frontend/src/components/forms/Connection.jsx +++ b/frontend/src/components/forms/Connection.jsx @@ -182,7 +182,7 @@ const ConnectionForm = ({ connection = null, isOpen, onClose }) => { const next = [...headers]; next[idx] = { ...next[idx], key: newValue }; setHeaders(next); - } + }; const onHeaderValueChange = (idx, newValue) => { const next = [...headers]; @@ -191,14 +191,12 @@ const ConnectionForm = ({ connection = null, isOpen, onClose }) => { value: newValue, }; setHeaders(next); - } + }; const onHeaderRemove = (idx) => { const next = headers.filter((_, i) => i !== idx); - setHeaders( - next.length ? next : [{ key: '', value: '' }] - ); - } + setHeaders(next.length ? next : [{ key: '', value: '' }]); + }; return ( @@ -257,8 +255,12 @@ const ConnectionForm = ({ connection = null, isOpen, onClose }) => { onHeaderKeyChange(idx, e.target.value)} - onValueChange={(e) => onHeaderValueChange(idx, e.target.value)} + onKeyChange={(e) => + onHeaderKeyChange(idx, e.target.value) + } + onValueChange={(e) => + onHeaderValueChange(idx, e.target.value) + } onRemove={() => onHeaderRemove(idx)} /> ))} diff --git a/frontend/src/components/forms/CronBuilder.jsx b/frontend/src/components/forms/CronBuilder.jsx index 4d92265a..9a704536 100644 --- a/frontend/src/components/forms/CronBuilder.jsx +++ b/frontend/src/components/forms/CronBuilder.jsx @@ -42,7 +42,9 @@ const CronPartInput = ({ field, cron, onChange }) => ( label={field.label} placeholder={field.placeholder} value={cron.split(' ')[field.index] || '*'} - onChange={(e) => onChange(updateCronPart(cron, field.index, e.currentTarget.value))} + onChange={(e) => + onChange(updateCronPart(cron, field.index, e.currentTarget.value)) + } /> ); diff --git a/frontend/src/components/forms/DummyEPG.jsx b/frontend/src/components/forms/DummyEPG.jsx index 166e6ab5..5a533f10 100644 --- a/frontend/src/components/forms/DummyEPG.jsx +++ b/frontend/src/components/forms/DummyEPG.jsx @@ -114,11 +114,17 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => { }); const patternValidation = useMemo(() => { - const title = matchPattern(titlePattern, sampleTitle, 'Title pattern error'); - const time = matchPattern(timePattern, sampleTitle, 'Time pattern error'); - const date = matchPattern(datePattern, sampleTitle, 'Date pattern error'); + const title = matchPattern( + titlePattern, + sampleTitle, + 'Title pattern error' + ); + const time = matchPattern(timePattern, sampleTitle, 'Time pattern error'); + const date = matchPattern(datePattern, sampleTitle, 'Date pattern error'); - const errors = [title.error, time.error, date.error].filter(Boolean).join('; '); + const errors = [title.error, time.error, date.error] + .filter(Boolean) + .join('; '); const timePlaceholders = buildTimePlaceholders( time.groups, @@ -138,40 +144,48 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => { const hasMatch = title.matched || time.matched; return { - titleMatch: title.matched, - timeMatch: time.matched, - dateMatch: date.matched, + titleMatch: title.matched, + timeMatch: time.matched, + dateMatch: date.matched, titleGroups: title.groups, - timeGroups: time.groups, - dateGroups: date.groups, + timeGroups: time.groups, + dateGroups: date.groups, calculatedPlaceholders: time.matched ? timePlaceholders : {}, error: errors || null, ...applyTemplates( - { titleTemplate, subtitleTemplate, descriptionTemplate, upcomingTitleTemplate, - upcomingDescriptionTemplate, endedTitleTemplate, endedDescriptionTemplate, - channelLogoUrl, programPosterUrl }, + { + titleTemplate, + subtitleTemplate, + descriptionTemplate, + upcomingTitleTemplate, + upcomingDescriptionTemplate, + endedTitleTemplate, + endedDescriptionTemplate, + channelLogoUrl, + programPosterUrl, + }, allGroups, hasMatch ), }; }, [ - titlePattern, - timePattern, - datePattern, - sampleTitle, - titleTemplate, - subtitleTemplate, - descriptionTemplate, - upcomingTitleTemplate, - upcomingDescriptionTemplate, - endedTitleTemplate, - endedDescriptionTemplate, - channelLogoUrl, - programPosterUrl, - form.values.custom_properties?.timezone, - form.values.custom_properties?.output_timezone, - form.values.custom_properties?.program_duration, - ]); + titlePattern, + timePattern, + datePattern, + sampleTitle, + titleTemplate, + subtitleTemplate, + descriptionTemplate, + upcomingTitleTemplate, + upcomingDescriptionTemplate, + endedTitleTemplate, + endedDescriptionTemplate, + channelLogoUrl, + programPosterUrl, + form.values.custom_properties?.timezone, + form.values.custom_properties?.output_timezone, + form.values.custom_properties?.program_duration, + ]); useEffect(() => { if (epg) { diff --git a/frontend/src/components/forms/__tests__/AccountInfoModal.test.jsx b/frontend/src/components/forms/__tests__/AccountInfoModal.test.jsx index 92117a3c..7fe40085 100644 --- a/frontend/src/components/forms/__tests__/AccountInfoModal.test.jsx +++ b/frontend/src/components/forms/__tests__/AccountInfoModal.test.jsx @@ -59,9 +59,7 @@ vi.mock('@mantine/core', () => ({ TableTd: ({ children }) => {children}, TableTr: ({ children }) => {children}, Text: ({ children }) => {children}, - Tooltip: ({ children, label }) => ( -
{children}
- ), + Tooltip: ({ children, label }) =>
{children}
, })); // ── lucide-react ─────────────────────────────────────────────────────────────── @@ -109,13 +107,11 @@ const makeProfile = (overrides = {}) => ({ }); const setupMocks = ({ - profiles = {}, - timeRemaining = '10 days 5 hours', - formattedTimestamp = 'Nov 14, 2023', - } = {}) => { - vi.mocked(usePlaylistsStore).mockImplementation((sel) => - sel({ profiles }) - ); + profiles = {}, + timeRemaining = '10 days 5 hours', + formattedTimestamp = 'Nov 14, 2023', +} = {}) => { + vi.mocked(usePlaylistsStore).mockImplementation((sel) => sel({ profiles })); vi.mocked(getTimeRemaining).mockReturnValue(timeRemaining); vi.mocked(formatTimestamp).mockReturnValue(formattedTimestamp); vi.mocked(refreshAccountInfo).mockResolvedValue({ success: true }); @@ -397,7 +393,10 @@ describe('AccountInfoModal', () => { it('disables the refresh button while refreshing', async () => { setupMocks(); vi.mocked(refreshAccountInfo).mockImplementation( - () => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 200)) + () => + new Promise((resolve) => + setTimeout(() => resolve({ success: true }), 200) + ) ); render(); fireEvent.click(screen.getByTestId('action-icon')); @@ -409,7 +408,9 @@ describe('AccountInfoModal', () => { it('re-enables the refresh button after refresh fails', async () => { setupMocks(); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const consoleSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); vi.mocked(refreshAccountInfo).mockRejectedValue(new Error('fail')); render(); fireEvent.click(screen.getByTestId('action-icon')); @@ -443,4 +444,4 @@ describe('AccountInfoModal', () => { expect(screen.getByTestId('modal')).toBeInTheDocument(); }); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/components/forms/__tests__/AssignChannelNumbers.test.jsx b/frontend/src/components/forms/__tests__/AssignChannelNumbers.test.jsx index 2bec2d7b..c1f7cea7 100644 --- a/frontend/src/components/forms/__tests__/AssignChannelNumbers.test.jsx +++ b/frontend/src/components/forms/__tests__/AssignChannelNumbers.test.jsx @@ -26,7 +26,15 @@ vi.mock('@mantine/form', () => ({ // ── Mantine core ─────────────────────────────────────────────────────────────── vi.mock('@mantine/core', () => ({ - Button: ({ children, onClick, disabled, loading, variant, color, leftSection }) => ( + Button: ({ + children, + onClick, + disabled, + loading, + variant, + color, + leftSection, + }) => ( ), Box: ({ children, style }) =>
{children}
, - Button: ({ children, onClick, disabled, loading, type, variant, color, leftSection, title }) => ( + Button: ({ + children, + onClick, + disabled, + loading, + type, + variant, + color, + leftSection, + title, + }) => ( + {children} ) : null, Paper: ({ children, style }) =>
{children}
, Popover: ({ children }) =>
{children}
, - PopoverDropdown: ({ children }) =>
{children}
, - PopoverTarget: ({ children }) =>
{children}
, + PopoverDropdown: ({ children }) => ( +
{children}
+ ), + PopoverTarget: ({ children }) => ( +
{children}
+ ), ScrollArea: ({ children, h }) =>
{children}
, Select: ({ label, data, value, onChange, disabled }) => ( ), Stack: ({ children }) =>
{children}
, Text: ({ children, size, c }) => ( - {children} + + {children} + ), TextInput: ({ label, placeholder, value, onChange, disabled }) => ( ({ ), Tooltip: ({ children, label }) =>
{children}
, UnstyledButton: ({ children, onClick, style }) => ( - + ), useMantineTheme: () => ({ tailwind: { green: { 5: '#38a169' } } }), })); @@ -246,7 +275,8 @@ const setupMocks = (overrides = {}) => { vi.mocked(useWarningsStore).mockImplementation((sel) => sel({ - isWarningSuppressed: overrides.isWarningSuppressed ?? vi.fn().mockReturnValue(false), + isWarningSuppressed: + overrides.isWarningSuppressed ?? vi.fn().mockReturnValue(false), suppressWarning: vi.fn(), }) ); @@ -269,14 +299,24 @@ const setupMocks = (overrides = {}) => { }); vi.mocked(ChannelBatchUtils.computeRegexPreview).mockReturnValue([]); - vi.mocked(ChannelBatchUtils.buildSubmitValues).mockReturnValue({ stream_profile_id: '1' }); + vi.mocked(ChannelBatchUtils.buildSubmitValues).mockReturnValue({ + stream_profile_id: '1', + }); vi.mocked(ChannelBatchUtils.buildEpgAssociations).mockResolvedValue(null); vi.mocked(ChannelBatchUtils.updateChannels).mockResolvedValue(undefined); - vi.mocked(ChannelBatchUtils.bulkRegexRenameChannels).mockResolvedValue(undefined); + vi.mocked(ChannelBatchUtils.bulkRegexRenameChannels).mockResolvedValue( + undefined + ); vi.mocked(ChannelBatchUtils.batchSetEPG).mockResolvedValue(undefined); - vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockResolvedValue(undefined); - vi.mocked(ChannelBatchUtils.setChannelLogosFromEpg).mockResolvedValue(undefined); - vi.mocked(ChannelBatchUtils.setChannelTvgIdsFromEpg).mockResolvedValue(undefined); + vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockResolvedValue( + undefined + ); + vi.mocked(ChannelBatchUtils.setChannelLogosFromEpg).mockResolvedValue( + undefined + ); + vi.mocked(ChannelBatchUtils.setChannelTvgIdsFromEpg).mockResolvedValue( + undefined + ); vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue(null); vi.mocked(ChannelBatchUtils.getLogoChange).mockReturnValue(null); vi.mocked(ChannelBatchUtils.getStreamProfileChange).mockReturnValue(null); @@ -353,12 +393,16 @@ describe('ChannelBatchForm', () => { expect(showNotification).toHaveBeenCalledWith( expect.objectContaining({ color: 'orange' }) ); - expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('confirmation-dialog') + ).not.toBeInTheDocument(); }); it('opens confirmation dialog when at least one change is present', () => { setupMocks(); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); renderForm(); fireEvent.click(screen.getByText('Submit')); @@ -371,11 +415,13 @@ describe('ChannelBatchForm', () => { describe('warning suppression', () => { it('skips confirmation dialog and calls onSubmit directly when batch-update warning is suppressed', async () => { - const isWarningSuppressed = vi.fn().mockImplementation((key) => - key === 'batch-update-channels' - ); + const isWarningSuppressed = vi + .fn() + .mockImplementation((key) => key === 'batch-update-channels'); setupMocks({ isWarningSuppressed }); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); const onClose = vi.fn(); renderForm({ onClose }); @@ -384,7 +430,9 @@ describe('ChannelBatchForm', () => { await waitFor(() => { expect(onClose).toHaveBeenCalled(); }); - expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('confirmation-dialog') + ).not.toBeInTheDocument(); }); }); @@ -393,27 +441,33 @@ describe('ChannelBatchForm', () => { describe('confirmation dialog', () => { it('closes confirmation dialog on cancel', () => { setupMocks(); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); renderForm(); fireEvent.click(screen.getByText('Submit')); expect(screen.getByTestId('confirmation-dialog')).toBeInTheDocument(); fireEvent.click(screen.getByTestId('dialog-cancel')); - expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('confirmation-dialog') + ).not.toBeInTheDocument(); }); it('calls updateChannels on confirm', async () => { setupMocks({ formValues: { - stream_profile_id: '1', // survives: not '-1' or '0', so kept as-is after parseInt... - user_level: '-1', // deleted - is_adult: '-1', // deleted - channel_group: '', // deleted (channel_group key is removed) - logo: '', // deleted + stream_profile_id: '1', // survives: not '-1' or '0', so kept as-is after parseInt... + user_level: '-1', // deleted + is_adult: '-1', // deleted + channel_group: '', // deleted (channel_group key is removed) + logo: '', // deleted }, }); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); renderForm(); fireEvent.click(screen.getByText('Submit')); @@ -429,7 +483,9 @@ describe('ChannelBatchForm', () => { it('calls requeryChannels after successful submit', async () => { setupMocks(); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); renderForm(); fireEvent.click(screen.getByText('Submit')); @@ -442,7 +498,9 @@ describe('ChannelBatchForm', () => { it('calls onClose after successful submit', async () => { setupMocks(); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); const onClose = vi.fn(); renderForm({ onClose }); @@ -477,7 +535,7 @@ describe('ChannelBatchForm', () => { expect(ChannelBatchUtils.bulkRegexRenameChannels).toHaveBeenCalledWith( CHANNEL_IDS, 'foo', - '', // regexReplace default is '' + '', // regexReplace default is '' 'g' ); }); @@ -504,20 +562,24 @@ describe('ChannelBatchForm', () => { fireEvent.click(screen.getByText('Set Names from EPG')); - expect(screen.getByTestId('dialog-title')).toHaveTextContent('Confirm Set Names from EPG'); + expect(screen.getByTestId('dialog-title')).toHaveTextContent( + 'Confirm Set Names from EPG' + ); }); it('skips dialog and executes directly when warning is suppressed', async () => { - const isWarningSuppressed = vi.fn().mockImplementation((key) => - key === 'batch-set-names-from-epg' - ); + const isWarningSuppressed = vi + .fn() + .mockImplementation((key) => key === 'batch-set-names-from-epg'); setupMocks({ isWarningSuppressed }); renderForm(); fireEvent.click(screen.getByText('Set Names from EPG')); await waitFor(() => { - expect(ChannelBatchUtils.setChannelNamesFromEpg).toHaveBeenCalledWith(CHANNEL_IDS); + expect(ChannelBatchUtils.setChannelNamesFromEpg).toHaveBeenCalledWith( + CHANNEL_IDS + ); }); }); @@ -529,7 +591,9 @@ describe('ChannelBatchForm', () => { fireEvent.click(screen.getByTestId('dialog-confirm')); await waitFor(() => { - expect(ChannelBatchUtils.setChannelNamesFromEpg).toHaveBeenCalledWith(CHANNEL_IDS); + expect(ChannelBatchUtils.setChannelNamesFromEpg).toHaveBeenCalledWith( + CHANNEL_IDS + ); }); }); @@ -549,7 +613,9 @@ describe('ChannelBatchForm', () => { it('shows error notification when setChannelNamesFromEpg rejects', async () => { setupMocks(); - vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockRejectedValue(new Error('fail')); + vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockRejectedValue( + new Error('fail') + ); renderForm(); fireEvent.click(screen.getByText('Set Names from EPG')); @@ -583,7 +649,9 @@ describe('ChannelBatchForm', () => { fireEvent.click(screen.getByText('Set Logos from EPG')); - expect(screen.getByTestId('dialog-title')).toHaveTextContent('Confirm Set Logos from EPG'); + expect(screen.getByTestId('dialog-title')).toHaveTextContent( + 'Confirm Set Logos from EPG' + ); }); it('calls setChannelLogosFromEpg on confirm', async () => { @@ -594,13 +662,17 @@ describe('ChannelBatchForm', () => { fireEvent.click(screen.getByTestId('dialog-confirm')); await waitFor(() => { - expect(ChannelBatchUtils.setChannelLogosFromEpg).toHaveBeenCalledWith(CHANNEL_IDS); + expect(ChannelBatchUtils.setChannelLogosFromEpg).toHaveBeenCalledWith( + CHANNEL_IDS + ); }); }); it('shows error notification when setChannelLogosFromEpg rejects', async () => { setupMocks(); - vi.mocked(ChannelBatchUtils.setChannelLogosFromEpg).mockRejectedValue(new Error('fail')); + vi.mocked(ChannelBatchUtils.setChannelLogosFromEpg).mockRejectedValue( + new Error('fail') + ); renderForm(); fireEvent.click(screen.getByText('Set Logos from EPG')); @@ -634,7 +706,9 @@ describe('ChannelBatchForm', () => { fireEvent.click(screen.getByText('Set TVG-IDs from EPG')); - expect(screen.getByTestId('dialog-title')).toHaveTextContent('Confirm Set TVG-IDs from EPG'); + expect(screen.getByTestId('dialog-title')).toHaveTextContent( + 'Confirm Set TVG-IDs from EPG' + ); }); it('calls setChannelTvgIdsFromEpg on confirm', async () => { @@ -645,13 +719,17 @@ describe('ChannelBatchForm', () => { fireEvent.click(screen.getByTestId('dialog-confirm')); await waitFor(() => { - expect(ChannelBatchUtils.setChannelTvgIdsFromEpg).toHaveBeenCalledWith(CHANNEL_IDS); + expect(ChannelBatchUtils.setChannelTvgIdsFromEpg).toHaveBeenCalledWith( + CHANNEL_IDS + ); }); }); it('shows error notification when setChannelTvgIdsFromEpg rejects', async () => { setupMocks(); - vi.mocked(ChannelBatchUtils.setChannelTvgIdsFromEpg).mockRejectedValue(new Error('fail')); + vi.mocked(ChannelBatchUtils.setChannelTvgIdsFromEpg).mockRejectedValue( + new Error('fail') + ); renderForm(); fireEvent.click(screen.getByText('Set TVG-IDs from EPG')); @@ -672,7 +750,9 @@ describe('ChannelBatchForm', () => { setupMocks(); renderForm(); - fireEvent.click(screen.getAllByTestId('icon-square-plus')[0].closest('button')); + fireEvent.click( + screen.getAllByTestId('icon-square-plus')[0].closest('button') + ); expect(screen.getByTestId('channel-group-form')).toBeInTheDocument(); }); @@ -681,20 +761,28 @@ describe('ChannelBatchForm', () => { setupMocks(); renderForm(); - fireEvent.click(screen.getAllByTestId('icon-square-plus')[0].closest('button')); + fireEvent.click( + screen.getAllByTestId('icon-square-plus')[0].closest('button') + ); fireEvent.click(screen.getByText('Save Group')); - expect(screen.queryByTestId('channel-group-form')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('channel-group-form') + ).not.toBeInTheDocument(); }); it('closes channel group modal without updating selection when cancelled', () => { setupMocks(); renderForm(); - fireEvent.click(screen.getAllByTestId('icon-square-plus')[0].closest('button')); + fireEvent.click( + screen.getAllByTestId('icon-square-plus')[0].closest('button') + ); fireEvent.click(screen.getByText('Cancel Group')); - expect(screen.queryByTestId('channel-group-form')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('channel-group-form') + ).not.toBeInTheDocument(); }); }); @@ -745,7 +833,9 @@ describe('ChannelBatchForm', () => { describe('error resilience', () => { it('does not throw when requeryChannels rejects after submit', async () => { setupMocks(); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); vi.mocked(requeryChannels).mockRejectedValue(new Error('network')); renderForm(); @@ -758,7 +848,9 @@ describe('ChannelBatchForm', () => { it('does not throw when setChannelNamesFromEpg rejects and no channels', async () => { setupMocks(); - vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockRejectedValue(new Error('fail')); + vi.mocked(ChannelBatchUtils.setChannelNamesFromEpg).mockRejectedValue( + new Error('fail') + ); renderForm(); fireEvent.click(screen.getByText('Set Names from EPG')); @@ -769,12 +861,14 @@ describe('ChannelBatchForm', () => { }); }); -// ── Batch update confirmation message ────────────────────────────────────── + // ── Batch update confirmation message ────────────────────────────────────── describe('batch update confirmation message', () => { it('displays the channel count in the confirmation message', () => { setupMocks(); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); renderForm(); fireEvent.click(screen.getByText('Submit')); @@ -784,19 +878,29 @@ describe('ChannelBatchForm', () => { it('displays a single change line in the confirmation message', () => { setupMocks(); - vi.mocked(ChannelBatchUtils.getStreamProfileChange).mockReturnValue('• Stream Profile: HD Profile'); + vi.mocked(ChannelBatchUtils.getStreamProfileChange).mockReturnValue( + '• Stream Profile: HD Profile' + ); renderForm(); fireEvent.click(screen.getByText('Submit')); - expect(screen.getByTestId('dialog-message')).toHaveTextContent('• Stream Profile: HD Profile'); + expect(screen.getByTestId('dialog-message')).toHaveTextContent( + '• Stream Profile: HD Profile' + ); }); it('displays multiple change lines when several fields are changed', () => { setupMocks(); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); - vi.mocked(ChannelBatchUtils.getStreamProfileChange).mockReturnValue('• Stream Profile: HD Profile'); - vi.mocked(ChannelBatchUtils.getUserLevelChange).mockReturnValue('• User Level: Admin'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); + vi.mocked(ChannelBatchUtils.getStreamProfileChange).mockReturnValue( + '• Stream Profile: HD Profile' + ); + vi.mocked(ChannelBatchUtils.getUserLevelChange).mockReturnValue( + '• User Level: Admin' + ); renderForm(); fireEvent.click(screen.getByText('Submit')); @@ -825,22 +929,30 @@ describe('ChannelBatchForm', () => { it('uses "Apply Changes" as the confirm button label', () => { setupMocks(); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); renderForm(); fireEvent.click(screen.getByText('Submit')); - expect(screen.getByTestId('dialog-confirm')).toHaveTextContent('Apply Changes'); + expect(screen.getByTestId('dialog-confirm')).toHaveTextContent( + 'Apply Changes' + ); }); it('shows "Confirm Batch Update" as the dialog title', () => { setupMocks(); - vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue('• Channel Group: Sports'); + vi.mocked(ChannelBatchUtils.getChannelGroupChange).mockReturnValue( + '• Channel Group: Sports' + ); renderForm(); fireEvent.click(screen.getByText('Submit')); - expect(screen.getByTestId('dialog-title')).toHaveTextContent('Confirm Batch Update'); + expect(screen.getByTestId('dialog-title')).toHaveTextContent( + 'Confirm Batch Update' + ); }); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/components/forms/__tests__/ChannelGroup.test.jsx b/frontend/src/components/forms/__tests__/ChannelGroup.test.jsx index ba16d70e..b159a681 100644 --- a/frontend/src/components/forms/__tests__/ChannelGroup.test.jsx +++ b/frontend/src/components/forms/__tests__/ChannelGroup.test.jsx @@ -155,7 +155,9 @@ describe('ChannelGroup', () => { renderForm(); - expect(screen.getByTestId('modal-title')).toHaveTextContent('Channel Group'); + expect(screen.getByTestId('modal-title')).toHaveTextContent( + 'Channel Group' + ); }); }); @@ -227,7 +229,10 @@ describe('ChannelGroup', () => { it('resets the form after successful add', async () => { setupMocks(); - vi.mocked(API.addChannelGroup).mockResolvedValue({ id: 99, name: 'NewGroup' }); + vi.mocked(API.addChannelGroup).mockResolvedValue({ + id: 99, + name: 'NewGroup', + }); const form = makeFormMock('NewGroup'); vi.mocked(useForm).mockReturnValue(form); @@ -317,7 +322,10 @@ describe('ChannelGroup', () => { renderForm({ channelGroup: makeGroup() }); - expect(screen.getByTestId('alert')).toHaveAttribute('data-color', 'yellow'); + expect(screen.getByTestId('alert')).toHaveAttribute( + 'data-color', + 'yellow' + ); }); it('disables the name input for non-editable group', () => { diff --git a/frontend/src/components/forms/__tests__/Connection.test.jsx b/frontend/src/components/forms/__tests__/Connection.test.jsx index cae1b60f..0850b101 100644 --- a/frontend/src/components/forms/__tests__/Connection.test.jsx +++ b/frontend/src/components/forms/__tests__/Connection.test.jsx @@ -75,7 +75,9 @@ vi.mock('@mantine/core', () => ({ opened ? (
{title}
- + {children}
) : null, @@ -106,11 +108,7 @@ vi.mock('@mantine/core', () => ({
{children}
), TabsTab: ({ children, value, onClick }) => ( - ), @@ -134,7 +132,13 @@ vi.mock('@mantine/core', () => ({ placeholder={placeholder} data-testid={`input-${label?.toLowerCase().replace(/\s+/g, '-')}`} /> - {error && {error}} + {error && ( + + {error} + + )} ), })); @@ -152,7 +156,11 @@ const makeConnection = (overrides = {}) => ({ }, subscriptions: [ { event: 'channel_added', enabled: true, payload_template: null }, - { event: 'recording_started', enabled: true, payload_template: '{"id": "{{id}}"}' }, + { + event: 'recording_started', + enabled: true, + payload_template: '{"id": "{{id}}"}', + }, ], ...overrides, }); @@ -212,12 +220,23 @@ const renderForm = (props = {}) => { describe('ConnectionForm', () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(ConnectionUtils.createConnectIntegration).mockResolvedValue({ id: 99 }); - vi.mocked(ConnectionUtils.updateConnectIntegration).mockResolvedValue(undefined); - vi.mocked(ConnectionUtils.setConnectSubscriptions).mockResolvedValue(undefined); - vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({ url: 'https://x.com' }); + vi.mocked(ConnectionUtils.createConnectIntegration).mockResolvedValue({ + id: 99, + }); + vi.mocked(ConnectionUtils.updateConnectIntegration).mockResolvedValue( + undefined + ); + vi.mocked(ConnectionUtils.setConnectSubscriptions).mockResolvedValue( + undefined + ); + vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({ + url: 'https://x.com', + }); vi.mocked(ConnectionUtils.buildSubscriptions).mockReturnValue([]); - vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: '' }); + vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ + fieldErrors: {}, + apiError: '', + }); }); // ── Visibility ───────────────────────────────────────────────────────────── @@ -253,7 +272,11 @@ describe('ConnectionForm', () => { }); it('calls createConnectIntegration on submit', async () => { - const form = makeFormMock({ name: 'New Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'New Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); renderForm({ connection: null }); @@ -269,7 +292,11 @@ describe('ConnectionForm', () => { }); it('does not call updateConnectIntegration when creating', async () => { - const form = makeFormMock({ name: 'New Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'New Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); renderForm({ connection: null }); @@ -284,9 +311,15 @@ describe('ConnectionForm', () => { it('calls setConnectSubscriptions with newly created connection', async () => { const created = { id: 99, name: 'New Hook' }; - vi.mocked(ConnectionUtils.createConnectIntegration).mockResolvedValue(created); + vi.mocked(ConnectionUtils.createConnectIntegration).mockResolvedValue( + created + ); - const form = makeFormMock({ name: 'New Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'New Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); renderForm({ connection: null }); @@ -301,7 +334,11 @@ describe('ConnectionForm', () => { }); it('calls onClose after successful create', async () => { - const form = makeFormMock({ name: 'New Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'New Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); const onClose = vi.fn(); @@ -357,7 +394,11 @@ describe('ConnectionForm', () => { it('does not call createConnectIntegration when editing', async () => { const connection = makeConnection(); - const form = makeFormMock({ name: 'My Webhook', type: 'webhook', url: 'https://example.com/hook' }); + const form = makeFormMock({ + name: 'My Webhook', + type: 'webhook', + url: 'https://example.com/hook', + }); vi.mocked(useForm).mockReturnValue(form); renderForm({ connection }); @@ -371,7 +412,11 @@ describe('ConnectionForm', () => { it('calls setConnectSubscriptions with existing connection on update', async () => { const connection = makeConnection(); - const form = makeFormMock({ name: 'My Webhook', type: 'webhook', url: 'https://example.com/hook' }); + const form = makeFormMock({ + name: 'My Webhook', + type: 'webhook', + url: 'https://example.com/hook', + }); vi.mocked(useForm).mockReturnValue(form); renderForm({ connection }); @@ -387,7 +432,11 @@ describe('ConnectionForm', () => { it('calls onClose after successful update', async () => { const connection = makeConnection(); - const form = makeFormMock({ name: 'My Webhook', type: 'webhook', url: 'https://example.com/hook' }); + const form = makeFormMock({ + name: 'My Webhook', + type: 'webhook', + url: 'https://example.com/hook', + }); vi.mocked(useForm).mockReturnValue(form); const onClose = vi.fn(); @@ -446,7 +495,9 @@ describe('ConnectionForm', () => { script_path: '/usr/local/bin/notify.sh', }); vi.mocked(useForm).mockReturnValue(form); - vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({ path: '/usr/local/bin/notify.sh' }); + vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({ + path: '/usr/local/bin/notify.sh', + }); renderForm({ connection: null }); fireEvent.submit(screen.getByTestId('modal').querySelector('form')); @@ -485,7 +536,9 @@ describe('ConnectionForm', () => { url: 'https://example.com/webhook', }); vi.mocked(useForm).mockReturnValue(form); - vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({ url: 'https://example.com/webhook' }); + vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({ + url: 'https://example.com/webhook', + }); renderForm({ connection: null }); fireEvent.submit(screen.getByTestId('modal').querySelector('form')); @@ -507,7 +560,9 @@ describe('ConnectionForm', () => { }); vi.mocked(useForm).mockReturnValue(form); vi.mocked(ConnectionUtils.buildConfig).mockReturnValue({ - url: 'https://example.com/hook', headers: { 'X-Token': 'abc123' } }); + url: 'https://example.com/hook', + headers: { 'X-Token': 'abc123' }, + }); renderForm({ connection }); fireEvent.submit(screen.getByTestId('modal').querySelector('form')); @@ -522,7 +577,9 @@ describe('ConnectionForm', () => { }); it('omits headers from webhook config when all header rows are empty', async () => { - const connection = makeConnection({ config: { url: 'https://example.com/hook', headers: {} } }); + const connection = makeConnection({ + config: { url: 'https://example.com/hook', headers: {} }, + }); const form = makeFormMock({ name: 'My Webhook', type: 'webhook', @@ -547,15 +604,22 @@ describe('ConnectionForm', () => { describe('subscriptions', () => { it('passes subscription list with enabled flags to setConnectSubscriptions', async () => { - const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); vi.mocked(ConnectionUtils.buildSubscriptions).mockReturnValue([ { event: 'channel_added', enabled: true, payload_template: null }, { event: 'channel_removed', enabled: false, payload_template: null }, - { event: 'recording_started', enabled: true, payload_template: '{"id": "{{id}}"}' }, + { + event: 'recording_started', + enabled: true, + payload_template: '{"id": "{{id}}"}', + }, ]); - renderForm({ connection: null }); // Toggle channel_added on @@ -567,8 +631,14 @@ describe('ConnectionForm', () => { expect.any(Object), expect.arrayContaining([ expect.objectContaining({ event: 'channel_added', enabled: true }), - expect.objectContaining({ event: 'channel_removed', enabled: false }), - expect.objectContaining({ event: 'recording_started', enabled: true }), + expect.objectContaining({ + event: 'channel_removed', + enabled: false, + }), + expect.objectContaining({ + event: 'recording_started', + enabled: true, + }), ]) ); }); @@ -576,12 +646,20 @@ describe('ConnectionForm', () => { it('toggles event off when checkbox is clicked twice', async () => { const connection = makeConnection(); - const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); vi.mocked(ConnectionUtils.buildSubscriptions).mockReturnValue([ { event: 'channel_added', enabled: false, payload_template: null }, { event: 'channel_removed', enabled: false, payload_template: null }, - { event: 'recording_started', enabled: true, payload_template: '{"id": "{{id}}"}' }, + { + event: 'recording_started', + enabled: true, + payload_template: '{"id": "{{id}}"}', + }, ]); renderForm({ connection }); @@ -595,8 +673,14 @@ describe('ConnectionForm', () => { expect.any(Object), expect.arrayContaining([ expect.objectContaining({ event: 'channel_added', enabled: false }), - expect.objectContaining({ event: 'channel_removed', enabled: false }), - expect.objectContaining({ event: 'recording_started', enabled: true }), + expect.objectContaining({ + event: 'channel_removed', + enabled: false, + }), + expect.objectContaining({ + event: 'recording_started', + enabled: true, + }), ]) ); }); @@ -604,17 +688,26 @@ describe('ConnectionForm', () => { it('includes payload_template in subscription when set', async () => { const connection = makeConnection(); - const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); vi.mocked(ConnectionUtils.buildSubscriptions).mockReturnValue([ - { event: 'recording_started', enabled: true, payload_template: '{"id": "{{id}}"}' }, + { + event: 'recording_started', + enabled: true, + payload_template: '{"id": "{{id}}"}', + }, ]); renderForm({ connection }); fireEvent.submit(screen.getByTestId('modal').querySelector('form')); await waitFor(() => { - const subs = vi.mocked(ConnectionUtils.setConnectSubscriptions).mock.calls[0][1]; + const subs = vi.mocked(ConnectionUtils.setConnectSubscriptions).mock + .calls[0][1]; const recordingSub = subs.find((s) => s.event === 'recording_started'); expect(recordingSub).toBeDefined(); expect(recordingSub.payload_template).toBe('{"id": "{{id}}"}'); @@ -622,14 +715,19 @@ describe('ConnectionForm', () => { }); it('sends null payload_template when not set for an event', async () => { - const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); renderForm({ connection: null }); fireEvent.submit(screen.getByTestId('modal').querySelector('form')); await waitFor(() => { - const subs = vi.mocked(ConnectionUtils.setConnectSubscriptions).mock.calls[0][1]; + const subs = vi.mocked(ConnectionUtils.setConnectSubscriptions).mock + .calls[0][1]; subs.forEach((s) => expect(s.payload_template).toBeNull()); }); }); @@ -642,9 +740,16 @@ describe('ConnectionForm', () => { vi.mocked(ConnectionUtils.createConnectIntegration).mockRejectedValue( new Error('Server error') ); - const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); - vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: 'Server error' }); + vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ + fieldErrors: {}, + apiError: 'Server error', + }); renderForm({ connection: null }); fireEvent.submit(screen.getByTestId('modal').querySelector('form')); @@ -659,9 +764,16 @@ describe('ConnectionForm', () => { new Error('Update failed') ); const connection = makeConnection(); - const form = makeFormMock({ name: 'My Webhook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'My Webhook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); - vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: 'Update failed' }); + vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ + fieldErrors: {}, + apiError: 'Update failed', + }); renderForm({ connection }); fireEvent.submit(screen.getByTestId('modal').querySelector('form')); @@ -675,9 +787,16 @@ describe('ConnectionForm', () => { vi.mocked(ConnectionUtils.createConnectIntegration).mockRejectedValue( new Error('fail') ); - const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); - vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: 'fail' }); + vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ + fieldErrors: {}, + apiError: 'fail', + }); const onClose = vi.fn(); renderForm({ connection: null, onClose }); @@ -694,19 +813,30 @@ describe('ConnectionForm', () => { .mockRejectedValueOnce(new Error('First error')) .mockResolvedValueOnce({ id: 99 }); - const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); - vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: 'First error' }); + vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ + fieldErrors: {}, + apiError: 'First error', + }); renderForm({ connection: null }); // First submit — fails fireEvent.submit(screen.getByTestId('modal').querySelector('form')); - await waitFor(() => expect(screen.queryByText('First error')).toBeInTheDocument()); + await waitFor(() => + expect(screen.queryByText('First error')).toBeInTheDocument() + ); // Second submit — succeeds fireEvent.submit(screen.getByTestId('modal').querySelector('form')); - await waitFor(() => expect(screen.queryByText('First error')).not.toBeInTheDocument()); + await waitFor(() => + expect(screen.queryByText('First error')).not.toBeInTheDocument() + ); }); }); @@ -727,18 +857,29 @@ describe('ConnectionForm', () => { vi.mocked(ConnectionUtils.createConnectIntegration).mockRejectedValue( new Error('fail') ); - const form = makeFormMock({ name: 'Hook', type: 'webhook', url: 'https://x.com' }); + const form = makeFormMock({ + name: 'Hook', + type: 'webhook', + url: 'https://x.com', + }); vi.mocked(useForm).mockReturnValue(form); - vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ fieldErrors: {}, apiError: 'fail' }); + vi.mocked(ConnectionUtils.parseApiError).mockReturnValue({ + fieldErrors: {}, + apiError: 'fail', + }); renderForm({ connection: null }); fireEvent.submit(screen.getByTestId('modal').querySelector('form')); - await waitFor(() => expect(screen.queryByText('fail')).toBeInTheDocument()); + await waitFor(() => + expect(screen.queryByText('fail')).toBeInTheDocument() + ); fireEvent.click(screen.getByTestId('modal-close')); - await waitFor(() => expect(screen.queryByText('fail')).not.toBeInTheDocument()); + await waitFor(() => + expect(screen.queryByText('fail')).not.toBeInTheDocument() + ); }); }); diff --git a/frontend/src/components/forms/__tests__/CronBuilder.test.jsx b/frontend/src/components/forms/__tests__/CronBuilder.test.jsx index 26584384..66ec77ac 100644 --- a/frontend/src/components/forms/__tests__/CronBuilder.test.jsx +++ b/frontend/src/components/forms/__tests__/CronBuilder.test.jsx @@ -7,27 +7,35 @@ vi.mock('../../../utils/forms/CronBuilderUtils.js', () => ({ buildCron: vi.fn(), parseCronPreset: vi.fn(), CRON_FIELDS: [ - { index: 0, label: 'Minute (0-59)', placeholder: '*, 0, */15' }, - { index: 1, label: 'Hour (0-23)', placeholder: '*, 0, 9-17' }, - { index: 2, label: 'Day of Month (1-31)', placeholder: '*, 1, 1-15' }, - { index: 3, label: 'Month (1-12)', placeholder: '*, 1, 1-6' }, - { index: 4, label: 'Day of Week (0-6, Sun-Sat)', placeholder: '*, 0, 1-5' }, + { index: 0, label: 'Minute (0-59)', placeholder: '*, 0, */15' }, + { index: 1, label: 'Hour (0-23)', placeholder: '*, 0, 9-17' }, + { index: 2, label: 'Day of Month (1-31)', placeholder: '*, 1, 1-15' }, + { index: 3, label: 'Month (1-12)', placeholder: '*, 1, 1-6' }, + { index: 4, label: 'Day of Week (0-6, Sun-Sat)', placeholder: '*, 0, 1-5' }, ], DAYS_OF_WEEK: [ - { value: '*', label: 'Every day' }, - { value: '1', label: 'Monday' }, - { value: '2', label: 'Tuesday' }, + { value: '*', label: 'Every day' }, + { value: '1', label: 'Monday' }, + { value: '2', label: 'Tuesday' }, ], FREQUENCY_OPTIONS: [ - { value: 'hourly', label: 'Hourly' }, - { value: 'daily', label: 'Daily' }, - { value: 'weekly', label: 'Weekly' }, + { value: 'hourly', label: 'Hourly' }, + { value: 'daily', label: 'Daily' }, + { value: 'weekly', label: 'Weekly' }, { value: 'monthly', label: 'Monthly' }, ], PRESETS: [ - { label: 'Every Hour', description: 'Runs every hour', value: '0 * * * *' }, - { label: 'Every Day 3am', description: 'Runs every day at 3 AM', value: '0 3 * * *' }, - { label: 'Every Sunday', description: 'Runs every Sunday at 3AM', value: '0 3 * * 0' }, + { label: 'Every Hour', description: 'Runs every hour', value: '0 * * * *' }, + { + label: 'Every Day 3am', + description: 'Runs every day at 3 AM', + value: '0 3 * * *', + }, + { + label: 'Every Sunday', + description: 'Runs every Sunday at 3AM', + value: '0 3 * * 0', + }, ], updateCronPart: vi.fn(), })); @@ -35,7 +43,12 @@ vi.mock('../../../utils/forms/CronBuilderUtils.js', () => ({ // ── Mantine core ─────────────────────────────────────────────────────────────── vi.mock('@mantine/core', async () => ({ Badge: ({ children, size, variant, color }) => ( - + {children} ), @@ -51,7 +64,9 @@ vi.mock('@mantine/core', async () => ({ opened ? (
{title}
- + {children}
) : null, @@ -122,7 +137,7 @@ vi.mock('@mantine/core', async () => ({ // ── lucide-react ─────────────────────────────────────────────────────────────── vi.mock('lucide-react', () => ({ Calendar: () => , - Clock: () => , + Clock: () => , })); // ── Imports after mocks ──────────────────────────────────────────────────────── @@ -241,7 +256,9 @@ describe('CronBuilder', () => { it('calls parseCronPreset with the preset value on click', () => { renderBuilder(); fireEvent.click(screen.getByText('Every Hour')); - expect(CronBuilderUtils.parseCronPreset).toHaveBeenCalledWith('0 * * * *'); + expect(CronBuilderUtils.parseCronPreset).toHaveBeenCalledWith( + '0 * * * *' + ); }); it('updates state from parseCronPreset result on preset click', () => { @@ -256,7 +273,11 @@ describe('CronBuilder', () => { fireEvent.click(screen.getByText('Every Hour')); // buildCron should be called with the updated frequency expect(CronBuilderUtils.buildCron).toHaveBeenCalledWith( - 'hourly', 0, 0, '*', 1 + 'hourly', + 0, + 0, + '*', + 1 ); }); }); @@ -268,56 +289,81 @@ describe('CronBuilder', () => { it('renders the Frequency select', () => { renderBuilder(); - expect(within(getSimplePanel()).getByLabelText('Frequency')).toBeInTheDocument(); + expect( + within(getSimplePanel()).getByLabelText('Frequency') + ).toBeInTheDocument(); }); it('renders the Minute input', () => { renderBuilder(); - expect(within(getSimplePanel()).getByLabelText('Minute (0-59)')).toBeInTheDocument(); + expect( + within(getSimplePanel()).getByLabelText('Minute (0-59)') + ).toBeInTheDocument(); }); it('renders the Hour input when frequency is not hourly', () => { renderBuilder(); - expect(within(getSimplePanel()).getByLabelText('Hour (0-23)')).toBeInTheDocument(); + expect( + within(getSimplePanel()).getByLabelText('Hour (0-23)') + ).toBeInTheDocument(); }); it('does not render Hour input when frequency is hourly', () => { renderBuilder(); const freqSelect = within(getSimplePanel()).getByLabelText('Frequency'); fireEvent.change(freqSelect, { target: { value: 'hourly' } }); - expect(within(getSimplePanel()).queryByLabelText('Hour (0-23)')).not.toBeInTheDocument(); + expect( + within(getSimplePanel()).queryByLabelText('Hour (0-23)') + ).not.toBeInTheDocument(); }); it('renders Day of Week select when frequency is weekly', () => { renderBuilder(); const freqSelect = within(getSimplePanel()).getByLabelText('Frequency'); fireEvent.change(freqSelect, { target: { value: 'weekly' } }); - expect(within(getSimplePanel()).getByLabelText('Day of Week')).toBeInTheDocument(); + expect( + within(getSimplePanel()).getByLabelText('Day of Week') + ).toBeInTheDocument(); }); it('does not render Day of Week for daily frequency', () => { renderBuilder(); - expect(within(getSimplePanel()).queryByLabelText('Day of Week')).not.toBeInTheDocument(); + expect( + within(getSimplePanel()).queryByLabelText('Day of Week') + ).not.toBeInTheDocument(); }); it('renders Day of Month input when frequency is monthly', () => { renderBuilder(); const freqSelect = within(getSimplePanel()).getByLabelText('Frequency'); fireEvent.change(freqSelect, { target: { value: 'monthly' } }); - expect(within(getSimplePanel()).getByLabelText('Day of Month (1-31)')).toBeInTheDocument(); + expect( + within(getSimplePanel()).getByLabelText('Day of Month (1-31)') + ).toBeInTheDocument(); }); it('does not render Day of Month for daily frequency', () => { renderBuilder(); - expect(within(getSimplePanel()).queryByLabelText('Day of Month (1-31)')).not.toBeInTheDocument(); + expect( + within(getSimplePanel()).queryByLabelText('Day of Month (1-31)') + ).not.toBeInTheDocument(); }); it('calls buildCron when minute changes', () => { renderBuilder(); - fireEvent.change(within(getSimplePanel()).getByLabelText('Minute (0-59)'), { - target: { value: 30 }, - }); - expect(CronBuilderUtils.buildCron).toHaveBeenCalledWith('daily', 30, 3, '*', 1); + fireEvent.change( + within(getSimplePanel()).getByLabelText('Minute (0-59)'), + { + target: { value: 30 }, + } + ); + expect(CronBuilderUtils.buildCron).toHaveBeenCalledWith( + 'daily', + 30, + 3, + '*', + 1 + ); }); it('calls buildCron when hour changes', () => { @@ -325,7 +371,13 @@ describe('CronBuilder', () => { fireEvent.change(within(getSimplePanel()).getByLabelText('Hour (0-23)'), { target: { value: 8 }, }); - expect(CronBuilderUtils.buildCron).toHaveBeenCalledWith('daily', 0, 8, '*', 1); + expect(CronBuilderUtils.buildCron).toHaveBeenCalledWith( + 'daily', + 0, + 8, + '*', + 1 + ); }); }); @@ -336,31 +388,47 @@ describe('CronBuilder', () => { it('renders all 5 cron field inputs', () => { renderBuilder(); - expect(within(getAdvancedPanel()).getByLabelText('Minute (0-59)')).toBeInTheDocument(); - expect(within(getAdvancedPanel()).getByLabelText('Hour (0-23)')).toBeInTheDocument(); - expect(within(getAdvancedPanel()).getByLabelText('Day of Month (1-31)')).toBeInTheDocument(); - expect(within(getAdvancedPanel()).getByLabelText('Month (1-12)')).toBeInTheDocument(); - expect(within(getAdvancedPanel()).getByLabelText('Day of Week (0-6, Sun-Sat)')).toBeInTheDocument(); + expect( + within(getAdvancedPanel()).getByLabelText('Minute (0-59)') + ).toBeInTheDocument(); + expect( + within(getAdvancedPanel()).getByLabelText('Hour (0-23)') + ).toBeInTheDocument(); + expect( + within(getAdvancedPanel()).getByLabelText('Day of Month (1-31)') + ).toBeInTheDocument(); + expect( + within(getAdvancedPanel()).getByLabelText('Month (1-12)') + ).toBeInTheDocument(); + expect( + within(getAdvancedPanel()).getByLabelText('Day of Week (0-6, Sun-Sat)') + ).toBeInTheDocument(); }); it('renders descriptive helper text', () => { renderBuilder(); - expect(screen.getByText(/Build advanced cron expressions/)).toBeInTheDocument(); + expect( + screen.getByText(/Build advanced cron expressions/) + ).toBeInTheDocument(); }); it('calls updateCronPart when an advanced field changes', () => { renderBuilder(); - const minuteInput = within(getAdvancedPanel()).getByLabelText('Minute (0-59)'); + const minuteInput = + within(getAdvancedPanel()).getByLabelText('Minute (0-59)'); fireEvent.change(minuteInput, { target: { value: '30' } }); expect(CronBuilderUtils.updateCronPart).toHaveBeenCalledWith( - '* * * * *', 0, '30' + '* * * * *', + 0, + '30' ); }); it('initializes advanced fields from currentValue when opened', () => { renderBuilder({ currentValue: '5 4 * * 1' }); // The minute field (index 0) should display '5' - const minuteInput = within(getAdvancedPanel()).getByLabelText('Minute (0-59)'); + const minuteInput = + within(getAdvancedPanel()).getByLabelText('Minute (0-59)'); expect(minuteInput).toHaveValue('5'); }); }); @@ -416,7 +484,8 @@ describe('CronBuilder', () => { // Switch to advanced mode by simulating tab onChange // The Tabs mock renders but doesn't fire onChange on tab clicks, // so we directly test that advanced panel input changes manualCron - const minuteInput = within(getAdvancedPanel()).getByLabelText('Minute (0-59)'); + const minuteInput = + within(getAdvancedPanel()).getByLabelText('Minute (0-59)'); // The advanced panel's minute input value should be '5' expect(minuteInput).toHaveValue('5'); }); @@ -429,13 +498,15 @@ describe('CronBuilder', () => { it('sets manualCron to currentValue when opened', () => { renderBuilder({ currentValue: '*/15 * * * *' }); - const minuteInput = within(getAdvancedPanel()).getByLabelText('Minute (0-59)'); + const minuteInput = + within(getAdvancedPanel()).getByLabelText('Minute (0-59)'); expect(minuteInput).toHaveValue('*/15'); }); it('keeps default manualCron when currentValue is empty', () => { renderBuilder({ currentValue: '' }); - const minuteInput = within(getAdvancedPanel()).getByLabelText('Minute (0-59)'); + const minuteInput = + within(getAdvancedPanel()).getByLabelText('Minute (0-59)'); expect(minuteInput).toHaveValue('*'); }); @@ -444,4 +515,4 @@ describe('CronBuilder', () => { expect(screen.queryByTestId('modal')).not.toBeInTheDocument(); }); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/components/forms/__tests__/DummyEPG.test.jsx b/frontend/src/components/forms/__tests__/DummyEPG.test.jsx index d7a55446..edd2ac1b 100644 --- a/frontend/src/components/forms/__tests__/DummyEPG.test.jsx +++ b/frontend/src/components/forms/__tests__/DummyEPG.test.jsx @@ -27,58 +27,58 @@ vi.mock('../../../utils/forms/DummyEpgUtils.js', () => ({ const fill = (tpl) => tpl ? Object.entries(allGroups).reduce( - (s, [k, v]) => s.replaceAll(`{${k}}`, v), - tpl - ) + (s, [k, v]) => s.replaceAll(`{${k}}`, v), + tpl + ) : ''; return { - formattedTitle: fill(templates.titleTemplate), - formattedSubtitle: fill(templates.subtitleTemplate), - formattedDescription: fill(templates.descriptionTemplate), - formattedUpcomingTitle: fill(templates.upcomingTitleTemplate), - formattedUpcomingDescription: fill(templates.upcomingDescriptionTemplate), - formattedEndedTitle: fill(templates.endedTitleTemplate), - formattedEndedDescription: fill(templates.endedDescriptionTemplate), - formattedChannelLogoUrl: fill(templates.channelLogoUrl), - formattedProgramPosterUrl: fill(templates.programPosterUrl), + formattedTitle: fill(templates.titleTemplate), + formattedSubtitle: fill(templates.subtitleTemplate), + formattedDescription: fill(templates.descriptionTemplate), + formattedUpcomingTitle: fill(templates.upcomingTitleTemplate), + formattedUpcomingDescription: fill(templates.upcomingDescriptionTemplate), + formattedEndedTitle: fill(templates.endedTitleTemplate), + formattedEndedDescription: fill(templates.endedDescriptionTemplate), + formattedChannelLogoUrl: fill(templates.channelLogoUrl), + formattedProgramPosterUrl: fill(templates.programPosterUrl), }; }), buildCustomProperties: vi.fn((custom = {}) => ({ - title_pattern: custom.title_pattern || '', - time_pattern: custom.time_pattern || '', - date_pattern: custom.date_pattern || '', - timezone: custom.timezone || 'US/Eastern', - output_timezone: custom.output_timezone || '', - program_duration: custom.program_duration || 180, - sample_title: custom.sample_title || '', - title_template: custom.title_template || '', - subtitle_template: custom.subtitle_template || '', - description_template: custom.description_template || '', - upcoming_title_template: custom.upcoming_title_template || '', - upcoming_description_template:custom.upcoming_description_template|| '', - ended_title_template: custom.ended_title_template || '', - ended_description_template: custom.ended_description_template || '', - fallback_title_template: custom.fallback_title_template || '', - fallback_description_template:custom.fallback_description_template|| '', - channel_logo_url: custom.channel_logo_url || '', - program_poster_url: custom.program_poster_url || '', - name_source: custom.name_source || 'channel', - stream_index: custom.stream_index || 1, - category: custom.category || '', - include_date: custom.include_date ?? true, - include_live: custom.include_live ?? false, - include_new: custom.include_new ?? false, + title_pattern: custom.title_pattern || '', + time_pattern: custom.time_pattern || '', + date_pattern: custom.date_pattern || '', + timezone: custom.timezone || 'US/Eastern', + output_timezone: custom.output_timezone || '', + program_duration: custom.program_duration || 180, + sample_title: custom.sample_title || '', + title_template: custom.title_template || '', + subtitle_template: custom.subtitle_template || '', + description_template: custom.description_template || '', + upcoming_title_template: custom.upcoming_title_template || '', + upcoming_description_template: custom.upcoming_description_template || '', + ended_title_template: custom.ended_title_template || '', + ended_description_template: custom.ended_description_template || '', + fallback_title_template: custom.fallback_title_template || '', + fallback_description_template: custom.fallback_description_template || '', + channel_logo_url: custom.channel_logo_url || '', + program_poster_url: custom.program_poster_url || '', + name_source: custom.name_source || 'channel', + stream_index: custom.stream_index || 1, + category: custom.category || '', + include_date: custom.include_date ?? true, + include_live: custom.include_live ?? false, + include_new: custom.include_new ?? false, })), buildTimePlaceholders: vi.fn((timeGroups) => { if (!timeGroups || !timeGroups.hour) return {}; - const hour = parseInt(timeGroups.hour, 10); + const hour = parseInt(timeGroups.hour, 10); const minute = String(timeGroups.minute ?? '00').padStart(2, '0'); - const ampm = timeGroups.ampm ? ` ${timeGroups.ampm}` : ''; + const ampm = timeGroups.ampm ? ` ${timeGroups.ampm}` : ''; return { - starttime: `${hour}:${minute}${ampm}`, + starttime: `${hour}:${minute}${ampm}`, starttime24: `${String(hour).padStart(2, '0')}:${minute}`, - endtime: `${(hour + 3) % 24}:${minute}${ampm}`, - endtime24: `${String((hour + 3) % 24).padStart(2, '0')}:${minute}`, + endtime: `${(hour + 3) % 24}:${minute}${ampm}`, + endtime24: `${String((hour + 3) % 24).padStart(2, '0')}:${minute}`, }; }), getDummyEpgFormInitialValues: vi.fn(() => ({ @@ -86,30 +86,30 @@ vi.mock('../../../utils/forms/DummyEpgUtils.js', () => ({ is_active: true, source_type: 'dummy', custom_properties: { - title_pattern: '', - time_pattern: '', - date_pattern: '', - timezone: 'US/Eastern', - output_timezone: '', - program_duration: 180, - sample_title: '', - title_template: '', - subtitle_template: '', - description_template: '', - upcoming_title_template: '', - upcoming_description_template:'', - ended_title_template: '', - ended_description_template: '', - fallback_title_template: '', - fallback_description_template:'', - channel_logo_url: '', - program_poster_url: '', - name_source: 'channel', - stream_index: 1, - category: '', - include_date: true, - include_live: false, - include_new: false, + title_pattern: '', + time_pattern: '', + date_pattern: '', + timezone: 'US/Eastern', + output_timezone: '', + program_duration: 180, + sample_title: '', + title_template: '', + subtitle_template: '', + description_template: '', + upcoming_title_template: '', + upcoming_description_template: '', + ended_title_template: '', + ended_description_template: '', + fallback_title_template: '', + fallback_description_template: '', + channel_logo_url: '', + program_poster_url: '', + name_source: 'channel', + stream_index: 1, + category: '', + include_date: true, + include_live: false, + include_new: false, }, })), getTimezones: vi.fn().mockResolvedValue({ @@ -118,19 +118,23 @@ vi.mock('../../../utils/forms/DummyEpgUtils.js', () => ({ matchPattern: vi.fn((pattern, sample, errorLabel = 'Pattern error') => { if (!pattern) return { matched: false, groups: {}, error: null }; try { - const regex = new RegExp(pattern, 'u'); + const regex = new RegExp(pattern, 'u'); const result = regex.exec(sample ?? ''); if (!result) return { matched: false, groups: {}, error: null }; const groups = result.groups ?? {}; return { matched: true, groups, error: null }; } catch { - return { matched: false, groups: {}, error: `${errorLabel}: invalid regex` }; + return { + matched: false, + groups: {}, + error: `${errorLabel}: invalid regex`, + }; } }), updateEPG: vi.fn(), - validateCustomNameSource: vi.fn(() => null), - validateCustomStreamIndex: vi.fn(() => null), - validateCustomTitlePattern: vi.fn(() => null), + validateCustomNameSource: vi.fn(() => null), + validateCustomStreamIndex: vi.fn(() => null), + validateCustomTitlePattern: vi.fn(() => null), })); // ── Mantine notifications ────────────────────────────────────────────────────── @@ -141,13 +145,25 @@ vi.mock('@mantine/notifications', () => ({ // ── Mantine core ─────────────────────────────────────────────────────────────── vi.mock('@mantine/core', async () => ({ Accordion: ({ children }) =>
{children}
, - AccordionControl: ({ children }) => , - AccordionItem: ({ children, value }) =>
{children}
, - AccordionPanel: ({ children }) =>
{children}
, - ActionIcon: ({ children, onClick }) => ( - + AccordionControl: ({ children }) => ( + + ), + AccordionItem: ({ children, value }) => ( +
{children}
+ ), + AccordionPanel: ({ children }) => ( +
{children}
+ ), + ActionIcon: ({ children, onClick }) => ( + + ), + Box: ({ children, mt, style }) => ( +
+ {children} +
), - Box: ({ children, mt, style }) =>
{children}
, Button: ({ children, onClick, disabled, loading, type, color, variant }) => ( + {children} ) : null, @@ -196,7 +214,9 @@ vi.mock('@mantine/core', async () => ({ ), Paper: ({ children }) =>
{children}
, Popover: ({ children }) =>
{children}
, - PopoverDropdown: ({ children }) =>
{children}
, + PopoverDropdown: ({ children }) => ( +
{children}
+ ), PopoverTarget: ({ children }) =>
{children}
, Select: ({ label, value, onChange, data, placeholder }) => ( ), Stack: ({ children }) =>
{children}
, Text: ({ children, size, c, fw, style }) => ( - {children} + + {children} + ), Textarea: ({ label, value, onChange, placeholder }) => (