Fix Virtualize End anchoring after initial provider load - #69310
ilonatommy wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
The E2E test setup must be corrected to exercise the intended transition safely and prevent callbacks from referencing deleted state.
Pull request overview
Fixes Virtualize<TItem> AnchorMode.End anchoring after the initial ItemsProvider load populates an empty viewport.
Changes:
- Corrects initial bottom-state tracking.
- Applies pending spacer styles before measuring scroll geometry.
- Adds refresh support and deterministic E2E coverage.
File summaries
| File | Summary |
|---|---|
src/Components/Web.JS/src/Virtualize.ts |
Updates End anchoring and layout synchronization. |
src/Components/test/testassets/BasicTestApp/VirtualizationAnchorMode.razor |
Adds a refresh-data test control. |
src/Components/test/E2ETest/Tests/VirtualizationTest.cs |
Adds initial-load anchoring coverage; two moderate findings remain, each with 1 vote, regarding zero-count requests and observer callback cleanup. |
Review details
Suppressed comments (2)
src/Components/test/E2ETest/Tests/VirtualizationTest.cs:5502
- Because the wrapper is installed before this reload, no
OnSpacerBeforeVisiblecallback reachesVirtualizeto initialize_visibleItemCapacity. The test provider honorsrequest.CountwithTake(request.Count), so this explicit refresh starts with a zero-count request and renders no items;scrollHeight - clientHeightremains zero and the assertion cannot exercise the intended 0→N transition. Seed a nonzero request capacity without allowing the bottom-tracking callback to run, or otherwise adjust the setup before callingRefreshDataAsync.
Browser.Exists(By.Id("reload-with-initial-index")).Click();
Browser.True(() => Convert.ToInt64(js.ExecuteScript(
"return window.__virtualizeObserverCallbacks.length;"), CultureInfo.InvariantCulture) > 0);
Browser.Exists(By.Id("refresh-data")).Click();
Browser.Contains("Refreshed data", () => Browser.Exists(By.Id("status")).Text);
Browser.True(() => GetMaximumScrollTop(js, container) > 0);
src/Components/test/E2ETest/Tests/VirtualizationTest.cs:5528
- Restoring the global constructor does not change the observer instance created from this wrapper. Its callback still calls
window.__virtualizeObserverCallbacks.push(...), so deleting that property can make any later spacer notification throw aTypeErrorwhile the mounted Virtualize is still observing (and can pollute subsequent tests). Disable the wrapper for existing observers or keep a safe queue until the observer/page is disposed.
window.IntersectionObserver = window.__nativeIntersectionObserver;
delete window.__nativeIntersectionObserver;
delete window.__virtualizeObserverCallbacks;
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Not true,
Looks valid. |
Virtualize<TItem>withAnchorMode.Endcould remain at the beginning of the list after the initialItemsProviderresult populated an empty viewport.Description
wasAtBottomLastRendershould be set conditionally.scrollHeightto pin the viewport. This is required for WebAssembly render mode, where observer callbacks can run after the synchronous refresh.For test reviewers:
The tests need to be deterministic, so using observer gate for timing the actions was necessary. Without the gate, the first
IntersectionObservercallback does two things:This creates a timing dependency. If the callback records the empty viewport's bottom state before the provider result is rendered, it can mask the bug and make an ordinary E2E test pass even on the unfixed implementation.
The test therefore temporarily holds Virtualize spacer observer callbacks and invokes
RefreshDataAsync()explicitly. This produces the relevant initial0 -> Nprovider transition before any observer callback can establish the bottom state. It then verifies thatAnchorMode.Endpins the populated viewport by itself.Fixes #69302