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