import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { IntelGpuContextValue, useIntelGpuContext } from '../api/IntelGpuDataContext'; import { fetchGpuMetrics, GpuChipMetrics, GpuMetrics } from '../api/metrics'; import MetricsPage from './MetricsPage'; vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({ Loader: ({ title }: { title: string }) =>
{title}
, SectionBox: ({ title, children }: { title: string; children?: React.ReactNode }) => (

{title}

{children}
), SectionHeader: ({ title }: { title: string }) =>

{title}

, NameValueTable: ({ rows, }: { rows: Array<{ name: React.ReactNode; value: React.ReactNode }>; }) => (
{rows.map((r, i) => (
{r.name}
{r.value}
))}
), SimpleTable: ({ columns, data, }: { columns: Array<{ label: string; getter: (item: unknown) => React.ReactNode }>; data: unknown[]; }) => ( {columns.map(c => ( ))} {data.map((item, i) => ( {columns.map(c => ( ))} ))}
{c.label}
{c.getter(item)}
), StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => ( {children} ), PercentageBar: () =>
, })); vi.mock('../api/IntelGpuDataContext', () => ({ useIntelGpuContext: vi.fn(), })); vi.mock('../api/metrics', () => ({ fetchGpuMetrics: vi.fn(), formatWatts: (w: number) => `${w.toFixed(1)} W`, formatPercent: (used: number, max: number) => max <= 0 ? '—' : `${Math.round((used / max) * 100)}%`, })); function makeContext(overrides: Partial = {}): IntelGpuContextValue { return { devicePlugins: [], pluginInstalled: false, gpuNodes: [], gpuPods: [], pluginPods: [], crdAvailable: false, loading: false, error: null, refresh: vi.fn(), ...overrides, }; } function makeMetrics(chips: GpuChipMetrics[]): GpuMetrics { return { chips, fetchedAt: new Date('2025-03-21T10:00:00Z').toISOString(), }; } const sampleChip: GpuChipMetrics = { nodeName: 'gpu-node-1', chip: '0000:09:01_0', instance: '192.168.1.10:9100', powerWatts: 45.3, powerMaxWatts: 120.0, }; describe('MetricsPage', () => { beforeEach(() => { vi.clearAllMocks(); }); it('shows loader when ctxLoading=true but heading is visible immediately', () => { vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: true })); // fetchGpuMetrics should never be called in loading state vi.mocked(fetchGpuMetrics).mockResolvedValue(null); render(); // Heading renders immediately, loader appears below it while waiting for context expect(screen.getByText('Intel GPU — Metrics')).toBeInTheDocument(); expect(screen.getByTestId('loader')).toHaveTextContent('Loading Intel GPU data...'); }); it('shows "Prometheus Unreachable" section when fetchGpuMetrics returns null', async () => { vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false })); vi.mocked(fetchGpuMetrics).mockResolvedValue(null); render(); await waitFor(() => { expect(screen.getByText('Prometheus Unreachable')).toBeInTheDocument(); }); }); it('shows "No i915 Metrics in Prometheus" when fetchGpuMetrics returns empty chips', async () => { vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false })); vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([])); render(); await waitFor(() => { expect(screen.getByText('No i915 Metrics in Prometheus')).toBeInTheDocument(); }); }); it('shows chip cards with node name when fetchGpuMetrics returns chips', async () => { vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false })); vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([sampleChip])); render(); await waitFor(() => { // GpuChipCard title format: "{nodeName} — {chip}" expect(screen.getByText('gpu-node-1 — 0000:09:01_0')).toBeInTheDocument(); }); }); it('always renders MetricRequirements section', async () => { vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false })); vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([])); render(); // The MetricRequirements section box is titled "Metric Availability" expect(screen.getByText('Metric Availability')).toBeInTheDocument(); }); it('shows GPU Power Summary section when chips are present', async () => { vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false })); vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([sampleChip])); render(); await waitFor(() => { expect(screen.getByText('GPU Power Summary')).toBeInTheDocument(); }); }); it('re-triggers fetch when refresh button is clicked', async () => { vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false })); vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([])); render(); // Wait for initial fetch to complete await waitFor(() => { expect(vi.mocked(fetchGpuMetrics)).toHaveBeenCalled(); }); const callsBefore = vi.mocked(fetchGpuMetrics).mock.calls.length; fireEvent.click(screen.getByRole('button', { name: /refresh metrics/i })); await waitFor(() => { expect(vi.mocked(fetchGpuMetrics).mock.calls.length).toBeGreaterThan(callsBefore); }); }); it('shows "Intel GPU — Metrics" heading', async () => { vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false })); vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([])); render(); expect(screen.getByText('Intel GPU — Metrics')).toBeInTheDocument(); }); it('shows power values for chip cards', async () => { vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false })); vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([sampleChip])); render(); await waitFor(() => { // formatWatts mock: "45.3 W" and "120.0 W" expect(screen.getAllByText(/45\.3 W/).length).toBeGreaterThan(0); }); }); });