diff --git a/dashboard/src/components/EventList.tsx b/dashboard/src/components/EventList.tsx index 989112c..afb2d2a 100644 --- a/dashboard/src/components/EventList.tsx +++ b/dashboard/src/components/EventList.tsx @@ -42,7 +42,9 @@ export const EventList = memo(function EventList({ events }: EventListProps) { const windowState = useMemo(() => { const visibleCount = Math.ceil(viewportHeight / ROW_HEIGHT) + OVERSCAN; - const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN); + const maxScrollTop = Math.max(0, events.length * ROW_HEIGHT - viewportHeight); + const clampedScrollTop = Math.min(scrollTop, maxScrollTop); + const startIndex = Math.max(0, Math.floor(clampedScrollTop / ROW_HEIGHT) - OVERSCAN); const endIndex = Math.min(events.length, startIndex + visibleCount + OVERSCAN); return { diff --git a/dashboard/src/store/eventStore.test.tsx b/dashboard/src/store/eventStore.test.tsx index f9dc377..5f51174 100644 --- a/dashboard/src/store/eventStore.test.tsx +++ b/dashboard/src/store/eventStore.test.tsx @@ -1,8 +1,9 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it } from '@jest/globals'; import { EventFiltersBar } from '../components/EventFiltersBar'; import { EventListPanel } from '../components/EventListPanel'; +import { EventList } from '../components/EventList'; import { useEventStore } from '../store/eventStore'; import { generateMockEvents } from '../utils/eventData'; @@ -32,3 +33,54 @@ describe('event store selective subscriptions', () => { expect(filteredRows[0].textContent).toContain('TaskCreated'); }); }); + +describe('pagination + filter interaction', () => { + it('applying a filter while scrolled does not blank the list', () => { + const events = generateMockEvents(200); + useEventStore.setState({ + events, + filters: { search: '', contractAddress: 'all', eventType: 'all' }, + isLoading: false, + error: null, + }); + + // Render just the list with a large scroll offset to simulate being deep in the list + const { rerender } = render(); + expect(screen.getAllByRole('listitem').length).toBeGreaterThan(0); + + // Now simulate a filter being applied — pass a small filtered result + // while the internal scrollTop state is still set to a large value + const filtered = events.filter((e) => e.eventName === 'TaskCreated'); + act(() => { + rerender(); + }); + + // The list must still render rows — not go blank + expect(screen.getAllByRole('listitem').length).toBeGreaterThan(0); + }); + + it('filter change resets scroll position to top', async () => { + useEventStore.setState({ + events: generateMockEvents(100), + filters: { search: '', contractAddress: 'all', eventType: 'all' }, + isLoading: false, + error: null, + }); + + render( +
+ + +
+ ); + + const before = screen.getAllByRole('article').length; + const searchInput = screen.getByLabelText('Search'); + await userEvent.type(searchInput, 'Withdrawal'); + + const after = screen.getAllByRole('article'); + expect(after.length).toBeGreaterThan(0); + expect(after.length).toBeLessThan(before); + expect(after[0].textContent).toContain('Withdrawal'); + }); +});