test(SubscriptionModal): Add subscription modal tests

This commit is contained in:
Peifan Li
2025-12-28 20:31:18 -05:00
parent 0acbcb7b42
commit aaa5a46e8a
10 changed files with 910 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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();
});
});