tests: Update frontend tests for changes to channel store.

This commit is contained in:
SergeantPanda 2026-02-18 14:12:03 -06:00
parent edc3fc951e
commit 8427ae0a2d
3 changed files with 73 additions and 28 deletions

View file

@ -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);

View file

@ -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(<Guide />);
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(<Guide />);
// 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(<Guide />);
expect(showNotification).toHaveBeenCalledWith({
title: 'No channels available',
color: 'red.5',
await waitFor(() => {
expect(
screen.getByText('No channels match your filters')
).toBeInTheDocument();
});
});
});

View file

@ -419,11 +419,15 @@ describe('StatsPage', () => {
render(<StatsPage />);
await waitFor(() => {
// Stats page now lazily loads channelsByUUID and channels via API
// (keyed by UUIDID and IDchannel 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
);
});