FN-6142: fix mobile visibility restore scroll anchoring

Restore pinned mobile chat scroll position when visibility-driven refetch hooks fire.

- re-anchor mobile chat to the bottom during visibility/pageshow refetch capture when the user was pinned
- tighten ChatView visibility restore regression tests to assert the synchronous re-anchor path directly
- remove the ChatView rooms test quarantine and restore the dashboard include list entry

Files changed:
 packages/dashboard/app/components/ChatView.tsx          |  6 +++++-
 .../app/components/__tests__/ChatView.rooms.test.tsx    | 17 ++++++++---------
 .../app/components/__tests__/ChatView.test.tsx          |  7 ++++---
 packages/dashboard/vitest.config.ts                     |  2 +-
 scripts/lib/test-quarantine.json                        |  8 +-------
 5 files changed, 19 insertions(+), 21 deletions(-)

Fusion-Task-Id: FN-6142

Fusion-Task-Lineage: 2974cf0d-5512-4b24-ab0c-2d2fb63fd41a
This commit is contained in:
gsxdsm
2026-06-09 15:52:34 -07:00
parent a57167e96a
commit cca7a61c4f
5 changed files with 19 additions and 21 deletions

View File

@@ -1708,7 +1708,11 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
}
const captureForRefetch = () => {
const wasPinnedBefore = !isUserScrollingRef.current;
captureScrollSnapshot();
if (wasPinnedBefore && isMobile && messagesContainerRef.current) {
scrollToBottom("visibility-restore");
}
};
const onVisibilityChange = () => {
@@ -1725,7 +1729,7 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
document.removeEventListener("visibilitychange", onVisibilityChange);
window.removeEventListener("pageshow", captureForRefetch);
};
}, [isMobile, activeSession, roomThreadActive, captureScrollSnapshot]);
}, [isMobile, activeSession, roomThreadActive, captureScrollSnapshot, scrollToBottom]);
useEffect(() => {
if (roomThreadActive) {

View File

@@ -801,9 +801,10 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
Object.defineProperty(document, "visibilityState", { configurable: true, value: "visible" });
fireEvent(document, new Event("visibilitychange"));
await waitFor(() => {
expect(metrics.getScrollTop()).toBe(1180);
});
// Regression guard: visibility restore must explicitly re-anchor when pinned.
// This previously passed only when an old anchorToBottom rAF happened to run
// after the visibility event and masked that the handler only captured a snapshot.
expect(metrics.getScrollTop()).toBe(1180);
} finally {
metrics.restore();
restoreMatchMedia.mockRestore();
@@ -826,9 +827,9 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
metrics.setScrollTop(300);
fireEvent(window, new Event("pageshow"));
await waitFor(() => {
expect(metrics.getScrollTop()).toBe(1180);
});
// Regression guard: pageshow shares the visibility restore path and must not
// depend on leftover rAF callbacks from the initial mount anchor.
expect(metrics.getScrollTop()).toBe(1180);
} finally {
metrics.restore();
restoreMatchMedia.mockRestore();
@@ -851,9 +852,7 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
Object.defineProperty(document, "visibilityState", { configurable: true, value: "visible" });
fireEvent(document, new Event("visibilitychange"));
await waitFor(() => {
expect(metrics.getScrollTop()).toBe(300);
});
expect(metrics.getScrollTop()).toBe(300);
} finally {
metrics.restore();
restoreMatchMedia.mockRestore();

View File

@@ -4577,9 +4577,10 @@ describe("ChatView mobile behavior", () => {
Object.defineProperty(document, "visibilityState", { configurable: true, value: "visible" });
fireEvent(document, new Event("visibilitychange"));
await waitFor(() => {
expect(scrollTopValue).toBe(1180);
});
// Regression guard: visibility restore must explicitly re-anchor when pinned.
// Without that, this only passed when leftover anchorToBottom rAF callbacks
// happened to run after the visibility event.
expect(scrollTopValue).toBe(1180);
} finally {
restoreMatchMedia.mockRestore();
Object.defineProperty(document, "visibilityState", { configurable: true, value: "visible" });

View File

@@ -231,7 +231,7 @@ const qualityAppComponentBatchBTests = buildComponentQualityInclude(batchedQuali
const qualityAppAppOnlyTests = ["app/components/__tests__/App.test.tsx"];
const qualityAppChatOnlyTests = ["app/components/__tests__/ChatView.test.tsx"];
const qualityAppSettingsOnlyTests = ["app/components/__tests__/SettingsModal.test.tsx"];
const quarantinedDashboardTests = ["app/components/__tests__/ChatView.rooms.test.tsx"];
const quarantinedDashboardTests: string[] = [];
const qualityApiTests = [
// Critical HTTP/server behavior: auth, task/project/settings mutation,