Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion dashboard/src/components/EventList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
54 changes: 53 additions & 1 deletion dashboard/src/store/eventStore.test.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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(<EventList events={events} />);
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(<EventList events={filtered} />);
});

// 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(
<div style={{ height: 600, width: 800 }}>
<EventFiltersBar />
<EventListPanel />
</div>
);

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');
});
});
Loading