From fc8dec3417e2fbcf8b0b3e5f279a3a2e286ba13a Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Sat, 25 Jul 2026 21:02:53 -0700 Subject: [PATCH] Fix busted tests --- frontend/src/components/MessageList.tsx | 11 ++++++++++ .../e2e/specs/channel-info-and-search.spec.ts | 6 ++++-- tests/e2e/specs/pagination.spec.ts | 21 ++++++++++++------- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/MessageList.tsx b/frontend/src/components/MessageList.tsx index 59a7733..a1b2338 100644 --- a/frontend/src/components/MessageList.tsx +++ b/frontend/src/components/MessageList.tsx @@ -602,11 +602,22 @@ export function MessageList({ let frames = 0; let raf = 0; + let lastAppliedTop: number | null = null; const step = () => { const list = listRef.current; if (!pendingBottomScrollRef.current || !list) return; + // Something other than us moved the scroll (a programmatic scrollTop, a + // restored position, assistive tech). Gesture handlers only catch a human + // at the wheel, so also bail when the position we last set has been moved + // upward — the pin must never fight another writer. + if (lastAppliedTop !== null && list.scrollTop < lastAppliedTop - 1) { + pendingBottomScrollRef.current = false; + return; + } + scrollToIndex(sortedMessages.length - 1, 'end'); + lastAppliedTop = list.scrollTop; const atBottom = list.scrollHeight - list.scrollTop - list.clientHeight <= 1; if (atBottom || (frames += 1) >= BOTTOM_SCROLL_FRAME_BUDGET) { diff --git a/tests/e2e/specs/channel-info-and-search.spec.ts b/tests/e2e/specs/channel-info-and-search.spec.ts index 36c98fc..3d3a9ad 100644 --- a/tests/e2e/specs/channel-info-and-search.spec.ts +++ b/tests/e2e/specs/channel-info-and-search.spec.ts @@ -37,8 +37,10 @@ test.describe('Channel info pane', () => { await page.goto(`/#channel/${channelKey}/flightless`); await expect(page.getByRole('status', { name: 'Radio OK' })).toBeVisible(); - // Wait for messages to load - await expect(page.getByText('seed-0')).toBeVisible({ timeout: 15_000 }); + // Wait for messages to load. Assert on the newest seeded message, not the + // oldest: the list is virtualized, so it opens at the bottom and only the + // visible window is in the DOM — seed-0 is 30 rows up and legitimately absent. + await expect(page.getByText(`seed-${SEED_COUNT - 1}`)).toBeVisible({ timeout: 15_000 }); // Click the channel name in the header to open info pane const headerTitle = page.locator('h2').filter({ hasText: '#flightless' }); diff --git a/tests/e2e/specs/pagination.spec.ts b/tests/e2e/specs/pagination.spec.ts index fb6cf69..5ac010a 100644 --- a/tests/e2e/specs/pagination.spec.ts +++ b/tests/e2e/specs/pagination.spec.ts @@ -25,13 +25,20 @@ test.describe('Message pagination ordering/dedup', () => { const list = page.locator('div.h-full.overflow-y-auto').first(); - // Scroll to top to trigger older fetch - await list.evaluate((el) => { - el.scrollTop = 0; - }); - - // Wait for oldest message to appear after pagination - await expect(page.getByText('seed-0')).toBeVisible({ timeout: 15_000 }); + // Scroll to top repeatedly until the oldest message renders. + // + // One scroll is not enough with a virtualized list: reaching the top fetches + // the older page, and the list then anchors to the message that was on top so + // the reader keeps their place, which leaves seed-0 fifty rows further up and + // unmounted. A reader scrolls again to get there; so does this. (Before + // virtualization every loaded row was in the DOM, so a single scroll made + // seed-0 findable regardless of where the viewport actually was.) + await expect(async () => { + await list.evaluate((el) => { + el.scrollTop = 0; + }); + await expect(page.getByText('seed-0')).toBeVisible({ timeout: 1_000 }); + }).toPass({ timeout: 20_000 }); // Spot-check ordering: seed-249 appears above seed-200; seed-50 above seed-10 // Fetch from API to validate ordering and dedup