diff --git a/frontend/packages/operator-lifecycle-manager/src/components/__tests__/catalog-source.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/catalog-source.spec.tsx
new file mode 100644
index 00000000000..a470f94adcf
--- /dev/null
+++ b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/catalog-source.spec.tsx
@@ -0,0 +1,219 @@
+import * as React from 'react';
+import { screen } from '@testing-library/react';
+import * as _ from 'lodash';
+import * as Router from 'react-router-dom-v5-compat';
+import { DetailsPage } from '@console/internal/components/factory';
+import { Firehose } from '@console/internal/components/utils';
+import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
+import { referenceForModel } from '@console/internal/module/k8s';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { testCatalogSource, testPackageManifest, dummyPackageManifest } from '../../../mocks';
+import { CatalogSourceModel, PackageManifestModel } from '../../models';
+import {
+ CatalogSourceDetails,
+ CatalogSourceDetailsPage,
+ CreateSubscriptionYAML,
+ CatalogSourceOperatorsPage,
+} from '../catalog-source';
+
+jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({
+ useK8sWatchResource: jest.fn(),
+}));
+
+jest.mock('react-router-dom-v5-compat', () => ({
+ ...jest.requireActual('react-router-dom-v5-compat'),
+ useParams: jest.fn(),
+ useLocation: jest.fn(),
+}));
+
+jest.mock('@console/internal/components/factory', () => ({
+ DetailsPage: jest.fn(() => null),
+ Table: jest.fn(() => null),
+ TableData: jest.fn(({ children }) => children),
+ MultiListPage: jest.fn(() => null),
+}));
+
+jest.mock('@console/internal/components/utils', () => ({
+ ...jest.requireActual('@console/internal/components/utils'),
+ Firehose: jest.fn(({ children }) => {
+ const props = { packageManifest: { loaded: false } };
+ return typeof children === 'function' ? children(props) : children;
+ }),
+ LoadingBox: jest.fn(() => 'Loading...'),
+ ResourceSummary: jest.fn(() => null),
+ SectionHeading: jest.fn(() => null),
+ DetailsItem: jest.fn(({ obj, path, children }) => {
+ if (children) return children;
+ if (path) {
+ const value = path.split('.').reduce((acc, key) => acc?.[key], obj);
+ return value || null;
+ }
+ return null;
+ }),
+}));
+
+jest.mock('@console/internal/components/create-yaml', () => ({
+ CreateYAML: jest.fn(({ template }) => template),
+}));
+
+jest.mock('../operator-group', () => ({
+ requireOperatorGroup: jest.fn((component) => component),
+}));
+
+jest.mock('@console/shared/src/components/error', () => ({
+ ErrorBoundary: jest.fn(({ children }) => children),
+ withFallback: jest.fn((successComponent) => (props) => successComponent(props)),
+}));
+
+jest.mock('../package-manifest', () => ({
+ PackageManifestsPage: jest.fn(() => null),
+}));
+
+jest.mock('../registry-poll-interval-details', () => ({
+ RegistryPollIntervalDetailItem: jest.fn(() => null),
+}));
+
+jest.mock('@console/shared/src/components/layout/PaneBody', () => ({
+ __esModule: true,
+ default: jest.fn(({ children }) => children),
+}));
+
+jest.mock('@patternfly/react-core', () => ({
+ ...jest.requireActual('@patternfly/react-core'),
+ Grid: jest.fn(({ children }) => children),
+ GridItem: jest.fn(({ children }) => children),
+ DescriptionList: jest.fn(({ children }) => children),
+}));
+
+const mockDetailsPage = (DetailsPage as unknown) as jest.Mock;
+const mockFirehose = (Firehose as unknown) as jest.Mock;
+const mockUseK8sWatchResource = useK8sWatchResource as jest.Mock;
+
+describe('CatalogSourceDetails', () => {
+ let obj;
+
+ beforeEach(() => {
+ obj = _.cloneDeep(testCatalogSource);
+ });
+
+ it('displays catalog source name and publisher', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(obj.spec.displayName, { exact: false })).toBeVisible();
+ expect(screen.getByText(obj.spec.publisher, { exact: false })).toBeVisible();
+ });
+});
+
+describe('CatalogSourceDetailsPage', () => {
+ beforeEach(() => {
+ mockUseK8sWatchResource.mockReturnValue([dummyPackageManifest, true, null]);
+ jest.spyOn(Router, 'useParams').mockReturnValue({ ns: 'default', name: 'some-catalog' });
+ mockDetailsPage.mockClear();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders catalog source details page without errors', () => {
+ expect(() => {
+ renderWithProviders();
+ }).not.toThrow();
+ });
+
+ // TODO: Refactor to test user behavior instead of implementation details
+ it('configures DetailsPage with correct navigation and resources', () => {
+ renderWithProviders();
+
+ expect(mockDetailsPage).toHaveBeenCalledTimes(1);
+ const [detailsPageProps] = mockDetailsPage.mock.calls[0];
+
+ expect(detailsPageProps.kind).toEqual(referenceForModel(CatalogSourceModel));
+
+ expect(detailsPageProps.pages).toHaveLength(3);
+ expect(detailsPageProps.pages[0]).toMatchObject({
+ nameKey: 'public~Details',
+ component: CatalogSourceDetails,
+ });
+ expect(detailsPageProps.pages[1]).toMatchObject({
+ nameKey: 'public~YAML',
+ });
+ expect(detailsPageProps.pages[2]).toMatchObject({
+ nameKey: 'olm~Operators',
+ component: CatalogSourceOperatorsPage,
+ });
+
+ expect(detailsPageProps.resources).toEqual([
+ {
+ kind: referenceForModel(PackageManifestModel),
+ isList: true,
+ prop: 'packageManifests',
+ namespace: 'default',
+ },
+ ]);
+ });
+});
+
+describe('CreateSubscriptionYAML', () => {
+ beforeEach(() => {
+ jest.spyOn(Router, 'useParams').mockReturnValue({
+ ns: 'default',
+ pkgName: testPackageManifest.metadata.name,
+ });
+ jest.spyOn(Router, 'useLocation').mockReturnValue({
+ ...window.location,
+ search: `?pkg=${testPackageManifest.metadata.name}&catalog=ocs&catalogNamespace=default`,
+ });
+ mockFirehose.mockClear();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('displays package name in the subscription YAML when loaded', () => {
+ mockFirehose.mockImplementationOnce((firehoseProps) => {
+ const childElement = firehoseProps.children;
+ return React.cloneElement(childElement, {
+ packageManifest: { loaded: true, data: testPackageManifest },
+ operatorGroup: { loaded: true, data: [] },
+ });
+ });
+
+ renderWithProviders();
+
+ expect(screen.getByText(new RegExp(testPackageManifest.metadata.name))).toBeVisible();
+ });
+
+ it('displays loading indicator when package manifest is not yet loaded', () => {
+ mockFirehose.mockImplementationOnce((firehoseProps) => {
+ const childElement = firehoseProps.children;
+ return React.cloneElement(childElement, {
+ packageManifest: { loaded: false },
+ operatorGroup: { loaded: false },
+ });
+ });
+
+ renderWithProviders();
+
+ expect(screen.getByText('Loading...')).toBeVisible();
+ });
+
+ it('displays subscription YAML with default channel information', () => {
+ mockFirehose.mockImplementationOnce((firehoseProps) => {
+ const childElement = firehoseProps.children;
+ return React.cloneElement(childElement, {
+ packageManifest: { loaded: true, data: testPackageManifest },
+ operatorGroup: { loaded: true, data: [] },
+ });
+ });
+
+ renderWithProviders();
+
+ expect(screen.getByText(/channel:\s*alpha/)).toBeInTheDocument();
+ expect(screen.getByText(/source:\s*ocs/)).toBeInTheDocument();
+ expect(screen.getByText(/startingCSV:\s*testapp/)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/__tests__/clusterserviceversion.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/clusterserviceversion.spec.tsx
new file mode 100644
index 00000000000..8b89107e99f
--- /dev/null
+++ b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/clusterserviceversion.spec.tsx
@@ -0,0 +1,356 @@
+import { screen } from '@testing-library/react';
+import * as _ from 'lodash';
+import operatorLogo from '@console/internal/imgs/operator.svg';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import {
+ testClusterServiceVersion,
+ testSubscription,
+ testPackageManifest,
+ testInstallPlan,
+ testModel,
+ testSubscriptions,
+} from '../../../mocks';
+import { ClusterServiceVersionPhase } from '../../types';
+import { ClusterServiceVersionLogo } from '../cluster-service-version-logo';
+import {
+ ClusterServiceVersionTableRow,
+ ClusterServiceVersionTableRowProps,
+ CRDCard,
+ CRDCardProps,
+ CSVSubscription,
+ CSVSubscriptionProps,
+} from '../clusterserviceversion';
+
+// Mock hooks
+jest.mock('@console/shared/src/hooks/useK8sModel', () => ({
+ useK8sModel: () => [testModel],
+}));
+
+jest.mock('@console/internal/components/utils/rbac', () => ({
+ useAccessReview: () => true,
+}));
+
+jest.mock('@console/dynamic-plugin-sdk', () => ({
+ ...jest.requireActual('@console/dynamic-plugin-sdk'),
+ useAccessReview: () => [true, false],
+ useAccessReviewAllowed: () => true,
+}));
+
+jest.mock('@console/shared/src/hooks/redux-selectors', () => ({
+ useActiveNamespace: jest.fn(),
+}));
+
+jest.mock('react-router-dom-v5-compat', () => ({
+ ...jest.requireActual('react-router-dom-v5-compat'),
+ useParams: jest.fn(),
+ useLocation: jest.fn(),
+}));
+
+jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({
+ useK8sWatchResource: jest.fn(() => [[], true, null]),
+}));
+
+jest.mock('../../utils/useClusterServiceVersion', () => ({
+ useClusterServiceVersion: jest.fn(() => [testClusterServiceVersion, true, null]),
+}));
+
+jest.mock('../../utils/useClusterServiceVersionPath', () => ({
+ useClusterServiceVersionPath: jest.fn(() => '/test-path'),
+}));
+
+jest.mock('@console/internal/components/utils', () => ({
+ ...jest.requireActual('@console/internal/components/utils'),
+ AsyncComponent: ({ children }) => children || null,
+}));
+
+jest.mock('@console/internal/components/conditions', () => ({
+ Conditions: () => 'Conditions',
+ ConditionTypes: { ClusterServiceVersion: 'ClusterServiceVersion' },
+}));
+
+jest.mock('@console/internal/components/events', () => ({
+ ResourceEventStream: () => 'ResourceEventStream',
+}));
+
+jest.mock('../operand', () => ({
+ ProvidedAPIsPage: () => 'ProvidedAPIsPage',
+ ProvidedAPIPage: () => 'ProvidedAPIPage',
+}));
+
+jest.mock('../subscription', () => ({
+ ...jest.requireActual('../subscription'),
+ SubscriptionDetails: () => 'SubscriptionDetails',
+ SubscriptionUpdates: () => 'SubscriptionUpdates',
+ catalogSourceForSubscription: jest.fn(),
+}));
+
+describe('ClusterServiceVersionTableRow', () => {
+ let clusterServiceVersionTableRowProps: ClusterServiceVersionTableRowProps;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ window.SERVER_FLAGS.copiedCSVsDisabled = false;
+
+ clusterServiceVersionTableRowProps = {
+ catalogSourceMissing: false,
+ obj: testClusterServiceVersion,
+ subscription: testSubscription,
+ };
+ });
+
+ it('renders component wrapped in ErrorBoundary', () => {
+ renderWithProviders(
+
,
+ );
+
+ expect(screen.getByText(testClusterServiceVersion.spec.displayName)).toBeVisible();
+ });
+
+ it('renders LazyActionMenu with correct context and variant', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('button', { name: 'Actions' })).toBeVisible();
+ });
+
+ it('renders clickable link with CSV logo and display name', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const link = screen.getByRole('link', {
+ name: new RegExp(testClusterServiceVersion.spec.displayName),
+ });
+ expect(link).toBeVisible();
+ });
+
+ it('renders managed namespace', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('link', { name: 'openshift-operators' })).toBeVisible();
+ });
+
+ it('renders last updated timestamp', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByTestId('timestamp')).toBeVisible();
+ });
+
+ it('renders status showing Succeeded phase', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(ClusterServiceVersionPhase.CSVPhaseSucceeded)).toBeVisible();
+ });
+
+ it('renders Deleting status when CSV has deletionTimestamp', () => {
+ const deletingCSV = _.cloneDeepWith(testClusterServiceVersion, (v, k) =>
+ k === 'metadata' ? { ...v, deletionTimestamp: Date.now() } : undefined,
+ );
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('Deleting')).toBeVisible();
+ });
+
+ it('renders links for each CRD provided by the Operator', () => {
+ renderWithProviders(
+ ,
+ );
+
+ testClusterServiceVersion.spec.customresourcedefinitions.owned.forEach((desc) => {
+ const crdLink = screen.getByRole('link', { name: new RegExp(desc.displayName || desc.kind) });
+ expect(crdLink).toBeVisible();
+ });
+ });
+});
+
+describe('ClusterServiceVersionLogo', () => {
+ it('renders logo image from base64 encoded string', () => {
+ const { provider, icon, displayName } = testClusterServiceVersion.spec;
+
+ renderWithProviders(
+ ,
+ );
+
+ const image = screen.getByRole('img');
+ expect(image).toHaveAttribute('src', `data:${icon[0].mediatype};base64,${icon[0].base64data}`);
+ });
+
+ it('renders fallback image when icon is invalid', () => {
+ const { provider, displayName } = testClusterServiceVersion.spec;
+
+ renderWithProviders(
+ ,
+ );
+
+ const image = screen.getByRole('img');
+ expect(image).toHaveAttribute('src', operatorLogo);
+ });
+
+ it('renders CSV display name and provider name', () => {
+ const { provider, icon, displayName } = testClusterServiceVersion.spec;
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(displayName)).toBeVisible();
+ expect(screen.getByText(new RegExp(provider.name))).toBeVisible();
+ });
+});
+
+describe('CRDCard', () => {
+ let crdCardProps: CRDCardProps;
+ const crd = testClusterServiceVersion.spec.customresourcedefinitions.owned[0];
+
+ beforeEach(() => {
+ crdCardProps = {
+ canCreate: false,
+ crd,
+ csv: testClusterServiceVersion,
+ };
+ });
+
+ it('does not render create link when canCreate is false', () => {
+ renderWithProviders();
+
+ expect(screen.queryByRole('link', { name: 'Create instance' })).not.toBeInTheDocument();
+ });
+});
+
+describe('CSVSubscription', () => {
+ let csvSubscriptionProps: CSVSubscriptionProps;
+
+ beforeEach(() => {
+ csvSubscriptionProps = {
+ obj: testClusterServiceVersion,
+ customData: {
+ subscriptions: testSubscriptions,
+ subscription: undefined,
+ subscriptionsLoaded: true,
+ },
+ packageManifests: [],
+ installPlans: [],
+ };
+ });
+
+ it('renders StatusBox with EmptyMsg when subscription does not exist', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('No Operator Subscription')).toBeVisible();
+ expect(screen.getByText('This Operator will not receive updates.')).toBeVisible();
+ });
+
+ it('renders SubscriptionDetails when subscription exists', () => {
+ const obj = _.set(_.cloneDeep(testClusterServiceVersion), 'metadata.annotations', {
+ 'olm.operatorNamespace': 'default',
+ });
+ const subscription = _.set(_.cloneDeep(testSubscription), 'status', {
+ installedCSV: obj.metadata.name,
+ });
+
+ renderWithProviders(
+ ,
+ );
+ expect(screen.queryByText('No Operator Subscription')).not.toBeInTheDocument();
+ });
+
+ it('passes matching PackageManifest when multiple exist with same name', () => {
+ const obj = _.set(_.cloneDeep(testClusterServiceVersion), 'metadata.annotations', {
+ 'olm.operatorNamespace': 'default',
+ });
+ const subscription = _.set(_.cloneDeep(testSubscription), 'status', {
+ installedCSV: obj.metadata.name,
+ });
+ const otherPkg = _.set(
+ _.cloneDeep(testPackageManifest),
+ 'status.catalogSource',
+ 'other-source',
+ );
+
+ renderWithProviders(
+ ,
+ );
+ expect(screen.queryByText('No Operator Subscription')).not.toBeInTheDocument();
+ });
+});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/__tests__/install-plan.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/install-plan.spec.tsx
new file mode 100644
index 00000000000..7ce08abb903
--- /dev/null
+++ b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/install-plan.spec.tsx
@@ -0,0 +1,426 @@
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import * as _ from 'lodash';
+import * as Router from 'react-router-dom-v5-compat';
+import * as k8sResourceModule from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-resource';
+import { Table, MultiListPage, DetailsPage } from '@console/internal/components/factory';
+import { useAccessReview } from '@console/internal/components/utils';
+import { referenceForModel } from '@console/internal/module/k8s';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { testInstallPlan } from '../../../mocks';
+import { InstallPlanModel, ClusterServiceVersionModel, OperatorGroupModel } from '../../models';
+import { InstallPlanKind, InstallPlanApproval } from '../../types';
+import {
+ InstallPlanTableRow,
+ InstallPlansList,
+ InstallPlansPage,
+ InstallPlanDetailsPage,
+ InstallPlanPreview,
+ InstallPlanDetails,
+} from '../install-plan';
+
+jest.mock('react-router-dom-v5-compat', () => ({
+ ...jest.requireActual('react-router-dom-v5-compat'),
+ useParams: jest.fn(),
+}));
+
+jest.mock('@console/internal/components/utils/rbac', () => ({
+ useAccessReview: jest.fn(),
+ asAccessReview: jest.fn(() => ({})),
+}));
+
+jest.mock('@console/internal/components/factory', () => ({
+ ...jest.requireActual('@console/internal/components/factory'),
+ Table: jest.fn(() => null),
+ MultiListPage: jest.fn(() => null),
+ DetailsPage: jest.fn(() => null),
+}));
+
+const mockTable = Table as jest.Mock;
+const mockMultiListPage = MultiListPage as jest.Mock;
+const mockDetailsPage = DetailsPage as jest.Mock;
+const mockUseAccessReview = useAccessReview as jest.Mock;
+
+describe('InstallPlanTableRow', () => {
+ let installPlan: InstallPlanKind;
+ const columns = [];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ installPlan = _.cloneDeep(testInstallPlan);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders install plan name with correct resource link', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const installPlanLinks = screen.getAllByText(installPlan.metadata.name);
+ const installPlanLink = installPlanLinks.find((link) =>
+ link.getAttribute('href')?.includes('InstallPlan'),
+ );
+ expect(installPlanLink).toBeVisible();
+ });
+
+ it('renders install plan namespace', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(installPlan.metadata.namespace)).toBeVisible();
+ });
+
+ it('renders install plan status', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const statusElement = screen.getByTestId('status-text');
+ expect(statusElement).toHaveTextContent(installPlan.status.phase);
+ });
+
+ it('renders fallback status when status.phase is undefined', () => {
+ const installPlanWithoutStatus = { ...installPlan, status: null };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('Unknown')).toBeVisible();
+ });
+
+ it('renders CSV component name', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const csvName = installPlan.spec.clusterServiceVersionNames[0];
+ const csvLinks = screen.getAllByText(csvName);
+ const csvLink = csvLinks.find((link) =>
+ link.getAttribute('href')?.includes('ClusterServiceVersion'),
+ );
+ expect(csvLink).toBeVisible();
+ });
+});
+
+describe('InstallPlansList', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockTable.mockClear();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders Table component with correct header titles', () => {
+ renderWithProviders();
+
+ expect(mockTable).toHaveBeenCalledTimes(1);
+ const [tableProps] = mockTable.mock.calls[0];
+
+ const headers = tableProps.Header({});
+ const headerTitles = headers.map((header) => header.title);
+
+ expect(headerTitles).toEqual([
+ 'Name',
+ 'Namespace',
+ 'Status',
+ 'Components',
+ 'Subscriptions',
+ '',
+ ]);
+ });
+
+ it('provides custom empty message for table', () => {
+ renderWithProviders();
+
+ expect(mockTable).toHaveBeenCalledTimes(1);
+ const [tableProps] = mockTable.mock.calls[0];
+
+ expect(tableProps.EmptyMsg).toBeDefined();
+ });
+});
+
+describe('InstallPlansPage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.spyOn(Router, 'useParams').mockReturnValue({ ns: 'default' });
+ mockMultiListPage.mockClear();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders MultiListPage with correct configuration', () => {
+ renderWithProviders();
+
+ expect(mockMultiListPage).toHaveBeenCalledTimes(1);
+ const [multiListPageProps] = mockMultiListPage.mock.calls[0];
+
+ expect(multiListPageProps.title).toEqual('InstallPlans');
+ expect(multiListPageProps.showTitle).toBe(false);
+ expect(multiListPageProps.ListComponent).toEqual(InstallPlansList);
+ });
+
+ it('fetches InstallPlans and OperatorGroups from correct namespace', () => {
+ renderWithProviders();
+
+ expect(mockMultiListPage).toHaveBeenCalledTimes(1);
+ const [multiListPageProps] = mockMultiListPage.mock.calls[0];
+
+ expect(multiListPageProps.resources).toEqual([
+ {
+ kind: referenceForModel(InstallPlanModel),
+ namespace: 'default',
+ namespaced: true,
+ prop: 'installPlan',
+ },
+ {
+ kind: referenceForModel(OperatorGroupModel),
+ namespace: 'default',
+ namespaced: true,
+ prop: 'operatorGroup',
+ },
+ ]);
+ });
+});
+
+describe('InstallPlanPreview', () => {
+ let installPlan: InstallPlanKind;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseAccessReview.mockReturnValue(true);
+
+ installPlan = {
+ ...testInstallPlan,
+ status: {
+ ...testInstallPlan.status,
+ plan: [
+ {
+ resolving: 'testoperator.v1.0.0',
+ status: 'Created',
+ resource: {
+ group: ClusterServiceVersionModel.apiGroup,
+ version: ClusterServiceVersionModel.apiVersion,
+ kind: ClusterServiceVersionModel.kind,
+ name: 'testoperator.v1.0.0',
+ manifest: '',
+ },
+ },
+ {
+ resolving: 'testoperator.v1.0.0',
+ status: 'Unknown',
+ resource: {
+ group: 'apiextensions.k8s.io',
+ version: 'v1',
+ kind: 'CustomResourceDefinition',
+ name: 'test-crds.test.com',
+ manifest: '',
+ },
+ },
+ ],
+ },
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders empty message when status.plan is empty', () => {
+ const emptyPlan = { ...installPlan, status: { ...installPlan.status, plan: [] } };
+
+ renderWithProviders();
+
+ expect(screen.getByText(/no components resolved/i)).toBeVisible();
+ });
+
+ it('renders Approve button when install plan requires approval', () => {
+ const manualPlan = {
+ ...installPlan,
+ spec: {
+ ...installPlan.spec,
+ approval: InstallPlanApproval.Manual,
+ approved: false,
+ },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Approve' })).toBeVisible();
+ });
+
+ it('calls k8sPatch to approve install plan when Approve button is clicked', async () => {
+ const k8sPatchSpy = jest.spyOn(k8sResourceModule, 'k8sPatch').mockResolvedValue(installPlan);
+
+ const manualPlan = {
+ ...installPlan,
+ spec: {
+ ...installPlan.spec,
+ approval: InstallPlanApproval.Manual,
+ approved: false,
+ },
+ };
+
+ renderWithProviders();
+
+ const approveButton = screen.getByRole('button', { name: 'Approve' });
+ fireEvent.click(approveButton);
+
+ await waitFor(() => {
+ expect(k8sPatchSpy).toHaveBeenCalledWith(
+ InstallPlanModel,
+ manualPlan,
+ expect.arrayContaining([
+ expect.objectContaining({
+ op: 'replace',
+ path: '/spec/approved',
+ value: true,
+ }),
+ ]),
+ );
+ });
+ });
+
+ it('renders Deny button when install plan requires approval', () => {
+ const manualPlan = {
+ ...installPlan,
+ spec: {
+ ...installPlan.spec,
+ approval: InstallPlanApproval.Manual,
+ approved: false,
+ },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByRole('button', { name: 'Deny' })).toBeVisible();
+ });
+
+ it('renders component names from install plan', () => {
+ renderWithProviders();
+
+ const resourceName = installPlan.status.plan[0].resource.name;
+ const elements = screen.getAllByText(resourceName);
+ expect(elements.length).toBeGreaterThan(0);
+ expect(elements[0]).toBeVisible();
+ });
+
+ it('renders preview button for uncreated components', () => {
+ renderWithProviders();
+
+ const uncreatedStep = installPlan.status.plan.find((step) => step.status === 'Unknown');
+ expect(screen.getByRole('button', { name: uncreatedStep.resource.name })).toBeVisible();
+ });
+});
+
+describe('InstallPlanDetails', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders link to Components tab when install plan needs approval', () => {
+ mockUseAccessReview.mockReturnValue(true);
+ const manualPlan = {
+ ...testInstallPlan,
+ spec: {
+ ...testInstallPlan.spec,
+ approval: InstallPlanApproval.Manual,
+ approved: false,
+ },
+ };
+
+ renderWithProviders();
+
+ const previewButton = screen.getByRole('button', { name: 'Preview InstallPlan' });
+ expect(previewButton).toBeVisible();
+
+ const link = previewButton.closest('a');
+ expect(link).toHaveAttribute(
+ 'href',
+ `/k8s/ns/default/${referenceForModel(InstallPlanModel)}/${
+ testInstallPlan.metadata.name
+ }/components`,
+ );
+ });
+
+ it('does not render Components link when install plan is automatic', () => {
+ mockUseAccessReview.mockReturnValue(true);
+ const automaticPlan = {
+ ...testInstallPlan,
+ spec: {
+ ...testInstallPlan.spec,
+ approval: InstallPlanApproval.Automatic,
+ approved: true,
+ },
+ };
+
+ renderWithProviders();
+
+ expect(screen.queryByRole('button', { name: 'Preview InstallPlan' })).not.toBeInTheDocument();
+ });
+});
+
+describe('InstallPlanDetailsPage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest
+ .spyOn(Router, 'useParams')
+ .mockReturnValue({ ns: 'default', name: testInstallPlan.metadata.name });
+ mockDetailsPage.mockClear();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders DetailsPage with three navigation tabs', () => {
+ renderWithProviders();
+
+ expect(mockDetailsPage).toHaveBeenCalledTimes(1);
+ const [detailsPageProps] = mockDetailsPage.mock.calls[0];
+
+ const pageNames = detailsPageProps.pages.map((p) => p.name || p.nameKey);
+ expect(pageNames).toEqual(['public~Details', 'public~YAML', 'olm~Components']);
+ });
+});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operator-group.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/operator-group.spec.tsx
similarity index 79%
rename from frontend/packages/operator-lifecycle-manager/src/components/operator-group.spec.tsx
rename to frontend/packages/operator-lifecycle-manager/src/components/__tests__/operator-group.spec.tsx
index 2313d266a0a..b7f84ba556b 100644
--- a/frontend/packages/operator-lifecycle-manager/src/components/operator-group.spec.tsx
+++ b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/operator-group.spec.tsx
@@ -1,48 +1,51 @@
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
import * as _ from 'lodash';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
import {
testOperatorGroup,
testSubscription,
testPackageManifest,
dummyPackageManifest,
-} from '../../mocks';
-import { OperatorGroupKind, SubscriptionKind, InstallModeType } from '../types';
+} from '../../../mocks';
+import { OperatorGroupKind, SubscriptionKind, InstallModeType } from '../../types';
import {
requireOperatorGroup,
- NoOperatorGroupMsg,
supports,
InstallModeSet,
installedFor,
subscriptionFor,
-} from './operator-group';
+} from '../operator-group';
describe('requireOperatorGroup', () => {
const SomeComponent = () => Requires OperatorGroup
;
- it('renders given component if `OperatorGroups` has not loaded yet', () => {
+ it('renders given component if OperatorGroups has not loaded yet', () => {
const WrappedComponent = requireOperatorGroup(SomeComponent);
- const wrapper = shallow();
- expect(wrapper.find(SomeComponent).exists()).toBe(true);
- expect(wrapper.find(NoOperatorGroupMsg).exists()).toBe(false);
+ renderWithProviders();
+
+ expect(screen.getByText('Requires OperatorGroup')).toBeVisible();
+ expect(screen.queryByText('Namespace not enabled')).not.toBeInTheDocument();
});
- it('renders message if no `OperatorGroups` loaded', () => {
+ it('renders message if no OperatorGroups loaded', () => {
const WrappedComponent = requireOperatorGroup(SomeComponent);
- const wrapper = shallow();
- expect(wrapper.find(NoOperatorGroupMsg).exists()).toBe(true);
- expect(wrapper.find(SomeComponent).exists()).toBe(false);
+ renderWithProviders();
+
+ expect(screen.getByText('Namespace not enabled')).toBeVisible();
+ expect(screen.queryByText('Requires OperatorGroup')).not.toBeInTheDocument();
});
- it('renders given component if `OperatorGroups` loaded and present', () => {
+ it('renders given component if OperatorGroups loaded and present', () => {
const WrappedComponent = requireOperatorGroup(SomeComponent);
- const wrapper = shallow(
+
+ renderWithProviders(
,
);
- expect(wrapper.find(SomeComponent).exists()).toBe(true);
- expect(wrapper.find(NoOperatorGroupMsg).exists()).toBe(false);
+ expect(screen.getByText('Requires OperatorGroup')).toBeVisible();
+ expect(screen.queryByText('Namespace not enabled')).not.toBeInTheDocument();
});
});
@@ -57,7 +60,7 @@ describe('subscriptionFor', () => {
operatorGroups = [];
});
- it('returns nothing if no `Subscriptions` exist for the given package', () => {
+ it('returns nothing if no Subscriptions exist for the given package', () => {
subscriptions = [testSubscription];
operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }];
@@ -66,7 +69,7 @@ describe('subscriptionFor', () => {
).toBeUndefined();
});
- it('returns nothing if no `OperatorGroups` target the given namespace', () => {
+ it('returns nothing if no OperatorGroups target the given namespace', () => {
subscriptions = [testSubscription];
operatorGroups = [
{ ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: null } },
@@ -75,7 +78,7 @@ describe('subscriptionFor', () => {
expect(subscriptionFor(subscriptions)(operatorGroups)(pkg)(ns)).toBeUndefined();
});
- it('returns nothing if no `Subscriptions` share the package namespace', () => {
+ it('returns nothing if no Subscriptions share the package namespace', () => {
subscriptions = [testSubscription];
operatorGroups = [
{ ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: null } },
@@ -86,7 +89,7 @@ describe('subscriptionFor', () => {
).toBeUndefined();
});
- it('returns nothing if no `Subscriptions` share the package catalog source', () => {
+ it('returns nothing if no Subscriptions share the package catalog source', () => {
subscriptions = [testSubscription];
operatorGroups = [
{ ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: null } },
@@ -97,21 +100,21 @@ describe('subscriptionFor', () => {
).toBeUndefined();
});
- it('returns nothing if checking for `all-namespaces`', () => {
+ it('returns nothing if checking for all-namespaces', () => {
subscriptions = [testSubscription];
operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }];
expect(subscriptionFor(subscriptions)(operatorGroups)(pkg)('')).toBeUndefined();
});
- it('returns `Subscription` when it exists in the "global" `OperatorGroup`', () => {
+ it('returns Subscription when it exists in the global OperatorGroup', () => {
subscriptions = [testSubscription];
operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [''], lastUpdated: null } }];
expect(subscriptionFor(subscriptions)(operatorGroups)(pkg)(ns)).toEqual(testSubscription);
});
- it('returns `Subscription` when it exists in an `OperatorGroup` that targets given namespace', () => {
+ it('returns Subscription when it exists in an OperatorGroup that targets given namespace', () => {
subscriptions = [testSubscription];
operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }];
@@ -130,14 +133,14 @@ describe('installedFor', () => {
operatorGroups = [];
});
- it('returns false if no `Subscriptions` exist for the given package', () => {
+ it('returns false if no Subscriptions exist for the given package', () => {
subscriptions = [testSubscription];
operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }];
expect(installedFor(subscriptions)(operatorGroups)(dummyPackageManifest)(ns)).toBe(false);
});
- it('returns false if no `OperatorGroups` target the given namespace', () => {
+ it('returns false if no OperatorGroups target the given namespace', () => {
subscriptions = [testSubscription];
operatorGroups = [
{ ...testOperatorGroup, status: { namespaces: ['prod-a', 'prod-b'], lastUpdated: null } },
@@ -146,35 +149,35 @@ describe('installedFor', () => {
expect(installedFor(subscriptions)(operatorGroups)(pkg)(ns)).toBe(false);
});
- it('returns false if checking for `all-namespaces`', () => {
+ it('returns false if checking for all-namespaces', () => {
subscriptions = [testSubscription];
operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }];
expect(installedFor(subscriptions)(operatorGroups)(pkg)('')).toBe(false);
});
- it('returns false if `Subscription` is in a different namespace than the given package', () => {
+ it('returns false if Subscription is in a different namespace than the given package', () => {
subscriptions = [testSubscription];
operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }];
expect(installedFor(subscriptions)(operatorGroups)(dummyPackageManifest)(ns)).toBe(false);
});
- it('returns false if `Subscription` is from a different catalog source than the given package', () => {
+ it('returns false if Subscription is from a different catalog source than the given package', () => {
subscriptions = [testSubscription];
operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }];
expect(installedFor(subscriptions)(operatorGroups)(dummyPackageManifest)(ns)).toBe(false);
});
- it('returns true if `Subscription` exists in the "global" `OperatorGroup`', () => {
+ it('returns true if Subscription exists in the global OperatorGroup', () => {
subscriptions = [testSubscription];
operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [''], lastUpdated: null } }];
expect(installedFor(subscriptions)(operatorGroups)(pkg)(ns)).toBe(true);
});
- it('returns true if `Subscription` exists in an `OperatorGroup` that targets given namespace', () => {
+ it('returns true if Subscription exists in an OperatorGroup that targets given namespace', () => {
subscriptions = [testSubscription];
operatorGroups = [{ ...testOperatorGroup, status: { namespaces: [ns], lastUpdated: null } }];
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/__tests__/package-manifest.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/package-manifest.spec.tsx
new file mode 100644
index 00000000000..093213ccf63
--- /dev/null
+++ b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/package-manifest.spec.tsx
@@ -0,0 +1,158 @@
+import { screen } from '@testing-library/react';
+import * as UIActions from '@console/internal/actions/ui';
+import { ResourceLink } from '@console/internal/components/utils';
+import { Timestamp } from '@console/shared/src/components/datetime/Timestamp';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { testPackageManifest, testCatalogSource } from '../../../mocks';
+import { ClusterServiceVersionLogo } from '../cluster-service-version-logo';
+import {
+ PackageManifestTableRow,
+ PackageManifestTableHeader,
+ PackageManifestTableHeaderWithCatalogSource,
+} from '../package-manifest';
+
+jest.mock('../cluster-service-version-logo', () => ({
+ ClusterServiceVersionLogo: jest.fn(() => null),
+}));
+
+jest.mock('@console/shared/src/components/datetime/Timestamp', () => ({
+ Timestamp: jest.fn(() => null),
+}));
+
+jest.mock('@console/internal/components/utils', () => ({
+ ...jest.requireActual('@console/internal/components/utils'),
+ ResourceLink: jest.fn(() => null),
+}));
+
+const mockClusterServiceVersionLogo = ClusterServiceVersionLogo as jest.Mock;
+const mockTimestamp = Timestamp as jest.Mock;
+const mockResourceLink = ResourceLink as jest.Mock;
+
+describe('PackageManifestTableHeader', () => {
+ it('renders column header for package name', () => {
+ const headers = PackageManifestTableHeader();
+ expect(headers[0].title).toEqual('Name');
+ });
+
+ it('renders column header for latest CSV version for package in catalog', () => {
+ const headers = PackageManifestTableHeader();
+ expect(headers[1].title).toEqual('Latest version');
+ });
+
+ it('renders column header for creation timestamp', () => {
+ const headers = PackageManifestTableHeader();
+ expect(headers[2].title).toEqual('Created');
+ });
+});
+
+describe('PackageManifestTableHeaderWithCatalogSource', () => {
+ it('renders column header for catalog source', () => {
+ const headers = PackageManifestTableHeaderWithCatalogSource();
+ expect(headers[3].title).toEqual('CatalogSource');
+ });
+});
+
+describe('PackageManifestTableRow', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.spyOn(UIActions, 'getActiveNamespace').mockReturnValue('default');
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders column for package name and logo', () => {
+ const columns: any[] = [];
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(mockClusterServiceVersionLogo).toHaveBeenCalledTimes(1);
+ const [logoProps] = mockClusterServiceVersionLogo.mock.calls[0];
+ expect(logoProps.displayName).toEqual(
+ testPackageManifest.status.channels[0].currentCSVDesc.displayName,
+ );
+ });
+
+ it('renders column for latest CSV version for package in catalog', () => {
+ const columns: any[] = [];
+ const {
+ name,
+ currentCSVDesc: { version },
+ } = testPackageManifest.status.channels[0];
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(`${version} (${name})`)).toBeVisible();
+ });
+
+ it('renders column for creation timestamp', () => {
+ const columns: any[] = [];
+ const pkgManifestCreationTimestamp = testPackageManifest.metadata.creationTimestamp;
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(mockTimestamp).toHaveBeenCalledTimes(1);
+ const [timestampProps] = mockTimestamp.mock.calls[0];
+ expect(timestampProps.timestamp).toEqual(pkgManifestCreationTimestamp);
+ });
+
+ it('renders column for catalog source for a package when no catalog source is defined', () => {
+ const catalogSourceName = testPackageManifest.status.catalogSource;
+ const columns: any[] = [];
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(mockResourceLink).toHaveBeenCalledTimes(1);
+ const [resourceLinkProps] = mockResourceLink.mock.calls[0];
+ expect(resourceLinkProps.name).toEqual(catalogSourceName);
+ });
+});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/__tests__/subscription.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/subscription.spec.tsx
new file mode 100644
index 00000000000..e7fcd0134ab
--- /dev/null
+++ b/frontend/packages/operator-lifecycle-manager/src/components/__tests__/subscription.spec.tsx
@@ -0,0 +1,353 @@
+import { screen } from '@testing-library/react';
+import * as _ from 'lodash';
+import * as Router from 'react-router-dom-v5-compat';
+import { Table, MultiListPage, DetailsPage } from '@console/internal/components/factory';
+import { ResourceLink } from '@console/internal/components/utils';
+import { referenceForModel } from '@console/internal/module/k8s';
+import { LazyActionMenu } from '@console/shared/src';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import {
+ testSubscription,
+ testSubscriptions,
+ testClusterServiceVersion,
+ testPackageManifest,
+} from '../../../mocks';
+import {
+ SubscriptionModel,
+ ClusterServiceVersionModel,
+ PackageManifestModel,
+ OperatorGroupModel,
+ InstallPlanModel,
+} from '../../models';
+import { SubscriptionState } from '../../types';
+import {
+ SubscriptionTableRow,
+ SubscriptionsList,
+ SubscriptionsPage,
+ SubscriptionDetails,
+ SubscriptionDetailsPage,
+ SubscriptionStatus,
+} from '../subscription';
+
+jest.mock('react-router-dom-v5-compat', () => ({
+ ...jest.requireActual('react-router-dom-v5-compat'),
+ useParams: jest.fn(),
+}));
+
+jest.mock('@console/internal/components/utils', () => ({
+ ...jest.requireActual('@console/internal/components/utils'),
+ ResourceLink: jest.fn(() => null),
+}));
+
+jest.mock('@console/shared/src', () => ({
+ ...jest.requireActual('@console/shared/src'),
+ LazyActionMenu: jest.fn(() => null),
+}));
+
+jest.mock('@console/internal/components/factory', () => ({
+ ...jest.requireActual('@console/internal/components/factory'),
+ Table: jest.fn(() => null),
+ MultiListPage: jest.fn(() => null),
+ DetailsPage: jest.fn(() => null),
+}));
+
+jest.mock('@console/internal/components/utils/details-page', () => ({
+ ...jest.requireActual('@console/internal/components/utils/details-page'),
+ ResourceSummary: jest.fn(() => null),
+}));
+
+jest.mock('@console/internal/components/conditions', () => ({
+ Conditions: jest.fn(() => null),
+}));
+
+const mockResourceLink = ResourceLink as jest.Mock;
+const mockLazyActionMenu = LazyActionMenu as jest.Mock;
+const mockTable = Table as jest.Mock;
+const mockMultiListPage = MultiListPage as jest.Mock;
+const mockDetailsPage = DetailsPage as jest.Mock;
+
+describe('SubscriptionTableRow', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders subscription name and namespace resource links', () => {
+ const subscription = {
+ ...testSubscription,
+ status: { installedCSV: 'testapp.v1.0.0' },
+ };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(mockResourceLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ kind: referenceForModel(SubscriptionModel),
+ name: subscription.metadata.name,
+ namespace: subscription.metadata.namespace,
+ }),
+ expect.anything(),
+ );
+
+ expect(mockResourceLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ kind: 'Namespace',
+ name: subscription.metadata.namespace,
+ }),
+ expect.anything(),
+ );
+ });
+
+ it('renders action menu with subscription context', () => {
+ const subscription = {
+ ...testSubscription,
+ status: { installedCSV: 'testapp.v1.0.0' },
+ };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(mockLazyActionMenu).toHaveBeenCalledTimes(1);
+ const [actionMenuProps] = mockLazyActionMenu.mock.calls[0];
+ expect(actionMenuProps.context).toEqual({
+ [referenceForModel(SubscriptionModel)]: subscription,
+ });
+ });
+
+ it('renders channel and approval strategy text', () => {
+ const subscription = {
+ ...testSubscription,
+ status: { installedCSV: 'testapp.v1.0.0' },
+ };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(subscription.spec.channel)).toBeVisible();
+ expect(screen.getByText('Automatic')).toBeVisible();
+ });
+});
+
+describe('SubscriptionStatus', () => {
+ it('renders "Upgrade available" when update is available', () => {
+ const subscription = {
+ ...testSubscription,
+ status: { state: SubscriptionState.SubscriptionStateUpgradeAvailable },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByText('Upgrade available')).toBeVisible();
+ });
+
+ it('renders "Unknown failure" when status is unknown', () => {
+ const subscription = {
+ ...testSubscription,
+ status: {},
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByText('Unknown failure')).toBeVisible();
+ });
+
+ it('renders "Upgrading" when update is pending', () => {
+ const subscription = {
+ ...testSubscription,
+ status: { state: SubscriptionState.SubscriptionStateUpgradePending },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByText('Upgrading')).toBeVisible();
+ });
+
+ it('renders "Up to date" when subscription is at latest', () => {
+ const subscription = {
+ ...testSubscription,
+ status: { state: SubscriptionState.SubscriptionStateAtLatest },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByText('Up to date')).toBeVisible();
+ });
+});
+
+describe('SubscriptionsList', () => {
+ it('renders table with correct header titles', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(mockTable).toHaveBeenCalledTimes(1);
+ const [tableProps] = mockTable.mock.calls[0];
+ const headerTitles = tableProps.Header().map((header) => header.title);
+
+ expect(headerTitles).toEqual([
+ 'Name',
+ 'Namespace',
+ 'Status',
+ 'Update channel',
+ 'Update approval',
+ '',
+ ]);
+ });
+});
+
+describe('SubscriptionsPage', () => {
+ it('renders MultiListPage with correct configuration', () => {
+ renderWithProviders();
+
+ expect(mockMultiListPage).toHaveBeenCalledTimes(1);
+ const [multiListPageProps] = mockMultiListPage.mock.calls[0];
+
+ expect(multiListPageProps.ListComponent).toEqual(SubscriptionsList);
+ expect(multiListPageProps.title).toEqual('Subscriptions');
+ expect(multiListPageProps.canCreate).toBe(true);
+ expect(multiListPageProps.createProps).toEqual({
+ to: '/catalog?catalogType=operator',
+ });
+ expect(multiListPageProps.createButtonText).toEqual('Create Subscription');
+ expect(multiListPageProps.filterLabel).toEqual('Subscriptions by package');
+ expect(multiListPageProps.resources).toEqual([
+ {
+ kind: referenceForModel(SubscriptionModel),
+ namespace: 'default',
+ namespaced: true,
+ prop: 'subscription',
+ },
+ {
+ kind: referenceForModel(OperatorGroupModel),
+ namespace: 'default',
+ namespaced: true,
+ prop: 'operatorGroup',
+ },
+ ]);
+ });
+});
+
+describe('SubscriptionDetails', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders installed CSV resource link when installed', () => {
+ const obj = _.cloneDeep(testSubscription);
+ obj.status = { installedCSV: testClusterServiceVersion.metadata.name };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('Installed version')).toBeVisible();
+ expect(mockResourceLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: obj.status.installedCSV,
+ name: obj.status.installedCSV,
+ }),
+ expect.anything(),
+ );
+ });
+
+ it('renders catalog source resource link', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('CatalogSource')).toBeVisible();
+ expect(mockResourceLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: testSubscription.spec.source,
+ }),
+ expect.anything(),
+ );
+ });
+});
+
+describe('SubscriptionDetailsPage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.spyOn(Router, 'useParams').mockReturnValue({ ns: 'default', name: 'example-sub' });
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders DetailsPage with correct configuration', () => {
+ renderWithProviders();
+
+ expect(mockDetailsPage).toHaveBeenCalledTimes(1);
+ const [detailsPageProps] = mockDetailsPage.mock.calls[0];
+
+ expect(detailsPageProps.kind).toEqual(referenceForModel(SubscriptionModel));
+ expect(detailsPageProps.pages).toHaveLength(2);
+ expect(detailsPageProps.customActionMenu).toBeDefined();
+ expect(detailsPageProps.resources).toEqual([
+ {
+ kind: referenceForModel(PackageManifestModel),
+ namespace: 'default',
+ isList: true,
+ prop: 'packageManifests',
+ },
+ {
+ kind: referenceForModel(InstallPlanModel),
+ isList: true,
+ namespace: 'default',
+ prop: 'installPlans',
+ },
+ {
+ kind: referenceForModel(ClusterServiceVersionModel),
+ isList: true,
+ namespace: 'default',
+ prop: 'clusterServiceVersions',
+ },
+ {
+ kind: referenceForModel(SubscriptionModel),
+ isList: true,
+ namespace: 'default',
+ prop: 'subscriptions',
+ },
+ ]);
+ });
+});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.spec.tsx
deleted file mode 100644
index 8c557b267a6..00000000000
--- a/frontend/packages/operator-lifecycle-manager/src/components/catalog-source.spec.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-import { shallow, ShallowWrapper } from 'enzyme';
-import { safeLoad } from 'js-yaml';
-import * as _ from 'lodash';
-import * as Router from 'react-router-dom-v5-compat';
-import { CreateYAML, CreateYAMLProps } from '@console/internal/components/create-yaml';
-import { DetailsPage } from '@console/internal/components/factory';
-import { Firehose, LoadingBox, DetailsItem } from '@console/internal/components/utils';
-import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
-import { referenceForModel } from '@console/internal/module/k8s';
-import { ErrorBoundary } from '@console/shared/src/components/error';
-import { testCatalogSource, testPackageManifest, dummyPackageManifest } from '../../mocks';
-import {
- SubscriptionModel,
- CatalogSourceModel,
- PackageManifestModel,
- OperatorGroupModel,
-} from '../models';
-import {
- CatalogSourceDetails,
- CatalogSourceDetailsProps,
- CatalogSourceDetailsPage,
- CreateSubscriptionYAML,
- CatalogSourceOperatorsPage,
-} from './catalog-source';
-
-jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({
- useK8sWatchResource: jest.fn(),
-}));
-
-jest.mock('react-router-dom-v5-compat', () => ({
- ...jest.requireActual('react-router-dom-v5-compat'),
- useParams: jest.fn(),
- useLocation: jest.fn(),
-}));
-
-describe(CatalogSourceDetails.displayName, () => {
- let wrapper: ShallowWrapper;
- let obj: CatalogSourceDetailsProps['obj'];
-
- beforeEach(() => {
- obj = _.cloneDeep(testCatalogSource);
- wrapper = shallow();
- });
-
- it('renders name and publisher of the catalog', () => {
- expect(wrapper.find(DetailsItem).at(1).props().obj.spec.displayName).toEqual(
- obj.spec.displayName,
- );
-
- expect(wrapper.find(DetailsItem).at(2).props().obj.spec.publisher).toEqual(obj.spec.publisher);
- });
-});
-
-describe(CatalogSourceDetailsPage.displayName, () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- (useK8sWatchResource as jest.Mock).mockReturnValue([dummyPackageManifest, true, null]);
- jest.spyOn(Router, 'useParams').mockReturnValue({ ns: 'default', name: 'some-catalog' });
- wrapper = shallow();
- });
-
- it('renders `DetailsPage` with correct props', () => {
- expect(wrapper.find(DetailsPage).props().kind).toEqual(referenceForModel(CatalogSourceModel));
-
- const detailsPage = wrapper.find(DetailsPage);
- const { pages } = detailsPage.props();
- expect(pages.length).toEqual(3);
- expect(pages[0].nameKey).toEqual(`public~Details`);
- expect(pages[1].nameKey).toEqual(`public~YAML`);
- expect(pages[2].nameKey).toEqual(`olm~Operators`);
-
- expect(pages[0].component).toEqual(CatalogSourceDetails);
- expect(pages[2].component).toEqual(CatalogSourceOperatorsPage);
-
- expect(wrapper.find(DetailsPage).props().resources).toEqual([
- {
- kind: referenceForModel(PackageManifestModel),
- isList: true,
- prop: 'packageManifests',
- namespace: 'default',
- },
- ]);
- });
-});
-
-describe(CreateSubscriptionYAML.displayName, () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- jest
- .spyOn(Router, 'useParams')
- .mockReturnValue({ ns: 'default', pkgName: testPackageManifest.metadata.name });
- jest.spyOn(Router, 'useLocation').mockReturnValue({
- ...window.location,
- search: `?pkg=${testPackageManifest.metadata.name}&catalog=ocs&catalogNamespace=default`,
- });
- wrapper = shallow();
- });
-
- it('renders a `Firehose` for the `PackageManfest` specified in the URL', () => {
- expect(wrapper.find(Firehose).props().resources).toEqual([
- {
- kind: referenceForModel(PackageManifestModel),
- isList: false,
- name: testPackageManifest.metadata.name,
- namespace: 'default',
- prop: 'packageManifest',
- },
- {
- kind: referenceForModel(OperatorGroupModel),
- isList: true,
- namespace: 'default',
- prop: 'operatorGroup',
- },
- ]);
- });
-
- it('renders YAML editor component wrapped by an error boundary component', () => {
- wrapper = wrapper.setProps({
- packageManifest: { loaded: true, data: testPackageManifest },
- } as any);
- const createYAML = wrapper.find(Firehose).childAt(0).dive().dive();
-
- expect(createYAML.find(ErrorBoundary).exists()).toBe(true);
- expect(createYAML.find(ErrorBoundary).childAt(0).dive().find(CreateYAML).exists()).toBe(true);
- });
-
- it('passes example YAML templates using the package default channel', () => {
- wrapper = wrapper.setProps({
- packageManifest: { loaded: true, data: testPackageManifest },
- } as any);
-
- const createYAML = wrapper
- .find(Firehose)
- .childAt(0)
- .dive()
- .dive()
- .find(ErrorBoundary)
- .childAt(0)
- .dive();
- const subTemplate = safeLoad(createYAML.props().template);
-
- window.location.search = `?pkg=${testPackageManifest.metadata.name}&catalog=ocs&catalogNamespace=default`;
-
- expect(subTemplate.kind).toContain(SubscriptionModel.kind);
- expect(subTemplate.spec.name).toEqual(testPackageManifest.metadata.name);
- expect(subTemplate.spec.channel).toEqual(testPackageManifest.status.channels[0].name);
- expect(subTemplate.spec.startingCSV).toEqual(testPackageManifest.status.channels[0].currentCSV);
- expect(subTemplate.spec.source).toEqual('ocs');
- });
-
- it('does not render YAML editor component if `PackageManifest` has not loaded yet', () => {
- wrapper = wrapper.setProps({ packageManifest: { loaded: false } } as any);
- const createYAML = wrapper.find(Firehose).childAt(0).dive().dive();
-
- expect(createYAML.find(CreateYAML).exists()).toBe(false);
- expect(createYAML.find(ErrorBoundary).childAt(0).dive().find(LoadingBox).exists()).toBe(true);
- });
-});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx
deleted file mode 100644
index 7abfe0d9096..00000000000
--- a/frontend/packages/operator-lifecycle-manager/src/components/clusterserviceversion.spec.tsx
+++ /dev/null
@@ -1,596 +0,0 @@
-import {
- CardTitle,
- CardBody,
- CardFooter,
- DescriptionListTerm,
- DescriptionListDescription,
-} from '@patternfly/react-core';
-import { shallow, ShallowWrapper, mount, ReactWrapper } from 'enzyme';
-import * as _ from 'lodash';
-import { Provider } from 'react-redux';
-import * as ReactRouter from 'react-router-dom-v5-compat';
-import { ActionMenuVariant } from '@console/dynamic-plugin-sdk/src/api/internal-types';
-import * as rbacModule from '@console/dynamic-plugin-sdk/src/app/components/utils/rbac';
-import {
- DetailsPage,
- Table,
- TableProps,
- ComponentProps,
-} from '@console/internal/components/factory';
-import {
- ScrollToTopOnMount,
- SectionHeading,
- resourceObjPath,
- StatusBox,
-} from '@console/internal/components/utils';
-import operatorLogo from '@console/internal/imgs/operator.svg';
-import { referenceForModel } from '@console/internal/module/k8s';
-import store from '@console/internal/redux';
-import LazyActionMenu from '@console/shared/src/components/actions/LazyActionMenu';
-import { Timestamp } from '@console/shared/src/components/datetime/Timestamp';
-import { ErrorBoundary } from '@console/shared/src/components/error';
-import PaneBody from '@console/shared/src/components/layout/PaneBody';
-import { useActiveNamespace } from '@console/shared/src/hooks/redux-selectors';
-import {
- testClusterServiceVersion,
- testSubscription,
- testPackageManifest,
- testCatalogSource,
- testInstallPlan,
- testModel,
- testSubscriptions,
-} from '../../mocks';
-import { ClusterServiceVersionModel } from '../models';
-import { ClusterServiceVersionKind, ClusterServiceVersionPhase } from '../types';
-import { ClusterServiceVersionLogo } from './cluster-service-version-logo';
-import {
- ClusterServiceVersionDetailsPage,
- ClusterServiceVersionDetails,
- ClusterServiceVersionDetailsProps,
- ClusterServiceVersionTableRow,
- ClusterServiceVersionTableRowProps,
- CRDCard,
- CRDCardRow,
- CSVSubscription,
- CSVSubscriptionProps,
- ClusterServiceVersionList,
-} from './clusterserviceversion';
-import { SubscriptionUpdates, SubscriptionDetails } from './subscription';
-import { ClusterServiceVersionLogoProps, referenceForProvidedAPI } from '.';
-
-jest.mock('@console/shared/src/hooks/useK8sModel', () => ({
- useK8sModel: () => [testModel],
-}));
-
-jest.mock('@console/internal/components/utils/rbac', () => ({
- useAccessReview: () => true,
-}));
-
-jest.mock('@console/shared/src/hooks/redux-selectors', () => {
- return {
- useActiveNamespace: jest.fn(),
- };
-});
-
-jest.mock('react-router-dom-v5-compat', () => ({
- ...jest.requireActual('react-router-dom-v5-compat'),
- useParams: jest.fn(),
- useLocation: jest.fn(),
-}));
-
-describe(ClusterServiceVersionTableRow.displayName, () => {
- let wrapper: ShallowWrapper;
- beforeEach(() => {
- window.SERVER_FLAGS.copiedCSVsDisabled = false;
- wrapper = shallow(
- ,
- )
- .childAt(0)
- .shallow();
- });
-
- it('renders a component wrapped in an `ErrorBoundary', () => {
- wrapper = shallow(
- ,
- );
-
- expect(wrapper.find(ErrorBoundary).exists()).toBe(true);
- });
-
- it('renders `ResourceKebab` with actions', () => {
- const col = wrapper;
-
- expect(col.find(LazyActionMenu).props().context).toEqual({
- 'operator-actions': { resource: testClusterServiceVersion, subscription: testSubscription },
- });
- expect(col.find(LazyActionMenu).props().variant).toEqual(ActionMenuVariant.KEBAB);
- });
-
- it('renders clickable column for app logo and name', () => {
- const col = wrapper.childAt(0);
-
- expect(col.find(ReactRouter.Link).props().to).toEqual(
- resourceObjPath(testClusterServiceVersion, referenceForModel(ClusterServiceVersionModel)),
- );
- expect(col.find(ReactRouter.Link).find(ClusterServiceVersionLogo).exists()).toBe(true);
- });
-
- it('renders column for managedNamespace', () => {
- const col = wrapper.childAt(1);
- const managedNamespace = col.childAt(0);
- expect(managedNamespace.exists()).toBeTruthy();
- });
-
- it('renders column for last updated', () => {
- const col = wrapper.childAt(3);
- expect(col.find(Timestamp).props().timestamp).toEqual('2020-04-21T18:19:49Z');
- });
-
- it('renders column for app status', () => {
- const col = wrapper.childAt(2);
- const statusComponent = col.childAt(0).find('ClusterServiceVersionStatus');
- expect(statusComponent.exists()).toBeTruthy();
- expect(statusComponent.render().text()).toContain(ClusterServiceVersionPhase.CSVPhaseSucceeded);
- });
-
- it('renders "disabling" status if CSV has `deletionTimestamp`', () => {
- wrapper = wrapper.setProps({
- obj: _.cloneDeepWith(testClusterServiceVersion, (v, k) =>
- k === 'metadata' ? { ...v, deletionTimestamp: Date.now() } : undefined,
- ),
- });
- const col = wrapper.childAt(2);
-
- expect(col.childAt(0).find('ClusterServiceVersionStatus').render().text()).toEqual('Deleting');
- });
-
- it('renders column with each CRD provided by the Operator', () => {
- const col = wrapper.childAt(4);
- testClusterServiceVersion.spec.customresourcedefinitions.owned.forEach((desc, i) => {
- expect(col.find(ReactRouter.Link).at(i).props().title).toEqual(desc.name);
- expect(col.find(ReactRouter.Link).at(i).props().to).toEqual(
- `${resourceObjPath(
- testClusterServiceVersion,
- referenceForModel(ClusterServiceVersionModel),
- )}/${referenceForProvidedAPI(desc)}`,
- );
- });
- });
-});
-
-describe(ClusterServiceVersionLogo.displayName, () => {
- let wrapper: ReactWrapper;
-
- beforeEach(() => {
- const { provider, icon, displayName } = testClusterServiceVersion.spec;
- wrapper = mount(
- ,
- );
- });
-
- it('renders logo image from given base64 encoded image string', () => {
- const image = wrapper.find('img');
-
- expect(image.props().src).toEqual(
- `data:${testClusterServiceVersion.spec.icon[0].mediatype};base64,${testClusterServiceVersion.spec.icon[0].base64data}`,
- );
- });
-
- it('renders fallback image if given icon is invalid', () => {
- wrapper.setProps({ icon: null });
- const fallbackImg = wrapper.find('img');
-
- expect(fallbackImg.props().src).toEqual(operatorLogo);
- });
-
- it('renders ClusterServiceVersion name and provider from given spec', () => {
- expect(wrapper.text()).toContain(testClusterServiceVersion.spec.displayName);
- expect(wrapper.text()).toContain(`by ${testClusterServiceVersion.spec.provider.name}`);
- });
-});
-
-describe(ClusterServiceVersionList.displayName, () => {
- it('renders `List` with SingleProjectTableHeader for namespace scoped CSV', () => {
- (useActiveNamespace as jest.Mock).mockImplementation(() => 'test');
- const wrapper = shallow(
- ,
- );
- const header = wrapper.find(Table).props().Header;
- expect(header.name).toEqual('SingleProjectTableHeader');
- const headerColumns = header({} as ComponentProps);
- expect(headerColumns[0].title).toEqual('Name');
- expect(headerColumns[1].title).toEqual('Managed Namespaces');
- expect(headerColumns[2].title).toEqual('Status');
- expect(headerColumns[3].title).toEqual('Last updated');
- expect(headerColumns[4].title).toEqual('Provided APIs');
- expect(headerColumns[5].title).toEqual('');
- });
- it('renders `List` with AllProjectTableHeader for all-namespaces scoped CSV', () => {
- (useActiveNamespace as jest.Mock).mockImplementation(() => '#ALL_NS#');
- const wrapper = shallow(
- ,
- );
- const header = wrapper.find(Table).props().Header;
- expect(header.name).toEqual('AllProjectsTableHeader');
- const headerColumns = header({} as ComponentProps);
- expect(headerColumns[0].title).toEqual('Name');
- expect(headerColumns[1].title).toEqual('Namespace');
- expect(headerColumns[2].title).toEqual('Managed Namespaces');
- expect(headerColumns[3].title).toEqual('Status');
- expect(headerColumns[4].title).toEqual('Last updated');
- expect(headerColumns[5].title).toEqual('Provided APIs');
- expect(headerColumns[6].title).toEqual('');
- });
-});
-
-describe(CRDCard.displayName, () => {
- const crd = testClusterServiceVersion.spec.customresourcedefinitions.owned[0];
-
- it('renders a card with title, body, and footer', () => {
- const wrapper = shallow();
-
- expect(wrapper.find(CardTitle).exists()).toBe(true);
- expect(wrapper.find(CardBody).exists()).toBe(true);
- expect(wrapper.find(CardFooter).exists()).toBe(true);
- });
-
- it('renders a link to create a new instance', () => {
- const wrapper = shallow();
-
- expect(wrapper.find(CardFooter).find(ReactRouter.Link).props().to).toEqual(
- `/k8s/ns/${testClusterServiceVersion.metadata.namespace}/${
- ClusterServiceVersionModel.plural
- }/${testClusterServiceVersion.metadata.name}/${referenceForProvidedAPI(crd)}/~new`,
- );
- });
-
- it('does not render link to create new instance if `props.canCreate` is false', () => {
- const wrapper = shallow(
- ,
- );
-
- expect(wrapper.find(CardFooter).find(ReactRouter.Link).exists()).toBe(false);
- });
-});
-
-describe(ClusterServiceVersionDetails.displayName, () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- wrapper = shallow(
- ,
- );
- });
-
- it('renders `ScrollToTopOnMount` component', () => {
- expect(wrapper.find(ScrollToTopOnMount).exists()).toBe(true);
- });
-
- it('renders row of cards for each "owned" CRD for the given `ClusterServiceVersion`', () => {
- expect(wrapper.find(CRDCardRow).props().csv).toEqual(testClusterServiceVersion);
- expect(wrapper.find(CRDCardRow).props().providedAPIs).toEqual(
- testClusterServiceVersion.spec.customresourcedefinitions.owned,
- );
- });
-
- it('renders description section for ClusterServiceVersion', () => {
- expect(wrapper.find(PaneBody).at(0).find(SectionHeading).at(1).props().text).toEqual(
- 'Description',
- );
- });
-
- it('renders creation date from ClusterServiceVersion', () => {
- expect(wrapper.find(Timestamp).props().timestamp).toEqual(
- testClusterServiceVersion.metadata.creationTimestamp,
- );
- });
-
- it('renders list of maintainers from ClusterServiceVersion', () => {
- const maintainers = wrapper
- .findWhere((node) => node.equals(Maintainers))
- .parents()
- .at(0)
- .find(DescriptionListDescription)
- .shallow();
-
- expect(maintainers.length).toEqual(testClusterServiceVersion.spec.maintainers.length);
-
- testClusterServiceVersion.spec.maintainers.forEach((maintainer, i) => {
- expect(maintainers.at(i).text()).toContain(maintainer.name);
- expect(maintainers.at(i).find('.co-break-all').text()).toEqual(maintainer.email);
- expect(maintainers.at(i).find('.co-break-all').props().href).toEqual(
- `mailto:${maintainer.email}`,
- );
- });
- });
-
- it('renders important links from ClusterServiceVersion', () => {
- const links = wrapper
- .findWhere((node) => node.equals(Links))
- .parents()
- .at(0)
- .find(DescriptionListDescription);
-
- expect(links.length).toEqual(testClusterServiceVersion.spec.links.length);
- });
-
- it('renders empty state for unfulfilled outputs and metadata', () => {
- const emptyClusterServiceVersion: ClusterServiceVersionKind = _.cloneDeep(
- testClusterServiceVersion,
- );
- emptyClusterServiceVersion.spec.description = '';
- emptyClusterServiceVersion.spec.provider = undefined;
- emptyClusterServiceVersion.spec.links = [];
- emptyClusterServiceVersion.spec.maintainers = [];
- wrapper.setProps({ obj: emptyClusterServiceVersion });
-
- const provider = wrapper
- .findWhere((node) => node.equals(Provider))
- .parents()
- .at(0)
- .find(DescriptionListDescription)
- .shallow();
- const links = wrapper
- .findWhere((node) => node.equals(Links))
- .parents()
- .at(0)
- .find(DescriptionListDescription)
- .shallow();
- const maintainers = wrapper
- .findWhere((node) => node.equals(Maintainers))
- .parents()
- .at(0)
- .find(DescriptionListDescription)
- .shallow();
-
- expect(provider.text()).toEqual('Not available');
- expect(links.text()).toEqual('Not available');
- expect(maintainers.text()).toEqual('Not available');
- });
-
- it('renders info section for ClusterServiceVersion', () => {
- expect(wrapper.find(PaneBody).at(1).find(SectionHeading).props().text).toEqual(
- 'ClusterServiceVersion details',
- );
- });
-
- it('renders conditions section for ClusterServiceVersion', () => {
- expect(wrapper.find(PaneBody).at(2).find(SectionHeading).props().text).toEqual('Conditions');
- });
-
- it('does not render service accounts section if empty', () => {
- const emptyTestClusterServiceVersion = _.cloneDeep(testClusterServiceVersion);
- emptyTestClusterServiceVersion.spec.install.spec.permissions = [];
- wrapper = shallow(
- ,
- );
- expect(emptyTestClusterServiceVersion.spec.install.spec.permissions.length).toEqual(0);
- expect(wrapper.findWhere((node) => node.text() === 'Operator ServiceAccounts').length).toEqual(
- 0,
- );
- });
-
- it('does not render duplicate service accounts', () => {
- const duplicateTestClusterServiceVersion = _.cloneDeep(testClusterServiceVersion);
- const permission = duplicateTestClusterServiceVersion.spec.install.spec.permissions[0];
- duplicateTestClusterServiceVersion.spec.install.spec.permissions.push(permission);
- wrapper = shallow(
- ,
- );
- expect(duplicateTestClusterServiceVersion.spec.install.spec.permissions.length).toEqual(2);
- expect(wrapper.findWhere((node) => node.text() === 'Operator ServiceAccounts').length).toEqual(
- 1,
- );
- expect(
- wrapper.find(`[data-service-account-name="${permission.serviceAccountName}"]`).length,
- ).toEqual(1);
- });
-});
-
-describe(CSVSubscription.displayName, () => {
- let wrapper: ShallowWrapper;
-
- it('renders `StatusBox` with correct props when Operator subscription does not exist', () => {
- wrapper = shallow(
- ,
- );
-
- expect(wrapper.find(StatusBox).props().EmptyMsg).toBeDefined();
- expect(wrapper.find(StatusBox).props().loaded).toBe(true);
- expect(wrapper.find(StatusBox).props().data).toBeUndefined();
- });
-
- it('renders `SubscriptionDetails` with correct props when Operator subscription exists', () => {
- const obj = _.set(_.cloneDeep(testClusterServiceVersion), 'metadata.annotations', {
- 'olm.operatorNamespace': 'default',
- });
- const subscription = _.set(_.cloneDeep(testSubscription), 'status', {
- installedCSV: obj.metadata.name,
- });
-
- wrapper = shallow(
- ,
- );
-
- const subscriptionUpdates = wrapper
- .find(StatusBox)
- .find(SubscriptionDetails)
- .dive()
- .find(SubscriptionUpdates);
- expect(subscriptionUpdates.props().obj).toEqual(subscription);
- expect(subscriptionUpdates.props().installedCSV).toEqual(obj);
- expect(subscriptionUpdates.props().pkg).toEqual(testPackageManifest);
- });
-
- it('passes the matching `PackageManifest` if there are multiple with the same `metadata.name`', () => {
- const obj = _.set(_.cloneDeep(testClusterServiceVersion), 'metadata.annotations', {
- 'olm.operatorNamespace': 'default',
- });
- const subscription = _.set(_.cloneDeep(testSubscription), 'status', {
- installedCSV: obj.metadata.name,
- });
- const otherPkg = _.set(
- _.cloneDeep(testPackageManifest),
- 'status.catalogSource',
- 'other-source',
- );
-
- wrapper = shallow(
- ,
- );
-
- expect(
- wrapper.find(StatusBox).find(SubscriptionDetails).dive().find(SubscriptionUpdates).props()
- .pkg,
- ).toEqual(testPackageManifest);
- });
-});
-
-describe(ClusterServiceVersionDetailsPage.displayName, () => {
- let wrapper: ReactWrapper;
- let spyUseAccessReview;
-
- const name = 'example';
- const ns = 'default';
-
- beforeEach(() => {
- spyUseAccessReview = jest.spyOn(rbacModule, 'useAccessReview');
- spyUseAccessReview.mockReturnValue([true, false]);
-
- jest.spyOn(ReactRouter, 'useParams').mockReturnValue({ name: 'example', ns: 'default' });
- jest.spyOn(ReactRouter, 'useLocation').mockReturnValue({ pathname: '' });
-
- window.SERVER_FLAGS.copiedCSVsDisabled = false;
- wrapper = mount(
-
-
-
-
- ,
- );
- });
-
- it('passes URL parameters to `DetailsPage`', () => {
- const detailsPage = wrapper.find(DetailsPage);
-
- expect(detailsPage.props().namespace).toEqual(ns);
- expect(detailsPage.props().name).toEqual(name);
- expect(detailsPage.props().kind).toEqual(referenceForModel(ClusterServiceVersionModel));
- });
-
- it('renders a `DetailsPage` with the correct subpages', () => {
- const detailsPage = wrapper.find(DetailsPage);
- expect(detailsPage.props().pagesFor(testClusterServiceVersion)[0].nameKey).toEqual(
- 'public~Details',
- );
- expect(detailsPage.props().pagesFor(testClusterServiceVersion)[0].href).toEqual('');
- expect(detailsPage.props().pagesFor(testClusterServiceVersion)[0].component).toEqual(
- ClusterServiceVersionDetails,
- );
- expect(detailsPage.props().pagesFor(testClusterServiceVersion)[1].nameKey).toEqual(
- `public~YAML`,
- );
- expect(detailsPage.props().pagesFor(testClusterServiceVersion)[1].href).toEqual('yaml');
- expect(detailsPage.props().pagesFor(testClusterServiceVersion)[2].nameKey).toEqual(
- 'olm~Subscription',
- );
- expect(detailsPage.props().pagesFor(testClusterServiceVersion)[2].href).toEqual('subscription');
- expect(detailsPage.props().pagesFor(testClusterServiceVersion)[3].nameKey).toEqual(
- `public~Events`,
- );
- expect(detailsPage.props().pagesFor(testClusterServiceVersion)[3].href).toEqual('events');
- });
-
- it('includes tab for each "owned" CRD', () => {
- const detailsPage = wrapper.find(DetailsPage);
-
- const csv = _.cloneDeep(testClusterServiceVersion);
- csv.spec.customresourcedefinitions.owned = csv.spec.customresourcedefinitions.owned.concat([
- { name: 'e.example.com', kind: 'E', version: 'v1alpha1', displayName: 'E' },
- ]);
-
- expect(detailsPage.props().pagesFor(csv)[4].nameKey).toEqual('olm~All instances');
- expect(detailsPage.props().pagesFor(csv)[4].href).toEqual('instances');
- csv.spec.customresourcedefinitions.owned.forEach((desc, i) => {
- expect(detailsPage.props().pagesFor(csv)[5 + i].name).toEqual(desc.displayName);
- expect(detailsPage.props().pagesFor(csv)[5 + i].href).toEqual(referenceForProvidedAPI(desc));
- });
- });
-
- it('does not include "All Instances" tab if only one "owned" CRD', () => {
- const detailsPage = wrapper.find(DetailsPage);
-
- expect(
- detailsPage
- .props()
- .pagesFor(testClusterServiceVersion)
- .some((p) => p.name === 'All Instances'),
- ).toBe(false);
- });
-});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/install-plan.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/install-plan.spec.tsx
deleted file mode 100644
index 3b5c73b5be8..00000000000
--- a/frontend/packages/operator-lifecycle-manager/src/components/install-plan.spec.tsx
+++ /dev/null
@@ -1,388 +0,0 @@
-import { Button, Hint } from '@patternfly/react-core';
-import { shallow, ShallowWrapper } from 'enzyme';
-import * as _ from 'lodash';
-import { Link } from 'react-router-dom-v5-compat';
-import * as Router from 'react-router-dom-v5-compat';
-import * as k8sResourceModule from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-resource';
-import {
- Table,
- MultiListPage,
- DetailsPage,
- RowFunctionArgs,
- ComponentProps,
-} from '@console/internal/components/factory';
-import {
- ResourceLink,
- ResourceIcon,
- ConsoleEmptyState,
- useAccessReview,
-} from '@console/internal/components/utils';
-import { CustomResourceDefinitionModel } from '@console/internal/models';
-import { referenceForModel, K8sResourceKind } from '@console/internal/module/k8s';
-import { testInstallPlan } from '../../mocks';
-import { InstallPlanModel, ClusterServiceVersionModel, OperatorGroupModel } from '../models';
-import { InstallPlanKind, InstallPlanApproval } from '../types';
-import {
- InstallPlanTableRow,
- InstallPlansList,
- InstallPlansListProps,
- InstallPlansPage,
- InstallPlansPageProps,
- InstallPlanDetailsPage,
- InstallPlanPreview,
- InstallPlanDetails,
- InstallPlanDetailsProps,
- InstallPlanHint,
-} from './install-plan';
-import * as modal from './modals/installplan-preview-modal';
-import { referenceForStepResource } from '.';
-import Spy = jasmine.Spy;
-
-const i18nNS = 'public';
-
-jest.mock('react-router-dom-v5-compat', () => ({
- ...jest.requireActual('react-router-dom-v5-compat'),
- useParams: jest.fn(),
-}));
-
-jest.mock('@console/internal/components/utils/rbac', () => ({
- useAccessReview: jest.fn(),
-}));
-
-const useAccessReviewMock = useAccessReview as jest.Mock;
-
-describe('InstallPlanTableRow', () => {
- let obj: InstallPlanKind;
- let wrapper: ShallowWrapper;
-
- const updateWrapper = () => {
- const rowArgs: RowFunctionArgs = {
- obj,
- } as any;
-
- wrapper = shallow();
- return wrapper;
- };
-
- beforeEach(() => {
- obj = _.cloneDeep(testInstallPlan);
- wrapper = updateWrapper();
- });
-
- it('renders column for install plan name', () => {
- expect(wrapper.childAt(0).find(ResourceLink).props().kind).toEqual(
- referenceForModel(InstallPlanModel),
- );
- expect(wrapper.childAt(0).find(ResourceLink).props().namespace).toEqual(
- testInstallPlan.metadata.namespace,
- );
- expect(wrapper.childAt(0).find(ResourceLink).props().name).toEqual(
- testInstallPlan.metadata.name,
- );
- });
-
- it('renders column for install plan namespace', () => {
- expect(wrapper.childAt(1).find(ResourceLink).props().kind).toEqual('Namespace');
- });
-
- it('renders column for install plan status', () => {
- expect(wrapper.childAt(2).render().find('[data-test="status-text"]').text()).toEqual(
- testInstallPlan.status.phase,
- );
- });
-
- it('renders column with fallback status if `status.phase` is undefined', () => {
- obj = { ..._.cloneDeep(testInstallPlan), status: null };
- wrapper = updateWrapper();
-
- expect(wrapper.childAt(2).render().text()).toEqual('Unknown');
- expect(wrapper.childAt(3).find(ResourceIcon).length).toEqual(1);
- expect(wrapper.childAt(3).find(ResourceIcon).at(0).props().kind).toEqual(
- referenceForModel(ClusterServiceVersionModel),
- );
- });
-
- it('render column for install plan components list', () => {
- expect(wrapper.childAt(3).find(ResourceLink).props().kind).toEqual(
- referenceForModel(ClusterServiceVersionModel),
- );
- expect(wrapper.childAt(3).find(ResourceLink).props().name).toEqual(
- testInstallPlan.spec.clusterServiceVersionNames.toString(),
- );
- expect(wrapper.childAt(3).find(ResourceLink).props().namespace).toEqual(
- testInstallPlan.metadata.namespace,
- );
- });
-
- it('renders column for parent subscription(s) determined by `metadata.ownerReferences`', () => {
- expect(wrapper.childAt(4).find(ResourceLink).length).toEqual(1);
- });
-});
-
-describe('InstallPlansList', () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- wrapper = shallow();
- });
-
- it('renders a `Table` component with the correct props', () => {
- const headerTitles = wrapper
- .find(Table)
- .props()
- .Header({} as ComponentProps)
- .map((header) => header.title);
- expect(headerTitles).toEqual([
- 'Name',
- 'Namespace',
- 'Status',
- 'Components',
- 'Subscriptions',
- '',
- ]);
- });
-
- it('passes custom empty message for table', () => {
- const MsgComponent = wrapper.find(Table).props().EmptyMsg;
- const msgWrapper = shallow();
- expect(msgWrapper.find(ConsoleEmptyState).props().title).toEqual('No InstallPlans found');
- expect(msgWrapper.find(ConsoleEmptyState).children().text()).toEqual(
- 'InstallPlans are created automatically by subscriptions or manually using the CLI.',
- );
- });
-});
-
-describe('InstallPlansPage', () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- jest.spyOn(Router, 'useParams').mockReturnValue({
- ns: 'default',
- });
- wrapper = shallow();
- });
-
- it('renders a `MultiListPage` with the correct props', () => {
- expect(wrapper.find(MultiListPage).props().title).toEqual('InstallPlans');
- expect(wrapper.find(MultiListPage).props().showTitle).toBe(false);
- expect(wrapper.find(MultiListPage).props().ListComponent).toEqual(InstallPlansList);
- expect(wrapper.find(MultiListPage).props().resources).toEqual([
- {
- kind: referenceForModel(InstallPlanModel),
- namespace: 'default',
- namespaced: true,
- prop: 'installPlan',
- },
- {
- kind: referenceForModel(OperatorGroupModel),
- namespace: 'default',
- namespaced: true,
- prop: 'operatorGroup',
- },
- ]);
- });
-});
-
-describe('InstallPlanPreview', () => {
- // let wrapper: ShallowWrapper;
- // let obj: InstallPlanKind;
- const obj: InstallPlanKind = {
- ...testInstallPlan,
- status: {
- ...testInstallPlan.status,
- plan: [
- {
- resolving: 'testoperator.v1.0.0',
- status: 'Created',
- resource: {
- group: ClusterServiceVersionModel.apiGroup,
- version: ClusterServiceVersionModel.apiVersion,
- kind: ClusterServiceVersionModel.kind,
- name: 'testoperator.v1.0.0',
- manifest: '',
- },
- },
- {
- resolving: 'testoperator.v1.0.0',
- status: 'Unknown',
- resource: {
- group: CustomResourceDefinitionModel.apiGroup,
- version: CustomResourceDefinitionModel.apiVersion,
- kind: CustomResourceDefinitionModel.kind,
- name: 'test-crds.test.com',
- manifest: '',
- },
- },
- ],
- },
- };
-
- const spyAndExpect = (spy: Spy) => (returnValue: any) =>
- new Promise((resolve) =>
- spy.and.callFake((...args) => {
- resolve(args);
- return returnValue;
- }),
- );
-
- it('renders empty message if `status.plan` is not filled', () => {
- const wrapper = shallow(
- ,
- );
- expect(wrapper.find(ConsoleEmptyState).exists()).toBe(true);
- });
-
- it('renders button to approve install plan if requires approval', () => {
- useAccessReviewMock.mockReturnValue(true);
- const wrapper = shallow(
- ,
- );
- expect(
- wrapper.find(InstallPlanHint).dive().find(Hint).shallow().find(Button).at(0).render().text(),
- ).toEqual('Approve');
- });
-
- it('calls `k8sPatch` to set `approved: true` when button is clicked', (done) => {
- jest
- .spyOn(k8sResourceModule, 'k8sPatch')
- .mockImplementation((_model, data) => Promise.resolve(data));
-
- spyAndExpect(spyOn(k8sResourceModule, 'k8sPatch'))(Promise.resolve(testInstallPlan))
- .then(([model, installPlan]) => {
- expect(model).toEqual(InstallPlanModel);
- expect(jest.spyOn(k8sResourceModule, 'k8sPatch')).toHaveBeenLastCalledWith(
- InstallPlanModel,
- installPlan,
- [
- {
- op: 'replace',
- path: '/spec/approved',
- value: true,
- },
- ],
- );
- done();
- })
- .catch((err) => fail(err));
-
- const wrapper = shallow(
- ,
- );
-
- wrapper.find(InstallPlanHint).dive().find(Hint).shallow().find(Button).at(0).simulate('click');
- });
-
- it('renders button to deny install plan if requires approval', () => {
- const wrapper = shallow(
- ,
- );
- expect(
- wrapper.find(InstallPlanHint).dive().find(Hint).shallow().find(Button).at(1).render().text(),
- ).toEqual('Deny');
- });
-
- it('renders section for each resolving `ClusterServiceVersion`', () => {
- const wrapper = shallow();
- expect(wrapper.find('.co-m-pane__body').length).toEqual(1);
- wrapper.find('.co-m-pane__body').forEach((section) => {
- expect(section.find('tbody').find('tr').length).toEqual(2);
- });
- });
-
- it('renders link to view install plan component if it exists', () => {
- const wrapper = shallow();
- const row = wrapper.find('.co-m-pane__body').find('tbody').find('tr').at(0);
-
- expect(row.find('td').at(0).find(ResourceLink).props().name).toEqual(
- obj.status.plan[0].resource.name,
- );
- });
-
- it('renders link to open preview modal for install plan component if not created yet', () => {
- const wrapper = shallow();
- const row = wrapper.find('.co-m-pane__body').find('tbody').find('tr').at(1);
- const modalSpy = spyOn(modal, 'installPlanPreviewModal').and.returnValue(null);
-
- expect(row.find('td').at(0).find(ResourceIcon).props().kind).toEqual(
- referenceForStepResource(obj.status.plan[1].resource),
- );
-
- row.find('td').at(0).find(Button).simulate('click');
-
- expect(modalSpy.calls.argsFor(0)[0].stepResource).toEqual(obj.status.plan[1].resource);
- });
-});
-
-describe('InstallPlanDetails', () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- wrapper = shallow();
- });
-
- it('renders link to "Components" tab if install plan needs approval', () => {
- const installPlan = _.cloneDeep(testInstallPlan);
- installPlan.spec.approval = InstallPlanApproval.Manual;
- installPlan.spec.approved = false;
- wrapper = wrapper.setProps({ obj: installPlan });
-
- expect(
- wrapper.find(InstallPlanHint).dive().find(Hint).shallow().find(Link).props().to,
- ).toEqual(
- `/k8s/ns/default/${referenceForModel(InstallPlanModel)}/${
- testInstallPlan.metadata.name
- }/components`,
- );
- });
-
- it('does not render link to "Components" tab if install plan does not need approval"', () => {
- expect(wrapper.find(InstallPlanHint).exists()).toBe(false);
- });
-});
-
-describe('InstallPlanDetailsPage', () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- jest
- .spyOn(Router, 'useParams')
- .mockReturnValue({ ns: 'default', name: testInstallPlan.metadata.name });
- wrapper = shallow();
- });
-
- it('renders a `DetailsPage` with correct props', () => {
- expect(
- wrapper
- .find(DetailsPage)
- .props()
- .pages.map((p) => p.name || p.nameKey),
- ).toEqual([`${i18nNS}~Details`, `${i18nNS}~YAML`, 'olm~Components']);
- });
-});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/installplan-approval-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/installplan-approval-modal.spec.tsx
new file mode 100644
index 00000000000..5f0da26d30d
--- /dev/null
+++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/installplan-approval-modal.spec.tsx
@@ -0,0 +1,154 @@
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import * as _ from 'lodash';
+import { modelFor } from '@console/internal/module/k8s';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { testSubscription, testInstallPlan } from '../../../../mocks';
+import { SubscriptionModel, InstallPlanModel } from '../../../models';
+import { InstallPlanApproval } from '../../../types';
+import {
+ InstallPlanApprovalModal,
+ InstallPlanApprovalModalProps,
+} from '../installplan-approval-modal';
+
+jest.mock('@console/internal/module/k8s', () => ({
+ ...jest.requireActual('@console/internal/module/k8s'),
+ modelFor: jest.fn(),
+}));
+
+jest.mock('@console/internal/components/factory/modal', () => ({
+ ...jest.requireActual('@console/internal/components/factory/modal'),
+ ModalTitle: jest.fn(({ children }) => children),
+ ModalBody: jest.fn(({ children }) => children),
+ ModalSubmitFooter: jest.fn(() => null),
+}));
+
+const mockModelFor = modelFor as jest.Mock;
+
+describe('InstallPlanApprovalModal', () => {
+ let installPlanApprovalModalProps: InstallPlanApprovalModalProps;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ installPlanApprovalModalProps = {
+ obj: { ...testSubscription },
+ k8sUpdate: jest.fn(() => Promise.resolve()),
+ close: jest.fn(),
+ cancel: jest.fn(),
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders modal with correct title', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Change update approval strategy')).toBeVisible();
+ });
+
+ it('renders two radio buttons for approval strategies', () => {
+ renderWithProviders();
+
+ const radios = screen.getAllByRole('radio');
+ expect(radios).toHaveLength(2);
+ expect(screen.getByRole('radio', { name: 'Automatic (default)' })).toBeVisible();
+ expect(screen.getByRole('radio', { name: 'Manual' })).toBeVisible();
+ });
+
+ it('pre-selects Automatic when subscription uses automatic approval', () => {
+ renderWithProviders();
+
+ const automaticRadio = screen.getByRole('radio', { name: 'Automatic (default)' });
+ const manualRadio = screen.getByRole('radio', { name: 'Manual' });
+
+ expect(automaticRadio).toBeChecked();
+ expect(manualRadio).not.toBeChecked();
+ });
+
+ it('pre-selects Manual when install plan uses manual approval', () => {
+ const installPlan = _.cloneDeep(testInstallPlan);
+ installPlan.spec.approval = InstallPlanApproval.Manual;
+
+ renderWithProviders(
+ ,
+ );
+
+ const automaticRadio = screen.getByRole('radio', { name: 'Automatic (default)' });
+ const manualRadio = screen.getByRole('radio', { name: 'Manual' });
+
+ expect(automaticRadio).not.toBeChecked();
+ expect(manualRadio).toBeChecked();
+ });
+
+ it('calls k8sUpdate with updated subscription when form is submitted', async () => {
+ mockModelFor.mockReturnValue(SubscriptionModel);
+
+ renderWithProviders();
+
+ const manualRadio = screen.getByRole('radio', { name: 'Manual' });
+ fireEvent.click(manualRadio);
+
+ const form = screen.getByRole('radio', { name: 'Manual' }).closest('form');
+ if (form) {
+ fireEvent.submit(form);
+ }
+
+ await waitFor(() => {
+ expect(installPlanApprovalModalProps.k8sUpdate).toHaveBeenCalledTimes(1);
+ });
+
+ expect(installPlanApprovalModalProps.k8sUpdate).toHaveBeenCalledWith(
+ SubscriptionModel,
+ expect.objectContaining({
+ spec: expect.objectContaining({
+ installPlanApproval: InstallPlanApproval.Manual,
+ }),
+ }),
+ );
+ });
+
+ it('calls k8sUpdate with updated install plan when form is submitted', async () => {
+ const installPlan = _.cloneDeep(testInstallPlan);
+ mockModelFor.mockReturnValue(InstallPlanModel);
+
+ renderWithProviders(
+ ,
+ );
+
+ const manualRadio = screen.getByRole('radio', { name: 'Manual' });
+ fireEvent.click(manualRadio);
+
+ const form = screen.getByRole('radio', { name: 'Manual' }).closest('form');
+ if (form) {
+ fireEvent.submit(form);
+ }
+
+ await waitFor(() => {
+ expect(installPlanApprovalModalProps.k8sUpdate).toHaveBeenCalledTimes(1);
+ });
+
+ expect(installPlanApprovalModalProps.k8sUpdate).toHaveBeenCalledWith(
+ InstallPlanModel,
+ expect.objectContaining({
+ spec: expect.objectContaining({
+ approval: InstallPlanApproval.Manual,
+ }),
+ }),
+ );
+ });
+
+ it('calls close callback after successful submit', async () => {
+ renderWithProviders();
+
+ const form = screen.getByRole('radio', { name: 'Automatic (default)' }).closest('form');
+ if (form) {
+ fireEvent.submit(form);
+ }
+
+ await waitFor(() => {
+ expect(installPlanApprovalModalProps.close).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/subscription-channel-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/subscription-channel-modal.spec.tsx
new file mode 100644
index 00000000000..35d23c3a78c
--- /dev/null
+++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/subscription-channel-modal.spec.tsx
@@ -0,0 +1,142 @@
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import * as _ from 'lodash';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { testSubscription, testPackageManifest } from '../../../../mocks';
+import { SubscriptionModel } from '../../../models';
+import { SubscriptionKind, PackageManifestKind } from '../../../types';
+import {
+ SubscriptionChannelModal,
+ SubscriptionChannelModalProps,
+} from '../subscription-channel-modal';
+
+describe('SubscriptionChannelModal', () => {
+ let subscriptionChannelModalProps: SubscriptionChannelModalProps;
+ let k8sUpdate: jest.Mock;
+ let close: jest.Mock;
+ let cancel: jest.Mock;
+ let subscription: SubscriptionKind;
+ let pkg: PackageManifestKind;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ k8sUpdate = jest.fn().mockResolvedValue({});
+ close = jest.fn();
+ cancel = jest.fn();
+ subscription = _.cloneDeep(testSubscription);
+ pkg = _.cloneDeep(testPackageManifest);
+ pkg.status.defaultChannel = 'stable';
+ pkg.status.channels = [
+ {
+ name: 'stable',
+ currentCSV: 'testapp',
+ currentCSVDesc: {
+ displayName: 'Test App',
+ icon: [{ mediatype: 'image/png', base64data: '' }],
+ version: '0.0.1',
+ provider: {
+ name: 'CoreOS, Inc',
+ },
+ installModes: [],
+ },
+ },
+ {
+ name: 'nightly',
+ currentCSV: 'testapp-nightly',
+ currentCSVDesc: {
+ displayName: 'Test App',
+ icon: [{ mediatype: 'image/png', base64data: '' }],
+ version: '0.0.1',
+ provider: {
+ name: 'CoreOS, Inc',
+ },
+ installModes: [],
+ },
+ },
+ ];
+
+ subscriptionChannelModalProps = {
+ subscription,
+ pkg,
+ k8sUpdate,
+ close,
+ cancel,
+ };
+ });
+
+ it('displays modal title and save button', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Change Subscription update channel')).toBeVisible();
+ expect(screen.getByRole('button', { name: 'Save' })).toBeVisible();
+ });
+
+ it('displays radio button for each available channel', () => {
+ renderWithProviders();
+
+ const radioButtons = screen.getAllByRole('radio');
+ expect(radioButtons).toHaveLength(pkg.status.channels.length);
+ expect(screen.getByRole('radio', { name: /stable/i })).toBeVisible();
+ expect(screen.getByRole('radio', { name: /nightly/i })).toBeVisible();
+ });
+
+ it('updates subscription when different channel is selected and form is submitted', async () => {
+ renderWithProviders();
+
+ const nightlyRadio = screen.getByRole('radio', { name: /nightly/i });
+ fireEvent.click(nightlyRadio);
+
+ const form = screen.getByRole('button', { name: 'Save' }).closest('form');
+ if (form) {
+ fireEvent.submit(form);
+ }
+
+ await waitFor(() => {
+ expect(k8sUpdate).toHaveBeenCalledTimes(1);
+ });
+
+ expect(k8sUpdate).toHaveBeenCalledWith(
+ SubscriptionModel,
+ expect.objectContaining({
+ spec: expect.objectContaining({
+ channel: 'nightly',
+ }),
+ }),
+ );
+ });
+
+ it('calls close callback after successful form submission', async () => {
+ renderWithProviders();
+
+ const nightlyRadio = screen.getByRole('radio', { name: /nightly/i });
+ fireEvent.click(nightlyRadio);
+
+ const form = screen.getByRole('button', { name: 'Save' }).closest('form');
+ if (form) {
+ fireEvent.submit(form);
+ }
+
+ await waitFor(() => {
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('disables submit button when no channel change is made', () => {
+ renderWithProviders();
+
+ const saveButton = screen.getByRole('button', { name: 'Save' });
+ expect(saveButton).toBeDisabled();
+ });
+
+ it('enables submit button when channel selection changes', () => {
+ renderWithProviders();
+
+ const saveButton = screen.getByRole('button', { name: 'Save' });
+ expect(saveButton).toBeDisabled();
+
+ const nightlyRadio = screen.getByRole('radio', { name: /nightly/i });
+ fireEvent.click(nightlyRadio);
+
+ expect(saveButton).not.toBeDisabled();
+ });
+});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/uninstall-operator-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/uninstall-operator-modal.spec.tsx
new file mode 100644
index 00000000000..0f92a62a59b
--- /dev/null
+++ b/frontend/packages/operator-lifecycle-manager/src/components/modals/__tests__/uninstall-operator-modal.spec.tsx
@@ -0,0 +1,134 @@
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import * as _ from 'lodash';
+import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
+import { useAccessReview } from '@console/internal/components/utils/rbac';
+import { useOperands } from '@console/shared/src/hooks/useOperands';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { testSubscription, dummyPackageManifest } from '../../../../mocks';
+import { ClusterServiceVersionModel, SubscriptionModel } from '../../../models';
+import { UninstallOperatorModal, UninstallOperatorModalProps } from '../uninstall-operator-modal';
+
+jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({
+ useK8sWatchResource: jest.fn(),
+}));
+
+jest.mock('@console/internal/components/utils/rbac', () => ({
+ useAccessReview: jest.fn(),
+}));
+
+jest.mock('@console/shared/src/hooks/useOperands', () => ({
+ useOperands: jest.fn(),
+}));
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key.replace(/^[^~]+~/, ''), // Remove namespace prefix (e.g., "olm~")
+ i18n: { language: 'en' },
+ }),
+ withTranslation: () => (component) => component,
+ Trans: () => null,
+}));
+
+jest.mock('@console/dynamic-plugin-sdk/src/utils/k8s', () => ({
+ k8sGetResource: jest.fn(),
+}));
+
+describe(UninstallOperatorModal.name, () => {
+ let uninstallOperatorModalProps: UninstallOperatorModalProps;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ uninstallOperatorModalProps = {
+ subscription: {
+ ..._.cloneDeep(testSubscription),
+ status: { installedCSV: 'testapp.v1.0.0' },
+ },
+ k8sKill: jest.fn().mockResolvedValue({}),
+ k8sGet: jest.fn().mockResolvedValue({}),
+ k8sPatch: jest.fn().mockResolvedValue({}),
+ close: jest.fn(),
+ cancel: jest.fn(),
+ };
+
+ (useK8sWatchResource as jest.Mock).mockReturnValue([dummyPackageManifest, true, null]);
+ (useAccessReview as jest.Mock).mockReturnValue(false);
+ (useOperands as jest.Mock).mockReturnValue([[], true, '']);
+ });
+
+ it('displays modal title and uninstall button when rendered', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Uninstall Operator?')).toBeVisible();
+ expect(screen.getByRole('button', { name: 'Uninstall' })).toBeVisible();
+ });
+
+ it('deletes subscription when form is submitted', async () => {
+ renderWithProviders();
+
+ fireEvent.submit(screen.getByRole('form'));
+
+ await waitFor(() => {
+ expect(uninstallOperatorModalProps.k8sKill).toHaveBeenCalledTimes(2);
+ });
+
+ expect(uninstallOperatorModalProps.k8sKill).toHaveBeenCalledWith(
+ SubscriptionModel,
+ uninstallOperatorModalProps.subscription,
+ {},
+ expect.objectContaining({
+ kind: 'DeleteOptions',
+ apiVersion: 'v1',
+ propagationPolicy: 'Foreground',
+ }),
+ );
+ });
+
+ it('deletes ClusterServiceVersion when form is submitted', async () => {
+ renderWithProviders();
+
+ fireEvent.submit(screen.getByRole('form'));
+
+ await waitFor(() => {
+ expect(uninstallOperatorModalProps.k8sKill).toHaveBeenCalledTimes(2);
+ });
+
+ expect(uninstallOperatorModalProps.k8sKill).toHaveBeenCalledWith(
+ ClusterServiceVersionModel,
+ expect.objectContaining({
+ metadata: expect.objectContaining({
+ name: 'testapp.v1.0.0',
+ namespace: testSubscription.metadata.namespace,
+ }),
+ }),
+ {},
+ expect.objectContaining({
+ kind: 'DeleteOptions',
+ apiVersion: 'v1',
+ propagationPolicy: 'Foreground',
+ }),
+ );
+ });
+
+ it('does not delete ClusterServiceVersion when installedCSV is missing from subscription', async () => {
+ renderWithProviders(
+ ,
+ );
+
+ fireEvent.submit(screen.getByRole('form'));
+
+ await waitFor(() => {
+ expect(uninstallOperatorModalProps.k8sKill).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('calls close callback after successful form submission', async () => {
+ renderWithProviders();
+
+ fireEvent.submit(screen.getByRole('form'));
+
+ await waitFor(() => {
+ expect(uninstallOperatorModalProps.close).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx
deleted file mode 100644
index b9969d4a94e..00000000000
--- a/frontend/packages/operator-lifecycle-manager/src/components/modals/installplan-approval-modal.spec.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import { Radio } from '@patternfly/react-core';
-import { ShallowWrapper, shallow } from 'enzyme';
-import * as _ from 'lodash';
-import {
- ModalTitle,
- ModalSubmitFooter,
- ModalBody,
-} from '@console/internal/components/factory/modal';
-import * as k8sModelsModule from '@console/internal/module/k8s/k8s-models';
-import { testSubscription, testInstallPlan } from '../../../mocks';
-import { SubscriptionModel, InstallPlanModel } from '../../models';
-import { SubscriptionKind, InstallPlanApproval } from '../../types';
-import {
- InstallPlanApprovalModal,
- InstallPlanApprovalModalProps,
-} from './installplan-approval-modal';
-import Spy = jasmine.Spy;
-
-describe(InstallPlanApprovalModal.name, () => {
- let wrapper: ShallowWrapper;
- let k8sUpdate: Spy;
- let close: Spy;
- let cancel: Spy;
- let subscription: SubscriptionKind;
-
- beforeEach(() => {
- k8sUpdate = jasmine.createSpy().and.returnValue(Promise.resolve());
- close = jasmine.createSpy();
- cancel = jasmine.createSpy();
- subscription = _.cloneDeep(testSubscription);
-
- wrapper = shallow(
- ,
- );
- });
-
- it('renders a modal form', () => {
- expect(wrapper.find('form').props().name).toEqual('form');
- expect(wrapper.find(ModalTitle).exists()).toBe(true);
- expect(wrapper.find(ModalSubmitFooter).props().submitText).toEqual('Save');
- });
-
- it('renders a radio button for each available approval strategy', () => {
- expect(wrapper.find(ModalBody).find(Radio).length).toEqual(2);
- });
-
- it('pre-selects the approval strategy option that is currently being used by a subscription', () => {
- expect(wrapper.find(ModalBody).find(Radio).at(0).props().isChecked).toBe(true);
- });
-
- it('pre-selects the approval strategy option that is currently being used by an install plan', () => {
- wrapper = wrapper.setProps({ obj: _.cloneDeep(testInstallPlan) });
- expect(wrapper.find(ModalBody).find(Radio).at(0).props().isChecked).toBe(true);
- });
-
- it('calls `props.k8sUpdate` to update the subscription when form is submitted', () => {
- spyOn(k8sModelsModule, 'modelFor').and.returnValue(SubscriptionModel);
- k8sUpdate.and.callFake((modelArg, subscriptionArg) => {
- expect(modelArg).toEqual(SubscriptionModel);
- expect(subscriptionArg?.spec?.installPlanApproval).toEqual(InstallPlanApproval.Manual);
- return Promise.resolve();
- });
- wrapper
- .find(ModalBody)
- .find(Radio)
- .at(1)
- .props()
- .onChange({ target: { value: InstallPlanApproval.Manual } } as any, true);
- wrapper.find('form').simulate('submit', new Event('submit'));
- });
-
- it('calls `props.k8sUpdate` to update the install plan when form is submitted', () => {
- wrapper = wrapper.setProps({ obj: _.cloneDeep(testInstallPlan) });
- spyOn(k8sModelsModule, 'modelFor').and.returnValue(InstallPlanModel);
- k8sUpdate.and.callFake((modelArg, installPlanArg) => {
- expect(modelArg).toEqual(InstallPlanModel);
- expect(installPlanArg?.spec?.approval).toEqual(InstallPlanApproval.Manual);
- return Promise.resolve();
- });
- wrapper
- .find(ModalBody)
- .find(Radio)
- .at(1)
- .props()
- .onChange({ target: { value: InstallPlanApproval.Manual } } as any, true);
- wrapper.find('form').simulate('submit', new Event('submit'));
- });
-
- it('calls `props.close` after successful submit', (done) => {
- close.and.callFake(() => {
- done();
- });
-
- wrapper.find('form').simulate('submit', new Event('submit'));
- });
-});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx
deleted file mode 100644
index 008a757eead..00000000000
--- a/frontend/packages/operator-lifecycle-manager/src/components/modals/subscription-channel-modal.spec.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Radio } from '@patternfly/react-core';
-import { ShallowWrapper, shallow } from 'enzyme';
-import * as _ from 'lodash';
-import {
- ModalTitle,
- ModalSubmitFooter,
- ModalBody,
-} from '@console/internal/components/factory/modal';
-import { testSubscription, testPackageManifest } from '../../../mocks';
-import { SubscriptionModel } from '../../models';
-import { SubscriptionKind, PackageManifestKind } from '../../types';
-import {
- SubscriptionChannelModal,
- SubscriptionChannelModalProps,
-} from './subscription-channel-modal';
-import Spy = jasmine.Spy;
-
-describe('SubscriptionChannelModal', () => {
- let wrapper: ShallowWrapper;
- let k8sUpdate: Spy;
- let close: Spy;
- let cancel: Spy;
- let subscription: SubscriptionKind;
- let pkg: PackageManifestKind;
-
- beforeEach(() => {
- k8sUpdate = jasmine.createSpy('k8sUpdate').and.returnValue(Promise.resolve());
- close = jasmine.createSpy('close');
- cancel = jasmine.createSpy('cancel');
- subscription = _.cloneDeep(testSubscription);
- pkg = _.cloneDeep(testPackageManifest);
- pkg.status.defaultChannel = 'stable';
- pkg.status.channels = [
- {
- name: 'stable',
- currentCSV: 'testapp',
- currentCSVDesc: {
- displayName: 'Test App',
- icon: [{ mediatype: 'image/png', base64data: '' }],
- version: '0.0.1',
- provider: {
- name: 'CoreOS, Inc',
- },
- installModes: [],
- },
- },
- {
- name: 'nightly',
- currentCSV: 'testapp-nightly',
- currentCSVDesc: {
- displayName: 'Test App',
- icon: [{ mediatype: 'image/png', base64data: '' }],
- version: '0.0.1',
- provider: {
- name: 'CoreOS, Inc',
- },
- installModes: [],
- },
- },
- ];
-
- wrapper = shallow(
- ,
- );
- });
-
- it('renders a modal form', () => {
- expect(wrapper.find('form').props().name).toEqual('form');
- expect(wrapper.find(ModalTitle).exists()).toBe(true);
- expect(wrapper.find(ModalSubmitFooter).props().submitText).toEqual('Save');
- });
-
- it('renders a radio button for each available channel in the package', () => {
- expect(wrapper.find(ModalBody).find(Radio).length).toEqual(pkg.status.channels.length);
- });
-
- it('calls `props.k8sUpdate` to update the subscription when form is submitted', (done) => {
- k8sUpdate.and.callFake((model, obj) => {
- expect(model).toEqual(SubscriptionModel);
- expect(obj.spec.channel).toEqual('nightly');
- done();
- return Promise.resolve();
- });
- wrapper
- .find(Radio)
- .at(1)
- .props()
- .onChange({ target: { value: 'nightly' } } as any, true);
- wrapper.find('form').simulate('submit', new Event('submit'));
- });
-
- it('calls `props.close` after successful submit', (done) => {
- close.and.callFake(() => {
- done();
- });
-
- wrapper.find('form').simulate('submit', new Event('submit'));
- });
-});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.spec.tsx
deleted file mode 100644
index a752a4eb3f4..00000000000
--- a/frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.spec.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import { ReactWrapper, mount } from 'enzyme';
-import * as _ from 'lodash';
-import { act } from 'react-dom/test-utils';
-import { ModalTitle, ModalSubmitFooter } from '@console/internal/components/factory/modal';
-import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
-import { useAccessReview } from '@console/internal/components/utils/rbac';
-import { useOperands } from '@console/shared/src/hooks/useOperands';
-import { testSubscription, dummyPackageManifest } from '../../../mocks';
-import { ClusterServiceVersionModel, SubscriptionModel } from '../../models';
-import { SubscriptionKind } from '../../types';
-import { UninstallOperatorModal, UninstallOperatorModalProps } from './uninstall-operator-modal';
-import Spy = jasmine.Spy;
-
-jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({
- useK8sWatchResource: jest.fn(),
-}));
-
-jest.mock('@console/internal/components/utils/rbac', () => ({
- useAccessReview: jest.fn(),
-}));
-
-jest.mock('@console/shared/src/hooks/useOperands', () => ({
- useOperands: jest.fn(),
-}));
-
-jest.mock('react-i18next', () => {
- const reactI18next = jest.requireActual('react-i18next');
- return {
- ...reactI18next,
- Trans: () => null,
- };
-});
-
-jest.mock('@console/dynamic-plugin-sdk/src/utils/k8s', () => ({
- k8sGetResource: jest.fn(),
-}));
-
-describe(UninstallOperatorModal.name, () => {
- let wrapper: ReactWrapper;
- let k8sKill: Spy;
- let k8sGet: Spy;
- let k8sPatch: Spy;
- let close: Spy;
- let cancel: Spy;
- let subscription: SubscriptionKind;
-
- const spyAndExpect = (spy: Spy) => (returnValue: any) =>
- new Promise((resolve) =>
- spy.and.callFake((...args) => {
- resolve(args);
- return returnValue;
- }),
- );
-
- beforeEach(() => {
- k8sKill = jasmine.createSpy('k8sKill').and.returnValue(Promise.resolve());
- k8sGet = jasmine.createSpy('k8sGet').and.returnValue(Promise.resolve());
- k8sPatch = jasmine.createSpy('k8sPatch').and.returnValue(Promise.resolve());
- close = jasmine.createSpy('close');
- cancel = jasmine.createSpy('cancel');
- subscription = { ..._.cloneDeep(testSubscription), status: { installedCSV: 'testapp.v1.0.0' } };
-
- (useK8sWatchResource as jest.Mock).mockReturnValue([dummyPackageManifest, true, null]);
- (useAccessReview as jest.Mock).mockReturnValue(false);
- (useOperands as jest.Mock).mockReturnValue([[], true, '']);
-
- // React.useEffect is not supported by Enzyme's shallow rendering, switching to mount
- wrapper = mount(
- ,
- );
- });
-
- afterEach(() => {
- jest.resetAllMocks();
- });
-
- it('renders a modal form', () => {
- expect(wrapper.find('form').props().name).toEqual('form');
- expect(wrapper.find(ModalTitle).exists()).toBe(true);
- expect(wrapper.find(ModalSubmitFooter).props().submitText).toEqual('Uninstall');
- });
-
- it('calls `props.k8sKill` to delete the subscription when form is submitted', async () => {
- await act(async () => {
- wrapper.find('form').simulate('submit', new Event('submit'));
- await spyAndExpect(close)(null);
- });
-
- expect(k8sKill).toHaveBeenCalledTimes(2);
- expect(k8sKill.calls.argsFor(0)[0]).toEqual(SubscriptionModel);
- expect(k8sKill.calls.argsFor(0)[1]).toEqual(subscription);
- expect(k8sKill.calls.argsFor(0)[2]).toEqual({});
- expect(k8sKill.calls.argsFor(0)[3]).toEqual({
- kind: 'DeleteOptions',
- apiVersion: 'v1',
- propagationPolicy: 'Foreground',
- });
- });
-
- it('calls `props.k8sKill` to delete the `ClusterServiceVersion` from the subscription namespace when form is submitted', async () => {
- await act(async () => {
- wrapper.find('form').simulate('submit', new Event('submit'));
- await spyAndExpect(close)(null);
- });
-
- expect(k8sKill).toHaveBeenCalledTimes(2);
- expect(k8sKill.calls.argsFor(1)[0]).toEqual(ClusterServiceVersionModel);
- expect(k8sKill.calls.argsFor(1)[1].metadata.namespace).toEqual(
- testSubscription.metadata.namespace,
- );
- expect(k8sKill.calls.argsFor(1)[1].metadata.name).toEqual('testapp.v1.0.0');
- expect(k8sKill.calls.argsFor(1)[2]).toEqual({});
- expect(k8sKill.calls.argsFor(1)[3]).toEqual({
- kind: 'DeleteOptions',
- apiVersion: 'v1',
- propagationPolicy: 'Foreground',
- });
- });
-
- it('does not call `props.k8sKill` to delete `ClusterServiceVersion` if `status.installedCSV` field missing from subscription', async () => {
- wrapper = wrapper.setProps({ subscription: testSubscription });
-
- await act(async () => {
- wrapper.find('form').simulate('submit', new Event('submit'));
- await spyAndExpect(close)(null);
- });
-
- expect(k8sKill).toHaveBeenCalledTimes(1);
- });
-
- it('adds delete options with `propagationPolicy`', async () => {
- await act(async () => {
- wrapper.find('form').simulate('submit', new Event('submit'));
- spyAndExpect(close)(null);
- });
-
- expect(k8sKill).toHaveBeenCalledTimes(2);
- expect(k8sKill.calls.argsFor(0)[3]).toEqual({
- kind: 'DeleteOptions',
- apiVersion: 'v1',
- propagationPolicy: 'Foreground',
- });
- expect(k8sKill.calls.argsFor(1)[3]).toEqual({
- kind: 'DeleteOptions',
- apiVersion: 'v1',
- propagationPolicy: 'Foreground',
- });
- });
-
- it('calls `props.close` after successful submit', async () => {
- await act(async () => {
- wrapper.find('form').simulate('submit', new Event('submit'));
- spyAndExpect(close)(null);
- });
- });
-});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/__tests__/create-operand.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/__tests__/create-operand.spec.tsx
new file mode 100644
index 00000000000..2e86cd8471e
--- /dev/null
+++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/__tests__/create-operand.spec.tsx
@@ -0,0 +1,216 @@
+import * as Router from 'react-router-dom-v5-compat';
+import { CreateYAML } from '@console/internal/components/create-yaml';
+import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
+import { SyncedEditor } from '@console/shared/src/components/synced-editor';
+import { EditorType } from '@console/shared/src/components/synced-editor/editor-toggle';
+import { useK8sModel } from '@console/shared/src/hooks/useK8sModel';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { testClusterServiceVersion, testModel, testCRD } from '../../../../mocks';
+import { CreateOperand } from '../create-operand';
+import { OperandYAML } from '../operand-yaml';
+
+jest.mock('@console/shared/src/hooks/useK8sModel', () => ({ useK8sModel: jest.fn() }));
+
+jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({
+ useK8sWatchResource: jest.fn(),
+}));
+
+jest.mock('react-router-dom-v5-compat', () => ({
+ ...jest.requireActual('react-router-dom-v5-compat'),
+ useParams: jest.fn(),
+}));
+
+jest.mock('@console/dynamic-plugin-sdk', () => ({
+ useActivePerspective: jest.fn(() => ['admin']),
+}));
+
+jest.mock('@console/shared/src/components/synced-editor', () => ({
+ SyncedEditor: jest.fn(() => null),
+}));
+
+jest.mock('@console/internal/components/create-yaml', () => ({
+ CreateYAML: jest.fn(() => null),
+}));
+
+jest.mock('@console/shared/src/components/heading/PageHeading', () => ({
+ PageHeading: jest.fn(() => null),
+}));
+
+const mockUseK8sModel = useK8sModel as jest.Mock;
+const mockUseK8sWatchResource = useK8sWatchResource as jest.Mock;
+const mockSyncedEditor = (SyncedEditor as unknown) as jest.Mock;
+const mockCreateYAML = (CreateYAML as unknown) as jest.Mock;
+
+describe('CreateOperand', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.spyOn(Router, 'useParams').mockReturnValue({ ns: 'default', plural: 'testresources' });
+ mockUseK8sModel.mockReturnValue([testModel, true]);
+ mockUseK8sWatchResource.mockReturnValue([testCRD, true, undefined]);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('configures SyncedEditor with YAML as initialType when initialEditorType is YAML', () => {
+ // Arrange
+ renderWithProviders(
+ ,
+ );
+
+ // Assert
+ expect(mockSyncedEditor).toHaveBeenCalledTimes(1);
+ const [syncedEditorProps] = mockSyncedEditor.mock.calls[0];
+ expect(syncedEditorProps.initialType).toEqual(EditorType.YAML);
+ });
+
+ it('configures SyncedEditor with Form as initialType when initialEditorType is Form', () => {
+ // Arrange
+ renderWithProviders(
+ ,
+ );
+
+ // Assert
+ expect(mockSyncedEditor).toHaveBeenCalledTimes(1);
+ const [syncedEditorProps] = mockSyncedEditor.mock.calls[0];
+ expect(syncedEditorProps.initialType).toEqual(EditorType.Form);
+ });
+
+ it('passes sample data to SyncedEditor when CSV contains alm-examples annotation', () => {
+ // Arrange
+ const csvWithExamples = {
+ ...testClusterServiceVersion,
+ metadata: {
+ ...testClusterServiceVersion.metadata,
+ annotations: {
+ ...testClusterServiceVersion.metadata.annotations,
+ 'alm-examples': JSON.stringify([
+ {
+ apiVersion: 'testapp.coreos.com/v1alpha1',
+ kind: 'TestResource',
+ metadata: { name: 'example-resource' },
+ spec: { size: 3 },
+ },
+ ]),
+ },
+ },
+ };
+
+ renderWithProviders(
+ ,
+ );
+
+ // Assert
+ expect(mockSyncedEditor).toHaveBeenCalledTimes(1);
+ const [syncedEditorProps] = mockSyncedEditor.mock.calls[0];
+ expect(syncedEditorProps.initialData).toMatchObject({
+ kind: 'TestResource',
+ metadata: expect.objectContaining({
+ name: 'example-resource',
+ }),
+ spec: expect.objectContaining({
+ size: 3,
+ }),
+ });
+ });
+
+ it('provides onChangeEditorType callback to SyncedEditor', () => {
+ // Arrange
+ renderWithProviders(
+ ,
+ );
+
+ // Assert
+ expect(mockSyncedEditor).toHaveBeenCalledTimes(1);
+ const [syncedEditorProps] = mockSyncedEditor.mock.calls[0];
+ expect(syncedEditorProps.onChangeEditorType).toBeDefined();
+ expect(typeof syncedEditorProps.onChangeEditorType).toBe('function');
+ });
+});
+
+describe('OperandYAML', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders CreateYAML with hideHeader prop set to true', () => {
+ renderWithProviders();
+
+ expect(mockCreateYAML).toHaveBeenCalledTimes(1);
+ const [createYAMLProps] = mockCreateYAML.mock.calls[0];
+ expect(createYAMLProps.hideHeader).toBe(true);
+ });
+
+ it('passes initialYAML as template prop to CreateYAML', () => {
+ const initialYAML = 'apiVersion: v1\nkind: Pod';
+
+ renderWithProviders();
+
+ expect(mockCreateYAML).toHaveBeenCalledTimes(1);
+ const [createYAMLProps] = mockCreateYAML.mock.calls[0];
+ expect(createYAMLProps.template).toEqual(initialYAML);
+ });
+
+ it('defaults initialYAML to empty string when not provided', () => {
+ renderWithProviders();
+
+ expect(mockCreateYAML).toHaveBeenCalledTimes(1);
+ const [createYAMLProps] = mockCreateYAML.mock.calls[0];
+ expect(createYAMLProps.template).toEqual('');
+ });
+
+ it('passes onChange callback to CreateYAML', () => {
+ const onChange = jest.fn();
+
+ renderWithProviders();
+
+ expect(mockCreateYAML).toHaveBeenCalledTimes(1);
+ const [createYAMLProps] = mockCreateYAML.mock.calls[0];
+ expect(createYAMLProps.onChange).toEqual(onChange);
+ });
+
+ it('passes resourceObjPath function when next prop is provided', () => {
+ const next = '/next-path';
+
+ renderWithProviders();
+
+ expect(mockCreateYAML).toHaveBeenCalledTimes(1);
+ const [createYAMLProps] = mockCreateYAML.mock.calls[0];
+ expect(createYAMLProps.resourceObjPath).toBeDefined();
+ expect(typeof createYAMLProps.resourceObjPath).toBe('function');
+ expect(createYAMLProps.resourceObjPath()).toEqual(next);
+ });
+
+ it('does not pass resourceObjPath when next prop is not provided', () => {
+ renderWithProviders();
+
+ expect(mockCreateYAML).toHaveBeenCalledTimes(1);
+ const [createYAMLProps] = mockCreateYAML.mock.calls[0];
+ expect(createYAMLProps.resourceObjPath).toBeUndefined();
+ });
+});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/__tests__/index.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/__tests__/index.spec.tsx
new file mode 100644
index 00000000000..f9d5852c74c
--- /dev/null
+++ b/frontend/packages/operator-lifecycle-manager/src/components/operand/__tests__/index.spec.tsx
@@ -0,0 +1,282 @@
+import { screen } from '@testing-library/react';
+import * as _ from 'lodash';
+import * as ReactRouter from 'react-router-dom-v5-compat';
+import { DetailsPage } from '@console/internal/components/factory';
+import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
+import { testResourceInstance, testClusterServiceVersion } from '../../../../mocks';
+import { ClusterServiceVersionModel } from '../../../models';
+import {
+ OperandTableRow,
+ OperandDetailsPage,
+ ProvidedAPIsPage,
+ ProvidedAPIPage,
+ OperandStatus,
+} from '../index';
+
+jest.mock('react-router-dom-v5-compat', () => ({
+ ...jest.requireActual('react-router-dom-v5-compat'),
+ useParams: jest.fn(),
+ useLocation: jest.fn(),
+}));
+
+jest.mock('@console/shared/src/hooks/useK8sModels', () => ({
+ useK8sModels: () => [
+ {
+ 'testapp.coreos.com~v1alpha1~TestResource': {
+ abbr: 'TR',
+ apiGroup: 'testapp.coreos.com',
+ apiVersion: 'v1alpha1',
+ crd: true,
+ kind: 'TestResource',
+ label: 'Test Resource',
+ labelPlural: 'Test Resources',
+ namespaced: true,
+ plural: 'testresources',
+ verbs: ['create'],
+ },
+ },
+ false,
+ null,
+ ],
+}));
+
+jest.mock('@console/shared/src/hooks/useK8sModel', () => ({
+ useK8sModel: () => [
+ {
+ abbr: 'TR',
+ apiGroup: 'testapp.coreos.com',
+ apiVersion: 'v1alpha1',
+ crd: true,
+ kind: 'TestResource',
+ label: 'Test Resource',
+ labelPlural: 'Test Resources',
+ namespaced: true,
+ plural: 'testresources',
+ verbs: ['create'],
+ },
+ false,
+ null,
+ ],
+}));
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useDispatch: () => jest.fn(),
+}));
+
+jest.mock('@console/internal/components/factory', () => ({
+ ...jest.requireActual('@console/internal/components/factory'),
+ DetailsPage: jest.fn(() => null),
+}));
+
+const mockDetailsPage = DetailsPage as jest.Mock;
+
+describe('OperandTableRow', () => {
+ it('renders operand name and namespace when provided', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(testResourceInstance.metadata.name)).toBeInTheDocument();
+ expect(screen.getByText(testResourceInstance.metadata.namespace)).toBeInTheDocument();
+ });
+
+ it('renders operand kind', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(testResourceInstance.kind)).toBeInTheDocument();
+ });
+});
+
+describe('OperandStatus', () => {
+ it('displays status when status field is present', () => {
+ const operand = {
+ status: {
+ status: 'Running',
+ state: 'Degraded',
+ conditions: [
+ {
+ type: 'Failed',
+ status: 'True',
+ },
+ ],
+ },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByText('Status')).toBeInTheDocument();
+ expect(screen.getByTestId('status-text')).toHaveTextContent('Running');
+ });
+
+ it('displays phase when phase field is present', () => {
+ const operand = {
+ status: {
+ phase: 'Running',
+ status: 'Installed',
+ state: 'Degraded',
+ conditions: [
+ {
+ type: 'Failed',
+ status: 'True',
+ },
+ ],
+ },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByText('Phase')).toBeInTheDocument();
+ expect(screen.getByTestId('status-text')).toHaveTextContent('Running');
+ });
+
+ it('displays state when only state field is present', () => {
+ const operand = {
+ status: {
+ state: 'Running',
+ conditions: [
+ {
+ type: 'Failed',
+ status: 'True',
+ },
+ ],
+ },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByText('State')).toBeInTheDocument();
+ expect(screen.getByTestId('status-text')).toHaveTextContent('Running');
+ });
+
+ it('displays condition type when condition status is True', () => {
+ const operand = {
+ status: {
+ conditions: [
+ {
+ type: 'Failed',
+ status: 'False',
+ },
+ {
+ type: 'Running',
+ status: 'True',
+ },
+ ],
+ },
+ };
+
+ renderWithProviders();
+
+ expect(screen.getByText('Condition')).toBeInTheDocument();
+ expect(screen.getByTestId('status-text')).toHaveTextContent('Running');
+ });
+
+ it('displays dash when no status information is available', () => {
+ const operand = {};
+
+ renderWithProviders();
+
+ expect(screen.getByText('-')).toBeInTheDocument();
+ });
+});
+
+describe('OperandDetailsPage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ jest.spyOn(ReactRouter, 'useParams').mockReturnValue({
+ ns: 'default',
+ appName: 'testapp',
+ plural: 'testapp.coreos.com~v1alpha1~TestResource',
+ name: 'my-test-resource',
+ });
+
+ jest.spyOn(ReactRouter, 'useLocation').mockReturnValue({
+ pathname: `/k8s/ns/default/${ClusterServiceVersionModel.plural}/testapp/testapp.coreos.com~v1alpha1~TestResource/my-test-resource`,
+ });
+ });
+
+ it('renders without errors', () => {
+ expect(() => {
+ renderWithProviders();
+ }).not.toThrow();
+ });
+
+ it('configures DetailsPage with Details, YAML, Resources, and Events tabs', () => {
+ renderWithProviders();
+
+ expect(mockDetailsPage).toHaveBeenCalledTimes(1);
+ const [detailsPageProps] = mockDetailsPage.mock.calls[0];
+
+ expect(detailsPageProps.pages).toHaveLength(4);
+ expect(detailsPageProps.pages[0].nameKey).toEqual('public~Details');
+ expect(detailsPageProps.pages[1].nameKey).toEqual('public~YAML');
+ expect(detailsPageProps.pages[2].nameKey).toEqual('olm~Resources');
+ expect(detailsPageProps.pages[3].nameKey).toEqual('public~Events');
+ });
+});
+
+describe('ProvidedAPIsPage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ jest.spyOn(ReactRouter, 'useParams').mockReturnValue({
+ ns: 'default',
+ appName: 'testapp',
+ });
+
+ jest.spyOn(ReactRouter, 'useLocation').mockReturnValue({
+ pathname: `/k8s/ns/default/${ClusterServiceVersionModel.plural}/testapp/instances`,
+ });
+ });
+
+ it('renders create dropdown when CSV has multiple owned CRDs', () => {
+ const csv = _.cloneDeep(testClusterServiceVersion);
+ csv.spec.customresourcedefinitions.owned.push({
+ name: 'foobars.testapp.coreos.com',
+ displayName: 'Foo Bars',
+ version: 'v1',
+ kind: 'FooBar',
+ });
+
+ renderWithProviders();
+
+ expect(screen.getByText('Create new')).toBeInTheDocument();
+ });
+});
+
+describe('ProvidedAPIPage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ jest.spyOn(ReactRouter, 'useParams').mockReturnValue({
+ ns: 'default',
+ appName: 'testapp',
+ plural: 'TestResource',
+ });
+
+ jest.spyOn(ReactRouter, 'useLocation').mockReturnValue({
+ pathname: `/k8s/ns/default/${ClusterServiceVersionModel.plural}/testapp/TestResource`,
+ });
+ });
+
+ it('renders create button with correct text for single CRD', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Create Test Resource')).toBeVisible();
+ });
+});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.spec.tsx
deleted file mode 100644
index 09a62e88da9..00000000000
--- a/frontend/packages/operator-lifecycle-manager/src/components/operand/create-operand.spec.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import { Alert, Button } from '@patternfly/react-core';
-import { shallow, ShallowWrapper } from 'enzyme';
-import { safeDump } from 'js-yaml';
-import * as _ from 'lodash';
-import * as Router from 'react-router-dom-v5-compat';
-import { CreateYAML } from '@console/internal/components/create-yaml';
-import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
-import { CustomResourceDefinitionModel } from '@console/internal/models';
-import * as k8s from '@console/internal/module/k8s';
-import { EditorType } from '@console/shared/src/components/synced-editor/editor-toggle';
-import { useK8sModel } from '@console/shared/src/hooks/useK8sModel';
-import { referenceForProvidedAPI } from '..';
-import {
- testClusterServiceVersion,
- testResourceInstance,
- testModel,
- testCRD,
-} from '../../../mocks';
-import { CreateOperand, CreateOperandProps } from './create-operand';
-import { OperandForm, OperandFormProps } from './operand-form';
-import { OperandYAML, OperandYAMLProps } from './operand-yaml';
-import Spy = jasmine.Spy;
-
-jest.mock('@console/shared/src/hooks/useK8sModel', () => ({ useK8sModel: jest.fn() }));
-
-(useK8sModel as jest.Mock).mockImplementation(() => [testModel, false]);
-
-jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({
- useK8sWatchResource: jest.fn(),
-}));
-
-jest.mock('react-router-dom-v5-compat', () => ({
- ...jest.requireActual('react-router-dom-v5-compat'),
- useParams: jest.fn(),
-}));
-
-(useK8sWatchResource as jest.Mock).mockImplementation((res) => [
- res.kind === CustomResourceDefinitionModel.kind ? testCRD : testClusterServiceVersion,
- true,
- undefined,
-]);
-
-xdescribe('[https://issues.redhat.com/browse/CONSOLE-2137] CreateOperand', () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- wrapper = shallow(
- ,
- );
- });
-
- it('renders YAML editor by default', () => {
- expect(wrapper.find(Button).childAt(0).text()).toEqual('Edit Form');
- expect(wrapper.find(OperandYAML).exists()).toBe(true);
- expect(wrapper.find(OperandForm).exists()).toBe(false);
- });
-
- it('passes correct YAML to YAML editor', () => {
- const data = _.cloneDeep(testClusterServiceVersion);
- const testResourceInstanceYAML = safeDump(testResourceInstance);
- data.metadata.annotations = { 'alm-examples': JSON.stringify([testResourceInstance]) };
- wrapper = wrapper.setProps({ csv: data, loaded: true, loadError: null });
- expect(wrapper.find(OperandYAML).props().initialYAML).toEqual(testResourceInstanceYAML);
- });
-
- it('switches to form component when button is clicked', () => {
- wrapper.find(Button).simulate('click');
-
- expect(wrapper.find(Button).childAt(0).text()).toEqual('Edit YAML');
- expect(wrapper.find(OperandYAML).exists()).toBe(false);
- expect(wrapper.find(OperandForm).exists()).toBe(true);
- });
-});
-
-xdescribe('[https://issues.redhat.com/browse/CONSOLE-2136] CreateOperandForm', () => {
- let wrapper: ShallowWrapper;
-
- const spyAndExpect = (spy: Spy) => (returnValue: any) =>
- new Promise((resolve) =>
- spy.and.callFake((...args) => {
- resolve(args);
- return returnValue;
- }),
- );
-
- beforeEach(() => {
- jest.spyOn(Router, 'useParams').mockReturnValue({ ns: 'default' });
- wrapper = shallow(
- ,
- );
- });
-
- it('renders form', () => {
- expect(
- referenceForProvidedAPI(testClusterServiceVersion.spec.customresourcedefinitions.owned[0]),
- ).toEqual(k8s.referenceForModel(testModel));
-
- expect(wrapper.find('form').exists()).toBe(true);
- });
-
- it('renders input component for each field', () => {
- wrapper.find('.co-dynamic-form__form-group').forEach((formGroup) => {
- const descriptor = testClusterServiceVersion.spec.customresourcedefinitions.owned[0].specDescriptors.find(
- (d) => d.displayName === formGroup.find('.form-label').text(),
- );
-
- expect(descriptor).toBeDefined();
- });
- });
-
- it('renders alert to use YAML editor for full control over all operand fields', () => {
- expect(wrapper.find(Alert).props().title).toEqual(
- 'Note: Some fields may not be represented in this form. Please select "Edit YAML" for full control of object creation.',
- );
- expect(wrapper.find(Alert).props().variant).toEqual('info');
- });
-
- it('calls `k8sCreate` to create new operand if form is valid', (done) => {
- spyAndExpect(spyOn(k8s, 'k8sCreate'))(Promise.resolve({}))
- .then(([model, obj]: [k8s.K8sKind, k8s.K8sResourceKind]) => {
- expect(model).toEqual(testModel);
- expect(obj.apiVersion).toEqual(k8s.apiVersionForModel(testModel));
- expect(obj.kind).toEqual(testModel.kind);
- expect(obj.metadata.name).toEqual('example');
- expect(obj.metadata.namespace).toEqual('default');
- done();
- })
- .catch((err) => fail(err));
-
- wrapper.find({ type: 'submit' }).simulate('click', new Event('click'));
- });
-
- it('displays errors if calling `k8sCreate` fails', (done) => {
- const error = { message: 'Failed to create' } as k8s.Status;
- /* eslint-disable-next-line prefer-promise-reject-errors */
- spyAndExpect(spyOn(k8s, 'k8sCreate'))(Promise.reject({ json: error }))
- .then(
- () => new Promise((resolve) => setTimeout(() => resolve(), 10)),
- )
- .then(() => {
- expect(wrapper.find(Alert).at(0).props().title).toEqual(error.message);
- done();
- })
- .catch((err) => fail(err));
-
- wrapper.find({ type: 'submit' }).simulate('click', new Event('click'));
- });
-});
-
-describe(OperandYAML.displayName, () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- wrapper = shallow();
- });
-
- it('renders `CreateYAML` component with correct props', () => {
- expect(wrapper.find(CreateYAML).props().hideHeader).toBe(true);
- });
-});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/operand/index.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/operand/index.spec.tsx
deleted file mode 100644
index e2f7f1925e9..00000000000
--- a/frontend/packages/operator-lifecycle-manager/src/components/operand/index.spec.tsx
+++ /dev/null
@@ -1,776 +0,0 @@
-import { shallow, ShallowWrapper, mount, ReactWrapper } from 'enzyme';
-import * as _ from 'lodash';
-import { Provider } from 'react-redux';
-import * as ReactRouter from 'react-router-dom-v5-compat';
-import { ListPageBody } from '@console/dynamic-plugin-sdk';
-import { Table, DetailsPage, MultiListPage } from '@console/internal/components/factory';
-import {
- ListPageCreateLink,
- ListPageCreateDropdown,
-} from '@console/internal/components/factory/ListPage/ListPageCreate';
-import ListPageFilter from '@console/internal/components/factory/ListPage/ListPageFilter';
-import ListPageHeader from '@console/internal/components/factory/ListPage/ListPageHeader';
-import {
- LabelList,
- FirehoseResourcesResult,
- ResourceLink,
-} from '@console/internal/components/utils';
-import { referenceFor, K8sResourceKind } from '@console/internal/module/k8s';
-import * as k8sModelsModule from '@console/internal/module/k8s/k8s-models';
-import store from '@console/internal/redux';
-import * as useExtensionsModule from '@console/plugin-sdk/src/api/useExtensions';
-import { LazyActionMenu } from '@console/shared';
-import { Timestamp } from '@console/shared/src/components/datetime/Timestamp';
-import {
- testCRD,
- testResourceInstance,
- testClusterServiceVersion,
- testModel,
- testConditionsDescriptor,
-} from '../../../mocks';
-import { ClusterServiceVersionModel } from '../../models';
-import { DescriptorDetailsItem, DescriptorDetailsItems } from '../descriptors';
-import { Resources } from '../k8s-resource';
-import { OperandLink } from './operand-link';
-import {
- OperandList,
- OperandListProps,
- ProvidedAPIsPage,
- ProvidedAPIsPageProps,
- OperandTableRowProps,
- OperandTableRow,
- OperandDetails,
- OperandDetailsProps,
- OperandDetailsPage,
- ProvidedAPIPage,
- ProvidedAPIPageProps,
- OperandStatus,
- OperandStatusProps,
-} from '.';
-
-jest.mock('react-router-dom-v5-compat', () => ({
- ...jest.requireActual('react-router-dom-v5-compat'),
- useParams: jest.fn(),
- useLocation: jest.fn(),
-}));
-
-jest.mock('@console/shared/src/hooks/useK8sModels', () => ({
- useK8sModels: () => [
- {
- 'testapp.coreos.com~v1alpha1~TestResource': {
- abbr: 'TR',
- apiGroup: 'testapp.coreos.com',
- apiVersion: 'v1alpha1',
- crd: true,
- kind: 'TestResource',
- label: 'Test Resource',
- labelPlural: 'Test Resources',
- namespaced: true,
- plural: 'testresources',
- verbs: ['create'],
- },
- },
- false,
- null,
- ],
-}));
-
-jest.mock('@console/shared/src/hooks/useK8sModel', () => {
- return {
- useK8sModel: (groupVersionKind) => [
- groupVersionKind === 'TestResourceRO'
- ? {
- abbr: 'TR',
- apiGroup: 'testapp.coreos.com',
- apiVersion: 'v1alpha1',
- crd: true,
- kind: 'TestResourceRO',
- label: 'Test Resource',
- labelPlural: 'Test Resources',
- namespaced: true,
- plural: 'testresources',
- verbs: ['get'],
- }
- : {
- abbr: 'TR',
- apiGroup: 'testapp.coreos.com',
- apiVersion: 'v1alpha1',
- crd: true,
- kind: 'TestResource',
- label: 'Test Resource',
- labelPlural: 'Test Resources',
- namespaced: true,
- plural: 'testresources',
- verbs: ['create'],
- },
- false,
- null,
- ],
- };
-});
-
-jest.mock('react-redux', () => ({
- ...jest.requireActual('react-redux'),
- useDispatch: () => jest.fn(),
-}));
-
-jest.mock('react-router-dom-v5-compat', () => ({
- ...jest.requireActual('react-router-dom-v5-compat'),
- useParams: jest.fn(),
- useLocation: jest.fn(),
-}));
-
-const i18nNS = 'public';
-
-describe(OperandTableRow.displayName, () => {
- let wrapper: ReactWrapper;
-
- beforeEach(() => {
- spyOn(useExtensionsModule, 'useExtensions').and.returnValue([]);
- wrapper = mount(, {
- wrappingComponent: (props) => (
-
-
-
- ),
- });
- });
-
- it('renders column for resource name', () => {
- const col = wrapper.childAt(0);
- const link = col.find(OperandLink);
-
- expect(link.props().obj).toEqual(testResourceInstance);
- });
-
- it('renders column for resource type', () => {
- const col = wrapper.childAt(1);
-
- expect(col.text()).toEqual(testResourceInstance.kind);
- });
- it('renders column for resource namespace', () => {
- const col = wrapper.childAt(2);
- const link = col.find(ResourceLink);
-
- expect(link.props().name).toEqual(testResourceInstance.metadata.namespace);
- });
- it('renders column for resource status', () => {
- const col = wrapper.childAt(3);
-
- expect(col.find(OperandStatus).props().operand).toEqual(testResourceInstance);
- });
-
- it('renders column for resource labels', () => {
- const col = wrapper.childAt(4);
- const labelList = col.find(LabelList);
-
- expect(labelList.props().kind).toEqual(testResourceInstance.kind);
- expect(labelList.props().labels).toEqual(testResourceInstance.metadata.labels);
- });
-
- it('renders column for last updated timestamp', () => {
- const col = wrapper.childAt(5);
- const timestamp = col.find(Timestamp);
-
- expect(timestamp.props().timestamp).toEqual(testResourceInstance.metadata.creationTimestamp);
- });
-
- it('renders a `LazyActionsMenu` for resource actions', () => {
- const kebab = wrapper.find(LazyActionMenu);
- expect(kebab.props().context.hasOwnProperty('operand-actions')).toBeTruthy();
- });
-});
-
-describe(OperandList.displayName, () => {
- let wrapper: ReactWrapper;
- let resources: K8sResourceKind[];
-
- beforeEach(() => {
- resources = [testResourceInstance];
- spyOn(useExtensionsModule, 'useExtensions').and.returnValue([]);
- wrapper = mount(, {
- wrappingComponent: (props) => (
-
-
-
- ),
- });
- });
-
- it('renders a `Table` of the custom resource instances of the given kind', () => {
- const table: ReactWrapper = wrapper.find(Table);
-
- expect(
- Object.keys(wrapper.props()).reduce(
- (k, prop) => _.isEqual(table.prop(prop), wrapper.prop(prop)),
- false,
- ),
- ).toBe(true);
- expect(table.props().Header().length).toEqual(7);
- expect(table.props().Header()[0].title).toEqual('Name');
- expect(table.props().Header()[1].title).toEqual('Kind');
- expect(table.props().Header()[2].title).toEqual('Namespace');
- expect(table.props().Header()[3].title).toEqual('Status');
- expect(table.props().Header()[4].title).toEqual('Labels');
- expect(table.props().Header()[5].title).toEqual('Last updated');
- expect(_.isFunction(table.props().Row)).toBe(true);
- });
-});
-
-describe(OperandDetails.displayName, () => {
- let wrapper: ShallowWrapper;
- let resourceDefinition: any;
-
- beforeEach(() => {
- resourceDefinition = {
- plural: testModel.plural,
- annotations: testCRD.metadata.annotations,
- apiVersion: testModel.apiVersion,
- };
- wrapper = shallow(
- ,
- );
- });
-
- it('renders description title', () => {
- const title = wrapper.find('SectionHeading').first().prop('text');
- expect(title).toEqual('Test Resource overview');
- });
-
- it('renders info section', () => {
- const section = wrapper.find('.co-operand-details__section.co-operand-details__section--info');
-
- expect(section.exists()).toBe(true);
- });
-
- it('does not render filtered status fields', () => {
- const crd = testClusterServiceVersion.spec.customresourcedefinitions.owned.find(
- (c) => c.name === 'testresources.testapp.coreos.com',
- );
- const filteredDescriptor = crd.statusDescriptors.find((sd) => sd.path === 'importantMetrics');
- const statusView = wrapper
- .find(DescriptorDetailsItem)
- .filterWhere((node) => node.props().descriptor === filteredDescriptor);
-
- expect(statusView.exists()).toBe(false);
- });
-
- it('does not render any spec descriptor fields if there are none defined on the `ClusterServiceVersion`', () => {
- const csv = _.cloneDeep(testClusterServiceVersion);
- csv.spec.customresourcedefinitions.owned = [];
- wrapper = wrapper.setProps({ csv });
-
- expect(wrapper.find(DescriptorDetailsItem).length).toEqual(0);
- });
-
- xit('[CONSOLE-2336] renders spec descriptor fields if the custom resource is `owned`', () => {
- expect(
- wrapper.find(DescriptorDetailsItems).last().shallow().find(DescriptorDetailsItem).length,
- ).toEqual(
- testClusterServiceVersion.spec.customresourcedefinitions.owned[0].specDescriptors.length,
- );
- });
-
- xit('[CONSOLE-2336] renders spec descriptor fields if the custom resource is `required`', () => {
- const csv = _.cloneDeep(testClusterServiceVersion);
- csv.spec.customresourcedefinitions.required = _.cloneDeep(
- csv.spec.customresourcedefinitions.owned,
- );
- csv.spec.customresourcedefinitions.owned = [];
- wrapper = wrapper.setProps({ csv });
-
- expect(
- wrapper.find(DescriptorDetailsItems).last().shallow().find(DescriptorDetailsItem).length,
- ).toEqual(csv.spec.customresourcedefinitions.required[0].specDescriptors.length);
- });
-
- it('renders a Condtions table', () => {
- expect(wrapper.find('SectionHeading').at(1).prop('text')).toEqual('Conditions');
-
- expect(wrapper.find('Conditions').prop('conditions')).toEqual(
- testResourceInstance.status.conditions,
- );
- });
-
- it('renders a DescriptorConditions component for conditions descriptor', () => {
- expect(wrapper.find('DescriptorConditions').prop('descriptor')).toEqual(
- testConditionsDescriptor,
- );
- expect(wrapper.find('DescriptorConditions').prop('obj')).toEqual(testResourceInstance);
- expect(wrapper.find('DescriptorConditions').prop('schema')).toEqual({});
- });
-});
-
-describe('ResourcesList', () => {
- jest.spyOn(ReactRouter, 'useLocation').mockReturnValue({
- pathname: `/k8s/ns/default/${ClusterServiceVersionModel.plural}/etcd/${referenceFor(
- testResourceInstance,
- )}/my-etcd`,
- });
- jest.spyOn(ReactRouter, 'useParams').mockReturnValue({
- ns: 'default',
- appName: 'etcd',
- plural: `${referenceFor(testResourceInstance)}`,
- name: 'my-etcd',
- });
- it('uses the resources defined in the CSV', () => {
- const wrapper = mount(
- ,
- {
- wrappingComponent: ({ children }) => (
-
- {children}
-
- ),
- },
- );
- const multiListPage = wrapper.find(MultiListPage);
- expect(multiListPage.props().resources).toEqual(
- testClusterServiceVersion.spec.customresourcedefinitions.owned[0].resources.map(
- (resource) => ({ kind: resource.kind, namespaced: true, prop: 'Pod' }),
- ),
- );
- });
-
- it('uses the default resources if the kind is not found in the CSV', () => {
- const wrapper = mount(, {
- wrappingComponent: ({ children }) => (
-
- {children}
-
- ),
- });
- const multiListPage = wrapper.find(MultiListPage);
- expect(multiListPage.props().resources.length > 5).toEqual(true);
- });
-});
-
-describe(OperandDetailsPage.displayName, () => {
- window.SERVER_FLAGS.copiedCSVsDisabled = false;
-
- beforeEach(() => {
- jest.spyOn(ReactRouter, 'useParams').mockReturnValue({
- ns: 'default',
- appName: 'testapp',
- plural: 'testapp.coreos.com~v1alpha1~TestResource',
- name: 'my-test-resource',
- });
-
- jest.spyOn(ReactRouter, 'useLocation').mockReturnValue({
- pathname: `/k8s/ns/default/${ClusterServiceVersionModel.plural}/testapp/testapp.coreos.com~v1alpha1~TestResource/my-test-resource`,
- });
- });
-
- it('renders a `DetailsPage` with the correct subpages', () => {
- const wrapper = mount(
-
-
-
-
- ,
- );
-
- const detailsPage = wrapper.find(DetailsPage);
- expect(detailsPage.props().pages[0].nameKey).toEqual(`${i18nNS}~Details`);
- expect(detailsPage.props().pages[0].href).toEqual('');
- expect(detailsPage.props().pages[1].nameKey).toEqual(`${i18nNS}~YAML`);
- expect(detailsPage.props().pages[1].href).toEqual('yaml');
- expect(detailsPage.props().pages[2].nameKey).toEqual('olm~Resources');
- expect(detailsPage.props().pages[2].href).toEqual('resources');
- });
-
- it('renders a `DetailsPage` which also watches the parent CSV', () => {
- const wrapper = mount(
-
-
-
-
- ,
- );
- expect(wrapper.find(DetailsPage).prop('resources')[0]).toEqual({
- kind: 'CustomResourceDefinition',
- name: 'testresources.testapp.coreos.com',
- isList: false,
- prop: 'crd',
- });
- });
-
- it('menu actions to `DetailsPage`', () => {
- const wrapper = mount(
-
-
-
-
- ,
- );
- expect(wrapper.find(DetailsPage).prop('customActionMenu')).toBeTruthy();
- });
-
- it('passes function to create breadcrumbs for resource to `DetailsPage`', () => {
- const wrapper = mount(
-
-
-
-
- ,
- );
- expect(wrapper.find(DetailsPage).props().breadcrumbsFor(null)).toEqual([
- {
- name: 'Installed Operators',
- path: `/k8s/ns/default/${ClusterServiceVersionModel.plural}`,
- },
- {
- name: 'testapp',
- path: `/k8s/ns/default/${ClusterServiceVersionModel.plural}/testapp/testapp.coreos.com~v1alpha1~TestResource`,
- },
- {
- name: `TestResource details`,
- path: `/k8s/ns/default/${ClusterServiceVersionModel.plural}/testapp/testapp.coreos.com~v1alpha1~TestResource/my-test-resource`,
- },
- ]);
- });
-
- it('creates correct breadcrumbs even if `namespace`, `plural`, `appName`, and `name` URL parameters are the same', () => {
- jest.spyOn(ReactRouter, 'useParams').mockReturnValue({
- ns: 'example',
- appName: 'example',
- plural: 'example',
- name: 'example',
- });
-
- jest.spyOn(ReactRouter, 'useLocation').mockReturnValue({
- pathname: `/k8s/ns/example/${ClusterServiceVersionModel.plural}/example/example/example`,
- });
-
- const wrapper = mount(
-
-
-
-
- ,
- );
-
- expect(wrapper.find(DetailsPage).props().breadcrumbsFor(null)).toEqual([
- {
- name: 'Installed Operators',
- path: `/k8s/ns/example/${ClusterServiceVersionModel.plural}`,
- },
- {
- name: 'example',
- path: `/k8s/ns/example/${ClusterServiceVersionModel.plural}/example/example`,
- },
- {
- name: `example details`,
- path: `/k8s/ns/example/${ClusterServiceVersionModel.plural}/example/example/example`,
- },
- ]);
- });
-
- it('passes `flatten` function to Resources component which returns only objects with `ownerReferences` to each other or parent object', () => {
- const wrapper = mount(
-
-
-
-
- ,
- );
- const { flatten } = wrapper.find(MultiListPage).props();
- const pod = {
- kind: 'Pod',
- metadata: {
- uid: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
- ownerReferences: [
- {
- uid: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
- name: 'foo',
- kind: 'fooKind',
- apiVersion: 'fooVersion',
- },
- ],
- },
- };
- const deployment = {
- kind: 'Deployment',
- metadata: {
- uid: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
- ownerReferences: [
- {
- uid: testResourceInstance.metadata.uid,
- name: 'foo',
- kind: 'fooKind',
- apiVersion: 'fooVersion',
- },
- ],
- },
- };
- const secret = {
- kind: 'Secret',
- metadata: {
- uid: 'cccccccc-cccc-cccc-cccc-cccccccccccc',
- },
- };
- const resources: FirehoseResourcesResult = {
- Deployment: {
- data: [deployment],
- loaded: true,
- loadError: undefined,
- },
- Secret: {
- data: [secret],
- loaded: true,
- loadError: undefined,
- },
- Pod: {
- data: [pod],
- loaded: true,
- loadError: undefined,
- },
- };
- const data = flatten(resources);
-
- expect(data.map((obj) => obj.metadata.uid)).not.toContain(secret.metadata.uid);
- expect(data.map((obj) => obj.metadata.uid)).toContain(pod.metadata.uid);
- expect(data.map((obj) => obj.metadata.uid)).toContain(deployment.metadata.uid);
- });
-});
-
-describe(ProvidedAPIsPage.displayName, () => {
- let wrapper: ReactWrapper;
-
- beforeAll(() => {
- // Since crd models have not been loaded into redux state, just force return of the correct model type
- spyOn(k8sModelsModule, 'modelFor').and.returnValue(testModel);
- });
-
- beforeEach(() => {
- jest.spyOn(ReactRouter, 'useParams').mockReturnValue({
- ns: 'default',
- appName: 'testapp',
- });
-
- jest.spyOn(ReactRouter, 'useLocation').mockReturnValue({
- pathname: `/k8s/ns/default/${ClusterServiceVersionModel.plural}/testapp/instances`,
- });
-
- wrapper = mount(
-
-
-
-
- ,
- );
- });
-
- it('render listpage components', () => {
- expect(wrapper.find(ListPageHeader).exists()).toBe(true);
- expect(wrapper.find(ListPageCreateDropdown).exists()).toBe(true);
- expect(wrapper.find(ListPageBody).exists()).toBe(true);
- expect(wrapper.find(ListPageFilter).exists()).toBe(true);
- });
-
- it('render ListPageCreateDropdown with the correct text', () => {
- expect(wrapper.find(ListPageCreateDropdown).children().text()).toEqual('Create new');
- });
-
- it('passes `items` props and render ListPageCreateDropdown create button if app has multiple owned CRDs', () => {
- const obj = _.cloneDeep(testClusterServiceVersion);
- obj.spec.customresourcedefinitions.owned.push({
- name: 'foobars.testapp.coreos.com',
- displayName: 'Foo Bars',
- version: 'v1',
- kind: 'FooBar',
- });
-
- wrapper = mount(
-
-
-
-
- ,
- );
-
- const listPageCreateDropdown = wrapper.find(ListPageCreateDropdown);
-
- expect(listPageCreateDropdown.props().items).toEqual({
- 'testapp.coreos.com~v1alpha1~TestResource': 'Test Resource',
- 'testapp.coreos.com~v1~FooBar': 'Foo Bars',
- });
- });
-
- it('check if ListPageBody component renders the correct children', () => {
- expect(wrapper.find(ListPageBody).children().length).toBe(1);
- });
-});
-
-describe(ProvidedAPIPage.displayName, () => {
- let wrapper: ReactWrapper;
-
- beforeEach(() => {
- jest.spyOn(ReactRouter, 'useParams').mockReturnValue({
- ns: 'default',
- appName: 'testapp',
- plural: 'TestResourceRO',
- });
-
- jest.spyOn(ReactRouter, 'useLocation').mockReturnValue({
- pathname: `/k8s/ns/default/${ClusterServiceVersionModel.plural}/testapp/TestResourceRO`,
- });
-
- wrapper = mount(
-
-
-
-
- ,
- );
- });
-
- it('render listpage components', () => {
- expect(wrapper.find(ListPageHeader).exists()).toBe(true);
- expect(wrapper.find(ListPageCreateLink).exists()).toBe(true);
- expect(wrapper.find(ListPageBody).exists()).toBe(true);
- expect(wrapper.find(ListPageFilter).exists()).toBe(true);
- });
-
- it('render ListPageCreateLink with the correct props for create button if app has single owned CRDs', () => {
- expect(wrapper.find(ListPageCreateLink).props().to).toEqual(
- `/k8s/ns/default/${ClusterServiceVersionModel.plural}/testapp/TestResourceRO/~new`,
- );
- });
-
- it('render ListPageCreateLink with the correct text', () => {
- expect(wrapper.find(ListPageCreateLink).children().text()).toEqual('Create Test Resource');
- });
-
- it('check if ListPageBody component renders the correct children', () => {
- expect(wrapper.find(ListPageBody).children().length).toBe(1);
- });
-});
-
-describe('OperandStatus', () => {
- let wrapper: ShallowWrapper;
-
- it('displays the correct status for a `status` value of `Running`', () => {
- const obj = {
- status: {
- status: 'Running',
- state: 'Degraded',
- conditions: [
- {
- type: 'Failed',
- status: 'True',
- },
- ],
- },
- };
- wrapper = shallow();
- expect(wrapper.childAt(0).text()).toEqual('Status');
- expect(wrapper.childAt(3).props().title).toEqual('Running');
- });
-
- it('displays the correct status for a `phase` value of `Running`', () => {
- const obj = {
- status: {
- phase: 'Running',
- status: 'Installed',
- state: 'Degraded',
- conditions: [
- {
- type: 'Failed',
- status: 'True',
- },
- ],
- },
- };
- wrapper = shallow();
- expect(wrapper.childAt(0).text()).toEqual('Phase');
- expect(wrapper.childAt(3).props().title).toEqual('Running');
- });
-
- it('displays the correct status for a `phase` value of `Running`', () => {
- const obj = {
- status: {
- phase: 'Running',
- status: 'Installed',
- state: 'Degraded',
- conditions: [
- {
- type: 'Failed',
- status: 'True',
- },
- ],
- },
- };
- wrapper = shallow();
- expect(wrapper.childAt(0).text()).toEqual('Phase');
- expect(wrapper.childAt(3).props().title).toEqual('Running');
- });
-
- it('displays the correct status for a `state` value of `Running`', () => {
- const obj = {
- status: {
- state: 'Running',
- conditions: [
- {
- type: 'Failed',
- status: 'True',
- },
- ],
- },
- };
- wrapper = shallow();
- expect(wrapper.childAt(0).text()).toEqual('State');
- expect(wrapper.childAt(3).props().title).toEqual('Running');
- });
-
- it('displays the correct status for a condition status of `True`', () => {
- const obj = {
- status: {
- conditions: [
- {
- type: 'Failed',
- status: 'False',
- },
- {
- type: 'Running',
- status: 'True',
- },
- ],
- },
- };
- wrapper = shallow();
- expect(wrapper.childAt(0).text()).toEqual('Condition');
- expect(wrapper.childAt(3).props().title).toEqual('Running');
- });
-
- it('displays the `-` status when no conditions are `True`', () => {
- const obj = {
- status: {
- conditions: [
- {
- type: 'Failed',
- status: 'False',
- },
- {
- type: 'Installed',
- status: 'False',
- },
- ],
- },
- };
- wrapper = shallow();
- expect(wrapper.text()).toEqual('-');
- });
-
- it('displays the `-` for a missing status stanza', () => {
- const obj = {};
- wrapper = shallow();
- expect(wrapper.text()).toEqual('-');
- });
-});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx
deleted file mode 100644
index ac05bce98f0..00000000000
--- a/frontend/packages/operator-lifecycle-manager/src/components/package-manifest.spec.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import * as React from 'react';
-import { shallow, ShallowWrapper } from 'enzyme';
-import * as UIActions from '@console/internal/actions/ui';
-import { RowFunctionArgs } from '@console/internal/components/factory';
-import { ResourceLink } from '@console/internal/components/utils';
-import { Timestamp } from '@console/shared/src/components/datetime/Timestamp';
-import { testPackageManifest, testCatalogSource } from '../../mocks';
-import { PackageManifestKind, CatalogSourceKind } from '../types';
-import { ClusterServiceVersionLogo } from './cluster-service-version-logo';
-import {
- PackageManifestTableRow,
- PackageManifestTableHeader,
- PackageManifestTableHeaderWithCatalogSource,
-} from './package-manifest';
-
-describe(PackageManifestTableHeader.displayName, () => {
- it('renders column header for package name', () => {
- expect(PackageManifestTableHeader()[0].title).toEqual('Name');
- });
-
- it('renders column header for latest CSV version for package in catalog', () => {
- expect(PackageManifestTableHeader()[1].title).toEqual('Latest version');
- });
-
- it('renders column header for creation timestamp', () => {
- expect(PackageManifestTableHeader()[2].title).toEqual('Created');
- });
-});
-
-describe(PackageManifestTableHeaderWithCatalogSource.displayName, () => {
- it('renders column header for catalog source', () => {
- expect(PackageManifestTableHeaderWithCatalogSource()[3].title).toEqual('CatalogSource');
- });
-});
-
-describe('PackageManifestTableRow', () => {
- let wrapper: ShallowWrapper
- >>;
-
- beforeEach(() => {
- jest.spyOn(UIActions, 'getActiveNamespace').mockReturnValue('default');
-
- const columns: any[] = [];
- wrapper = shallow(
- ,
- );
- });
-
- it('renders column for package name and logo', () => {
- expect(wrapper.childAt(0).dive().find(ClusterServiceVersionLogo).props().displayName).toEqual(
- testPackageManifest.status.channels[0].currentCSVDesc.displayName,
- );
- });
-
- it('renders column for latest CSV version for package in catalog', () => {
- const {
- name,
- currentCSVDesc: { version },
- } = testPackageManifest.status.channels[0];
- expect(wrapper.childAt(1).dive().text()).toEqual(`${version} (${name})`);
- });
-
- it('renders column for creation timestamp', () => {
- const pkgManifestCreationTimestamp = testPackageManifest.metadata.creationTimestamp;
- expect(wrapper.childAt(2).dive().find(Timestamp).props().timestamp).toEqual(
- `${pkgManifestCreationTimestamp}`,
- );
- });
-
- // This is to verify cataloSource column gets rendered on the Search page for PackageManifest resource
- it('renders column for catalog source for a package when no catalog source is defined', () => {
- const catalogSourceName = testPackageManifest.status.catalogSource;
- const columns: any[] = [];
-
- wrapper = shallow(
- ,
- );
- expect(wrapper.childAt(3).dive().find(ResourceLink).props().name).toEqual(
- `${catalogSourceName}`,
- );
- });
-});
diff --git a/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx b/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx
deleted file mode 100644
index c0d599ea1e6..00000000000
--- a/frontend/packages/operator-lifecycle-manager/src/components/subscription.spec.tsx
+++ /dev/null
@@ -1,338 +0,0 @@
-import { Button, DescriptionListDescription, DescriptionListTerm } from '@patternfly/react-core';
-import { shallow, ShallowWrapper } from 'enzyme';
-import * as _ from 'lodash';
-import * as Router from 'react-router-dom-v5-compat';
-import {
- Table,
- MultiListPage,
- DetailsPage,
- RowFunctionArgs,
-} from '@console/internal/components/factory';
-import { ResourceLink } from '@console/internal/components/utils';
-import { referenceForModel } from '@console/internal/module/k8s';
-import { LazyActionMenu } from '@console/shared/src';
-import { DescriptionListTermHelp } from '@console/shared/src/components/description-list/DescriptionListTermHelp';
-import {
- testSubscription,
- testSubscriptions,
- testClusterServiceVersion,
- testPackageManifest,
-} from '../../mocks';
-import {
- SubscriptionModel,
- ClusterServiceVersionModel,
- PackageManifestModel,
- OperatorGroupModel,
- InstallPlanModel,
-} from '../models';
-import { SubscriptionKind, SubscriptionState } from '../types';
-import {
- SubscriptionTableRow,
- SubscriptionsList,
- SubscriptionsListProps,
- SubscriptionsPage,
- SubscriptionsPageProps,
- SubscriptionDetails,
- SubscriptionDetailsPage,
- SubscriptionDetailsProps,
- SubscriptionUpdates,
- SubscriptionUpdatesProps,
- SubscriptionUpdatesState,
- SubscriptionStatus,
-} from './subscription';
-
-jest.mock('react-router-dom-v5-compat', () => ({
- ...jest.requireActual('react-router-dom-v5-compat'),
- useParams: jest.fn(),
-}));
-
-describe('SubscriptionTableRow', () => {
- let wrapper: ShallowWrapper;
- let subscription: SubscriptionKind;
-
- const updateWrapper = () => {
- const rowArgs: RowFunctionArgs = {
- obj: subscription,
- } as any;
-
- wrapper = shallow();
- return wrapper;
- };
-
- beforeEach(() => {
- subscription = {
- ...testSubscription,
- status: { installedCSV: 'testapp.v1.0.0' },
- };
- wrapper = updateWrapper();
- });
-
- it('renders column for subscription name', () => {
- expect(wrapper.childAt(0).shallow().find(ResourceLink).props().name).toEqual(
- subscription.metadata.name,
- );
- expect(wrapper.childAt(0).shallow().find(ResourceLink).props().namespace).toEqual(
- subscription.metadata.namespace,
- );
- expect(wrapper.childAt(0).shallow().find(ResourceLink).props().kind).toEqual(
- referenceForModel(SubscriptionModel),
- );
- });
-
- it('renders actions kebab', () => {
- expect(wrapper.find(LazyActionMenu).props().context).toEqual({
- [referenceForModel(SubscriptionModel)]: subscription,
- });
- });
-
- it('renders column for namespace name', () => {
- expect(wrapper.childAt(1).shallow().find(ResourceLink).props().name).toEqual(
- subscription.metadata.namespace,
- );
- expect(wrapper.childAt(1).shallow().find(ResourceLink).props().kind).toEqual('Namespace');
- });
-
- it('renders column for subscription state when update available', () => {
- subscription.status.state = SubscriptionState.SubscriptionStateUpgradeAvailable;
- wrapper = updateWrapper();
-
- expect(wrapper.childAt(2).find(SubscriptionStatus).shallow().text()).toContain(
- 'Upgrade available',
- );
- });
-
- it('renders column for subscription state when unknown state', () => {
- expect(wrapper.childAt(2).find(SubscriptionStatus).shallow().text()).toEqual('Unknown failure');
- });
-
- it('renders column for subscription state when update in progress', () => {
- subscription.status.state = SubscriptionState.SubscriptionStateUpgradePending;
- wrapper = updateWrapper();
-
- expect(wrapper.childAt(2).find(SubscriptionStatus).shallow().text()).toContain('Upgrading');
- });
-
- it('renders column for subscription state when no updates available', () => {
- subscription.status.state = SubscriptionState.SubscriptionStateAtLatest;
- wrapper = updateWrapper();
-
- expect(wrapper.childAt(2).find(SubscriptionStatus).shallow().text()).toContain('Up to date');
- });
-
- it('renders column for current subscription channel', () => {
- expect(wrapper.childAt(3).shallow().text()).toEqual(subscription.spec.channel);
- });
-
- it('renders column for approval strategy', () => {
- expect(wrapper.childAt(4).shallow().text()).toEqual('Automatic');
- });
-});
-
-describe('SubscriptionsList', () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- wrapper = shallow(
- ,
- );
- });
-
- it('renders a `Table` component with correct header', () => {
- const headerTitles = wrapper
- .find(Table)
- .props()
- .Header()
- .map((header) => header.title);
- expect(headerTitles).toEqual([
- 'Name',
- 'Namespace',
- 'Status',
- 'Update channel',
- 'Update approval',
- '',
- ]);
- });
-});
-
-describe('SubscriptionsPage', () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- wrapper = shallow();
- });
-
- it('renders a `MultiListPage` component with the correct props', () => {
- expect(wrapper.find(MultiListPage).props().ListComponent).toEqual(SubscriptionsList);
- expect(wrapper.find(MultiListPage).props().title).toEqual('Subscriptions');
- expect(wrapper.find(MultiListPage).props().canCreate).toBe(true);
- expect(wrapper.find(MultiListPage).props().createProps).toEqual({
- to: '/catalog?catalogType=operator',
- });
- expect(wrapper.find(MultiListPage).props().createButtonText).toEqual('Create Subscription');
- expect(wrapper.find(MultiListPage).props().filterLabel).toEqual('Subscriptions by package');
- expect(wrapper.find(MultiListPage).props().resources).toEqual([
- {
- kind: referenceForModel(SubscriptionModel),
- namespace: 'default',
- namespaced: true,
- prop: 'subscription',
- },
- {
- kind: referenceForModel(OperatorGroupModel),
- namespace: 'default',
- namespaced: true,
- prop: 'operatorGroup',
- },
- ]);
- });
-});
-
-describe('SubscriptionUpdates', () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- wrapper = shallow(
- ,
- );
- });
-
- it('renders link to configure update channel', () => {
- const channel = wrapper
- .findWhere((node) =>
- node.equals(
- ,
- ),
- )
- .parents()
- .at(0)
- .find(Button)
- .render()
- .text();
-
- expect(channel).toEqual(testSubscription.spec.channel);
- });
-
- it('renders link to set approval strategy', () => {
- const strategy = wrapper
- .findWhere((node) =>
- node.equals(
- ,
- ),
- )
- .parents()
- .at(0)
- .find(Button)
- .render()
- .text();
-
- expect(strategy).toEqual(testSubscription.spec.installPlanApproval || 'Automatic');
- });
-});
-
-describe('SubscriptionDetails', () => {
- let wrapper: ShallowWrapper;
-
- beforeEach(() => {
- wrapper = shallow(
- ,
- );
- });
-
- it('renders subscription update channel and approval component', () => {
- expect(wrapper.find(SubscriptionUpdates).exists()).toBe(true);
- });
-
- it('renders link to `ClusterServiceVersion` if installed', () => {
- const obj = _.cloneDeep(testSubscription);
- obj.status = { installedCSV: testClusterServiceVersion.metadata.name };
- wrapper = wrapper.setProps({ obj, clusterServiceVersions: [testClusterServiceVersion] });
-
- const link = wrapper
- .findWhere((node) =>
- node.equals(Installed version),
- )
- .parents()
- .at(0)
- .find(DescriptionListDescription)
- .find(ResourceLink)
- .at(0);
-
- expect(link.props().title).toEqual(obj.status.installedCSV);
- expect(link.props().name).toEqual(obj.status.installedCSV);
- });
-
- it('renders link to catalog source', () => {
- const link = wrapper
- .findWhere((node) => node.equals(CatalogSource))
- .parents()
- .at(0)
- .find(DescriptionListDescription)
- .find(ResourceLink)
- .at(0);
-
- expect(link.props().name).toEqual(testSubscription.spec.source);
- });
-});
-
-describe('SubscriptionDetailsPage', () => {
- it('renders `DetailsPage` with correct props', () => {
- jest.spyOn(Router, 'useParams').mockReturnValue({ ns: 'default', name: 'example-sub' });
- const wrapper = shallow();
-
- expect(wrapper.find(DetailsPage).props().kind).toEqual(referenceForModel(SubscriptionModel));
- expect(wrapper.find(DetailsPage).props().pages.length).toEqual(2);
- expect(wrapper.find(DetailsPage).props().customActionMenu).toBeDefined();
- });
-
- it('passes additional resources to watch', () => {
- jest.spyOn(Router, 'useParams').mockReturnValue({ ns: 'default', name: 'example-sub' });
- const wrapper = shallow();
-
- expect(wrapper.find(DetailsPage).props().resources).toEqual([
- {
- kind: referenceForModel(PackageManifestModel),
- namespace: 'default',
- isList: true,
- prop: 'packageManifests',
- },
- {
- kind: referenceForModel(InstallPlanModel),
- isList: true,
- namespace: 'default',
- prop: 'installPlans',
- },
- {
- kind: referenceForModel(ClusterServiceVersionModel),
- isList: true,
- namespace: 'default',
- prop: 'clusterServiceVersions',
- },
- {
- kind: referenceForModel(SubscriptionModel),
- isList: true,
- namespace: 'default',
- prop: 'subscriptions',
- },
- ]);
- });
-});