From c27c29ac2d1874c5d590cd488a9d5317da0b9556 Mon Sep 17 00:00:00 2001 From: sowmya-sl Date: Wed, 5 Aug 2026 15:20:17 +0530 Subject: [PATCH] Add RTL unit tests for 9 critical Helm plugin components Adds React Testing Library tests covering user-facing behavior for HelmInstallUpgradeForm, HelmReleaseList, HelmReleaseRollbackForm, HelmTabbedPage, HelmURLChartForm, CreateHelmChartRepositoryForm, HelmChartVersionDropdown, HelmReleaseHistory, and RepositoriesList (79 new tests total) as part of the Helm testing SDLC Goal 3. Co-Authored-By: Claude Opus 4.6 --- .../__tests__/HelmReleaseHistory.spec.tsx | 169 ++++++++++ .../CreateHelmChartRepositoryForm.spec.tsx | 204 ++++++++++++ .../HelmChartVersionDropdown.spec.tsx | 182 +++++++++++ .../__tests__/HelmInstallUpgradeForm.spec.tsx | 290 ++++++++++++++++++ .../HelmReleaseRollbackForm.spec.tsx | 221 +++++++++++++ .../__tests__/HelmURLChartForm.spec.tsx | 194 ++++++++++++ .../__tests__/HelmReleaseList.spec.tsx | 200 ++++++++++++ .../__tests__/HelmTabbedPage.spec.tsx | 195 ++++++++++++ .../__tests__/RepositoriesList.spec.tsx | 125 ++++++++ 9 files changed, 1780 insertions(+) create mode 100644 frontend/packages/helm-plugin/src/components/details-page/history/__tests__/HelmReleaseHistory.spec.tsx create mode 100644 frontend/packages/helm-plugin/src/components/forms/HelmChartRepository/__tests__/CreateHelmChartRepositoryForm.spec.tsx create mode 100644 frontend/packages/helm-plugin/src/components/forms/install-upgrade/__tests__/HelmChartVersionDropdown.spec.tsx create mode 100644 frontend/packages/helm-plugin/src/components/forms/install-upgrade/__tests__/HelmInstallUpgradeForm.spec.tsx create mode 100644 frontend/packages/helm-plugin/src/components/forms/rollback/__tests__/HelmReleaseRollbackForm.spec.tsx create mode 100644 frontend/packages/helm-plugin/src/components/forms/url-chart/__tests__/HelmURLChartForm.spec.tsx create mode 100644 frontend/packages/helm-plugin/src/components/list-page/__tests__/HelmReleaseList.spec.tsx create mode 100644 frontend/packages/helm-plugin/src/components/list-page/__tests__/HelmTabbedPage.spec.tsx create mode 100644 frontend/packages/helm-plugin/src/components/list-page/__tests__/RepositoriesList.spec.tsx 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) => ( +
+ {children} +
+ ), +})); + +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) => ( +
+ {children} +
+ ), +})); + +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}
+
+ ), +})); + +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) => ( +
+

{title}

+
{helpText}
+
+ ), +})); + +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) => ( +
+ {children} +
+ ), +})); + +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, + }), + ); + }); +});