mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
Added unit tests
This commit is contained in:
parent
8521df94ad
commit
bd4c9e1d95
18 changed files with 6631 additions and 0 deletions
110
frontend/src/hooks/__tests__/useLocalStorage.test.jsx
Normal file
110
frontend/src/hooks/__tests__/useLocalStorage.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
297
frontend/src/hooks/__tests__/useSmartLogos.test.jsx
Normal file
297
frontend/src/hooks/__tests__/useSmartLogos.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
479
frontend/src/store/__tests__/auth.test.jsx
Normal file
479
frontend/src/store/__tests__/auth.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
442
frontend/src/store/__tests__/channels.test.jsx
Normal file
442
frontend/src/store/__tests__/channels.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
295
frontend/src/store/__tests__/channelsTable.test.jsx
Normal file
295
frontend/src/store/__tests__/channelsTable.test.jsx
Normal 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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
687
frontend/src/store/__tests__/epgs.test.jsx
Normal file
687
frontend/src/store/__tests__/epgs.test.jsx
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
916
frontend/src/store/__tests__/logos.test.jsx
Normal file
916
frontend/src/store/__tests__/logos.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
266
frontend/src/store/__tests__/playlists.test.jsx
Normal file
266
frontend/src/store/__tests__/playlists.test.jsx
Normal 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');
|
||||
});
|
||||
});
|
||||
234
frontend/src/store/__tests__/plugins.test.jsx
Normal file
234
frontend/src/store/__tests__/plugins.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
204
frontend/src/store/__tests__/settings.test.jsx
Normal file
204
frontend/src/store/__tests__/settings.test.jsx
Normal 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({});
|
||||
});
|
||||
});
|
||||
265
frontend/src/store/__tests__/streamProfiles.test.jsx
Normal file
265
frontend/src/store/__tests__/streamProfiles.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
289
frontend/src/store/__tests__/streams.test.jsx
Normal file
289
frontend/src/store/__tests__/streams.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
755
frontend/src/store/__tests__/useVODStore.test.jsx
Normal file
755
frontend/src/store/__tests__/useVODStore.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
182
frontend/src/store/__tests__/useVideoStore.test.jsx
Normal file
182
frontend/src/store/__tests__/useVideoStore.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
277
frontend/src/store/__tests__/userAgents.test.jsx
Normal file
277
frontend/src/store/__tests__/userAgents.test.jsx
Normal 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([]);
|
||||
});
|
||||
});
|
||||
258
frontend/src/store/__tests__/users.test.jsx
Normal file
258
frontend/src/store/__tests__/users.test.jsx
Normal 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' });
|
||||
});
|
||||
});
|
||||
480
frontend/src/store/__tests__/vodLogos.test.jsx
Normal file
480
frontend/src/store/__tests__/vodLogos.test.jsx
Normal 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 });
|
||||
});
|
||||
});
|
||||
195
frontend/src/store/__tests__/warnings.test.jsx
Normal file
195
frontend/src/store/__tests__/warnings.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue