Files
ReadMeABook/tests/app/admin-logs-chips.test.tsx
kikootwo eef6ae3462 Add admin system logs UI and API support
Introduce a complete admin System Logs feature: adds frontend components (filters, date picker, active filter chips, rows, detail panel, skeletons, pagination, toolbar, user typeahead, and styles) under src/app/admin/logs/components, plus hooks (useAutoRefreshControl, useLogsUrlState, useUserSearch) and types. Add constants for job labels and log filters, wire URL-driven filters/search/date-range/hasError/user/audiobookQuery with pause-on-interact behavior and page-size options. Update API route (/api/admin/logs) to support the expanded query params and exported where-builder. Update documentation (TABLEOFCONTENTS and admin-dashboard) and add/adjust tests for the new admin logs UI and API behavior.
2026-05-18 08:29:32 -04:00

141 lines
5.0 KiB
TypeScript

/**
* Component: Admin Logs — ActiveFilterChips Tests
* Documentation: documentation/admin-dashboard.md
*/
// @vitest-environment jsdom
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import ActiveFilterChips from '@/app/admin/logs/components/ActiveFilterChips';
import { DEFAULT_FILTER_STATE, type LogsFilterState } from '@/app/admin/logs/types';
const setFiltersMock = vi.fn();
const removeFilterMock = vi.fn();
const clearAllMock = vi.fn();
let mockFilters: LogsFilterState = { ...DEFAULT_FILTER_STATE };
vi.mock('@/app/admin/logs/hooks/useLogsUrlState', () => ({
useLogsUrlState: () => ({
filters: mockFilters,
setFilters: setFiltersMock,
setSearchInput: vi.fn(),
searchInput: mockFilters.search,
clearAll: clearAllMock,
removeFilter: removeFilterMock,
}),
}));
const findUserByIdMock = vi.fn();
vi.mock('@/app/admin/logs/hooks/useUserSearch', () => ({
useUserSearch: () => ({
users: [],
filterByQuery: vi.fn(),
findUserById: findUserByIdMock,
isLoading: false,
error: null,
}),
}));
describe('ActiveFilterChips', () => {
beforeEach(() => {
setFiltersMock.mockReset();
removeFilterMock.mockReset();
findUserByIdMock.mockReset();
mockFilters = { ...DEFAULT_FILTER_STATE };
});
it('renders nothing when all filters are at default and no search', () => {
const { container } = render(<ActiveFilterChips />);
expect(container.firstChild).toBeNull();
});
it('renders a status chip with the correct aria-label and label', () => {
mockFilters = { ...DEFAULT_FILTER_STATE, status: 'failed' };
render(<ActiveFilterChips />);
const chip = screen.getByRole('button', { name: 'Remove filter: status' });
expect(chip).toHaveTextContent('Status: Failed');
});
it('renders a job-type chip using JOB_TYPE_LABELS for the display label', () => {
mockFilters = { ...DEFAULT_FILTER_STATE, type: 'search_indexers' };
render(<ActiveFilterChips />);
const chip = screen.getByRole('button', { name: 'Remove filter: job type' });
expect(chip).toHaveTextContent('Type: Search Indexers');
});
it('renders an Errors only chip when hasError is true', () => {
mockFilters = { ...DEFAULT_FILTER_STATE, hasError: true };
render(<ActiveFilterChips />);
const chip = screen.getByRole('button', { name: 'Remove filter: errors only' });
expect(chip).toHaveTextContent('Errors only');
});
it('clicking a chip calls removeFilter with the correct key', () => {
mockFilters = { ...DEFAULT_FILTER_STATE, status: 'failed' };
render(<ActiveFilterChips />);
fireEvent.click(screen.getByRole('button', { name: 'Remove filter: status' }));
expect(removeFilterMock).toHaveBeenCalledWith('status');
});
it('clicking the date chip clears both dateFrom and dateTo via setFilters', () => {
mockFilters = {
...DEFAULT_FILTER_STATE,
dateFrom: '2026-05-10T00:00:00.000Z',
dateTo: '2026-05-12T00:00:00.000Z',
};
render(<ActiveFilterChips />);
const chip = screen.getByRole('button', { name: 'Remove filter: date range' });
fireEvent.click(chip);
expect(setFiltersMock).toHaveBeenCalledWith({ dateFrom: null, dateTo: null });
});
it('renders a search chip when search is non-empty', () => {
mockFilters = { ...DEFAULT_FILTER_STATE, search: 'timeout' };
render(<ActiveFilterChips />);
const chip = screen.getByRole('button', { name: 'Remove filter: search' });
expect(chip).toHaveTextContent('Search: "timeout"');
fireEvent.click(chip);
expect(removeFilterMock).toHaveBeenCalledWith('search');
});
it('user chip uses resolved plexUsername when available, falls back to id', () => {
findUserByIdMock.mockReturnValue({ id: 'user-1', plexUsername: 'alice', role: 'user' });
mockFilters = { ...DEFAULT_FILTER_STATE, userId: 'user-1' };
const { unmount } = render(<ActiveFilterChips />);
expect(
screen.getByRole('button', { name: 'Remove filter: user' })
).toHaveTextContent('User: alice');
unmount();
findUserByIdMock.mockReturnValue(undefined);
render(<ActiveFilterChips />);
expect(
screen.getByRole('button', { name: 'Remove filter: user' })
).toHaveTextContent('User: user-1');
});
it('audiobook chip shows the query string', () => {
mockFilters = { ...DEFAULT_FILTER_STATE, audiobookQuery: 'Dune' };
render(<ActiveFilterChips />);
const chip = screen.getByRole('button', { name: 'Remove filter: audiobook' });
expect(chip).toHaveTextContent('Book: "Dune"');
});
it('renders all chips together when multiple filters are active', () => {
mockFilters = {
...DEFAULT_FILTER_STATE,
status: 'failed',
type: 'search_indexers',
hasError: true,
search: 'oops',
audiobookQuery: 'Dune',
};
render(<ActiveFilterChips />);
const group = screen.getByRole('group', { name: 'Active filters' });
// Five chips for five active values.
expect(group.querySelectorAll('button')).toHaveLength(5);
});
});