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