Files
ReadMeABook/tests/app/admin-jobs.page.test.tsx
T
kikootwo a97979358f Implement file hash-based library matching and remove fuzzy ASIN matching
Adds file hash-based matching for Audiobookshelf library items to ensure 100% accurate ASIN assignment for RMAB-organized content. Removes fuzzy matching from library availability checks, making all matching ASIN-only to eliminate false positives and race conditions. Updates database schema, processors, and matcher utilities; adds new tests and documentation for the new matching strategy. Removes obsolete scripts, Dockerfile, and related tests; updates docker-compose for test environments.
2026-01-28 11:42:00 -05:00

112 lines
3.3 KiB
TypeScript

/**
* Component: Admin Jobs Page Tests
* Documentation: documentation/backend/services/scheduler.md
*/
// @vitest-environment jsdom
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import AdminJobsPage from '@/app/admin/jobs/page';
const authenticatedFetcherMock = vi.hoisted(() => vi.fn());
const fetchJSONMock = vi.hoisted(() => vi.fn());
const toastMock = vi.hoisted(() => ({
success: vi.fn(),
error: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
}));
vi.mock('@/lib/utils/api', () => ({
authenticatedFetcher: authenticatedFetcherMock,
fetchJSON: fetchJSONMock,
}));
vi.mock('@/components/ui/Toast', () => ({
ToastProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
useToast: () => toastMock,
}));
describe('AdminJobsPage', () => {
beforeEach(() => {
authenticatedFetcherMock.mockReset();
fetchJSONMock.mockReset();
toastMock.success.mockReset();
toastMock.error.mockReset();
});
it('renders scheduled jobs and allows manual trigger', async () => {
authenticatedFetcherMock.mockResolvedValue({
jobs: [
{
id: 'job-1',
name: 'Library Scan',
type: 'scan_plex',
schedule: '0 * * * *',
enabled: true,
lastRun: null,
nextRun: null,
},
],
});
fetchJSONMock.mockResolvedValue({ success: true });
render(<AdminJobsPage />);
expect(await screen.findByText('Library Scan')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Trigger Now/i }));
fireEvent.click(screen.getByRole('button', { name: 'Trigger Job' }));
await waitFor(() => {
expect(fetchJSONMock).toHaveBeenCalledWith('/api/admin/jobs/job-1/trigger', {
method: 'POST',
});
expect(toastMock.success).toHaveBeenCalledWith('Job "Library Scan" triggered successfully');
});
expect(authenticatedFetcherMock.mock.calls.length).toBeGreaterThanOrEqual(2);
});
it('updates a job schedule using preset selection', async () => {
authenticatedFetcherMock.mockResolvedValue({
jobs: [
{
id: 'job-2',
name: 'Audible Refresh',
type: 'audible_refresh',
schedule: '0 * * * *',
enabled: true,
lastRun: null,
nextRun: null,
},
],
});
fetchJSONMock.mockResolvedValue({ success: true });
render(<AdminJobsPage />);
fireEvent.click(await screen.findByRole('button', { name: 'Edit' }));
fireEvent.click(screen.getByRole('radio', { name: /Every 2 hours/i }));
fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }));
await waitFor(() => {
expect(fetchJSONMock).toHaveBeenCalledWith('/api/admin/jobs/job-2', {
method: 'PUT',
body: JSON.stringify({ schedule: '0 */2 * * *', enabled: true }),
});
expect(toastMock.success).toHaveBeenCalledWith('Job "Audible Refresh" updated successfully');
});
});
it('shows an error when jobs fail to load', async () => {
authenticatedFetcherMock.mockRejectedValue(new Error('boom'));
render(<AdminJobsPage />);
expect(await screen.findByText('Failed to load scheduled jobs')).toBeInTheDocument();
});
});