[Blazor] Re-derive Virtualize at-bottom state when the anchor mode changes - #68748
Open
lewing wants to merge 2 commits into
Open
[Blazor] Re-derive Virtualize at-bottom state when the anchor mode changes#68748lewing wants to merge 2 commits into
lewing wants to merge 2 commits into
Conversation
…anges `setAnchorMode` refreshed `bottomTracking.following` and `bottomTracking.reached` but left `wasAtBottomLastRender` untouched. `isViewportAtBottom()` reports true for any container that is not yet scrollable, so an empty first render or an async ItemsProvider round trip latches that flag, and it is otherwise only cleared by a user scroll. Switching into End mode afterwards satisfied the re-pin guard in `refreshObservedElements` and jumped the viewport to the bottom even though the user was parked at the top. Re-derive all three flags from the live viewport, mirroring what the user-scroll path already does. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 50118332-337f-40e9-ae34-1a373d3e8550
#68731 quarantined this test while the fix was in review. The anchor-mode latch it was tripping over is fixed in this PR, so re-enable it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 50118332-337f-40e9-ae34-1a373d3e8550
lewing
force-pushed
the
lewing-fix-virtualize-end-anchor-latch
branch
from
August 24, 2026 16:21
013fc31 to
43fe790
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a Blazor Virtualize/QuickGrid flake where switching into AnchorMode.End could incorrectly “pin” the viewport to the bottom due to a stale cached “at-bottom” flag captured during a transient non-scrollable render (e.g., async ItemsProvider in-flight).
Changes:
- Re-derives
bottomTrackingat-bottom state (reachedandwasAtBottomLastRender) from the live viewport whensetAnchorModeis invoked. - Adds a focused Jest regression test that simulates the transient “short list” window and validates that switching into End mode while at top does not jump to bottom.
- Adds an additional Jest test asserting End-mode follow behavior on append.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Components/Web.JS/src/Virtualize.ts | Recomputes all at-bottom tracking flags when anchor mode changes to prevent stale-state re-pinning. |
| src/Components/Web.JS/test/VirtualizeEndAnchorLatch.test.ts | Adds regression coverage for the End-mode “at-bottom latch” scenario and for End-mode follow-on-append behavior. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+135
to
+145
| // User scrolls to the bottom of the loaded list. | ||
| container.scrollTop = LOADED_SCROLL_HEIGHT - CLIENT_HEIGHT; | ||
| container.dispatchEvent(new Event('scroll')); | ||
| Virtualize.refreshObservers(helper, false); | ||
|
|
||
| // New items are appended. | ||
| geometry.scrollHeight = LOADED_SCROLL_HEIGHT + 500; | ||
| Virtualize.refreshObservers(helper, false); | ||
|
|
||
| expect(container.scrollTop).toBe(geometry.scrollHeight); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the flake that #68731 quarantined, and re-enables the test.
Problem
VirtualizationTest.QuickGrid_AnchorMode_End_PrependAtTop_ViewportStaysStablefailed 3 times in 30 days onmain, always asServerVirtualizationTeston the Mono E2E leg withuseItemsProvider: True:scrollTop == maxScrollTop, with byte-identical numbers across all three failures — the grid is pinned to the bottom of the list at a point where the test has not scrolled at all.Root cause
isViewportAtBottom()treats a container that is not yet scrollable as being at the bottom:bottomTracking.wasAtBottomLastRenderis written from that predicate on every render, and outside ofclearBottomFollowit is only reset by a user scroll. This test never scrolls —scrollTopstays0from mount until the assertion.So an empty first render, or an async
ItemsProviderround trip that briefly leaves the container shorter than its viewport, latcheswasAtBottomLastRender = trueand it stays latched.setAnchorModethen refreshedfollowingandreachedbut left that stale flag alone:Selecting End mode therefore satisfied the re-pin guard in
refreshObservedElementson the next render:It is intermittent because it depends on a render landing while the container is not scrollable. Interactive Server with a delayed provider widens that window, which matches the observed parameterization exactly.
Introduced by #67639 (
bf41edc66b, Jul 20), which added thebottomTrackingstate machine and thewasAtBottomLastRenderterm in the guard. First failure Jul 30.Fix
Re-derive all three flags from the live viewport when the anchor mode changes, mirroring what the user-scroll path already does. This also matches the existing reasoning in
restoreAnchorForShift— "Don't rely on the cachedwasAtBottomLastRender— it may be stale" — whichsetAnchorModewas not honoring.Also removes the
[QuarantinedTest]attribute added by #68731, since the underlying cause is fixed here.Testing
Virtualize.test.tswas a 13-line export smoke test, so none of this state machine was covered. AddedVirtualizeEndAnchorLatch.test.ts, which drivesinit/refreshObservers/setAnchorModeagainst stubbed observers and a DOM whose geometry can model "provider in flight" versus "data loaded":switching into End mode at the top does not jump to the bottom after a transient short list— fails without the fix withReceived: 55078. The browser clamps that to55078 - 300 = 54778, reproducing the CI value exactly. Passes with the fix.End mode still follows appends once the viewport has actually reached the bottom— passes with and without the fix, so the first test is not vacuous and ImproveAnchorModetests #67639's intended End-mode behavior is still exercised.Full
Web.JSjest suite: 208 passing, 0 failing. The 5 failing suites are pre-existingCannot find module '@microsoft/signalr'resolution errors, identical with and without this change.eslintonVirtualize.tsgoes from 40 to 38 reported problems — no new ones.E2E confirmation depends on CI here, since the failure needs the Interactive Server + delayed-provider timing window.
Notes for reviewers
Virtualize.tssubstantially. This change is confined tosetAnchorMode, so it should rebase cleanly either way.InitialItemIndexnear-end failure (Quarantine Microsoft.AspNetCore.Components.E2ETest.Tests.VirtualizationTest.QuickGrid_InitialIndex_TallContainer_NearEnd_FillsVi [Content truncated due to length] #68724). That one is still open: [Blazor] Keep Virtualize spacer callbacks programmatic while an alignment is pending #68709 only reduced its failure rate to 2/4 and was closed in favour of Reject stale Blazor Virtualize viewport measurements #68691.Unrelated: the Aug 23 quarantine batch looks misattributed
The five quarantine PRs from Aug 23 cite a "most recent failing build" each. For three of them, that build's complete test-result set does not contain a failure of the test being quarantined:
QuickGrid_InitialIndex_TallContainer_NearEnd_FillsViewportWithoutUserScrollandScrollToItem_UserScrollDuringProviderFetch_UserScrollWins.AnchorMode_End_AppendAfterLeavingBottom_DoesNotReengage— in the Quarantine run, for a test already quarantined under Quarantine ServerVirtualizationTest.AnchorMode_End_AppendAfterLeavingBottom_DoesNotReengage flaky test #66970.QuickGrid_AnchorMode_End_AppendAfterLeavingBottom_DoesNotReengage.The pattern fits resolving the stack-trace line number against current
mainrather than using the fully-qualified test name that is already in the trace, so a real failure gets attributed to whichever test now occupies that line:8a3632c7), stack line 4290 →AnchorMode_End_AppendAfterLeavingBottom_DoesNotReengage. Line 4290 in today'smainis insideAnchorMode_Start_LargePrependAtTop_StillShowsNewItems→ filed as [test-quarantine] Quarantine AnchorMode_Start_LargePrependAtTop_StillShowsNewItems #68729.9d6f69ad), stack line 2346 →QuickGrid_AnchorMode_End_AppendAfterLeavingBottom_DoesNotReengage. Line 2346 today is inside this PR's test → filed as [test-quarantine] Quarantine QuickGrid_AnchorMode_End_PrependAtTop_ViewportStaysStable #68731.Observed failure counts versus what the PRs claimed:
QuickGrid_AnchorMode_NearTop_AppendKeepsViewportStableAnchorMode_Start_LargePrependAtTop_StillShowsNewItemsScope of that check, so the limits are explicit: every
aspnetcore-components-e2ebuild — the only pipeline that runs these, sinceaspnetcore-ciHelix legs contain noComponents.E2ETestresults — across all branches including 201 PR builds, counting every non-passing outcome, over the 392 builds from Jul 27 to Aug 24. AzDO has purged def-87 builds older than Jul 27, so roughly the first two days of the PRs' 30-day window are no longer observable.The single
NearTopfailure was build 1562094 on PR #68664, an unrelated Kestrel DirectTLS change:Assert.NotNull() Failure: Value is nullinGetItemPositionInContainer— a different signature from the scroll-position failures, and not the defect fixed here.Two follow-ons regardless: already-quarantined failures should not seed new quarantines, and the batch missed a genuinely un-quarantined flake,
QuickGrid_AnchorMode_End_AppendAfterLeavingBottom_DoesNotReengage(2 live failures), which shares the async-provider/Server/Mono signature and may share this PR's root cause.