diff --git a/packages/react-virtual/e2e/app/smooth-scroll/main.tsx b/packages/react-virtual/e2e/app/smooth-scroll/main.tsx index 565d48c36..a23d4980e 100644 --- a/packages/react-virtual/e2e/app/smooth-scroll/main.tsx +++ b/packages/react-virtual/e2e/app/smooth-scroll/main.tsx @@ -27,6 +27,10 @@ const App = () => { getScrollElement: () => parentRef.current, estimateSize: () => 50, }) + // Test hook: lets the spec wait on the virtualizer's own settlement signal + // (reconcileScroll retiring `scrollState`) instead of guessing from + // scrollTop samples. + ;(window as any).__virtualizer = rowVirtualizer return (
diff --git a/packages/react-virtual/e2e/app/test/smooth-scroll.spec.ts b/packages/react-virtual/e2e/app/test/smooth-scroll.spec.ts index d8650db91..a893dc958 100644 --- a/packages/react-virtual/e2e/app/test/smooth-scroll.spec.ts +++ b/packages/react-virtual/e2e/app/test/smooth-scroll.spec.ts @@ -1,13 +1,34 @@ import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' + +// A smooth scroll's duration scales with distance (index 1000 is ~50,000px), +// and reconcileScroll may re-drive it as rows measure, so neither a fixed wait +// nor a pair of equal scrollTop samples proves completion: the "index 1000" +// case flaked on a fixed 2s wait, and two samples can straddle the pause +// between two re-drives. The virtualizer has the real signal: reconcileScroll +// retires `scrollState` only once the target is stable and reached. Wait for +// the target row to render, then for that retirement. +async function waitForSmoothScroll(page: Page, testId: string) { + await expect(page.locator(`[data-testid="${testId}"]`)).toBeVisible({ + timeout: 15_000, + }) + await expect + .poll( + () => + page.evaluate(() => { + const v = (window as any).__virtualizer + return v.scrollState === null && v.isScrolling === false + }), + { timeout: 15_000, intervals: [50] }, + ) + .toBe(true) +} test('smooth scrolls to index 1000', async ({ page }) => { await page.goto('/smooth-scroll/') await page.click('#scroll-to-1000') - // Smooth scroll animation is 500ms + reconciliation time - await page.waitForTimeout(2000) - - await expect(page.locator('[data-testid="item-1000"]')).toBeVisible() + await waitForSmoothScroll(page, 'item-1000') const delta = await page.evaluate(() => { const item = document.querySelector('[data-testid="item-1000"]') @@ -29,9 +50,7 @@ test('smooth scrolls to index 100', async ({ page }) => { await page.goto('/smooth-scroll/') await page.click('#scroll-to-100') - await page.waitForTimeout(2000) - - await expect(page.locator('[data-testid="item-100"]')).toBeVisible() + await waitForSmoothScroll(page, 'item-100') }) test('smooth scrolls to index 0 after scrolling away', async ({ page }) => { @@ -39,14 +58,11 @@ test('smooth scrolls to index 0 after scrolling away', async ({ page }) => { // First scroll down await page.click('#scroll-to-500') - await page.waitForTimeout(2000) - await expect(page.locator('[data-testid="item-500"]')).toBeVisible() + await waitForSmoothScroll(page, 'item-500') // Then smooth scroll back to top await page.click('#scroll-to-0') - await page.waitForTimeout(2000) - - await expect(page.locator('[data-testid="item-0"]')).toBeVisible() + await waitForSmoothScroll(page, 'item-0') const scrollTop = await page.evaluate(() => { const container = document.querySelector('#scroll-container') @@ -59,9 +75,7 @@ test('smooth scrolls to index 500 with start alignment', async ({ page }) => { await page.goto('/smooth-scroll/') await page.click('#scroll-to-500-start') - await page.waitForTimeout(2000) - - await expect(page.locator('[data-testid="item-500"]')).toBeVisible() + await waitForSmoothScroll(page, 'item-500') const delta = await page.evaluate( ([idx, align]) => { @@ -84,9 +98,7 @@ test('smooth scrolls to index 500 with center alignment', async ({ page }) => { await page.goto('/smooth-scroll/') await page.click('#scroll-to-500-center') - await page.waitForTimeout(2000) - - await expect(page.locator('[data-testid="item-500"]')).toBeVisible() + await waitForSmoothScroll(page, 'item-500') const delta = await page.evaluate( ([idx]) => { @@ -110,18 +122,15 @@ test('smooth scrolls sequentially to multiple targets', async ({ page }) => { // Scroll to 100 first await page.click('#scroll-to-100') - await page.waitForTimeout(2000) - await expect(page.locator('[data-testid="item-100"]')).toBeVisible() + await waitForSmoothScroll(page, 'item-100') // Then scroll to 500 await page.click('#scroll-to-500') - await page.waitForTimeout(2000) - await expect(page.locator('[data-testid="item-500"]')).toBeVisible() + await waitForSmoothScroll(page, 'item-500') // Then scroll to 1000 await page.click('#scroll-to-1000') - await page.waitForTimeout(2000) - await expect(page.locator('[data-testid="item-1000"]')).toBeVisible() + await waitForSmoothScroll(page, 'item-1000') }) test('interrupting smooth scroll with another smooth scroll', async ({ @@ -135,9 +144,5 @@ test('interrupting smooth scroll with another smooth scroll', async ({ await page.waitForTimeout(200) await page.click('#scroll-to-100') - // Wait for the second scroll to complete - await page.waitForTimeout(2000) - - // Should have ended at 100, not 1000 - await expect(page.locator('[data-testid="item-100"]')).toBeVisible() + await waitForSmoothScroll(page, 'item-100') })