fix(C5): pause index polling when hidden

Guard Header market-index refreshes with document.visibilityState and resume on visibilitychange so hidden tabs stop polling the backend while preserving stale index data.

Refs: docs/AUDIT_REDESIGN.md item C5.

Co-Authored-By: GPT-5 Codex <noreply@openai.com>
This commit is contained in:
Saravana Achu Mac 2026-05-04 16:50:37 -07:00
parent a1f5234617
commit e089832039
2 changed files with 105 additions and 3 deletions

View File

@ -0,0 +1,90 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { AppContext, type AppContextValue } from '../../context/AppContext';
import { Header } from './Header';
const { fetchMarketIndicesMock } = vi.hoisted(() => ({
fetchMarketIndicesMock: vi.fn(),
}));
vi.mock('../../lib/marketApi', () => ({
fetchMarketIndices: (...args: any[]) => fetchMarketIndicesMock(...args),
}));
const appContext: AppContextValue = {
botState: {
settings: {},
symbols: {},
health: {},
alerts: [],
positions: [],
orders: [],
history: [],
uptime: 0,
} as any,
socket: null,
connected: true,
activeSymbol: 'AAPL',
setActiveSymbol: vi.fn(),
isAdmin: false,
user: { id: 'u1' },
profile: {},
showBacktestTab: false,
showMarketplaceTab: false,
handleSignOut: vi.fn(),
};
function setVisibilityState(value: DocumentVisibilityState) {
Object.defineProperty(document, 'visibilityState', {
configurable: true,
value,
});
}
function renderHeader() {
return render(
<AppContext.Provider value={appContext}>
<MemoryRouter>
<Header />
</MemoryRouter>
</AppContext.Provider>,
);
}
describe('Header market index polling', () => {
beforeEach(() => {
vi.useFakeTimers();
fetchMarketIndicesMock.mockReset();
fetchMarketIndicesMock.mockResolvedValue([]);
setVisibilityState('visible');
});
afterEach(() => {
vi.useRealTimers();
setVisibilityState('visible');
});
it('pauses index polling while the document is hidden and resumes when visible', async () => {
setVisibilityState('hidden');
renderHeader();
expect(fetchMarketIndicesMock).not.toHaveBeenCalled();
act(() => {
vi.advanceTimersByTime(60_000);
});
expect(fetchMarketIndicesMock).not.toHaveBeenCalled();
setVisibilityState('visible');
act(() => {
document.dispatchEvent(new Event('visibilitychange'));
});
expect(fetchMarketIndicesMock).toHaveBeenCalledTimes(1);
act(() => {
vi.advanceTimersByTime(60_000);
});
expect(fetchMarketIndicesMock).toHaveBeenCalledTimes(2);
});
});

View File

@ -12,16 +12,28 @@ export function Header() {
const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement>(null);
// Fetch live market indices once on mount, refresh every 60s
// Fetch live market indices while visible; hidden tabs should not burn API quota.
useEffect(() => {
let cancelled = false;
const load = () =>
const isVisible = () => typeof document === 'undefined' || document.visibilityState === 'visible';
const load = () => {
if (!isVisible()) return;
fetchMarketIndices()
.then(data => { if (!cancelled) setIndices(data); })
.catch(() => {/* ignore — keep stale data */});
};
const handleVisibilityChange = () => {
if (isVisible()) load();
};
load();
const interval = setInterval(load, 60_000);
return () => { cancelled = true; clearInterval(interval); };
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
cancelled = true;
clearInterval(interval);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
const handleSearch = (raw: string) => {