import React from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { EntityFilters } from '../src/renderer/components/entity/entity-filters'; import { waitFor } from '@testing-library/react'; /** * Mock debounced hook for search input */ jest.mock('../src/renderer/hooks/use-debounced-value', () => ({ useDebounce: jest.fn((value: string) => value), })); const mockFilters = { search: '', entityType: '', dateRange: { start: null, end: null }, sessionId: '', }; const mockEntityTypes = ['topic', 'token', 'account', 'schedule', 'contract']; const defaultProps = { filters: mockFilters, onFiltersChange: jest.fn(), entityTypes: mockEntityTypes, onClearFilters: jest.fn(), totalEntities: 150, filteredEntities: 25, }; describe('EntityFilters', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should render all filter components', async () => { const user = userEvent.setup(); render(); expect(screen.getByPlaceholderText('Search entities...')).toBeInTheDocument(); expect(screen.getByLabelText('Entity Type')).toBeInTheDocument(); expect(screen.getByText('Clear Filters')).toBeInTheDocument(); const advancedButton = screen.getByLabelText('Advanced filters'); await user.click(advancedButton); expect(screen.getByLabelText('Start Date')).toBeInTheDocument(); expect(screen.getByLabelText('End Date')).toBeInTheDocument(); }); it('should display entity count information', () => { render(); expect(screen.getByText('Showing 25 of 150 entities')).toBeInTheDocument(); }); it('should handle search input changes', async () => { const user = userEvent.setup(); const onFiltersChange = jest.fn(); render(); const searchInput = screen.getByPlaceholderText('Search entities...'); await user.type(searchInput, 'test'); await waitFor(() => { expect(onFiltersChange).toHaveBeenCalledWith({ ...mockFilters, search: 'test', }); }); }); it('should debounce search input to prevent excessive API calls', async () => { const { useDebounce } = jest.requireActual('../src/renderer/hooks/use-debounced-value') as { useDebounce: jest.Mock }; useDebounce.mockReturnValue('debounced-value'); const user = userEvent.setup(); const onFiltersChange = jest.fn(); render(); const searchInput = screen.getByPlaceholderText('Search entities...'); await user.type(searchInput, 'test'); expect(useDebounce).toHaveBeenCalledWith('test', 300); }); it('should handle entity type filter changes', async () => { const user = userEvent.setup(); const onFiltersChange = jest.fn(); render(); const typeSelect = screen.getByLabelText('Entity Type'); await user.selectOptions(typeSelect, 'topic'); expect(onFiltersChange).toHaveBeenCalledWith({ ...mockFilters, entityType: 'topic', }); }); it('should render all available entity types in dropdown', () => { render(); const _typeSelect = screen.getByLabelText('Entity Type'); expect(screen.getByRole('option', { name: 'All Types' })).toBeInTheDocument(); expect(screen.getByRole('option', { name: 'topic' })).toBeInTheDocument(); expect(screen.getByRole('option', { name: 'token' })).toBeInTheDocument(); expect(screen.getByRole('option', { name: 'account' })).toBeInTheDocument(); expect(screen.getByRole('option', { name: 'schedule' })).toBeInTheDocument(); expect(screen.getByRole('option', { name: 'contract' })).toBeInTheDocument(); }); it('should handle date range filter changes', async () => { const user = userEvent.setup(); const onFiltersChange = jest.fn(); render(); const advancedButton = screen.getByLabelText('Advanced filters'); await user.click(advancedButton); const startDateInput = screen.getByLabelText('Start Date'); const endDateInput = screen.getByLabelText('End Date'); await user.type(startDateInput, '2023-01-01'); await user.type(endDateInput, '2023-12-31'); expect(onFiltersChange).toHaveBeenCalledWith({ ...mockFilters, dateRange: { start: new Date('2023-01-01'), end: new Date('2023-12-31') }, }); }); it('should handle clear filters action', async () => { const user = userEvent.setup(); const onClearFilters = jest.fn(); const activeFilters = { search: 'test', entityType: 'topic', dateRange: { start: new Date('2023-01-01'), end: new Date('2023-12-31') }, sessionId: 'session-1', }; render(); const clearButton = screen.getByText('Clear Filters'); await user.click(clearButton); expect(onClearFilters).toHaveBeenCalled(); }); it('should show filter badges when filters are active', () => { const activeFilters = { search: 'test', entityType: 'topic', dateRange: { start: new Date('2023-01-01'), end: new Date('2023-12-31') }, sessionId: 'session-1', }; render(); expect(screen.getByText('Search: test')).toBeInTheDocument(); expect(screen.getByText('Type: topic')).toBeInTheDocument(); expect(screen.getByText('Date Range: 2023-01-01 to 2023-12-31')).toBeInTheDocument(); expect(screen.getByText('Session: session-1')).toBeInTheDocument(); }); it('should allow removing individual filter badges', async () => { const user = userEvent.setup(); const onFiltersChange = jest.fn(); const activeFilters = { search: 'test', entityType: 'topic', dateRange: { start: null, end: null }, sessionId: '', }; render(); const searchBadge = screen.getByText('Search: test'); const removeSearchButton = searchBadge.parentElement?.querySelector('[aria-label="Remove search filter"]'); if (removeSearchButton) { await user.click(removeSearchButton); expect(onFiltersChange).toHaveBeenCalledWith({ ...activeFilters, search: '', }); } }); it('should validate date range inputs', async () => { const user = userEvent.setup(); const onFiltersChange = jest.fn(); render(); const startDateInput = screen.getByLabelText('Start Date'); const endDateInput = screen.getByLabelText('End Date'); await user.type(startDateInput, '2023-12-31'); await user.type(endDateInput, '2023-01-01'); expect(screen.getByText('End date must be after start date')).toBeInTheDocument(); expect(onFiltersChange).not.toHaveBeenCalled(); }); it('should support preset filter combinations', async () => { const user = userEvent.setup(); const onFiltersChange = jest.fn(); render(); const presetButton = screen.getByText('Last 30 Days'); await user.click(presetButton); const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); expect(onFiltersChange).toHaveBeenCalledWith({ ...mockFilters, dateRange: { start: expect.any(Date), end: expect.any(Date) }, }); }); it('should display saved filter presets', () => { render(); expect(screen.getByText('Today')).toBeInTheDocument(); expect(screen.getByText('Last 7 Days')).toBeInTheDocument(); expect(screen.getByText('Last 30 Days')).toBeInTheDocument(); expect(screen.getByText('This Month')).toBeInTheDocument(); }); it('should handle session filter when multiple sessions exist', async () => { const user = userEvent.setup(); const onFiltersChange = jest.fn(); const sessionIds = ['session-1', 'session-2', 'session-3']; render(); const sessionSelect = screen.getByLabelText('Session'); await user.selectOptions(sessionSelect, 'session-2'); expect(onFiltersChange).toHaveBeenCalledWith({ ...mockFilters, sessionId: 'session-2', }); }); it('should hide session filter when only one session exists', () => { const singleSession = ['session-1']; render(); expect(screen.queryByLabelText('Session')).not.toBeInTheDocument(); }); it('should support keyboard navigation through filters', async () => { const user = userEvent.setup(); render(); const searchInput = screen.getByPlaceholderText('Search entities...'); searchInput.focus(); await user.keyboard('{Tab}'); expect(screen.getByLabelText('Entity Type')).toHaveFocus(); await user.keyboard('{Tab}'); expect(screen.getByLabelText('Start Date')).toHaveFocus(); await user.keyboard('{Tab}'); expect(screen.getByLabelText('End Date')).toHaveFocus(); }); it('should display filter results count dynamically', () => { const { rerender } = render(); expect(screen.getByText('Showing 25 of 150 entities')).toBeInTheDocument(); rerender(); expect(screen.getByText('Showing 5 of 150 entities')).toBeInTheDocument(); }); it('should handle empty search gracefully', async () => { const user = userEvent.setup(); const onFiltersChange = jest.fn(); render(); const searchInput = screen.getByPlaceholderText('Search entities...'); await user.type(searchInput, 'test'); await user.clear(searchInput); await waitFor(() => { expect(onFiltersChange).toHaveBeenLastCalledWith({ ...mockFilters, search: '', }); }); }); it('should preserve filter state during component updates', () => { const activeFilters = { search: 'test query', entityType: 'token', dateRange: { start: new Date('2023-01-01'), end: new Date('2023-12-31') }, sessionId: 'session-1', }; const { rerender } = render(); rerender(); expect(screen.getByDisplayValue('test query')).toBeInTheDocument(); expect(screen.getByDisplayValue('token')).toBeInTheDocument(); }); it('should support advanced search syntax', async () => { const user = userEvent.setup(); const onFiltersChange = jest.fn(); render(); const searchInput = screen.getByPlaceholderText('Search entities...'); await user.type(searchInput, 'type:topic name:*test*'); await waitFor(() => { expect(onFiltersChange).toHaveBeenCalledWith({ ...mockFilters, search: 'type:topic name:*test*', }); }); }); it('should display search suggestions when available', async () => { const user = userEvent.setup(); render(); const searchInput = screen.getByPlaceholderText('Search entities...'); await user.type(searchInput, 'test'); expect(screen.getByText('Search in names')).toBeInTheDocument(); expect(screen.getByText('Search in IDs')).toBeInTheDocument(); expect(screen.getByText('Search in transaction IDs')).toBeInTheDocument(); }); it('should handle filter persistence across sessions', () => { const mockLocalStorage = { getItem: jest.fn().mockReturnValue('{"entityType": "topic", "search": "saved query"}'), setItem: jest.fn(), }; Object.defineProperty(window, 'localStorage', { value: mockLocalStorage }); render(); expect(mockLocalStorage.getItem).toHaveBeenCalledWith('entityManagerFilters'); }); });