514de78ba7
- Fix ExemptionManager apiVersion bug (apps/batch resources used wrong API path) - Replace resource: any with proper KubeResource interface (strict TypeScript) - Replace all var(--mui-palette-*) CSS variables with useTheme() + theme.palette.* - Replace custom drawer with MUI Drawer component (proper a11y and theming) - Replace alert() calls with StatusLabel-based inline feedback - Add PolarisErrorBoundary wrapping all registered plugin components - Export getPolarisApiPath/isFullUrl from polaris.ts, deduplicate in PolarisSettings - Fix PolarisDataContext test mock missing triggerRefresh - Fix DashboardView test SimpleTable mock using any - Remove dead NamespaceDetailView (replaced by drawer), unused MockPolarisProvider, unused getSeverityColor export - Add tests for InlineAuditSection, AppBarScoreBadge, topIssues, checkMapping (32 new) - Update CLAUDE.md, CHANGELOG.md, README.md for v0.6.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
335 lines
8.7 KiB
TypeScript
335 lines
8.7 KiB
TypeScript
import { render, screen } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import React from 'react';
|
|
import { MemoryRouter } from 'react-router-dom';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { makeAuditData, makeResult } from '../test-utils';
|
|
|
|
// Mock Headlamp lib
|
|
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
|
ApiProxy: { request: vi.fn() },
|
|
Router: {
|
|
createRouteURL: (name: string, params: Record<string, string>) =>
|
|
`/polaris/ns/${params.namespace}`,
|
|
},
|
|
}));
|
|
|
|
// Mock MUI components
|
|
vi.mock('@mui/material/Drawer', () => ({
|
|
default: ({
|
|
open,
|
|
children,
|
|
}: {
|
|
open: boolean;
|
|
children: React.ReactNode;
|
|
onClose?: () => void;
|
|
anchor?: string;
|
|
}) => (open ? <div data-testid="mui-drawer">{children}</div> : null),
|
|
}));
|
|
|
|
vi.mock('@mui/material/IconButton', () => ({
|
|
default: ({
|
|
children,
|
|
onClick,
|
|
'aria-label': ariaLabel,
|
|
}: {
|
|
children: React.ReactNode;
|
|
onClick: () => void;
|
|
'aria-label': string;
|
|
title?: string;
|
|
size?: string;
|
|
}) => (
|
|
<button onClick={onClick} aria-label={ariaLabel}>
|
|
{children}
|
|
</button>
|
|
),
|
|
}));
|
|
|
|
vi.mock('@mui/material/styles', () => ({
|
|
useTheme: () => ({
|
|
palette: {
|
|
primary: { main: '#1976d2' },
|
|
text: { primary: '#000', secondary: '#666' },
|
|
action: { hover: 'rgba(0,0,0,0.04)' },
|
|
background: { default: '#fafafa' },
|
|
},
|
|
}),
|
|
}));
|
|
|
|
// Mock Headlamp CommonComponents
|
|
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 }) => (
|
|
<div data-testid="section-box" data-title={title}>
|
|
{children}
|
|
</div>
|
|
),
|
|
SectionHeader: ({ title }: { title: string }) => <div data-testid="section-header">{title}</div>,
|
|
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
|
<span data-testid="status-label" data-status={status}>
|
|
{children}
|
|
</span>
|
|
),
|
|
NameValueTable: ({ rows }: { rows: Array<{ name: string; value: React.ReactNode }> }) => (
|
|
<table data-testid="name-value-table">
|
|
<tbody>
|
|
{rows.map(row => (
|
|
<tr key={row.name}>
|
|
<td>{row.name}</td>
|
|
<td>{row.value}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
),
|
|
SimpleTable: ({
|
|
columns,
|
|
data,
|
|
emptyMessage,
|
|
}: {
|
|
columns: Array<{ label: string; getter: (row: unknown) => React.ReactNode }>;
|
|
data: unknown[];
|
|
emptyMessage?: string;
|
|
}) =>
|
|
data.length === 0 ? (
|
|
<div data-testid="simple-table-empty">{emptyMessage}</div>
|
|
) : (
|
|
<table data-testid="simple-table">
|
|
<thead>
|
|
<tr>
|
|
{columns.map(col => (
|
|
<th key={col.label}>{col.label}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.map((row, i) => (
|
|
<tr key={i}>
|
|
{columns.map(col => (
|
|
<td key={col.label}>{col.getter(row)}</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
),
|
|
}));
|
|
|
|
const mockUsePolarisDataContext = vi.fn();
|
|
vi.mock('../api/PolarisDataContext', () => ({
|
|
usePolarisDataContext: () => mockUsePolarisDataContext(),
|
|
}));
|
|
|
|
import NamespacesListView from './NamespacesListView';
|
|
|
|
function renderWithRouter(ui: React.ReactElement) {
|
|
return render(<MemoryRouter>{ui}</MemoryRouter>);
|
|
}
|
|
|
|
describe('NamespacesListView', () => {
|
|
it('renders loader when loading', () => {
|
|
mockUsePolarisDataContext.mockReturnValue({
|
|
data: null,
|
|
loading: true,
|
|
error: null,
|
|
});
|
|
|
|
renderWithRouter(<NamespacesListView />);
|
|
expect(screen.getByTestId('loader')).toHaveTextContent('Loading Polaris audit data');
|
|
});
|
|
|
|
it('renders error message when error is set', () => {
|
|
mockUsePolarisDataContext.mockReturnValue({
|
|
data: null,
|
|
loading: false,
|
|
error: 'Polaris dashboard not reachable',
|
|
});
|
|
|
|
renderWithRouter(<NamespacesListView />);
|
|
expect(screen.getByText('Polaris dashboard not reachable')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders "No Data" when no data and no error', () => {
|
|
mockUsePolarisDataContext.mockReturnValue({
|
|
data: null,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithRouter(<NamespacesListView />);
|
|
expect(screen.getByText('No Polaris audit results found.')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders namespace rows with correct scores and buttons', () => {
|
|
const data = makeAuditData([
|
|
makeResult({
|
|
Name: 'deploy-a',
|
|
Namespace: 'alpha',
|
|
Results: {
|
|
c1: {
|
|
ID: 'c1',
|
|
Message: '',
|
|
Details: [],
|
|
Success: true,
|
|
Severity: 'warning',
|
|
Category: 'X',
|
|
},
|
|
},
|
|
}),
|
|
makeResult({
|
|
Name: 'deploy-b',
|
|
Namespace: 'beta',
|
|
Results: {
|
|
c2: {
|
|
ID: 'c2',
|
|
Message: '',
|
|
Details: [],
|
|
Success: false,
|
|
Severity: 'danger',
|
|
Category: 'X',
|
|
},
|
|
},
|
|
}),
|
|
]);
|
|
|
|
mockUsePolarisDataContext.mockReturnValue({
|
|
data,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithRouter(<NamespacesListView />);
|
|
|
|
// Namespace buttons (now buttons instead of links for drawer)
|
|
const alphaButton = screen.getByText('alpha');
|
|
expect(alphaButton).toBeInTheDocument();
|
|
expect(alphaButton.tagName).toBe('BUTTON');
|
|
|
|
const betaButton = screen.getByText('beta');
|
|
expect(betaButton).toBeInTheDocument();
|
|
expect(betaButton.tagName).toBe('BUTTON');
|
|
});
|
|
|
|
it('uses correct scoreStatus: >=80 success, >=50 warning, <50 error', () => {
|
|
// Create a namespace with 100% score (1 pass) and one with 0% (1 danger)
|
|
const data = makeAuditData([
|
|
makeResult({
|
|
Name: 'perfect',
|
|
Namespace: 'good-ns',
|
|
Results: {
|
|
c1: {
|
|
ID: 'c1',
|
|
Message: '',
|
|
Details: [],
|
|
Success: true,
|
|
Severity: 'warning',
|
|
Category: 'X',
|
|
},
|
|
},
|
|
}),
|
|
makeResult({
|
|
Name: 'bad',
|
|
Namespace: 'bad-ns',
|
|
Results: {
|
|
c2: {
|
|
ID: 'c2',
|
|
Message: '',
|
|
Details: [],
|
|
Success: false,
|
|
Severity: 'danger',
|
|
Category: 'X',
|
|
},
|
|
},
|
|
}),
|
|
]);
|
|
|
|
mockUsePolarisDataContext.mockReturnValue({
|
|
data,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithRouter(<NamespacesListView />);
|
|
|
|
// Find score StatusLabels - good-ns has 100% (success), bad-ns has 0% (error)
|
|
const statusLabels = screen.getAllByTestId('status-label');
|
|
const scoreLabels = statusLabels.filter(el => el.textContent?.includes('%'));
|
|
|
|
const successScore = scoreLabels.find(el => el.textContent === '100%');
|
|
expect(successScore).toHaveAttribute('data-status', 'success');
|
|
|
|
const errorScore = scoreLabels.find(el => el.textContent === '0%');
|
|
expect(errorScore).toHaveAttribute('data-status', 'error');
|
|
});
|
|
|
|
it('opens drawer when namespace button is clicked and URL hash is updated', async () => {
|
|
const user = userEvent.setup();
|
|
const data = makeAuditData([
|
|
makeResult({
|
|
Name: 'deploy-a',
|
|
Namespace: 'alpha',
|
|
Results: {
|
|
c1: {
|
|
ID: 'c1',
|
|
Message: '',
|
|
Details: [],
|
|
Success: true,
|
|
Severity: 'warning',
|
|
Category: 'X',
|
|
},
|
|
},
|
|
}),
|
|
]);
|
|
|
|
mockUsePolarisDataContext.mockReturnValue({
|
|
data,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithRouter(<NamespacesListView />);
|
|
|
|
// Click the namespace button
|
|
const alphaButton = screen.getByText('alpha');
|
|
await user.click(alphaButton);
|
|
|
|
// Drawer should open (check for the panel title)
|
|
expect(screen.getByText(/Polaris — alpha/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('initializes drawer from URL hash', () => {
|
|
const data = makeAuditData([
|
|
makeResult({
|
|
Name: 'deploy-a',
|
|
Namespace: 'test-ns',
|
|
Results: {
|
|
c1: {
|
|
ID: 'c1',
|
|
Message: '',
|
|
Details: [],
|
|
Success: true,
|
|
Severity: 'warning',
|
|
Category: 'X',
|
|
},
|
|
},
|
|
}),
|
|
]);
|
|
|
|
mockUsePolarisDataContext.mockReturnValue({
|
|
data,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
|
|
// Render with initial hash in URL
|
|
render(
|
|
<MemoryRouter initialEntries={['/polaris/namespaces#test-ns']}>
|
|
<NamespacesListView />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
// Drawer should be open with the namespace from hash
|
|
expect(screen.getByText(/Polaris — test-ns/)).toBeInTheDocument();
|
|
});
|
|
});
|