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/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..637763098b
--- /dev/null
+++ b/workspaces/orchestrator/plugins/orchestrator/src/hooks/usePermissionArray.test.tsx
@@ -0,0 +1,159 @@
+/*
+ * 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 } 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,
+} 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 = [
+ orchestratorWorkflowPermission,
+ orchestratorWorkflowUsePermission,
+];
+
+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');
+ });
+});