Skip to content

fix(virtual-core): don't let a stale scroll target clobber the prepend anchor - #1248

Open
piecyk wants to merge 1 commit into
TanStack:mainfrom
piecyk:fix/anchor-clobbered-by-stale-scroll-state
Open

fix(virtual-core): don't let a stale scroll target clobber the prepend anchor#1248
piecyk wants to merge 1 commit into
TanStack:mainfrom
piecyk:fix/anchor-clobbered-by-stale-scroll-state

Conversation

@piecyk

@piecyk piecyk commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

With anchorTo: 'end', a chat-style list that mounts, runs scrollToEnd(), and is then scrolled away from could jump the reader several screens on the next prepend instead of holding their position.

🎯 Changes

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Tests
    • Added an end-to-end scenario covering smooth scrolling interrupted by prepending history.
    • Documented a known issue where scrolling may not reach the top after new content is added.
  • New Examples
    • Added a smooth-prepend virtualized list example with stable message positioning while loading earlier items.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 94998fa8-514e-4d43-9260-b017a78fbe44

📥 Commits

Reviewing files that changed from the base of the PR and between 9f9632b and b96bba2.

📒 Files selected for processing (4)
  • packages/react-virtual/e2e/app/smooth-prepend/index.html
  • packages/react-virtual/e2e/app/smooth-prepend/main.tsx
  • packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts
  • packages/react-virtual/e2e/app/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/react-virtual/e2e/app/vite.config.ts
  • packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts
  • packages/react-virtual/e2e/app/smooth-prepend/index.html

📝 Walkthrough

Walkthrough

This change adds a smooth-prepend virtualized list example, registers it in the Vite build, and adds a Playwright regression test for scrolling interrupted by prepended history.

Changes

Smooth prepend end-to-end scenario

Layer / File(s) Summary
Smooth prepend example
packages/react-virtual/e2e/app/smooth-prepend/index.html, packages/react-virtual/e2e/app/smooth-prepend/main.tsx, packages/react-virtual/e2e/app/vite.config.ts
The new app renders 200 virtualized messages, starts at the list end, supports smooth scrolling and prepending five messages, and is added to the Vite inputs.
Smooth prepend regression coverage
packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts
The Playwright test checks the initial bottom position, interrupts smooth scrolling by prepending history, and verifies that scrolling reaches the top. The test records the current behavior as an expected failure.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the fix for stale scroll targets affecting prepend anchoring.
Description check ✅ Passed The description includes the required sections, completed checklist items, release impact, and a clear issue summary.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Jul 31, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit b96bba2

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 57s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 16s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-31 12:59:58 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 31, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-virtual

npm i https://pkg.pr.new/@tanstack/angular-virtual@1248

@tanstack/lit-virtual

npm i https://pkg.pr.new/@tanstack/lit-virtual@1248

@tanstack/marko-virtual

npm i https://pkg.pr.new/@tanstack/marko-virtual@1248

@tanstack/react-virtual

npm i https://pkg.pr.new/@tanstack/react-virtual@1248

@tanstack/solid-virtual

npm i https://pkg.pr.new/@tanstack/solid-virtual@1248

@tanstack/svelte-virtual

npm i https://pkg.pr.new/@tanstack/svelte-virtual@1248

@tanstack/virtual-core

npm i https://pkg.pr.new/@tanstack/virtual-core@1248

@tanstack/vue-virtual

npm i https://pkg.pr.new/@tanstack/vue-virtual@1248

commit: b96bba2

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/virtual-core/src/index.ts`:
- Around line 670-684: The anchorResolved handling must not clear an active
smooth scroll. Update the scroll-state retirement logic near reconcileScroll so
anchor correction only retires stale programmatic targets after the destination
has been reached and the user has moved away, while preserving in-flight
scrollToIndex({ behavior: "smooth" }) state through prepend/setOptions updates;
add regression coverage for prepending during an active smooth scroll.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 28b4746c-99bc-4c31-84d9-5e8c279082c0

📥 Commits

Reviewing files that changed from the base of the PR and between d2cf98b and 4bf3ab9.

📒 Files selected for processing (3)
  • .changeset/lucky-pandas-listen.md
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts

Comment thread packages/virtual-core/src/index.ts Outdated
@piecyk
piecyk force-pushed the fix/anchor-clobbered-by-stale-scroll-state branch from 4bf3ab9 to 709d666 Compare July 31, 2026 10:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/virtual-core/src/index.ts`:
- Around line 1132-1142: Update the reachedTarget re-arm logic in the scroll
target handling block to use the same approximately 1.01 measurement-settling
tolerance as the retirement check, rather than the viewport distance. Re-arm
this.scrollState.reachedTarget whenever the target changes beyond that
tolerance, including changes smaller than a viewport but larger than the
settling threshold; preserve the existing behavior for minor measurement nudges.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ae39a292-8f42-4251-a538-6cd0c1e82b54

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf3ab9 and 709d666.

📒 Files selected for processing (3)
  • .changeset/lucky-pandas-listen.md
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/lucky-pandas-listen.md

Comment thread packages/virtual-core/src/index.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/react-virtual/e2e/app/smooth-prepend/main.tsx`:
- Line 34: Remove the unnecessary non-null assertions from the array accesses in
the getItemKey callback and the corresponding access at the second reported
location, preserving their existing indexing and key behavior.

In `@packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts`:
- Around line 42-45: Remove the top-level test.fail() call before the
smooth-scroll test declaration; leave the test('a prepend mid-flight does not
abandon a smooth scrollToIndex', ...) registration and body unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 512dd6ae-228e-488e-95e4-7b06c3040ef4

📥 Commits

Reviewing files that changed from the base of the PR and between 709d666 and 88c894a.

📒 Files selected for processing (4)
  • packages/react-virtual/e2e/app/smooth-prepend/index.html
  • packages/react-virtual/e2e/app/smooth-prepend/main.tsx
  • packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts
  • packages/react-virtual/e2e/app/vite.config.ts

count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
getItemKey: (index) => messages[index]!.id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unnecessary non-null assertions.

ESLint reports errors at Line 34 and Line 88. Remove ! from both array accesses.

Proposed fix
-    getItemKey: (index) => messages[index]!.id,
+    getItemKey: (index) => messages[index].id,
...
-            const message = messages[item.index]!
+            const message = messages[item.index]

Also applies to: 88-88

🧰 Tools
🪛 ESLint

[error] 34-34: This assertion is unnecessary since it does not change the type of the expression.

(@typescript-eslint/no-unnecessary-type-assertion)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-virtual/e2e/app/smooth-prepend/main.tsx` at line 34, Remove
the unnecessary non-null assertions from the array accesses in the getItemKey
callback and the corresponding access at the second reported location,
preserving their existing indexing and key behavior.

Source: Linters/SAST tools

Comment on lines +42 to +45
test.fail()
test('a prepend mid-flight does not abandon a smooth scrollToIndex', async ({
page,
}) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the installed Playwright version declaration and all test.fail() uses.
fd -a 'package.json|pnpm-lock.yaml' . -x rg -n -C2 '`@playwright/test`|playwright' {}
rg -n -C3 'test\.fail\(' packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts

Repository: TanStack/virtual

Length of output: 3169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file context =="
sed -n '35,55p' packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts

echo
echo =="playwright test types around test.fail (if available)==="
if [ -f node_modules/@playwright/test/types/test.d.ts ]; then
  rg -n -C4 'fail\(' node_modules/@playwright/test/types/test.d.ts
else
  fd 'test.d.ts|index.d.ts' node_modules/@playwright -x sh -c 'echo "-- $1"; rg -n -C4 "fail\\(" "$1"' sh {}
fi

echo
echo "== lock/test package context =="
python3 - <<'PY'
import json
from pathlib import Path
for p in Path('.').glob('packages/react-virtual/e2e/app/package.json'):
    data=json.loads(p.read_text())
    print(p, 'scripts:', data.get('test:e2e'), 'dependencies:', data.get('dependencies'), 'devDependencies:', {k:v for k,v in data.get('devDependencies',{}).items() if 'playwright' in k.lower()})
PY

Repository: TanStack/virtual

Length of output: 1174


🌐 Web query:

Playwright @playwright/test test.fail zero argument test fail runtime annotation only during execution

💡 Result:

In Playwright, calling test.fail without arguments inside a test body is a valid way to mark the current test as expected to fail at runtime [1][2]. When invoked this way, Playwright ensures the test actually fails during its execution [2][3]. If the test unexpectedly passes, Playwright will report an error [3][4]. While test.fail can be called without arguments, official documentation generally recommends declaring failing tests using the arguments-based syntax—such as test.fail(title, body) or test.fail(condition, description)—for better readability and intent [1][2]. If you need to include a description when using the unconditional form, you can use the syntax test.fail(true, 'description') as a workaround [5]. This runtime annotation method is useful for acknowledging known bugs or issues that should be addressed later, as it allows the test suite to remain "green" (passing) even when the specific test is known to fail [1][2].

Citations:


🌐 Web query:

Playwright TestAnnotations fail not called during test execution fail during test execution

💡 Result:

In Playwright Test, annotations like test.fail, test.skip, and test.fixme have distinct execution behaviors [1][2]. Understanding why an annotation might appear "not called" or why unexpected skipping occurs requires identifying which annotation is used and where it is placed [1][2]. Execution Behaviors - test.fail: Playwright executes the test body and expects the test to fail [1][2]. If the test passes instead of failing, Playwright reports an error [1][3]. - test.skip: Playwright does not execute the test at all; it is marked as skipped [1][2]. - test.fixme: Playwright does not execute the test; it is marked as fixme, similar to skip, but specifically intended for tests known to be broken or needing work [1][2]. Why tests may be skipped unexpectedly or annotations appear not to take effect: 1. Test Failure or Uncaught Exceptions: If an error occurs (such as an unawaited promise or an uncaught exception) before the annotation or during test execution, it may cause the test worker to crash or terminate early, leading to subsequent tests being skipped or the test not reporting as expected [4][5]. 2. Placement in Hooks: Annotations placed inside beforeEach or beforeAll hooks affect the tests within those scopes [1][6]. If a failure occurs in a hook, it may prevent the test body from running, causing the annotation logic not to be reached [6]. 3. Execution Context: Annotations like test.slow or test.fail called inside the test body only register once that line of code is executed [2][7]. If a test fails or hangs before that line is reached, the annotation will not be active for that specific test run [7]. 4. Parallelism and Worker Issues: In parallel mode, unhandled rejections or severe errors can kill a worker process, which causes other tests assigned to that worker to be skipped [4][5]. Best Practices - Always await asynchronous operations within your tests [4]. Failure to await promises, particularly when using expect or other Playwright actions, is a common cause of flaky or skipped tests [4]. - Use static annotations in the test declaration if you need them to be available immediately, such as in onTestBegin in custom reporters [7]. - Ensure your test environment configuration (e.g., timeouts) aligns with your intended test behavior [8]. If a test times out before reaching a test.fail call, it may be reported as a timeout failure rather than a successful "expected failure" [8].

Citations:


Remove the unsupported test.fail() annotation.

Call test.fail(...) only inside a test body during execution. This call runs during module evaluation, before Playwright registers the test, so Playwright throws when the spec is loaded.

Proposed fix
-test.fail()
-test('a prepend mid-flight does not abandon a smooth scrollToIndex', async ({
+test.fail('a prepend mid-flight does not abandon a smooth scrollToIndex', async ({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test.fail()
test('a prepend mid-flight does not abandon a smooth scrollToIndex', async ({
page,
}) => {
test.fail('a prepend mid-flight does not abandon a smooth scrollToIndex', async ({
page,
}) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-virtual/e2e/app/test/smooth-prepend.spec.ts` around lines 42 -
45, Remove the top-level test.fail() call before the smooth-scroll test
declaration; leave the test('a prepend mid-flight does not abandon a smooth
scrollToIndex', ...) registration and body unchanged.

@piecyk
piecyk force-pushed the fix/anchor-clobbered-by-stale-scroll-state branch from 9f9632b to b96bba2 Compare July 31, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant