foreman-tasks 12.2.4 → 13.1.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/foreman_tasks/tasks_controller.rb +0 -5
  3. data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
  4. data/config/routes.rb +3 -2
  5. data/lib/foreman_tasks/engine.rb +2 -2
  6. data/lib/foreman_tasks/version.rb +1 -1
  7. data/test/controllers/tasks_controller_test.rb +0 -9
  8. data/test/foreman_tasks_test_helper.rb +2 -2
  9. data/test/integration/tasks_test.rb +17 -0
  10. data/test/test_plugin_helper.rb +8 -0
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
  13. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
  14. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
  15. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +237 -177
  16. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js +1 -6
  17. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -35
  18. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -69
  19. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
  20. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
  21. data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
  22. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +76 -65
  23. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -44
  24. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -1
  25. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +20 -10
  26. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
  27. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +100 -1
  28. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +226 -18
  29. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js +81 -0
  30. data/webpack/ForemanTasks/Components/common/taskResultIcon.js +53 -0
  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 +51 -0
  34. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
  35. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +277 -0
  36. data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +13 -12
  37. data/webpack/Routes/routes.js +6 -0
  38. data/webpack/Routes/routes.test.js +16 -5
  39. data/webpack/index.js +0 -5
  40. metadata +16 -3
  41. data/app/views/foreman_tasks/tasks/show.html.erb +0 -18
@@ -0,0 +1,65 @@
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 showingRunningSteps =
18
+ state === 'running' ||
19
+ state === 'pending' ||
20
+ state === 'paused' ||
21
+ runningSteps.length > 0;
22
+
23
+ return (
24
+ <div
25
+ id="execution-details-panel"
26
+ data-ouia-component-id="execution-details-panel"
27
+ >
28
+ {showingRunningSteps ? (
29
+ <RunningSteps
30
+ executionPlan={executionPlan}
31
+ result={result}
32
+ runningSteps={runningSteps}
33
+ id={id}
34
+ cancelStep={cancelStep}
35
+ taskReload={taskReload}
36
+ taskReloadStart={taskReloadStart}
37
+ />
38
+ ) : (
39
+ <Errors executionPlan={executionPlan} failedSteps={failedSteps} />
40
+ )}
41
+ </div>
42
+ );
43
+ };
44
+
45
+ ExecutionDetails.propTypes = {
46
+ state: PropTypes.string,
47
+ result: PropTypes.string,
48
+ runningSteps: PropTypes.array,
49
+ cancelStep: PropTypes.func.isRequired,
50
+ id: PropTypes.string.isRequired,
51
+ taskReload: PropTypes.bool.isRequired,
52
+ taskReloadStart: PropTypes.func.isRequired,
53
+ executionPlan: PropTypes.shape({}),
54
+ failedSteps: PropTypes.array,
55
+ };
56
+
57
+ ExecutionDetails.defaultProps = {
58
+ state: '',
59
+ result: undefined,
60
+ runningSteps: [],
61
+ executionPlan: {},
62
+ failedSteps: [],
63
+ };
64
+
65
+ export default ExecutionDetails;
@@ -1,20 +1,27 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { Tabs, Tab, TabTitleText } from '@patternfly/react-core';
4
- import { translate as __, sprintf } from 'foremanReact/common/I18n';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { STATUS } from 'foremanReact/constants';
6
- import MessageBox from 'foremanReact/components/common/MessageBox';
6
+ import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
7
+ import { ResourceLoadFailedEmptyState } from 'foremanReact/components/common/EmptyState';
7
8
  import Task from './Components/Task';
8
- import RunningSteps from './Components/RunningSteps';
9
- import Errors from './Components/Errors';
10
9
  import Locks from './Components/Locks';
11
10
  import Raw from './Components/Raw';
11
+ import ExecutionDetails from './ExecutionDetails';
12
12
  import Dependencies from './Components/Dependencies';
13
- import { getTaskID } from './TasksDetailsHelper';
13
+ import { TASKS_PATH, VIEW_FOREMAN_TASKS } from './TaskDetailsConstants';
14
14
  import { TaskSkeleton } from './Components/TaskSkeleton';
15
15
 
16
16
  import './TaskDetails.scss';
17
17
 
18
+ export const TASK_DETAILS_TAB_KEYS = Object.freeze({
19
+ EXECUTION: 'execution',
20
+ DEPENDENCIES: 'dependencies',
21
+ LOCKS: 'locks',
22
+ RAW: 'raw',
23
+ });
24
+
18
25
  const TaskDetails = ({
19
26
  executionPlan,
20
27
  failedSteps,
@@ -26,12 +33,15 @@ const TaskDetails = ({
26
33
  cancelStep,
27
34
  taskReloadStart,
28
35
  taskReloadStop,
29
- APIerror,
36
+ apiStatus,
37
+ apiErrorMessage,
38
+ apiErrorCode,
39
+ id,
30
40
  ...props
31
41
  }) => {
32
- const id = getTaskID();
33
- const { taskReload, status, isLoading, result } = props;
34
- const [activeTabKey, setActiveTabKey] = useState(1);
42
+ const { taskReload, isLoading } = props;
43
+ const [activeTab, setActiveTab] = useState(TASK_DETAILS_TAB_KEYS.EXECUTION);
44
+ const hasViewPermission = usePermissions([VIEW_FOREMAN_TASKS]);
35
45
 
36
46
  useEffect(() => {
37
47
  taskReloadStart(id);
@@ -48,74 +58,83 @@ const TaskDetails = ({
48
58
  }
49
59
  };
50
60
 
51
- if (status === STATUS.ERROR) {
61
+ if (apiStatus === STATUS.ERROR || !hasViewPermission) {
52
62
  return (
53
- <MessageBox
54
- key="task-details-error"
55
- icontype="error-circle-o"
56
- msg={sprintf(__('Could not receive data: %s'), APIerror?.message)}
63
+ <ResourceLoadFailedEmptyState
64
+ resourceLabel={__('task')}
65
+ resourceId={id}
66
+ httpStatus={apiErrorCode}
67
+ errorMessage={apiErrorMessage}
68
+ viewPermissions={['view_foreman_tasks']}
69
+ requiredPermissions={['view_foreman_tasks']}
70
+ ouiaIdPrefix="task-details-empty-state"
71
+ primaryAction={{
72
+ label: __('Back to tasks'),
73
+ url: TASKS_PATH,
74
+ ouiaId: 'task-details-empty-state-tasks-list',
75
+ }}
57
76
  />
58
77
  );
59
78
  }
79
+
60
80
  const resumable = executionPlan ? executionPlan.state === 'paused' : false;
61
81
  const cancellable = executionPlan ? executionPlan.cancellable : false;
62
82
  const lockRecords = locks.concat(links);
63
83
 
64
- const taskComponentProps = {
84
+ const taskProps = {
65
85
  ...props,
66
86
  cancellable,
67
87
  resumable,
68
88
  id,
69
- status,
70
89
  taskProgressToggle,
71
90
  taskReloadStart,
72
91
  };
73
92
 
74
93
  return (
75
- <div className="task-details-react well">
94
+ <div className="task-details-react">
95
+ <section className="task-details-overview-section">
96
+ {isLoading ? <TaskSkeleton /> : <Task {...taskProps} />}
97
+ </section>
76
98
  <Tabs
99
+ aria-label={__('Task details')}
77
100
  id="task-details-tabs"
78
101
  ouiaId="task-details-tabs"
79
- activeKey={activeTabKey}
80
- onSelect={(_event, tabKey) => setActiveTabKey(tabKey)}
102
+ activeKey={activeTab}
81
103
  mountOnEnter
104
+ onSelect={(_e, tabKey) => setActiveTab(tabKey)}
82
105
  >
83
106
  <Tab
84
- eventKey={1}
85
- title={<TabTitleText>{__('Task')}</TabTitleText>}
86
- aria-label={__('Task')}
87
- ouiaId="task-details-tab-task"
88
- >
89
- {isLoading ? <TaskSkeleton /> : <Task {...taskComponentProps} />}
90
- </Tab>
91
- <Tab
92
- eventKey={2}
93
- title={<TabTitleText>{__('Running Steps')}</TabTitleText>}
94
107
  isDisabled={isLoading}
95
- aria-label={__('Running Steps')}
96
- 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"
97
112
  >
98
- <RunningSteps
99
- executionPlan={executionPlan}
100
- result={result}
101
- runningSteps={runningSteps}
102
- id={id}
103
- cancelStep={cancelStep}
104
- taskReload={taskReload}
105
- taskReloadStart={taskReloadStart}
106
- />
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
+ )}
107
126
  </Tab>
108
127
  <Tab
109
- eventKey={3}
110
- title={<TabTitleText>{__('Errors')}</TabTitleText>}
128
+ eventKey={TASK_DETAILS_TAB_KEYS.DEPENDENCIES}
111
129
  isDisabled={isLoading}
112
- aria-label={__('Errors')}
113
- ouiaId="task-details-tab-errors"
130
+ title={<TabTitleText>{__('Dependencies')}</TabTitleText>}
131
+ aria-label={__('Dependencies')}
132
+ ouiaId="task-details-tab-dependencies"
114
133
  >
115
- <Errors executionPlan={executionPlan} failedSteps={failedSteps} />
134
+ <Dependencies dependsOn={dependsOn} blocks={blocks} />
116
135
  </Tab>
117
136
  <Tab
118
- eventKey={4}
137
+ eventKey={TASK_DETAILS_TAB_KEYS.LOCKS}
119
138
  title={<TabTitleText>{__('Locks')}</TabTitleText>}
120
139
  isDisabled={isLoading}
121
140
  aria-label={__('Locks')}
@@ -124,16 +143,7 @@ const TaskDetails = ({
124
143
  <Locks locks={lockRecords} />
125
144
  </Tab>
126
145
  <Tab
127
- eventKey={5}
128
- title={<TabTitleText>{__('Dependencies')}</TabTitleText>}
129
- isDisabled={isLoading}
130
- aria-label={__('Dependencies')}
131
- ouiaId="task-details-tab-dependencies"
132
- >
133
- <Dependencies dependsOn={dependsOn} blocks={blocks} />
134
- </Tab>
135
- <Tab
136
- eventKey={6}
146
+ eventKey={TASK_DETAILS_TAB_KEYS.RAW}
137
147
  title={<TabTitleText>{__('Raw')}</TabTitleText>}
138
148
  isDisabled={isLoading}
139
149
  aria-label={__('Raw')}
@@ -155,36 +165,37 @@ const TaskDetails = ({
155
165
  };
156
166
 
157
167
  TaskDetails.propTypes = {
168
+ id: PropTypes.string.isRequired,
158
169
  label: PropTypes.string,
170
+ result: PropTypes.string,
159
171
  runningSteps: PropTypes.array,
160
172
  cancelStep: PropTypes.func.isRequired,
161
173
  taskReload: PropTypes.bool.isRequired,
162
- status: PropTypes.oneOf(Object.keys(STATUS)),
163
- APIerror: PropTypes.object,
174
+ apiStatus: PropTypes.oneOf(Object.keys(STATUS)),
175
+ apiErrorMessage: PropTypes.string,
176
+ apiErrorCode: PropTypes.number,
164
177
  taskReloadStop: PropTypes.func.isRequired,
165
178
  taskReloadStart: PropTypes.func.isRequired,
166
179
  links: PropTypes.array,
167
180
  dependsOn: PropTypes.array,
168
181
  blocks: PropTypes.array,
182
+ executionPlan: PropTypes.shape({}),
183
+ failedSteps: PropTypes.array,
169
184
  ...Task.propTypes,
170
- ...Errors.propTypes,
171
185
  ...Locks.propTypes,
172
- ...Dependencies.propTypes,
173
186
  ...Raw.propTypes,
174
187
  };
175
188
  TaskDetails.defaultProps = {
176
189
  label: '',
177
190
  runningSteps: [],
178
- APIerror: null,
179
- status: STATUS.PENDING,
191
+ apiErrorMessage: '',
180
192
  links: [],
181
193
  dependsOn: [],
182
194
  blocks: [],
195
+ failedSteps: [],
196
+ executionPlan: {},
183
197
  ...Task.defaultProps,
184
- ...RunningSteps.defaultProps,
185
- ...Errors.defaultProps,
186
198
  ...Locks.defaultProps,
187
- ...Dependencies.defaultProps,
188
199
  ...Raw.defaultProps,
189
200
  };
190
201
 
@@ -1,54 +1,11 @@
1
1
  .task-details-react {
2
- .progress-label-top-right {
3
- font-size: 11px;
4
- text-align: right;
5
- }
6
2
  a,
7
- button {
3
+ #pf-tab-section-1-task-details-tabs > button {
8
4
  margin-right: 3px;
9
5
  }
10
6
  .container {
11
7
  margin: 0;
12
8
  }
13
- .spin {
14
- -webkit-animation: spin 1s infinite linear;
15
- -moz-animation: spin 1s infinite linear;
16
- -o-animation: spin 1s infinite linear;
17
- animation: spin 1s infinite linear;
18
- -webkit-transform-origin: 50% 50%;
19
- transform-origin: 50% 50%;
20
- -ms-transform-origin: 50% 50%; /* IE 9 */
21
- }
22
-
23
- @-moz-keyframes spin {
24
- from {
25
- -moz-transform: rotate(0deg);
26
- }
27
- to {
28
- -moz-transform: rotate(360deg);
29
- }
30
- }
31
-
32
- @-webkit-keyframes spin {
33
- from {
34
- -webkit-transform: rotate(0deg);
35
- }
36
- to {
37
- -webkit-transform: rotate(360deg);
38
- }
39
- }
40
-
41
- @keyframes spin {
42
- from {
43
- transform: rotate(0deg);
44
- }
45
- to {
46
- transform: rotate(360deg);
47
- }
48
- }
49
- .dynflow-button > span {
50
- pointer-events: auto;
51
- }
52
9
 
53
10
  pre {
54
11
  white-space: pre-wrap;
@@ -1,4 +1,5 @@
1
1
  export const FOREMAN_TASK_DETAILS = 'FOREMAN_TASK_DETAILS';
2
2
  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
+ export const VIEW_FOREMAN_TASKS = 'view_foreman_tasks';
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable camelcase */
2
2
  import {
3
3
  selectAPIResponse,
4
- selectAPIByKey,
4
+ selectAPIStatus as selectAPIStatusByKey,
5
+ selectAPIError as selectAPIErrorByKey,
5
6
  } from 'foremanReact/redux/API/APISelectors';
6
7
  import { selectDoesIntervalExist } from 'foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors';
7
8
  import { STATUS } from 'foremanReact/constants';
@@ -38,11 +39,6 @@ export const selectResumable = state =>
38
39
  export const selectCancellable = state =>
39
40
  selectTaskDetailsResponse(state).cancellable || false;
40
41
 
41
- export const selectErrors = state => {
42
- const { humanized } = selectTaskDetailsResponse(state);
43
- return humanized ? humanized.errors : [];
44
- };
45
-
46
42
  export const selectProgress = state =>
47
43
  selectTaskDetailsResponse(state).progress
48
44
  ? Math.trunc(selectTaskDetailsResponse(state).progress * 100)
@@ -101,14 +97,28 @@ export const selectDynflowEnableConsole = state =>
101
97
  export const selectCanEdit = state =>
102
98
  selectTaskDetailsResponse(state).can_edit || false;
103
99
 
104
- export const selectStatus = state => selectTaskDetailsResponse(state).status;
100
+ export const selectAPIStatus = state =>
101
+ selectAPIStatusByKey(state, FOREMAN_TASK_DETAILS);
105
102
 
106
103
  export const selectAPIError = state =>
107
- selectTaskDetailsResponse(state)?.APIerror;
104
+ selectAPIErrorByKey(state, FOREMAN_TASK_DETAILS);
105
+
106
+ export const selectAPIErrorMessage = state => {
107
+ const apiError = selectAPIError(state);
108
+
109
+ if (apiError?.response?.data?.error) {
110
+ return apiError.response.data.error.message;
111
+ }
112
+
113
+ return apiError?.message;
114
+ };
115
+
116
+ export const selectAPIErrorCode = state =>
117
+ selectAPIError(state)?.response?.status;
108
118
 
109
119
  export const selectIsLoading = state =>
110
- !!selectAPIByKey(state, FOREMAN_TASK_DETAILS).response &&
111
- selectStatus(state) === STATUS.PENDING;
120
+ !Object.keys(selectTaskDetailsResponse(state) ?? {}).length &&
121
+ selectAPIStatus(state) === STATUS.PENDING;
112
122
 
113
123
  export const selectDependsOn = state =>
114
124
  selectTaskDetailsResponse(state).depends_on || [];
@@ -0,0 +1,194 @@
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('[data-ouia-component-id="execution-details-panel"]')
32
+ ).toBeInTheDocument();
33
+ expect(document.getElementById('execution-details-panel')).toBeInTheDocument();
34
+ });
35
+
36
+ it('shows Errors pane when stopped with no running steps', () => {
37
+ render(
38
+ <ExecutionDetails
39
+ {...rtlBaseProps}
40
+ state="stopped"
41
+ runningSteps={[]}
42
+ executionPlan={{ state: 'stopped', cancellable: false }}
43
+ failedSteps={[]}
44
+ />
45
+ );
46
+
47
+ expect(screen.getByRole('heading', { name: /^no errors found$/i })).toBeInTheDocument();
48
+ expect(
49
+ screen.getByText(/the task finished with no errors or warnings/i)
50
+ ).toBeInTheDocument();
51
+ });
52
+
53
+ it('shows RunningSteps when stopped but runningSteps list is non-empty', () => {
54
+ render(
55
+ <ExecutionDetails
56
+ {...rtlBaseProps}
57
+ state="stopped"
58
+ runningSteps={[
59
+ {
60
+ cancellable: false,
61
+ id: 1,
62
+ action_class: 'Actions::Stale',
63
+ state: 'paused',
64
+ input: '{}',
65
+ output: '{}',
66
+ },
67
+ ]}
68
+ executionPlan={{ state: 'stopped', cancellable: false }}
69
+ failedSteps={[]}
70
+ />
71
+ );
72
+
73
+ expect(
74
+ screen.getByRole('heading', { name: 'Warning alert: Running step 1' })
75
+ ).toBeInTheDocument();
76
+ });
77
+
78
+ it('forwards executionPlan and result to RunningSteps when state is pending without steps', () => {
79
+ render(
80
+ <ExecutionDetails
81
+ {...rtlBaseProps}
82
+ state="pending"
83
+ runningSteps={[]}
84
+ executionPlan={{ state: 'planned', cancellable: false }}
85
+ result="pending"
86
+ failedSteps={[]}
87
+ />
88
+ );
89
+
90
+ expect(
91
+ screen.getByRole('heading', { level: 2, name: /planned task/i })
92
+ ).toBeInTheDocument();
93
+ });
94
+
95
+ it('shows temporarily suspended messaging when pending, plan running, result pending', () => {
96
+ render(
97
+ <ExecutionDetails
98
+ {...rtlBaseProps}
99
+ state="pending"
100
+ runningSteps={[]}
101
+ executionPlan={{ state: 'running', cancellable: false }}
102
+ result="pending"
103
+ failedSteps={[]}
104
+ />
105
+ );
106
+
107
+ expect(
108
+ screen.getByRole('heading', {
109
+ level: 4,
110
+ name: /temporarily suspended step/i,
111
+ })
112
+ ).toBeInTheDocument();
113
+ });
114
+
115
+ it('routes failed steps through Errors pane when stopped', () => {
116
+ render(
117
+ <ExecutionDetails
118
+ {...rtlBaseProps}
119
+ {...fixtureFailedExecutionDetail}
120
+ executionPlan={fixtureFailedExecutionDetail.executionPlan}
121
+ failedSteps={fixtureFailedExecutionDetail.failedSteps}
122
+ />
123
+ );
124
+
125
+ expect(
126
+ screen.getByRole('tab', {
127
+ name: /action actions::katello::eventqueue::monitor is already active/i,
128
+ })
129
+ ).toBeInTheDocument();
130
+ expect(
131
+ screen.getByLabelText(/failed task errors/i)
132
+ ).toBeInTheDocument();
133
+ expect(screen.getByText('Exception:')).toBeInTheDocument();
134
+ });
135
+
136
+ it('renders RunningSteps with cancel when task is running', () => {
137
+ const cancelStep = jest.fn();
138
+
139
+ render(
140
+ <ExecutionDetails
141
+ {...rtlBaseProps}
142
+ cancelStep={cancelStep}
143
+ {...fixtureRunningExecutionDetail}
144
+ runningSteps={[
145
+ {
146
+ ...fixtureRunningExecutionDetail.runningSteps[0],
147
+ cancellable: true,
148
+ },
149
+ ]}
150
+ />
151
+ );
152
+
153
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
154
+ });
155
+
156
+ it('shows RunningSteps when state is paused without running steps', () => {
157
+ render(
158
+ <ExecutionDetails
159
+ {...rtlBaseProps}
160
+ state="paused"
161
+ runningSteps={[]}
162
+ executionPlan={{ state: 'paused', cancellable: false }}
163
+ failedSteps={[]}
164
+ />
165
+ );
166
+
167
+ expect(screen.getByText(/no running steps/i)).toBeInTheDocument();
168
+ expect(
169
+ screen.queryByRole('heading', { name: /^no errors found$/i })
170
+ ).not.toBeInTheDocument();
171
+ });
172
+
173
+ it('shows RunningSteps when state is paused with running steps', () => {
174
+ render(
175
+ <ExecutionDetails
176
+ {...rtlBaseProps}
177
+ state="paused"
178
+ runningSteps={[
179
+ {
180
+ ...fixtureRunningExecutionDetail.runningSteps[0],
181
+ state: 'paused',
182
+ },
183
+ ]}
184
+ executionPlan={{ state: 'paused', cancellable: false }}
185
+ failedSteps={[]}
186
+ />
187
+ );
188
+
189
+ expect(
190
+ screen.getByRole('heading', { name: 'Warning alert: Running step 1' })
191
+ ).toBeInTheDocument();
192
+ expect(screen.getByText('paused')).toBeInTheDocument();
193
+ });
194
+ });