diff --git a/components/settings/__tests__/settings-boot-skeleton.test.tsx b/components/settings/__tests__/settings-boot-skeleton.test.tsx
index d43c271c9..5417f9871 100644
--- a/components/settings/__tests__/settings-boot-skeleton.test.tsx
+++ b/components/settings/__tests__/settings-boot-skeleton.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { render, screen } from '@testing-library/react';
+import { act, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Until the session is up, Settings draws its page skeleton (the one the
@@ -23,12 +23,15 @@ vi.mock('@/hooks/use-config', () => ({ useConfig: () => ({ appName: 'Mail', stal
vi.mock('@/components/layout/navigation-rail', () => ({ NavigationRail: () => null }));
vi.mock('@/components/layout/sidebar-apps-modal', () => ({ SidebarAppsModal: () => null }));
vi.mock('@/components/plugins/app-top-banner-slot', () => ({ AppTopBannerSlot: () => null }));
-vi.mock('@/components/settings/appearance-settings', () => ({ AppearanceSettings: () =>
appearance pane
}));
+vi.mock('@/components/settings/appearance-settings', () => ({
+ AppearanceSettings: () => appearance pane
,
+}));
const { SettingsApp } = await import('../settings-app');
const { useAuthStore } = await import('@/stores/auth-store');
const { usePluginStore } = await import('@/stores/plugin-store');
const { useThemeStore } = await import('@/stores/theme-store');
+const { useSettingsStore } = await import('@/stores/settings-store');
const { SETTINGS_NAV_GROUPS } = await import('../settings-tab-icons');
const { BOOT_HINTS_KEY } = await import('@/lib/boot-skeleton/keys');
const { resetRailHintsMemo } = await import('@/lib/boot-skeleton/hints');
@@ -102,3 +105,26 @@ describe('Settings while the session restores', () => {
expect(hints?.settings).not.toContain('vacation');
});
});
+
+describe('a link to a section while the session restores', () => {
+ it('scrolls once the page is drawn, without switching Advanced on meanwhile', async () => {
+ vi.useFakeTimers();
+ try {
+ const scroll = vi.fn();
+ Element.prototype.scrollIntoView = scroll;
+ useSettingsStore.setState({ showAdvancedSettings: false });
+ useAuthStore.setState({ isAuthenticated: true, isLoading: true, client: null });
+ render();
+ await act(async () => { vi.advanceTimersByTime(3000); });
+ // The skeleton has no sections; nothing to find is not a hidden section.
+ expect(useSettingsStore.getState().showAdvancedSettings).toBe(false);
+ expect(scroll).not.toHaveBeenCalled();
+
+ act(() => { useAuthStore.setState({ isLoading: false, client: fullClient as never }); });
+ await act(async () => { vi.advanceTimersByTime(200); });
+ expect(scroll).toHaveBeenCalled();
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+});
diff --git a/components/settings/settings-app.tsx b/components/settings/settings-app.tsx
index e02596afb..ccda165a0 100644
--- a/components/settings/settings-app.tsx
+++ b/components/settings/settings-app.tsx
@@ -446,8 +446,12 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps =
// an advanced section stays hidden until the switch is on, so after a
// short wait turn it on rather than give up.
const updateSetting = useSettingsStore((s) => s.updateSetting);
+ // The page itself rather than its skeleton (see the gate below).
+ const pageShown = isAuthenticated && !!client;
useEffect(() => {
if (!pendingSection || pendingSection.tab !== activeTab) return;
+ // The skeleton has no sections: wait until the page is drawn.
+ if (!pageShown) return;
if (typeof window === 'undefined') return;
let cancelled = false;
let timer: ReturnType | undefined;
@@ -468,7 +472,7 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps =
window.cancelAnimationFrame(raf);
if (timer) clearTimeout(timer);
};
- }, [pendingSection, activeTab, updateSetting]);
+ }, [pendingSection, activeTab, updateSetting, pageShown]);
// Leaving the settings panel drops any shared-account scope so it never
// leaks into the next visit or another session.