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 }) => (
),
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 => (
| {c.label} |
))}
{data.map((item, i) => (
{columns.map(c => (
| {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);
});
});
});