From 8427ae0a2dc6ce415cdd47bdd410fa1af57e3faf Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Wed, 18 Feb 2026 14:12:03 -0600 Subject: [PATCH] tests: Update frontend tests for changes to channel store. --- frontend/src/pages/__tests__/DVR.test.jsx | 24 ++++++-- frontend/src/pages/__tests__/Guide.test.jsx | 67 ++++++++++++++------- frontend/src/pages/__tests__/Stats.test.jsx | 10 ++- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/frontend/src/pages/__tests__/DVR.test.jsx b/frontend/src/pages/__tests__/DVR.test.jsx index be68bd7f..ba7b187f 100644 --- a/frontend/src/pages/__tests__/DVR.test.jsx +++ b/frontend/src/pages/__tests__/DVR.test.jsx @@ -1,11 +1,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import DVRPage from '../DVR'; import dayjs from 'dayjs'; import useChannelsStore from '../../store/channels'; import useSettingsStore from '../../store/settings'; import useVideoStore from '../../store/useVideoStore'; import useLocalStorage from '../../hooks/useLocalStorage'; +import API from '../../api'; import { isAfter, isBefore, @@ -22,6 +23,7 @@ vi.mock('../../store/channels'); vi.mock('../../store/settings'); vi.mock('../../store/useVideoStore'); vi.mock('../../hooks/useLocalStorage'); +vi.mock('../../api'); // Mock Mantine components vi.mock('@mantine/core', () => ({ @@ -169,6 +171,9 @@ describe('DVRPage', () => { const now = new Date('2024-01-15T12:00:00Z'); vi.setSystemTime(now); + // Default: API.getChannel returns null (no channel data loaded by default) + API.getChannel.mockResolvedValue(null); + isAfter.mockImplementation((a, b) => new Date(a) > new Date(b)); isBefore.mockImplementation((a, b) => new Date(a) < new Date(b)); useTimeHelpers.mockReturnValue({ @@ -448,13 +453,19 @@ describe('DVRPage', () => { custom_properties: { Title: 'Live Show' }, }; + // DVR.jsx now lazy-loads channel data via API.getChannel rather than + // reading s.channels from the store. Mock the API so channelsById + // gets populated before the Watch Live handler runs. + API.getChannel.mockResolvedValue({ + id: 1, + name: 'Channel 1', + stream_url: 'http://stream.url', + }); + useChannelsStore.mockImplementation((selector) => { const state = { ...defaultChannelsState, recordings: [recording], - channels: { - 1: { id: 1, name: 'Channel 1', stream_url: 'http://stream.url' }, - }, }; return selector ? selector(state) : state; }); @@ -466,6 +477,11 @@ describe('DVRPage', () => { await screen.findByTestId('details-modal'); + // Wait for channelsById to be populated from the async API call + await waitFor(() => { + expect(API.getChannel).toHaveBeenCalledWith(1); + }); + const watchLiveButton = screen.getByText('Watch Live'); fireEvent.click(watchLiveButton); diff --git a/frontend/src/pages/__tests__/Guide.test.jsx b/frontend/src/pages/__tests__/Guide.test.jsx index 5c468a0c..7eece391 100644 --- a/frontend/src/pages/__tests__/Guide.test.jsx +++ b/frontend/src/pages/__tests__/Guide.test.jsx @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import API from '../../api'; import dayjs from 'dayjs'; import Guide from '../Guide'; import useChannelsStore from '../../store/channels'; @@ -21,6 +22,7 @@ vi.mock('../../store/epgs'); vi.mock('../../store/settings'); vi.mock('../../store/useVideoStore'); vi.mock('../../hooks/useLocalStorage'); +vi.mock('../../api'); vi.mock('@mantine/hooks', () => ({ useElementSize: () => ({ @@ -261,7 +263,13 @@ describe('Guide', () => { }, recordings: [], channelGroups: { - 'group-1': { id: 'group-1', name: 'News', channels: ['channel-1'] }, + // hasChannels is required: Guide.jsx filters groups by this property + 'group-1': { + id: 'group-1', + name: 'News', + channels: ['channel-1'], + hasChannels: true, + }, }, profiles: { 'profile-1': { id: 'profile-1', name: 'HD Profile' }, @@ -346,6 +354,14 @@ describe('Guide', () => { ]); recordingCardUtils.getShowVideoUrl.mockReturnValue('http://video.test'); + + // Guide.jsx now fetches channels from the API rather than reading them + // from the channel store. Mock both calls so guideChannels gets populated. + const mockChannelsArray = Object.values(mockChannelsState.channels); + API.getAllChannelIds.mockResolvedValue( + Object.keys(mockChannelsState.channels) + ); + API.getChannelsSummary.mockResolvedValue(mockChannelsArray); }); afterEach(() => { @@ -367,9 +383,14 @@ describe('Guide', () => { }); it('renders channel rows when channels are available', async () => { + vi.useRealTimers(); + render(); - expect(screen.getAllByTestId('guide-row')).toHaveLength(2); + // Channels are now fetched asynchronously from the API + await waitFor(() => { + expect(screen.getAllByTestId('guide-row')).toHaveLength(2); + }); }); it('shows no channels message when filters exclude all channels', async () => { @@ -385,11 +406,14 @@ describe('Guide', () => { }); it('displays channel count', async () => { + vi.useRealTimers(); + render(); - // await waitFor(() => { - expect(screen.getByText(/2 channels/)).toBeInTheDocument(); - // }); + // Channels are now fetched asynchronously from the API + await waitFor(() => { + expect(screen.getByText(/2 channels/)).toBeInTheDocument(); + }); }); }); @@ -416,7 +440,9 @@ describe('Guide', () => { await user.type(searchInput, 'Test'); expect(searchInput).toHaveValue('Test'); - await user.click(screen.getByText('Clear Filters')); + // Use getAllByText to safely handle the brief window where both the + // filter-bar and the empty-state buttons are in the DOM simultaneously. + await user.click(screen.getAllByText('Clear Filters')[0]); expect(searchInput).toHaveValue(''); }); @@ -495,8 +521,10 @@ describe('Guide', () => { await user.type(searchInput, 'Test'); // Clear them - const clearButton = await screen.findByText('Clear Filters'); - await user.click(clearButton); + // Use findAllByText + [0] to target the filter-bar button specifically + // in case the empty-state also shows a Clear Filters button. + const clearButtons = await screen.findAllByText('Clear Filters'); + await user.click(clearButtons[0]); expect(searchInput).toHaveValue(''); }); @@ -572,22 +600,19 @@ describe('Guide', () => { }); describe('Error Handling', () => { - it('shows notification when no channels are available', async () => { - useChannelsStore.mockImplementation((selector) => { - const state = { - channels: {}, - recordings: [], - channelGroups: {}, - profiles: {}, - }; - return selector ? selector(state) : state; - }); + it('shows empty state when the API returns no channels', async () => { + vi.useRealTimers(); + + // Guide.jsx no longer emits a notification for an empty channel list; + // instead it renders an empty-state message directly in the UI. + API.getChannelsSummary.mockResolvedValue([]); render(); - expect(showNotification).toHaveBeenCalledWith({ - title: 'No channels available', - color: 'red.5', + await waitFor(() => { + expect( + screen.getByText('No channels match your filters') + ).toBeInTheDocument(); }); }); }); diff --git a/frontend/src/pages/__tests__/Stats.test.jsx b/frontend/src/pages/__tests__/Stats.test.jsx index c22ceac9..ffc90f3f 100644 --- a/frontend/src/pages/__tests__/Stats.test.jsx +++ b/frontend/src/pages/__tests__/Stats.test.jsx @@ -419,11 +419,15 @@ describe('StatsPage', () => { render(); await waitFor(() => { + // Stats page now lazily loads channelsByUUID and channels via API + // (keyed by UUID→ID and ID→channel respectively) rather than reading + // them directly from the channel store. Both start as empty objects + // and are populated on demand; the first call therefore sees {}. expect(getStatsByChannelId).toHaveBeenCalledWith( mockChannelStats, - expect.any(Object), - mockChannelsByUUID, - mockChannels, + expect.any(Object), // prevChannelHistory + {}, // channelsByUUID (local state, starts empty) + {}, // channels (local state, starts empty) mockStreamProfiles ); });