Files
headlamp-intel-gpu-plugin/src/components/NodesPage.test.tsx
T
privilegedescalation-engineer[bot] 6cd159b5a4 test: add component test coverage for all untested files (#17)
* test: add component test coverage for all untested files

Adds 60 new tests (108 total) covering every untested module:
- IntelGpuDataContext: provider renders, loading/loaded states, CRD
  available/unavailable paths, refresh, useIntelGpuContext throws outside
  provider
- OverviewPage: loading, plugin-not-detected, error, populated, refresh
  button, CRD notice, device plugin table, plugin daemon pods, active pods
- NodesPage: loading, empty state, GPU node summary table, detail cards
- PodsPage: loading, empty state, summary counts, pending pod attention,
  all-pods table
- DevicePluginsPage: loading, CRD unavailable, no-plugins, plugin detail,
  daemon pod table
- NodeDetailSection: null for non-GPU nodes, GPU capacity/allocatable rows,
  pod list, loading state
- PodDetailSection: null for non-GPU pods, GPU resource rows, phase status,
  limits-only containers
- MetricsPage: context loading gate, Prometheus unreachable, empty chips,
  chip cards with power values, MetricRequirements always rendered, refresh

Also fixes vitest.config.mts to pin NODE_ENV=test so tests run correctly
without requiring callers to set it explicitly.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: remove unused act import and merge duplicate metrics imports in MetricsPage.test.tsx

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: cast useList mock return values to any in IntelGpuDataContext.test.tsx

The Headlamp useList() return type is an intersection of a tuple and
QueryListResponse, which plain array literals like [[], null] and
[null, null] do not satisfy. Cast all useList mockReturnValue arguments
to any so tsc passes without requiring full KubeObject stub objects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: run Prettier formatting and ESLint lint:fix on test files

Addresses CI format:check failures and import-sort warning in
MetricsPage.test.tsx flagged by QA on PR #17.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Hugh Hackman <hugh@privilegedescalation.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Gandalf the Greybeard <gandalf@privilegedescalation.com>
Co-authored-by: Gandalf the Greybeard <gandalf@privilegedescalation.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Gandalf the Greybeard <gandalf-the-greybeard[bot]@users.noreply.github.com>
2026-03-21 12:53:04 +00:00

154 lines
4.7 KiB
TypeScript

import { render, screen } from '@testing-library/react';
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { IntelGpuContextValue, useIntelGpuContext } from '../api/IntelGpuDataContext';
import { IntelGpuNode } from '../api/k8s';
import NodesPage from './NodesPage';
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
Loader: ({ title }: { title: string }) => <div data-testid="loader">{title}</div>,
SectionBox: ({ title, children }: { title: string; children?: React.ReactNode }) => (
<section>
<h2>{title}</h2>
{children}
</section>
),
SectionHeader: ({ title }: { title: string }) => <h1>{title}</h1>,
NameValueTable: ({
rows,
}: {
rows: Array<{ name: React.ReactNode; value: React.ReactNode }>;
}) => (
<dl>
{rows.map((r, i) => (
<div key={i}>
<dt>{r.name}</dt>
<dd>{r.value}</dd>
</div>
))}
</dl>
),
SimpleTable: ({
columns,
data,
}: {
columns: Array<{ label: string; getter: (item: unknown) => React.ReactNode }>;
data: unknown[];
}) => (
<table>
<thead>
<tr>
{columns.map(c => (
<th key={c.label}>{c.label}</th>
))}
</tr>
</thead>
<tbody>
{data.map((item, i) => (
<tr key={i}>
{columns.map(c => (
<td key={c.label}>{c.getter(item)}</td>
))}
</tr>
))}
</tbody>
</table>
),
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
<span data-status={status}>{children}</span>
),
PercentageBar: () => <div data-testid="percentage-bar" />,
}));
vi.mock('../api/IntelGpuDataContext', () => ({
useIntelGpuContext: vi.fn(),
}));
function makeContext(overrides: Partial<IntelGpuContextValue> = {}): IntelGpuContextValue {
return {
devicePlugins: [],
pluginInstalled: false,
gpuNodes: [],
gpuPods: [],
pluginPods: [],
crdAvailable: false,
loading: false,
error: null,
refresh: vi.fn(),
...overrides,
};
}
const gpuNode: IntelGpuNode = {
metadata: {
name: 'gpu-node-1',
uid: 'uid-001',
labels: { 'intel.feature.node.kubernetes.io/gpu': 'true' },
creationTimestamp: '2025-01-01T00:00:00Z',
},
status: {
capacity: { 'gpu.intel.com/i915': '2', cpu: '8' },
allocatable: { 'gpu.intel.com/i915': '2', cpu: '8' },
conditions: [{ type: 'Ready', status: 'True' }],
nodeInfo: {
osImage: 'Ubuntu 22.04',
kernelVersion: '5.15.0',
kubeletVersion: 'v1.28.0',
},
},
};
describe('NodesPage', () => {
it('shows loader when loading=true', () => {
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: true }));
render(<NodesPage />);
expect(screen.getByTestId('loader')).toHaveTextContent('Loading GPU node data...');
});
it('shows "No GPU Nodes Found" when gpuNodes is empty', () => {
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false, gpuNodes: [] }));
render(<NodesPage />);
expect(screen.getByText('No GPU Nodes Found')).toBeInTheDocument();
});
it('shows "GPU Node Summary" section and per-node detail card when gpuNodes present', () => {
vi.mocked(useIntelGpuContext).mockReturnValue(
makeContext({ loading: false, gpuNodes: [gpuNode] })
);
render(<NodesPage />);
expect(screen.getByText('GPU Node Summary')).toBeInTheDocument();
// Node name appears in both the summary table and the detail card section header
expect(screen.getAllByText('gpu-node-1').length).toBeGreaterThanOrEqual(1);
});
it('renders a detail card for each GPU node', () => {
const secondNode: IntelGpuNode = {
metadata: {
name: 'gpu-node-2',
uid: 'uid-002',
labels: { 'intel.feature.node.kubernetes.io/gpu': 'true' },
},
status: {
capacity: { 'gpu.intel.com/i915': '1' },
allocatable: { 'gpu.intel.com/i915': '1' },
conditions: [{ type: 'Ready', status: 'True' }],
},
};
vi.mocked(useIntelGpuContext).mockReturnValue(
makeContext({ loading: false, gpuNodes: [gpuNode, secondNode] })
);
render(<NodesPage />);
// Node names appear in both the summary table cell and the detail card heading
expect(screen.getAllByText('gpu-node-1').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('gpu-node-2').length).toBeGreaterThanOrEqual(1);
});
it('shows error section when error is set', () => {
vi.mocked(useIntelGpuContext).mockReturnValue(
makeContext({ loading: false, error: 'node fetch failed', gpuNodes: [] })
);
render(<NodesPage />);
expect(screen.getByText('node fetch failed')).toBeInTheDocument();
});
});