diff --git a/ods/extensions/services/dashboard/src/hooks/__tests__/useGPUDetailed.initial.test.js b/ods/extensions/services/dashboard/src/hooks/__tests__/useGPUDetailed.initial.test.js new file mode 100644 index 0000000000..3b57c0edc0 --- /dev/null +++ b/ods/extensions/services/dashboard/src/hooks/__tests__/useGPUDetailed.initial.test.js @@ -0,0 +1,44 @@ +import { act, renderHook, waitFor } from '@testing-library/react' +import { useGPUDetailed } from '../useGPUDetailed' + +const response = (body, status = 200) => ({ + ok: status >= 200 && status < 300, + status, + json: async () => body, +}) + +describe('useGPUDetailed', () => { + afterEach(() => { + vi.useRealTimers() + vi.restoreAllMocks() + Object.defineProperty(document, 'hidden', { configurable: true, value: false }) + }) + + test('loads initial GPU state while the document is hidden', async () => { + Object.defineProperty(document, 'hidden', { configurable: true, value: true }) + vi.stubGlobal('fetch', vi.fn() + .mockResolvedValueOnce(response({ gpus: [{ name: 'GPU' }] })) + .mockResolvedValueOnce(response({ samples: [] })) + .mockResolvedValueOnce(response({ links: [] }))) + + const { result } = renderHook(() => useGPUDetailed()) + + await waitFor(() => expect(result.current.loading).toBe(false)) + expect(fetch).toHaveBeenCalledTimes(3) + expect(result.current.detailed.gpus[0].name).toBe('GPU') + expect(result.current.error).toBeNull() + }) + + test('pauses after hidden initial data and resumes immediately when visible', async () => { + vi.useFakeTimers() + Object.defineProperty(document, 'hidden', { configurable: true, value: true }) + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response({ gpus: [] }))) + const { unmount } = renderHook(() => useGPUDetailed()) + await act(async () => { await vi.advanceTimersByTimeAsync(20000) }) + expect(fetch).toHaveBeenCalledTimes(3) + Object.defineProperty(document, 'hidden', { configurable: true, value: false }) + await act(async () => { document.dispatchEvent(new Event('visibilitychange')) }) + expect(fetch).toHaveBeenCalledTimes(6) + unmount() + }) +}) diff --git a/ods/extensions/services/dashboard/src/hooks/useGPUDetailed.js b/ods/extensions/services/dashboard/src/hooks/useGPUDetailed.js index c2188ad2c3..f167d3da36 100644 --- a/ods/extensions/services/dashboard/src/hooks/useGPUDetailed.js +++ b/ods/extensions/services/dashboard/src/hooks/useGPUDetailed.js @@ -16,9 +16,10 @@ export function useGPUDetailed() { // the replacement lifetime or release its in-flight guard. let fetchInFlight = false let disposed = false + let hasFetched = false let activeController = null const fetchAll = async () => { - if (disposed || document.hidden || fetchInFlight) return + if (disposed || (document.hidden && hasFetched) || fetchInFlight) return fetchInFlight = true const controller = new AbortController() activeController = controller @@ -38,6 +39,7 @@ export function useGPUDetailed() { if (details !== undefined) setDetailed(details) if (samples !== undefined) setHistory(samples) if (links !== undefined) setTopology(links) + hasFetched = true setError(null) } catch (err) { if (!disposed) setError(err.message)