test(useVideoHoverPreview): Add hover delay for desktop
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import FullscreenControl from '../FullscreenControl';
|
||||
|
||||
// Mock useLanguage
|
||||
vi.mock('../../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key })
|
||||
}));
|
||||
|
||||
describe('FullscreenControl', () => {
|
||||
it('should render enter fullscreen icon initially', () => {
|
||||
render(<FullscreenControl isFullscreen={false} onToggle={() => { }} />);
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
// Check for specific icon if possible, or just button presence
|
||||
});
|
||||
|
||||
it('should render exit fullscreen icon when active', () => {
|
||||
render(<FullscreenControl isFullscreen={true} onToggle={() => { }} />);
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onToggle when clicked', () => {
|
||||
const toggleMock = vi.fn();
|
||||
render(<FullscreenControl isFullscreen={false} onToggle={toggleMock} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
expect(toggleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import LoopControl from '../LoopControl';
|
||||
|
||||
// Mock useLanguage
|
||||
vi.mock('../../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key })
|
||||
}));
|
||||
|
||||
describe('LoopControl', () => {
|
||||
it('should render loop button', () => {
|
||||
render(<LoopControl isLooping={false} onToggle={() => { }} />);
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show active color when looping', () => {
|
||||
render(<LoopControl isLooping={true} onToggle={() => { }} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
// Check class or style if needed, but presence is good first step
|
||||
});
|
||||
|
||||
it('should call onToggle when clicked', () => {
|
||||
const toggleMock = vi.fn();
|
||||
render(<LoopControl isLooping={false} onToggle={toggleMock} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
expect(toggleMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import SubtitleControl from '../SubtitleControl';
|
||||
|
||||
// Correct mock path (4 levels deep)
|
||||
vi.mock('../../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key })
|
||||
}));
|
||||
|
||||
describe('SubtitleControl', () => {
|
||||
// Props suitable for the component
|
||||
const defaultProps = {
|
||||
subtitles: [
|
||||
{ language: 'en', filename: 'sub.en.vtt', path: '/subs/sub.en.vtt' },
|
||||
{ language: 'es', filename: 'sub.es.vtt', path: '/subs/sub.es.vtt' }
|
||||
],
|
||||
subtitlesEnabled: false,
|
||||
subtitleMenuAnchor: null,
|
||||
onSubtitleClick: vi.fn(),
|
||||
onCloseMenu: vi.fn(),
|
||||
onSelectSubtitle: vi.fn(),
|
||||
showOnMobile: false
|
||||
};
|
||||
|
||||
it('should render subtitle button when subtitles exist', () => {
|
||||
render(<SubtitleControl {...defaultProps} />);
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
// Check for icon (using testid or implied presence)
|
||||
// Since we don't have icon testids handy in source without checking imports, using getByRole is safe.
|
||||
});
|
||||
|
||||
it('should not render anything if no subtitles', () => {
|
||||
render(<SubtitleControl {...defaultProps} subtitles={[]} />);
|
||||
const button = screen.queryByRole('button');
|
||||
expect(button).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onSubtitleClick when clicked', () => {
|
||||
render(<SubtitleControl {...defaultProps} />);
|
||||
const button = screen.getByRole('button');
|
||||
fireEvent.click(button);
|
||||
expect(defaultProps.onSubtitleClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render menu when anchor is provided', () => {
|
||||
// Create a dummy anchor
|
||||
const anchor = document.createElement('div');
|
||||
render(<SubtitleControl {...defaultProps} subtitleMenuAnchor={anchor} />);
|
||||
|
||||
// Menu should be open, verify items
|
||||
// MUI Menu renders into a portal, so queryByText for items
|
||||
expect(screen.getByText('EN')).toBeInTheDocument();
|
||||
expect(screen.getByText('ES')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onSelectSubtitle when item clicked', () => {
|
||||
const anchor = document.createElement('div');
|
||||
render(<SubtitleControl {...defaultProps} subtitleMenuAnchor={anchor} />);
|
||||
|
||||
const enOption = screen.getByText('EN');
|
||||
fireEvent.click(enOption);
|
||||
expect(defaultProps.onSelectSubtitle).toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
142
frontend/src/contexts/__tests__/CollectionContext.test.tsx
Normal file
142
frontend/src/contexts/__tests__/CollectionContext.test.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import axios from 'axios';
|
||||
import React from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CollectionProvider, useCollection } from '../CollectionContext';
|
||||
import { LanguageProvider } from '../LanguageContext';
|
||||
import { SnackbarProvider } from '../SnackbarContext';
|
||||
|
||||
// Mock axios
|
||||
vi.mock('axios');
|
||||
const mockedAxios = vi.mocked(axios, true);
|
||||
|
||||
// Wrappers
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SnackbarProvider>
|
||||
<LanguageProvider>
|
||||
<CollectionProvider>{children}</CollectionProvider>
|
||||
</LanguageProvider>
|
||||
</SnackbarProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('CollectionContext', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock global localStorage
|
||||
const storageMock: Record<string, string> = {};
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: {
|
||||
getItem: vi.fn((key) => storageMock[key] || null),
|
||||
setItem: vi.fn((key, value) => {
|
||||
storageMock[key] = value.toString();
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
for (const key in storageMock) delete storageMock[key];
|
||||
}),
|
||||
removeItem: vi.fn((key) => delete storageMock[key]),
|
||||
length: 0,
|
||||
key: vi.fn(),
|
||||
},
|
||||
writable: true
|
||||
});
|
||||
|
||||
// Default mocks
|
||||
mockedAxios.get.mockResolvedValue({ data: [] });
|
||||
});
|
||||
|
||||
it('should provide collections data', async () => {
|
||||
const mockCollections = [{ id: '1', name: 'My Collection', videos: [] }];
|
||||
mockedAxios.get.mockResolvedValueOnce({ data: mockCollections });
|
||||
|
||||
const { result } = renderHook(() => useCollection(), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.collections).toEqual(mockCollections);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a collection', async () => {
|
||||
const mockCollection = { id: 'new', name: 'New Col', videos: ['v1'] };
|
||||
mockedAxios.post.mockResolvedValueOnce({ data: mockCollection });
|
||||
|
||||
const { result } = renderHook(() => useCollection(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.createCollection('New Col', 'v1');
|
||||
});
|
||||
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/collections'),
|
||||
{ name: 'New Col', videoId: 'v1' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should add video to collection', async () => {
|
||||
mockedAxios.put.mockResolvedValueOnce({ data: { success: true } });
|
||||
|
||||
const { result } = renderHook(() => useCollection(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.addToCollection('col1', 'vid1');
|
||||
});
|
||||
|
||||
expect(mockedAxios.put).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/collections/col1'),
|
||||
{ videoId: 'vid1', action: 'add' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove video from collection', async () => {
|
||||
const mockCollections = [{ id: '1', name: 'C1', videos: ['vid1'] }];
|
||||
// First get is initialization
|
||||
mockedAxios.get.mockResolvedValueOnce({ data: mockCollections });
|
||||
|
||||
const { result } = renderHook(() => useCollection(), { wrapper: createWrapper() });
|
||||
|
||||
// Wait for load
|
||||
await waitFor(() => {
|
||||
expect(result.current.collections).toHaveLength(1);
|
||||
});
|
||||
|
||||
mockedAxios.put.mockResolvedValueOnce({ data: { success: true } });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.removeFromCollection('vid1');
|
||||
});
|
||||
|
||||
expect(mockedAxios.put).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/collections/1'),
|
||||
{ videoId: 'vid1', action: 'remove' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete a collection', async () => {
|
||||
mockedAxios.delete.mockResolvedValueOnce({ data: { success: true } });
|
||||
|
||||
const { result } = renderHook(() => useCollection(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
const res = await result.current.deleteCollection('col1');
|
||||
expect(res.success).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockedAxios.delete).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/collections/col1'),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
149
frontend/src/contexts/__tests__/DownloadContext.test.tsx
Normal file
149
frontend/src/contexts/__tests__/DownloadContext.test.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import axios from 'axios';
|
||||
import React from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CollectionProvider } from '../CollectionContext';
|
||||
import { DownloadProvider, useDownload } from '../DownloadContext';
|
||||
import { LanguageProvider } from '../LanguageContext';
|
||||
import { SnackbarProvider } from '../SnackbarContext';
|
||||
import { VideoProvider } from '../VideoContext';
|
||||
|
||||
vi.mock('axios');
|
||||
const mockedAxios = vi.mocked(axios, true);
|
||||
|
||||
// Create a wrapper with all necessary providers
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SnackbarProvider>
|
||||
<LanguageProvider>
|
||||
<VideoProvider>
|
||||
<CollectionProvider>
|
||||
<DownloadProvider>{children}</DownloadProvider>
|
||||
</CollectionProvider>
|
||||
</VideoProvider>
|
||||
</LanguageProvider>
|
||||
</SnackbarProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('DownloadContext', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock global localStorage
|
||||
const storageMock: Record<string, string> = {};
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: {
|
||||
getItem: vi.fn((key) => storageMock[key] || null),
|
||||
setItem: vi.fn((key, value) => {
|
||||
storageMock[key] = value.toString();
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
for (const key in storageMock) delete storageMock[key];
|
||||
}),
|
||||
removeItem: vi.fn((key) => delete storageMock[key]),
|
||||
length: 0,
|
||||
key: vi.fn(),
|
||||
},
|
||||
writable: true
|
||||
});
|
||||
|
||||
// Setup default mocks for initialization
|
||||
mockedAxios.get.mockImplementation((url) => {
|
||||
if (url.includes('/download-status')) {
|
||||
return Promise.resolve({ data: { activeDownloads: [], queuedDownloads: [] } });
|
||||
}
|
||||
if (url.includes('/videos')) {
|
||||
return Promise.resolve({ data: [] });
|
||||
}
|
||||
if (url.includes('/settings')) {
|
||||
return Promise.resolve({ data: {} });
|
||||
}
|
||||
if (url.includes('/collections')) {
|
||||
return Promise.resolve({ data: [] });
|
||||
}
|
||||
return Promise.resolve({ data: {} });
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialize with empty downloads', async () => {
|
||||
const { result } = renderHook(() => useDownload(), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeDownloads).toEqual([]);
|
||||
expect(result.current.queuedDownloads).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle single video submission', async () => {
|
||||
mockedAxios.post.mockResolvedValueOnce({ data: { downloadId: '123' } });
|
||||
|
||||
const { result } = renderHook(() => useDownload(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
const res = await result.current.handleVideoSubmit('https://youtube.com/watch?v=123');
|
||||
expect(res.success).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/download'),
|
||||
expect.objectContaining({ youtubeUrl: 'https://youtube.com/watch?v=123' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect playlist and set modal state', async () => {
|
||||
mockedAxios.get.mockImplementation((url) => {
|
||||
if (url.includes('/check-playlist')) {
|
||||
return Promise.resolve({
|
||||
data: { success: true, title: 'My Playlist', videoCount: 10 }
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ data: {} });
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useDownload(), { wrapper: createWrapper() });
|
||||
const playlistUrl = 'https://youtube.com/playlist?list=PL123';
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleVideoSubmit(playlistUrl);
|
||||
});
|
||||
|
||||
expect(result.current.showBilibiliPartsModal).toBe(true);
|
||||
expect(result.current.bilibiliPartsInfo.type).toBe('playlist');
|
||||
});
|
||||
|
||||
it('should handle playlist download confirmation', async () => {
|
||||
const { result } = renderHook(() => useDownload(), { wrapper: createWrapper() });
|
||||
|
||||
mockedAxios.get.mockImplementation((url) => {
|
||||
if (url.includes('/check-playlist')) {
|
||||
return Promise.resolve({
|
||||
data: { success: true, title: 'PL', videoCount: 5 }
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ data: {} });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleVideoSubmit('https://youtube.com/playlist?list=PL123');
|
||||
});
|
||||
|
||||
mockedAxios.post.mockResolvedValueOnce({ data: { success: true } });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleDownloadAllBilibiliParts('My Playlist');
|
||||
});
|
||||
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/subscriptions/tasks/playlist'),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
143
frontend/src/contexts/__tests__/VideoContext.test.tsx
Normal file
143
frontend/src/contexts/__tests__/VideoContext.test.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import axios from 'axios';
|
||||
import React from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { LanguageProvider } from '../LanguageContext';
|
||||
import { SnackbarProvider } from '../SnackbarContext';
|
||||
import { VideoProvider, useVideo } from '../VideoContext';
|
||||
import { VisitorModeProvider } from '../VisitorModeContext';
|
||||
|
||||
// Mock axios
|
||||
vi.mock('axios');
|
||||
const mockedAxios = vi.mocked(axios, true);
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SnackbarProvider>
|
||||
<LanguageProvider>
|
||||
<VisitorModeProvider>
|
||||
<VideoProvider>{children}</VideoProvider>
|
||||
</VisitorModeProvider>
|
||||
</LanguageProvider>
|
||||
</SnackbarProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('VideoContext', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock global localStorage
|
||||
const storageMock: Record<string, string> = {};
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: {
|
||||
getItem: vi.fn((key) => storageMock[key] || null),
|
||||
setItem: vi.fn((key, value) => {
|
||||
storageMock[key] = value.toString();
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
for (const key in storageMock) delete storageMock[key];
|
||||
}),
|
||||
removeItem: vi.fn((key) => delete storageMock[key]),
|
||||
length: 0,
|
||||
key: vi.fn(),
|
||||
},
|
||||
writable: true
|
||||
});
|
||||
|
||||
// Default mocks
|
||||
mockedAxios.get.mockImplementation((url) => {
|
||||
if (url.includes('/videos')) return Promise.resolve({ data: [] });
|
||||
if (url.includes('/settings')) return Promise.resolve({ data: { tags: [] } });
|
||||
return Promise.resolve({ data: {} });
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch videos on mount', async () => {
|
||||
const mockVideos = [{ id: '1', title: 'Test Video', author: 'Test Author' }];
|
||||
mockedAxios.get.mockImplementation((url) => {
|
||||
if (url.includes('/videos')) return Promise.resolve({ data: mockVideos });
|
||||
return Promise.resolve({ data: {} });
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVideo(), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.videos).toEqual(mockVideos);
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a video', async () => {
|
||||
const mockVideos = [{ id: '1', title: 'Video 1' }];
|
||||
mockedAxios.get.mockImplementation((url) => {
|
||||
if (url.includes('/videos')) return Promise.resolve({ data: mockVideos });
|
||||
return Promise.resolve({ data: {} });
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVideo(), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => expect(result.current.videos).toHaveLength(1));
|
||||
|
||||
mockedAxios.delete.mockResolvedValueOnce({ data: { success: true } });
|
||||
|
||||
await act(async () => {
|
||||
const res = await result.current.deleteVideo('1');
|
||||
expect(res.success).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockedAxios.delete).toHaveBeenCalledWith(expect.stringContaining('/videos/1'));
|
||||
});
|
||||
|
||||
it('should handle search (local)', async () => {
|
||||
const mockVideos = [
|
||||
{ id: '1', title: 'React Tutorial', author: 'User A' },
|
||||
{ id: '2', title: 'Vue Guide', author: 'User B' }
|
||||
];
|
||||
|
||||
mockedAxios.get.mockImplementation((url) => {
|
||||
if (url.includes('/videos')) return Promise.resolve({ data: mockVideos });
|
||||
if (url.includes('/settings')) return Promise.resolve({ data: { showYoutubeSearch: false } }); // Disable YT search for this test
|
||||
return Promise.resolve({ data: {} });
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVideo(), { wrapper: createWrapper() });
|
||||
await waitFor(() => expect(result.current.videos).toHaveLength(2));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSearch('React');
|
||||
});
|
||||
|
||||
expect(result.current.isSearchMode).toBe(true);
|
||||
expect(result.current.localSearchResults).toHaveLength(1);
|
||||
expect(result.current.localSearchResults[0].title).toBe('React Tutorial');
|
||||
});
|
||||
|
||||
it('should increment view count', async () => {
|
||||
const mockVideos = [{ id: '1', title: 'V1', viewCount: 0 }];
|
||||
mockedAxios.get.mockImplementation((url) => {
|
||||
if (url.includes('/videos')) return Promise.resolve({ data: mockVideos });
|
||||
return Promise.resolve({ data: {} });
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVideo(), { wrapper: createWrapper() });
|
||||
await waitFor(() => expect(result.current.videos).toHaveLength(1));
|
||||
|
||||
mockedAxios.post.mockResolvedValueOnce({ data: { success: true, viewCount: 1 } });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.incrementView('1');
|
||||
});
|
||||
|
||||
// The queryClient setQueryData is synchronous, but we might need to wait for re-render
|
||||
// However, useQuery data reference might not update immediately in "videos" since it's from state/memo
|
||||
// Let's verify axios call
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(expect.stringContaining('/videos/1/view'));
|
||||
});
|
||||
});
|
||||
52
frontend/src/hooks/__tests__/useShareVideo.test.ts
Normal file
52
frontend/src/hooks/__tests__/useShareVideo.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useShareVideo } from '../useShareVideo';
|
||||
|
||||
const mockShowSnackbar = vi.fn();
|
||||
const mockT = vi.fn((key) => key);
|
||||
|
||||
vi.mock('../../contexts/SnackbarContext', () => ({
|
||||
useSnackbar: () => ({ showSnackbar: mockShowSnackbar })
|
||||
}));
|
||||
|
||||
vi.mock('../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: mockT })
|
||||
}));
|
||||
|
||||
describe('useShareVideo', () => {
|
||||
const mockVideo = { id: '1', title: 'Test' };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset navigator mocks
|
||||
Object.defineProperty(navigator, 'share', { value: undefined, configurable: true });
|
||||
Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true });
|
||||
});
|
||||
|
||||
it('should use navigator.share if available', async () => {
|
||||
const mockShare = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, 'share', { value: mockShare, configurable: true });
|
||||
|
||||
const { result } = renderHook(() => useShareVideo(mockVideo as any));
|
||||
await result.current.handleShare();
|
||||
|
||||
expect(mockShare).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: 'Test',
|
||||
url: expect.any(String)
|
||||
}));
|
||||
});
|
||||
|
||||
it('should use clipboard API if navigator.share unavailable', async () => {
|
||||
const mockWriteText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: mockWriteText },
|
||||
configurable: true
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useShareVideo(mockVideo as any));
|
||||
await result.current.handleShare();
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalled();
|
||||
expect(mockShowSnackbar).toHaveBeenCalledWith('linkCopied', 'success');
|
||||
});
|
||||
});
|
||||
91
frontend/src/hooks/__tests__/useVideoCardActions.test.ts
Normal file
91
frontend/src/hooks/__tests__/useVideoCardActions.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useVideoCardActions } from '../useVideoCardActions';
|
||||
|
||||
// Mocks
|
||||
const mockShowSnackbar = vi.fn();
|
||||
const mockUpdateVideo = vi.fn();
|
||||
const mockT = vi.fn((key) => key);
|
||||
|
||||
vi.mock('../../contexts/SnackbarContext', () => ({
|
||||
useSnackbar: () => ({ showSnackbar: mockShowSnackbar })
|
||||
}));
|
||||
|
||||
vi.mock('../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: mockT })
|
||||
}));
|
||||
|
||||
vi.mock('../../contexts/VideoContext', () => ({
|
||||
useVideo: () => ({ updateVideo: mockUpdateVideo })
|
||||
}));
|
||||
|
||||
describe('useVideoCardActions', () => {
|
||||
const mockVideo = {
|
||||
id: '1',
|
||||
title: 'Test Video',
|
||||
visibility: 1
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should delete video successfully', async () => {
|
||||
const mockOnDelete = vi.fn().mockResolvedValue(true);
|
||||
const { result } = renderHook(() => useVideoCardActions({
|
||||
video: mockVideo as any,
|
||||
onDeleteVideo: mockOnDelete,
|
||||
showDeleteButton: true
|
||||
}));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.confirmDelete();
|
||||
});
|
||||
|
||||
expect(mockOnDelete).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('should toggle visibility (hide)', async () => {
|
||||
mockUpdateVideo.mockResolvedValue({ success: true });
|
||||
|
||||
const { result } = renderHook(() => useVideoCardActions({
|
||||
video: { ...mockVideo, visibility: 1 } as any
|
||||
}));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleToggleVisibility();
|
||||
});
|
||||
|
||||
expect(mockUpdateVideo).toHaveBeenCalledWith('1', { visibility: 0 });
|
||||
expect(mockShowSnackbar).toHaveBeenCalledWith('hideVideo', 'success');
|
||||
});
|
||||
|
||||
it('should toggle visibility (show)', async () => {
|
||||
mockUpdateVideo.mockResolvedValue({ success: true });
|
||||
|
||||
const { result } = renderHook(() => useVideoCardActions({
|
||||
video: { ...mockVideo, visibility: 0 } as any
|
||||
}));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleToggleVisibility();
|
||||
});
|
||||
|
||||
expect(mockUpdateVideo).toHaveBeenCalledWith('1', { visibility: 1 });
|
||||
expect(mockShowSnackbar).toHaveBeenCalledWith('showVideo', 'success');
|
||||
});
|
||||
|
||||
it('should handle update error', async () => {
|
||||
mockUpdateVideo.mockResolvedValue({ success: false });
|
||||
|
||||
const { result } = renderHook(() => useVideoCardActions({
|
||||
video: mockVideo as any
|
||||
}));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleToggleVisibility();
|
||||
});
|
||||
|
||||
expect(mockShowSnackbar).toHaveBeenCalledWith('error', 'error');
|
||||
});
|
||||
});
|
||||
60
frontend/src/hooks/__tests__/useVideoCardMetadata.test.ts
Normal file
60
frontend/src/hooks/__tests__/useVideoCardMetadata.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { useVideoCardMetadata } from '../useVideoCardMetadata';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../useCloudStorageUrl', () => ({
|
||||
useCloudStorageUrl: (path: string | null) => path ? `cloud-url/${path}` : null
|
||||
}));
|
||||
|
||||
// Mock isNewVideo util
|
||||
vi.mock('../../utils/videoCardUtils', () => ({
|
||||
isNewVideo: () => true
|
||||
}));
|
||||
|
||||
describe('useVideoCardMetadata', () => {
|
||||
it('should return cloud url if available', async () => {
|
||||
const mockVideo = {
|
||||
id: '1',
|
||||
videoPath: 'cloud:video.mp4',
|
||||
thumbnailPath: 'cloud:thumb.jpg'
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useVideoCardMetadata({ video: mockVideo as any }));
|
||||
|
||||
expect(result.current.videoUrl).toBe('cloud-url/cloud:video.mp4');
|
||||
expect(result.current.thumbnailSrc).toBe('cloud-url/cloud:thumb.jpg');
|
||||
|
||||
const url = await result.current.getVideoUrl();
|
||||
expect(url).toBe('cloud-url/cloud:video.mp4');
|
||||
});
|
||||
|
||||
it('should return local url fallback', async () => {
|
||||
const mockVideo = {
|
||||
id: '1',
|
||||
videoPath: '/local/video.mp4',
|
||||
thumbnailPath: '/local/thumb.jpg',
|
||||
thumbnailUrl: 'http://thumb.url'
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useVideoCardMetadata({ video: mockVideo as any }));
|
||||
|
||||
// Mock import.meta.env behavior or window.location if needed for exact string match
|
||||
// Based on implementation: `${window.location.origin}${videoPath}`
|
||||
// In test env, window.location.origin is usually http://localhost:3000
|
||||
|
||||
const url = await result.current.getVideoUrl();
|
||||
expect(url).toContain('/local/video.mp4');
|
||||
expect(result.current.thumbnailSrc).toContain('/local/thumb.jpg');
|
||||
});
|
||||
|
||||
it('should prioritize thumbnailUrl if no local/cloud path', () => {
|
||||
const mockVideo = {
|
||||
id: '1',
|
||||
thumbnailUrl: 'http://external.com/thumb.jpg'
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useVideoCardMetadata({ video: mockVideo as any }));
|
||||
expect(result.current.thumbnailSrc).toBe('http://external.com/thumb.jpg');
|
||||
});
|
||||
});
|
||||
45
frontend/src/hooks/__tests__/useVideoCardNavigation.test.ts
Normal file
45
frontend/src/hooks/__tests__/useVideoCardNavigation.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { useVideoCardNavigation } from '../useVideoCardNavigation';
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockNavigate
|
||||
}));
|
||||
|
||||
describe('useVideoCardNavigation', () => {
|
||||
const mockVideo = { id: 'v1', author: 'Test Author' };
|
||||
|
||||
it('should navigate to video player normally', () => {
|
||||
const { result } = renderHook(() => useVideoCardNavigation({
|
||||
video: mockVideo as any,
|
||||
collectionInfo: { isFirstInAnyCollection: false, firstCollectionId: null, videoCollections: [], firstInCollectionNames: [] }
|
||||
}));
|
||||
|
||||
result.current.handleVideoNavigation();
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/video/v1');
|
||||
});
|
||||
|
||||
it('should navigate to collection if first in collection', () => {
|
||||
const { result } = renderHook(() => useVideoCardNavigation({
|
||||
video: mockVideo as any,
|
||||
collectionInfo: { isFirstInAnyCollection: true, firstCollectionId: 'c1', videoCollections: [], firstInCollectionNames: [] }
|
||||
}));
|
||||
|
||||
result.current.handleVideoNavigation();
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/collection/c1');
|
||||
});
|
||||
|
||||
it('should handle author click navigation', () => {
|
||||
const { result } = renderHook(() => useVideoCardNavigation({
|
||||
video: mockVideo as any,
|
||||
collectionInfo: { isFirstInAnyCollection: false, firstCollectionId: null, videoCollections: [], firstInCollectionNames: [] }
|
||||
}));
|
||||
|
||||
const mockEvent = { stopPropagation: vi.fn() };
|
||||
result.current.handleAuthorClick(mockEvent as any);
|
||||
|
||||
expect(mockEvent.stopPropagation).toHaveBeenCalled();
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/author/Test%20Author');
|
||||
});
|
||||
});
|
||||
79
frontend/src/hooks/__tests__/useVideoCollections.test.ts
Normal file
79
frontend/src/hooks/__tests__/useVideoCollections.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useVideoCollections } from '../useVideoCollections';
|
||||
|
||||
// Mocks
|
||||
const mockCollections = [
|
||||
{ id: 'c1', name: 'Collection 1', videos: ['v1', 'v2'] },
|
||||
{ id: 'c2', name: 'Collection 2', videos: ['v3'] }
|
||||
];
|
||||
|
||||
const mockAddToCollection = vi.fn();
|
||||
const mockCreateCollection = vi.fn();
|
||||
const mockRemoveFromCollection = vi.fn();
|
||||
|
||||
vi.mock('../../contexts/CollectionContext', () => ({
|
||||
useCollection: () => ({
|
||||
collections: mockCollections,
|
||||
addToCollection: mockAddToCollection,
|
||||
createCollection: mockCreateCollection,
|
||||
removeFromCollection: mockRemoveFromCollection
|
||||
})
|
||||
}));
|
||||
|
||||
describe('useVideoCollections', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should filter collections containing the video', () => {
|
||||
const { result } = renderHook(() => useVideoCollections({ videoId: 'v1' }));
|
||||
|
||||
expect(result.current.videoCollections).toHaveLength(1);
|
||||
expect(result.current.videoCollections[0].id).toBe('c1');
|
||||
});
|
||||
|
||||
it('should return empty if video not in any collection', () => {
|
||||
const { result } = renderHook(() => useVideoCollections({ videoId: 'v99' }));
|
||||
expect(result.current.videoCollections).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle add to collection modal', () => {
|
||||
const { result } = renderHook(() => useVideoCollections({ videoId: 'v1' }));
|
||||
|
||||
act(() => {
|
||||
result.current.handleAddToCollection();
|
||||
});
|
||||
|
||||
expect(result.current.showCollectionModal).toBe(true);
|
||||
expect(result.current.activeCollectionVideoId).toBe('v1');
|
||||
});
|
||||
|
||||
it('should create new collection', async () => {
|
||||
const { result } = renderHook(() => useVideoCollections({ videoId: 'v1' }));
|
||||
|
||||
act(() => {
|
||||
result.current.handleAddToCollection();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleCreateCollection('New List');
|
||||
});
|
||||
|
||||
expect(mockCreateCollection).toHaveBeenCalledWith('New List', 'v1');
|
||||
});
|
||||
|
||||
it('should remove from collection', async () => {
|
||||
const { result } = renderHook(() => useVideoCollections({ videoId: 'v1' }));
|
||||
|
||||
act(() => {
|
||||
result.current.handleAddToCollection();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRemoveFromCollection();
|
||||
});
|
||||
|
||||
expect(mockRemoveFromCollection).toHaveBeenCalledWith('v1');
|
||||
});
|
||||
});
|
||||
62
frontend/src/hooks/__tests__/useVideoHoverPreview.test.ts
Normal file
62
frontend/src/hooks/__tests__/useVideoHoverPreview.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useVideoHoverPreview } from '../useVideoHoverPreview';
|
||||
|
||||
// Mocks
|
||||
vi.mock('@mui/material', () => ({
|
||||
useTheme: () => ({ breakpoints: { down: () => 'sm' } }),
|
||||
useMediaQuery: (query: string) => query === 'mobile-query' // Simulate desktop usually unless specified
|
||||
}));
|
||||
|
||||
describe('useVideoHoverPreview', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should set hovered state after delay on desktop', () => {
|
||||
const { result } = renderHook(() => useVideoHoverPreview({ videoPath: 'path.mp4' }));
|
||||
|
||||
act(() => {
|
||||
result.current.handleMouseEnter();
|
||||
});
|
||||
|
||||
expect(result.current.isHovered).toBe(false);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
expect(result.current.isHovered).toBe(true);
|
||||
});
|
||||
|
||||
it('should clear timeout and state on mouse leave', () => {
|
||||
const { result } = renderHook(() => useVideoHoverPreview({ videoPath: 'path.mp4' }));
|
||||
|
||||
// Mock video ref
|
||||
// @ts-ignore
|
||||
result.current.videoRef.current = {
|
||||
pause: vi.fn(),
|
||||
load: vi.fn(),
|
||||
removeAttribute: vi.fn(),
|
||||
src: 'blob:...'
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.handleMouseEnter();
|
||||
// Leave before timeout finishes
|
||||
result.current.handleMouseLeave();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
expect(result.current.isHovered).toBe(false);
|
||||
// @ts-ignore
|
||||
expect(result.current.videoRef.current.pause).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user