From 24cad4849de40d2ebd400fd122c8a8ca832b81c6 Mon Sep 17 00:00:00 2001 From: rostalan Date: Thu, 25 Jun 2026 15:38:16 +0200 Subject: [PATCH 1/2] test(orchestrator): add frontend hook and status coverage Add targeted frontend tests for orchestrator hooks and status components so config and status regressions are caught earlier. Harden the status indicator fallback for unknown states so the covered behavior matches runtime behavior. Co-authored-by: Cursor --- .../MissingSchemaNotice.test.tsx | 158 ++++++++++++++++++ .../WorkflowInstanceStatusIndicator.test.tsx | 157 +++++++++++++++++ .../ui/WorkflowInstanceStatusIndicator.tsx | 3 +- .../src/components/ui/WorkflowStatus.test.tsx | 65 +++++++ .../src/hooks/useKafkaEnabled.test.ts | 60 +++++++ .../src/hooks/useLanguage.test.ts | 45 +++++ .../src/hooks/useLogsEnabled.test.ts | 60 +++++++ .../src/hooks/usePermissionArray.test.tsx | 157 +++++++++++++++++ .../src/hooks/useTranslation.test.ts | 48 ++++++ .../useWorkflowInstanceCardHeightMode.test.ts | 70 ++++++++ .../useWorkflowInstanceStatusColors.test.tsx | 58 +++++++ 11 files changed, 879 insertions(+), 2 deletions(-) create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/MissingSchemaNotice.test.tsx create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowInstanceStatusIndicator.test.tsx create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowStatus.test.tsx create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/hooks/useKafkaEnabled.test.ts create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/hooks/useLanguage.test.ts create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/hooks/useLogsEnabled.test.ts create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/hooks/usePermissionArray.test.tsx create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/hooks/useTranslation.test.ts create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceCardHeightMode.test.ts create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceStatusColors.test.tsx diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/MissingSchemaNotice.test.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/MissingSchemaNotice.test.tsx new file mode 100644 index 0000000000..7e3bf8cade --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/ExecuteWorkflowPage/MissingSchemaNotice.test.tsx @@ -0,0 +1,158 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; + +import { ReactNode } from 'react'; + +import { fireEvent, render, screen } from '@testing-library/react'; + +import MissingSchemaNotice from './MissingSchemaNotice'; + +jest.mock('../../hooks/useTranslation', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +jest.mock( + '@red-hat-developer-hub/backstage-plugin-orchestrator-form-react', + () => ({ + SubmitButton: ({ + children, + handleClick, + submitting, + }: { + children: ReactNode; + handleClick: () => void; + submitting?: boolean; + }) => ( + + ), + }), +); + +describe('MissingSchemaNotice', () => { + it('renders the missing schema guidance and run button', () => { + render( + , + ); + + expect( + screen.getByText('messages.missingJsonSchema.title'), + ).toBeInTheDocument(); + expect( + screen.getByText(/messages\.missingJsonSchema\.message/), + ).toBeInTheDocument(); + expect(screen.getByText('dataInputSchema')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'common.run' }), + ).toBeInTheDocument(); + }); + + it('invokes handleExecute with empty payload when run is clicked', () => { + const handleExecute = jest.fn().mockResolvedValue(undefined); + + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: 'common.run' })); + expect(handleExecute).toHaveBeenCalledWith({}); + }); + + it('renders and invokes execute-as-event button when configured', () => { + const handleExecuteAsEvent = jest.fn().mockResolvedValue(undefined); + + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: 'Run as event' })); + expect(handleExecuteAsEvent).toHaveBeenCalledWith({}); + }); + + it('does not render execute-as-event button when label is missing', () => { + render( + , + ); + + expect(screen.queryByRole('button', { name: 'Run as event' })).toBeNull(); + }); + + it('disables action buttons while execution is in progress', () => { + render( + , + ); + + expect(screen.getByRole('button', { name: 'common.run' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Run as event' })).toBeDisabled(); + }); + + it('re-enables action buttons when execution completes', () => { + const { rerender } = render( + , + ); + + expect(screen.getByRole('button', { name: 'common.run' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Run as event' })).toBeDisabled(); + + rerender( + , + ); + + expect( + screen.getByRole('button', { name: 'common.run' }), + ).not.toBeDisabled(); + expect( + screen.getByRole('button', { name: 'Run as event' }), + ).not.toBeDisabled(); + }); +}); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowInstanceStatusIndicator.test.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowInstanceStatusIndicator.test.tsx new file mode 100644 index 0000000000..40a6fe52c7 --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowInstanceStatusIndicator.test.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; + +import { ReactNode } from 'react'; + +import { render, screen } from '@testing-library/react'; + +import { ProcessInstanceStatusDTO } from '@red-hat-developer-hub/backstage-plugin-orchestrator-common'; + +import { VALUE_UNAVAILABLE } from '../../constants'; +import { useWorkflowInstanceStateColors } from '../../hooks/useWorkflowInstanceStatusColors'; +import { WorkflowInstanceStatusIndicator } from './WorkflowInstanceStatusIndicator'; + +jest.mock('@backstage/core-components', () => ({ + Link: ({ to, children }: { to: string; children: ReactNode }) => ( + {children} + ), +})); + +jest.mock('../../hooks/useTranslation', () => ({ + useTranslation: () => ({ + t: (key: string) => + ({ + 'table.status.running': 'Running', + 'table.status.completed': 'Completed', + 'tooltips.suspended': 'Suspended', + 'table.status.aborted': 'Aborted', + 'table.status.failed': 'Failed', + 'table.status.pending': 'Pending', + })[key] ?? key, + }), +})); + +jest.mock('../../hooks/useWorkflowInstanceStatusColors', () => ({ + useWorkflowInstanceStateColors: jest.fn(() => 'status-icon'), +})); + +describe('WorkflowInstanceStatusIndicator', () => { + it('renders unavailable value when status is missing', () => { + render(); + + expect(screen.getByText(VALUE_UNAVAILABLE)).toBeInTheDocument(); + }); + + it('renders completed status text without link when instanceLink is not provided', () => { + render( + , + ); + + expect(screen.getByText('Completed')).toBeInTheDocument(); + expect(screen.queryByRole('link', { name: 'Completed' })).toBeNull(); + }); + + it('renders status title as a link when instanceLink is provided', () => { + render( + , + ); + + const link = screen.getByRole('link', { name: 'Failed' }); + expect(link).toHaveAttribute('href', '/orchestrator/instances/1'); + }); + + it('renders pending title for pending status', () => { + render( + , + ); + + expect(screen.getByText('Pending')).toBeInTheDocument(); + }); + + it('renders running title for active status', () => { + render( + , + ); + + expect(screen.getByText('Running')).toBeInTheDocument(); + }); + + it('renders suspended title for suspended status', () => { + render( + , + ); + + expect(screen.getByText('Suspended')).toBeInTheDocument(); + }); + + it('aborted title for aborted status', () => { + render( + , + ); + + expect(screen.getByText('Aborted')).toBeInTheDocument(); + }); + + it('renders unavailable for unknown status', () => { + render( + , + ); + + expect(screen.getByText(VALUE_UNAVAILABLE)).toBeInTheDocument(); + }); + + it('calls useWorkflowInstanceStateColors with correct status', () => { + render( + , + ); + + expect(useWorkflowInstanceStateColors).toHaveBeenCalledWith( + ProcessInstanceStatusDTO.Completed, + ); + }); + + it('applies color class to the rendered icon element', () => { + const { container } = render( + , + ); + + expect(useWorkflowInstanceStateColors).toHaveBeenCalledWith( + ProcessInstanceStatusDTO.Completed, + ); + expect(container.querySelector('svg.status-icon')).toBeInTheDocument(); + }); +}); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowInstanceStatusIndicator.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowInstanceStatusIndicator.tsx index 8c63db16c8..3a6bef4eba 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowInstanceStatusIndicator.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowInstanceStatusIndicator.tsx @@ -73,8 +73,7 @@ export const WorkflowInstanceStatusIndicator = ({ title = t('table.status.pending'); break; default: - icon = VALUE_UNAVAILABLE; - break; + return <>{VALUE_UNAVAILABLE}; } return ( diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowStatus.test.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowStatus.test.tsx new file mode 100644 index 0000000000..986f0514dc --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/ui/WorkflowStatus.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; + +import { render, screen } from '@testing-library/react'; + +import { AVAILABLE, UNAVAILABLE } from '../../constants'; +import { WorkflowStatus } from './WorkflowStatus'; + +jest.mock('../../hooks/useTranslation', () => ({ + useTranslation: () => ({ + t: (key: string) => + ({ + 'workflow.status.available': 'Available', + 'workflow.status.unavailable': 'Unavailable', + 'tooltips.workflowDown': 'Workflow down', + })[key] ?? key, + }), +})); + +describe('WorkflowStatus', () => { + it('renders available status for AVAILABLE string', () => { + render(); + + expect(screen.getByText('Available')).toBeInTheDocument(); + }); + + it('renders available status for true boolean', () => { + render(); + + expect(screen.getByText('Available')).toBeInTheDocument(); + }); + + it('renders unavailable status for false boolean', () => { + render(); + + expect(screen.getByText('Unavailable')).toBeInTheDocument(); + }); + + it('renders raw value for unsupported availability content', () => { + render(); + + expect(screen.getByText('Unknown')).toBeInTheDocument(); + }); + + it('renders unavailable status for UNAVAILABLE string', () => { + render(); + + expect(screen.getByText('Unavailable')).toBeInTheDocument(); + }); +}); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/hooks/useKafkaEnabled.test.ts b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useKafkaEnabled.test.ts new file mode 100644 index 0000000000..22edda1212 --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useKafkaEnabled.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderHook } from '@testing-library/react'; + +import { useKafkaEnabled } from './useKafkaEnabled'; + +const mockUseApi = jest.fn(); + +jest.mock('@backstage/core-plugin-api', () => ({ + configApiRef: {}, + useApi: (...args: unknown[]) => mockUseApi(...args), +})); + +describe('useKafkaEnabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns true when kafka config exists', () => { + const configApi = { + getOptionalConfig: jest.fn().mockReturnValue({}), + }; + mockUseApi.mockReturnValue(configApi); + + const { result } = renderHook(() => useKafkaEnabled()); + + expect(result.current).toBe(true); + expect(configApi.getOptionalConfig).toHaveBeenCalledWith( + 'orchestrator.kafka', + ); + }); + + it('returns false when kafka config is missing', () => { + const configApi = { + getOptionalConfig: jest.fn().mockReturnValue(undefined), + }; + mockUseApi.mockReturnValue(configApi); + + const { result } = renderHook(() => useKafkaEnabled()); + + expect(result.current).toBe(false); + expect(configApi.getOptionalConfig).toHaveBeenCalledWith( + 'orchestrator.kafka', + ); + }); +}); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/hooks/useLanguage.test.ts b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useLanguage.test.ts new file mode 100644 index 0000000000..6d72fc7ca2 --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useLanguage.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderHook } from '@testing-library/react'; + +import { useLanguage } from './useLanguage'; + +const mockUseApi = jest.fn(); + +jest.mock('@backstage/core-plugin-api', () => ({ + useApi: (...args: unknown[]) => mockUseApi(...args), +})); + +jest.mock('@backstage/core-plugin-api/alpha', () => ({ + appLanguageApiRef: {}, +})); + +describe('useLanguage', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns the language code from app language api', () => { + mockUseApi.mockReturnValue({ + getLanguage: jest.fn().mockReturnValue({ language: 'ja' }), + }); + + const { result } = renderHook(() => useLanguage()); + + expect(result.current).toBe('ja'); + }); +}); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/hooks/useLogsEnabled.test.ts b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useLogsEnabled.test.ts new file mode 100644 index 0000000000..9a9c3a53de --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useLogsEnabled.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderHook } from '@testing-library/react'; + +import { useLogsEnabled } from './useLogsEnabled'; + +const mockUseApi = jest.fn(); + +jest.mock('@backstage/core-plugin-api', () => ({ + configApiRef: {}, + useApi: (...args: unknown[]) => mockUseApi(...args), +})); + +describe('useLogsEnabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns true when workflow log provider config exists', () => { + const configApi = { + getOptionalConfig: jest.fn().mockReturnValue({}), + }; + mockUseApi.mockReturnValue(configApi); + + const { result } = renderHook(() => useLogsEnabled()); + + expect(result.current).toBe(true); + expect(configApi.getOptionalConfig).toHaveBeenCalledWith( + 'orchestrator.workflowLogProvider', + ); + }); + + it('returns false when workflow log provider config is missing', () => { + const configApi = { + getOptionalConfig: jest.fn().mockReturnValue(undefined), + }; + mockUseApi.mockReturnValue(configApi); + + const { result } = renderHook(() => useLogsEnabled()); + + expect(result.current).toBe(false); + expect(configApi.getOptionalConfig).toHaveBeenCalledWith( + 'orchestrator.workflowLogProvider', + ); + }); +}); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/hooks/usePermissionArray.test.tsx b/workspaces/orchestrator/plugins/orchestrator/src/hooks/usePermissionArray.test.tsx new file mode 100644 index 0000000000..1d405f592c --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/hooks/usePermissionArray.test.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ReactNode } from 'react'; + +import { + AuthorizeResult, + Permission, +} from '@backstage/plugin-permission-common'; + +import { renderHook, waitFor } from '@testing-library/react'; +import { SWRConfig } from 'swr'; + +import { + usePermissionArray, + usePermissionArrayDecision, +} from './usePermissionArray'; + +const mockUseApi = jest.fn(); + +jest.mock('@backstage/core-plugin-api', () => ({ + useApi: (...args: unknown[]) => mockUseApi(...args), +})); + +jest.mock('@backstage/plugin-permission-react', () => ({ + permissionApiRef: {}, +})); + +const permissions = [ + { name: 'orchestrator.workflow.read' }, + { name: 'orchestrator.workflow.execute' }, +] as Permission[]; + +describe('usePermissionArray', () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + new Map() }}>{children} + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns loading state before authorization resolves', () => { + const authorize = jest.fn().mockReturnValue(new Promise(() => {})); + mockUseApi.mockReturnValue({ authorize }); + + const { result } = renderHook(() => usePermissionArray(permissions), { + wrapper, + }); + + expect(result.current.loading).toBe(true); + expect(result.current.allowed).toEqual([false, false]); + }); + + it('returns allowed flags based on authorization responses', async () => { + const authorize = jest + .fn() + .mockResolvedValueOnce({ result: AuthorizeResult.ALLOW }) + .mockResolvedValueOnce({ result: AuthorizeResult.DENY }); + mockUseApi.mockReturnValue({ authorize }); + + const { result } = renderHook(() => usePermissionArray(permissions), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + expect(result.current.allowed).toEqual([true, false]); + expect(result.current.error).toBeUndefined(); + expect(authorize).toHaveBeenNthCalledWith(1, { + permission: permissions[0], + }); + expect(authorize).toHaveBeenNthCalledWith(2, { + permission: permissions[1], + }); + }); + + it('returns error state when authorization request fails', async () => { + const error = new Error('permission backend unavailable'); + const authorize = jest.fn().mockRejectedValue(error); + mockUseApi.mockReturnValue({ authorize }); + + const { result } = renderHook(() => usePermissionArray(permissions), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + expect(result.current.error).toBe(error); + expect(result.current.allowed).toEqual([false, false]); + }); +}); + +describe('usePermissionArrayDecision', () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + new Map() }}>{children} + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns allowed=true when at least one permission is allowed', async () => { + const authorize = jest + .fn() + .mockResolvedValueOnce({ result: AuthorizeResult.DENY }) + .mockResolvedValueOnce({ result: AuthorizeResult.ALLOW }); + mockUseApi.mockReturnValue({ authorize }); + + const { result } = renderHook( + () => usePermissionArrayDecision(permissions), + { + wrapper, + }, + ); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + expect(result.current.allowed).toBe(true); + }); + + it('returns allowed=false when no permission is allowed', async () => { + const authorize = jest + .fn() + .mockResolvedValueOnce({ result: AuthorizeResult.DENY }) + .mockResolvedValueOnce({ result: AuthorizeResult.CONDITIONAL }); + mockUseApi.mockReturnValue({ authorize }); + + const { result } = renderHook( + () => usePermissionArrayDecision(permissions), + { + wrapper, + }, + ); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + expect(result.current.allowed).toBe(false); + }); +}); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/hooks/useTranslation.test.ts b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useTranslation.test.ts new file mode 100644 index 0000000000..9f7e6b456c --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useTranslation.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderHook } from '@testing-library/react'; + +import { orchestratorTranslationRef } from '../translations'; +import { useTranslation } from './useTranslation'; + +const mockUseTranslationRef = jest.fn(); + +jest.mock('@backstage/core-plugin-api/alpha', () => ({ + useTranslationRef: (...args: unknown[]) => mockUseTranslationRef(...args), +})); + +jest.mock('../translations', () => ({ + orchestratorTranslationRef: { id: 'plugin.orchestrator.test' }, +})); + +describe('useTranslation', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('delegates to useTranslationRef with orchestrator translation ref', () => { + const translator = { t: jest.fn() }; + mockUseTranslationRef.mockReturnValue(translator); + + const { result } = renderHook(() => useTranslation()); + + expect(mockUseTranslationRef).toHaveBeenCalledWith( + orchestratorTranslationRef, + ); + expect(result.current).toBe(translator); + }); +}); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceCardHeightMode.test.ts b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceCardHeightMode.test.ts new file mode 100644 index 0000000000..809082e7da --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceCardHeightMode.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderHook } from '@testing-library/react'; + +import { useWorkflowInstanceCardHeightMode } from './useWorkflowInstanceCardHeightMode'; + +const mockUseApi = jest.fn(); + +jest.mock('@backstage/core-plugin-api', () => ({ + configApiRef: {}, + useApi: (...args: unknown[]) => mockUseApi(...args), +})); + +describe('useWorkflowInstanceCardHeightMode', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns fixed when config is missing', () => { + const configApi = { + getOptionalString: jest.fn().mockReturnValue(undefined), + }; + mockUseApi.mockReturnValue(configApi); + + const { result } = renderHook(() => useWorkflowInstanceCardHeightMode()); + + expect(result.current).toBe('fixed'); + }); + + it('returns content when config value is content', () => { + const configApi = { + getOptionalString: jest.fn().mockReturnValue('content'), + }; + mockUseApi.mockReturnValue(configApi); + + const { result } = renderHook(() => useWorkflowInstanceCardHeightMode()); + + expect(result.current).toBe('content'); + }); + + it('returns fixed and warns when config value is invalid', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const configApi = { + getOptionalString: jest.fn().mockReturnValue('stretch'), + }; + mockUseApi.mockReturnValue(configApi); + + const { result } = renderHook(() => useWorkflowInstanceCardHeightMode()); + + expect(result.current).toBe('fixed'); + expect(warnSpy).toHaveBeenCalledWith( + 'Unknown cardHeightMode "stretch", falling back to "fixed"', + ); + warnSpy.mockRestore(); + }); +}); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceStatusColors.test.tsx b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceStatusColors.test.tsx new file mode 100644 index 0000000000..54b1a47118 --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceStatusColors.test.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderHook } from '@testing-library/react'; + +import { ProcessInstanceStatusDTO } from '@red-hat-developer-hub/backstage-plugin-orchestrator-common'; + +import { useWorkflowInstanceStateColors } from './useWorkflowInstanceStatusColors'; + +jest.mock('tss-react/mui', () => ({ + makeStyles: () => () => () => ({ + classes: { + [ProcessInstanceStatusDTO.Active]: 'active-class', + [ProcessInstanceStatusDTO.Completed]: 'completed-class', + [ProcessInstanceStatusDTO.Suspended]: 'suspended-class', + [ProcessInstanceStatusDTO.Aborted]: 'aborted-class', + [ProcessInstanceStatusDTO.Error]: 'error-class', + [ProcessInstanceStatusDTO.Pending]: 'pending-class', + }, + }), +})); + +describe('useWorkflowInstanceStateColors', () => { + it('returns undefined for undefined status', () => { + const { result } = renderHook(() => useWorkflowInstanceStateColors()); + + expect(result.current).toBeUndefined(); + }); + + it('returns the class mapped to a completed status', () => { + const { result } = renderHook(() => + useWorkflowInstanceStateColors(ProcessInstanceStatusDTO.Completed), + ); + + expect(result.current).toBe('completed-class'); + }); + + it('returns the class mapped to an error status', () => { + const { result } = renderHook(() => + useWorkflowInstanceStateColors(ProcessInstanceStatusDTO.Error), + ); + + expect(result.current).toBe('error-class'); + }); +}); From 3ee64c1b0b63fdbbf20c3f3a122eeb5b35aa2952 Mon Sep 17 00:00:00 2001 From: rostalan Date: Thu, 25 Jun 2026 15:53:22 +0200 Subject: [PATCH 2/2] test(orchestrator): fix permission hook test typing Use real orchestrator basic permissions in the permission-array hook test so the fixture matches the hook contract, and add the required changeset for the plugin package. Co-authored-by: Cursor --- .../.changeset/tame-dragons-begin.md | 5 +++++ .../src/hooks/usePermissionArray.test.tsx | 16 +++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 workspaces/orchestrator/.changeset/tame-dragons-begin.md diff --git a/workspaces/orchestrator/.changeset/tame-dragons-begin.md b/workspaces/orchestrator/.changeset/tame-dragons-begin.md new file mode 100644 index 0000000000..2a2666aa5d --- /dev/null +++ b/workspaces/orchestrator/.changeset/tame-dragons-begin.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-orchestrator': patch +--- + +Add frontend unit test coverage for orchestrator hooks and status components, and harden the status indicator fallback for unknown states. diff --git a/workspaces/orchestrator/plugins/orchestrator/src/hooks/usePermissionArray.test.tsx b/workspaces/orchestrator/plugins/orchestrator/src/hooks/usePermissionArray.test.tsx index 1d405f592c..637763098b 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/hooks/usePermissionArray.test.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/hooks/usePermissionArray.test.tsx @@ -16,14 +16,16 @@ import { ReactNode } from 'react'; -import { - AuthorizeResult, - Permission, -} from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { renderHook, waitFor } from '@testing-library/react'; import { SWRConfig } from 'swr'; +import { + orchestratorWorkflowPermission, + orchestratorWorkflowUsePermission, +} from '@red-hat-developer-hub/backstage-plugin-orchestrator-common'; + import { usePermissionArray, usePermissionArrayDecision, @@ -40,9 +42,9 @@ jest.mock('@backstage/plugin-permission-react', () => ({ })); const permissions = [ - { name: 'orchestrator.workflow.read' }, - { name: 'orchestrator.workflow.execute' }, -] as Permission[]; + orchestratorWorkflowPermission, + orchestratorWorkflowUsePermission, +]; describe('usePermissionArray', () => { const wrapper = ({ children }: { children: ReactNode }) => (