From 01b8bcf8e08443f265000dedac86929fdd264411 Mon Sep 17 00:00:00 2001 From: SathvikaSingoti Date: Tue, 7 Jul 2026 02:02:28 +0530 Subject: [PATCH] fix(frontend): remove exhaustive-deps suppression from Findings virtualizer effect --- frontend/src/pages/Findings.tsx | 26 +++++++++++++----- frontend/testing/unit/pages/Findings.test.tsx | 27 +++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/Findings.tsx b/frontend/src/pages/Findings.tsx index ca6fe1985..dceb1248c 100644 --- a/frontend/src/pages/Findings.tsx +++ b/frontend/src/pages/Findings.tsx @@ -639,16 +639,30 @@ export default function Findings() { overscan: 6, }) - // Scroll selected finding into view when it changes + // Keep latest virtualRows/virtualizer available without making them + // reactive dependencies — they change on every filter/sort, but we only + // want to re-scroll when the *selection* actually changes. + const virtualRowsRef = useRef(virtualRows) useEffect(() => { - if (!selectedFinding) return - const rowIdx = virtualRows.findIndex( - (row) => row.kind === 'finding' && row.finding.id === selectedFinding.id, + virtualRowsRef.current = virtualRows + }) + + const virtualizerRef = useRef(virtualizer) + useEffect(() => { + virtualizerRef.current = virtualizer + }) + + // Scroll selected finding into view when the selection changes + useEffect(() => { + if (!selectedFindingId) return + const rows = virtualRowsRef.current + const rowIdx = rows.findIndex( + (row) => row.kind === 'finding' && row.finding.id === selectedFindingId, ) if (rowIdx !== -1) { - virtualizer.scrollToIndex(rowIdx, { align: 'auto', behavior: 'smooth' }) + virtualizerRef.current.scrollToIndex(rowIdx, { align: 'auto', behavior: 'smooth' }) } - }, [selectedFindingId]) // eslint-disable-line react-hooks/exhaustive-deps + }, [selectedFindingId]) return (
diff --git a/frontend/testing/unit/pages/Findings.test.tsx b/frontend/testing/unit/pages/Findings.test.tsx index e797bf45d..2cd885950 100644 --- a/frontend/testing/unit/pages/Findings.test.tsx +++ b/frontend/testing/unit/pages/Findings.test.tsx @@ -535,3 +535,30 @@ describe('Findings — virtualizer scrolling', () => { expect(mockScrollToIndex).not.toHaveBeenCalled() }) }) + +it('scrolls to the correct fresh index after sort order changes then selection changes', async () => { + const findings = [ + makeFinding({ id: 'f1', title: 'Finding Alpha', severity: 'critical', discovered_at: '2024-01-01T00:00:00Z' }), + makeFinding({ id: 'f2', title: 'Finding Beta', severity: 'high', discovered_at: '2024-01-03T00:00:00Z' }), + makeFinding({ id: 'f3', title: 'Finding Gamma', severity: 'medium', discovered_at: '2024-01-02T00:00:00Z' }), + ] + vi.mocked(getFindings).mockResolvedValue({ findings }) + + render() + await waitFor(() => expect(screen.queryByText('Synchronizing findings feed...')).not.toBeInTheDocument()) + + // Switch to "newest" sort — new order is Beta(0), Gamma(1), Alpha(2) + const selects = screen.getAllByRole('combobox') + const sortSelect = selects.find((s) => + Array.from(s.querySelectorAll('option')).some((o) => /Newest First/i.test(o.textContent || '')), + ) + await userEvent.selectOptions(sortSelect!, 'newest') + + mockScrollToIndex.mockClear() + + // Now select Gamma — should scroll to its *post-sort* index (1), not a stale pre-sort index + const gammaOption = await screen.findByRole('option', { name: /Finding Gamma/i }) + await userEvent.click(gammaOption) + + expect(mockScrollToIndex).toHaveBeenCalledWith(1, { align: 'auto', behavior: 'smooth' }) + })