import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('@kinvolk/headlamp-plugin/lib', () => ({ ApiProxy: { request: vi.fn().mockResolvedValue({}), }, ConfigStore: class { get() { return {}; } set() {} update() {} useConfig() { return () => ({}); } }, })); vi.mock( '@kinvolk/headlamp-plugin/lib/CommonComponents', async () => await import('./__mocks__/commonComponents') ); vi.mock('../api/TnsCsiDataContext'); vi.mock('../api/kbench', async importOriginal => { const actual = await importOriginal(); return { ...actual, createPvc: vi.fn().mockResolvedValue(undefined), createJob: vi.fn().mockResolvedValue(undefined), deleteJob: vi.fn().mockResolvedValue(undefined), deletePvc: vi.fn().mockResolvedValue(undefined), getJobPhase: vi.fn().mockResolvedValue({ phase: 'Active', job: {} }), fetchKbenchLogs: vi.fn().mockResolvedValue(''), listKbenchJobs: vi.fn().mockResolvedValue([]), generateJobName: vi.fn().mockReturnValue('kbench-abc123'), generatePvcName: vi.fn().mockReturnValue('kbench-abc123-pvc'), }; }); import { ApiProxy } from '@kinvolk/headlamp-plugin/lib'; import { createJob, createPvc, listKbenchJobs } from '../api/kbench'; import { useTnsCsiContext } from '../api/TnsCsiDataContext'; import { defaultContext, makeSampleStorageClass } from '../test-helpers'; import BenchmarkPage from './BenchmarkPage'; function mockContext(overrides?: Parameters[0]) { vi.mocked(useTnsCsiContext).mockReturnValue(defaultContext(overrides)); } describe('BenchmarkPage', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(listKbenchJobs).mockResolvedValue([]); }); it('shows loader when loading', () => { mockContext({ loading: true }); render(); expect(screen.getByTestId('loader')).toHaveTextContent('Loading tns-csi data...'); }); it('renders benchmark guide section', () => { mockContext(); render(); expect(screen.getByText('Benchmark Guide')).toBeInTheDocument(); expect(screen.getByText(/Do not cancel mid-run/)).toBeInTheDocument(); }); it('renders Run New Benchmark form', () => { const sc = makeSampleStorageClass(); mockContext({ storageClasses: [sc] }); render(); expect(screen.getByText('Run New Benchmark')).toBeInTheDocument(); expect(screen.getByLabelText('Select storage class for benchmark')).toBeInTheDocument(); }); it('populates SC dropdown with storage class names', () => { const sc1 = makeSampleStorageClass({ metadata: { name: 'sc-a' } }); const sc2 = makeSampleStorageClass({ metadata: { name: 'sc-b' } }); mockContext({ storageClasses: [sc1, sc2] }); render(); const select = screen.getByLabelText('Select storage class for benchmark') as HTMLSelectElement; expect(select.options.length).toBe(2); expect(select.options[0].value).toBe('sc-a'); expect(select.options[1].value).toBe('sc-b'); }); it('shows "No tns-csi storage classes found" when empty', () => { mockContext({ storageClasses: [] }); render(); const select = screen.getByLabelText('Select storage class for benchmark') as HTMLSelectElement; expect(select.options[0].text).toContain('No tns-csi storage classes'); }); it('shows confirmation dialog when Run Benchmark is clicked', () => { const sc = makeSampleStorageClass(); mockContext({ storageClasses: [sc] }); render(); fireEvent.click(screen.getByLabelText('Start kbench storage benchmark')); expect(screen.getByText('Confirm Benchmark')).toBeInTheDocument(); expect(screen.getByText(/~33Gi PVC/)).toBeInTheDocument(); }); it('cancels confirmation dialog', () => { const sc = makeSampleStorageClass(); mockContext({ storageClasses: [sc] }); render(); fireEvent.click(screen.getByLabelText('Start kbench storage benchmark')); fireEvent.click(screen.getByLabelText('Cancel benchmark')); expect(screen.queryByText('Confirm Benchmark')).not.toBeInTheDocument(); }); it('starts benchmark on confirmation and calls createPvc', async () => { vi.useFakeTimers(); const sc = makeSampleStorageClass(); mockContext({ storageClasses: [sc] }); // PVC bind check vi.mocked(ApiProxy.request).mockResolvedValue({ status: { phase: 'Bound' } }); render(); fireEvent.click(screen.getByLabelText('Start kbench storage benchmark')); await act(async () => { fireEvent.click(screen.getByLabelText('Confirm and start benchmark')); }); expect(vi.mocked(createPvc)).toHaveBeenCalledTimes(1); vi.useRealTimers(); }); it('shows failed state when PVC creation fails', async () => { const sc = makeSampleStorageClass(); mockContext({ storageClasses: [sc] }); vi.mocked(createPvc).mockRejectedValueOnce(new Error('quota exceeded')); render(); fireEvent.click(screen.getByLabelText('Start kbench storage benchmark')); await act(async () => { fireEvent.click(screen.getByLabelText('Confirm and start benchmark')); }); await waitFor(() => { expect(screen.getByText(/quota exceeded/)).toBeInTheDocument(); }); expect(screen.getByText('Failed')).toBeInTheDocument(); }); it('renders past benchmarks section', async () => { mockContext(); vi.mocked(listKbenchJobs).mockResolvedValueOnce([]); render(); await waitFor(() => { expect(screen.getByText('Past Benchmarks')).toBeInTheDocument(); }); expect(screen.getByText('No past benchmark jobs found.')).toBeInTheDocument(); }); it('renders past benchmark jobs in table', async () => { mockContext(); vi.mocked(listKbenchJobs).mockResolvedValueOnce([ { jobName: 'kbench-old', namespace: 'default', storageClass: 'tns-nfs', phase: 'Complete', startedAt: '2025-01-01T00:00:00Z', }, ]); render(); await waitFor(() => { expect(screen.getByText('kbench-old')).toBeInTheDocument(); }); expect(screen.getByText('Complete')).toBeInTheDocument(); }); it('disables Run Benchmark button when no storage classes', () => { mockContext({ storageClasses: [] }); render(); const btn = screen.getByLabelText('Start kbench storage benchmark'); expect(btn).toBeDisabled(); }); it('shows confirmation dialog with selected SC and namespace', () => { const sc = makeSampleStorageClass(); mockContext({ storageClasses: [sc] }); render(); // Change namespace const nsInput = screen.getByLabelText( 'Kubernetes namespace for benchmark job' ) as HTMLInputElement; fireEvent.change(nsInput, { target: { value: 'bench-ns' } }); fireEvent.click(screen.getByLabelText('Start kbench storage benchmark')); // Confirm dialog shows SC and namespace in tags expect(screen.getByText('Confirm Benchmark')).toBeInTheDocument(); expect(screen.getByLabelText('Confirm and start benchmark')).toBeInTheDocument(); // Namespace is shown in the dialog const dialogText = screen.getByText(/bench-ns/); expect(dialogText).toBeInTheDocument(); }); it('can change test size and mode', () => { const sc = makeSampleStorageClass(); mockContext({ storageClasses: [sc] }); render(); const sizeInput = screen.getByLabelText('FIO test size') as HTMLInputElement; fireEvent.change(sizeInput, { target: { value: '10G' } }); expect(sizeInput.value).toBe('10G'); const modeSelect = screen.getByLabelText('Benchmark mode') as HTMLSelectElement; fireEvent.change(modeSelect, { target: { value: 'quick' } }); expect(modeSelect.value).toBe('quick'); }); it('shows failed state when job creation fails', async () => { const sc = makeSampleStorageClass(); mockContext({ storageClasses: [sc] }); vi.mocked(createPvc).mockResolvedValueOnce(undefined); // PVC binds immediately vi.mocked(ApiProxy.request).mockResolvedValue({ status: { phase: 'Bound' } }); vi.mocked(createJob).mockRejectedValueOnce(new Error('job already exists')); render(); fireEvent.click(screen.getByLabelText('Start kbench storage benchmark')); await act(async () => { fireEvent.click(screen.getByLabelText('Confirm and start benchmark')); }); await waitFor(() => { expect(screen.getByText(/job already exists/)).toBeInTheDocument(); }); expect(screen.getByText('Failed')).toBeInTheDocument(); }); });