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:
parent
a1f5234617
commit
e089832039
90
web/src/components/layout/Header.dom.test.tsx
Normal file
90
web/src/components/layout/Header.dom.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user