test(SubscriptionModal): Add subscription modal tests
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import axios from 'axios';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { Settings } from '../../../types';
|
||||
import CloudDriveSettings from '../CloudDriveSettings';
|
||||
|
||||
// Mock language context
|
||||
vi.mock('../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
// Mock ConfirmationModal
|
||||
vi.mock('../../ConfirmationModal', () => ({
|
||||
default: ({ isOpen, onConfirm, onClose, title, message }: any) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div data-testid="confirmation-modal">
|
||||
<h2>{title}</h2>
|
||||
<p>{message}</p>
|
||||
<button onClick={onConfirm}>Confirm</button>
|
||||
<button onClick={onClose}>Cancel</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock axios
|
||||
vi.mock('axios');
|
||||
|
||||
describe('CloudDriveSettings', () => {
|
||||
const defaultSettings: Settings = {
|
||||
cloudDriveEnabled: true,
|
||||
openListApiUrl: 'http://localhost/api/fs/put',
|
||||
openListToken: 'test-token',
|
||||
openListPublicUrl: 'http://localhost',
|
||||
cloudDrivePath: '/uploads',
|
||||
cloudDriveScanPaths: '/scan',
|
||||
} as Settings;
|
||||
|
||||
const mockOnChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render all fields when enabled', () => {
|
||||
render(<CloudDriveSettings settings={defaultSettings} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText(/enableAutoSave/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/apiUrl/i)).toHaveValue('http://localhost/api/fs/put');
|
||||
expect(screen.getByLabelText(/token/i)).toHaveValue('test-token');
|
||||
expect(screen.getByLabelText(/publicUrl/i)).toHaveValue('http://localhost');
|
||||
expect(screen.getByLabelText(/uploadPath/i)).toHaveValue('/uploads');
|
||||
expect(screen.getByLabelText(/scanPaths/i)).toHaveValue('/scan');
|
||||
});
|
||||
|
||||
it('should hide fields when disabled', () => {
|
||||
render(<CloudDriveSettings settings={{ ...defaultSettings, cloudDriveEnabled: false }} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText(/enableAutoSave/i)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/apiUrl/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should validate API URL format', async () => {
|
||||
render(<CloudDriveSettings settings={{ ...defaultSettings, openListApiUrl: 'invalid-url' }} onChange={mockOnChange} />);
|
||||
|
||||
// This relies on the component rendering the error message based on the invalid prop passed
|
||||
// The component validates props immediately on render
|
||||
// Check for error helper text if rendered locally
|
||||
// Looking at the code: const apiUrlError = ...
|
||||
// So we might need to find the error message.
|
||||
// Actually, MUI helperText usually renders.
|
||||
// The validation logic is inside the component render body.
|
||||
|
||||
await waitFor(() => {
|
||||
// We can check if invalid-url causes "Invalid URL format" or similar if the component shows it.
|
||||
// The mock t returns key.
|
||||
// We can assume validateApiUrl returns 'URL must start with http:// or https://' or 'URL should end with /api/fs/put'
|
||||
// 'invalid-url' fails 'http' check.
|
||||
expect(screen.getByLabelText(/apiUrl/i)).toBeInvalid();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onChange when fields are updated', async () => {
|
||||
render(<CloudDriveSettings settings={defaultSettings} onChange={mockOnChange} />);
|
||||
|
||||
const urlInput = screen.getByLabelText(/apiUrl/i);
|
||||
fireEvent.change(urlInput, { target: { value: 'http://new-url' } });
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('openListApiUrl', 'http://new-url');
|
||||
});
|
||||
|
||||
it('should handle test connection success', async () => {
|
||||
const user = userEvent.setup();
|
||||
(axios.request as any).mockResolvedValue({ status: 200 });
|
||||
|
||||
render(<CloudDriveSettings settings={defaultSettings} onChange={mockOnChange} />);
|
||||
|
||||
const testBtn = screen.getByText('testConnection');
|
||||
await user.click(testBtn);
|
||||
|
||||
expect(axios.request).toHaveBeenCalled();
|
||||
expect(await screen.findByText('connectionTestSuccess')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle test connection failure', async () => {
|
||||
const user = userEvent.setup();
|
||||
(axios.request as any).mockRejectedValue(new Error('Network Error'));
|
||||
|
||||
render(<CloudDriveSettings settings={defaultSettings} onChange={mockOnChange} />);
|
||||
|
||||
const testBtn = screen.getByText('testConnection');
|
||||
await user.click(testBtn);
|
||||
|
||||
expect(await screen.findByText(/connectionTestFailed/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle sync flow', async () => {
|
||||
const user = userEvent.setup();
|
||||
// Mock global fetch for sync
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
body: {
|
||||
getReader: () => ({
|
||||
read: vi.fn()
|
||||
.mockResolvedValueOnce({ done: false, value: new TextEncoder().encode(JSON.stringify({ type: 'progress', current: 1, total: 2 }) + '\n') })
|
||||
.mockResolvedValueOnce({ done: false, value: new TextEncoder().encode(JSON.stringify({ type: 'complete', report: { total: 2, uploaded: 2, failed: 0, errors: [] } }) + '\n') })
|
||||
.mockResolvedValueOnce({ done: true })
|
||||
})
|
||||
}
|
||||
} as any);
|
||||
|
||||
render(<CloudDriveSettings settings={defaultSettings} onChange={mockOnChange} />);
|
||||
|
||||
// Click Sync button to open modal
|
||||
await user.click(screen.getByText('sync'));
|
||||
|
||||
// Confirm in modal
|
||||
expect(screen.getByTestId('confirmation-modal')).toBeInTheDocument();
|
||||
await user.click(screen.getByText('Confirm'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('/cloud/sync'), expect.any(Object));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useSnackbar } from '../../../contexts/SnackbarContext';
|
||||
import { useCloudflareStatus } from '../../../hooks/useCloudflareStatus';
|
||||
import CloudflareSettings from '../CloudflareSettings';
|
||||
|
||||
// Mock contexts and hooks
|
||||
vi.mock('../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
vi.mock('../../../contexts/SnackbarContext', () => ({
|
||||
useSnackbar: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../hooks/useCloudflareStatus', () => ({
|
||||
useCloudflareStatus: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('CloudflareSettings', () => {
|
||||
const mockOnChange = vi.fn();
|
||||
const mockShowSnackbar = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(useSnackbar as any).mockReturnValue({ showSnackbar: mockShowSnackbar });
|
||||
// Default mock for hook
|
||||
(useCloudflareStatus as any).mockReturnValue({
|
||||
data: { isRunning: false },
|
||||
isLoading: false
|
||||
});
|
||||
});
|
||||
|
||||
it('should render switch and fields', () => {
|
||||
render(<CloudflareSettings enabled={true} token="test-token" visitorMode={false} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText(/enableCloudflaredTunnel/i)).toBeChecked();
|
||||
expect(screen.getByLabelText(/cloudflaredToken/i)).toHaveValue('test-token');
|
||||
});
|
||||
|
||||
it('should update enable state on switch toggle', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CloudflareSettings enabled={false} token="" visitorMode={false} onChange={mockOnChange} />);
|
||||
|
||||
const switchControl = screen.getByRole('switch', { name: /enableCloudflaredTunnel/i });
|
||||
await user.click(switchControl);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cloudflaredTunnelEnabled', true);
|
||||
});
|
||||
|
||||
it('should update token on change', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<CloudflareSettings enabled={true} token="" visitorMode={false} onChange={mockOnChange} />);
|
||||
|
||||
const tokenInput = screen.getByLabelText(/cloudflaredToken/i);
|
||||
await user.type(tokenInput, 'new-token');
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cloudflaredToken', 'n');
|
||||
});
|
||||
|
||||
it('should display running status', () => {
|
||||
(useCloudflareStatus as any).mockReturnValue({
|
||||
data: { isRunning: true, publicUrl: 'https://test.trycloudflare.com', tunnelId: '123' },
|
||||
isLoading: false
|
||||
});
|
||||
|
||||
render(<CloudflareSettings enabled={true} token="test-token" visitorMode={false} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByText('running')).toBeInTheDocument();
|
||||
expect(screen.getByText('https://test.trycloudflare.com')).toBeInTheDocument();
|
||||
expect(screen.getByText('123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle copy to clipboard', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock navigator.clipboard
|
||||
const originalClipboard = navigator.clipboard;
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: {
|
||||
writeText: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
(useCloudflareStatus as any).mockReturnValue({
|
||||
data: { isRunning: true, publicUrl: 'https://test.trycloudflare.com' },
|
||||
isLoading: false
|
||||
});
|
||||
|
||||
render(<CloudflareSettings enabled={true} token="test-token" visitorMode={false} onChange={mockOnChange} />);
|
||||
|
||||
const urlElement = screen.getByText('https://test.trycloudflare.com');
|
||||
await user.click(urlElement);
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('https://test.trycloudflare.com');
|
||||
|
||||
// Cleanup
|
||||
if (originalClipboard) {
|
||||
Object.defineProperty(navigator, 'clipboard', { value: originalClipboard });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import axios from 'axios';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import CookieSettings from '../CookieSettings';
|
||||
|
||||
// Mock contexts and hooks
|
||||
vi.mock('../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQuery: vi.fn(),
|
||||
useMutation: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock ConfirmationModal
|
||||
vi.mock('../../ConfirmationModal', () => ({
|
||||
default: ({ isOpen, onConfirm, onClose }: any) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div data-testid="confirmation-modal">
|
||||
<button onClick={onConfirm}>Confirm</button>
|
||||
<button onClick={onClose}>Cancel</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock axios
|
||||
vi.mock('axios');
|
||||
|
||||
describe('CookieSettings', () => {
|
||||
const mockOnSuccess = vi.fn();
|
||||
const mockOnError = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Default useQuery mock
|
||||
(useQuery as any).mockReturnValue({
|
||||
data: { exists: false },
|
||||
refetch: vi.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
// Default useMutation mock
|
||||
(useMutation as any).mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render upload button and not found status initially', () => {
|
||||
render(<CookieSettings onSuccess={mockOnSuccess} onError={mockOnError} />);
|
||||
|
||||
expect(screen.getByText('uploadCookies')).toBeInTheDocument();
|
||||
expect(screen.getByText('cookiesNotFound')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render delete button and found status when cookies exist', () => {
|
||||
(useQuery as any).mockReturnValue({
|
||||
data: { exists: true },
|
||||
refetch: vi.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
render(<CookieSettings onSuccess={mockOnSuccess} onError={mockOnError} />);
|
||||
|
||||
expect(screen.getByText('deleteCookies')).toBeInTheDocument();
|
||||
expect(screen.getByText('cookiesFound')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle file upload', async () => {
|
||||
const user = userEvent.setup();
|
||||
const refetchMock = vi.fn();
|
||||
(useQuery as any).mockReturnValue({
|
||||
data: { exists: false },
|
||||
refetch: refetchMock,
|
||||
isLoading: false,
|
||||
});
|
||||
(axios.post as any).mockResolvedValue({ data: {} });
|
||||
|
||||
render(<CookieSettings onSuccess={mockOnSuccess} onError={mockOnError} />);
|
||||
|
||||
const file = new File(['cookie data'], 'cookies.txt', { type: 'text/plain' });
|
||||
// Use hidden input to upload
|
||||
// Finding the input is tricky as it's hidden.
|
||||
// We can look for the button component="label" which wraps the input.
|
||||
// Or directly select by implicit accessibility if possible, but input type=file is hidden.
|
||||
// testing-library userEvent.upload can attach to input or label.
|
||||
|
||||
// Use container query to find input
|
||||
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
if (fileInput) {
|
||||
await user.upload(fileInput, file);
|
||||
}
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/settings/upload-cookies'),
|
||||
expect.any(FormData),
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSuccess).toHaveBeenCalledWith('cookiesUploadedSuccess');
|
||||
expect(refetchMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject non-txt files', async () => {
|
||||
const { container } = render(<CookieSettings onSuccess={mockOnSuccess} onError={mockOnError} />);
|
||||
|
||||
const file = new File(['data'], 'test.png', { type: 'image/png' });
|
||||
const fileInput = container.querySelector('input[type="file"]');
|
||||
|
||||
if (fileInput) {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||
}
|
||||
|
||||
expect(mockOnError).toHaveBeenCalledWith('onlyTxtFilesAllowed');
|
||||
expect(axios.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle delete cookies', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mutateMock = vi.fn();
|
||||
(useQuery as any).mockReturnValue({
|
||||
data: { exists: true },
|
||||
refetch: vi.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
(useMutation as any).mockReturnValue({
|
||||
mutate: mutateMock,
|
||||
isPending: false,
|
||||
});
|
||||
|
||||
render(<CookieSettings onSuccess={mockOnSuccess} onError={mockOnError} />);
|
||||
|
||||
// Click delete button
|
||||
await user.click(screen.getByText('deleteCookies'));
|
||||
|
||||
// Confirm modal
|
||||
expect(screen.getByTestId('confirmation-modal')).toBeInTheDocument();
|
||||
await user.click(screen.getByText('Confirm'));
|
||||
|
||||
expect(mutateMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import DatabaseSettings from '../DatabaseSettings';
|
||||
|
||||
// Mock language context
|
||||
vi.mock('../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
describe('DatabaseSettings', () => {
|
||||
const defaultProps = {
|
||||
onMigrate: vi.fn(),
|
||||
onDeleteLegacy: vi.fn(),
|
||||
onFormatFilenames: vi.fn(),
|
||||
onExportDatabase: vi.fn(),
|
||||
onImportDatabase: vi.fn(),
|
||||
onCleanupBackupDatabases: vi.fn(),
|
||||
onRestoreFromLastBackup: vi.fn(),
|
||||
isSaving: false,
|
||||
lastBackupInfo: { exists: true, timestamp: '2023-01-01-00-00-00' } as any,
|
||||
moveSubtitlesToVideoFolder: false,
|
||||
onMoveSubtitlesToVideoFolderChange: vi.fn(),
|
||||
moveThumbnailsToVideoFolder: false,
|
||||
onMoveThumbnailsToVideoFolderChange: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render all sections and buttons', () => {
|
||||
render(<DatabaseSettings {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('database')).toBeInTheDocument();
|
||||
expect(screen.getByText('migrateDataButton')).toBeInTheDocument();
|
||||
expect(screen.getByText('formatLegacyFilenamesButton')).toBeInTheDocument();
|
||||
expect(screen.getByText('deleteLegacyDataButton')).toBeInTheDocument();
|
||||
expect(screen.getByText('exportDatabase')).toBeInTheDocument();
|
||||
expect(screen.getByText('importDatabase')).toBeInTheDocument();
|
||||
expect(screen.getByText('restoreFromLastBackup')).toBeInTheDocument();
|
||||
expect(screen.getByText('cleanupBackupDatabases')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onMigrate when clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<DatabaseSettings {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByText('migrateDataButton'));
|
||||
expect(defaultProps.onMigrate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle import flow', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<DatabaseSettings {...defaultProps} />);
|
||||
|
||||
// Open modal
|
||||
await user.click(screen.getByText('importDatabase'));
|
||||
|
||||
// Find modal title
|
||||
expect(screen.getAllByText('importDatabase').length).toBeGreaterThan(1); // Title + Button
|
||||
|
||||
// Mock File Upload
|
||||
const file = new File(['db'], 'test.db', { type: 'application/octet-stream' });
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
if (input) {
|
||||
await user.upload(input, file);
|
||||
}
|
||||
|
||||
// Confirm
|
||||
// The confirm button in dialog is "importDatabase"
|
||||
// We need to target the specific button in dialog actions
|
||||
const buttons = screen.getAllByRole('button', { name: 'importDatabase' });
|
||||
// The one in dialog actions should be the last one usually, or enabled
|
||||
const dialogButton = buttons[buttons.length - 1];
|
||||
await user.click(dialogButton);
|
||||
|
||||
expect(defaultProps.onImportDatabase).toHaveBeenCalledWith(file);
|
||||
});
|
||||
|
||||
it('should call onCleanupBackupDatabases when confirmed', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<DatabaseSettings {...defaultProps} />);
|
||||
|
||||
// Open modal
|
||||
await user.click(screen.getByText('cleanupBackupDatabases'));
|
||||
|
||||
// Confirm
|
||||
const confirmBtn = screen.getAllByRole('button', { name: 'cleanupBackupDatabases' }).pop();
|
||||
if (confirmBtn) await user.click(confirmBtn);
|
||||
|
||||
expect(defaultProps.onCleanupBackupDatabases).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render switches for moving files', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<DatabaseSettings {...defaultProps} />);
|
||||
|
||||
// Check labels
|
||||
expect(screen.getByText('moveSubtitlesToVideoFolder')).toBeInTheDocument();
|
||||
expect(screen.getByText('moveThumbnailsToVideoFolder')).toBeInTheDocument();
|
||||
|
||||
// Toggle switches
|
||||
const subtitleSwitch = screen.getByLabelText(/moveSubtitlesToVideoFolderOff/i);
|
||||
await user.click(subtitleSwitch);
|
||||
expect(defaultProps.onMoveSubtitlesToVideoFolderChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import DownloadSettings from '../DownloadSettings';
|
||||
|
||||
// Mock language context
|
||||
vi.mock('../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
describe('DownloadSettings', () => {
|
||||
const mockOnChange = vi.fn();
|
||||
const mockOnCleanup = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
settings: {
|
||||
maxConcurrentDownloads: 3,
|
||||
} as any,
|
||||
onChange: mockOnChange,
|
||||
activeDownloadsCount: 0,
|
||||
onCleanup: mockOnCleanup,
|
||||
isSaving: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render slider and cleanup button', () => {
|
||||
render(<DownloadSettings {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('downloadSettings')).toBeInTheDocument();
|
||||
expect(screen.getByText('maxConcurrent: 3')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('cleanupTempFiles')[0]).toBeInTheDocument();
|
||||
expect(screen.getByRole('slider')).toHaveValue('3');
|
||||
});
|
||||
|
||||
it('should call onCleanup when button clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<DownloadSettings {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'cleanupTempFiles' }));
|
||||
expect(mockOnCleanup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should disable cleanup button when active downloads exist', () => {
|
||||
render(<DownloadSettings {...defaultProps} activeDownloadsCount={1} />);
|
||||
|
||||
expect(screen.getByText('cleanupTempFilesActiveDownloads')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'cleanupTempFiles' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should change max concurrent downloads via slider', () => {
|
||||
render(<DownloadSettings {...defaultProps} />);
|
||||
|
||||
const slider = screen.getByRole('slider');
|
||||
fireEvent.change(slider, { target: { value: 5 } });
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('maxConcurrentDownloads', 5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import SecuritySettings from '../SecuritySettings';
|
||||
|
||||
// Mock language context
|
||||
vi.mock('../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
describe('SecuritySettings', () => {
|
||||
const mockOnChange = vi.fn();
|
||||
const defaultSettings: any = {
|
||||
loginEnabled: false,
|
||||
password: '',
|
||||
isPasswordSet: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render enable switch', () => {
|
||||
render(<SecuritySettings settings={defaultSettings} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('enableLogin')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('password')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show password field when enabled', () => {
|
||||
render(<SecuritySettings settings={{ ...defaultSettings, loginEnabled: true }} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('password')).toBeInTheDocument();
|
||||
expect(screen.getByText('passwordSetHelper')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle switch change', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SecuritySettings settings={defaultSettings} onChange={mockOnChange} />);
|
||||
|
||||
await user.click(screen.getByLabelText('enableLogin'));
|
||||
expect(mockOnChange).toHaveBeenCalledWith('loginEnabled', true);
|
||||
});
|
||||
|
||||
it('should handle password change', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SecuritySettings settings={{ ...defaultSettings, loginEnabled: true }} onChange={mockOnChange} />);
|
||||
|
||||
const input = screen.getByLabelText('password');
|
||||
await user.type(input, 'secret');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('password', 's');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import TagsSettings from '../TagsSettings';
|
||||
|
||||
// Mock language context
|
||||
vi.mock('../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
describe('TagsSettings', () => {
|
||||
const mockOnTagsChange = vi.fn();
|
||||
const defaultTags = ['React', 'TypeScript'];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render existing tags', () => {
|
||||
render(<TagsSettings tags={defaultTags} onTagsChange={mockOnTagsChange} />);
|
||||
|
||||
expect(screen.getByText('React')).toBeInTheDocument();
|
||||
expect(screen.getByText('TypeScript')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('newTag')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should add new tag via button', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TagsSettings tags={defaultTags} onTagsChange={mockOnTagsChange} />);
|
||||
|
||||
await user.type(screen.getByLabelText('newTag'), 'Vitest');
|
||||
await user.click(screen.getByText('add'));
|
||||
|
||||
expect(mockOnTagsChange).toHaveBeenCalledWith([...defaultTags, 'Vitest']);
|
||||
});
|
||||
|
||||
it('should add new tag via Enter key', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TagsSettings tags={defaultTags} onTagsChange={mockOnTagsChange} />);
|
||||
|
||||
await user.type(screen.getByLabelText('newTag'), 'Jest{Enter}');
|
||||
|
||||
expect(mockOnTagsChange).toHaveBeenCalledWith([...defaultTags, 'Jest']);
|
||||
});
|
||||
|
||||
it('should delete tag', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TagsSettings tags={defaultTags} onTagsChange={mockOnTagsChange} />);
|
||||
|
||||
// Find the delete button within the specific chip
|
||||
// MUI Chip delete icon usually implies userEvent on the chip or delete icon
|
||||
const chip = screen.getByText('React').closest('.MuiChip-root');
|
||||
if (chip) {
|
||||
const deleteIcon = within(chip as HTMLElement).getByTestId('CancelIcon');
|
||||
// Note: in default view_file output, we saw onDelete prop on Chip.
|
||||
// MUI renders CancelIcon by default for onDelete.
|
||||
// We can click the delete icon.
|
||||
await user.click(deleteIcon);
|
||||
}
|
||||
|
||||
expect(mockOnTagsChange).toHaveBeenCalledWith(['TypeScript']);
|
||||
});
|
||||
|
||||
it('should prevent duplicate tags', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TagsSettings tags={defaultTags} onTagsChange={mockOnTagsChange} />);
|
||||
|
||||
await user.type(screen.getByLabelText('newTag'), 'React');
|
||||
await user.click(screen.getByText('add'));
|
||||
|
||||
expect(mockOnTagsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import VideoDefaultSettings from '../VideoDefaultSettings';
|
||||
|
||||
// Mock language context
|
||||
vi.mock('../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
describe('VideoDefaultSettings', () => {
|
||||
const mockOnChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render autoPlay switch', () => {
|
||||
render(<VideoDefaultSettings settings={{ defaultAutoPlay: false } as any} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByText('autoPlay')).toBeInTheDocument();
|
||||
expect(screen.getByRole('switch', { name: 'autoPlay' })).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should toggle autoPlay switch', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<VideoDefaultSettings settings={{ defaultAutoPlay: false } as any} onChange={mockOnChange} />);
|
||||
|
||||
await user.click(screen.getByLabelText('autoPlay'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('defaultAutoPlay', true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import YtDlpSettings from '../YtDlpSettings';
|
||||
|
||||
// Mock language context
|
||||
vi.mock('../../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
describe('YtDlpSettings', () => {
|
||||
const mockOnChange = vi.fn();
|
||||
const mockOnProxyChange = vi.fn();
|
||||
const defaultConfig = '# Default Config';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render initial state', () => {
|
||||
render(
|
||||
<YtDlpSettings
|
||||
config={defaultConfig}
|
||||
proxyOnlyYoutube={false}
|
||||
onChange={mockOnChange}
|
||||
onProxyOnlyYoutubeChange={mockOnProxyChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('ytDlpConfiguration')).toBeInTheDocument();
|
||||
expect(screen.getByText('customize')).toBeInTheDocument();
|
||||
// Textarea should be hidden initially
|
||||
expect(screen.queryByPlaceholderText(/yt-dlp Configuration/)).not.toBeVisible();
|
||||
});
|
||||
|
||||
it('should expand configuration on customize click', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<YtDlpSettings
|
||||
config={defaultConfig}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('customize'));
|
||||
|
||||
expect(screen.getByText('hide')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox')).toBeVisible();
|
||||
expect(screen.getByRole('textbox')).toHaveValue(defaultConfig);
|
||||
});
|
||||
|
||||
it('should handle config changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<YtDlpSettings
|
||||
config={defaultConfig}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('customize'));
|
||||
|
||||
const textarea = screen.getByRole('textbox');
|
||||
await user.clear(textarea);
|
||||
await user.type(textarea, 'New Config');
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('New Config');
|
||||
});
|
||||
|
||||
it('should reset config', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<YtDlpSettings
|
||||
config="Custom Config"
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('customize'));
|
||||
await user.click(screen.getByText('reset'));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(expect.stringContaining('# yt-dlp Configuration File'));
|
||||
});
|
||||
|
||||
it('should toggle proxy only youtube', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<YtDlpSettings
|
||||
config={defaultConfig}
|
||||
proxyOnlyYoutube={false}
|
||||
onChange={mockOnChange}
|
||||
onProxyOnlyYoutubeChange={mockOnProxyChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('customize'));
|
||||
await user.click(screen.getByLabelText('proxyOnlyApplyToYoutube'));
|
||||
|
||||
expect(mockOnProxyChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
82
frontend/src/components/__tests__/SubscribeModal.test.tsx
Normal file
82
frontend/src/components/__tests__/SubscribeModal.test.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import SubscribeModal from '../SubscribeModal';
|
||||
|
||||
// Mock language context
|
||||
vi.mock('../../contexts/LanguageContext', () => ({
|
||||
useLanguage: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
describe('SubscribeModal', () => {
|
||||
const mockOnClose = vi.fn();
|
||||
const mockOnConfirm = vi.fn();
|
||||
const defaultProps = {
|
||||
open: true,
|
||||
onClose: mockOnClose,
|
||||
onConfirm: mockOnConfirm,
|
||||
url: 'http://test.com',
|
||||
authorName: 'Test Author',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render strictly when open', () => {
|
||||
render(<SubscribeModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('subscribeToAuthor')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('checkIntervalMinutes')).toHaveValue(60);
|
||||
expect(screen.getByLabelText('downloadAllPreviousVideos')).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should handle input changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SubscribeModal {...defaultProps} />);
|
||||
|
||||
// Change interval
|
||||
const intervalInput = screen.getByLabelText('checkIntervalMinutes');
|
||||
await user.clear(intervalInput);
|
||||
await user.type(intervalInput, '120');
|
||||
|
||||
// Toggle checkbox
|
||||
await user.click(screen.getByLabelText('downloadAllPreviousVideos'));
|
||||
|
||||
expect(screen.getByLabelText('checkIntervalMinutes')).toHaveValue(120);
|
||||
expect(screen.getByLabelText('downloadAllPreviousVideos')).toBeChecked();
|
||||
// Warning should appear when checkbox is checked
|
||||
expect(screen.getByText('downloadAllPreviousWarning')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onConfirm with values', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SubscribeModal {...defaultProps} />);
|
||||
|
||||
// Defaults: 60, false
|
||||
await user.click(screen.getByText('subscribe'));
|
||||
expect(mockOnConfirm).toHaveBeenCalledWith(60, false);
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onConfirm with updated values', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SubscribeModal {...defaultProps} />);
|
||||
|
||||
const intervalInput = screen.getByLabelText('checkIntervalMinutes');
|
||||
await user.clear(intervalInput);
|
||||
await user.type(intervalInput, '30');
|
||||
await user.click(screen.getByLabelText('downloadAllPreviousVideos'));
|
||||
|
||||
await user.click(screen.getByText('subscribe'));
|
||||
expect(mockOnConfirm).toHaveBeenCalledWith(30, true);
|
||||
});
|
||||
|
||||
it('should call onClose when cancel clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SubscribeModal {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByText('cancel'));
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user