foreman-tasks 13.0.0 → 13.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
  3. data/lib/foreman_tasks/version.rb +1 -1
  4. data/webpack/ForemanTasks/Components/Chart/Chart.js +3 -1
  5. data/webpack/ForemanTasks/Components/TaskActions/TaskActionHelpers.js +8 -1
  6. data/webpack/ForemanTasks/Components/TaskActions/TaskActionsConstants.js +1 -0
  7. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
  8. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
  9. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
  10. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +58 -1
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +236 -131
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -33
  13. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -67
  14. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
  15. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
  16. data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +71 -0
  17. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +48 -51
  18. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -39
  19. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsActions.js +6 -2
  20. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -0
  21. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +2 -7
  22. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +241 -0
  23. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +99 -0
  24. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +129 -8
  25. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCard.scss +0 -1
  26. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/TasksDonutCard/TasksDonutCard.scss +1 -0
  27. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.scss +1 -0
  28. data/webpack/ForemanTasks/Components/TasksDashboard/TasksDashboardHelper.js +9 -4
  29. data/webpack/ForemanTasks/Components/TasksTable/TasksTableHelpers.js +2 -1
  30. data/webpack/ForemanTasks/Components/common/taskResultIcon.js +39 -29
  31. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
  32. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
  33. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +10 -33
  34. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
  35. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +100 -88
  36. data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +7 -8
  37. data/webpack/Routes/routes.js +1 -1
  38. data/webpack/Routes/routes.test.js +1 -1
  39. data/webpack/index.js +0 -5
  40. metadata +9 -3
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import RunningSteps from './Components/RunningSteps';
4
+ import Errors from './Components/Errors';
5
+
6
+ const ExecutionDetails = ({
7
+ state,
8
+ runningSteps,
9
+ cancelStep,
10
+ id,
11
+ taskReload,
12
+ taskReloadStart,
13
+ executionPlan,
14
+ failedSteps,
15
+ result,
16
+ }) => {
17
+ const hasFailedSteps = failedSteps.length > 0;
18
+ const hasRunningSteps = runningSteps.length > 0;
19
+ const isActiveExecutionState =
20
+ state === 'running' || state === 'pending' || state === 'paused';
21
+
22
+ // Prefer Errors when failed steps exist (e.g. paused-after-error). Only fall
23
+ // back to the RunningSteps empty state for active tasks with no failures.
24
+ const showRunningSteps =
25
+ hasRunningSteps || (isActiveExecutionState && !hasFailedSteps);
26
+ const showErrors = hasFailedSteps || !showRunningSteps;
27
+
28
+ return (
29
+ <div
30
+ id="execution-details-panel"
31
+ data-ouia-component-id="execution-details-panel"
32
+ >
33
+ {showRunningSteps && (
34
+ <RunningSteps
35
+ executionPlan={executionPlan}
36
+ result={result}
37
+ runningSteps={runningSteps}
38
+ id={id}
39
+ cancelStep={cancelStep}
40
+ taskReload={taskReload}
41
+ taskReloadStart={taskReloadStart}
42
+ />
43
+ )}
44
+ {showErrors && (
45
+ <Errors executionPlan={executionPlan} failedSteps={failedSteps} />
46
+ )}
47
+ </div>
48
+ );
49
+ };
50
+
51
+ ExecutionDetails.propTypes = {
52
+ state: PropTypes.string,
53
+ result: PropTypes.string,
54
+ runningSteps: PropTypes.array,
55
+ cancelStep: PropTypes.func.isRequired,
56
+ id: PropTypes.string.isRequired,
57
+ taskReload: PropTypes.bool.isRequired,
58
+ taskReloadStart: PropTypes.func.isRequired,
59
+ executionPlan: PropTypes.shape({}),
60
+ failedSteps: PropTypes.array,
61
+ };
62
+
63
+ ExecutionDetails.defaultProps = {
64
+ state: '',
65
+ result: undefined,
66
+ runningSteps: [],
67
+ executionPlan: {},
68
+ failedSteps: [],
69
+ };
70
+
71
+ export default ExecutionDetails;
@@ -6,17 +6,22 @@ import { STATUS } from 'foremanReact/constants';
6
6
  import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
7
7
  import { ResourceLoadFailedEmptyState } from 'foremanReact/components/common/EmptyState';
8
8
  import Task from './Components/Task';
9
- import RunningSteps from './Components/RunningSteps';
10
- import Errors from './Components/Errors';
11
9
  import Locks from './Components/Locks';
12
10
  import Raw from './Components/Raw';
11
+ import ExecutionDetails from './ExecutionDetails';
13
12
  import Dependencies from './Components/Dependencies';
14
13
  import { TASKS_PATH, VIEW_FOREMAN_TASKS } from './TaskDetailsConstants';
15
- import { getTaskID } from './TasksDetailsHelper';
16
14
  import { TaskSkeleton } from './Components/TaskSkeleton';
17
15
 
18
16
  import './TaskDetails.scss';
19
17
 
18
+ export const TASK_DETAILS_TAB_KEYS = Object.freeze({
19
+ EXECUTION: 'execution',
20
+ DEPENDENCIES: 'dependencies',
21
+ LOCKS: 'locks',
22
+ RAW: 'raw',
23
+ });
24
+
20
25
  const TaskDetails = ({
21
26
  executionPlan,
22
27
  failedSteps,
@@ -31,11 +36,11 @@ const TaskDetails = ({
31
36
  apiStatus,
32
37
  apiErrorMessage,
33
38
  apiErrorCode,
39
+ id,
34
40
  ...props
35
41
  }) => {
36
- const id = getTaskID();
37
- const { taskReload, isLoading, result } = props;
38
- const [activeTabKey, setActiveTabKey] = useState(1);
42
+ const { taskReload, isLoading } = props;
43
+ const [activeTab, setActiveTab] = useState(TASK_DETAILS_TAB_KEYS.EXECUTION);
39
44
  const hasViewPermission = usePermissions([VIEW_FOREMAN_TASKS]);
40
45
 
41
46
  useEffect(() => {
@@ -76,7 +81,7 @@ const TaskDetails = ({
76
81
  const cancellable = executionPlan ? executionPlan.cancellable : false;
77
82
  const lockRecords = locks.concat(links);
78
83
 
79
- const taskComponentProps = {
84
+ const taskProps = {
80
85
  ...props,
81
86
  cancellable,
82
87
  resumable,
@@ -87,49 +92,49 @@ const TaskDetails = ({
87
92
 
88
93
  return (
89
94
  <div className="task-details-react">
95
+ <section className="task-details-overview-section">
96
+ {isLoading ? <TaskSkeleton /> : <Task {...taskProps} />}
97
+ </section>
90
98
  <Tabs
99
+ aria-label={__('Task details')}
91
100
  id="task-details-tabs"
92
101
  ouiaId="task-details-tabs"
93
- activeKey={activeTabKey}
94
- onSelect={(_event, tabKey) => setActiveTabKey(tabKey)}
102
+ activeKey={activeTab}
95
103
  mountOnEnter
104
+ onSelect={(_e, tabKey) => setActiveTab(tabKey)}
96
105
  >
97
106
  <Tab
98
- eventKey={1}
99
- title={<TabTitleText>{__('Task')}</TabTitleText>}
100
- aria-label={__('Task')}
101
- ouiaId="task-details-tab-task"
102
- >
103
- {isLoading ? <TaskSkeleton /> : <Task {...taskComponentProps} />}
104
- </Tab>
105
- <Tab
106
- eventKey={2}
107
- title={<TabTitleText>{__('Running Steps')}</TabTitleText>}
108
107
  isDisabled={isLoading}
109
- aria-label={__('Running Steps')}
110
- ouiaId="task-details-tab-running-steps"
108
+ eventKey={TASK_DETAILS_TAB_KEYS.EXECUTION}
109
+ title={<TabTitleText>{__('Execution details')}</TabTitleText>}
110
+ aria-label={__('Execution details')}
111
+ ouiaId="task-details-tab-execution-details"
111
112
  >
112
- <RunningSteps
113
- executionPlan={executionPlan}
114
- result={result}
115
- runningSteps={runningSteps}
116
- id={id}
117
- cancelStep={cancelStep}
118
- taskReload={taskReload}
119
- taskReloadStart={taskReloadStart}
120
- />
113
+ {!isLoading && (
114
+ <ExecutionDetails
115
+ state={props.state}
116
+ result={props.result}
117
+ runningSteps={runningSteps}
118
+ cancelStep={cancelStep}
119
+ id={id}
120
+ taskReload={taskReload}
121
+ taskReloadStart={taskReloadStart}
122
+ executionPlan={executionPlan}
123
+ failedSteps={failedSteps}
124
+ />
125
+ )}
121
126
  </Tab>
122
127
  <Tab
123
- eventKey={3}
124
- title={<TabTitleText>{__('Errors')}</TabTitleText>}
128
+ eventKey={TASK_DETAILS_TAB_KEYS.DEPENDENCIES}
125
129
  isDisabled={isLoading}
126
- aria-label={__('Errors')}
127
- ouiaId="task-details-tab-errors"
130
+ title={<TabTitleText>{__('Dependencies')}</TabTitleText>}
131
+ aria-label={__('Dependencies')}
132
+ ouiaId="task-details-tab-dependencies"
128
133
  >
129
- <Errors executionPlan={executionPlan} failedSteps={failedSteps} />
134
+ <Dependencies dependsOn={dependsOn} blocks={blocks} />
130
135
  </Tab>
131
136
  <Tab
132
- eventKey={4}
137
+ eventKey={TASK_DETAILS_TAB_KEYS.LOCKS}
133
138
  title={<TabTitleText>{__('Locks')}</TabTitleText>}
134
139
  isDisabled={isLoading}
135
140
  aria-label={__('Locks')}
@@ -138,16 +143,7 @@ const TaskDetails = ({
138
143
  <Locks locks={lockRecords} />
139
144
  </Tab>
140
145
  <Tab
141
- eventKey={5}
142
- title={<TabTitleText>{__('Dependencies')}</TabTitleText>}
143
- isDisabled={isLoading}
144
- aria-label={__('Dependencies')}
145
- ouiaId="task-details-tab-dependencies"
146
- >
147
- <Dependencies dependsOn={dependsOn} blocks={blocks} />
148
- </Tab>
149
- <Tab
150
- eventKey={6}
146
+ eventKey={TASK_DETAILS_TAB_KEYS.RAW}
151
147
  title={<TabTitleText>{__('Raw')}</TabTitleText>}
152
148
  isDisabled={isLoading}
153
149
  aria-label={__('Raw')}
@@ -169,7 +165,9 @@ const TaskDetails = ({
169
165
  };
170
166
 
171
167
  TaskDetails.propTypes = {
168
+ id: PropTypes.string.isRequired,
172
169
  label: PropTypes.string,
170
+ result: PropTypes.string,
173
171
  runningSteps: PropTypes.array,
174
172
  cancelStep: PropTypes.func.isRequired,
175
173
  taskReload: PropTypes.bool.isRequired,
@@ -181,10 +179,10 @@ TaskDetails.propTypes = {
181
179
  links: PropTypes.array,
182
180
  dependsOn: PropTypes.array,
183
181
  blocks: PropTypes.array,
182
+ executionPlan: PropTypes.shape({}),
183
+ failedSteps: PropTypes.array,
184
184
  ...Task.propTypes,
185
- ...Errors.propTypes,
186
185
  ...Locks.propTypes,
187
- ...Dependencies.propTypes,
188
186
  ...Raw.propTypes,
189
187
  };
190
188
  TaskDetails.defaultProps = {
@@ -194,11 +192,10 @@ TaskDetails.defaultProps = {
194
192
  links: [],
195
193
  dependsOn: [],
196
194
  blocks: [],
195
+ failedSteps: [],
196
+ executionPlan: {},
197
197
  ...Task.defaultProps,
198
- ...RunningSteps.defaultProps,
199
- ...Errors.defaultProps,
200
198
  ...Locks.defaultProps,
201
- ...Dependencies.defaultProps,
202
199
  ...Raw.defaultProps,
203
200
  };
204
201
 
@@ -3,48 +3,10 @@
3
3
  #pf-tab-section-1-task-details-tabs > button {
4
4
  margin-right: 3px;
5
5
  }
6
+
6
7
  .container {
7
8
  margin: 0;
8
9
  }
9
- .spin {
10
- -webkit-animation: spin 1s infinite linear;
11
- -moz-animation: spin 1s infinite linear;
12
- -o-animation: spin 1s infinite linear;
13
- animation: spin 1s infinite linear;
14
- -webkit-transform-origin: 50% 50%;
15
- transform-origin: 50% 50%;
16
- -ms-transform-origin: 50% 50%; /* IE 9 */
17
- }
18
-
19
- @-moz-keyframes spin {
20
- from {
21
- -moz-transform: rotate(0deg);
22
- }
23
- to {
24
- -moz-transform: rotate(360deg);
25
- }
26
- }
27
-
28
- @-webkit-keyframes spin {
29
- from {
30
- -webkit-transform: rotate(0deg);
31
- }
32
- to {
33
- -webkit-transform: rotate(360deg);
34
- }
35
- }
36
-
37
- @keyframes spin {
38
- from {
39
- transform: rotate(0deg);
40
- }
41
- to {
42
- transform: rotate(360deg);
43
- }
44
- }
45
- .dynflow-button > span {
46
- pointer-events: auto;
47
- }
48
10
 
49
11
  pre {
50
12
  white-space: pre-wrap;
@@ -6,7 +6,11 @@ import {
6
6
  stopInterval,
7
7
  } from 'foremanReact/redux/middlewares/IntervalMiddleware';
8
8
  import { foremanTasksApiPath, foremanTasksPath } from '../common/urlHelpers';
9
- import { TASK_STEP_CANCEL, FOREMAN_TASK_DETAILS } from './TaskDetailsConstants';
9
+ import {
10
+ TASK_STEP_CANCEL,
11
+ FOREMAN_TASK_DETAILS,
12
+ TASK_RELOAD_INTERVAL_MS,
13
+ } from './TaskDetailsConstants';
10
14
  import {
11
15
  errorToastData,
12
16
  infoToastData,
@@ -30,7 +34,7 @@ export const taskReloadStart = id => dispatch => {
30
34
  dispatch(stopInterval(FOREMAN_TASK_DETAILS));
31
35
  },
32
36
  }),
33
- 5000
37
+ TASK_RELOAD_INTERVAL_MS
34
38
  )
35
39
  );
36
40
  };
@@ -3,3 +3,5 @@ export const FOREMAN_TASK_DETAILS_SUCCESS = 'FOREMAN_TASK_DETAILS_SUCCESS';
3
3
  export const TASKS_PATH = '/foreman_tasks/tasks';
4
4
  export const TASK_STEP_CANCEL = 'TASK_STEP_CANCEL';
5
5
  export const VIEW_FOREMAN_TASKS = 'view_foreman_tasks';
6
+
7
+ export const TASK_RELOAD_INTERVAL_MS = 5000;
@@ -5,7 +5,7 @@ import {
5
5
  selectAPIError as selectAPIErrorByKey,
6
6
  } from 'foremanReact/redux/API/APISelectors';
7
7
  import { selectDoesIntervalExist } from 'foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors';
8
- import { STATUS } from 'foremanReact/constants';
8
+ import { STATUS, PERCENT_MULTIPLIER } from 'foremanReact/constants';
9
9
  import { selectForemanTasks } from '../../ForemanTasksSelectors';
10
10
  import { FOREMAN_TASK_DETAILS } from './TaskDetailsConstants';
11
11
 
@@ -39,14 +39,9 @@ export const selectResumable = state =>
39
39
  export const selectCancellable = state =>
40
40
  selectTaskDetailsResponse(state).cancellable || false;
41
41
 
42
- export const selectErrors = state => {
43
- const { humanized } = selectTaskDetailsResponse(state);
44
- return humanized ? humanized.errors : [];
45
- };
46
-
47
42
  export const selectProgress = state =>
48
43
  selectTaskDetailsResponse(state).progress
49
- ? Math.trunc(selectTaskDetailsResponse(state).progress * 100)
44
+ ? Math.trunc(selectTaskDetailsResponse(state).progress * PERCENT_MULTIPLIER)
50
45
  : 0;
51
46
 
52
47
  export const selectUsername = state =>
@@ -0,0 +1,241 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+
5
+ import ExecutionDetails from '../ExecutionDetails';
6
+ import {
7
+ fixtureFailedExecutionDetail,
8
+ fixtureRunningExecutionDetail,
9
+ } from './TaskDetails.fixtures';
10
+
11
+ const rtlBaseProps = {
12
+ cancelStep: jest.fn(),
13
+ taskReloadStart: jest.fn(),
14
+ id: 'a15dd820-32f1-4ced-9ab7-c0fab8234c47',
15
+ taskReload: false,
16
+ };
17
+
18
+ describe('ExecutionDetails', () => {
19
+ it('renders execution details panel with OUIA id', () => {
20
+ render(
21
+ <ExecutionDetails
22
+ {...rtlBaseProps}
23
+ state="stopped"
24
+ runningSteps={[]}
25
+ executionPlan={{ state: 'stopped', cancellable: false }}
26
+ failedSteps={[]}
27
+ />
28
+ );
29
+
30
+ expect(
31
+ document.querySelector(
32
+ '[data-ouia-component-id="execution-details-panel"]'
33
+ )
34
+ ).toBeInTheDocument();
35
+ expect(
36
+ document.getElementById('execution-details-panel')
37
+ ).toBeInTheDocument();
38
+ });
39
+
40
+ it('shows Errors pane when stopped with no running steps', () => {
41
+ render(
42
+ <ExecutionDetails
43
+ {...rtlBaseProps}
44
+ state="stopped"
45
+ runningSteps={[]}
46
+ executionPlan={{ state: 'stopped', cancellable: false }}
47
+ failedSteps={[]}
48
+ />
49
+ );
50
+
51
+ expect(
52
+ screen.getByRole('heading', { name: /^no errors found$/i })
53
+ ).toBeInTheDocument();
54
+ expect(
55
+ screen.getByText(/the task finished with no errors or warnings/i)
56
+ ).toBeInTheDocument();
57
+ });
58
+
59
+ it('shows RunningSteps when stopped but runningSteps list is non-empty', () => {
60
+ render(
61
+ <ExecutionDetails
62
+ {...rtlBaseProps}
63
+ state="stopped"
64
+ runningSteps={[
65
+ {
66
+ cancellable: false,
67
+ id: 1,
68
+ action_class: 'Actions::Stale',
69
+ state: 'paused',
70
+ input: '{}',
71
+ output: '{}',
72
+ },
73
+ ]}
74
+ executionPlan={{ state: 'stopped', cancellable: false }}
75
+ failedSteps={[]}
76
+ />
77
+ );
78
+
79
+ expect(
80
+ screen.getByRole('heading', { name: 'Warning alert: Running step 1' })
81
+ ).toBeInTheDocument();
82
+ });
83
+
84
+ it('forwards executionPlan and result to RunningSteps when state is pending without steps', () => {
85
+ render(
86
+ <ExecutionDetails
87
+ {...rtlBaseProps}
88
+ state="pending"
89
+ runningSteps={[]}
90
+ executionPlan={{ state: 'planned', cancellable: false }}
91
+ result="pending"
92
+ failedSteps={[]}
93
+ />
94
+ );
95
+
96
+ expect(
97
+ screen.getByRole('heading', { level: 2, name: /planned task/i })
98
+ ).toBeInTheDocument();
99
+ });
100
+
101
+ it('shows temporarily suspended messaging when pending, plan running, result pending', () => {
102
+ render(
103
+ <ExecutionDetails
104
+ {...rtlBaseProps}
105
+ state="pending"
106
+ runningSteps={[]}
107
+ executionPlan={{ state: 'running', cancellable: false }}
108
+ result="pending"
109
+ failedSteps={[]}
110
+ />
111
+ );
112
+
113
+ expect(
114
+ screen.getByRole('heading', {
115
+ level: 4,
116
+ name: /temporarily suspended step/i,
117
+ })
118
+ ).toBeInTheDocument();
119
+ });
120
+
121
+ it('routes failed steps through Errors pane when stopped', () => {
122
+ render(
123
+ <ExecutionDetails
124
+ {...rtlBaseProps}
125
+ {...fixtureFailedExecutionDetail}
126
+ executionPlan={fixtureFailedExecutionDetail.executionPlan}
127
+ failedSteps={fixtureFailedExecutionDetail.failedSteps}
128
+ />
129
+ );
130
+
131
+ expect(
132
+ screen.getByRole('tab', {
133
+ name: /action actions::katello::eventqueue::monitor is already active/i,
134
+ })
135
+ ).toBeInTheDocument();
136
+ expect(screen.getByLabelText(/failed task errors/i)).toBeInTheDocument();
137
+ expect(screen.getByText('Exception:')).toBeInTheDocument();
138
+ });
139
+
140
+ it('renders RunningSteps with cancel when task is running', () => {
141
+ const cancelStep = jest.fn();
142
+
143
+ render(
144
+ <ExecutionDetails
145
+ {...rtlBaseProps}
146
+ cancelStep={cancelStep}
147
+ {...fixtureRunningExecutionDetail}
148
+ runningSteps={[
149
+ {
150
+ ...fixtureRunningExecutionDetail.runningSteps[0],
151
+ cancellable: true,
152
+ },
153
+ ]}
154
+ />
155
+ );
156
+
157
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
158
+ });
159
+
160
+ it('shows RunningSteps when state is paused without running steps', () => {
161
+ render(
162
+ <ExecutionDetails
163
+ {...rtlBaseProps}
164
+ state="paused"
165
+ runningSteps={[]}
166
+ executionPlan={{ state: 'paused', cancellable: false }}
167
+ failedSteps={[]}
168
+ />
169
+ );
170
+
171
+ expect(screen.getByText(/no running steps/i)).toBeInTheDocument();
172
+ expect(
173
+ screen.queryByRole('heading', { name: /^no errors found$/i })
174
+ ).not.toBeInTheDocument();
175
+ });
176
+
177
+ it('shows RunningSteps when state is paused with running steps', () => {
178
+ render(
179
+ <ExecutionDetails
180
+ {...rtlBaseProps}
181
+ state="paused"
182
+ runningSteps={[
183
+ {
184
+ ...fixtureRunningExecutionDetail.runningSteps[0],
185
+ state: 'paused',
186
+ },
187
+ ]}
188
+ executionPlan={{ state: 'paused', cancellable: false }}
189
+ failedSteps={[]}
190
+ />
191
+ );
192
+
193
+ expect(
194
+ screen.getByRole('heading', { name: 'Warning alert: Running step 1' })
195
+ ).toBeInTheDocument();
196
+ expect(screen.getByText('paused')).toBeInTheDocument();
197
+ });
198
+
199
+ it('shows Errors when state is paused with failed steps', () => {
200
+ render(
201
+ <ExecutionDetails
202
+ {...rtlBaseProps}
203
+ {...fixtureFailedExecutionDetail}
204
+ state="paused"
205
+ executionPlan={{ state: 'paused', cancellable: false }}
206
+ runningSteps={[]}
207
+ />
208
+ );
209
+
210
+ expect(
211
+ screen.getByRole('tab', {
212
+ name: /action actions::katello::eventqueue::monitor is already active/i,
213
+ })
214
+ ).toBeInTheDocument();
215
+ expect(screen.getByLabelText(/failed task errors/i)).toBeInTheDocument();
216
+ expect(screen.getByText('Exception:')).toBeInTheDocument();
217
+ expect(screen.queryByText(/no running steps/i)).not.toBeInTheDocument();
218
+ });
219
+
220
+ it('shows Errors and RunningSteps when paused with both failed and running steps', () => {
221
+ render(
222
+ <ExecutionDetails
223
+ {...rtlBaseProps}
224
+ {...fixtureFailedExecutionDetail}
225
+ state="paused"
226
+ executionPlan={{ state: 'paused', cancellable: false }}
227
+ runningSteps={[
228
+ {
229
+ ...fixtureRunningExecutionDetail.runningSteps[0],
230
+ state: 'suspended',
231
+ },
232
+ ]}
233
+ />
234
+ );
235
+
236
+ expect(
237
+ screen.getByRole('heading', { name: 'Warning alert: Running step 1' })
238
+ ).toBeInTheDocument();
239
+ expect(screen.getByLabelText(/failed task errors/i)).toBeInTheDocument();
240
+ });
241
+ });