diff --git a/frontend/packages/helm-plugin/src/components/details-page/history/__tests__/HelmReleaseHistory.spec.tsx b/frontend/packages/helm-plugin/src/components/details-page/history/__tests__/HelmReleaseHistory.spec.tsx
new file mode 100644
index 00000000000..c96f428658e
--- /dev/null
+++ b/frontend/packages/helm-plugin/src/components/details-page/history/__tests__/HelmReleaseHistory.spec.tsx
@@ -0,0 +1,169 @@
+import { screen, waitFor } from '@testing-library/react';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { fetchHelmReleaseHistory } from '../../../../utils/helm-utils';
+import HelmReleaseHistory from '../HelmReleaseHistory';
+
+jest.mock('react-router', () => ({
+ ...jest.requireActual('react-router'),
+ useParams: jest.fn().mockReturnValue({ ns: 'test-ns', name: 'my-release' }),
+}));
+
+jest.mock('../../../../utils/helm-utils', () => ({
+ ...jest.requireActual('../../../../utils/helm-utils'),
+ fetchHelmReleaseHistory: jest.fn(),
+}));
+
+jest.mock('../HelmReleaseHistoryTable', () => {
+ const MockTable = (props: { releaseHistory: unknown[]; isLoading: boolean }) => (
+
+ );
+ return { __esModule: true, default: MockTable };
+});
+
+jest.mock('../HelmReleaseHistoryTableHelpers', () => ({
+ useHelmReleaseHistoryColumns: jest.fn(),
+ getHelmReleaseHistoryRows: jest.fn().mockReturnValue([]),
+ getHistoryColumnIndexById: jest.fn().mockReturnValue(0),
+}));
+
+jest.mock('@console/internal/components/utils', () => ({
+ StatusBox: (props: { loadError: string; label: string }) => (
+ {props.loadError}
+ ),
+}));
+
+jest.mock('@console/shared/src/components/layout/PaneBody', () => {
+ const MockPaneBody = (props: { children: unknown }) => (
+ {props.children as string}
+ );
+ return { __esModule: true, default: MockPaneBody };
+});
+
+jest.mock('@console/shared/src/hooks/useDeepCompareMemoize', () => ({
+ useDeepCompareMemoize: (value: unknown) => value,
+}));
+
+const mockFetchHistory = fetchHelmReleaseHistory as jest.Mock;
+
+const mockObj = {
+ metadata: { name: 'my-release', namespace: 'test-ns' },
+};
+
+const mockHelmRelease = {
+ name: 'my-release',
+ namespace: 'test-ns',
+ version: 3,
+ chart: {
+ metadata: { name: 'my-chart', version: '1.0.0', apiVersion: 'v2', urls: [] },
+ files: [],
+ templates: [],
+ values: {},
+ },
+ info: {
+ description: 'Install complete',
+ deleted: '',
+ first_deployed: '2024-01-01T00:00:00Z',
+ last_deployed: '2024-01-03T00:00:00Z',
+ status: 'deployed',
+ notes: '',
+ },
+};
+
+const mockRevisions = [
+ {
+ ...mockHelmRelease,
+ version: 1,
+ info: { ...mockHelmRelease.info, last_deployed: '2024-01-01T00:00:00Z' },
+ },
+ {
+ ...mockHelmRelease,
+ version: 2,
+ info: { ...mockHelmRelease.info, last_deployed: '2024-01-02T00:00:00Z' },
+ },
+ {
+ ...mockHelmRelease,
+ version: 3,
+ info: { ...mockHelmRelease.info, last_deployed: '2024-01-03T00:00:00Z' },
+ },
+];
+
+const defaultProps = {
+ obj: mockObj,
+ customData: mockHelmRelease,
+};
+
+describe('HelmReleaseHistory', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the history table when revisions load successfully', async () => {
+ mockFetchHistory.mockResolvedValue(mockRevisions);
+ renderWithProviders();
+ await waitFor(() => {
+ expect(screen.getByTestId('mock-history-table')).toBeTruthy();
+ });
+ });
+
+ it('should show loading state while fetching revisions', () => {
+ mockFetchHistory.mockReturnValue(new Promise(() => {}));
+ renderWithProviders();
+ const table = screen.getByTestId('mock-history-table');
+ expect(table.getAttribute('data-loading')).toBe('true');
+ });
+
+ it('should show StatusBox with error when fetchHelmReleaseHistory fails', async () => {
+ mockFetchHistory.mockRejectedValue(new Error('Network error'));
+ renderWithProviders();
+ await waitFor(() => {
+ expect(screen.getByTestId('status-box')).toBeTruthy();
+ });
+ expect(screen.getByText('Network error')).toBeTruthy();
+ });
+
+ it('should pass revisions to HelmReleaseHistoryTable', async () => {
+ mockFetchHistory.mockResolvedValue(mockRevisions);
+ renderWithProviders();
+ await waitFor(() => {
+ const table = screen.getByTestId('mock-history-table');
+ expect(table.getAttribute('data-count')).toBe('3');
+ });
+ });
+
+ it('should fetch history with the correct namespace and release name', () => {
+ mockFetchHistory.mockReturnValue(new Promise(() => {}));
+ renderWithProviders();
+ expect(mockFetchHistory).toHaveBeenCalledWith('my-release', 'test-ns');
+ });
+
+ it('should show default error message when error has no message', async () => {
+ mockFetchHistory.mockRejectedValue(new Error());
+ renderWithProviders();
+ await waitFor(() => {
+ expect(screen.getByTestId('status-box')).toBeTruthy();
+ });
+ expect(screen.getByText('Unable to load Helm Release history')).toBeTruthy();
+ });
+
+ it('should render within PaneBody when loaded successfully', async () => {
+ mockFetchHistory.mockResolvedValue(mockRevisions);
+ renderWithProviders();
+ await waitFor(() => {
+ expect(screen.getByTestId('pane-body')).toBeTruthy();
+ });
+ expect(screen.getByTestId('mock-history-table')).toBeTruthy();
+ });
+
+ it('should not render PaneBody when there is a load error', async () => {
+ mockFetchHistory.mockRejectedValue(new Error('Server error'));
+ renderWithProviders();
+ await waitFor(() => {
+ expect(screen.getByTestId('status-box')).toBeTruthy();
+ });
+ expect(screen.queryByTestId('pane-body')).toBeNull();
+ });
+});
diff --git a/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/__tests__/CreateHelmChartRepositoryForm.spec.tsx b/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/__tests__/CreateHelmChartRepositoryForm.spec.tsx
new file mode 100644
index 00000000000..d92c18531c3
--- /dev/null
+++ b/frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/__tests__/CreateHelmChartRepositoryForm.spec.tsx
@@ -0,0 +1,204 @@
+import { screen } from '@testing-library/react';
+import { EditorType } from '@console/shared/src/components/synced-editor/editor-toggle';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import CreateHelmChartRepositoryForm from '../CreateHelmChartRepositoryForm';
+
+jest.mock('@console/shared/src/components/form-utils/FlexForm', () => ({
+ FlexForm: ({ children, onSubmit }: any) => (
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormBody', () => ({
+ FormBody: ({ children }: any) => {children}
,
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormHeader', () => ({
+ FormHeader: ({ title, helpText }: any) => (
+
+
{title}
+ {helpText && {helpText}}
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormFooter', () => ({
+ FormFooter: ({ submitLabel, disableSubmit, errorMessage, successMessage, handleCancel }: any) => (
+
+
+
+ {errorMessage &&
{errorMessage}
}
+ {successMessage &&
{successMessage}
}
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/formik-fields/SyncedEditorField', () => ({
+ SyncedEditorField: ({ formContext, yamlContext }: any) => (
+
+ {formContext?.editor}
+ {yamlContext?.editor}
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/formik-fields/CodeEditorField', () => ({
+ CodeEditorField: () => YAML Editor
,
+}));
+
+jest.mock('../CreateHelmChartRepositoryFormEditor', () => ({
+ __esModule: true,
+ default: () => Form Editor
,
+}));
+
+jest.mock('../helmchartrepository-create-utils', () => ({
+ convertToForm: jest.fn((v: any) => v),
+ convertToHelmChartRepository: jest.fn(() => ({})),
+}));
+
+jest.mock('@console/shared/src/components/editor/yaml-download-utils', () => ({
+ downloadYaml: jest.fn(),
+}));
+
+jest.mock('@console/shared/src/utils/yaml', () => ({
+ safeJSToYAML: jest.fn(() => 'yaml: data'),
+}));
+
+const defaultFormData = {
+ repoName: 'my-repo',
+ repoUrl: 'https://example.com/charts',
+ scope: 'HelmChartRepository',
+ repoDisplayName: '',
+ repoDescription: '',
+};
+
+const defaultFormikProps = {
+ values: {
+ editorType: EditorType.Form,
+ formData: defaultFormData,
+ yamlData: 'apiVersion: v1',
+ },
+ errors: {},
+ touched: {},
+ isSubmitting: false,
+ isValidating: false,
+ status: undefined,
+ submitCount: 0,
+ dirty: false,
+ isValid: true,
+ initialValues: {
+ editorType: EditorType.Form,
+ formData: defaultFormData,
+ yamlData: 'apiVersion: v1',
+ },
+ initialErrors: {},
+ initialTouched: {},
+ initialStatus: undefined,
+ handleSubmit: jest.fn(),
+ handleReset: jest.fn(),
+ handleBlur: jest.fn(),
+ handleChange: jest.fn(),
+ resetForm: jest.fn(),
+ setErrors: jest.fn(),
+ setFieldError: jest.fn(),
+ setFieldTouched: jest.fn(),
+ setFieldValue: jest.fn(),
+ setFormikState: jest.fn(),
+ setStatus: jest.fn(),
+ setSubmitting: jest.fn(),
+ setTouched: jest.fn(),
+ setValues: jest.fn(),
+ submitForm: jest.fn(),
+ validateForm: jest.fn(),
+ validateField: jest.fn(),
+ getFieldProps: jest.fn(),
+ getFieldMeta: jest.fn(),
+ getFieldHelpers: jest.fn(),
+ registerField: jest.fn(),
+ unregisterField: jest.fn(),
+};
+
+const defaultProps = {
+ ...defaultFormikProps,
+ namespace: 'test-ns',
+ handleCancel: jest.fn(),
+ showScopeType: true,
+ existingRepo: null,
+};
+
+describe('CreateHelmChartRepositoryForm', () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should render the form title for creating a new repository', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Create Helm Chart Repository')).toBeVisible();
+ });
+
+ it('should render the form and YAML editors via SyncedEditorField', () => {
+ renderWithProviders();
+
+ expect(screen.getByTestId('synced-editor')).toBeVisible();
+ expect(screen.getByTestId('form-editor')).toBeVisible();
+ expect(screen.getByTestId('code-editor')).toBeVisible();
+ });
+
+ it('should render Create as the submit button label for new repos', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Create' })).toBeVisible();
+ });
+
+ it('should render Save as the submit button label when editing existing repo', () => {
+ const existingRepo = {
+ apiVersion: 'helm.openshift.io/v1beta1',
+ kind: 'HelmChartRepository',
+ metadata: { name: 'existing-repo' },
+ spec: { connectionConfig: { url: 'https://example.com' } },
+ };
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('button', { name: 'Save' })).toBeVisible();
+ });
+
+ it('should disable submit button when form is not dirty', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled();
+ });
+
+ it('should enable submit button when form is dirty and has no errors', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('button', { name: 'Create' })).not.toBeDisabled();
+ });
+
+ it('should display submit error message', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('Failed to create')).toBeVisible();
+ });
+
+ it('should show the form description text', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Add helm chart repository.')).toBeVisible();
+ });
+});
diff --git a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/__tests__/HelmChartVersionDropdown.spec.tsx b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/__tests__/HelmChartVersionDropdown.spec.tsx
new file mode 100644
index 00000000000..f42c0ac7f59
--- /dev/null
+++ b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/__tests__/HelmChartVersionDropdown.spec.tsx
@@ -0,0 +1,182 @@
+import { screen, waitFor } from '@testing-library/react';
+import { Formik } from 'formik';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { HelmActionType } from '../../../../types/helm-types';
+import { getChartEntriesByName, getChartVersions } from '../../../../utils/helm-utils';
+import HelmChartVersionDropdown from '../HelmChartVersionDropdown';
+
+jest.mock('@console/shared/src/components/formik-fields/DropdownField', () => ({
+ DropdownField: ({ label, title, disabled, helpText, name }: any) => (
+
+
+ {title}
+ {disabled && disabled}
+ {helpText && {helpText}}
+
+
+ ),
+}));
+
+jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({
+ useK8sWatchResource: jest.fn(() => [[], true, null]),
+}));
+
+jest.mock('@console/shared/src/hooks/useWarningModal', () => ({
+ useWarningModal: jest.fn(() => jest.fn()),
+}));
+
+const mockCoFetch = jest.fn();
+jest.mock('@console/shared/src/utils/console-fetch', () => ({
+ coFetchJSON: jest.fn(),
+ coFetch: (...args: any[]) => mockCoFetch(...args),
+}));
+
+jest.mock('../../../../models/helm', () => ({
+ HelmChartRepositoryModel: {
+ apiGroup: 'helm.openshift.io',
+ apiVersion: 'v1beta1',
+ kind: 'HelmChartRepository',
+ plural: 'helmchartrepositories',
+ },
+}));
+
+jest.mock('@console/internal/module/k8s', () => ({
+ referenceForModel: jest.fn(() => 'helm.openshift.io~v1beta1~HelmChartRepository'),
+}));
+
+jest.mock('../../../../utils/helm-utils', () => ({
+ getChartEntriesByName: jest.fn(() => []),
+ getChartVersions: jest.fn(() => ({})),
+ getChartIndexEntry: jest.fn(() => 'test-chart--my-repo'),
+ concatVersions: jest.fn((version: string) => version),
+ getChartURL: jest.fn(() => ''),
+ getChartReadme: jest.fn(() => ''),
+ getChartRepositoryTitle: jest.fn(() => ''),
+ mergeHelmValuesOnChartVersionChange: jest.fn(() => ({})),
+}));
+
+const indexYaml = `entries:
+ test-chart:
+ - name: test-chart
+ version: "2.0.0"
+ - name: test-chart
+ version: "1.0.0"`;
+
+const formikInitialValues = {
+ chartVersion: '1.0.0',
+ chartURL: 'https://example.com/charts/test-chart-1.0.0.tgz',
+ chartRepoName: 'my-repo',
+ chartName: 'test-chart',
+ chartReadme: '',
+ appVersion: '1.0',
+ yamlData: 'key: value',
+ formData: {},
+ formSchema: {},
+ editorType: 'form',
+ releaseName: 'my-release',
+ chartIndexEntry: '',
+};
+
+const defaultProps = {
+ chartVersion: '1.0.0',
+ chartName: 'test-chart',
+ helmAction: HelmActionType.Create,
+ onVersionChange: jest.fn(),
+ namespace: 'test-ns',
+ chartIndexEntry: 'test-chart--my-repo',
+};
+
+const renderDropdown = (props = {}) => {
+ return renderWithProviders(
+
+
+ ,
+ );
+};
+
+describe('HelmChartVersionDropdown', () => {
+ beforeEach(() => {
+ mockCoFetch.mockResolvedValue({ text: () => Promise.resolve(indexYaml) });
+ (getChartEntriesByName as jest.Mock).mockReturnValue([
+ { version: '2.0.0', repoName: 'my-repo' },
+ { version: '1.0.0', repoName: 'my-repo' },
+ ]);
+ (getChartVersions as jest.Mock).mockReturnValue({
+ '2.0.0': '2.0.0',
+ '1.0.0': '1.0.0',
+ });
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should render the Chart version label', async () => {
+ renderDropdown();
+
+ expect(screen.getByText('Chart version')).toBeVisible();
+ await waitFor(() => {
+ expect(screen.queryByTestId('dropdown-disabled')).not.toBeInTheDocument();
+ });
+ });
+
+ it('should display the current chart version in the dropdown title', async () => {
+ renderDropdown();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('dropdown-title')).toHaveTextContent('1.0.0');
+ });
+ });
+
+ it('should show help text for upgrade action', async () => {
+ renderDropdown({ helmAction: HelmActionType.Upgrade });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('dropdown-help')).toHaveTextContent(
+ 'Select the version to upgrade to.',
+ );
+ });
+ });
+
+ it('should not show help text for create action', async () => {
+ renderDropdown({ helmAction: HelmActionType.Create });
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('dropdown-disabled')).not.toBeInTheDocument();
+ });
+ expect(screen.queryByTestId('dropdown-help')).not.toBeInTheDocument();
+ });
+
+ it('should show "No versions available" when no chart versions are loaded and no chartVersion', async () => {
+ (getChartEntriesByName as jest.Mock).mockReturnValue([]);
+ (getChartVersions as jest.Mock).mockReturnValue({});
+
+ renderDropdown({ chartVersion: '' });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('dropdown-title')).toHaveTextContent('No versions available');
+ });
+ });
+
+ it('should disable dropdown when only one version is available', async () => {
+ (getChartEntriesByName as jest.Mock).mockReturnValue([
+ { version: '1.0.0', repoName: 'my-repo' },
+ ]);
+ (getChartVersions as jest.Mock).mockReturnValue({ '1.0.0': '1.0.0' });
+
+ renderDropdown();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('dropdown-disabled')).toBeVisible();
+ });
+ });
+
+ it('should fetch chart index from the correct namespace', async () => {
+ renderDropdown({ namespace: 'my-namespace' });
+
+ expect(mockCoFetch).toHaveBeenCalledWith('/api/helm/charts/index.yaml?namespace=my-namespace');
+ await waitFor(() => {
+ expect(screen.queryByTestId('dropdown-disabled')).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/frontend/packages/helm-plugin/src/components/forms/install-upgrade/__tests__/HelmInstallUpgradeForm.spec.tsx b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/__tests__/HelmInstallUpgradeForm.spec.tsx
new file mode 100644
index 00000000000..db71e446723
--- /dev/null
+++ b/frontend/packages/helm-plugin/src/components/forms/install-upgrade/__tests__/HelmInstallUpgradeForm.spec.tsx
@@ -0,0 +1,290 @@
+import { screen } from '@testing-library/react';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { HelmActionType } from '../../../../types/helm-types';
+import type { HelmInstallUpgradeFormData } from '../HelmInstallUpgradeForm';
+import HelmInstallUpgradeForm from '../HelmInstallUpgradeForm';
+
+jest.mock('@console/shared/src/components/formik-fields/InputField', () => ({
+ InputField: (props: any) => (
+
+
+
+ {props.helpText && {props.helpText}}
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/formik-fields/ResourceDropdownField', () => ({
+ ResourceDropdownField: (props: any) => (
+
+
+ {props.helpText && {props.helpText}}
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/formik-fields/SyncedEditorField', () => ({
+ SyncedEditorField: ({ formContext, yamlContext }: any) => (
+
+ {formContext?.editor}
+ {yamlContext?.editor}
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/formik-fields/DynamicFormField', () => ({
+ DynamicFormField: ({ formDescription }: any) => (
+ {formDescription}
+ ),
+}));
+
+jest.mock('@console/shared/src/components/formik-fields/CodeEditorField', () => ({
+ CodeEditorField: ({ label }: any) => {label}
,
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FlexForm', () => ({
+ FlexForm: ({ children, onSubmit }: any) => (
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormBody', () => ({
+ FormBody: ({ children }: any) => {children}
,
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormHeader', () => ({
+ FormHeader: ({ title, helpText }: any) => (
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormFooter', () => ({
+ FormFooter: ({ submitLabel, disableSubmit, resetLabel, errorMessage }: any) => (
+
+
+
+ {errorMessage &&
{errorMessage}
}
+
+ ),
+}));
+
+jest.mock('@console/dev-console/src/components/import/section/FormSection', () => ({
+ __esModule: true,
+ default: ({ children }: any) => {children}
,
+}));
+
+jest.mock('../HelmChartVersionDropdown', () => ({
+ __esModule: true,
+ default: ({ chartName, chartVersion }: any) => (
+
+ {chartName} - {chartVersion}
+
+ ),
+}));
+
+jest.mock('../HelmReadmeModal', () => ({
+ useHelmReadmeModalLauncher: jest.fn(() => jest.fn()),
+}));
+
+jest.mock('../../url-chart/useBasicAuthSecretDropdown', () => ({
+ useBasicAuthSecretDropdown: jest.fn(() => ({ handleSecretChange: jest.fn() })),
+ CREATE_SECRET_KEY: '__create_secret__',
+ NONE_SECRET_KEY: '__none__',
+}));
+
+jest.mock('../../url-chart/useSecretResources', () => ({
+ useSecretResources: jest.fn(() => [{ data: [], loaded: true, loadError: null, kind: 'Secret' }]),
+}));
+
+jest.mock('@console/shared/src/components/dynamic-form/utils', () => ({
+ getJSONSchemaOrder: jest.fn(() => ({})),
+ prune: jest.fn((v: any) => v),
+}));
+
+const defaultValues: HelmInstallUpgradeFormData = {
+ releaseName: 'my-release',
+ chartName: 'my-chart',
+ chartRepoName: 'my-repo',
+ chartVersion: '1.0.0',
+ chartReadme: '',
+ appVersion: '1.0.0',
+ yamlData: 'key: value',
+ formData: { replicas: 1 },
+ formSchema: { type: 'object', properties: { replicas: { type: 'number' } } },
+ editorType: 'form' as any,
+ basicAuthSecretName: '',
+ isURLInstall: false,
+};
+
+const defaultFormikProps = {
+ values: defaultValues,
+ errors: {},
+ touched: {},
+ isSubmitting: false,
+ isValidating: false,
+ status: undefined,
+ submitCount: 0,
+ dirty: false,
+ isValid: true,
+ initialValues: defaultValues,
+ initialErrors: {},
+ initialTouched: {},
+ initialStatus: undefined,
+ handleSubmit: jest.fn(),
+ handleReset: jest.fn(),
+ handleBlur: jest.fn(),
+ handleChange: jest.fn(),
+ resetForm: jest.fn(),
+ setErrors: jest.fn(),
+ setFieldError: jest.fn(),
+ setFieldTouched: jest.fn(),
+ setFieldValue: jest.fn(),
+ setFormikState: jest.fn(),
+ setStatus: jest.fn(),
+ setSubmitting: jest.fn(),
+ setTouched: jest.fn(),
+ setValues: jest.fn(),
+ submitForm: jest.fn(),
+ validateForm: jest.fn(),
+ validateField: jest.fn(),
+ getFieldProps: jest.fn(),
+ getFieldMeta: jest.fn(),
+ getFieldHelpers: jest.fn(),
+ registerField: jest.fn(),
+ unregisterField: jest.fn(),
+};
+
+const defaultProps = {
+ ...defaultFormikProps,
+ chartHasValues: true,
+ helmActionConfig: {
+ type: HelmActionType.Create,
+ title: 'Install Helm Chart',
+ subTitle: 'Install a Helm Chart to create a Helm Release.',
+ helmReleaseApi: '/api/helm/release',
+ fetch: jest.fn(),
+ redirectURL: '/helm-releases',
+ },
+ chartMetaDescription: 'A test chart description',
+ onVersionChange: jest.fn(),
+ chartError: null as Error,
+ namespace: 'test-ns',
+};
+
+describe('HelmInstallUpgradeForm', () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should render the form title and release name field', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Install Helm Chart')).toBeVisible();
+ expect(screen.getByLabelText('Release name')).toBeVisible();
+ expect(screen.getByText('A unique name for the Helm Release.')).toBeVisible();
+ });
+
+ it('should render the chart version dropdown with chart name and version', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('my-chart - 1.0.0')).toBeVisible();
+ });
+
+ it('should render the synced editor when chart has values and no chart error', () => {
+ renderWithProviders();
+
+ expect(screen.getByTestId('synced-editor')).toBeVisible();
+ });
+
+ it('should show non-configurable alert when chart has no values and no schema', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(
+ screen.getByText(
+ /Helm release is not configurable since the Helm Chart doesn't define any values/,
+ ),
+ ).toBeVisible();
+ });
+
+ it('should display chart error alert and disable the release name field when chartError is set', () => {
+ const chartError = new Error('Chart fetch failed');
+ renderWithProviders();
+
+ expect(screen.getByText('Helm Chart cannot be installed')).toBeVisible();
+ expect(screen.getByLabelText('Release name')).toBeDisabled();
+ });
+
+ it('should disable submit button when form is submitting', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled();
+ });
+
+ it('should disable submit button on upgrade when form is not dirty', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('button', { name: 'Upgrade' })).toBeDisabled();
+ });
+
+ it('should show the README link when chartReadme is provided', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('button', { name: 'README' })).toBeVisible();
+ });
+
+ it('should display submit error message when status has submitError', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('Something went wrong')).toBeVisible();
+ });
+
+ it('should show secret dropdown when isURLInstall is true', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('Secret for Basic authentication')).toBeVisible();
+ });
+});
diff --git a/frontend/packages/helm-plugin/src/components/forms/rollback/__tests__/HelmReleaseRollbackForm.spec.tsx b/frontend/packages/helm-plugin/src/components/forms/rollback/__tests__/HelmReleaseRollbackForm.spec.tsx
new file mode 100644
index 00000000000..7c70f497597
--- /dev/null
+++ b/frontend/packages/helm-plugin/src/components/forms/rollback/__tests__/HelmReleaseRollbackForm.spec.tsx
@@ -0,0 +1,221 @@
+import { screen } from '@testing-library/react';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { HelmActionType } from '../../../../types/helm-types';
+import type { HelmRelease } from '../../../../types/helm-types';
+import HelmReleaseRollbackForm from '../HelmReleaseRollbackForm';
+
+jest.mock('react-i18next', () => ({
+ ...jest.requireActual('react-i18next'),
+ useTranslation: () => ({
+ t: (key: string) => key.replace(/^helm-plugin~/, ''),
+ i18n: { language: 'en' },
+ }),
+ Trans: () => Select the version to rollback to,
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormBody', () => ({
+ FormBody: ({ children }: any) => {children}
,
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormHeader', () => ({
+ FormHeader: ({ title, helpText }: any) => (
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormFooter', () => ({
+ FormFooter: ({ submitLabel, disableSubmit, resetLabel, errorMessage }: any) => (
+
+
+
+ {errorMessage &&
{errorMessage}
}
+
+ ),
+}));
+
+jest.mock('../../../details-page/history/HelmReleaseHistoryTable', () => ({
+ __esModule: true,
+ default: ({ releaseHistory }: any) => (
+
+
+ {releaseHistory.map((r: HelmRelease) => (
+
+ | {r.name} |
+ {r.version} |
+
+ ))}
+
+
+ ),
+}));
+
+const mockReleaseHistory: HelmRelease[] = [
+ {
+ name: 'my-release',
+ namespace: 'test-ns',
+ chart: {
+ files: [],
+ metadata: {
+ name: 'test-chart',
+ version: '1.0.0',
+ apiVersion: 'v2',
+ urls: [],
+ },
+ templates: [],
+ values: {},
+ },
+ info: {
+ description: 'Revision 1',
+ deleted: '',
+ first_deployed: '2026-01-01T00:00:00Z',
+ last_deployed: '2026-01-01T00:00:00Z',
+ status: 'deployed',
+ notes: '',
+ },
+ version: 1,
+ },
+ {
+ name: 'my-release',
+ namespace: 'test-ns',
+ chart: {
+ files: [],
+ metadata: {
+ name: 'test-chart',
+ version: '1.0.0',
+ apiVersion: 'v2',
+ urls: [],
+ },
+ templates: [],
+ values: {},
+ },
+ info: {
+ description: 'Revision 2',
+ deleted: '',
+ first_deployed: '2026-01-01T00:00:00Z',
+ last_deployed: '2026-01-02T00:00:00Z',
+ status: 'deployed',
+ notes: '',
+ },
+ version: 2,
+ },
+];
+
+const defaultFormikProps = {
+ values: { version: 1 },
+ errors: {},
+ touched: {},
+ isSubmitting: false,
+ isValidating: false,
+ status: undefined,
+ submitCount: 0,
+ dirty: false,
+ isValid: true,
+ initialValues: { version: 1 },
+ initialErrors: {},
+ initialTouched: {},
+ initialStatus: undefined,
+ handleSubmit: jest.fn(),
+ handleReset: jest.fn(),
+ handleBlur: jest.fn(),
+ handleChange: jest.fn(),
+ resetForm: jest.fn(),
+ setErrors: jest.fn(),
+ setFieldError: jest.fn(),
+ setFieldTouched: jest.fn(),
+ setFieldValue: jest.fn(),
+ setFormikState: jest.fn(),
+ setStatus: jest.fn(),
+ setSubmitting: jest.fn(),
+ setTouched: jest.fn(),
+ setValues: jest.fn(),
+ submitForm: jest.fn(),
+ validateForm: jest.fn(),
+ validateField: jest.fn(),
+ getFieldProps: jest.fn(),
+ getFieldMeta: jest.fn(),
+ getFieldHelpers: jest.fn(),
+ registerField: jest.fn(),
+ unregisterField: jest.fn(),
+};
+
+const defaultProps = {
+ ...defaultFormikProps,
+ releaseName: 'my-release',
+ releaseHistory: mockReleaseHistory,
+ helmActionConfig: {
+ type: HelmActionType.Rollback,
+ title: 'Rollback Helm Release',
+ subTitle: 'Select a version to rollback to.',
+ helmReleaseApi: '/api/helm/release',
+ fetch: jest.fn(),
+ redirectURL: '/helm-releases',
+ },
+};
+
+describe('HelmReleaseRollbackForm', () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should render the form title', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Rollback Helm Release')).toBeVisible();
+ });
+
+ it('should display the rollback help text', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Select the version to rollback to')).toBeVisible();
+ });
+
+ it('should render the revision history table with release entries', () => {
+ renderWithProviders();
+
+ expect(screen.getByTestId('history-table')).toBeVisible();
+ expect(screen.getByText('Revision history')).toBeVisible();
+ });
+
+ it('should render Rollback as the submit button label', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Rollback' })).toBeVisible();
+ });
+
+ it('should disable submit button when form is not dirty', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Rollback' })).toBeDisabled();
+ });
+
+ it('should disable submit button when form is submitting', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Rollback' })).toBeDisabled();
+ });
+
+ it('should enable submit button when form is dirty and has no errors', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Rollback' })).not.toBeDisabled();
+ });
+
+ it('should display submit error when status has submitError', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('Rollback failed')).toBeVisible();
+ });
+
+ it('should render Cancel button', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
+ });
+});
diff --git a/frontend/packages/helm-plugin/src/components/forms/url-chart/__tests__/HelmURLChartForm.spec.tsx b/frontend/packages/helm-plugin/src/components/forms/url-chart/__tests__/HelmURLChartForm.spec.tsx
new file mode 100644
index 00000000000..9360a1d8ed0
--- /dev/null
+++ b/frontend/packages/helm-plugin/src/components/forms/url-chart/__tests__/HelmURLChartForm.spec.tsx
@@ -0,0 +1,194 @@
+import { screen } from '@testing-library/react';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import HelmURLChartForm from '../HelmURLChartForm';
+
+jest.mock('@console/shared/src/components/formik-fields/InputField', () => ({
+ InputField: (props: any) => (
+
+
+
+ {props.helpText && {props.helpText}}
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/formik-fields/ResourceDropdownField', () => ({
+ ResourceDropdownField: (props: any) => (
+
+
+ {props.helpText && {props.helpText}}
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FlexForm', () => ({
+ FlexForm: ({ children, onSubmit }: any) => (
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormBody', () => ({
+ FormBody: ({ children }: any) => {children}
,
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormHeader', () => ({
+ FormHeader: ({ title, helpText }: any) => (
+
+
{title}
+ {helpText && {helpText}}
+
+ ),
+}));
+
+jest.mock('@console/shared/src/components/form-utils/FormFooter', () => ({
+ FormFooter: ({ submitLabel, disableSubmit, resetLabel, errorMessage }: any) => (
+
+
+
+ {errorMessage &&
{errorMessage}
}
+
+ ),
+}));
+
+jest.mock('@console/dev-console/src/components/import/section/FormSection', () => ({
+ __esModule: true,
+ default: ({ children }: any) => {children}
,
+}));
+
+jest.mock('../useBasicAuthSecretDropdown', () => ({
+ useBasicAuthSecretDropdown: jest.fn(() => ({ handleSecretChange: jest.fn() })),
+ CREATE_SECRET_KEY: '__create_secret__',
+}));
+
+jest.mock('../useSecretResources', () => ({
+ useSecretResources: jest.fn(() => [{ data: [], loaded: true, loadError: null, kind: 'Secret' }]),
+}));
+
+const defaultValues = {
+ chartURL: '',
+ releaseName: '',
+ chartVersion: '',
+ basicAuthSecretName: '',
+};
+
+const defaultFormikProps = {
+ values: defaultValues,
+ errors: {},
+ touched: {},
+ isSubmitting: false,
+ isValidating: false,
+ status: undefined,
+ submitCount: 0,
+ dirty: false,
+ isValid: true,
+ initialValues: defaultValues,
+ initialErrors: {},
+ initialTouched: {},
+ initialStatus: undefined,
+ handleSubmit: jest.fn(),
+ handleReset: jest.fn(),
+ handleBlur: jest.fn(),
+ handleChange: jest.fn(),
+ resetForm: jest.fn(),
+ setErrors: jest.fn(),
+ setFieldError: jest.fn(),
+ setFieldTouched: jest.fn(),
+ setFieldValue: jest.fn(),
+ setFormikState: jest.fn(),
+ setStatus: jest.fn(),
+ setSubmitting: jest.fn(),
+ setTouched: jest.fn(),
+ setValues: jest.fn(),
+ submitForm: jest.fn(),
+ validateForm: jest.fn(),
+ validateField: jest.fn(),
+ getFieldProps: jest.fn(),
+ getFieldMeta: jest.fn(),
+ getFieldHelpers: jest.fn(),
+ registerField: jest.fn(),
+ unregisterField: jest.fn(),
+};
+
+const defaultProps = {
+ ...defaultFormikProps,
+ namespace: 'test-ns',
+ onNext: jest.fn(),
+};
+
+describe('HelmURLChartForm', () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should render the form title and description', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Install Helm chart from URL')).toBeVisible();
+ });
+
+ it('should render Chart URL, Release name, and Chart version input fields', () => {
+ renderWithProviders();
+
+ expect(screen.getByLabelText('Chart URL')).toBeVisible();
+ expect(screen.getByLabelText('Release name')).toBeVisible();
+ expect(screen.getByLabelText('Chart version')).toBeVisible();
+ });
+
+ it('should render the Secret for Basic authentication dropdown', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Secret for Basic authentication')).toBeVisible();
+ });
+
+ it('should render Next as the submit button label', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Next' })).toBeVisible();
+ });
+
+ it('should disable Next button when form is not dirty', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Next' })).toBeDisabled();
+ });
+
+ it('should disable Next button when form is invalid', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Next' })).toBeDisabled();
+ });
+
+ it('should enable Next button when form is valid and dirty', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Next' })).not.toBeDisabled();
+ });
+
+ it('should display submit error message when status has submitError', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('Chart not found')).toBeVisible();
+ });
+
+ it('should render Cancel button', () => {
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
+ });
+});
diff --git a/frontend/packages/helm-plugin/src/components/list-page/__tests__/HelmReleaseList.spec.tsx b/frontend/packages/helm-plugin/src/components/list-page/__tests__/HelmReleaseList.spec.tsx
new file mode 100644
index 00000000000..453c03f53a7
--- /dev/null
+++ b/frontend/packages/helm-plugin/src/components/list-page/__tests__/HelmReleaseList.spec.tsx
@@ -0,0 +1,200 @@
+import { screen, waitFor } from '@testing-library/react';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { fetchHelmReleases } from '../../../utils/helm-utils';
+import HelmReleaseList from '../HelmReleaseList';
+
+const mockUseParams = jest.fn();
+jest.mock('react-router', () => ({
+ ...jest.requireActual('react-router'),
+ useParams: () => mockUseParams(),
+ Link: ({ children, to }: any) => (
+ {children}
+ ),
+}));
+
+const mockUseK8sWatchResource = jest.fn();
+jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({
+ useK8sWatchResource: (...args: any[]) => mockUseK8sWatchResource(...args),
+}));
+
+jest.mock('@console/app/src/components/data-view/ConsoleDataView', () => ({
+ ConsoleDataView: ({ label, data, loaded, loadError }: any) => (
+
+ {label}
+ {!loaded && Loading...}
+ {loadError && {String(loadError)}}
+ {loaded && !loadError && {data?.length ?? 0} releases}
+
+ ),
+ initialFiltersDefault: { name: '' },
+ actionsCellProps: {},
+ nameCellProps: {},
+}));
+
+jest.mock('@console/app/src/components/data-view/useResizableColumnProps', () => ({
+ useColumnWidthSettings: jest.fn(() => ({
+ getResizableProps: jest.fn(() => ({})),
+ resetAllColumnWidths: jest.fn(),
+ })),
+}));
+
+jest.mock('@console/internal/components/utils', () => ({
+ LoadingBox: () => Loading...
,
+}));
+
+jest.mock('@console/shared/src/components/catalog/utils/catalog-utils', () => ({
+ isCatalogTypeEnabled: jest.fn(() => true),
+}));
+
+jest.mock('@console/shared/src/components/document-title/DocumentTitle', () => ({
+ DocumentTitle: ({ children }: any) => {children},
+}));
+
+jest.mock('@console/shared/src/components/layout/PaneBody', () => ({
+ __esModule: true,
+ default: ({ children }: any) => {children}
,
+}));
+
+jest.mock('../../../utils/icons', () => ({
+ HelmCatalogIcon: () => null,
+}));
+
+jest.mock('../HelmReleaseListRow', () => ({
+ getDataViewRows: jest.fn(() => []),
+ tableColumnInfo: [
+ { id: 'name' },
+ { id: 'namespace' },
+ { id: 'revision' },
+ { id: 'updated' },
+ { id: 'status' },
+ { id: 'chart-name' },
+ { id: 'chart-version' },
+ { id: 'app-version' },
+ { id: 'actions' },
+ ],
+}));
+
+jest.mock('@patternfly/react-data-view', () => ({
+ DataViewCheckboxFilter: () => null,
+}));
+
+jest.mock('../../../utils/helm-utils', () => ({
+ ...jest.requireActual('../../../utils/helm-utils'),
+ fetchHelmReleases: jest.fn(),
+}));
+
+const mockHelmRelease = {
+ name: 'test-release',
+ namespace: 'test-ns',
+ chart: {
+ files: [],
+ metadata: {
+ name: 'test-chart',
+ version: '1.0.0',
+ apiVersion: 'v2',
+ appVersion: '2.0.0',
+ urls: [],
+ },
+ templates: [],
+ values: {},
+ },
+ info: {
+ description: 'A test release',
+ deleted: '',
+ first_deployed: '2026-01-01T00:00:00Z',
+ last_deployed: '2026-01-01T00:00:00Z',
+ status: 'deployed',
+ notes: '',
+ },
+ version: 1,
+};
+
+describe('HelmReleaseList', () => {
+ beforeEach(() => {
+ mockUseParams.mockReturnValue({ ns: 'test-ns' });
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should render the document title and data view label as Helm Releases', () => {
+ mockUseK8sWatchResource.mockReturnValue([[], false, null]);
+
+ renderWithProviders();
+
+ expect(screen.getAllByText('Helm Releases')).toHaveLength(2);
+ expect(screen.getByTestId('data-view-label')).toHaveTextContent('Helm Releases');
+ });
+
+ it('should show ConsoleDataView in loading state when secrets are not loaded', () => {
+ mockUseK8sWatchResource.mockReturnValue([[], false, null]);
+
+ renderWithProviders();
+
+ expect(screen.getByText('Loading...')).toBeVisible();
+ });
+
+ it('should show empty state when no Helm releases exist', async () => {
+ mockUseK8sWatchResource.mockReturnValue([[], true, null]);
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ expect(screen.getByText('No Helm Releases found')).toBeVisible();
+ });
+ });
+
+ it('should show a link to browse the catalog in empty state', async () => {
+ mockUseK8sWatchResource.mockReturnValue([[], true, null]);
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Browse the catalog to discover available Helm Charts'),
+ ).toBeVisible();
+ });
+ });
+
+ it('should render ConsoleDataView with fetched releases when secrets exist', async () => {
+ const secretsData = [{ metadata: { name: 'helm-secret-1' } }];
+ mockUseK8sWatchResource.mockReturnValue([secretsData, true, null]);
+ (fetchHelmReleases as jest.Mock).mockResolvedValue([mockHelmRelease as any]);
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('data-count')).toHaveTextContent('1 releases');
+ });
+ });
+
+ it('should display load error from secrets watch', () => {
+ mockUseK8sWatchResource.mockReturnValue([[], true, 'Failed to load secrets']);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('load-error')).toHaveTextContent('Failed to load secrets');
+ });
+
+ it('should display error when fetchHelmReleases fails', async () => {
+ const secretsData = [{ metadata: { name: 'helm-secret-1' } }];
+ mockUseK8sWatchResource.mockReturnValue([secretsData, true, null]);
+ (fetchHelmReleases as jest.Mock).mockRejectedValue(new Error('Network failure'));
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('load-error')).toHaveTextContent('Network failure');
+ });
+ });
+
+ it('should use mock mode and show ConsoleDataView without empty state', () => {
+ mockUseK8sWatchResource.mockReturnValue([[], true, null]);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('console-data-view')).toBeVisible();
+ expect(screen.queryByText('No Helm Releases found')).not.toBeInTheDocument();
+ });
+});
diff --git a/frontend/packages/helm-plugin/src/components/list-page/__tests__/HelmTabbedPage.spec.tsx b/frontend/packages/helm-plugin/src/components/list-page/__tests__/HelmTabbedPage.spec.tsx
new file mode 100644
index 00000000000..87d158a782e
--- /dev/null
+++ b/frontend/packages/helm-plugin/src/components/list-page/__tests__/HelmTabbedPage.spec.tsx
@@ -0,0 +1,195 @@
+import { screen } from '@testing-library/react';
+import * as Router from 'react-router';
+import * as MultiTabListPageModule from '@console/shared/src/components/multi-tab-list/MultiTabListPage';
+import { useFlag } from '@console/shared/src/hooks/useFlag';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import HelmTabbedPage from '../HelmTabbedPage';
+
+const mockUseAccessReview = jest.fn();
+const mockUseActivePerspective = jest.fn();
+
+jest.mock('react-router', () => ({
+ ...jest.requireActual('react-router'),
+ useParams: jest.fn(),
+}));
+
+jest.mock('@console/dynamic-plugin-sdk/src', () => ({
+ useAccessReview: (...args: unknown[]) => mockUseAccessReview(...args),
+ useActivePerspective: () => mockUseActivePerspective(),
+}));
+
+jest.mock('@console/shared/src/hooks/useFlag', () => ({
+ useFlag: jest.fn(),
+}));
+
+jest.mock('@console/internal/components/start-guide', () => ({
+ withStartGuide: (Component: React.ComponentType) => Component,
+}));
+
+jest.mock('@console/dev-console/src/components/NamespacedPage', () => ({
+ __esModule: true,
+ default: ({ children }: { children: React.ReactNode }) => children,
+ NamespacedPageVariants: { light: 'light' },
+}));
+
+jest.mock('@console/dev-console/src/components/projects/CreateProjectListPage', () => ({
+ __esModule: true,
+ default: ({
+ title,
+ children,
+ }: {
+ title: string;
+ children: (fn: () => void) => React.ReactNode;
+ }) => (
+
+ {title}
+ {typeof children === 'function' ? children(jest.fn()) : children}
+
+ ),
+ CreateAProjectButton: () => null,
+}));
+
+jest.mock('@console/shared/src/components/multi-tab-list/MultiTabListPage', () => ({
+ MultiTabListPage: jest.fn(({ title }: { title: string }) => (
+
+ {title}
+
+ )),
+}));
+
+jest.mock('@console/internal/components/utils', () => ({
+ LoadingBox: () => Loading...
,
+}));
+
+jest.mock('../HelmReleaseList', () => ({
+ __esModule: true,
+ default: 'HelmReleaseList',
+}));
+
+jest.mock('../HelmReleaseListPage', () => ({
+ __esModule: true,
+ default: () => Helm Release List Page
,
+}));
+
+jest.mock('../RepositoriesListPage', () => ({
+ __esModule: true,
+ default: 'RepositoriesPage',
+}));
+
+jest.mock('../../../models/helm', () => ({
+ HelmChartRepositoryModel: {
+ apiGroup: 'helm.openshift.io',
+ plural: 'helmchartrepositories',
+ },
+ ProjectHelmChartRepositoryModel: {
+ apiGroup: 'helm.openshift.io',
+ plural: 'projecthelmchartrepositories',
+ },
+}));
+
+const useParamsMock = Router.useParams as jest.Mock;
+const useFlagMock = useFlag as jest.Mock;
+const mockMultiTabListPage = MultiTabListPageModule.MultiTabListPage as jest.Mock;
+
+/** Helper: configure all 6 useAccessReview calls to return the same tuple. */
+const setAllAccessReviews = (allowed: boolean, loading: boolean) => {
+ mockUseAccessReview.mockReturnValue([allowed, loading]);
+};
+
+describe('HelmTabbedPage', () => {
+ beforeEach(() => {
+ useParamsMock.mockReturnValue({ ns: 'test-ns' });
+ useFlagMock.mockReturnValue(true);
+ mockUseActivePerspective.mockReturnValue(['dev']);
+ mockMultiTabListPage.mockClear();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should show LoadingBox when access reviews are loading', () => {
+ setAllAccessReviews(false, true);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('loading-box')).toBeVisible();
+ expect(screen.getByText('Loading...')).toBeVisible();
+ });
+
+ it('should show MultiTabListPage with "Helm" title when user has full access', () => {
+ setAllAccessReviews(true, false);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('multi-tab-list-page')).toBeVisible();
+ expect(screen.getByText('Helm')).toBeVisible();
+ });
+
+ it('should show HelmReleaseListPage when user has no repository access', () => {
+ setAllAccessReviews(false, false);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('helm-release-list-page')).toBeVisible();
+ expect(screen.queryByTestId('multi-tab-list-page')).not.toBeInTheDocument();
+ });
+
+ it('should pass "Helm Releases" and "Repositories" pages to MultiTabListPage', () => {
+ setAllAccessReviews(true, false);
+
+ renderWithProviders();
+
+ const callArgs = mockMultiTabListPage.mock.calls[0][0];
+ expect(callArgs.pages).toHaveLength(2);
+ expect(callArgs.pages[0].nameKey).toBe('helm-plugin~Helm Releases');
+ expect(callArgs.pages[1].nameKey).toBe('helm-plugin~Repositories');
+ });
+
+ it('should show CreateProjectListPage when no namespace and dev perspective', () => {
+ useParamsMock.mockReturnValue({});
+ mockUseActivePerspective.mockReturnValue(['dev']);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('create-project-list-page')).toBeVisible();
+ expect(screen.queryByTestId('multi-tab-list-page')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('loading-box')).not.toBeInTheDocument();
+ });
+
+ it('should show HelmPage when admin perspective even without namespace', () => {
+ useParamsMock.mockReturnValue({});
+ mockUseActivePerspective.mockReturnValue(['admin']);
+ setAllAccessReviews(true, false);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('multi-tab-list-page')).toBeVisible();
+ expect(screen.queryByTestId('create-project-list-page')).not.toBeInTheDocument();
+ });
+
+ it('should pass correct telemetryPrefix to MultiTabListPage', () => {
+ setAllAccessReviews(true, false);
+
+ renderWithProviders();
+
+ const callArgs = mockMultiTabListPage.mock.calls[0][0];
+ expect(callArgs.telemetryPrefix).toBe('Helm');
+ });
+
+ it('should pass menuActions with helmRelease, projectHelmChartRepository, and helmChartInstallation', () => {
+ setAllAccessReviews(true, false);
+
+ renderWithProviders();
+
+ const callArgs = mockMultiTabListPage.mock.calls[0][0];
+ const actionKeys = Object.keys(callArgs.menuActions);
+ expect(actionKeys).toEqual(
+ expect.arrayContaining([
+ 'helmRelease',
+ 'projectHelmChartRepository',
+ 'helmChartInstallation',
+ ]),
+ );
+ });
+});
diff --git a/frontend/packages/helm-plugin/src/components/list-page/__tests__/RepositoriesList.spec.tsx b/frontend/packages/helm-plugin/src/components/list-page/__tests__/RepositoriesList.spec.tsx
new file mode 100644
index 00000000000..ad795bea294
--- /dev/null
+++ b/frontend/packages/helm-plugin/src/components/list-page/__tests__/RepositoriesList.spec.tsx
@@ -0,0 +1,125 @@
+import { screen } from '@testing-library/react';
+import type { K8sResourceKind } from '@console/internal/module/k8s';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import RepositoriesList from '../RepositoriesList';
+
+const mockConsoleDataView = jest.fn();
+jest.mock('@console/app/src/components/data-view/ConsoleDataView', () => ({
+ ConsoleDataView: (props: Record) => {
+ mockConsoleDataView(props);
+ return props.label as string;
+ },
+}));
+
+jest.mock('@console/internal/components/utils', () => ({
+ LoadingBox: () => 'LoadingBox',
+}));
+
+const mockResetAllColumnWidths = jest.fn();
+const mockColumns = [
+ { id: 'name', title: 'Name' },
+ { id: 'repoUrl', title: 'Repo URL' },
+];
+
+jest.mock('../RepositoriesHeader', () => ({
+ useRepositoriesColumns: () => ({
+ columns: mockColumns,
+ resetAllColumnWidths: mockResetAllColumnWidths,
+ }),
+}));
+
+jest.mock('../RepositoriesRow', () => ({
+ getDataViewRows: jest.fn(),
+}));
+
+jest.mock('../../../models/helm', () => ({
+ HelmRepositoriesCombinedListModel: {
+ apiGroup: 'console.ui',
+ apiVersion: 'v1',
+ kind: 'HelmRepositoriesCombinedList',
+ id: 'helmrepositoriescombinedlist',
+ plural: 'helmrepositoriescombinedlists',
+ label: 'Helm Chart Repositories',
+ labelPlural: 'Helm Chart Repositories',
+ abbr: 'HCRL',
+ namespaced: false,
+ crd: true,
+ },
+}));
+
+const mockData: K8sResourceKind[] = [
+ {
+ apiVersion: 'helm.openshift.io/v1beta1',
+ kind: 'HelmChartRepository',
+ metadata: { name: 'repo-1', namespace: 'default' },
+ spec: { name: 'Test Repo 1' },
+ },
+ {
+ apiVersion: 'helm.openshift.io/v1beta1',
+ kind: 'HelmChartRepository',
+ metadata: { name: 'repo-2', namespace: 'test-ns' },
+ spec: { name: 'Test Repo 2' },
+ },
+];
+
+describe('RepositoriesList', () => {
+ beforeEach(() => {
+ mockConsoleDataView.mockClear();
+ mockResetAllColumnWidths.mockClear();
+ });
+
+ it('should render ConsoleDataView with HelmChartRepositories label', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('HelmChartRepositories')).toBeInTheDocument();
+ expect(mockConsoleDataView).toHaveBeenCalledWith(
+ expect.objectContaining({ label: 'HelmChartRepositories' }),
+ );
+ });
+
+ it('should pass data and loaded props to ConsoleDataView', () => {
+ renderWithProviders();
+
+ expect(mockConsoleDataView).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: mockData,
+ loaded: true,
+ }),
+ );
+ });
+
+ it('should pass the correct data count when loaded with data', () => {
+ renderWithProviders();
+
+ const calledProps = mockConsoleDataView.mock.calls[0][0];
+ expect(calledProps.data).toHaveLength(2);
+ expect(calledProps.loaded).toBe(true);
+ });
+
+ it('should pass loaded as false when not loaded', () => {
+ renderWithProviders();
+
+ const calledProps = mockConsoleDataView.mock.calls[0][0];
+ expect(calledProps.loaded).toBe(false);
+ });
+
+ it('should pass loadError to ConsoleDataView when loadError is set', () => {
+ const loadError = 'Failed to fetch repositories';
+ renderWithProviders();
+
+ expect(mockConsoleDataView).toHaveBeenCalledWith(
+ expect.objectContaining({ loadError: 'Failed to fetch repositories' }),
+ );
+ });
+
+ it('should pass columns and resetAllColumnWidths from useRepositoriesColumns', () => {
+ renderWithProviders();
+
+ expect(mockConsoleDataView).toHaveBeenCalledWith(
+ expect.objectContaining({
+ columns: mockColumns,
+ resetAllColumnWidths: mockResetAllColumnWidths,
+ }),
+ );
+ });
+});