Added unit tests

This commit is contained in:
Nick Sandstrom 2026-01-27 13:39:04 -08:00
parent 8521df94ad
commit bd4c9e1d95
18 changed files with 6631 additions and 0 deletions

View file

@ -0,0 +1,110 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import useLocalStorage from '../useLocalStorage';
// Mock localStorage
const localStorageMock = (() => {
let store = {};
return {
getItem: vi.fn((key) => store[key] || null),
setItem: vi.fn((key, value) => {
store[key] = value.toString();
}),
clear: vi.fn(() => {
store = {};
}),
removeItem: vi.fn((key) => {
delete store[key];
})
};
})();
global.localStorage = localStorageMock;
// Mock console.error to avoid cluttering test output
global.console.error = vi.fn();
describe('useLocalStorage', () => {
beforeEach(() => {
localStorageMock.clear();
vi.clearAllMocks();
});
it('should initialize with default value when localStorage is empty', () => {
const { result } = renderHook(() => useLocalStorage('testKey', 'defaultValue'));
expect(result.current[0]).toBe('defaultValue');
});
it('should initialize with value from localStorage if available', () => {
localStorageMock.setItem('testKey', JSON.stringify('storedValue'));
const { result } = renderHook(() => useLocalStorage('testKey', 'defaultValue'));
expect(result.current[0]).toBe('storedValue');
});
it('should update localStorage when value changes', () => {
const { result } = renderHook(() => useLocalStorage('testKey', 'initial'));
act(() => {
result.current[1]('updated');
});
expect(localStorageMock.setItem).toHaveBeenCalledWith('testKey', JSON.stringify('updated'));
expect(result.current[0]).toBe('updated');
});
it('should handle complex objects', () => {
const complexObject = { name: 'test', count: 42, nested: { value: true } };
const { result } = renderHook(() => useLocalStorage('testKey', complexObject));
act(() => {
result.current[1]({ name: 'updated', count: 100 });
});
expect(result.current[0]).toEqual({ name: 'updated', count: 100 });
});
it('should handle errors when reading from localStorage', () => {
localStorageMock.getItem.mockImplementationOnce(() => {
throw new Error('Read error');
});
const { result } = renderHook(() => useLocalStorage('testKey', 'defaultValue'));
expect(result.current[0]).toBe('defaultValue');
expect(console.error).toHaveBeenCalledWith(
'Error reading key "testKey":',
expect.any(Error)
);
});
it('should handle errors when writing to localStorage', () => {
localStorageMock.setItem.mockImplementationOnce(() => {
throw new Error('Write error');
});
const { result } = renderHook(() => useLocalStorage('testKey', 'initial'));
act(() => {
result.current[1]('updated');
});
expect(console.error).toHaveBeenCalledWith(
'Error saving setting: testKey:',
expect.any(Error)
);
});
it('should handle invalid JSON in localStorage', () => {
localStorageMock.getItem.mockReturnValueOnce('invalid json{');
const { result } = renderHook(() => useLocalStorage('testKey', 'defaultValue'));
expect(result.current[0]).toBe('defaultValue');
expect(console.error).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,297 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useLogoSelection, useChannelLogoSelection, useLogosById } from '../useSmartLogos';
import useLogosStore from '../../store/logos';
// Mock the logos store
vi.mock('../../store/logos');
describe('useSmartLogos', () => {
describe('useLogoSelection', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should initialize with empty state', () => {
useLogosStore.mockImplementation((selector) => selector({
logos: {},
fetchLogos: vi.fn(),
}));
const { result } = renderHook(() => useLogoSelection());
expect(result.current.logos).toEqual({});
expect(result.current.isLoading).toBe(false);
expect(result.current.hasLogos).toBe(false);
});
it('should load logos when ensureLogosLoaded is called', async () => {
const mockFetchLogos = vi.fn().mockResolvedValue();
useLogosStore.mockImplementation((selector) => selector({
logos: {},
fetchLogos: mockFetchLogos,
}));
const { result } = renderHook(() => useLogoSelection());
await act(async () => {
await result.current.ensureLogosLoaded();
});
expect(mockFetchLogos).toHaveBeenCalledTimes(1);
});
it('should not reload logos if already loaded', async () => {
const mockFetchLogos = vi.fn().mockResolvedValue();
useLogosStore.mockImplementation((selector) => selector({
logos: { logo1: { id: 'logo1' } },
fetchLogos: mockFetchLogos,
}));
const { result } = renderHook(() => useLogoSelection());
await act(async () => {
await result.current.ensureLogosLoaded();
});
await act(async () => {
await result.current.ensureLogosLoaded();
});
expect(mockFetchLogos).toHaveBeenCalledTimes(1);
});
it('should handle errors when fetching logos', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const mockFetchLogos = vi.fn().mockRejectedValue(new Error('Fetch failed'));
useLogosStore.mockImplementation((selector) => selector({
logos: {},
fetchLogos: mockFetchLogos,
}));
const { result } = renderHook(() => useLogoSelection());
await act(async () => {
await result.current.ensureLogosLoaded();
});
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to load logos for selection:',
expect.any(Error)
);
consoleErrorSpy.mockRestore();
});
it('should indicate hasLogos when logos are present', () => {
useLogosStore.mockImplementation((selector) => selector({
logos: { logo1: { id: 'logo1' }, logo2: { id: 'logo2' } },
fetchLogos: vi.fn(),
}));
const { result } = renderHook(() => useLogoSelection());
expect(result.current.hasLogos).toBe(true);
});
});
describe('useChannelLogoSelection', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should initialize with channel logos state', () => {
useLogosStore.mockImplementation((selector) => selector({
channelLogos: {},
hasLoadedChannelLogos: false,
backgroundLoading: false,
fetchChannelAssignableLogos: vi.fn(),
}));
const { result } = renderHook(() => useChannelLogoSelection());
expect(result.current.logos).toEqual({});
expect(result.current.isLoading).toBe(false);
expect(result.current.hasLogos).toBe(false);
});
it('should load channel logos when ensureLogosLoaded is called', async () => {
const mockFetchChannelLogos = vi.fn().mockResolvedValue();
useLogosStore.mockImplementation((selector) => selector({
channelLogos: {},
hasLoadedChannelLogos: false,
backgroundLoading: false,
fetchChannelAssignableLogos: mockFetchChannelLogos,
}));
const { result } = renderHook(() => useChannelLogoSelection());
await act(async () => {
await result.current.ensureLogosLoaded();
});
expect(mockFetchChannelLogos).toHaveBeenCalledTimes(1);
});
it('should not reload if already loaded', async () => {
const mockFetchChannelLogos = vi.fn().mockResolvedValue();
useLogosStore.mockImplementation((selector) => selector({
channelLogos: { logo1: { id: 'logo1' } },
hasLoadedChannelLogos: true,
backgroundLoading: false,
fetchChannelAssignableLogos: mockFetchChannelLogos,
}));
const { result } = renderHook(() => useChannelLogoSelection());
await act(async () => {
await result.current.ensureLogosLoaded();
});
expect(mockFetchChannelLogos).not.toHaveBeenCalled();
});
it('should not load if backgroundLoading is true', async () => {
const mockFetchChannelLogos = vi.fn().mockResolvedValue();
useLogosStore.mockImplementation((selector) => selector({
channelLogos: {},
hasLoadedChannelLogos: false,
backgroundLoading: true,
fetchChannelAssignableLogos: mockFetchChannelLogos,
}));
const { result } = renderHook(() => useChannelLogoSelection());
await act(async () => {
await result.current.ensureLogosLoaded();
});
expect(mockFetchChannelLogos).not.toHaveBeenCalled();
});
it('should handle errors when fetching channel logos', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const mockFetchChannelLogos = vi.fn().mockRejectedValue(new Error('Fetch failed'));
useLogosStore.mockImplementation((selector) => selector({
channelLogos: {},
hasLoadedChannelLogos: false,
backgroundLoading: false,
fetchChannelAssignableLogos: mockFetchChannelLogos,
}));
const { result } = renderHook(() => useChannelLogoSelection());
await act(async () => {
await result.current.ensureLogosLoaded();
});
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to load channel logos:',
expect.any(Error)
);
consoleErrorSpy.mockRestore();
});
});
describe('useLogosById', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should initialize with empty logos', () => {
useLogosStore.mockImplementation((selector) => selector({
logos: {},
fetchLogosByIds: vi.fn(),
}));
const { result } = renderHook(() => useLogosById([]));
expect(result.current.logos).toEqual({});
expect(result.current.isLoading).toBe(false);
expect(result.current.missingLogos).toBe(0);
});
it('should fetch missing logos by IDs', async () => {
const mockFetchLogosByIds = vi.fn().mockResolvedValue();
useLogosStore.mockImplementation((selector) => selector({
logos: {},
fetchLogosByIds: mockFetchLogosByIds,
}));
renderHook(() => useLogosById(['logo1', 'logo2']));
await waitFor(() => {
expect(mockFetchLogosByIds).toHaveBeenCalledWith(['logo1', 'logo2']);
});
});
it('should not fetch logos that are already loaded', async () => {
const mockFetchLogosByIds = vi.fn().mockResolvedValue();
useLogosStore.mockImplementation((selector) => selector({
logos: { logo1: { id: 'logo1' } },
fetchLogosByIds: mockFetchLogosByIds,
}));
renderHook(() => useLogosById(['logo1', 'logo2']));
await waitFor(() => {
expect(mockFetchLogosByIds).toHaveBeenCalledWith(['logo2']);
});
});
it('should handle errors when fetching logos by IDs', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const mockFetchLogosByIds = vi.fn().mockRejectedValue(new Error('Fetch failed'));
useLogosStore.mockImplementation((selector) => selector({
logos: {},
fetchLogosByIds: mockFetchLogosByIds,
}));
renderHook(() => useLogosById(['logo1']));
await waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to load logos by IDs:',
expect.any(Error)
);
});
consoleErrorSpy.mockRestore();
});
it('should filter out null/undefined IDs', async () => {
const mockFetchLogosByIds = vi.fn().mockResolvedValue();
useLogosStore.mockImplementation((selector) => selector({
logos: {},
fetchLogosByIds: mockFetchLogosByIds,
}));
renderHook(() => useLogosById(['logo1', null, undefined, 'logo2']));
await waitFor(() => {
expect(mockFetchLogosByIds).toHaveBeenCalledWith(['logo1', 'logo2']);
});
});
it('should not refetch the same IDs multiple times', async () => {
const mockFetchLogosByIds = vi.fn().mockResolvedValue();
useLogosStore.mockImplementation((selector) => selector({
logos: {},
fetchLogosByIds: mockFetchLogosByIds,
}));
const { rerender } = renderHook(() => useLogosById(['logo1']));
await waitFor(() => {
expect(mockFetchLogosByIds).toHaveBeenCalledTimes(1);
});
rerender();
await waitFor(() => {
expect(mockFetchLogosByIds).toHaveBeenCalledTimes(1);
});
});
});
});

View file

@ -0,0 +1,479 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import useAuthStore from '../auth';
import useSettingsStore from '../settings';
import useChannelsStore from '../channels';
import usePlaylistsStore from '../playlists';
import useEPGsStore from '../epgs';
import useStreamProfilesStore from '../streamProfiles';
import useUserAgentsStore from '../userAgents';
import useUsersStore from '../users';
import API from '../../api';
import { USER_LEVELS } from '../../constants';
// Mock all store dependencies
vi.mock('../settings');
vi.mock('../channels');
vi.mock('../playlists');
vi.mock('../epgs');
vi.mock('../streamProfiles');
vi.mock('../userAgents');
vi.mock('../users');
vi.mock('../../api');
// Mock localStorage
const localStorageMock = (() => {
let store = {};
return {
getItem: vi.fn((key) => store[key] || null),
setItem: vi.fn((key, value) => {
store[key] = value.toString();
}),
removeItem: vi.fn((key) => {
delete store[key];
}),
clear: vi.fn(() => {
store = {};
}),
};
})();
global.localStorage = localStorageMock;
// Mock console methods
global.console.error = vi.fn();
// Helper to create a mock JWT token
const createMockToken = (expiresInSeconds = 3600) => {
const now = Math.floor(Date.now() / 1000);
const exp = now + expiresInSeconds;
const payload = btoa(JSON.stringify({ exp }));
return `header.${payload}.signature`;
};
describe('useAuthStore', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorageMock.clear();
// Setup default store mocks
useSettingsStore.mockImplementation((selector) => selector({
fetchSettings: vi.fn().mockResolvedValue(),
}));
useChannelsStore.mockImplementation((selector) => selector({
fetchChannels: vi.fn().mockResolvedValue(),
fetchChannelGroups: vi.fn().mockResolvedValue(),
fetchChannelProfiles: vi.fn().mockResolvedValue(),
}));
usePlaylistsStore.mockImplementation((selector) => selector({
fetchPlaylists: vi.fn().mockResolvedValue(),
}));
useEPGsStore.mockImplementation((selector) => selector({
fetchEPGs: vi.fn().mockResolvedValue(),
fetchEPGData: vi.fn().mockResolvedValue(),
}));
useStreamProfilesStore.mockImplementation((selector) => selector({
fetchProfiles: vi.fn().mockResolvedValue(),
}));
useUserAgentsStore.mockImplementation((selector) => selector({
fetchUserAgents: vi.fn().mockResolvedValue(),
}));
useUsersStore.mockImplementation((selector) => selector({
fetchUsers: vi.fn().mockResolvedValue(),
}));
});
afterEach(() => {
// Reset the store state
const { setState } = useAuthStore;
if (setState) {
setState({
isAuthenticated: false,
isInitialized: false,
needsSuperuser: false,
user: {
username: '',
email: '',
user_level: '',
},
isLoading: false,
error: null,
accessToken: null,
refreshToken: null,
tokenExpiration: null,
superuserExists: true,
});
}
});
describe('Initial State', () => {
it('should initialize with default state', () => {
const { result } = renderHook(() => useAuthStore());
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.isInitialized).toBe(false);
expect(result.current.needsSuperuser).toBe(false);
expect(result.current.user).toEqual({
username: '',
email: '',
user_level: '',
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
expect(result.current.superuserExists).toBe(true);
});
});
describe('login', () => {
it('should successfully login and store tokens', async () => {
const mockAccessToken = createMockToken();
const mockRefreshToken = createMockToken(86400);
API.login.mockResolvedValue({
access: mockAccessToken,
refresh: mockRefreshToken,
});
const { result } = renderHook(() => useAuthStore());
await act(async () => {
await result.current.login({ username: 'testuser', password: 'password' });
});
expect(API.login).toHaveBeenCalledWith('testuser', 'password');
expect(localStorageMock.setItem).toHaveBeenCalledWith('accessToken', mockAccessToken);
expect(localStorageMock.setItem).toHaveBeenCalledWith('refreshToken', mockRefreshToken);
expect(localStorageMock.setItem).toHaveBeenCalledWith('tokenExpiration', expect.any(Number));
});
it('should handle login failure', async () => {
API.login.mockRejectedValue(new Error('Invalid credentials'));
const { result } = renderHook(() => useAuthStore());
await act(async () => {
await result.current.login({ username: 'testuser', password: 'wrong' });
});
expect(console.error).toHaveBeenCalledWith('Login failed:', expect.any(Error));
});
});
describe('getRefreshToken', () => {
it('should refresh token successfully', async () => {
const mockNewAccessToken = createMockToken();
localStorageMock.getItem.mockReturnValue('old-refresh-token');
API.refreshToken.mockResolvedValue({
access: mockNewAccessToken,
});
const { result } = renderHook(() => useAuthStore());
let newToken;
await act(async () => {
newToken = await result.current.getRefreshToken();
});
expect(API.refreshToken).toHaveBeenCalledWith('old-refresh-token');
expect(newToken).toBe(mockNewAccessToken);
expect(result.current.isAuthenticated).toBe(true);
expect(localStorageMock.setItem).toHaveBeenCalledWith('accessToken', mockNewAccessToken);
});
it('should return false if no refresh token exists', async () => {
localStorageMock.getItem.mockReturnValue(null);
const { result } = renderHook(() => useAuthStore());
let response;
await act(async () => {
response = await result.current.getRefreshToken();
});
expect(response).toBe(false);
expect(API.refreshToken).not.toHaveBeenCalled();
});
it('should logout on refresh token failure', async () => {
localStorageMock.getItem.mockReturnValue('invalid-refresh-token');
API.refreshToken.mockRejectedValue(new Error('Invalid token'));
API.logout.mockResolvedValue();
const { result } = renderHook(() => useAuthStore());
await act(async () => {
await result.current.getRefreshToken();
});
expect(console.error).toHaveBeenCalledWith('Token refresh failed:', expect.any(Error));
expect(result.current.isAuthenticated).toBe(false);
expect(localStorageMock.removeItem).toHaveBeenCalledWith('accessToken');
expect(localStorageMock.removeItem).toHaveBeenCalledWith('refreshToken');
expect(localStorageMock.removeItem).toHaveBeenCalledWith('tokenExpiration');
});
});
describe('getToken', () => {
it('should return valid access token if not expired', async () => {
const mockToken = createMockToken(3600);
const now = Math.floor(Date.now() / 1000);
localStorageMock.getItem.mockImplementation((key) => {
if (key === 'tokenExpiration') return (now + 1800).toString();
if (key === 'accessToken') return mockToken;
return null;
});
const { result } = renderHook(() => useAuthStore());
let token;
await act(async () => {
token = await result.current.getToken();
});
expect(token).toBe(mockToken);
});
it('should refresh token if expired', async () => {
const now = Math.floor(Date.now() / 1000);
const mockNewToken = createMockToken();
localStorageMock.getItem.mockImplementation((key) => {
if (key === 'tokenExpiration') return (now - 100).toString();
if (key === 'refreshToken') return 'refresh-token';
return null;
});
API.refreshToken.mockResolvedValue({
access: mockNewToken,
});
const { result } = renderHook(() => useAuthStore());
await act(async () => {
await result.current.getToken();
});
expect(API.refreshToken).toHaveBeenCalled();
});
});
describe('logout', () => {
it('should clear tokens and call logout API', async () => {
API.logout.mockResolvedValue();
const { result } = renderHook(() => useAuthStore());
await act(async () => {
await result.current.logout();
});
expect(API.logout).toHaveBeenCalled();
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBeNull();
expect(localStorageMock.removeItem).toHaveBeenCalledWith('accessToken');
expect(localStorageMock.removeItem).toHaveBeenCalledWith('refreshToken');
expect(localStorageMock.removeItem).toHaveBeenCalledWith('tokenExpiration');
});
it('should continue logout even if API call fails', async () => {
API.logout.mockRejectedValue(new Error('API error'));
const { result } = renderHook(() => useAuthStore());
await act(async () => {
await result.current.logout();
});
expect(console.error).toHaveBeenCalledWith('Logout API call failed:', expect.any(Error));
expect(result.current.isAuthenticated).toBe(false);
expect(localStorageMock.removeItem).toHaveBeenCalled();
});
});
describe('initializeAuth', () => {
it('should initialize auth with valid refresh token', async () => {
const mockToken = createMockToken();
localStorageMock.getItem.mockReturnValue('valid-refresh-token');
API.refreshToken.mockResolvedValue({ access: mockToken });
const { result } = renderHook(() => useAuthStore());
let initialized;
await act(async () => {
initialized = await result.current.initializeAuth();
});
expect(initialized).toBe(true);
});
it('should return false if no refresh token exists', async () => {
localStorageMock.getItem.mockReturnValue(null);
const { result } = renderHook(() => useAuthStore());
let initialized;
await act(async () => {
initialized = await result.current.initializeAuth();
});
expect(initialized).toBe(false);
});
});
describe('initData', () => {
const fetchSettings = vi.fn().mockResolvedValue();
const fetchChannels = vi.fn().mockResolvedValue();
const fetchChannelGroups = vi.fn().mockResolvedValue();
const fetchChannelProfiles = vi.fn().mockResolvedValue();
const fetchPlaylists = vi.fn().mockResolvedValue();
const fetchEPGs = vi.fn().mockResolvedValue();
const fetchEPGData = vi.fn().mockResolvedValue();
const fetchProfiles = vi.fn().mockResolvedValue();
const fetchUserAgents = vi.fn().mockResolvedValue();
const fetchUsers = vi.fn().mockResolvedValue();
// Mock getState for each store
useSettingsStore.getState = () => ({ fetchSettings });
useChannelsStore.getState = () => ({
fetchChannels,
fetchChannelGroups,
fetchChannelProfiles,
});
usePlaylistsStore.getState = () => ({ fetchPlaylists });
useEPGsStore.getState = () => ({ fetchEPGs, fetchEPGData });
useStreamProfilesStore.getState = () => ({ fetchProfiles });
useUserAgentsStore.getState = () => ({ fetchUserAgents });
useUsersStore.getState = () => ({ fetchUsers });
it('should initialize data for admin user', async () => {
const mockUser = {
username: 'admin',
email: 'admin@test.com',
user_level: USER_LEVELS.ADMIN,
};
API.me.mockResolvedValue(mockUser);
const { result } = renderHook(() => useAuthStore());
await act(async () => {
await result.current.initData();
});
expect(API.me).toHaveBeenCalled();
expect(result.current.user).toEqual(mockUser);
expect(result.current.isAuthenticated).toBe(true);
expect(fetchSettings).toHaveBeenCalled();
expect(fetchChannels).toHaveBeenCalled();
expect(fetchUsers).toHaveBeenCalled();
});
it('should not fetch users for non-admin user', async () => {
const mockUser = {
username: 'reseller',
email: 'reseller@test.com',
user_level: USER_LEVELS.RESELLER,
};
API.me.mockResolvedValue(mockUser);
const { result } = renderHook(() => useAuthStore());
await act(async () => {
await result.current.initData();
});
expect(fetchUsers).not.toHaveBeenCalled();
});
it('should throw error for unauthorized user level', async () => {
const mockUser = {
username: 'streamer',
email: 'streamer@test.com',
user_level: USER_LEVELS.STREAMER,
};
API.me.mockResolvedValue(mockUser);
const { result } = renderHook(() => useAuthStore());
await expect(
act(async () => {
await result.current.initData();
})
).rejects.toThrow('Unauthorized');
});
it('should handle errors during data initialization', async () => {
const mockUser = {
username: 'admin',
email: 'admin@test.com',
user_level: USER_LEVELS.ADMIN,
};
API.me.mockResolvedValue(mockUser);
const fetchChannels = vi.fn().mockRejectedValue(new Error('Fetch failed'));
useChannelsStore.getState = vi.fn(() => ({
fetchChannels,
fetchChannelGroups: vi.fn().mockResolvedValue(),
fetchChannelProfiles: vi.fn().mockResolvedValue(),
}));
const { result } = renderHook(() => useAuthStore());
await act(async () => {
await result.current.initData();
});
expect(console.error).toHaveBeenCalledWith('Error initializing data:', expect.any(Error));
});
});
describe('setUser', () => {
it('should update user state', () => {
const { result } = renderHook(() => useAuthStore());
const newUser = { username: 'test', email: 'test@test.com', user_level: USER_LEVELS.ADMIN };
act(() => {
result.current.setUser(newUser);
});
expect(result.current.user).toEqual(newUser);
});
});
describe('setIsAuthenticated', () => {
it('should update authentication state', () => {
const { result } = renderHook(() => useAuthStore());
act(() => {
result.current.setIsAuthenticated(true);
});
expect(result.current.isAuthenticated).toBe(true);
});
});
describe('setSuperuserExists', () => {
it('should update superuser exists state', () => {
const { result } = renderHook(() => useAuthStore());
act(() => {
result.current.setSuperuserExists(false);
});
expect(result.current.superuserExists).toBe(false);
});
});
});

View file

@ -0,0 +1,442 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import useChannelsStore from '../channels';
import api from '../../api';
import { showNotification } from '../../utils/notificationUtils';
// Mock dependencies
vi.mock('../../api');
vi.mock('../../utils/notificationUtils');
describe('useChannelsStore', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset store state between tests
useChannelsStore.setState({
channels: {},
channelsByUUID: {},
channelGroups: {},
profiles: { 0: { id: '0', name: 'All', channels: new Set() } },
selectedProfileId: '0',
channelsPageSelection: [],
stats: {},
activeChannels: {},
activeClients: {},
recordings: [],
recurringRules: [],
isLoading: false,
error: null,
forceUpdate: 0,
});
});
describe('fetchChannels', () => {
it('should fetch and store channels successfully', async () => {
const mockChannels = [
{ id: 1, uuid: 'uuid-1', name: 'Channel 1' },
{ id: 2, uuid: 'uuid-2', name: 'Channel 2' },
];
api.getChannels.mockResolvedValue(mockChannels);
const { result } = renderHook(() => useChannelsStore());
await act(async () => {
await result.current.fetchChannels();
});
expect(api.getChannels).toHaveBeenCalledOnce();
expect(result.current.channels).toEqual({
1: mockChannels[0],
2: mockChannels[1],
});
expect(result.current.channelsByUUID).toEqual({
'uuid-1': 1,
'uuid-2': 2,
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});
it('should handle fetch error', async () => {
const errorMessage = 'Network error';
api.getChannels.mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useChannelsStore());
await act(async () => {
await result.current.fetchChannels();
});
expect(result.current.error).toBe(errorMessage);
expect(result.current.isLoading).toBe(false);
});
});
describe('fetchChannelGroups', () => {
it('should fetch and process channel groups', async () => {
const mockGroups = [
{ id: 1, name: 'Group 1', channel_count: 5, m3u_account_count: 0 },
{ id: 2, name: 'Group 2', channel_count: 0, m3u_account_count: 2 },
];
api.getChannelGroups.mockResolvedValue(mockGroups);
const { result } = renderHook(() => useChannelsStore());
await act(async () => {
await result.current.fetchChannelGroups();
});
expect(api.getChannelGroups).toHaveBeenCalledOnce();
expect(result.current.channelGroups[1]).toMatchObject({
hasChannels: true,
hasM3UAccounts: false,
canEdit: true,
canDelete: false,
});
expect(result.current.channelGroups[2]).toMatchObject({
hasChannels: false,
hasM3UAccounts: true,
canEdit: false,
canDelete: false,
});
});
});
describe('fetchChannelProfiles', () => {
it('should fetch and process channel profiles', async () => {
const mockProfiles = [
{ id: '1', name: 'Profile 1', channels: [1, 2, 3] },
];
api.getChannelProfiles.mockResolvedValue(mockProfiles);
const { result } = renderHook(() => useChannelsStore());
await act(async () => {
await result.current.fetchChannelProfiles();
});
expect(api.getChannelProfiles).toHaveBeenCalledOnce();
expect(result.current.profiles['1'].channels).toBeInstanceOf(Set);
expect(result.current.profiles['1'].channels.has(1)).toBe(true);
});
});
describe('addChannel', () => {
it('should add a new channel', async () => {
const newChannel = { id: 3, uuid: 'uuid-3', name: 'Channel 3' };
api.getChannelProfiles.mockResolvedValue([]);
const { result } = renderHook(() => useChannelsStore());
await act(async () => {
result.current.addChannel(newChannel);
});
expect(result.current.channels[3]).toEqual(newChannel);
expect(result.current.channelsByUUID['uuid-3']).toBe(3);
});
});
describe('updateChannel', () => {
it('should update an existing channel', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
channels: { 1: { id: 1, uuid: 'uuid-1', name: 'Old Name' } },
channelsByUUID: { 'uuid-1': 1 },
});
});
const updatedChannel = { id: 1, uuid: 'uuid-1', name: 'New Name' };
act(() => {
result.current.updateChannel(updatedChannel);
});
expect(result.current.channels[1].name).toBe('New Name');
});
});
describe('removeChannels', () => {
it('should remove channels by IDs', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
channels: {
1: { id: 1, uuid: 'uuid-1' },
2: { id: 2, uuid: 'uuid-2' },
},
channelsByUUID: { 'uuid-1': 1, 'uuid-2': 2 },
});
});
act(() => {
result.current.removeChannels([1]);
});
expect(result.current.channels[1]).toBeUndefined();
expect(result.current.channelsByUUID['uuid-1']).toBeUndefined();
expect(result.current.channels[2]).toBeDefined();
});
});
describe('channel groups operations', () => {
it('should add a channel group', () => {
const { result } = renderHook(() => useChannelsStore());
const newGroup = { id: 1, name: 'New Group' };
act(() => {
result.current.addChannelGroup(newGroup);
});
expect(result.current.channelGroups[1]).toEqual(newGroup);
});
it('should update a channel group', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
channelGroups: { 1: { id: 1, name: 'Old Name' } },
});
});
act(() => {
result.current.updateChannelGroup({ id: 1, name: 'Updated Name' });
});
expect(result.current.channelGroups[1].name).toBe('Updated Name');
});
it('should remove a channel group', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
channelGroups: { 1: { id: 1, name: 'Group' } },
});
});
act(() => {
result.current.removeChannelGroup(1);
});
expect(result.current.channelGroups[1]).toBeUndefined();
});
});
describe('profile operations', () => {
it('should add a profile', () => {
const { result } = renderHook(() => useChannelsStore());
const newProfile = { id: '1', name: 'Profile', channels: [1, 2] };
act(() => {
result.current.addProfile(newProfile);
});
expect(result.current.profiles['1'].channels).toBeInstanceOf(Set);
});
it('should update a profile', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
result.current.updateProfile({ id: '1', name: 'Updated', channels: [3] });
});
expect(result.current.profiles['1'].name).toBe('Updated');
});
it('should remove profiles and reset selected if needed', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
profiles: { '1': { id: '1' }, '2': { id: '2' } },
selectedProfileId: '1',
});
});
act(() => {
result.current.removeProfiles(['1']);
});
expect(result.current.profiles['1']).toBeUndefined();
expect(result.current.selectedProfileId).toBe('0');
});
});
describe('updateProfileChannels', () => {
it('should add channels to profile', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
profiles: { '1': { id: '1', channels: new Set([1]) } },
});
});
act(() => {
result.current.updateProfileChannels([2, 3], '1', true);
});
expect(result.current.profiles['1'].channels.has(2)).toBe(true);
expect(result.current.profiles['1'].channels.has(3)).toBe(true);
});
it('should remove channels from profile', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
profiles: { '1': { id: '1', channels: new Set([1, 2, 3]) } },
});
});
act(() => {
result.current.updateProfileChannels([2], '1', false);
});
expect(result.current.profiles['1'].channels.has(2)).toBe(false);
});
});
describe('setChannelStats', () => {
it('should update stats and show notifications for new channels', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
channels: { 1: { id: 1, name: 'Channel 1' } },
channelsByUUID: { 'uuid-1': 1 },
stats: { channels: [] },
});
});
const newStats = {
channels: [
{ channel_id: 'uuid-1', clients: [] },
],
};
act(() => {
result.current.setChannelStats(newStats);
});
expect(result.current.stats).toEqual(newStats);
expect(showNotification).toHaveBeenCalled();
});
});
describe('recordings operations', () => {
it('should fetch recordings', async () => {
const mockRecordings = [{ id: 1, title: 'Recording 1' }];
api.getRecordings.mockResolvedValue(mockRecordings);
const { result } = renderHook(() => useChannelsStore());
await act(async () => {
await result.current.fetchRecordings();
});
expect(result.current.recordings).toEqual(mockRecordings);
});
it('should remove a recording', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
recordings: [{ id: 1 }, { id: 2 }],
});
});
act(() => {
result.current.removeRecording(1);
});
expect(result.current.recordings).toHaveLength(1);
expect(result.current.recordings[0].id).toBe(2);
});
});
describe('recurring rules operations', () => {
it('should fetch recurring rules', async () => {
const mockRules = [{ id: 1, name: 'Rule 1' }];
api.listRecurringRules.mockResolvedValue(mockRules);
const { result } = renderHook(() => useChannelsStore());
await act(async () => {
await result.current.fetchRecurringRules();
});
expect(result.current.recurringRules).toEqual(mockRules);
});
it('should remove a recurring rule', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
recurringRules: [{ id: 1 }, { id: 2 }],
});
});
act(() => {
result.current.removeRecurringRule(1);
});
expect(result.current.recurringRules).toHaveLength(1);
});
});
describe('helper methods', () => {
it('should validate if channel group can be edited', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
channelGroups: {
1: { id: 1, canEdit: true },
2: { id: 2, canEdit: false },
},
});
});
expect(result.current.canEditChannelGroup(1)).toBe(true);
expect(result.current.canEditChannelGroup(2)).toBe(false);
});
it('should validate if channel group can be deleted', () => {
const { result } = renderHook(() => useChannelsStore());
act(() => {
useChannelsStore.setState({
channelGroups: {
1: { id: 1, canDelete: true },
2: { id: 2, canDelete: false },
},
});
});
expect(result.current.canDeleteChannelGroup(1)).toBe(true);
expect(result.current.canDeleteChannelGroup(2)).toBe(false);
});
});
describe('triggerUpdate', () => {
it('should update forceUpdate timestamp', () => {
const { result } = renderHook(() => useChannelsStore());
const initialUpdate = result.current.forceUpdate;
act(() => {
result.current.triggerUpdate();
});
expect(result.current.forceUpdate).not.toBe(initialUpdate);
});
});
});

View file

@ -0,0 +1,295 @@
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import useChannelsTableStore from '../channelsTable';
describe('useChannelsTableStore', () => {
beforeEach(() => {
// Mock localStorage
const mockLocalStorage = {
getItem: vi.fn(),
setItem: vi.fn(),
clear: vi.fn(),
};
global.localStorage = mockLocalStorage;
vi.clearAllMocks();
// Reset store state between tests
useChannelsTableStore.setState({
channels: [],
pageCount: 0,
totalCount: 0,
sorting: [{ id: 'channel_number', desc: false }],
pagination: {
pageIndex: 0,
pageSize: 50,
},
selectedChannelIds: [],
allQueryIds: [],
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('initial state', () => {
it('should initialize with default values', () => {
const { result } = renderHook(() => useChannelsTableStore());
expect(result.current.channels).toEqual([]);
expect(result.current.pageCount).toBe(0);
expect(result.current.totalCount).toBe(0);
expect(result.current.sorting).toEqual([{ id: 'channel_number', desc: false }]);
expect(result.current.pagination.pageIndex).toBe(0);
expect(result.current.pagination.pageSize).toBe(50);
expect(result.current.selectedChannelIds).toEqual([]);
expect(result.current.allQueryIds).toEqual([]);
});
});
describe('queryChannels', () => {
it('should update channels, totalCount, and pageCount', () => {
const { result } = renderHook(() => useChannelsTableStore());
const mockResults = [
{ id: 1, name: 'Channel 1' },
{ id: 2, name: 'Channel 2' },
];
const mockData = {
results: mockResults,
count: 150,
};
const mockParams = new URLSearchParams({ page_size: '50' });
act(() => {
result.current.queryChannels(mockData, mockParams);
});
expect(result.current.channels).toEqual(mockResults);
expect(result.current.totalCount).toBe(150);
expect(result.current.pageCount).toBe(3); // Math.ceil(150 / 50)
});
it('should calculate pageCount correctly with different page sizes', () => {
const { result } = renderHook(() => useChannelsTableStore());
const mockData = {
results: [],
count: 75,
};
const mockParams = new URLSearchParams({ page_size: '25' });
act(() => {
result.current.queryChannels(mockData, mockParams);
});
expect(result.current.pageCount).toBe(3); // Math.ceil(75 / 25)
});
it('should handle zero results', () => {
const { result } = renderHook(() => useChannelsTableStore());
const mockData = {
results: [],
count: 0,
};
const mockParams = new URLSearchParams({ page_size: '50' });
act(() => {
result.current.queryChannels(mockData, mockParams);
});
expect(result.current.channels).toEqual([]);
expect(result.current.totalCount).toBe(0);
expect(result.current.pageCount).toBe(0);
});
});
describe('setAllQueryIds', () => {
it('should update allQueryIds', () => {
const { result } = renderHook(() => useChannelsTableStore());
const mockIds = [1, 2, 3, 4, 5];
act(() => {
result.current.setAllQueryIds(mockIds);
});
expect(result.current.allQueryIds).toEqual(mockIds);
});
it('should replace existing allQueryIds', () => {
const { result } = renderHook(() => useChannelsTableStore());
act(() => {
result.current.setAllQueryIds([1, 2, 3]);
});
expect(result.current.allQueryIds).toEqual([1, 2, 3]);
act(() => {
result.current.setAllQueryIds([4, 5, 6]);
});
expect(result.current.allQueryIds).toEqual([4, 5, 6]);
});
});
describe('setSelectedChannelIds', () => {
it('should update selectedChannelIds', () => {
const { result } = renderHook(() => useChannelsTableStore());
const mockIds = [1, 3, 5];
act(() => {
result.current.setSelectedChannelIds(mockIds);
});
expect(result.current.selectedChannelIds).toEqual(mockIds);
});
it('should clear selectedChannelIds when empty array is passed', () => {
const { result } = renderHook(() => useChannelsTableStore());
act(() => {
result.current.setSelectedChannelIds([1, 2, 3]);
});
expect(result.current.selectedChannelIds).toEqual([1, 2, 3]);
act(() => {
result.current.setSelectedChannelIds([]);
});
expect(result.current.selectedChannelIds).toEqual([]);
});
});
describe('getChannelStreams', () => {
it('should return streams for existing channel', () => {
const { result } = renderHook(() => useChannelsTableStore());
const mockChannels = [
{ id: 1, name: 'Channel 1', streams: ['stream1', 'stream2'] },
{ id: 2, name: 'Channel 2', streams: ['stream3'] },
];
act(() => {
useChannelsTableStore.setState({ channels: mockChannels });
});
const streams = result.current.getChannelStreams(1);
expect(streams).toEqual(['stream1', 'stream2']);
});
it('should return empty array for channel without streams', () => {
const { result } = renderHook(() => useChannelsTableStore());
const mockChannels = [
{ id: 1, name: 'Channel 1' },
];
act(() => {
useChannelsTableStore.setState({ channels: mockChannels });
});
const streams = result.current.getChannelStreams(1);
expect(streams).toEqual([]);
});
it('should return empty array for non-existent channel', () => {
const { result } = renderHook(() => useChannelsTableStore());
const mockChannels = [
{ id: 1, name: 'Channel 1', streams: ['stream1'] },
];
act(() => {
useChannelsTableStore.setState({ channels: mockChannels });
});
const streams = result.current.getChannelStreams(999);
expect(streams).toEqual([]);
});
});
describe('setPagination', () => {
it('should update pagination', () => {
const { result } = renderHook(() => useChannelsTableStore());
const newPagination = {
pageIndex: 2,
pageSize: 100,
};
act(() => {
result.current.setPagination(newPagination);
});
expect(result.current.pagination).toEqual(newPagination);
});
it('should update only changed pagination properties', () => {
const { result } = renderHook(() => useChannelsTableStore());
act(() => {
result.current.setPagination({
pageIndex: 5,
pageSize: 50,
});
});
expect(result.current.pagination.pageIndex).toBe(5);
expect(result.current.pagination.pageSize).toBe(50);
});
});
describe('setSorting', () => {
it('should update sorting', () => {
const { result } = renderHook(() => useChannelsTableStore());
const newSorting = [{ id: 'name', desc: true }];
act(() => {
result.current.setSorting(newSorting);
});
expect(result.current.sorting).toEqual(newSorting);
});
it('should handle multiple sorting columns', () => {
const { result } = renderHook(() => useChannelsTableStore());
const newSorting = [
{ id: 'name', desc: false },
{ id: 'channel_number', desc: true },
];
act(() => {
result.current.setSorting(newSorting);
});
expect(result.current.sorting).toEqual(newSorting);
});
it('should clear sorting when empty array is passed', () => {
const { result } = renderHook(() => useChannelsTableStore());
act(() => {
result.current.setSorting([{ id: 'name', desc: true }]);
});
expect(result.current.sorting).toHaveLength(1);
act(() => {
result.current.setSorting([]);
});
expect(result.current.sorting).toEqual([]);
});
});
});

View file

@ -0,0 +1,687 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import useEPGsStore from '../epgs';
import api from '../../api';
// Mock the api module
vi.mock('../../api');
describe('useEPGsStore', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset store state between tests
useEPGsStore.setState({
epgs: {},
tvgs: [],
tvgsById: {},
tvgsLoaded: false,
isLoading: false,
error: null,
refreshProgress: {},
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('initial state', () => {
it('should initialize with default values', () => {
const { result } = renderHook(() => useEPGsStore());
expect(result.current.epgs).toEqual({});
expect(result.current.tvgs).toEqual([]);
expect(result.current.tvgsById).toEqual({});
expect(result.current.tvgsLoaded).toBe(false);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
expect(result.current.refreshProgress).toEqual({});
});
});
describe('fetchEPGs', () => {
it('should fetch and store EPGs successfully', async () => {
const mockEPGs = [
{ id: 'epg1', name: 'EPG 1', status: 'idle' },
{ id: 'epg2', name: 'EPG 2', status: 'success' },
];
api.getEPGs.mockResolvedValue(mockEPGs);
const { result } = renderHook(() => useEPGsStore());
await act(async () => {
await result.current.fetchEPGs();
});
expect(result.current.epgs).toEqual({
epg1: { id: 'epg1', name: 'EPG 1', status: 'idle' },
epg2: { id: 'epg2', name: 'EPG 2', status: 'success' },
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
expect(api.getEPGs).toHaveBeenCalledTimes(1);
});
it('should set loading state while fetching', async () => {
api.getEPGs.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve([]), 100)));
const { result } = renderHook(() => useEPGsStore());
const fetchPromise = act(async () => {
await result.current.fetchEPGs();
});
// Check loading state immediately after calling
await waitFor(() => {
expect(result.current.isLoading).toBe(true);
});
await fetchPromise;
expect(result.current.isLoading).toBe(false);
});
it('should handle fetch error', async () => {
const mockError = new Error('Network error');
api.getEPGs.mockRejectedValue(mockError);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useEPGsStore());
await act(async () => {
await result.current.fetchEPGs();
});
expect(result.current.error).toBe('Failed to load epgs.');
expect(result.current.isLoading).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch epgs:', mockError);
consoleSpy.mockRestore();
});
it('should handle empty EPGs array', async () => {
api.getEPGs.mockResolvedValue([]);
const { result } = renderHook(() => useEPGsStore());
await act(async () => {
await result.current.fetchEPGs();
});
expect(result.current.epgs).toEqual({});
expect(result.current.isLoading).toBe(false);
});
});
describe('fetchEPGData', () => {
it('should fetch and store TVG data successfully', async () => {
const mockTVGs = [
{ id: 'tvg1', name: 'TVG 1' },
{ id: 'tvg2', name: 'TVG 2' },
];
api.getEPGData.mockResolvedValue(mockTVGs);
const { result } = renderHook(() => useEPGsStore());
await act(async () => {
await result.current.fetchEPGData();
});
expect(result.current.tvgs).toEqual(mockTVGs);
expect(result.current.tvgsById).toEqual({
tvg1: { id: 'tvg1', name: 'TVG 1' },
tvg2: { id: 'tvg2', name: 'TVG 2' },
});
expect(result.current.tvgsLoaded).toBe(true);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
expect(api.getEPGData).toHaveBeenCalledTimes(1);
});
it('should set loading state while fetching', async () => {
api.getEPGData.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve([]), 100)));
const { result } = renderHook(() => useEPGsStore());
const fetchPromise = act(async () => {
await result.current.fetchEPGData();
});
await waitFor(() => {
expect(result.current.isLoading).toBe(true);
});
await fetchPromise;
expect(result.current.isLoading).toBe(false);
});
it('should handle fetch error', async () => {
const mockError = new Error('API error');
api.getEPGData.mockRejectedValue(mockError);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useEPGsStore());
await act(async () => {
await result.current.fetchEPGData();
});
expect(result.current.error).toBe('Failed to load tvgs.');
expect(result.current.tvgsLoaded).toBe(true);
expect(result.current.isLoading).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch tvgs:', mockError);
consoleSpy.mockRestore();
});
it('should handle empty TVG array', async () => {
api.getEPGData.mockResolvedValue([]);
const { result } = renderHook(() => useEPGsStore());
await act(async () => {
await result.current.fetchEPGData();
});
expect(result.current.tvgs).toEqual([]);
expect(result.current.tvgsById).toEqual({});
expect(result.current.tvgsLoaded).toBe(true);
});
});
describe('addEPG', () => {
it('should add new EPG to store', () => {
const { result } = renderHook(() => useEPGsStore());
const newEPG = { id: 'epg1', name: 'New EPG', status: 'idle' };
act(() => {
result.current.addEPG(newEPG);
});
expect(result.current.epgs).toEqual({
epg1: newEPG,
});
});
it('should add multiple EPGs', () => {
const { result } = renderHook(() => useEPGsStore());
const epg1 = { id: 'epg1', name: 'EPG 1', status: 'idle' };
const epg2 = { id: 'epg2', name: 'EPG 2', status: 'success' };
act(() => {
result.current.addEPG(epg1);
result.current.addEPG(epg2);
});
expect(result.current.epgs).toEqual({
epg1,
epg2,
});
});
it('should not overwrite existing EPGs', () => {
const { result } = renderHook(() => useEPGsStore());
const originalEPG = { id: 'epg1', name: 'Original', status: 'idle' };
const newEPG = { id: 'epg2', name: 'New', status: 'success' };
act(() => {
result.current.addEPG(originalEPG);
result.current.addEPG(newEPG);
});
expect(result.current.epgs).toEqual({
epg1: originalEPG,
epg2: newEPG,
});
});
});
describe('updateEPG', () => {
it('should update existing EPG', () => {
const { result } = renderHook(() => useEPGsStore());
const originalEPG = { id: 'epg1', name: 'Original', status: 'idle' };
const updatedEPG = { id: 'epg1', name: 'Updated', status: 'success' };
act(() => {
result.current.addEPG(originalEPG);
});
act(() => {
result.current.updateEPG(updatedEPG);
});
expect(result.current.epgs.epg1).toEqual(updatedEPG);
});
it('should add EPG if it does not exist', () => {
const { result } = renderHook(() => useEPGsStore());
const newEPG = { id: 'epg1', name: 'New', status: 'idle' };
act(() => {
result.current.updateEPG(newEPG);
});
expect(result.current.epgs.epg1).toEqual(newEPG);
});
it('should not update state when called with invalid epg (null)', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useEPGsStore());
const initialEPGs = { epg1: { id: 'epg1', name: 'Test' } };
act(() => {
useEPGsStore.setState({ epgs: initialEPGs });
});
act(() => {
result.current.updateEPG(null);
});
expect(result.current.epgs).toEqual(initialEPGs);
expect(consoleSpy).toHaveBeenCalledWith('updateEPG called with invalid epg:', null);
consoleSpy.mockRestore();
});
it('should not update state when called with invalid epg (missing id)', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useEPGsStore());
const initialEPGs = { epg1: { id: 'epg1', name: 'Test' } };
act(() => {
useEPGsStore.setState({ epgs: initialEPGs });
});
const invalidEPG = { name: 'No ID' };
act(() => {
result.current.updateEPG(invalidEPG);
});
expect(result.current.epgs).toEqual(initialEPGs);
expect(consoleSpy).toHaveBeenCalledWith('updateEPG called with invalid epg:', invalidEPG);
consoleSpy.mockRestore();
});
it('should not update state when called with invalid epg (non-object)', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useEPGsStore());
const initialEPGs = { epg1: { id: 'epg1', name: 'Test' } };
act(() => {
useEPGsStore.setState({ epgs: initialEPGs });
});
act(() => {
result.current.updateEPG('invalid');
});
expect(result.current.epgs).toEqual(initialEPGs);
expect(consoleSpy).toHaveBeenCalledWith('updateEPG called with invalid epg:', 'invalid');
consoleSpy.mockRestore();
});
});
describe('removeEPGs', () => {
it('should remove single EPG', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
useEPGsStore.setState({
epgs: {
epg1: { id: 'epg1', name: 'EPG 1' },
epg2: { id: 'epg2', name: 'EPG 2' },
},
});
});
act(() => {
result.current.removeEPGs(['epg1']);
});
expect(result.current.epgs).toEqual({
epg2: { id: 'epg2', name: 'EPG 2' },
});
});
it('should remove multiple EPGs', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
useEPGsStore.setState({
epgs: {
epg1: { id: 'epg1', name: 'EPG 1' },
epg2: { id: 'epg2', name: 'EPG 2' },
epg3: { id: 'epg3', name: 'EPG 3' },
},
});
});
act(() => {
result.current.removeEPGs(['epg1', 'epg3']);
});
expect(result.current.epgs).toEqual({
epg2: { id: 'epg2', name: 'EPG 2' },
});
});
it('should handle removing non-existent EPG', () => {
const { result } = renderHook(() => useEPGsStore());
const initialEPGs = {
epg1: { id: 'epg1', name: 'EPG 1' },
};
act(() => {
useEPGsStore.setState({ epgs: initialEPGs });
});
act(() => {
result.current.removeEPGs(['nonexistent']);
});
expect(result.current.epgs).toEqual(initialEPGs);
});
it('should handle empty array', () => {
const { result } = renderHook(() => useEPGsStore());
const initialEPGs = {
epg1: { id: 'epg1', name: 'EPG 1' },
};
act(() => {
useEPGsStore.setState({ epgs: initialEPGs });
});
act(() => {
result.current.removeEPGs([]);
});
expect(result.current.epgs).toEqual(initialEPGs);
});
});
describe('updateEPGProgress', () => {
beforeEach(() => {
act(() => {
useEPGsStore.setState({
epgs: {
source1: { id: 'source1', status: 'idle', last_message: '' },
},
});
});
});
it('should update progress for downloading action', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
result.current.updateEPGProgress({
source: 'source1',
action: 'downloading',
progress: 50,
speed: '1.5 MB/s',
elapsed_time: '00:00:30',
time_remaining: '00:00:30',
});
});
expect(result.current.refreshProgress.source1).toEqual({
action: 'downloading',
progress: 50,
speed: '1.5 MB/s',
elapsed_time: '00:00:30',
time_remaining: '00:00:30',
status: 'in_progress',
});
expect(result.current.epgs.source1.status).toBe('fetching');
});
it('should update progress for parsing_channels action', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
result.current.updateEPGProgress({
source: 'source1',
action: 'parsing_channels',
progress: 75,
});
});
expect(result.current.epgs.source1.status).toBe('parsing');
});
it('should update progress for parsing_programs action', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
result.current.updateEPGProgress({
source: 'source1',
action: 'parsing_programs',
progress: 90,
});
});
expect(result.current.epgs.source1.status).toBe('parsing');
});
it('should set status to success when progress is 100', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
result.current.updateEPGProgress({
source: 'source1',
action: 'success',
progress: 100,
});
});
expect(result.current.epgs.source1.status).toBe('success');
});
it('should use explicit status from data', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
result.current.updateEPGProgress({
source: 'source1',
status: 'error',
progress: 50,
});
});
expect(result.current.epgs.source1.status).toBe('error');
expect(result.current.refreshProgress.source1.status).toBe('error');
});
it('should set last_message on error status', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
result.current.updateEPGProgress({
source: 'source1',
status: 'error',
error: 'Connection failed',
});
});
expect(result.current.epgs.source1.last_message).toBe('Connection failed');
});
it('should use default error message if error is not provided', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
result.current.updateEPGProgress({
source: 'source1',
status: 'error',
});
});
expect(result.current.epgs.source1.last_message).toBe('Unknown error');
});
it('should not update state when called with invalid data (null)', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useEPGsStore());
const initialEPGs = { ...result.current.epgs };
const initialProgress = { ...result.current.refreshProgress };
act(() => {
result.current.updateEPGProgress(null);
});
expect(result.current.epgs).toEqual(initialEPGs);
expect(result.current.refreshProgress).toEqual(initialProgress);
expect(consoleSpy).toHaveBeenCalledWith('updateEPGProgress called with invalid data:', null);
consoleSpy.mockRestore();
});
it('should not update state when called with invalid data (missing source)', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useEPGsStore());
const initialEPGs = { ...result.current.epgs };
const initialProgress = { ...result.current.refreshProgress };
act(() => {
result.current.updateEPGProgress({ progress: 50 });
});
expect(result.current.epgs).toEqual(initialEPGs);
expect(result.current.refreshProgress).toEqual(initialProgress);
expect(consoleSpy).toHaveBeenCalledWith('updateEPGProgress called with invalid data:', { progress: 50 });
consoleSpy.mockRestore();
});
it('should not update state when source does not exist and no status', () => {
const { result } = renderHook(() => useEPGsStore());
const initialEPGs = { ...result.current.epgs };
const initialProgress = { ...result.current.refreshProgress };
act(() => {
result.current.updateEPGProgress({
source: 'nonexistent',
progress: 50,
});
});
expect(result.current.epgs).toEqual(initialEPGs);
expect(result.current.refreshProgress).toEqual(initialProgress);
});
it('should update refreshProgress even when source does not exist but status is provided', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
result.current.updateEPGProgress({
source: 'newSource',
status: 'success',
progress: 100,
});
});
expect(result.current.refreshProgress.newSource).toEqual({
action: undefined,
progress: 100,
speed: undefined,
elapsed_time: undefined,
time_remaining: undefined,
status: 'success',
});
});
it('should not update EPG if status and last_message have not changed', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
useEPGsStore.setState({
epgs: {
source1: { id: 'source1', status: 'fetching', last_message: '' },
},
});
});
const epgsBeforeUpdate = result.current.epgs;
act(() => {
result.current.updateEPGProgress({
source: 'source1',
action: 'downloading',
progress: 25,
});
});
// EPGs object should be the same reference (not updated)
expect(result.current.epgs).toBe(epgsBeforeUpdate);
// But refreshProgress should be updated
expect(result.current.refreshProgress.source1.progress).toBe(25);
});
it('should update EPG if status changed', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
useEPGsStore.setState({
epgs: {
source1: { id: 'source1', status: 'idle', last_message: '' },
},
});
});
const epgsBeforeUpdate = result.current.epgs;
act(() => {
result.current.updateEPGProgress({
source: 'source1',
action: 'downloading',
progress: 25,
});
});
// EPGs object should be different (updated) because status changed from 'idle' to 'fetching'
expect(result.current.epgs).not.toBe(epgsBeforeUpdate);
expect(result.current.epgs.source1.status).toBe('fetching');
});
it('should preserve current EPG status when no status change is detected', () => {
const { result } = renderHook(() => useEPGsStore());
act(() => {
useEPGsStore.setState({
epgs: {
source1: { id: 'source1', status: 'parsing', last_message: 'Processing' },
},
});
});
act(() => {
result.current.updateEPGProgress({
source: 'source1',
action: 'parsing_programs',
progress: 85,
});
});
expect(result.current.epgs.source1.status).toBe('parsing');
expect(result.current.epgs.source1.last_message).toBe('Processing');
});
});
});

View file

@ -0,0 +1,916 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import useLogosStore from '../logos';
import api from '../../api';
// Mock the api module
vi.mock('../../api');
describe('useLogosStore', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset store state between tests
useLogosStore.setState({
logos: {},
channelLogos: {},
isLoading: false,
backgroundLoading: false,
hasLoadedAll: false,
hasLoadedChannelLogos: false,
error: null,
allowLogoRendering: false,
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('initial state', () => {
it('should initialize with default values', () => {
const { result } = renderHook(() => useLogosStore());
expect(result.current.logos).toEqual({});
expect(result.current.channelLogos).toEqual({});
expect(result.current.isLoading).toBe(false);
expect(result.current.backgroundLoading).toBe(false);
expect(result.current.hasLoadedAll).toBe(false);
expect(result.current.hasLoadedChannelLogos).toBe(false);
expect(result.current.error).toBeNull();
expect(result.current.allowLogoRendering).toBe(false);
});
});
describe('enableLogoRendering', () => {
it('should enable logo rendering', () => {
const { result } = renderHook(() => useLogosStore());
act(() => {
result.current.enableLogoRendering();
});
expect(result.current.allowLogoRendering).toBe(true);
});
});
describe('addLogo', () => {
it('should add logo to main logos store', () => {
const { result } = renderHook(() => useLogosStore());
const newLogo = { id: 'logo1', name: 'Logo 1', url: 'http://example.com/logo1.png' };
act(() => {
result.current.addLogo(newLogo);
});
expect(result.current.logos).toEqual({
logo1: newLogo,
});
});
it('should add logo to channelLogos if hasLoadedChannelLogos is true', () => {
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ hasLoadedChannelLogos: true });
});
const newLogo = { id: 'logo1', name: 'Logo 1', url: 'http://example.com/logo1.png' };
act(() => {
result.current.addLogo(newLogo);
});
expect(result.current.logos).toEqual({ logo1: newLogo });
expect(result.current.channelLogos).toEqual({ logo1: newLogo });
});
it('should not add logo to channelLogos if hasLoadedChannelLogos is false', () => {
const { result } = renderHook(() => useLogosStore());
const newLogo = { id: 'logo1', name: 'Logo 1', url: 'http://example.com/logo1.png' };
act(() => {
result.current.addLogo(newLogo);
});
expect(result.current.logos).toEqual({ logo1: newLogo });
expect(result.current.channelLogos).toEqual({});
});
});
describe('updateLogo', () => {
it('should update logo in main logos store', () => {
const { result } = renderHook(() => useLogosStore());
const originalLogo = { id: 'logo1', name: 'Original', url: 'http://example.com/original.png' };
const updatedLogo = { id: 'logo1', name: 'Updated', url: 'http://example.com/updated.png' };
act(() => {
useLogosStore.setState({ logos: { logo1: originalLogo } });
});
act(() => {
result.current.updateLogo(updatedLogo);
});
expect(result.current.logos.logo1).toEqual(updatedLogo);
});
it('should update logo in channelLogos if it exists there', () => {
const { result } = renderHook(() => useLogosStore());
const originalLogo = { id: 'logo1', name: 'Original', url: 'http://example.com/original.png' };
const updatedLogo = { id: 'logo1', name: 'Updated', url: 'http://example.com/updated.png' };
act(() => {
useLogosStore.setState({
logos: { logo1: originalLogo },
channelLogos: { logo1: originalLogo },
});
});
act(() => {
result.current.updateLogo(updatedLogo);
});
expect(result.current.logos.logo1).toEqual(updatedLogo);
expect(result.current.channelLogos.logo1).toEqual(updatedLogo);
});
it('should not update channelLogos if logo does not exist there', () => {
const { result } = renderHook(() => useLogosStore());
const originalLogo = { id: 'logo1', name: 'Original', url: 'http://example.com/original.png' };
const updatedLogo = { id: 'logo1', name: 'Updated', url: 'http://example.com/updated.png' };
act(() => {
useLogosStore.setState({ logos: { logo1: originalLogo } });
});
act(() => {
result.current.updateLogo(updatedLogo);
});
expect(result.current.channelLogos).toEqual({});
});
});
describe('removeLogo', () => {
it('should remove logo from both stores', () => {
const { result } = renderHook(() => useLogosStore());
const logo1 = { id: 'logo1', name: 'Logo 1' };
const logo2 = { id: 'logo2', name: 'Logo 2' };
act(() => {
useLogosStore.setState({
logos: { logo1, logo2 },
channelLogos: { logo1, logo2 },
});
});
act(() => {
result.current.removeLogo('logo1');
});
expect(result.current.logos).toEqual({ logo2 });
expect(result.current.channelLogos).toEqual({ logo2 });
});
it('should handle removing non-existent logo', () => {
const { result } = renderHook(() => useLogosStore());
const logo1 = { id: 'logo1', name: 'Logo 1' };
act(() => {
useLogosStore.setState({ logos: { logo1 } });
});
act(() => {
result.current.removeLogo('nonexistent');
});
expect(result.current.logos).toEqual({ logo1 });
});
});
describe('fetchLogos', () => {
it('should fetch logos successfully with array response', async () => {
const mockLogos = [
{ id: 'logo1', name: 'Logo 1' },
{ id: 'logo2', name: 'Logo 2' },
];
api.getLogos.mockResolvedValue(mockLogos);
const { result } = renderHook(() => useLogosStore());
let response;
await act(async () => {
response = await result.current.fetchLogos(100);
});
expect(result.current.logos).toEqual({
logo1: { id: 'logo1', name: 'Logo 1' },
logo2: { id: 'logo2', name: 'Logo 2' },
});
expect(result.current.isLoading).toBe(false);
expect(api.getLogos).toHaveBeenCalledWith({ page_size: 100 });
expect(response).toEqual(mockLogos);
});
it('should fetch logos successfully with paginated response', async () => {
const mockResponse = {
results: [
{ id: 'logo1', name: 'Logo 1' },
{ id: 'logo2', name: 'Logo 2' },
],
count: 2,
};
api.getLogos.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useLogosStore());
await act(async () => {
await result.current.fetchLogos(50);
});
expect(result.current.logos).toEqual({
logo1: { id: 'logo1', name: 'Logo 1' },
logo2: { id: 'logo2', name: 'Logo 2' },
});
expect(api.getLogos).toHaveBeenCalledWith({ page_size: 50 });
});
it('should handle fetch error', async () => {
const mockError = new Error('Network error');
api.getLogos.mockRejectedValue(mockError);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useLogosStore());
await expect(
act(async () => {
await result.current.fetchLogos();
})
).rejects.toThrow('Network error');
await waitFor(() => {
expect(result.current.error).toBe('Failed to load logos.');
expect(result.current.isLoading).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch logos:', mockError);
});
consoleSpy.mockRestore();
});
});
describe('fetchAllLogos', () => {
it('should fetch all logos successfully', async () => {
const mockLogos = [
{ id: 'logo1', name: 'Logo 1' },
{ id: 'logo2', name: 'Logo 2' },
];
api.getLogos.mockResolvedValue(mockLogos);
const { result } = renderHook(() => useLogosStore());
let response;
await act(async () => {
response = await result.current.fetchAllLogos();
});
expect(result.current.logos).toEqual({
logo1: { id: 'logo1', name: 'Logo 1' },
logo2: { id: 'logo2', name: 'Logo 2' },
});
expect(result.current.hasLoadedAll).toBe(true);
expect(result.current.isLoading).toBe(false);
expect(api.getLogos).toHaveBeenCalledWith({ no_pagination: 'true' });
expect(response).toEqual(mockLogos);
});
it('should not refetch if already loaded and not forced', async () => {
const mockLogos = [{ id: 'logo1', name: 'Logo 1' }];
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({
logos: { logo1: mockLogos[0] },
hasLoadedAll: true,
});
});
const response = await act(async () => {
return await result.current.fetchAllLogos();
});
expect(api.getLogos).not.toHaveBeenCalled();
expect(response).toEqual([mockLogos[0]]);
});
it('should refetch if forced', async () => {
const mockLogos = [{ id: 'logo1', name: 'Logo 1' }];
api.getLogos.mockResolvedValue(mockLogos);
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({
logos: { logo1: mockLogos[0] },
hasLoadedAll: true,
});
});
await act(async () => {
await result.current.fetchAllLogos(true);
});
expect(api.getLogos).toHaveBeenCalledWith({ no_pagination: 'true' });
});
it('should not refetch if already loading', async () => {
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ isLoading: true });
});
const response = await act(async () => {
return await result.current.fetchAllLogos();
});
expect(api.getLogos).not.toHaveBeenCalled();
expect(response).toEqual([]);
});
it('should handle fetch error', async () => {
const mockError = new Error('API error');
api.getLogos.mockRejectedValue(mockError);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useLogosStore());
await expect(
act(async () => {
await result.current.fetchAllLogos();
})
).rejects.toThrow('API error');
await waitFor(() => {
expect(result.current.error).toBe('Failed to load all logos.');
expect(result.current.isLoading).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch all logos:', mockError);
});
consoleSpy.mockRestore();
});
});
describe('fetchUsedLogos', () => {
it('should fetch used logos successfully', async () => {
const mockResponse = {
results: [
{ id: 'logo1', name: 'Used Logo 1' },
{ id: 'logo2', name: 'Used Logo 2' },
],
};
api.getLogos.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useLogosStore());
let response;
await act(async () => {
response = await result.current.fetchUsedLogos(100);
});
expect(result.current.logos).toEqual({
logo1: { id: 'logo1', name: 'Used Logo 1' },
logo2: { id: 'logo2', name: 'Used Logo 2' },
});
expect(result.current.isLoading).toBe(false);
expect(api.getLogos).toHaveBeenCalledWith({ used: 'true', page_size: 100 });
expect(response).toEqual(mockResponse);
});
it('should merge with existing logos', async () => {
const existingLogo = { id: 'logo1', name: 'Existing Logo' };
const newLogo = { id: 'logo2', name: 'New Logo' };
api.getLogos.mockResolvedValue({ results: [newLogo] });
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ logos: { logo1: existingLogo } });
});
await act(async () => {
await result.current.fetchUsedLogos();
});
expect(result.current.logos).toEqual({
logo1: existingLogo,
logo2: newLogo,
});
});
it('should handle fetch error', async () => {
const mockError = new Error('Fetch error');
api.getLogos.mockRejectedValue(mockError);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useLogosStore());
await expect(
act(async () => {
await result.current.fetchUsedLogos();
})
).rejects.toThrow('Fetch error');
await waitFor(() => {
expect(result.current.error).toBe('Failed to load used logos.');
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch used logos:', mockError);
});
consoleSpy.mockRestore();
});
});
describe('fetchChannelAssignableLogos', () => {
it('should return cached logos if already loaded', async () => {
const cachedLogos = {
logo1: { id: 'logo1', name: 'Cached Logo' },
};
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({
channelLogos: cachedLogos,
hasLoadedChannelLogos: true,
});
});
const response = await act(async () => {
return await result.current.fetchChannelAssignableLogos();
});
expect(api.getLogos).not.toHaveBeenCalled();
expect(response).toEqual([cachedLogos.logo1]);
});
it('should fetch and cache logos if not loaded', async () => {
const mockLogos = [
{ id: 'logo1', name: 'Logo 1' },
{ id: 'logo2', name: 'Logo 2' },
];
api.getLogos.mockResolvedValue(mockLogos);
const { result } = renderHook(() => useLogosStore());
await act(async () => {
await result.current.fetchChannelAssignableLogos();
});
expect(result.current.channelLogos).toEqual({
logo1: { id: 'logo1', name: 'Logo 1' },
logo2: { id: 'logo2', name: 'Logo 2' },
});
expect(result.current.hasLoadedChannelLogos).toBe(true);
expect(api.getLogos).toHaveBeenCalledWith({ no_pagination: 'true' });
});
});
describe('fetchLogosByIds', () => {
it('should fetch missing logos by IDs', async () => {
const existingLogo = { id: 'logo1', name: 'Existing' };
const newLogo = { id: 'logo2', name: 'New' };
api.getLogosByIds.mockResolvedValue([newLogo]);
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ logos: { logo1: existingLogo } });
});
let response;
await act(async () => {
response = await result.current.fetchLogosByIds(['logo1', 'logo2']);
});
expect(api.getLogosByIds).toHaveBeenCalledWith(['logo2']);
expect(result.current.logos).toEqual({
logo1: existingLogo,
logo2: newLogo,
});
expect(response).toEqual([newLogo]);
});
it('should return empty array if all logos exist', async () => {
const logo1 = { id: 'logo1', name: 'Logo 1' };
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ logos: { logo1 } });
});
const response = await act(async () => {
return await result.current.fetchLogosByIds(['logo1']);
});
expect(api.getLogosByIds).not.toHaveBeenCalled();
expect(response).toEqual([]);
});
it('should handle fetch error', async () => {
const mockError = new Error('Fetch error');
api.getLogosByIds.mockRejectedValue(mockError);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useLogosStore());
await expect(
act(async () => {
await result.current.fetchLogosByIds(['logo1']);
})
).rejects.toThrow('Fetch error');
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch logos by IDs:', mockError);
consoleSpy.mockRestore();
});
});
describe('fetchLogosInBackground', () => {
it('should fetch logos in background with pagination', async () => {
// vi.useRealTimers();
const page1 = {
results: [{ id: 'logo1', name: 'Logo 1' }],
next: 'http://example.com/page2',
};
const page2 = {
results: [{ id: 'logo2', name: 'Logo 2' }],
next: null,
};
api.getLogos
.mockResolvedValueOnce(page1)
.mockResolvedValueOnce(page2);
const { result } = renderHook(() => useLogosStore());
await act(async () => {
await result.current.fetchLogosInBackground();
});
expect(result.current.logos).toEqual({
logo1: { id: 'logo1', name: 'Logo 1' },
logo2: { id: 'logo2', name: 'Logo 2' },
});
expect(result.current.backgroundLoading).toBe(false);
expect(api.getLogos).toHaveBeenCalledTimes(2);
expect(api.getLogos).toHaveBeenCalledWith({ page: 1, page_size: 200 });
expect(api.getLogos).toHaveBeenCalledWith({ page: 2, page_size: 200 });
});
it('should handle errors gracefully without throwing', async () => {
const mockError = new Error('Network error');
api.getLogos.mockRejectedValue(mockError);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useLogosStore());
await act(async () => {
await result.current.fetchLogosInBackground();
});
expect(result.current.backgroundLoading).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith('Background logo loading failed:', mockError);
consoleSpy.mockRestore();
});
});
describe('backgroundLoadAllLogos', () => {
it('should not start if already loading', async () => {
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ backgroundLoading: true });
});
await act(async () => {
await result.current.backgroundLoadAllLogos();
});
expect(api.getLogos).not.toHaveBeenCalled();
});
it('should not start if already loaded', async () => {
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ hasLoadedAll: true });
});
await act(async () => {
await result.current.backgroundLoadAllLogos();
});
expect(api.getLogos).not.toHaveBeenCalled();
});
it('should load logos in background asynchronously', async () => {
vi.useFakeTimers();
const mockLogos = Array.from({ length: 2500 }, (_, i) => ({
id: `logo${i}`,
name: `Logo ${i}`,
}));
api.getLogos.mockResolvedValue(mockLogos);
const { result } = renderHook(() => useLogosStore());
// Start background loading
result.current.backgroundLoadAllLogos();
// Advance timers to execute setTimeout
await act(async () => {
await vi.runAllTimersAsync();
});
expect(result.current.hasLoadedAll).toBe(true);
expect(result.current.backgroundLoading).toBe(false);
expect(Object.keys(result.current.logos).length).toBe(2500);
vi.useRealTimers();
});
it('should handle errors gracefully', async () => {
vi.useFakeTimers();
const mockError = new Error('Fetch error');
api.getLogos.mockRejectedValue(mockError);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useLogosStore());
result.current.backgroundLoadAllLogos();
await act(async () => {
await vi.runAllTimersAsync();
});
expect(result.current.backgroundLoading).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith('Background all logos loading failed:', mockError);
consoleSpy.mockRestore();
vi.useRealTimers();
});
});
describe('backgroundLoadChannelLogos', () => {
it('should not start if already loading', async () => {
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ backgroundLoading: true });
});
await act(async () => {
await result.current.backgroundLoadChannelLogos();
});
expect(api.getLogos).not.toHaveBeenCalled();
});
it('should not start if already loaded', async () => {
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ hasLoadedChannelLogos: true });
});
await act(async () => {
await result.current.backgroundLoadChannelLogos();
});
expect(api.getLogos).not.toHaveBeenCalled();
});
it('should not start if channelLogos already has many items', async () => {
const channelLogos = Array.from({ length: 150 }, (_, i) => [`logo${i}`, { id: `logo${i}` }]);
const channelLogosObj = Object.fromEntries(channelLogos);
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ channelLogos: channelLogosObj });
});
await act(async () => {
await result.current.backgroundLoadChannelLogos();
});
expect(api.getLogos).not.toHaveBeenCalled();
});
it('should load channel logos in background', async () => {
const mockLogos = [
{ id: 'logo1', name: 'Logo 1' },
{ id: 'logo2', name: 'Logo 2' },
];
api.getLogos.mockResolvedValue(mockLogos);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const { result } = renderHook(() => useLogosStore());
await act(async () => {
await result.current.backgroundLoadChannelLogos();
});
expect(result.current.hasLoadedChannelLogos).toBe(true);
expect(result.current.backgroundLoading).toBe(false);
expect(Object.keys(result.current.channelLogos).length).toBe(2);
expect(consoleSpy).toHaveBeenCalledWith('Background loading channel logos...');
expect(consoleSpy).toHaveBeenCalledWith('Background loaded 2 channel logos');
consoleSpy.mockRestore();
});
it('should handle errors gracefully', async () => {
const mockError = new Error('Fetch error');
api.getLogos.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const { result } = renderHook(() => useLogosStore());
await act(async () => {
await result.current.backgroundLoadChannelLogos();
});
expect(result.current.backgroundLoading).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith('Background channel logo loading failed:', mockError);
consoleErrorSpy.mockRestore();
consoleLogSpy.mockRestore();
});
});
describe('startBackgroundLoading', () => {
it('should start background loading after delay', async () => {
vi.useFakeTimers();
const mockLogos = [{ id: 'logo1', name: 'Logo 1' }];
api.getLogos.mockResolvedValue(mockLogos);
const { result } = renderHook(() => useLogosStore());
result.current.startBackgroundLoading();
// Advance timer by 3 seconds
await act(async () => {
vi.advanceTimersByTime(3000);
await vi.runAllTimersAsync();
});
expect(result.current.hasLoadedAll).toBe(true);
vi.useRealTimers();
});
it('should handle errors in background loading', async () => {
vi.useFakeTimers();
const mockError = new Error('Background error');
api.getLogos.mockRejectedValue(mockError);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useLogosStore());
result.current.startBackgroundLoading();
await act(async () => {
vi.advanceTimersByTime(3000);
await vi.runAllTimersAsync();
});
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
vi.useRealTimers();
});
});
describe('helper methods', () => {
describe('getLogoById', () => {
it('should return logo if it exists', () => {
const logo = { id: 'logo1', name: 'Logo 1' };
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ logos: { logo1: logo } });
});
expect(result.current.getLogoById('logo1')).toEqual(logo);
});
it('should return null if logo does not exist', () => {
const { result } = renderHook(() => useLogosStore());
expect(result.current.getLogoById('nonexistent')).toBeNull();
});
});
describe('hasLogo', () => {
it('should return true if logo exists', () => {
const logo = { id: 'logo1', name: 'Logo 1' };
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ logos: { logo1: logo } });
});
expect(result.current.hasLogo('logo1')).toBe(true);
});
it('should return false if logo does not exist', () => {
const { result } = renderHook(() => useLogosStore());
expect(result.current.hasLogo('nonexistent')).toBe(false);
});
});
describe('getLogosCount', () => {
it('should return correct count of logos', () => {
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({
logos: {
logo1: { id: 'logo1' },
logo2: { id: 'logo2' },
logo3: { id: 'logo3' },
},
});
});
expect(result.current.getLogosCount()).toBe(3);
});
it('should return 0 for empty logos', () => {
const { result } = renderHook(() => useLogosStore());
expect(result.current.getLogosCount()).toBe(0);
});
});
describe('needsAllLogos', () => {
it('should return true if hasLoadedAll is false', () => {
const { result } = renderHook(() => useLogosStore());
expect(result.current.needsAllLogos()).toBe(true);
});
it('should return true if logos is empty', () => {
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({ hasLoadedAll: true, logos: {} });
});
expect(result.current.needsAllLogos()).toBe(true);
});
it('should return false if hasLoadedAll is true and logos exist', () => {
const { result } = renderHook(() => useLogosStore());
act(() => {
useLogosStore.setState({
hasLoadedAll: true,
logos: { logo1: { id: 'logo1' } },
});
});
expect(result.current.needsAllLogos()).toBe(false);
});
});
});
});

View file

@ -0,0 +1,266 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import usePlaylistsStore from '../playlists';
import api from '../../api';
vi.mock('../../api');
describe('usePlaylistsStore', () => {
beforeEach(() => {
const { result } = renderHook(() => usePlaylistsStore());
act(() => {
result.current.playlists = [];
result.current.profiles = {};
result.current.refreshProgress = {};
result.current.isLoading = false;
result.current.error = null;
result.current.profileSearchPreview = '';
result.current.profileResult = '';
result.current.editPlaylistId = null;
});
vi.clearAllMocks();
});
it('should initialize with default state', () => {
const { result } = renderHook(() => usePlaylistsStore());
expect(result.current.playlists).toEqual([]);
expect(result.current.profiles).toEqual({});
expect(result.current.refreshProgress).toEqual({});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
expect(result.current.profileSearchPreview).toBe('');
expect(result.current.profileResult).toBe('');
expect(result.current.editPlaylistId).toBe(null);
});
it('should set edit playlist id', () => {
const { result } = renderHook(() => usePlaylistsStore());
act(() => {
result.current.setEditPlaylistId('playlist1');
});
expect(result.current.editPlaylistId).toBe('playlist1');
});
it('should fetch playlist successfully', async () => {
const mockPlaylist = {
id: 'playlist1',
name: 'Test Playlist',
profiles: ['profile1', 'profile2'],
};
api.getPlaylist.mockResolvedValue(mockPlaylist);
const { result } = renderHook(() => usePlaylistsStore());
act(() => {
result.current.playlists = [{ id: 'playlist1', name: 'Old Name' }];
});
await act(async () => {
await result.current.fetchPlaylist('playlist1');
});
expect(api.getPlaylist).toHaveBeenCalledWith('playlist1');
expect(result.current.playlists).toEqual([mockPlaylist]);
expect(result.current.profiles).toEqual({ playlist1: ['profile1', 'profile2'] });
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle fetch playlist error', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
api.getPlaylist.mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() => usePlaylistsStore());
await act(async () => {
await result.current.fetchPlaylist('playlist1');
});
expect(result.current.error).toBe('Failed to load playlists.');
expect(result.current.isLoading).toBe(false);
consoleErrorSpy.mockRestore();
});
it('should fetch playlists successfully', async () => {
const mockPlaylists = [
{ id: 'playlist1', name: 'Playlist 1', profiles: ['profile1'] },
{ id: 'playlist2', name: 'Playlist 2', profiles: ['profile2'] },
];
api.getPlaylists.mockResolvedValue(mockPlaylists);
const { result } = renderHook(() => usePlaylistsStore());
await act(async () => {
await result.current.fetchPlaylists();
});
expect(api.getPlaylists).toHaveBeenCalled();
expect(result.current.playlists).toEqual(mockPlaylists);
expect(result.current.profiles).toEqual({
playlist1: ['profile1'],
playlist2: ['profile2'],
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle fetch playlists error', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
api.getPlaylists.mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() => usePlaylistsStore());
await act(async () => {
await result.current.fetchPlaylists();
});
expect(result.current.error).toBe('Failed to load playlists.');
expect(result.current.isLoading).toBe(false);
consoleErrorSpy.mockRestore();
});
it('should add playlist', () => {
const { result } = renderHook(() => usePlaylistsStore());
const newPlaylist = {
id: 'playlist1',
name: 'New Playlist',
profiles: ['profile1'],
};
act(() => {
result.current.addPlaylist(newPlaylist);
});
expect(result.current.playlists).toEqual([newPlaylist]);
expect(result.current.profiles).toEqual({ playlist1: ['profile1'] });
});
it('should update playlist', () => {
const { result } = renderHook(() => usePlaylistsStore());
const existingPlaylist = { id: 'playlist1', name: 'Old Name', profiles: ['profile1'] };
const updatedPlaylist = { id: 'playlist1', name: 'New Name', profiles: ['profile1', 'profile2'] };
act(() => {
result.current.playlists = [existingPlaylist];
result.current.profiles = { playlist1: ['profile1'] };
});
act(() => {
result.current.updatePlaylist(updatedPlaylist);
});
expect(result.current.playlists).toEqual([updatedPlaylist]);
expect(result.current.profiles).toEqual({ playlist1: ['profile1', 'profile2'] });
});
it('should update profiles', () => {
const { result } = renderHook(() => usePlaylistsStore());
act(() => {
result.current.profiles = { playlist1: ['profile1'] };
});
act(() => {
result.current.updateProfiles('playlist1', ['profile1', 'profile2', 'profile3']);
});
expect(result.current.profiles).toEqual({ playlist1: ['profile1', 'profile2', 'profile3'] });
});
it('should remove playlists', () => {
const { result } = renderHook(() => usePlaylistsStore());
act(() => {
result.current.playlists = [
{ id: 'playlist1', name: 'Playlist 1' },
{ id: 'playlist2', name: 'Playlist 2' },
{ id: 'playlist3', name: 'Playlist 3' },
];
});
act(() => {
result.current.removePlaylists(['playlist1', 'playlist3']);
});
expect(result.current.playlists).toEqual([{ id: 'playlist2', name: 'Playlist 2' }]);
});
it('should set refresh progress with two parameters', () => {
const { result } = renderHook(() => usePlaylistsStore());
const progressData = { action: 'refreshing', progress: 50 };
act(() => {
result.current.setRefreshProgress('account1', progressData);
});
expect(result.current.refreshProgress).toEqual({ account1: progressData });
});
it('should set refresh progress with WebSocket data', () => {
const { result } = renderHook(() => usePlaylistsStore());
const wsData = { account: 'account1', action: 'refreshing', progress: 50 };
act(() => {
result.current.setRefreshProgress(wsData);
});
expect(result.current.refreshProgress).toEqual({ account1: wsData });
});
it('should preserve initializing status until real progress', () => {
const { result } = renderHook(() => usePlaylistsStore());
act(() => {
result.current.refreshProgress = {
account1: { action: 'initializing', progress: 0 },
};
});
act(() => {
result.current.setRefreshProgress({ account: 'account1', progress: 0 });
});
expect(result.current.refreshProgress.account1.action).toBe('initializing');
act(() => {
result.current.setRefreshProgress({ account: 'account1', action: 'refreshing', progress: 25 });
});
expect(result.current.refreshProgress.account1.action).toBe('refreshing');
});
it('should remove refresh progress', () => {
const { result } = renderHook(() => usePlaylistsStore());
act(() => {
result.current.refreshProgress = {
account1: { action: 'refreshing', progress: 50 },
account2: { action: 'refreshing', progress: 75 },
};
});
act(() => {
result.current.removeRefreshProgress('account1');
});
expect(result.current.refreshProgress).toEqual({
account2: { action: 'refreshing', progress: 75 },
});
});
it('should set profile preview', () => {
const { result } = renderHook(() => usePlaylistsStore());
act(() => {
result.current.setProfilePreview('search text', 'result data');
});
expect(result.current.profileSearchPreview).toBe('search text');
expect(result.current.profileResult).toBe('result data');
});
});

View file

@ -0,0 +1,234 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { usePluginStore } from '../plugins';
import API from '../../api';
vi.mock('../../api');
describe('usePluginStore', () => {
beforeEach(() => {
vi.clearAllMocks();
usePluginStore.setState({
plugins: [],
loading: false,
error: null,
});
});
it('should initialize with default state', () => {
const { result } = renderHook(() => usePluginStore());
expect(result.current.plugins).toEqual([]);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should fetch plugins successfully', async () => {
const mockPlugins = [
{ key: 'plugin1', name: 'Plugin 1', enabled: true },
{ key: 'plugin2', name: 'Plugin 2', enabled: false },
];
API.getPlugins.mockResolvedValue(mockPlugins);
const { result } = renderHook(() => usePluginStore());
await act(async () => {
await result.current.fetchPlugins();
});
expect(API.getPlugins).toHaveBeenCalled();
expect(result.current.plugins).toEqual(mockPlugins);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle fetch plugins with empty response', async () => {
API.getPlugins.mockResolvedValue(null);
const { result } = renderHook(() => usePluginStore());
await act(async () => {
await result.current.fetchPlugins();
});
expect(result.current.plugins).toEqual([]);
expect(result.current.loading).toBe(false);
});
it('should handle fetch plugins error', async () => {
const mockError = new Error('Network error');
API.getPlugins.mockRejectedValue(mockError);
const { result } = renderHook(() => usePluginStore());
await act(async () => {
await result.current.fetchPlugins();
});
expect(result.current.error).toEqual(mockError);
expect(result.current.loading).toBe(false);
});
it('should set loading state during fetch', async () => {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
API.getPlugins.mockReturnValue(promise);
const { result } = renderHook(() => usePluginStore());
// Start the fetch without awaiting
act(() => {
result.current.fetchPlugins();
});
// Check loading is true synchronously
expect(result.current.loading).toBe(true);
// Resolve the promise and wait for state update
await act(async () => {
resolvePromise([]);
await promise;
});
expect(result.current.loading).toBe(false);
});
it('should update plugin', () => {
const { result } = renderHook(() => usePluginStore());
act(() => {
usePluginStore.setState({
plugins: [
{ key: 'plugin1', name: 'Plugin 1', enabled: false },
{ key: 'plugin2', name: 'Plugin 2', enabled: false },
],
});
});
act(() => {
result.current.updatePlugin('plugin1', { enabled: true });
});
expect(result.current.plugins).toEqual([
{ key: 'plugin1', name: 'Plugin 1', enabled: true },
{ key: 'plugin2', name: 'Plugin 2', enabled: false },
]);
});
it('should not modify other plugins when updating', () => {
const { result } = renderHook(() => usePluginStore());
act(() => {
usePluginStore.setState({
plugins: [
{ key: 'plugin1', name: 'Plugin 1', enabled: false },
{ key: 'plugin2', name: 'Plugin 2', enabled: false },
],
});
});
act(() => {
result.current.updatePlugin('plugin1', { name: 'Updated Plugin' });
});
expect(result.current.plugins[1]).toEqual({ key: 'plugin2', name: 'Plugin 2', enabled: false });
});
it('should add plugin', () => {
const { result } = renderHook(() => usePluginStore());
const newPlugin = { key: 'plugin1', name: 'New Plugin', enabled: true };
act(() => {
result.current.addPlugin(newPlugin);
});
expect(result.current.plugins).toEqual([newPlugin]);
});
it('should add plugin to existing plugins', () => {
const { result } = renderHook(() => usePluginStore());
const existingPlugin = { key: 'plugin1', name: 'Existing Plugin', enabled: true };
const newPlugin = { key: 'plugin2', name: 'New Plugin', enabled: false };
act(() => {
usePluginStore.setState({ plugins: [existingPlugin] });
});
act(() => {
result.current.addPlugin(newPlugin);
});
expect(result.current.plugins).toEqual([existingPlugin, newPlugin]);
});
it('should remove plugin', () => {
const { result } = renderHook(() => usePluginStore());
act(() => {
usePluginStore.setState({
plugins: [
{ key: 'plugin1', name: 'Plugin 1', enabled: true },
{ key: 'plugin2', name: 'Plugin 2', enabled: false },
],
});
});
act(() => {
result.current.removePlugin('plugin1');
});
expect(result.current.plugins).toEqual([
{ key: 'plugin2', name: 'Plugin 2', enabled: false },
]);
});
it('should handle removing non-existent plugin', () => {
const { result } = renderHook(() => usePluginStore());
act(() => {
usePluginStore.setState({
plugins: [
{ key: 'plugin1', name: 'Plugin 1', enabled: true },
],
});
});
act(() => {
result.current.removePlugin('nonexistent');
});
expect(result.current.plugins).toEqual([
{ key: 'plugin1', name: 'Plugin 1', enabled: true },
]);
});
it('should invalidate plugins and refetch', async () => {
const mockPlugins = [
{ key: 'plugin1', name: 'Plugin 1', enabled: true },
];
API.getPlugins.mockResolvedValue(mockPlugins);
const { result } = renderHook(() => usePluginStore());
act(() => {
usePluginStore.setState({
plugins: [
{ key: 'old-plugin', name: 'Old Plugin', enabled: false },
],
});
});
await act(async () => {
await result.current.invalidatePlugins();
});
expect(result.current.plugins).toEqual(mockPlugins);
expect(API.getPlugins).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,204 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import useSettingsStore from '../settings';
import api from '../../api';
vi.mock('../../api');
describe('useSettingsStore', () => {
beforeEach(() => {
vi.clearAllMocks();
useSettingsStore.setState({
settings: {},
environment: {
public_ip: '',
country_code: '',
country_name: '',
env_mode: 'prod',
},
isLoading: false,
error: null,
});
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useSettingsStore());
expect(result.current.settings).toEqual({});
expect(result.current.environment).toEqual({
public_ip: '',
country_code: '',
country_name: '',
env_mode: 'prod',
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should fetch settings successfully', async () => {
const mockSettings = [
{ key: 'setting1', value: 'value1' },
{ key: 'setting2', value: 'value2' },
];
const mockEnv = {
public_ip: '192.168.1.1',
country_code: 'US',
country_name: 'United States',
env_mode: 'dev',
};
api.getSettings.mockResolvedValue(mockSettings);
api.getEnvironmentSettings.mockResolvedValue(mockEnv);
const { result } = renderHook(() => useSettingsStore());
await act(async () => {
await result.current.fetchSettings();
});
expect(api.getSettings).toHaveBeenCalled();
expect(api.getEnvironmentSettings).toHaveBeenCalled();
expect(result.current.settings).toEqual({
setting1: { key: 'setting1', value: 'value1' },
setting2: { key: 'setting2', value: 'value2' },
});
expect(result.current.environment).toEqual(mockEnv);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle null environment response', async () => {
const mockSettings = [{ key: 'setting1', value: 'value1' }];
api.getSettings.mockResolvedValue(mockSettings);
api.getEnvironmentSettings.mockResolvedValue(null);
const { result } = renderHook(() => useSettingsStore());
await act(async () => {
await result.current.fetchSettings();
});
expect(result.current.environment).toEqual({
public_ip: '',
country_code: '',
country_name: '',
env_mode: 'prod',
});
});
it('should handle fetch settings error', async () => {
const mockError = new Error('Network error');
api.getSettings.mockRejectedValue(mockError);
const { result } = renderHook(() => useSettingsStore());
await act(async () => {
await result.current.fetchSettings();
});
expect(result.current.error).toBe('Failed to load settings.');
expect(result.current.isLoading).toBe(false);
});
it('should set loading state during fetch', async () => {
let resolveSettingsPromise;
let resolveEnvPromise;
const settingsPromise = new Promise((resolve) => {
resolveSettingsPromise = resolve;
});
const envPromise = new Promise((resolve) => {
resolveEnvPromise = resolve;
});
api.getSettings.mockReturnValue(settingsPromise);
api.getEnvironmentSettings.mockReturnValue(envPromise);
const { result } = renderHook(() => useSettingsStore());
act(() => {
result.current.fetchSettings();
});
expect(result.current.isLoading).toBe(true);
expect(result.current.error).toBe(null);
await act(async () => {
resolveSettingsPromise([]);
resolveEnvPromise({});
await settingsPromise;
await envPromise;
});
expect(result.current.isLoading).toBe(false);
});
it('should update setting', () => {
useSettingsStore.setState({
settings: {
setting1: { key: 'setting1', value: 'old_value' },
setting2: { key: 'setting2', value: 'value2' },
},
});
const { result } = renderHook(() => useSettingsStore());
act(() => {
result.current.updateSetting({ key: 'setting1', value: 'new_value' });
});
expect(result.current.settings).toEqual({
setting1: { key: 'setting1', value: 'new_value' },
setting2: { key: 'setting2', value: 'value2' },
});
});
it('should add new setting when updating non-existent key', () => {
useSettingsStore.setState({
settings: {
setting1: { key: 'setting1', value: 'value1' },
},
});
const { result } = renderHook(() => useSettingsStore());
act(() => {
result.current.updateSetting({ key: 'setting2', value: 'new_value' });
});
expect(result.current.settings).toEqual({
setting1: { key: 'setting1', value: 'value1' },
setting2: { key: 'setting2', value: 'new_value' },
});
});
it('should not modify other settings when updating', () => {
useSettingsStore.setState({
settings: {
setting1: { key: 'setting1', value: 'value1' },
setting2: { key: 'setting2', value: 'value2' },
},
});
const { result } = renderHook(() => useSettingsStore());
act(() => {
result.current.updateSetting({ key: 'setting1', value: 'updated' });
});
expect(result.current.settings.setting2).toEqual({ key: 'setting2', value: 'value2' });
});
it('should handle empty settings array', async () => {
api.getSettings.mockResolvedValue([]);
api.getEnvironmentSettings.mockResolvedValue({});
const { result } = renderHook(() => useSettingsStore());
await act(async () => {
await result.current.fetchSettings();
});
expect(result.current.settings).toEqual({});
});
});

View file

@ -0,0 +1,265 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import useStreamProfilesStore from '../streamProfiles';
import api from '../../api';
vi.mock('../../api');
describe('useStreamProfilesStore', () => {
beforeEach(() => {
vi.clearAllMocks();
useStreamProfilesStore.setState({
profiles: [],
isLoading: false,
error: null,
});
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useStreamProfilesStore());
expect(result.current.profiles).toEqual([]);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should fetch profiles successfully', async () => {
const mockProfiles = [
{ id: 1, name: 'Profile 1', bitrate: 5000 },
{ id: 2, name: 'Profile 2', bitrate: 8000 },
];
api.getStreamProfiles.mockResolvedValue(mockProfiles);
const { result } = renderHook(() => useStreamProfilesStore());
await act(async () => {
await result.current.fetchProfiles();
});
expect(api.getStreamProfiles).toHaveBeenCalled();
expect(result.current.profiles).toEqual(mockProfiles);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle fetch profiles error', async () => {
const mockError = new Error('Network error');
api.getStreamProfiles.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useStreamProfilesStore());
await act(async () => {
await result.current.fetchProfiles();
});
expect(result.current.error).toBe('Failed to load profiles.');
expect(result.current.isLoading).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch profiles:', mockError);
consoleErrorSpy.mockRestore();
});
it('should set loading state during fetch', async () => {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
api.getStreamProfiles.mockReturnValue(promise);
const { result } = renderHook(() => useStreamProfilesStore());
act(() => {
result.current.fetchProfiles();
});
expect(result.current.isLoading).toBe(true);
expect(result.current.error).toBe(null);
await act(async () => {
resolvePromise([]);
await promise;
});
expect(result.current.isLoading).toBe(false);
});
it('should add stream profile', () => {
useStreamProfilesStore.setState({
profiles: [{ id: 1, name: 'Profile 1', bitrate: 5000 }],
});
const { result } = renderHook(() => useStreamProfilesStore());
const newProfile = { id: 2, name: 'Profile 2', bitrate: 8000 };
act(() => {
result.current.addStreamProfile(newProfile);
});
expect(result.current.profiles).toEqual([
{ id: 1, name: 'Profile 1', bitrate: 5000 },
{ id: 2, name: 'Profile 2', bitrate: 8000 },
]);
});
it('should add stream profile to empty profiles', () => {
const { result } = renderHook(() => useStreamProfilesStore());
const newProfile = { id: 1, name: 'Profile 1', bitrate: 5000 };
act(() => {
result.current.addStreamProfile(newProfile);
});
expect(result.current.profiles).toEqual([newProfile]);
});
it('should update stream profile', () => {
useStreamProfilesStore.setState({
profiles: [
{ id: 1, name: 'Profile 1', bitrate: 5000 },
{ id: 2, name: 'Profile 2', bitrate: 8000 },
],
});
const { result } = renderHook(() => useStreamProfilesStore());
const updatedProfile = { id: 1, name: 'Updated Profile', bitrate: 10000 };
act(() => {
result.current.updateStreamProfile(updatedProfile);
});
expect(result.current.profiles).toEqual([
{ id: 1, name: 'Updated Profile', bitrate: 10000 },
{ id: 2, name: 'Profile 2', bitrate: 8000 },
]);
});
it('should not modify other profiles when updating', () => {
useStreamProfilesStore.setState({
profiles: [
{ id: 1, name: 'Profile 1', bitrate: 5000 },
{ id: 2, name: 'Profile 2', bitrate: 8000 },
],
});
const { result } = renderHook(() => useStreamProfilesStore());
const updatedProfile = { id: 1, name: 'Updated Profile', bitrate: 10000 };
act(() => {
result.current.updateStreamProfile(updatedProfile);
});
expect(result.current.profiles[1]).toEqual({ id: 2, name: 'Profile 2', bitrate: 8000 });
});
it('should not modify profiles when updating non-existent profile', () => {
const initialProfiles = [
{ id: 1, name: 'Profile 1', bitrate: 5000 },
{ id: 2, name: 'Profile 2', bitrate: 8000 },
];
useStreamProfilesStore.setState({
profiles: initialProfiles,
});
const { result } = renderHook(() => useStreamProfilesStore());
const nonExistentProfile = { id: 999, name: 'Non-existent', bitrate: 10000 };
act(() => {
result.current.updateStreamProfile(nonExistentProfile);
});
expect(result.current.profiles).toEqual(initialProfiles);
});
it('should remove single stream profile', () => {
useStreamProfilesStore.setState({
profiles: [
{ id: 1, name: 'Profile 1', bitrate: 5000 },
{ id: 2, name: 'Profile 2', bitrate: 8000 },
{ id: 3, name: 'Profile 3', bitrate: 10000 },
],
});
const { result } = renderHook(() => useStreamProfilesStore());
act(() => {
result.current.removeStreamProfiles([2]);
});
expect(result.current.profiles).toEqual([
{ id: 1, name: 'Profile 1', bitrate: 5000 },
{ id: 3, name: 'Profile 3', bitrate: 10000 },
]);
});
it('should remove multiple stream profiles', () => {
useStreamProfilesStore.setState({
profiles: [
{ id: 1, name: 'Profile 1', bitrate: 5000 },
{ id: 2, name: 'Profile 2', bitrate: 8000 },
{ id: 3, name: 'Profile 3', bitrate: 10000 },
],
});
const { result } = renderHook(() => useStreamProfilesStore());
act(() => {
result.current.removeStreamProfiles([1, 3]);
});
expect(result.current.profiles).toEqual([
{ id: 2, name: 'Profile 2', bitrate: 8000 },
]);
});
it('should handle removing non-existent profiles', () => {
const initialProfiles = [
{ id: 1, name: 'Profile 1', bitrate: 5000 },
{ id: 2, name: 'Profile 2', bitrate: 8000 },
];
useStreamProfilesStore.setState({
profiles: initialProfiles,
});
const { result } = renderHook(() => useStreamProfilesStore());
act(() => {
result.current.removeStreamProfiles([999]);
});
expect(result.current.profiles).toEqual(initialProfiles);
});
it('should handle removing from empty profiles', () => {
const { result } = renderHook(() => useStreamProfilesStore());
act(() => {
result.current.removeStreamProfiles([1, 2]);
});
expect(result.current.profiles).toEqual([]);
});
it('should handle empty array when removing profiles', () => {
const initialProfiles = [
{ id: 1, name: 'Profile 1', bitrate: 5000 },
];
useStreamProfilesStore.setState({
profiles: initialProfiles,
});
const { result } = renderHook(() => useStreamProfilesStore());
act(() => {
result.current.removeStreamProfiles([]);
});
expect(result.current.profiles).toEqual(initialProfiles);
});
});

View file

@ -0,0 +1,289 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import useStreamsStore from '../streams';
import api from '../../api';
vi.mock('../../api');
describe('useStreamsStore', () => {
beforeEach(() => {
vi.clearAllMocks();
useStreamsStore.setState({
streams: [],
count: 0,
isLoading: false,
error: null,
});
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useStreamsStore());
expect(result.current.streams).toEqual([]);
expect(result.current.count).toBe(0);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should fetch streams successfully', async () => {
const mockResponse = {
results: [
{ id: 1, name: 'Stream 1', url: 'http://example.com/1' },
{ id: 2, name: 'Stream 2', url: 'http://example.com/2' },
],
count: 2,
};
api.getStreams.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useStreamsStore());
await act(async () => {
await result.current.fetchStreams();
});
expect(api.getStreams).toHaveBeenCalled();
expect(result.current.streams).toEqual(mockResponse.results);
expect(result.current.count).toBe(2);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle fetch streams error', async () => {
const mockError = new Error('Network error');
api.getStreams.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useStreamsStore());
await act(async () => {
await result.current.fetchStreams();
});
expect(result.current.error).toBe('Failed to load streams.');
expect(result.current.isLoading).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch streams:', mockError);
consoleErrorSpy.mockRestore();
});
it('should set loading state during fetch', async () => {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
api.getStreams.mockReturnValue(promise);
const { result } = renderHook(() => useStreamsStore());
act(() => {
result.current.fetchStreams();
});
expect(result.current.isLoading).toBe(true);
expect(result.current.error).toBe(null);
await act(async () => {
resolvePromise({ results: [], count: 0 });
await promise;
});
expect(result.current.isLoading).toBe(false);
});
it('should add stream', () => {
useStreamsStore.setState({
streams: [{ id: 1, name: 'Stream 1', url: 'http://example.com/1' }],
});
const { result } = renderHook(() => useStreamsStore());
const newStream = { id: 2, name: 'Stream 2', url: 'http://example.com/2' };
act(() => {
result.current.addStream(newStream);
});
expect(result.current.streams).toEqual([
{ id: 1, name: 'Stream 1', url: 'http://example.com/1' },
{ id: 2, name: 'Stream 2', url: 'http://example.com/2' },
]);
});
it('should add stream to empty streams', () => {
const { result } = renderHook(() => useStreamsStore());
const newStream = { id: 1, name: 'Stream 1', url: 'http://example.com/1' };
act(() => {
result.current.addStream(newStream);
});
expect(result.current.streams).toEqual([newStream]);
});
it('should update stream', () => {
useStreamsStore.setState({
streams: [
{ id: 1, name: 'Stream 1', url: 'http://example.com/1' },
{ id: 2, name: 'Stream 2', url: 'http://example.com/2' },
],
});
const { result } = renderHook(() => useStreamsStore());
const updatedStream = { id: 1, name: 'Updated Stream', url: 'http://example.com/updated' };
act(() => {
result.current.updateStream(updatedStream);
});
expect(result.current.streams).toEqual([
{ id: 1, name: 'Updated Stream', url: 'http://example.com/updated' },
{ id: 2, name: 'Stream 2', url: 'http://example.com/2' },
]);
});
it('should not modify other streams when updating', () => {
useStreamsStore.setState({
streams: [
{ id: 1, name: 'Stream 1', url: 'http://example.com/1' },
{ id: 2, name: 'Stream 2', url: 'http://example.com/2' },
],
});
const { result } = renderHook(() => useStreamsStore());
const updatedStream = { id: 1, name: 'Updated Stream', url: 'http://example.com/updated' };
act(() => {
result.current.updateStream(updatedStream);
});
expect(result.current.streams[1]).toEqual({ id: 2, name: 'Stream 2', url: 'http://example.com/2' });
});
it('should not modify streams when updating non-existent stream', () => {
const initialStreams = [
{ id: 1, name: 'Stream 1', url: 'http://example.com/1' },
{ id: 2, name: 'Stream 2', url: 'http://example.com/2' },
];
useStreamsStore.setState({
streams: initialStreams,
});
const { result } = renderHook(() => useStreamsStore());
const nonExistentStream = { id: 999, name: 'Non-existent', url: 'http://example.com/999' };
act(() => {
result.current.updateStream(nonExistentStream);
});
expect(result.current.streams).toEqual(initialStreams);
});
it('should remove single stream', () => {
useStreamsStore.setState({
streams: [
{ id: 1, name: 'Stream 1', url: 'http://example.com/1' },
{ id: 2, name: 'Stream 2', url: 'http://example.com/2' },
{ id: 3, name: 'Stream 3', url: 'http://example.com/3' },
],
});
const { result } = renderHook(() => useStreamsStore());
act(() => {
result.current.removeStreams([2]);
});
expect(result.current.streams).toEqual([
{ id: 1, name: 'Stream 1', url: 'http://example.com/1' },
{ id: 3, name: 'Stream 3', url: 'http://example.com/3' },
]);
});
it('should remove multiple streams', () => {
useStreamsStore.setState({
streams: [
{ id: 1, name: 'Stream 1', url: 'http://example.com/1' },
{ id: 2, name: 'Stream 2', url: 'http://example.com/2' },
{ id: 3, name: 'Stream 3', url: 'http://example.com/3' },
],
});
const { result } = renderHook(() => useStreamsStore());
act(() => {
result.current.removeStreams([1, 3]);
});
expect(result.current.streams).toEqual([
{ id: 2, name: 'Stream 2', url: 'http://example.com/2' },
]);
});
it('should handle removing non-existent streams', () => {
const initialStreams = [
{ id: 1, name: 'Stream 1', url: 'http://example.com/1' },
{ id: 2, name: 'Stream 2', url: 'http://example.com/2' },
];
useStreamsStore.setState({
streams: initialStreams,
});
const { result } = renderHook(() => useStreamsStore());
act(() => {
result.current.removeStreams([999]);
});
expect(result.current.streams).toEqual(initialStreams);
});
it('should handle removing from empty streams', () => {
const { result } = renderHook(() => useStreamsStore());
act(() => {
result.current.removeStreams([1, 2]);
});
expect(result.current.streams).toEqual([]);
});
it('should handle empty array when removing streams', () => {
const initialStreams = [
{ id: 1, name: 'Stream 1', url: 'http://example.com/1' },
];
useStreamsStore.setState({
streams: initialStreams,
});
const { result } = renderHook(() => useStreamsStore());
act(() => {
result.current.removeStreams([]);
});
expect(result.current.streams).toEqual(initialStreams);
});
it('should handle fetch with empty results', async () => {
const mockResponse = {
results: [],
count: 0,
};
api.getStreams.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useStreamsStore());
await act(async () => {
await result.current.fetchStreams();
});
expect(result.current.streams).toEqual([]);
expect(result.current.count).toBe(0);
});
});

View file

@ -0,0 +1,755 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import useVODStore from '../useVODStore';
import api from '../../api';
vi.mock('../../api');
describe('useVODStore', () => {
beforeEach(() => {
vi.clearAllMocks();
useVODStore.setState({
content: {},
currentPageContent: [],
episodes: {},
categories: {},
loading: false,
error: null,
filters: {
type: 'all',
search: '',
category: '',
},
currentPage: 1,
totalCount: 0,
pageSize: 24,
});
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useVODStore());
expect(result.current.content).toEqual({});
expect(result.current.currentPageContent).toEqual([]);
expect(result.current.episodes).toEqual({});
expect(result.current.categories).toEqual({});
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(null);
expect(result.current.filters).toEqual({
type: 'all',
search: '',
category: '',
});
expect(result.current.currentPage).toBe(1);
expect(result.current.totalCount).toBe(0);
expect(result.current.pageSize).toBe(24);
});
it('should set filters and reset to first page', () => {
useVODStore.setState({ currentPage: 5 });
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.setFilters({ search: 'test', category: 'action' });
});
expect(result.current.filters).toEqual({
type: 'all',
search: 'test',
category: 'action',
});
expect(result.current.currentPage).toBe(1);
});
it('should set page', () => {
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.setPage(3);
});
expect(result.current.currentPage).toBe(3);
});
it('should set page size and reset to first page', () => {
useVODStore.setState({ currentPage: 3 });
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.setPageSize(50);
});
expect(result.current.pageSize).toBe(50);
expect(result.current.currentPage).toBe(1);
});
it('should fetch all content successfully', async () => {
const mockResponse = {
results: [
{ id: 1, name: 'Movie 1', content_type: 'movie' },
{ id: 2, name: 'Series 1', content_type: 'series' },
],
count: 2,
};
api.getAllContent.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useVODStore());
await act(async () => {
await result.current.fetchContent();
});
expect(api.getAllContent).toHaveBeenCalled();
expect(result.current.currentPageContent).toEqual([
{ id: 1, name: 'Movie 1', content_type: 'movie', contentType: 'movie' },
{ id: 2, name: 'Series 1', content_type: 'series', contentType: 'series' },
]);
expect(result.current.totalCount).toBe(2);
expect(result.current.loading).toBe(false);
});
it('should fetch only movies when filter type is movies', async () => {
const mockResponse = {
results: [
{ id: 1, name: 'Movie 1' },
{ id: 2, name: 'Movie 2' },
],
count: 2,
};
api.getMovies.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.setFilters({ type: 'movies' });
});
await act(async () => {
await result.current.fetchContent();
});
expect(api.getMovies).toHaveBeenCalled();
expect(result.current.currentPageContent).toEqual([
{ id: 1, name: 'Movie 1', contentType: 'movie' },
{ id: 2, name: 'Movie 2', contentType: 'movie' },
]);
expect(result.current.totalCount).toBe(2);
});
it('should fetch only series when filter type is series', async () => {
const mockResponse = {
results: [
{ id: 1, name: 'Series 1' },
{ id: 2, name: 'Series 2' },
],
count: 2,
};
api.getSeries.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.setFilters({ type: 'series' });
});
await act(async () => {
await result.current.fetchContent();
});
expect(api.getSeries).toHaveBeenCalled();
expect(result.current.currentPageContent).toEqual([
{ id: 1, name: 'Series 1', contentType: 'series' },
{ id: 2, name: 'Series 2', contentType: 'series' },
]);
expect(result.current.totalCount).toBe(2);
});
it('should handle fetch content error', async () => {
const mockError = new Error('Network error');
api.getAllContent.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODStore());
await act(async () => {
await result.current.fetchContent();
});
expect(result.current.error).toBe('Failed to load content.');
expect(result.current.loading).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch content:', mockError);
consoleErrorSpy.mockRestore();
});
it('should handle invalid response format', async () => {
api.getAllContent.mockResolvedValue({ results: 'not-an-array' });
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODStore());
await act(async () => {
await result.current.fetchContent();
});
expect(result.current.error).toBe('Failed to load content.');
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
it('should fetch movie details successfully', async () => {
const mockResponse = {
id: 1,
name: 'Test Movie',
description: 'A test movie',
year: 2023,
url: 'http://example.com/movie.mp4',
};
api.getMovieDetails.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useVODStore());
let movieDetails;
await act(async () => {
movieDetails = await result.current.fetchMovieDetails(1);
});
expect(api.getMovieDetails).toHaveBeenCalledWith(1);
expect(movieDetails.id).toBe(1);
expect(movieDetails.name).toBe('Test Movie');
expect(movieDetails.stream_url).toBe('http://example.com/movie.mp4');
expect(result.current.content['movie_1']).toBeDefined();
expect(result.current.content['movie_1'].contentType).toBe('movie');
expect(result.current.loading).toBe(false);
});
it('should handle fetch movie details error', async () => {
const mockError = new Error('Not found');
api.getMovieDetails.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODStore());
await act(async () => {
try {
await result.current.fetchMovieDetails(999);
} catch (error) {
expect(error).toBe(mockError);
}
});
expect(result.current.error).toBe('Failed to load movie details.');
expect(result.current.loading).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch movie details:', mockError);
consoleErrorSpy.mockRestore();
});
it('should fetch movie details from provider without merging to store', async () => {
const mockResponse = {
id: 1,
name: 'Provider Movie',
plot: 'From provider',
stream_url: 'http://provider.com/movie.mp4',
backdrop_path: ['path1', 'path2'],
};
api.getMovieProviderInfo.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useVODStore());
let movieDetails;
await act(async () => {
movieDetails = await result.current.fetchMovieDetailsFromProvider(1);
});
expect(api.getMovieProviderInfo).toHaveBeenCalledWith(1);
expect(movieDetails.name).toBe('Provider Movie');
expect(movieDetails.description).toBe('From provider');
expect(movieDetails.backdrop_path).toEqual(['path1', 'path2']);
expect(result.current.content['movie_1']).toBeUndefined();
expect(result.current.loading).toBe(false);
});
it('should handle fetch movie provider error', async () => {
const mockError = new Error('Provider error');
api.getMovieProviderInfo.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODStore());
await act(async () => {
try {
await result.current.fetchMovieDetailsFromProvider(1);
} catch (error) {
expect(error).toBe(mockError);
}
});
expect(result.current.error).toBe('Failed to load movie details from provider.');
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch movie details from provider:', mockError);
consoleErrorSpy.mockRestore();
});
it('should fetch movie providers successfully', async () => {
const mockProviders = [
{ id: 1, name: 'Provider 1' },
{ id: 2, name: 'Provider 2' },
];
api.getMovieProviders.mockResolvedValue(mockProviders);
const { result } = renderHook(() => useVODStore());
let providers;
await act(async () => {
providers = await result.current.fetchMovieProviders(1);
});
expect(api.getMovieProviders).toHaveBeenCalledWith(1);
expect(providers).toEqual(mockProviders);
});
it('should handle fetch movie providers error', async () => {
const mockError = new Error('Providers error');
api.getMovieProviders.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODStore());
await act(async () => {
try {
await result.current.fetchMovieProviders(1);
} catch (error) {
expect(error).toBe(mockError);
}
});
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch movie providers:', mockError);
consoleErrorSpy.mockRestore();
});
it('should fetch series providers successfully', async () => {
const mockProviders = [{ id: 1, name: 'Series Provider 1' }];
api.getSeriesProviders.mockResolvedValue(mockProviders);
const { result } = renderHook(() => useVODStore());
let providers;
await act(async () => {
providers = await result.current.fetchSeriesProviders(1);
});
expect(api.getSeriesProviders).toHaveBeenCalledWith(1);
expect(providers).toEqual(mockProviders);
});
it('should handle fetch series providers error', async () => {
const mockError = new Error('Series providers error');
api.getSeriesProviders.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODStore());
await act(async () => {
try {
await result.current.fetchSeriesProviders(1);
} catch (error) {
expect(error).toBe(mockError);
}
});
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch series providers:', mockError);
consoleErrorSpy.mockRestore();
});
it('should fetch series info successfully', async () => {
const mockResponse = {
id: 1,
name: 'Test Series',
description: 'A test series',
year: 2023,
cover: 'http://example.com/cover.jpg',
episodes: {
1: [
{
id: 101,
title: 'Episode 1',
episode_number: 1,
plot: 'First episode',
},
{
id: 102,
title: 'Episode 2',
episode_number: 2,
plot: 'Second episode',
},
],
},
};
api.getSeriesInfo.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useVODStore());
let seriesInfo;
await act(async () => {
seriesInfo = await result.current.fetchSeriesInfo(1);
});
expect(api.getSeriesInfo).toHaveBeenCalledWith(1);
expect(seriesInfo.id).toBe(1);
expect(seriesInfo.name).toBe('Test Series');
expect(seriesInfo.episodesList).toHaveLength(2);
expect(result.current.content['series_1']).toBeDefined();
expect(result.current.content['series_1'].contentType).toBe('series');
expect(result.current.episodes[101]).toBeDefined();
expect(result.current.episodes[102]).toBeDefined();
expect(result.current.episodes[101].name).toBe('Episode 1');
expect(result.current.loading).toBe(false);
});
it('should handle fetch series info error', async () => {
const mockError = new Error('Series not found');
api.getSeriesInfo.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODStore());
await act(async () => {
try {
await result.current.fetchSeriesInfo(999);
} catch (error) {
expect(error).toBe(mockError);
}
});
expect(result.current.error).toBe('Failed to load series details.');
expect(result.current.loading).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch series info:', mockError);
consoleErrorSpy.mockRestore();
});
it('should fetch categories successfully with array response', async () => {
const mockCategories = [
{ id: 1, name: 'Action' },
{ id: 2, name: 'Comedy' },
];
api.getVODCategories.mockResolvedValue(mockCategories);
const { result } = renderHook(() => useVODStore());
await act(async () => {
await result.current.fetchCategories();
});
expect(api.getVODCategories).toHaveBeenCalled();
expect(result.current.categories).toEqual({
1: { id: 1, name: 'Action' },
2: { id: 2, name: 'Comedy' },
});
});
it('should fetch categories successfully with paginated response', async () => {
const mockResponse = {
results: [
{ id: 1, name: 'Drama' },
{ id: 2, name: 'Thriller' },
],
};
api.getVODCategories.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useVODStore());
await act(async () => {
await result.current.fetchCategories();
});
expect(result.current.categories).toEqual({
1: { id: 1, name: 'Drama' },
2: { id: 2, name: 'Thriller' },
});
});
it('should handle fetch categories error', async () => {
const mockError = new Error('Categories error');
api.getVODCategories.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODStore());
await act(async () => {
await result.current.fetchCategories();
});
expect(result.current.error).toBe('Failed to load categories.');
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch VOD categories:', mockError);
consoleErrorSpy.mockRestore();
});
it('should add movie to content', () => {
const { result } = renderHook(() => useVODStore());
const movie = { id: 1, name: 'New Movie' };
act(() => {
result.current.addMovie(movie);
});
expect(result.current.content['movie_1']).toEqual({
id: 1,
name: 'New Movie',
contentType: 'movie',
});
});
it('should update movie in content', () => {
useVODStore.setState({
content: {
movie_1: { id: 1, name: 'Old Movie', contentType: 'movie' },
},
});
const { result } = renderHook(() => useVODStore());
const updatedMovie = { id: 1, name: 'Updated Movie' };
act(() => {
result.current.updateMovie(updatedMovie);
});
expect(result.current.content['movie_1']).toEqual({
id: 1,
name: 'Updated Movie',
contentType: 'movie',
});
});
it('should remove movie from content', () => {
useVODStore.setState({
content: {
movie_1: { id: 1, name: 'Movie to Remove', contentType: 'movie' },
movie_2: { id: 2, name: 'Movie to Keep', contentType: 'movie' },
},
});
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.removeMovie(1);
});
expect(result.current.content['movie_1']).toBeUndefined();
expect(result.current.content['movie_2']).toBeDefined();
});
it('should add series to content', () => {
const { result } = renderHook(() => useVODStore());
const series = { id: 1, name: 'New Series' };
act(() => {
result.current.addSeries(series);
});
expect(result.current.content['series_1']).toEqual({
id: 1,
name: 'New Series',
contentType: 'series',
});
});
it('should update series in content', () => {
useVODStore.setState({
content: {
series_1: { id: 1, name: 'Old Series', contentType: 'series' },
},
});
const { result } = renderHook(() => useVODStore());
const updatedSeries = { id: 1, name: 'Updated Series' };
act(() => {
result.current.updateSeries(updatedSeries);
});
expect(result.current.content['series_1']).toEqual({
id: 1,
name: 'Updated Series',
contentType: 'series',
});
});
it('should remove series from content', () => {
useVODStore.setState({
content: {
series_1: { id: 1, name: 'Series to Remove', contentType: 'series' },
series_2: { id: 2, name: 'Series to Keep', contentType: 'series' },
},
});
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.removeSeries(1);
});
expect(result.current.content['series_1']).toBeUndefined();
expect(result.current.content['series_2']).toBeDefined();
});
it('should get filtered content from current page', () => {
const mockContent = [
{ id: 1, name: 'Movie 1', contentType: 'movie' },
{ id: 2, name: 'Series 1', contentType: 'series' },
];
useVODStore.setState({
currentPageContent: mockContent,
});
const { result } = renderHook(() => useVODStore());
const filtered = result.current.getFilteredContent();
expect(filtered).toEqual(mockContent);
});
it('should get only movies from content', () => {
useVODStore.setState({
content: {
movie_1: { id: 1, name: 'Movie 1', contentType: 'movie' },
series_1: { id: 2, name: 'Series 1', contentType: 'series' },
movie_2: { id: 3, name: 'Movie 2', contentType: 'movie' },
},
});
const { result } = renderHook(() => useVODStore());
const movies = result.current.getMovies();
expect(movies).toHaveLength(2);
expect(movies.every((item) => item.contentType === 'movie')).toBe(true);
});
it('should get only series from content', () => {
useVODStore.setState({
content: {
movie_1: { id: 1, name: 'Movie 1', contentType: 'movie' },
series_1: { id: 2, name: 'Series 1', contentType: 'series' },
series_2: { id: 3, name: 'Series 2', contentType: 'series' },
},
});
const { result } = renderHook(() => useVODStore());
const series = result.current.getSeries();
expect(series).toHaveLength(2);
expect(series.every((item) => item.contentType === 'series')).toBe(true);
});
it('should clear all content', () => {
useVODStore.setState({
content: {
movie_1: { id: 1, name: 'Movie 1', contentType: 'movie' },
series_1: { id: 2, name: 'Series 1', contentType: 'series' },
},
totalCount: 2,
});
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.clearContent();
});
expect(result.current.content).toEqual({});
expect(result.current.totalCount).toBe(0);
});
it('should handle fetch content with search filter', async () => {
const mockResponse = {
results: [{ id: 1, name: 'Searched Movie', content_type: 'movie' }],
count: 1,
};
api.getAllContent.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.setFilters({ search: 'Searched' });
});
await act(async () => {
await result.current.fetchContent();
});
expect(api.getAllContent).toHaveBeenCalled();
const callArgs = api.getAllContent.mock.calls[0][0];
expect(callArgs.get('search')).toBe('Searched');
});
it('should handle fetch content with category filter', async () => {
const mockResponse = {
results: [{ id: 1, name: 'Action Movie', content_type: 'movie' }],
count: 1,
};
api.getAllContent.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.setFilters({ category: 'action' });
});
await act(async () => {
await result.current.fetchContent();
});
const callArgs = api.getAllContent.mock.calls[0][0];
expect(callArgs.get('category')).toBe('action');
});
it('should set loading state during fetch', async () => {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
api.getAllContent.mockReturnValue(promise);
const { result } = renderHook(() => useVODStore());
act(() => {
result.current.fetchContent();
});
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(null);
await act(async () => {
resolvePromise({ results: [], count: 0 });
await promise;
});
expect(result.current.loading).toBe(false);
});
});

View file

@ -0,0 +1,182 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach } from 'vitest';
import useVideoStore from '../useVideoStore';
describe('useVideoStore', () => {
beforeEach(() => {
useVideoStore.setState({
isVisible: false,
streamUrl: null,
contentType: 'live',
metadata: null,
});
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useVideoStore());
expect(result.current.isVisible).toBe(false);
expect(result.current.streamUrl).toBe(null);
expect(result.current.contentType).toBe('live');
expect(result.current.metadata).toBe(null);
});
it('should show video with live stream', () => {
const { result } = renderHook(() => useVideoStore());
const streamUrl = 'http://example.com/stream.ts';
act(() => {
result.current.showVideo(streamUrl);
});
expect(result.current.isVisible).toBe(true);
expect(result.current.streamUrl).toBe(streamUrl);
expect(result.current.contentType).toBe('live');
expect(result.current.metadata).toBe(null);
});
it('should show video with VOD content', () => {
const { result } = renderHook(() => useVideoStore());
const streamUrl = 'http://example.com/video.mp4';
const metadata = { title: 'Test Video', duration: 120 };
act(() => {
result.current.showVideo(streamUrl, 'vod', metadata);
});
expect(result.current.isVisible).toBe(true);
expect(result.current.streamUrl).toBe(streamUrl);
expect(result.current.contentType).toBe('vod');
expect(result.current.metadata).toEqual(metadata);
});
it('should show video with custom content type', () => {
const { result } = renderHook(() => useVideoStore());
const streamUrl = 'http://example.com/video.mkv';
act(() => {
result.current.showVideo(streamUrl, 'vod');
});
expect(result.current.isVisible).toBe(true);
expect(result.current.streamUrl).toBe(streamUrl);
expect(result.current.contentType).toBe('vod');
expect(result.current.metadata).toBe(null);
});
it('should hide video and reset state', () => {
const { result } = renderHook(() => useVideoStore());
act(() => {
result.current.showVideo('http://example.com/stream.ts', 'vod', { title: 'Test' });
});
expect(result.current.isVisible).toBe(true);
act(() => {
result.current.hideVideo();
});
expect(result.current.isVisible).toBe(false);
expect(result.current.streamUrl).toBe(null);
expect(result.current.contentType).toBe('live');
expect(result.current.metadata).toBe(null);
});
it('should update stream when showing different video', () => {
const { result } = renderHook(() => useVideoStore());
const firstUrl = 'http://example.com/stream1.ts';
const secondUrl = 'http://example.com/stream2.ts';
act(() => {
result.current.showVideo(firstUrl);
});
expect(result.current.streamUrl).toBe(firstUrl);
act(() => {
result.current.showVideo(secondUrl);
});
expect(result.current.streamUrl).toBe(secondUrl);
expect(result.current.isVisible).toBe(true);
});
it('should handle showing video with null metadata explicitly', () => {
const { result } = renderHook(() => useVideoStore());
const streamUrl = 'http://example.com/stream.ts';
act(() => {
result.current.showVideo(streamUrl, 'live', null);
});
expect(result.current.isVisible).toBe(true);
expect(result.current.streamUrl).toBe(streamUrl);
expect(result.current.contentType).toBe('live');
expect(result.current.metadata).toBe(null);
});
it('should preserve metadata when showing VOD content', () => {
const { result } = renderHook(() => useVideoStore());
const metadata = {
title: 'Test Video',
duration: 120,
thumbnailUrl: 'http://example.com/thumb.jpg'
};
act(() => {
result.current.showVideo('http://example.com/video.mp4', 'vod', metadata);
});
expect(result.current.metadata).toEqual(metadata);
expect(result.current.metadata.title).toBe('Test Video');
expect(result.current.metadata.duration).toBe(120);
});
it('should override previous metadata when showing new video', () => {
const { result } = renderHook(() => useVideoStore());
const firstMetadata = { title: 'First Video' };
const secondMetadata = { title: 'Second Video' };
act(() => {
result.current.showVideo('http://example.com/first.mp4', 'vod', firstMetadata);
});
expect(result.current.metadata).toEqual(firstMetadata);
act(() => {
result.current.showVideo('http://example.com/second.mp4', 'vod', secondMetadata);
});
expect(result.current.metadata).toEqual(secondMetadata);
});
it('should handle hiding video when already hidden', () => {
const { result } = renderHook(() => useVideoStore());
expect(result.current.isVisible).toBe(false);
act(() => {
result.current.hideVideo();
});
expect(result.current.isVisible).toBe(false);
expect(result.current.streamUrl).toBe(null);
});
it('should handle showing video multiple times consecutively', () => {
const { result } = renderHook(() => useVideoStore());
const url1 = 'http://example.com/stream1.ts';
const url2 = 'http://example.com/stream2.ts';
const url3 = 'http://example.com/stream3.ts';
act(() => {
result.current.showVideo(url1);
result.current.showVideo(url2);
result.current.showVideo(url3);
});
expect(result.current.isVisible).toBe(true);
expect(result.current.streamUrl).toBe(url3);
});
});

View file

@ -0,0 +1,277 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import useUserAgentsStore from '../userAgents';
import api from '../../api';
vi.mock('../../api');
describe('useUserAgentsStore', () => {
beforeEach(() => {
vi.clearAllMocks();
useUserAgentsStore.setState({
userAgents: [],
isLoading: false,
error: null,
});
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useUserAgentsStore());
expect(result.current.userAgents).toEqual([]);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should fetch user agents successfully', async () => {
const mockUserAgents = [
{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' },
{ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' },
];
api.getUserAgents.mockResolvedValue(mockUserAgents);
const { result } = renderHook(() => useUserAgentsStore());
await act(async () => {
await result.current.fetchUserAgents();
});
expect(api.getUserAgents).toHaveBeenCalled();
expect(result.current.userAgents).toEqual(mockUserAgents);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle fetch user agents error', async () => {
const mockError = new Error('Network error');
api.getUserAgents.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useUserAgentsStore());
await act(async () => {
await result.current.fetchUserAgents();
});
expect(result.current.error).toBe('Failed to load userAgents.');
expect(result.current.isLoading).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch userAgents:', mockError);
consoleErrorSpy.mockRestore();
});
it('should set loading state during fetch', async () => {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
api.getUserAgents.mockReturnValue(promise);
const { result } = renderHook(() => useUserAgentsStore());
act(() => {
result.current.fetchUserAgents();
});
expect(result.current.isLoading).toBe(true);
expect(result.current.error).toBe(null);
await act(async () => {
resolvePromise([]);
await promise;
});
expect(result.current.isLoading).toBe(false);
});
it('should add user agent', () => {
useUserAgentsStore.setState({
userAgents: [{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' }],
});
const { result } = renderHook(() => useUserAgentsStore());
const newUserAgent = { id: 2, name: 'Firefox', string: 'Mozilla/5.0...' };
act(() => {
result.current.addUserAgent(newUserAgent);
});
expect(result.current.userAgents).toEqual([
{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' },
{ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' },
]);
});
it('should add user agent to empty user agents', () => {
const { result } = renderHook(() => useUserAgentsStore());
const newUserAgent = { id: 1, name: 'Chrome', string: 'Mozilla/5.0...' };
act(() => {
result.current.addUserAgent(newUserAgent);
});
expect(result.current.userAgents).toEqual([newUserAgent]);
});
it('should update user agent', () => {
useUserAgentsStore.setState({
userAgents: [
{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' },
{ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' },
],
});
const { result } = renderHook(() => useUserAgentsStore());
const updatedUserAgent = { id: 1, name: 'Chrome Updated', string: 'Mozilla/5.0 Updated...' };
act(() => {
result.current.updateUserAgent(updatedUserAgent);
});
expect(result.current.userAgents).toEqual([
{ id: 1, name: 'Chrome Updated', string: 'Mozilla/5.0 Updated...' },
{ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' },
]);
});
it('should not modify other user agents when updating', () => {
useUserAgentsStore.setState({
userAgents: [
{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' },
{ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' },
],
});
const { result } = renderHook(() => useUserAgentsStore());
const updatedUserAgent = { id: 1, name: 'Chrome Updated', string: 'Mozilla/5.0 Updated...' };
act(() => {
result.current.updateUserAgent(updatedUserAgent);
});
expect(result.current.userAgents[1]).toEqual({ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' });
});
it('should not modify user agents when updating non-existent user agent', () => {
const initialUserAgents = [
{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' },
{ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' },
];
useUserAgentsStore.setState({
userAgents: initialUserAgents,
});
const { result } = renderHook(() => useUserAgentsStore());
const nonExistentUserAgent = { id: 999, name: 'Non-existent', string: 'Mozilla/5.0...' };
act(() => {
result.current.updateUserAgent(nonExistentUserAgent);
});
expect(result.current.userAgents).toEqual(initialUserAgents);
});
it('should remove single user agent', () => {
useUserAgentsStore.setState({
userAgents: [
{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' },
{ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' },
{ id: 3, name: 'Safari', string: 'Mozilla/5.0...' },
],
});
const { result } = renderHook(() => useUserAgentsStore());
act(() => {
result.current.removeUserAgents([2]);
});
expect(result.current.userAgents).toEqual([
{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' },
{ id: 3, name: 'Safari', string: 'Mozilla/5.0...' },
]);
});
it('should remove multiple user agents', () => {
useUserAgentsStore.setState({
userAgents: [
{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' },
{ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' },
{ id: 3, name: 'Safari', string: 'Mozilla/5.0...' },
],
});
const { result } = renderHook(() => useUserAgentsStore());
act(() => {
result.current.removeUserAgents([1, 3]);
});
expect(result.current.userAgents).toEqual([
{ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' },
]);
});
it('should handle removing non-existent user agents', () => {
const initialUserAgents = [
{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' },
{ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' },
];
useUserAgentsStore.setState({
userAgents: initialUserAgents,
});
const { result } = renderHook(() => useUserAgentsStore());
act(() => {
result.current.removeUserAgents([999]);
});
expect(result.current.userAgents).toEqual(initialUserAgents);
});
it('should handle removing from empty user agents', () => {
const { result } = renderHook(() => useUserAgentsStore());
act(() => {
result.current.removeUserAgents([1, 2]);
});
expect(result.current.userAgents).toEqual([]);
});
it('should handle empty array when removing user agents', () => {
const initialUserAgents = [
{ id: 1, name: 'Chrome', string: 'Mozilla/5.0...' },
];
useUserAgentsStore.setState({
userAgents: initialUserAgents,
});
const { result } = renderHook(() => useUserAgentsStore());
act(() => {
result.current.removeUserAgents([]);
});
expect(result.current.userAgents).toEqual(initialUserAgents);
});
it('should handle fetch with empty results', async () => {
api.getUserAgents.mockResolvedValue([]);
const { result } = renderHook(() => useUserAgentsStore());
await act(async () => {
await result.current.fetchUserAgents();
});
expect(result.current.userAgents).toEqual([]);
});
});

View file

@ -0,0 +1,258 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import useUsersStore from '../users';
import api from '../../api';
vi.mock('../../api');
describe('useUsersStore', () => {
beforeEach(() => {
vi.clearAllMocks();
useUsersStore.setState({
users: [],
isLoading: false,
error: null,
});
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useUsersStore());
expect(result.current.users).toEqual([]);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should fetch users successfully', async () => {
const mockUsers = [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
];
api.getUsers.mockResolvedValue(mockUsers);
const { result } = renderHook(() => useUsersStore());
await act(async () => {
await result.current.fetchUsers();
});
expect(api.getUsers).toHaveBeenCalled();
expect(result.current.users).toEqual(mockUsers);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle fetch users error', async () => {
const mockError = new Error('Network error');
api.getUsers.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useUsersStore());
await act(async () => {
await result.current.fetchUsers();
});
expect(result.current.error).toBe('Failed to load users.');
expect(result.current.isLoading).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch users:', mockError);
consoleErrorSpy.mockRestore();
});
it('should set loading state during fetch', async () => {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
api.getUsers.mockReturnValue(promise);
const { result } = renderHook(() => useUsersStore());
act(() => {
result.current.fetchUsers();
});
expect(result.current.isLoading).toBe(true);
expect(result.current.error).toBe(null);
await act(async () => {
resolvePromise([]);
await promise;
});
expect(result.current.isLoading).toBe(false);
});
it('should add user', () => {
useUsersStore.setState({
users: [{ id: 1, name: 'User 1', email: 'user1@example.com' }],
});
const { result } = renderHook(() => useUsersStore());
const newUser = { id: 2, name: 'User 2', email: 'user2@example.com' };
act(() => {
result.current.addUser(newUser);
});
expect(result.current.users).toEqual([
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
]);
});
it('should add user to empty users', () => {
const { result } = renderHook(() => useUsersStore());
const newUser = { id: 1, name: 'User 1', email: 'user1@example.com' };
act(() => {
result.current.addUser(newUser);
});
expect(result.current.users).toEqual([newUser]);
});
it('should update user', () => {
useUsersStore.setState({
users: [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
],
});
const { result } = renderHook(() => useUsersStore());
const updatedUser = { id: 1, name: 'Updated User', email: 'updated@example.com' };
act(() => {
result.current.updateUser(updatedUser);
});
expect(result.current.users).toEqual([
{ id: 1, name: 'Updated User', email: 'updated@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
]);
});
it('should not modify other users when updating', () => {
useUsersStore.setState({
users: [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
],
});
const { result } = renderHook(() => useUsersStore());
const updatedUser = { id: 1, name: 'Updated User', email: 'updated@example.com' };
act(() => {
result.current.updateUser(updatedUser);
});
expect(result.current.users[1]).toEqual({ id: 2, name: 'User 2', email: 'user2@example.com' });
});
it('should not modify users when updating non-existent user', () => {
const initialUsers = [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
];
useUsersStore.setState({
users: initialUsers,
});
const { result } = renderHook(() => useUsersStore());
const nonExistentUser = { id: 999, name: 'Non-existent', email: 'none@example.com' };
act(() => {
result.current.updateUser(nonExistentUser);
});
expect(result.current.users).toEqual(initialUsers);
});
it('should remove user', () => {
useUsersStore.setState({
users: [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
{ id: 3, name: 'User 3', email: 'user3@example.com' },
],
});
const { result } = renderHook(() => useUsersStore());
act(() => {
result.current.removeUser(2);
});
expect(result.current.users).toEqual([
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 3, name: 'User 3', email: 'user3@example.com' },
]);
});
it('should handle removing non-existent user', () => {
const initialUsers = [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
];
useUsersStore.setState({
users: initialUsers,
});
const { result } = renderHook(() => useUsersStore());
act(() => {
result.current.removeUser(999);
});
expect(result.current.users).toEqual(initialUsers);
});
it('should handle removing from empty users', () => {
const { result } = renderHook(() => useUsersStore());
act(() => {
result.current.removeUser(1);
});
expect(result.current.users).toEqual([]);
});
it('should handle fetch with empty results', async () => {
api.getUsers.mockResolvedValue([]);
const { result } = renderHook(() => useUsersStore());
await act(async () => {
await result.current.fetchUsers();
});
expect(result.current.users).toEqual([]);
});
it('should not modify other users when removing', () => {
useUsersStore.setState({
users: [
{ id: 1, name: 'User 1', email: 'user1@example.com' },
{ id: 2, name: 'User 2', email: 'user2@example.com' },
{ id: 3, name: 'User 3', email: 'user3@example.com' },
],
});
const { result } = renderHook(() => useUsersStore());
act(() => {
result.current.removeUser(2);
});
expect(result.current.users[0]).toEqual({ id: 1, name: 'User 1', email: 'user1@example.com' });
expect(result.current.users[1]).toEqual({ id: 3, name: 'User 3', email: 'user3@example.com' });
});
});

View file

@ -0,0 +1,480 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import useVODLogosStore from '../vodLogos';
import api from '../../api';
vi.mock('../../api');
describe('useVODLogosStore', () => {
beforeEach(() => {
vi.clearAllMocks();
useVODLogosStore.setState({
vodLogos: {},
logos: [],
isLoading: false,
hasLoaded: false,
error: null,
totalCount: 0,
currentPage: 1,
pageSize: 25,
});
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useVODLogosStore());
expect(result.current.vodLogos).toEqual({});
expect(result.current.logos).toEqual([]);
expect(result.current.isLoading).toBe(false);
expect(result.current.hasLoaded).toBe(false);
expect(result.current.error).toBe(null);
expect(result.current.totalCount).toBe(0);
expect(result.current.currentPage).toBe(1);
expect(result.current.pageSize).toBe(25);
});
it('should set VOD logos with normalized structure', () => {
const { result } = renderHook(() => useVODLogosStore());
const mockLogos = [
{ id: 1, name: 'Logo 1', url: 'http://example.com/logo1.png' },
{ id: 2, name: 'Logo 2', url: 'http://example.com/logo2.png' },
];
act(() => {
result.current.setVODLogos(mockLogos, 2);
});
expect(result.current.vodLogos).toEqual({
1: { id: 1, name: 'Logo 1', url: 'http://example.com/logo1.png' },
2: { id: 2, name: 'Logo 2', url: 'http://example.com/logo2.png' },
});
expect(result.current.totalCount).toBe(2);
expect(result.current.hasLoaded).toBe(true);
});
it('should fetch VOD logos successfully with array response', async () => {
const mockLogos = [
{ id: 1, name: 'Logo 1', url: 'http://example.com/logo1.png' },
{ id: 2, name: 'Logo 2', url: 'http://example.com/logo2.png' },
];
api.getVODLogos.mockResolvedValue(mockLogos);
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
await result.current.fetchVODLogos();
});
expect(api.getVODLogos).toHaveBeenCalled();
expect(result.current.vodLogos).toEqual({
1: { id: 1, name: 'Logo 1', url: 'http://example.com/logo1.png' },
2: { id: 2, name: 'Logo 2', url: 'http://example.com/logo2.png' },
});
expect(result.current.logos).toEqual(mockLogos);
expect(result.current.totalCount).toBe(2);
expect(result.current.isLoading).toBe(false);
expect(result.current.hasLoaded).toBe(true);
});
it('should fetch VOD logos successfully with paginated response', async () => {
const mockLogos = [
{ id: 1, name: 'Logo 1', url: 'http://example.com/logo1.png' },
];
const mockResponse = {
results: mockLogos,
count: 10,
};
api.getVODLogos.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
await result.current.fetchVODLogos({ page: 1, page_size: 25 });
});
expect(result.current.vodLogos).toEqual({
1: { id: 1, name: 'Logo 1', url: 'http://example.com/logo1.png' },
});
expect(result.current.logos).toEqual(mockLogos);
expect(result.current.totalCount).toBe(10);
});
it('should handle fetch VOD logos error', async () => {
const mockError = new Error('Network error');
api.getVODLogos.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
try {
await result.current.fetchVODLogos();
} catch (error) {
// Expected error
}
});
expect(result.current.error).toBe('Failed to load VOD logos.');
expect(result.current.isLoading).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch VOD logos:', mockError);
consoleErrorSpy.mockRestore();
});
it('should set loading state during fetch', async () => {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
api.getVODLogos.mockReturnValue(promise);
const { result } = renderHook(() => useVODLogosStore());
act(() => {
result.current.fetchVODLogos();
});
expect(result.current.isLoading).toBe(true);
expect(result.current.error).toBe(null);
await act(async () => {
resolvePromise([]);
await promise;
});
expect(result.current.isLoading).toBe(false);
});
it('should remove single VOD logo from state', () => {
useVODLogosStore.setState({
vodLogos: {
1: { id: 1, name: 'Logo 1' },
2: { id: 2, name: 'Logo 2' },
3: { id: 3, name: 'Logo 3' },
},
logos: [
{ id: 1, name: 'Logo 1' },
{ id: 2, name: 'Logo 2' },
{ id: 3, name: 'Logo 3' },
],
totalCount: 3,
});
const { result } = renderHook(() => useVODLogosStore());
act(() => {
result.current.removeVODLogo(2);
});
expect(result.current.vodLogos).toEqual({
1: { id: 1, name: 'Logo 1' },
3: { id: 3, name: 'Logo 3' },
});
expect(result.current.logos).toEqual([
{ id: 1, name: 'Logo 1' },
{ id: 3, name: 'Logo 3' },
]);
expect(result.current.totalCount).toBe(2);
});
it('should remove multiple VOD logos from state using _removeLogosFromState', () => {
useVODLogosStore.setState({
vodLogos: {
1: { id: 1, name: 'Logo 1' },
2: { id: 2, name: 'Logo 2' },
3: { id: 3, name: 'Logo 3' },
},
logos: [
{ id: 1, name: 'Logo 1' },
{ id: 2, name: 'Logo 2' },
{ id: 3, name: 'Logo 3' },
],
totalCount: 3,
});
const { result } = renderHook(() => useVODLogosStore());
act(() => {
result.current._removeLogosFromState([1, 3]);
});
expect(result.current.vodLogos).toEqual({
2: { id: 2, name: 'Logo 2' },
});
expect(result.current.logos).toEqual([
{ id: 2, name: 'Logo 2' },
]);
expect(result.current.totalCount).toBe(1);
});
it('should delete single VOD logo successfully', async () => {
useVODLogosStore.setState({
vodLogos: {
1: { id: 1, name: 'Logo 1' },
2: { id: 2, name: 'Logo 2' },
},
logos: [
{ id: 1, name: 'Logo 1' },
{ id: 2, name: 'Logo 2' },
],
totalCount: 2,
});
api.deleteVODLogo.mockResolvedValue({});
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
await result.current.deleteVODLogo(1);
});
expect(api.deleteVODLogo).toHaveBeenCalledWith(1);
expect(result.current.vodLogos).toEqual({
2: { id: 2, name: 'Logo 2' },
});
expect(result.current.logos).toEqual([
{ id: 2, name: 'Logo 2' },
]);
expect(result.current.totalCount).toBe(1);
});
it('should handle delete VOD logo error', async () => {
const mockError = new Error('Delete failed');
api.deleteVODLogo.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
try {
await result.current.deleteVODLogo(1);
} catch (error) {
expect(error).toBe(mockError);
}
});
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to delete VOD logo:', mockError);
consoleErrorSpy.mockRestore();
});
it('should delete multiple VOD logos successfully', async () => {
useVODLogosStore.setState({
vodLogos: {
1: { id: 1, name: 'Logo 1' },
2: { id: 2, name: 'Logo 2' },
3: { id: 3, name: 'Logo 3' },
},
logos: [
{ id: 1, name: 'Logo 1' },
{ id: 2, name: 'Logo 2' },
{ id: 3, name: 'Logo 3' },
],
totalCount: 3,
});
api.deleteVODLogos.mockResolvedValue({});
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
await result.current.deleteVODLogos([1, 2]);
});
expect(api.deleteVODLogos).toHaveBeenCalledWith([1, 2]);
expect(result.current.vodLogos).toEqual({
3: { id: 3, name: 'Logo 3' },
});
expect(result.current.logos).toEqual([
{ id: 3, name: 'Logo 3' },
]);
expect(result.current.totalCount).toBe(1);
});
it('should handle delete multiple VOD logos error', async () => {
const mockError = new Error('Bulk delete failed');
api.deleteVODLogos.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
try {
await result.current.deleteVODLogos([1, 2]);
} catch (error) {
expect(error).toBe(mockError);
}
});
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to delete VOD logos:', mockError);
consoleErrorSpy.mockRestore();
});
it('should cleanup unused VOD logos and refresh', async () => {
useVODLogosStore.setState({
currentPage: 2,
pageSize: 10,
});
const mockCleanupResult = { deleted: 5 };
const mockRefreshedLogos = [{ id: 1, name: 'Logo 1' }];
api.cleanupUnusedVODLogos.mockResolvedValue(mockCleanupResult);
api.getVODLogos.mockResolvedValue(mockRefreshedLogos);
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
const cleanupResult = await result.current.cleanupUnusedVODLogos();
expect(cleanupResult).toEqual(mockCleanupResult);
});
expect(api.cleanupUnusedVODLogos).toHaveBeenCalled();
expect(api.getVODLogos).toHaveBeenCalledWith({
page: 2,
page_size: 10,
});
expect(result.current.logos).toEqual(mockRefreshedLogos);
});
it('should handle cleanup unused VOD logos error', async () => {
const mockError = new Error('Cleanup failed');
api.cleanupUnusedVODLogos.mockRejectedValue(mockError);
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
try {
await result.current.cleanupUnusedVODLogos();
} catch (error) {
expect(error).toBe(mockError);
}
});
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to cleanup unused VOD logos:', mockError);
consoleErrorSpy.mockRestore();
});
it('should clear VOD logos', () => {
useVODLogosStore.setState({
vodLogos: {
1: { id: 1, name: 'Logo 1' },
},
logos: [{ id: 1, name: 'Logo 1' }],
hasLoaded: true,
totalCount: 1,
error: 'Some error',
});
const { result } = renderHook(() => useVODLogosStore());
act(() => {
result.current.clearVODLogos();
});
expect(result.current.vodLogos).toEqual({});
expect(result.current.logos).toEqual([]);
expect(result.current.hasLoaded).toBe(false);
expect(result.current.totalCount).toBe(0);
expect(result.current.error).toBe(null);
});
it('should handle removing non-existent logo', () => {
useVODLogosStore.setState({
vodLogos: {
1: { id: 1, name: 'Logo 1' },
},
logos: [{ id: 1, name: 'Logo 1' }],
totalCount: 1,
});
const { result } = renderHook(() => useVODLogosStore());
act(() => {
result.current.removeVODLogo(999);
});
expect(result.current.vodLogos).toEqual({
1: { id: 1, name: 'Logo 1' },
});
expect(result.current.logos).toEqual([{ id: 1, name: 'Logo 1' }]);
expect(result.current.totalCount).toBe(1);
});
it('should handle fetch with empty response', async () => {
api.getVODLogos.mockResolvedValue([]);
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
await result.current.fetchVODLogos();
});
expect(result.current.vodLogos).toEqual({});
expect(result.current.logos).toEqual([]);
expect(result.current.totalCount).toBe(0);
});
it('should not allow totalCount to go below zero', () => {
useVODLogosStore.setState({
vodLogos: {
1: { id: 1, name: 'Logo 1' },
},
logos: [{ id: 1, name: 'Logo 1' }],
totalCount: 1,
});
const { result } = renderHook(() => useVODLogosStore());
act(() => {
result.current._removeLogosFromState([1, 2, 3]); // Remove more than exist
});
expect(result.current.totalCount).toBe(0);
});
it('should handle _removeLogosFromState with empty array', () => {
const initialState = {
vodLogos: {
1: { id: 1, name: 'Logo 1' },
},
logos: [{ id: 1, name: 'Logo 1' }],
totalCount: 1,
};
useVODLogosStore.setState(initialState);
const { result } = renderHook(() => useVODLogosStore());
act(() => {
result.current._removeLogosFromState([]);
});
expect(result.current.vodLogos).toEqual(initialState.vodLogos);
expect(result.current.logos).toEqual(initialState.logos);
expect(result.current.totalCount).toBe(1);
});
it('should fetch with custom params', async () => {
const mockLogos = [{ id: 1, name: 'Logo 1' }];
api.getVODLogos.mockResolvedValue(mockLogos);
const { result } = renderHook(() => useVODLogosStore());
await act(async () => {
await result.current.fetchVODLogos({ search: 'test', page: 2 });
});
expect(api.getVODLogos).toHaveBeenCalledWith({ search: 'test', page: 2 });
});
});

View file

@ -0,0 +1,195 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach } from 'vitest';
import useWarningsStore from '../warnings';
describe('useWarningsStore', () => {
beforeEach(() => {
useWarningsStore.setState({
suppressedWarnings: {},
});
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useWarningsStore());
expect(result.current.suppressedWarnings).toEqual({});
});
it('should suppress a warning', () => {
const { result } = renderHook(() => useWarningsStore());
act(() => {
result.current.suppressWarning('deleteStream');
});
expect(result.current.suppressedWarnings).toEqual({
deleteStream: true,
});
});
it('should suppress multiple warnings', () => {
const { result } = renderHook(() => useWarningsStore());
act(() => {
result.current.suppressWarning('deleteStream');
result.current.suppressWarning('deleteProfile');
});
expect(result.current.suppressedWarnings).toEqual({
deleteStream: true,
deleteProfile: true,
});
});
it('should suppress warning with explicit true value', () => {
const { result } = renderHook(() => useWarningsStore());
act(() => {
result.current.suppressWarning('deleteStream', true);
});
expect(result.current.suppressedWarnings.deleteStream).toBe(true);
});
it('should unsuppress a warning with explicit false value', () => {
const { result } = renderHook(() => useWarningsStore());
act(() => {
result.current.suppressWarning('deleteStream');
});
expect(result.current.suppressedWarnings.deleteStream).toBe(true);
act(() => {
result.current.suppressWarning('deleteStream', false);
});
expect(result.current.suppressedWarnings.deleteStream).toBe(false);
});
it('should check if warning is suppressed', () => {
const { result } = renderHook(() => useWarningsStore());
expect(result.current.isWarningSuppressed('deleteStream')).toBe(false);
act(() => {
result.current.suppressWarning('deleteStream');
});
expect(result.current.isWarningSuppressed('deleteStream')).toBe(true);
});
it('should return false for non-existent warning', () => {
const { result } = renderHook(() => useWarningsStore());
expect(result.current.isWarningSuppressed('nonExistentAction')).toBe(false);
});
it('should reset all suppressions', () => {
const { result } = renderHook(() => useWarningsStore());
act(() => {
result.current.suppressWarning('deleteStream');
result.current.suppressWarning('deleteProfile');
result.current.suppressWarning('deleteUser');
});
expect(result.current.suppressedWarnings).toEqual({
deleteStream: true,
deleteProfile: true,
deleteUser: true,
});
act(() => {
result.current.resetSuppressions();
});
expect(result.current.suppressedWarnings).toEqual({});
});
it('should maintain other suppressions when adding new one', () => {
const { result } = renderHook(() => useWarningsStore());
act(() => {
result.current.suppressWarning('deleteStream');
result.current.suppressWarning('deleteProfile');
});
expect(result.current.suppressedWarnings).toEqual({
deleteStream: true,
deleteProfile: true,
});
});
it('should handle resetting when already empty', () => {
const { result } = renderHook(() => useWarningsStore());
expect(result.current.suppressedWarnings).toEqual({});
act(() => {
result.current.resetSuppressions();
});
expect(result.current.suppressedWarnings).toEqual({});
});
it('should handle checking suppression after unsuppressing', () => {
const { result } = renderHook(() => useWarningsStore());
act(() => {
result.current.suppressWarning('deleteStream', true);
});
expect(result.current.isWarningSuppressed('deleteStream')).toBe(true);
act(() => {
result.current.suppressWarning('deleteStream', false);
});
expect(result.current.isWarningSuppressed('deleteStream')).toBe(false);
});
it('should preserve other warnings when unsuppressing one', () => {
const { result } = renderHook(() => useWarningsStore());
act(() => {
result.current.suppressWarning('deleteStream');
result.current.suppressWarning('deleteProfile');
});
act(() => {
result.current.suppressWarning('deleteStream', false);
});
expect(result.current.suppressedWarnings).toEqual({
deleteStream: false,
deleteProfile: true,
});
});
it('should handle suppressing same warning multiple times', () => {
const { result } = renderHook(() => useWarningsStore());
act(() => {
result.current.suppressWarning('deleteStream');
result.current.suppressWarning('deleteStream');
});
expect(result.current.suppressedWarnings).toEqual({
deleteStream: true,
});
});
it('should handle different action keys independently', () => {
const { result } = renderHook(() => useWarningsStore());
act(() => {
result.current.suppressWarning('action1');
result.current.suppressWarning('action2', false);
});
expect(result.current.isWarningSuppressed('action1')).toBe(true);
expect(result.current.isWarningSuppressed('action2')).toBe(false);
expect(result.current.isWarningSuppressed('action3')).toBe(false);
});
});