Files
ReadMeABook/tests/components/requests/RequestCard.test.tsx
kikootwo 4b90b35748 Add Transmission/NZBGet and per-client paths and much more
Extend multi-download-client support to include Transmission and NZBGet and introduce per-client custom download paths. Adds protocol mapping and new client types, Transmission/NZBGet integration services, API CRUD and validation changes, UI components/modal updates and live path previews, and manager routing by protocol. Includes DB migrations (download_path on download_history, interactive_search_access on users), schema updates, and related processor/service fixes and tests to ensure backward compatibility and proper path resolution.
2026-02-09 19:45:43 -05:00

204 lines
5.8 KiB
TypeScript

/**
* Component: Request Card Tests
* Documentation: documentation/frontend/components.md
*/
// @vitest-environment jsdom
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const cancelRequestMock = vi.hoisted(() => vi.fn());
const manualSearchMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/hooks/useRequests', () => ({
useCancelRequest: () => ({ cancelRequest: cancelRequestMock, isLoading: false }),
useManualSearch: () => ({ triggerManualSearch: manualSearchMock, isLoading: false }),
}));
vi.mock('@/components/requests/InteractiveTorrentSearchModal', () => ({
InteractiveTorrentSearchModal: ({
isOpen,
requestId,
}: {
isOpen: boolean;
requestId?: string;
}) => (
<div data-testid="interactive-modal" data-open={String(isOpen)} data-request-id={requestId} />
),
}));
vi.mock('next/image', () => ({
__esModule: true,
default: (props: any) => <img {...props} />,
}));
vi.mock('@/contexts/PreferencesContext', () => ({
usePreferences: () => ({ squareCovers: false, setSquareCovers: vi.fn(), cardSize: 5, setCardSize: vi.fn() }),
}));
vi.mock('@/contexts/AuthContext', () => ({
useAuth: () => ({
user: { id: 'user-1', role: 'user', permissions: { interactiveSearch: true } },
accessToken: 'test-token',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
refreshToken: vi.fn(),
setAuthData: vi.fn(),
}),
}));
const baseRequest = {
id: 'req-1',
status: 'pending',
progress: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
audiobook: {
id: 'book-1',
title: 'Test Book',
author: 'Test Author',
},
};
describe('RequestCard', () => {
beforeEach(() => {
cancelRequestMock.mockReset();
manualSearchMock.mockReset();
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it('shows progress and active indicator for downloads', async () => {
const { RequestCard } = await import('@/components/requests/RequestCard');
render(
<RequestCard
request={{
...baseRequest,
status: 'downloading',
progress: 45,
}}
/>
);
expect(screen.getByText('Downloading')).toBeInTheDocument();
expect(screen.getByText('Active')).toBeInTheDocument();
expect(screen.getByText('45%')).toBeInTheDocument();
});
it('toggles the error message for failed requests', async () => {
const { RequestCard } = await import('@/components/requests/RequestCard');
render(
<RequestCard
request={{
...baseRequest,
status: 'failed',
errorMessage: 'Failure details',
}}
/>
);
fireEvent.click(screen.getByRole('button', { name: 'Show error' }));
expect(await screen.findByText('Failure details')).toBeInTheDocument();
});
it('triggers manual search, interactive search, and cancel actions', async () => {
const { RequestCard } = await import('@/components/requests/RequestCard');
manualSearchMock.mockResolvedValueOnce(undefined);
cancelRequestMock.mockResolvedValueOnce(undefined);
vi.spyOn(window, 'confirm').mockReturnValue(true);
render(<RequestCard request={baseRequest} />);
fireEvent.click(screen.getByRole('button', { name: 'Manual Search' }));
await waitFor(() => {
expect(manualSearchMock).toHaveBeenCalledWith('req-1');
});
fireEvent.click(screen.getByRole('button', { name: 'Interactive Search' }));
expect(screen.getByTestId('interactive-modal')).toHaveAttribute('data-open', 'true');
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
await waitFor(() => {
expect(cancelRequestMock).toHaveBeenCalledWith('req-1');
});
});
it('shows setup indicator when progress is zero', async () => {
const { RequestCard } = await import('@/components/requests/RequestCard');
render(
<RequestCard
request={{
...baseRequest,
status: 'processing',
progress: 0,
}}
/>
);
expect(screen.getByText('Setting up...')).toBeInTheDocument();
});
it('hides action buttons when showActions is false', async () => {
const { RequestCard } = await import('@/components/requests/RequestCard');
render(<RequestCard request={baseRequest} showActions={false} />);
expect(screen.queryByRole('button', { name: 'Manual Search' })).toBeNull();
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
});
it('alerts when manual search fails', async () => {
const { RequestCard } = await import('@/components/requests/RequestCard');
manualSearchMock.mockRejectedValueOnce(new Error('Search failed'));
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
render(<RequestCard request={baseRequest} />);
fireEvent.click(screen.getByRole('button', { name: 'Manual Search' }));
await waitFor(() => {
expect(alertSpy).toHaveBeenCalledWith('Search failed');
});
});
it('does not cancel when confirmation is declined', async () => {
const { RequestCard } = await import('@/components/requests/RequestCard');
vi.spyOn(window, 'confirm').mockReturnValue(false);
render(<RequestCard request={baseRequest} />);
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
await waitFor(() => {
expect(cancelRequestMock).not.toHaveBeenCalled();
});
});
it('shows completed timestamp when available', async () => {
const { RequestCard } = await import('@/components/requests/RequestCard');
render(
<RequestCard
request={{
...baseRequest,
completedAt: new Date('2024-01-01T00:00:00Z').toISOString(),
}}
/>
);
expect(screen.getByText(/Completed/)).toBeInTheDocument();
});
});