Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions workspaces/orchestrator/.changeset/tame-dragons-begin.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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;
}) => (
<button
type="button"
onClick={handleClick}
disabled={submitting ?? false}
data-testid="submit-button"
>
{children}
</button>
),
}),
);

describe('MissingSchemaNotice', () => {
it('renders the missing schema guidance and run button', () => {
render(
<MissingSchemaNotice
isExecuting={false}
handleExecute={jest.fn().mockResolvedValue(undefined)}
/>,
);

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(
<MissingSchemaNotice isExecuting={false} handleExecute={handleExecute} />,
);

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(
<MissingSchemaNotice
isExecuting={false}
handleExecute={jest.fn().mockResolvedValue(undefined)}
handleExecuteAsEvent={handleExecuteAsEvent}
executeAsEventLabel="Run as event"
/>,
);

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(
<MissingSchemaNotice
isExecuting={false}
handleExecute={jest.fn().mockResolvedValue(undefined)}
handleExecuteAsEvent={jest.fn().mockResolvedValue(undefined)}
/>,
);

expect(screen.queryByRole('button', { name: 'Run as event' })).toBeNull();
});

it('disables action buttons while execution is in progress', () => {
render(
<MissingSchemaNotice
isExecuting
handleExecute={jest.fn().mockResolvedValue(undefined)}
handleExecuteAsEvent={jest.fn().mockResolvedValue(undefined)}
executeAsEventLabel="Run as event"
/>,
);

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(
<MissingSchemaNotice
isExecuting
handleExecute={jest.fn().mockResolvedValue(undefined)}
handleExecuteAsEvent={jest.fn().mockResolvedValue(undefined)}
executeAsEventLabel="Run as event"
/>,
);

expect(screen.getByRole('button', { name: 'common.run' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Run as event' })).toBeDisabled();

rerender(
<MissingSchemaNotice
isExecuting={false}
handleExecute={jest.fn().mockResolvedValue(undefined)}
handleExecuteAsEvent={jest.fn().mockResolvedValue(undefined)}
executeAsEventLabel="Run as event"
/>,
);

expect(
screen.getByRole('button', { name: 'common.run' }),
).not.toBeDisabled();
expect(
screen.getByRole('button', { name: 'Run as event' }),
).not.toBeDisabled();
});
});
Original file line number Diff line number Diff line change
@@ -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 }) => (
<a href={to}>{children}</a>
),
}));

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(<WorkflowInstanceStatusIndicator />);

expect(screen.getByText(VALUE_UNAVAILABLE)).toBeInTheDocument();
});

it('renders completed status text without link when instanceLink is not provided', () => {
render(
<WorkflowInstanceStatusIndicator
status={ProcessInstanceStatusDTO.Completed}
/>,
);

expect(screen.getByText('Completed')).toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'Completed' })).toBeNull();
});

it('renders status title as a link when instanceLink is provided', () => {
render(
<WorkflowInstanceStatusIndicator
status={ProcessInstanceStatusDTO.Error}
instanceLink="/orchestrator/instances/1"
/>,
);

const link = screen.getByRole('link', { name: 'Failed' });
expect(link).toHaveAttribute('href', '/orchestrator/instances/1');
});

it('renders pending title for pending status', () => {
render(
<WorkflowInstanceStatusIndicator
status={ProcessInstanceStatusDTO.Pending}
/>,
);

expect(screen.getByText('Pending')).toBeInTheDocument();
});

it('renders running title for active status', () => {
render(
<WorkflowInstanceStatusIndicator
status={ProcessInstanceStatusDTO.Active}
/>,
);

expect(screen.getByText('Running')).toBeInTheDocument();
});

it('renders suspended title for suspended status', () => {
render(
<WorkflowInstanceStatusIndicator
status={ProcessInstanceStatusDTO.Suspended}
/>,
);

expect(screen.getByText('Suspended')).toBeInTheDocument();
});

it('aborted title for aborted status', () => {
render(
<WorkflowInstanceStatusIndicator
status={ProcessInstanceStatusDTO.Aborted}
/>,
);

expect(screen.getByText('Aborted')).toBeInTheDocument();
});

it('renders unavailable for unknown status', () => {
render(
<WorkflowInstanceStatusIndicator
status={'UnknownStatus' as ProcessInstanceStatusDTO}
/>,
);

expect(screen.getByText(VALUE_UNAVAILABLE)).toBeInTheDocument();
});

it('calls useWorkflowInstanceStateColors with correct status', () => {
render(
<WorkflowInstanceStatusIndicator
status={ProcessInstanceStatusDTO.Completed}
/>,
);

expect(useWorkflowInstanceStateColors).toHaveBeenCalledWith(
ProcessInstanceStatusDTO.Completed,
);
});

it('applies color class to the rendered icon element', () => {
const { container } = render(
<WorkflowInstanceStatusIndicator
status={ProcessInstanceStatusDTO.Completed}
/>,
);

expect(useWorkflowInstanceStateColors).toHaveBeenCalledWith(
ProcessInstanceStatusDTO.Completed,
);
expect(container.querySelector('svg.status-icon')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ export const WorkflowInstanceStatusIndicator = ({
title = t('table.status.pending');
break;
default:
icon = VALUE_UNAVAILABLE;
break;
return <>{VALUE_UNAVAILABLE}</>;
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<WorkflowStatus availability={AVAILABLE} />);

expect(screen.getByText('Available')).toBeInTheDocument();
});

it('renders available status for true boolean', () => {
render(<WorkflowStatus availability />);

expect(screen.getByText('Available')).toBeInTheDocument();
});

it('renders unavailable status for false boolean', () => {
render(<WorkflowStatus availability={false} />);

expect(screen.getByText('Unavailable')).toBeInTheDocument();
});

it('renders raw value for unsupported availability content', () => {
render(<WorkflowStatus availability="Unknown" />);

expect(screen.getByText('Unknown')).toBeInTheDocument();
});

it('renders unavailable status for UNAVAILABLE string', () => {
render(<WorkflowStatus availability={UNAVAILABLE} />);

expect(screen.getByText('Unavailable')).toBeInTheDocument();
});
});
Loading
Loading