foreman-tasks 13.0.0 → 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.
- checksums.yaml +4 -4
- data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
- data/lib/foreman_tasks/version.rb +1 -1
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +236 -131
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -33
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -67
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
- data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +48 -51
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +0 -39
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +0 -5
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +99 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +129 -8
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +10 -33
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +100 -88
- data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +7 -8
- data/webpack/Routes/routes.js +1 -1
- data/webpack/Routes/routes.test.js +1 -1
- data/webpack/index.js +0 -5
- metadata +8 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
2
3
|
import { render, screen } from '@testing-library/react';
|
|
3
4
|
import '@testing-library/jest-dom';
|
|
4
5
|
import { configureStore } from '@reduxjs/toolkit';
|
|
@@ -7,7 +8,13 @@ import { createMemoryHistory } from 'history';
|
|
|
7
8
|
import { Router } from 'react-router-dom';
|
|
8
9
|
|
|
9
10
|
import TaskDetails from '../TaskDetails';
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
fixtureFailedExecutionDetail,
|
|
13
|
+
fixtureWithDependencies,
|
|
14
|
+
fixtureWithOverviewMessages,
|
|
15
|
+
minProps,
|
|
16
|
+
taskDetailsWithExecutionTabDefaults,
|
|
17
|
+
} from './TaskDetails.fixtures';
|
|
11
18
|
import { VIEW_FOREMAN_TASKS } from '../TaskDetailsConstants';
|
|
12
19
|
|
|
13
20
|
const mockUseForemanPermissions = jest.fn(
|
|
@@ -45,6 +52,10 @@ describe('TaskDetails', () => {
|
|
|
45
52
|
);
|
|
46
53
|
});
|
|
47
54
|
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
jest.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
48
59
|
it(`shows ResourceLoadFailedEmptyState when ${VIEW_FOREMAN_TASKS} is absent`, () => {
|
|
49
60
|
mockUseForemanPermissions.mockImplementation(() => new Set());
|
|
50
61
|
|
|
@@ -111,25 +122,135 @@ describe('TaskDetails', () => {
|
|
|
111
122
|
expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
|
|
112
123
|
});
|
|
113
124
|
|
|
114
|
-
it('shows skeleton
|
|
125
|
+
it('shows skeleton in the overview while loading', () => {
|
|
115
126
|
const { container } = renderTaskDetails({ isLoading: true });
|
|
116
127
|
expect(
|
|
117
128
|
container.querySelector('.react-loading-skeleton')
|
|
118
129
|
).toBeInTheDocument();
|
|
119
130
|
});
|
|
120
131
|
|
|
121
|
-
it('renders
|
|
132
|
+
it('renders four tabs with expected labels', () => {
|
|
122
133
|
renderTaskDetails();
|
|
123
|
-
expect(document.getElementById('task-details-tabs')).toBeInTheDocument();
|
|
124
|
-
expect(screen.getByRole('tab', { name: /^task$/i })).toBeInTheDocument();
|
|
125
134
|
expect(
|
|
126
|
-
screen.getByRole('tab', { name: /
|
|
135
|
+
screen.getByRole('tab', { name: /execution details/i })
|
|
127
136
|
).toBeInTheDocument();
|
|
128
|
-
expect(screen.getByRole('tab', { name: /errors/i })).toBeInTheDocument();
|
|
129
|
-
expect(screen.getByRole('tab', { name: /locks/i })).toBeInTheDocument();
|
|
130
137
|
expect(
|
|
131
138
|
screen.getByRole('tab', { name: /dependencies/i })
|
|
132
139
|
).toBeInTheDocument();
|
|
140
|
+
expect(screen.getByRole('tab', { name: /locks/i })).toBeInTheDocument();
|
|
133
141
|
expect(screen.getByRole('tab', { name: /raw/i })).toBeInTheDocument();
|
|
134
142
|
});
|
|
143
|
+
|
|
144
|
+
it('exposes stable ouiaIds on tabs for automation', () => {
|
|
145
|
+
renderTaskDetails();
|
|
146
|
+
|
|
147
|
+
expect(
|
|
148
|
+
document.querySelector(
|
|
149
|
+
'[data-ouia-component-id="task-details-tab-execution-details"]'
|
|
150
|
+
)
|
|
151
|
+
).toBeInTheDocument();
|
|
152
|
+
expect(
|
|
153
|
+
document.querySelector(
|
|
154
|
+
'[data-ouia-component-id="task-details-tab-dependencies"]'
|
|
155
|
+
)
|
|
156
|
+
).toBeInTheDocument();
|
|
157
|
+
expect(
|
|
158
|
+
document.querySelector(
|
|
159
|
+
'[data-ouia-component-id="task-details-tab-locks"]'
|
|
160
|
+
)
|
|
161
|
+
).toBeInTheDocument();
|
|
162
|
+
expect(
|
|
163
|
+
document.querySelector('[data-ouia-component-id="task-details-tab-raw"]')
|
|
164
|
+
).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('shows failed step errors on the default execution details tab', () => {
|
|
168
|
+
renderTaskDetails({
|
|
169
|
+
...fixtureFailedExecutionDetail,
|
|
170
|
+
executionPlan: fixtureFailedExecutionDetail.executionPlan,
|
|
171
|
+
failedSteps: fixtureFailedExecutionDetail.failedSteps,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(
|
|
175
|
+
screen.getByRole('tab', {
|
|
176
|
+
name: /action actions::katello::eventqueue::monitor is already active/i,
|
|
177
|
+
})
|
|
178
|
+
).toBeInTheDocument();
|
|
179
|
+
expect(screen.getByLabelText(/failed task errors/i)).toBeInTheDocument();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('starts task reload on mount and stops on unmount', () => {
|
|
183
|
+
const taskReloadStart = jest.fn();
|
|
184
|
+
const taskReloadStop = jest.fn();
|
|
185
|
+
|
|
186
|
+
const { unmount } = renderTaskDetails({
|
|
187
|
+
...taskDetailsWithExecutionTabDefaults,
|
|
188
|
+
taskReloadStart,
|
|
189
|
+
taskReloadStop,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(taskReloadStart).toHaveBeenCalledWith(
|
|
193
|
+
'a15dd820-32f1-4ced-9ab7-c0fab8234c47'
|
|
194
|
+
);
|
|
195
|
+
unmount();
|
|
196
|
+
expect(taskReloadStop).toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('shows execution details panel on the default tab when loaded', () => {
|
|
200
|
+
renderTaskDetails({ ...taskDetailsWithExecutionTabDefaults });
|
|
201
|
+
|
|
202
|
+
expect(
|
|
203
|
+
screen.getByRole('heading', { name: /^no errors found$/i })
|
|
204
|
+
).toBeInTheDocument();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('disables tabs and hides execution panel while loading', () => {
|
|
208
|
+
renderTaskDetails({
|
|
209
|
+
...taskDetailsWithExecutionTabDefaults,
|
|
210
|
+
isLoading: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(
|
|
214
|
+
screen.queryByRole('heading', { name: /^no errors found$/i })
|
|
215
|
+
).not.toBeInTheDocument();
|
|
216
|
+
expect(
|
|
217
|
+
screen.getByRole('tab', { name: /execution details/i })
|
|
218
|
+
).toBeDisabled();
|
|
219
|
+
expect(screen.getByRole('tab', { name: /dependencies/i })).toBeDisabled();
|
|
220
|
+
expect(screen.getByRole('tab', { name: /locks/i })).toBeDisabled();
|
|
221
|
+
expect(screen.getByRole('tab', { name: /raw/i })).toBeDisabled();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('renders troubleshooting help in overview after expanding details', async () => {
|
|
225
|
+
renderTaskDetails({ ...fixtureWithOverviewMessages });
|
|
226
|
+
|
|
227
|
+
await userEvent.click(
|
|
228
|
+
screen.getByRole('button', { name: /show more details/i })
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect(screen.getByText('Troubleshooting')).toBeInTheDocument();
|
|
232
|
+
expect(screen.getByText('See logs')).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('renders raw output when the Raw tab is selected', async () => {
|
|
236
|
+
renderTaskDetails({ ...fixtureWithOverviewMessages });
|
|
237
|
+
|
|
238
|
+
await userEvent.click(screen.getByRole('tab', { name: /raw/i }));
|
|
239
|
+
|
|
240
|
+
expect(screen.getByText(/raw output/i)).toBeInTheDocument();
|
|
241
|
+
expect(screen.getByText(/partial output/i)).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('renders dependency tables when the Dependencies tab is selected', async () => {
|
|
245
|
+
renderTaskDetails({ ...fixtureWithDependencies });
|
|
246
|
+
|
|
247
|
+
await userEvent.click(screen.getByRole('tab', { name: /dependencies/i }));
|
|
248
|
+
|
|
249
|
+
expect(
|
|
250
|
+
screen.getByRole('link', { name: 'Foo Bar Action' })
|
|
251
|
+
).toBeInTheDocument();
|
|
252
|
+
expect(
|
|
253
|
+
screen.getByRole('link', { name: 'Test Action' })
|
|
254
|
+
).toBeInTheDocument();
|
|
255
|
+
});
|
|
135
256
|
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import {
|
|
4
|
+
Flex,
|
|
5
|
+
FlexItem,
|
|
6
|
+
Split,
|
|
7
|
+
SplitItem,
|
|
8
|
+
Title,
|
|
9
|
+
} from '@patternfly/react-core';
|
|
10
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
11
|
+
import { STATUS } from 'foremanReact/constants';
|
|
12
|
+
import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
|
|
13
|
+
import { TaskButtons } from '../../Components/TaskDetails/Components/TaskButtons';
|
|
14
|
+
import { VIEW_FOREMAN_TASKS } from '../../Components/TaskDetails/TaskDetailsConstants';
|
|
15
|
+
import {
|
|
16
|
+
ForceUnlockConfirmationModal,
|
|
17
|
+
UnlockConfirmationModal,
|
|
18
|
+
} from '../../Components/common/ClickConfirmation';
|
|
19
|
+
import { taskResultIconEl } from '../../Components/common/taskResultIcon';
|
|
20
|
+
import './TaskDetailsHeader.scss';
|
|
21
|
+
|
|
22
|
+
const TitleComponent = ({ action, state, result }) => (
|
|
23
|
+
<Flex
|
|
24
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
25
|
+
spaceItems={{ default: 'spaceItemsXs' }}
|
|
26
|
+
>
|
|
27
|
+
<FlexItem>
|
|
28
|
+
<Title
|
|
29
|
+
headingLevel="h1"
|
|
30
|
+
className="foreman-tasks-task-details-header__title"
|
|
31
|
+
ouiaId="task-overview-title"
|
|
32
|
+
>
|
|
33
|
+
{action || __('Task Details')}
|
|
34
|
+
</Title>
|
|
35
|
+
</FlexItem>
|
|
36
|
+
<FlexItem>{taskResultIconEl(state, result)}</FlexItem>
|
|
37
|
+
</Flex>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
TitleComponent.propTypes = {
|
|
41
|
+
action: PropTypes.string,
|
|
42
|
+
state: PropTypes.string,
|
|
43
|
+
result: PropTypes.string,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
TitleComponent.defaultProps = {
|
|
47
|
+
action: '',
|
|
48
|
+
state: '',
|
|
49
|
+
result: '',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const TaskDetailsHeader = props => {
|
|
53
|
+
const {
|
|
54
|
+
taskReload,
|
|
55
|
+
id,
|
|
56
|
+
forceCancelTaskRequest,
|
|
57
|
+
unlockTaskRequest,
|
|
58
|
+
action,
|
|
59
|
+
taskReloadStart,
|
|
60
|
+
taskReloadStop,
|
|
61
|
+
executionPlan,
|
|
62
|
+
apiStatus,
|
|
63
|
+
} = props;
|
|
64
|
+
const hasViewPermission = usePermissions([VIEW_FOREMAN_TASKS]);
|
|
65
|
+
const showActions = hasViewPermission && apiStatus !== STATUS.ERROR;
|
|
66
|
+
const resumable = executionPlan ? executionPlan.state === 'paused' : false;
|
|
67
|
+
const cancellable = executionPlan ? executionPlan.cancellable : false;
|
|
68
|
+
|
|
69
|
+
const forceUnlock = () => {
|
|
70
|
+
if (!taskReload) {
|
|
71
|
+
taskReloadStart(id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
forceCancelTaskRequest(id, action);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const unlock = () => {
|
|
78
|
+
if (!taskReload) {
|
|
79
|
+
taskReloadStart(id);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
unlockTaskRequest(id, action);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const taskProgressToggle = () => {
|
|
86
|
+
if (taskReload) {
|
|
87
|
+
taskReloadStop();
|
|
88
|
+
} else {
|
|
89
|
+
taskReloadStart(id);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const [unlockModalOpen, setUnlockModalOpen] = useState(false);
|
|
94
|
+
const [forceUnlockModalOpen, setForceUnlockModalOpen] = useState(false);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<React.Fragment>
|
|
98
|
+
{showActions && (
|
|
99
|
+
<>
|
|
100
|
+
<UnlockConfirmationModal
|
|
101
|
+
onClick={unlock}
|
|
102
|
+
isOpen={unlockModalOpen}
|
|
103
|
+
setModalClosed={() => setUnlockModalOpen(false)}
|
|
104
|
+
/>
|
|
105
|
+
<ForceUnlockConfirmationModal
|
|
106
|
+
onClick={forceUnlock}
|
|
107
|
+
isOpen={forceUnlockModalOpen}
|
|
108
|
+
setModalClosed={() => setForceUnlockModalOpen(false)}
|
|
109
|
+
/>
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
<Split
|
|
113
|
+
className="foreman-tasks-task-details-header"
|
|
114
|
+
hasGutter
|
|
115
|
+
isWrappable
|
|
116
|
+
data-ouia-component-id="foreman-tasks-task-details-title-row"
|
|
117
|
+
>
|
|
118
|
+
<SplitItem isFilled>
|
|
119
|
+
<TitleComponent
|
|
120
|
+
action={props.action}
|
|
121
|
+
state={props.state}
|
|
122
|
+
result={props.result}
|
|
123
|
+
/>
|
|
124
|
+
</SplitItem>
|
|
125
|
+
{showActions && (
|
|
126
|
+
<SplitItem>
|
|
127
|
+
<TaskButtons
|
|
128
|
+
{...props}
|
|
129
|
+
taskReloadStart={taskReloadStart}
|
|
130
|
+
taskProgressToggle={taskProgressToggle}
|
|
131
|
+
setUnlockModalOpen={setUnlockModalOpen}
|
|
132
|
+
setForceUnlockModalOpen={setForceUnlockModalOpen}
|
|
133
|
+
resumable={resumable}
|
|
134
|
+
cancellable={cancellable}
|
|
135
|
+
/>
|
|
136
|
+
</SplitItem>
|
|
137
|
+
)}
|
|
138
|
+
</Split>
|
|
139
|
+
</React.Fragment>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
TaskDetailsHeader.propTypes = {
|
|
144
|
+
taskReload: PropTypes.bool,
|
|
145
|
+
id: PropTypes.string,
|
|
146
|
+
forceCancelTaskRequest: PropTypes.func,
|
|
147
|
+
unlockTaskRequest: PropTypes.func,
|
|
148
|
+
action: PropTypes.string,
|
|
149
|
+
state: PropTypes.string,
|
|
150
|
+
result: PropTypes.string,
|
|
151
|
+
taskReloadStart: PropTypes.func,
|
|
152
|
+
taskReloadStop: PropTypes.func,
|
|
153
|
+
executionPlan: PropTypes.shape({
|
|
154
|
+
state: PropTypes.string,
|
|
155
|
+
cancellable: PropTypes.bool,
|
|
156
|
+
}),
|
|
157
|
+
apiStatus: PropTypes.oneOf(Object.keys(STATUS)),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
TaskDetailsHeader.defaultProps = {
|
|
161
|
+
taskReload: false,
|
|
162
|
+
id: '',
|
|
163
|
+
forceCancelTaskRequest: () => null,
|
|
164
|
+
unlockTaskRequest: () => null,
|
|
165
|
+
action: '',
|
|
166
|
+
state: '',
|
|
167
|
+
result: '',
|
|
168
|
+
taskReloadStart: () => null,
|
|
169
|
+
taskReloadStop: () => null,
|
|
170
|
+
executionPlan: {},
|
|
171
|
+
apiStatus: STATUS.RESOLVED,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export default TaskDetailsHeader;
|
|
@@ -1,48 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { useSelector } from 'react-redux';
|
|
4
|
-
import { Flex, FlexItem, TextContent, Text } from '@patternfly/react-core';
|
|
5
3
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
|
6
4
|
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
|
7
|
-
import TaskDetails from '../../Components/TaskDetails';
|
|
8
|
-
import
|
|
9
|
-
selectAction,
|
|
10
|
-
selectResult,
|
|
11
|
-
selectState,
|
|
12
|
-
} from '../../Components/TaskDetails/TaskDetailsSelectors';
|
|
13
|
-
import { taskResultIconEl } from '../../Components/common/taskResultIcon';
|
|
5
|
+
import TaskDetails from '../../Components/TaskDetails/TaskDetails';
|
|
6
|
+
import TaskDetailsHeader from './TaskDetailsHeader';
|
|
14
7
|
|
|
15
8
|
const TaskDetailsPage = props => {
|
|
16
|
-
const {
|
|
17
|
-
const
|
|
18
|
-
const taskState = useSelector(selectState);
|
|
19
|
-
const taskResult = useSelector(selectResult);
|
|
9
|
+
const { match, action } = props;
|
|
10
|
+
const { id } = match.params;
|
|
20
11
|
const headerText = action
|
|
21
12
|
? sprintf(__('Details of %s task'), action)
|
|
22
13
|
: __('Task Details');
|
|
23
14
|
|
|
24
|
-
const header = (
|
|
25
|
-
<Flex
|
|
26
|
-
component="span"
|
|
27
|
-
data-ouia-component-id="foreman-tasks-task-details-title-row"
|
|
28
|
-
display={{ default: 'inlineFlex' }}
|
|
29
|
-
alignItems={{ default: 'alignItemsCenter' }}
|
|
30
|
-
gap={{ default: 'gapSm' }}
|
|
31
|
-
>
|
|
32
|
-
<TextContent>
|
|
33
|
-
<Text ouiaId="breadcrumb_title" component="h1">
|
|
34
|
-
{headerText}
|
|
35
|
-
</Text>
|
|
36
|
-
</TextContent>
|
|
37
|
-
<FlexItem component="span">
|
|
38
|
-
{taskResultIconEl(taskState, taskResult)}
|
|
39
|
-
</FlexItem>
|
|
40
|
-
</Flex>
|
|
41
|
-
);
|
|
42
|
-
|
|
43
15
|
return (
|
|
44
16
|
<PageLayout
|
|
45
|
-
customHeader={
|
|
17
|
+
customHeader={<TaskDetailsHeader {...props} />}
|
|
46
18
|
header={headerText}
|
|
47
19
|
searchable={false}
|
|
48
20
|
breadcrumbOptions={{
|
|
@@ -69,6 +41,11 @@ TaskDetailsPage.propTypes = {
|
|
|
69
41
|
id: PropTypes.string.isRequired,
|
|
70
42
|
}).isRequired,
|
|
71
43
|
}).isRequired,
|
|
44
|
+
action: PropTypes.string,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
TaskDetailsPage.defaultProps = {
|
|
48
|
+
action: '',
|
|
72
49
|
};
|
|
73
50
|
|
|
74
51
|
export default TaskDetailsPage;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { STATUS } from 'foremanReact/constants';
|
|
5
|
+
|
|
6
|
+
import TaskDetailsHeader from '../TaskDetailsHeader';
|
|
7
|
+
import { VIEW_FOREMAN_TASKS } from '../../../Components/TaskDetails/TaskDetailsConstants';
|
|
8
|
+
|
|
9
|
+
const mockUseForemanPermissions = jest.fn(
|
|
10
|
+
() => new Set(['view_foreman_tasks'])
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
|
|
14
|
+
...jest.requireActual('foremanReact/Root/Context/ForemanContext'),
|
|
15
|
+
useForemanPermissions: (...args) => mockUseForemanPermissions(...args),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const baseHeaderProps = {
|
|
19
|
+
id: 'test-id',
|
|
20
|
+
action: 'Refresh hosts',
|
|
21
|
+
state: 'running',
|
|
22
|
+
result: 'pending',
|
|
23
|
+
taskReload: false,
|
|
24
|
+
taskReloadStart: jest.fn(),
|
|
25
|
+
taskReloadStop: jest.fn(),
|
|
26
|
+
forceCancelTaskRequest: jest.fn(),
|
|
27
|
+
unlockTaskRequest: jest.fn(),
|
|
28
|
+
cancelTaskRequest: jest.fn(),
|
|
29
|
+
resumeTaskRequest: jest.fn(),
|
|
30
|
+
canEdit: true,
|
|
31
|
+
dynflowEnableConsole: true,
|
|
32
|
+
externalId: 'ext-99',
|
|
33
|
+
executionPlan: {},
|
|
34
|
+
apiStatus: STATUS.RESOLVED,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const expectHeaderActionsVisible = () => {
|
|
38
|
+
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
|
39
|
+
expect(
|
|
40
|
+
screen.getByRole('link', { name: /dynflow console/i })
|
|
41
|
+
).toBeInTheDocument();
|
|
42
|
+
expect(
|
|
43
|
+
screen.getByRole('button', { name: /^task actions$/i })
|
|
44
|
+
).toBeInTheDocument();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const expectHeaderActionsHidden = () => {
|
|
48
|
+
expect(
|
|
49
|
+
screen.queryByRole('button', { name: /cancel/i })
|
|
50
|
+
).not.toBeInTheDocument();
|
|
51
|
+
expect(
|
|
52
|
+
screen.queryByRole('link', { name: /dynflow console/i })
|
|
53
|
+
).not.toBeInTheDocument();
|
|
54
|
+
expect(
|
|
55
|
+
screen.queryByRole('button', { name: /^task actions$/i })
|
|
56
|
+
).not.toBeInTheDocument();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
describe('TaskDetailsHeader', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
mockUseForemanPermissions.mockImplementation(
|
|
62
|
+
() => new Set(['view_foreman_tasks'])
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
jest.clearAllMocks();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders action heading and exposes cancel, dynflow link, and task actions toggle', () => {
|
|
71
|
+
render(<TaskDetailsHeader {...baseHeaderProps} />);
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
screen.getByRole('heading', { level: 1, name: 'Refresh hosts' })
|
|
75
|
+
).toBeInTheDocument();
|
|
76
|
+
expectHeaderActionsVisible();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('shows running status icon next to the title when task state is running', () => {
|
|
80
|
+
render(
|
|
81
|
+
<TaskDetailsHeader
|
|
82
|
+
{...baseHeaderProps}
|
|
83
|
+
action="Running job"
|
|
84
|
+
state="running"
|
|
85
|
+
result="pending"
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
screen.getByRole('heading', { level: 1, name: 'Running job' })
|
|
91
|
+
).toBeInTheDocument();
|
|
92
|
+
expect(screen.getByTitle('Running')).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('shows error status icon next to the title when task stopped with error', () => {
|
|
96
|
+
render(
|
|
97
|
+
<TaskDetailsHeader
|
|
98
|
+
{...baseHeaderProps}
|
|
99
|
+
action="Failed job"
|
|
100
|
+
state="stopped"
|
|
101
|
+
result="error"
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
screen.getByRole('heading', { level: 1, name: 'Failed job' })
|
|
107
|
+
).toBeInTheDocument();
|
|
108
|
+
expect(screen.getByTitle('Error')).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('uses generic title when action is unset', () => {
|
|
112
|
+
render(<TaskDetailsHeader {...baseHeaderProps} action="" />);
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
screen.getByRole('heading', { level: 1, name: 'Task Details' })
|
|
116
|
+
).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it.each([
|
|
120
|
+
[
|
|
121
|
+
`${VIEW_FOREMAN_TASKS} is absent`,
|
|
122
|
+
() => {
|
|
123
|
+
mockUseForemanPermissions.mockImplementation(() => new Set());
|
|
124
|
+
},
|
|
125
|
+
{},
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
'apiStatus is ERROR',
|
|
129
|
+
() => {},
|
|
130
|
+
{ apiStatus: STATUS.ERROR },
|
|
131
|
+
],
|
|
132
|
+
])('hides task actions when %s', (_label, setupPermissions, propOverrides) => {
|
|
133
|
+
setupPermissions();
|
|
134
|
+
|
|
135
|
+
render(<TaskDetailsHeader {...baseHeaderProps} {...propOverrides} />);
|
|
136
|
+
|
|
137
|
+
expect(
|
|
138
|
+
screen.getByRole('heading', { level: 1, name: 'Refresh hosts' })
|
|
139
|
+
).toBeInTheDocument();
|
|
140
|
+
expectHeaderActionsHidden();
|
|
141
|
+
});
|
|
142
|
+
});
|