From dafdb1a19b0fae1420212f1fde2148af1745ee32 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 30 Jun 2026 16:49:30 -0500 Subject: [PATCH] refactor(tests): Improve formatting and structure in ChannelsTable tests; streamline requeryChannels function in ChannelUtils --- .../tables/__tests__/ChannelsTable.test.jsx | 233 ++++++++++++++---- frontend/src/utils/forms/ChannelUtils.js | 4 +- .../forms/__tests__/ChannelUtils.test.js | 13 +- 3 files changed, 198 insertions(+), 52 deletions(-) diff --git a/frontend/src/components/tables/__tests__/ChannelsTable.test.jsx b/frontend/src/components/tables/__tests__/ChannelsTable.test.jsx index a3e20609..1d36f682 100644 --- a/frontend/src/components/tables/__tests__/ChannelsTable.test.jsx +++ b/frontend/src/components/tables/__tests__/ChannelsTable.test.jsx @@ -1,5 +1,11 @@ import React from 'react'; -import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; +import { + render, + screen, + fireEvent, + waitFor, + within, +} from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; // ── DnD Kit ──────────────────────────────────────────────────────────────────── @@ -79,7 +85,10 @@ vi.mock('../CustomTable', () => ({ vi.mock('../ChannelsTable/ChannelsTableOnboarding', () => ({ default: ({ editChannel }) => (
-
@@ -99,7 +108,10 @@ vi.mock('../ChannelsTable/ChannelTableHeader', () => ({ setShowOnlyOverriddenChannels, }) => (
- @@ -125,7 +139,9 @@ vi.mock('../ChannelsTable/ChannelTableHeader', () => ({ @@ -175,7 +191,15 @@ vi.mock('../../forms/Recording', () => ({ ) : null, })); vi.mock('../../ConfirmationDialog', () => ({ - default: ({ opened, onClose, onConfirm, title, loading, confirmLabel, cancelLabel }) => + default: ({ + opened, + onClose, + onConfirm, + title, + loading, + confirmLabel, + cancelLabel, + }) => opened ? (
{title} @@ -205,7 +229,11 @@ vi.mock('@mantine/core', () => ({
), Button: ({ children, onClick, leftSection, disabled, loading }) => ( - @@ -236,27 +264,57 @@ vi.mock('@mantine/core', () => ({ ), MenuTarget: ({ children }) =>
{children}
, MultiSelect: ({ value, data }) => ( - {}} + > {(data || []).map((d) => ( - + ))} ), NativeSelect: ({ value, data, onChange }) => ( - {(data || []).map((d) => ( - + ))} ), NumberInput: ({ value, onChange }) => ( - onChange(Number(e.target.value))} /> + onChange(Number(e.target.value))} + /> ), Pagination: ({ total, value, onChange }) => (
- + {value} - +
), Paper: ({ children, style }) =>
{children}
, @@ -264,11 +322,17 @@ vi.mock('@mantine/core', () => ({ ({ children }) =>
{children}
, { Target: ({ children }) =>
{children}
, - Dropdown: ({ children }) =>
{children}
, + Dropdown: ({ children }) => ( +
{children}
+ ), } ), PopoverDropdown: ({ children, onClick, onMouseDown }) => ( -
+
{children}
), @@ -281,7 +345,9 @@ vi.mock('@mantine/core', () => ({ > {(data || []).map((d) => ( - + ))} ), @@ -297,7 +363,9 @@ vi.mock('@mantine/core', () => ({ /> ), Text: ({ children, style }) => ( - {children} + + {children} + ), TextInput: ({ value, placeholder, onChange, readOnly }) => ( ({ ), Tooltip: ({ children, label }) =>
{children}
, UnstyledButton: ({ children, onClick }) => ( - + ), useMantineTheme: vi.fn(() => ({ - tailwind: { yellow: { 3: '#fde047' }, red: { 6: '#dc2626' }, green: { 5: '#22c55e' } }, + tailwind: { + yellow: { 3: '#fde047' }, + red: { 6: '#dc2626' }, + green: { 5: '#22c55e' }, + }, palette: { - custom: { greenMain: '#22c55e', indigoMain: '#6366f1', greyBorder: '#71717a' }, + custom: { + greenMain: '#22c55e', + indigoMain: '#6366f1', + greyBorder: '#71717a', + }, }, })), })); @@ -353,7 +431,12 @@ import useOutputProfilesStore from '../../../store/outputProfiles'; import useWarningsStore from '../../../store/warnings'; import useLocalStorage from '../../../hooks/useLocalStorage'; import { useTable } from '../CustomTable'; -import { deleteChannel, deleteChannels, queryChannels, getAllChannelIds } from '../../../utils/tables/ChannelsTableUtils.js'; +import { + deleteChannel, + deleteChannels, + queryChannels, + getAllChannelIds, +} from '../../../utils/tables/ChannelsTableUtils.js'; import { requeryChannels } from '../../../utils/forms/ChannelUtils.js'; import { copyToClipboard } from '../../../utils'; import { buildLiveStreamUrl } from '../../../utils/components/FloatingVideoUtils.js'; @@ -433,9 +516,7 @@ const setupMocks = ({ }) ); - vi.mocked(useAuthStore).mockImplementation((sel) => - sel({ user: authUser }) - ); + vi.mocked(useAuthStore).mockImplementation((sel) => sel({ user: authUser })); vi.mocked(useEPGsStore).mockImplementation((sel) => sel({ epgs, tvgsById: {}, tvgsLoaded: true }) @@ -471,8 +552,7 @@ const setupMocks = ({ const getActionsCol = () => capturedTableOptions.columns.find((c) => c.id === 'actions'); -const getCol = (id) => - capturedTableOptions.columns.find((c) => c.id === id); +const getCol = (id) => capturedTableOptions.columns.find((c) => c.id === id); // ══════════════════════════════════════════════════════════════════════════════ // Tests @@ -516,10 +596,17 @@ describe('ChannelsTable', () => { tableOverrides: { getRowModel: () => ({ rows: [] }) }, }); vi.mocked(useChannelsStore).mockImplementation((sel) => - sel({ channelIds: [], profiles: {}, selectedProfileId: '0', channelGroups: {} }) + sel({ + channelIds: [], + profiles: {}, + selectedProfileId: '0', + channelGroups: {}, + }) ); render(); - await waitFor(() => expect(screen.getByTestId('onboarding')).toBeInTheDocument()); + await waitFor(() => + expect(screen.getByTestId('onboarding')).toBeInTheDocument() + ); }); it('renders pagination controls when channels exist', () => { @@ -633,7 +720,12 @@ describe('ChannelsTable', () => { tableOverrides: { getRowModel: () => ({ rows: [] }) }, }); vi.mocked(useChannelsStore).mockImplementation((sel) => - sel({ channelIds: [], profiles: {}, selectedProfileId: '0', channelGroups: {} }) + sel({ + channelIds: [], + profiles: {}, + selectedProfileId: '0', + channelGroups: {}, + }) ); render(); const addBtn = await screen.findByTestId('onboarding-add'); @@ -665,7 +757,9 @@ describe('ChannelsTable', () => { const { tableInstance } = setupMocks({ authUser: makeAdminUser() }); render(); const { getByTestId } = renderActionCell(channel, tableInstance); - expect(getByTestId('icon-square-pen').closest('button')).not.toBeDisabled(); + expect( + getByTestId('icon-square-pen').closest('button') + ).not.toBeDisabled(); }); it('clicking edit button opens channel form populated with the channel', () => { @@ -702,7 +796,9 @@ describe('ChannelsTable', () => { const { tableInstance } = setupMocks({ authUser: makeAdminUser() }); render(); const { getByTestId } = renderActionCell(channel, tableInstance); - expect(getByTestId('icon-square-minus').closest('button')).not.toBeDisabled(); + expect( + getByTestId('icon-square-minus').closest('button') + ).not.toBeDisabled(); }); it('opens confirmation dialog when delete is clicked and warning is not suppressed', () => { @@ -820,9 +916,38 @@ describe('ChannelsTable', () => { render(); fireEvent.click(screen.getByTestId('header-delete-channels')); fireEvent.click(screen.getByTestId('confirm-ok')); - await waitFor(() => - expect(deleteChannels).toHaveBeenCalledWith([1, 2]) - ); + await waitFor(() => expect(deleteChannels).toHaveBeenCalledWith([1, 2])); + }); + + it('awaits requeryChannels before clearing bulk selection', async () => { + const channels = [makeChannel({ id: 1 }), makeChannel({ id: 2 })]; + const setSelectedTableIds = vi.fn(); + let resolveRequery; + const requeryPromise = new Promise((resolve) => { + resolveRequery = resolve; + }); + vi.mocked(requeryChannels).mockReturnValue(requeryPromise); + + setupMocks({ + channels, + tableOverrides: { + getRowModel: vi.fn(() => ({ rows: [] })), + getHeaderGroups: vi.fn(() => []), + setSelectedTableIds, + selectedTableIds: [1, 2], + }, + isWarningSuppressed: vi.fn(() => false), + }); + render(); + fireEvent.click(screen.getByTestId('header-delete-channels')); + fireEvent.click(screen.getByTestId('confirm-ok')); + + await waitFor(() => expect(deleteChannels).toHaveBeenCalledWith([1, 2])); + expect(requeryChannels).toHaveBeenCalled(); + expect(setSelectedTableIds).not.toHaveBeenCalled(); + + resolveRequery(); + await waitFor(() => expect(setSelectedTableIds).toHaveBeenCalledWith([])); }); it('skips dialog and calls deleteChannels directly when warning is suppressed', async () => { @@ -839,9 +964,7 @@ describe('ChannelsTable', () => { }); render(); fireEvent.click(screen.getByTestId('header-delete-channels')); - await waitFor(() => - expect(deleteChannels).toHaveBeenCalledWith([1, 2]) - ); + await waitFor(() => expect(deleteChannels).toHaveBeenCalledWith([1, 2])); expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument(); }); }); @@ -856,7 +979,9 @@ describe('ChannelsTable', () => { vi.mocked(useVideoStore).mockImplementation((sel) => sel({ showVideo: showVideoMock }) ); - vi.mocked(buildLiveStreamUrl).mockReturnValue('/proxy/ts/stream/uuid-abc'); + vi.mocked(buildLiveStreamUrl).mockReturnValue( + '/proxy/ts/stream/uuid-abc' + ); render(); const col = getActionsCol(); const { getByTestId } = render( @@ -884,10 +1009,14 @@ describe('ChannelsTable', () => { ); // Record menu item is the second menu-item (after Copy URL) const menuItems = getAllByTestId('menu-item'); - const recordItem = menuItems.find((el) => el.textContent.includes('Record')); + const recordItem = menuItems.find((el) => + el.textContent.includes('Record') + ); fireEvent.click(recordItem); expect(screen.getByTestId('recording-form')).toBeInTheDocument(); - expect(screen.getByTestId('recording-form-channel')).toHaveTextContent('CNN'); + expect(screen.getByTestId('recording-form-channel')).toHaveTextContent( + 'CNN' + ); }); it('closes recording form when onClose is called', () => { @@ -899,7 +1028,9 @@ describe('ChannelsTable', () => { col.cell({ row: { original: channel }, table: tableInstance }) ); const menuItems = getAllByTestId('menu-item'); - const recordItem = menuItems.find((el) => el.textContent.includes('Record')); + const recordItem = menuItems.find((el) => + el.textContent.includes('Record') + ); fireEvent.click(recordItem); fireEvent.click(screen.getByTestId('recording-form-close')); expect(screen.queryByTestId('recording-form')).not.toBeInTheDocument(); @@ -918,7 +1049,9 @@ describe('ChannelsTable', () => { col.cell({ row: { original: channel }, table: tableInstance }) ); const menuItems = getAllByTestId('unstyled-button'); - const copyBtn = menuItems.find((el) => el.textContent.includes('Copy URL')); + const copyBtn = menuItems.find((el) => + el.textContent.includes('Copy URL') + ); fireEvent.click(copyBtn); expect(copyToClipboard).toHaveBeenCalled(); }); @@ -949,7 +1082,9 @@ describe('ChannelsTable', () => { const tableInstance = makeDefaultTableInstance({ getState: () => ({ selectedTableIds: [] }), }); - return render(col.cell({ row: { original: channel }, table: tableInstance })); + return render( + col.cell({ row: { original: channel }, table: tableInstance }) + ); }; it('renders a Switch for the enabled column', () => { @@ -1002,7 +1137,10 @@ describe('ChannelsTable', () => { setupMocks(); render(); const col = getCol('name'); - const result = col.accessorFn({ effective_name: 'Override Name', name: 'Original' }); + const result = col.accessorFn({ + effective_name: 'Override Name', + name: 'Original', + }); expect(result).toBe('Override Name'); }); @@ -1057,7 +1195,10 @@ describe('ChannelsTable', () => { describe('expandedRowRenderer', () => { it('renders ChannelTableStreams for the expanded row', () => { - const channel = makeChannel({ id: 1, streams: [{ id: 10, is_stale: false }] }); + const channel = makeChannel({ + id: 1, + streams: [{ id: 10, is_stale: false }], + }); setupMocks({ channels: [channel] }); render(); const { expandedRowRenderer } = capturedTableOptions; diff --git a/frontend/src/utils/forms/ChannelUtils.js b/frontend/src/utils/forms/ChannelUtils.js index 40cf9a27..7eabab0a 100644 --- a/frontend/src/utils/forms/ChannelUtils.js +++ b/frontend/src/utils/forms/ChannelUtils.js @@ -40,9 +40,7 @@ export const updateChannel = (values) => { export const addChannel = (channel) => { return API.addChannel(channel); }; -export const requeryChannels = () => { - API.requeryChannels(); -}; +export const requeryChannels = () => API.requeryChannels(); // PATCH semantic: `override: null` deletes the override row; per-field // nulls only clear the matching field. diff --git a/frontend/src/utils/forms/__tests__/ChannelUtils.test.js b/frontend/src/utils/forms/__tests__/ChannelUtils.test.js index 88002c09..53cd0d99 100644 --- a/frontend/src/utils/forms/__tests__/ChannelUtils.test.js +++ b/frontend/src/utils/forms/__tests__/ChannelUtils.test.js @@ -114,13 +114,20 @@ describe('ChannelUtils', () => { describe('requeryChannels', () => { it('calls API.requeryChannels', () => { + vi.mocked(API.requeryChannels).mockResolvedValue(undefined); requeryChannels(); expect(API.requeryChannels).toHaveBeenCalledTimes(1); }); - it('returns undefined', () => { - const result = requeryChannels(); - expect(result).toBeUndefined(); + it('returns the promise from API.requeryChannels', async () => { + const response = { results: [{ id: 1 }] }; + vi.mocked(API.requeryChannels).mockResolvedValue(response); + await expect(requeryChannels()).resolves.toBe(response); + }); + + it('propagates rejection from API.requeryChannels', async () => { + vi.mocked(API.requeryChannels).mockRejectedValue(new Error('network')); + await expect(requeryChannels()).rejects.toThrow('network'); }); });