import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib'; import { act, render, renderHook, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { IntelGpuDataProvider, useIntelGpuContext } from './IntelGpuDataContext'; vi.mock('@kinvolk/headlamp-plugin/lib', () => ({ K8s: { ResourceClasses: { Node: { useList: vi.fn() }, Pod: { useList: vi.fn() }, }, }, ApiProxy: { request: vi.fn() }, })); // Minimal GPU node fixture const gpuNodeRaw = { metadata: { name: 'gpu-node-1', uid: 'uid-001', labels: { 'intel.feature.node.kubernetes.io/gpu': 'true' }, }, status: { capacity: { 'gpu.intel.com/i915': '1' }, allocatable: { 'gpu.intel.com/i915': '1' }, }, }; // Minimal GPU plugin CRD fixture const gpuDevicePluginRaw = { kind: 'GpuDevicePlugin', metadata: { name: 'gpu-plugin-default', uid: 'uid-dp-001' }, spec: {}, }; function makeNodeWrapper(raw: unknown) { return { jsonData: raw }; } function Wrapper({ children }: { children: React.ReactNode }) { return {children}; } describe('useIntelGpuContext', () => { it('throws when used outside provider', () => { // Suppress React error boundary output const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); expect(() => renderHook(() => useIntelGpuContext())).toThrow( 'useIntelGpuContext must be used within an IntelGpuDataProvider' ); consoleError.mockRestore(); }); }); describe('IntelGpuDataProvider', () => { it('renders children', async () => { vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[], null] as any); vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any); vi.mocked(ApiProxy.request).mockResolvedValue({ items: [] }); render(
hello
); expect(screen.getByTestId('child')).toBeInTheDocument(); }); it('exposes loading=true while nodes/pods are null', async () => { vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([null, null] as any); vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([null, null] as any); // Keep async request pending forever vi.mocked(ApiProxy.request).mockReturnValue(new Promise(() => {})); const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); expect(result.current.loading).toBe(true); }); it('exposes loaded state with GPU nodes once data arrives', async () => { vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([ [makeNodeWrapper(gpuNodeRaw)] as any, null, ] as any); vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any); vi.mocked(ApiProxy.request).mockResolvedValue({ items: [] }); const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); await waitFor(() => expect(result.current.loading).toBe(false)); expect(result.current.gpuNodes).toHaveLength(1); expect(result.current.gpuNodes[0].metadata.name).toBe('gpu-node-1'); }); it('sets crdAvailable=true and populates devicePlugins when ApiProxy returns plugin list', async () => { vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[], null] as any); vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any); // First call = CRD list, subsequent calls = plugin pod selectors (empty) vi.mocked(ApiProxy.request) .mockResolvedValueOnce({ items: [gpuDevicePluginRaw] }) .mockResolvedValue({ items: [] }); const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); await waitFor(() => expect(result.current.loading).toBe(false)); expect(result.current.crdAvailable).toBe(true); expect(result.current.devicePlugins).toHaveLength(1); expect(result.current.devicePlugins[0].metadata.name).toBe('gpu-plugin-default'); }); it('sets crdAvailable=false and does not surface error when ApiProxy throws on CRD request', async () => { vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[], null] as any); vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any); // First call (CRD endpoint) throws, plugin pod selectors resolve empty vi.mocked(ApiProxy.request) .mockRejectedValueOnce(new Error('CRD not found')) .mockResolvedValue({ items: [] }); const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); await waitFor(() => expect(result.current.loading).toBe(false)); expect(result.current.crdAvailable).toBe(false); expect(result.current.devicePlugins).toHaveLength(0); // Inner CRD error should NOT be bubbled up to the top-level error field expect(result.current.error).toBeNull(); }); it('increments refreshKey and re-runs the effect when refresh() is called', async () => { vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[], null] as any); vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any); vi.mocked(ApiProxy.request).mockResolvedValue({ items: [] }); const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); await waitFor(() => expect(result.current.loading).toBe(false)); const callCountBefore = vi.mocked(ApiProxy.request).mock.calls.length; await act(async () => { result.current.refresh(); }); await waitFor(() => { const callCountAfter = vi.mocked(ApiProxy.request).mock.calls.length; expect(callCountAfter).toBeGreaterThan(callCountBefore); }); }); it('treats a hanging CRD request as unavailable after 2s timeout', async () => { vi.useFakeTimers(); const nodeWrapper = { jsonData: {} }; vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[nodeWrapper], null] as any); vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[nodeWrapper], null] as any); vi.mocked(ApiProxy.request) .mockReturnValueOnce(new Promise(() => {})) .mockResolvedValueOnce({ items: [] }) .mockResolvedValueOnce({ items: [] }) .mockResolvedValueOnce({ items: [] }); const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper }); expect(result.current.loading).toBe(true); vi.advanceTimersByTime(2000); await act(async () => {}); expect(result.current.crdAvailable).toBe(false); expect(result.current.loading).toBe(false); vi.useRealTimers(); }); });