foreman-tasks 12.2.3 → 13.0.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/controllers/foreman_tasks/tasks_controller.rb +0 -5
- data/config/routes.rb +3 -2
- data/lib/foreman_tasks/engine.rb +2 -2
- data/lib/foreman_tasks/version.rb +1 -1
- data/test/controllers/tasks_controller_test.rb +0 -9
- data/test/foreman_tasks_test_helper.rb +2 -2
- data/test/integration/tasks_test.rb +17 -0
- data/test/test_plugin_helper.rb +8 -0
- data/webpack/ForemanTasks/Components/TaskActions/TaskAction.test.js +212 -42
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Errors.js +224 -55
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Errors.scss +41 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +6 -51
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js +1 -6
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Errors.test.js +159 -11
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +0 -2
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +0 -2
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +29 -15
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -5
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +20 -5
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +1 -1
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +97 -10
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js +81 -0
- data/webpack/ForemanTasks/Components/TaskDetails/index.js +6 -4
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboard.test.js +94 -7
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardActions.test.js +97 -16
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardReducer.test.js +112 -46
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardSelectors.test.js +103 -15
- data/webpack/ForemanTasks/Components/TasksTable/TasksTableConstants.js +1 -1
- data/webpack/ForemanTasks/Components/common/taskResultIcon.js +53 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +74 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +265 -0
- data/webpack/Routes/routes.js +6 -0
- data/webpack/Routes/routes.test.js +16 -5
- metadata +10 -7
- data/app/views/foreman_tasks/tasks/show.html.erb +0 -18
- data/webpack/ForemanTasks/Components/TaskActions/__snapshots__/TaskAction.test.js.snap +0 -233
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboard.test.js.snap +0 -51
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboardActions.test.js.snap +0 -151
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboardReducer.test.js.snap +0 -275
- data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboardSelectors.test.js.snap +0 -119
|
@@ -1,15 +1,17 @@
|
|
|
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 __
|
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
5
|
import { STATUS } from 'foremanReact/constants';
|
|
6
|
-
import
|
|
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
9
|
import RunningSteps from './Components/RunningSteps';
|
|
9
10
|
import Errors from './Components/Errors';
|
|
10
11
|
import Locks from './Components/Locks';
|
|
11
12
|
import Raw from './Components/Raw';
|
|
12
13
|
import Dependencies from './Components/Dependencies';
|
|
14
|
+
import { TASKS_PATH, VIEW_FOREMAN_TASKS } from './TaskDetailsConstants';
|
|
13
15
|
import { getTaskID } from './TasksDetailsHelper';
|
|
14
16
|
import { TaskSkeleton } from './Components/TaskSkeleton';
|
|
15
17
|
|
|
@@ -26,12 +28,15 @@ const TaskDetails = ({
|
|
|
26
28
|
cancelStep,
|
|
27
29
|
taskReloadStart,
|
|
28
30
|
taskReloadStop,
|
|
29
|
-
|
|
31
|
+
apiStatus,
|
|
32
|
+
apiErrorMessage,
|
|
33
|
+
apiErrorCode,
|
|
30
34
|
...props
|
|
31
35
|
}) => {
|
|
32
36
|
const id = getTaskID();
|
|
33
|
-
const { taskReload,
|
|
37
|
+
const { taskReload, isLoading, result } = props;
|
|
34
38
|
const [activeTabKey, setActiveTabKey] = useState(1);
|
|
39
|
+
const hasViewPermission = usePermissions([VIEW_FOREMAN_TASKS]);
|
|
35
40
|
|
|
36
41
|
useEffect(() => {
|
|
37
42
|
taskReloadStart(id);
|
|
@@ -48,15 +53,25 @@ const TaskDetails = ({
|
|
|
48
53
|
}
|
|
49
54
|
};
|
|
50
55
|
|
|
51
|
-
if (
|
|
56
|
+
if (apiStatus === STATUS.ERROR || !hasViewPermission) {
|
|
52
57
|
return (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
<ResourceLoadFailedEmptyState
|
|
59
|
+
resourceLabel={__('task')}
|
|
60
|
+
resourceId={id}
|
|
61
|
+
httpStatus={apiErrorCode}
|
|
62
|
+
errorMessage={apiErrorMessage}
|
|
63
|
+
viewPermissions={['view_foreman_tasks']}
|
|
64
|
+
requiredPermissions={['view_foreman_tasks']}
|
|
65
|
+
ouiaIdPrefix="task-details-empty-state"
|
|
66
|
+
primaryAction={{
|
|
67
|
+
label: __('Back to tasks'),
|
|
68
|
+
url: TASKS_PATH,
|
|
69
|
+
ouiaId: 'task-details-empty-state-tasks-list',
|
|
70
|
+
}}
|
|
57
71
|
/>
|
|
58
72
|
);
|
|
59
73
|
}
|
|
74
|
+
|
|
60
75
|
const resumable = executionPlan ? executionPlan.state === 'paused' : false;
|
|
61
76
|
const cancellable = executionPlan ? executionPlan.cancellable : false;
|
|
62
77
|
const lockRecords = locks.concat(links);
|
|
@@ -66,13 +81,12 @@ const TaskDetails = ({
|
|
|
66
81
|
cancellable,
|
|
67
82
|
resumable,
|
|
68
83
|
id,
|
|
69
|
-
status,
|
|
70
84
|
taskProgressToggle,
|
|
71
85
|
taskReloadStart,
|
|
72
86
|
};
|
|
73
87
|
|
|
74
88
|
return (
|
|
75
|
-
<div className="task-details-react
|
|
89
|
+
<div className="task-details-react">
|
|
76
90
|
<Tabs
|
|
77
91
|
id="task-details-tabs"
|
|
78
92
|
ouiaId="task-details-tabs"
|
|
@@ -159,8 +173,9 @@ TaskDetails.propTypes = {
|
|
|
159
173
|
runningSteps: PropTypes.array,
|
|
160
174
|
cancelStep: PropTypes.func.isRequired,
|
|
161
175
|
taskReload: PropTypes.bool.isRequired,
|
|
162
|
-
|
|
163
|
-
|
|
176
|
+
apiStatus: PropTypes.oneOf(Object.keys(STATUS)),
|
|
177
|
+
apiErrorMessage: PropTypes.string,
|
|
178
|
+
apiErrorCode: PropTypes.number,
|
|
164
179
|
taskReloadStop: PropTypes.func.isRequired,
|
|
165
180
|
taskReloadStart: PropTypes.func.isRequired,
|
|
166
181
|
links: PropTypes.array,
|
|
@@ -175,8 +190,7 @@ TaskDetails.propTypes = {
|
|
|
175
190
|
TaskDetails.defaultProps = {
|
|
176
191
|
label: '',
|
|
177
192
|
runningSteps: [],
|
|
178
|
-
|
|
179
|
-
status: STATUS.PENDING,
|
|
193
|
+
apiErrorMessage: '',
|
|
180
194
|
links: [],
|
|
181
195
|
dependsOn: [],
|
|
182
196
|
blocks: [],
|
|
@@ -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
|
-
|
|
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';
|
|
@@ -101,14 +102,28 @@ export const selectDynflowEnableConsole = state =>
|
|
|
101
102
|
export const selectCanEdit = state =>
|
|
102
103
|
selectTaskDetailsResponse(state).can_edit || false;
|
|
103
104
|
|
|
104
|
-
export const
|
|
105
|
+
export const selectAPIStatus = state =>
|
|
106
|
+
selectAPIStatusByKey(state, FOREMAN_TASK_DETAILS);
|
|
105
107
|
|
|
106
108
|
export const selectAPIError = state =>
|
|
107
|
-
|
|
109
|
+
selectAPIErrorByKey(state, FOREMAN_TASK_DETAILS);
|
|
110
|
+
|
|
111
|
+
export const selectAPIErrorMessage = state => {
|
|
112
|
+
const apiError = selectAPIError(state);
|
|
113
|
+
|
|
114
|
+
if (apiError?.response?.data?.error) {
|
|
115
|
+
return apiError.response.data.error.message;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return apiError?.message;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const selectAPIErrorCode = state =>
|
|
122
|
+
selectAPIError(state)?.response?.status;
|
|
108
123
|
|
|
109
124
|
export const selectIsLoading = state =>
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
!Object.keys(selectTaskDetailsResponse(state) ?? {}).length &&
|
|
126
|
+
selectAPIStatus(state) === STATUS.PENDING;
|
|
112
127
|
|
|
113
128
|
export const selectDependsOn = state =>
|
|
114
129
|
selectTaskDetailsResponse(state).depends_on || [];
|
|
@@ -1,38 +1,125 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
|
+
import { configureStore } from '@reduxjs/toolkit';
|
|
5
|
+
import { Provider } from 'react-redux';
|
|
6
|
+
import { createMemoryHistory } from 'history';
|
|
7
|
+
import { Router } from 'react-router-dom';
|
|
4
8
|
|
|
5
9
|
import TaskDetails from '../TaskDetails';
|
|
6
10
|
import { minProps } from './TaskDetails.fixtures';
|
|
11
|
+
import { VIEW_FOREMAN_TASKS } from '../TaskDetailsConstants';
|
|
12
|
+
|
|
13
|
+
const mockUseForemanPermissions = jest.fn(
|
|
14
|
+
() => new Set(['view_foreman_tasks'])
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
|
|
18
|
+
...jest.requireActual('foremanReact/Root/Context/ForemanContext'),
|
|
19
|
+
useForemanPermissions: (...args) => mockUseForemanPermissions(...args),
|
|
20
|
+
}));
|
|
7
21
|
|
|
8
22
|
delete window.location;
|
|
9
23
|
window.location = new URL(
|
|
10
24
|
'https://foreman.com/foreman_tasks/tasks/a15dd820-32f1-4ced-9ab7-c0fab8234c47/'
|
|
11
25
|
);
|
|
12
26
|
|
|
27
|
+
const store = configureStore({ reducer: state => state || {} });
|
|
28
|
+
|
|
29
|
+
function renderTaskDetails(props = {}) {
|
|
30
|
+
const history = createMemoryHistory();
|
|
31
|
+
|
|
32
|
+
return render(
|
|
33
|
+
<Router history={history}>
|
|
34
|
+
<Provider store={store}>
|
|
35
|
+
<TaskDetails {...minProps} {...props} />
|
|
36
|
+
</Provider>
|
|
37
|
+
</Router>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
13
41
|
describe('TaskDetails', () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{...minProps}
|
|
18
|
-
status="ERROR"
|
|
19
|
-
APIerror={{ message: 'some-error' }}
|
|
20
|
-
/>
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
mockUseForemanPermissions.mockImplementation(
|
|
44
|
+
() => new Set(['view_foreman_tasks'])
|
|
21
45
|
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it(`shows ResourceLoadFailedEmptyState when ${VIEW_FOREMAN_TASKS} is absent`, () => {
|
|
49
|
+
mockUseForemanPermissions.mockImplementation(() => new Set());
|
|
50
|
+
|
|
51
|
+
renderTaskDetails();
|
|
52
|
+
|
|
53
|
+
expect(
|
|
54
|
+
screen.getByRole('heading', { name: /permission denied/i })
|
|
55
|
+
).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText(VIEW_FOREMAN_TASKS)).toBeInTheDocument();
|
|
57
|
+
expect(
|
|
58
|
+
screen.getByRole('button', { name: /back to tasks/i })
|
|
59
|
+
).toBeInTheDocument();
|
|
60
|
+
expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('shows ResourceLoadFailedEmptyState when apiStatus is ERROR', () => {
|
|
64
|
+
renderTaskDetails({
|
|
65
|
+
apiStatus: 'ERROR',
|
|
66
|
+
apiErrorMessage: 'some-error',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(
|
|
70
|
+
screen.getByRole('heading', { name: /unable to load task/i })
|
|
71
|
+
).toBeInTheDocument();
|
|
72
|
+
expect(
|
|
73
|
+
screen.getByText('Server returned: some-error')
|
|
74
|
+
).toBeInTheDocument();
|
|
75
|
+
expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('shows permission denied when apiErrorCode is 403', () => {
|
|
79
|
+
renderTaskDetails({
|
|
80
|
+
apiStatus: 'ERROR',
|
|
81
|
+
apiErrorCode: 403,
|
|
82
|
+
apiErrorMessage: 'Forbidden',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
screen.getByRole('heading', { name: /permission denied/i })
|
|
87
|
+
).toBeInTheDocument();
|
|
88
|
+
expect(screen.getByText('view_foreman_tasks')).toBeInTheDocument();
|
|
89
|
+
expect(
|
|
90
|
+
screen.getByText('Server returned: Forbidden')
|
|
91
|
+
).toBeInTheDocument();
|
|
92
|
+
expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('shows not found messaging when apiErrorCode is 404', () => {
|
|
96
|
+
renderTaskDetails({
|
|
97
|
+
apiStatus: 'ERROR',
|
|
98
|
+
apiErrorCode: 404,
|
|
99
|
+
apiErrorMessage: 'Task missing',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(
|
|
103
|
+
screen.getByRole('heading', { name: /unable to load task/i })
|
|
104
|
+
).toBeInTheDocument();
|
|
105
|
+
expect(
|
|
106
|
+
screen.getByText(/could not be found/i)
|
|
107
|
+
).toBeInTheDocument();
|
|
22
108
|
expect(
|
|
23
|
-
screen.getByText(
|
|
109
|
+
screen.getByText('Server returned: Task missing')
|
|
24
110
|
).toBeInTheDocument();
|
|
111
|
+
expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
|
|
25
112
|
});
|
|
26
113
|
|
|
27
114
|
it('shows skeleton while loading on the Task tab', () => {
|
|
28
|
-
const { container } =
|
|
115
|
+
const { container } = renderTaskDetails({ isLoading: true });
|
|
29
116
|
expect(
|
|
30
117
|
container.querySelector('.react-loading-skeleton')
|
|
31
118
|
).toBeInTheDocument();
|
|
32
119
|
});
|
|
33
120
|
|
|
34
121
|
it('renders six tabs with expected labels', () => {
|
|
35
|
-
|
|
122
|
+
renderTaskDetails();
|
|
36
123
|
expect(document.getElementById('task-details-tabs')).toBeInTheDocument();
|
|
37
124
|
expect(screen.getByRole('tab', { name: /^task$/i })).toBeInTheDocument();
|
|
38
125
|
expect(
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { STATUS } from 'foremanReact/constants';
|
|
2
|
+
|
|
3
|
+
import { FOREMAN_TASK_DETAILS } from '../TaskDetailsConstants';
|
|
4
|
+
import {
|
|
5
|
+
selectAPIError,
|
|
6
|
+
selectAPIErrorCode,
|
|
7
|
+
selectAPIErrorMessage,
|
|
8
|
+
} from '../TaskDetailsSelectors';
|
|
9
|
+
|
|
10
|
+
const axiosErrorWithApiBody = {
|
|
11
|
+
message: 'Request failed with status code 404',
|
|
12
|
+
response: {
|
|
13
|
+
status: 404,
|
|
14
|
+
data: {
|
|
15
|
+
error: {
|
|
16
|
+
message: 'Task not found',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const plainError = {
|
|
23
|
+
message: 'Network Error',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const forbiddenError = {
|
|
27
|
+
message: 'Request failed with status code 403',
|
|
28
|
+
response: {
|
|
29
|
+
status: 403,
|
|
30
|
+
data: {
|
|
31
|
+
error: {
|
|
32
|
+
message: 'Forbidden',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const buildState = (response, status = STATUS.ERROR) => ({
|
|
39
|
+
API: {
|
|
40
|
+
[FOREMAN_TASK_DETAILS]: {
|
|
41
|
+
payload: {},
|
|
42
|
+
response,
|
|
43
|
+
status,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('TaskDetailsSelectors - API error selectors', () => {
|
|
49
|
+
it('selectAPIError returns null when API status is not ERROR', () => {
|
|
50
|
+
expect(selectAPIError(buildState({}, STATUS.RESOLVED))).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('selectAPIError returns the stored response when API status is ERROR', () => {
|
|
54
|
+
expect(selectAPIError(buildState(plainError))).toEqual(plainError);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('selectAPIErrorMessage prefers the API error body message', () => {
|
|
58
|
+
expect(
|
|
59
|
+
selectAPIErrorMessage(buildState(axiosErrorWithApiBody))
|
|
60
|
+
).toBe('Task not found');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('selectAPIErrorMessage falls back to the top-level error message', () => {
|
|
64
|
+
expect(selectAPIErrorMessage(buildState(plainError))).toBe('Network Error');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('selectAPIErrorMessage returns undefined when there is no API error', () => {
|
|
68
|
+
expect(
|
|
69
|
+
selectAPIErrorMessage(buildState({}, STATUS.RESOLVED))
|
|
70
|
+
).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('selectAPIErrorCode returns the HTTP status from the error response', () => {
|
|
74
|
+
expect(selectAPIErrorCode(buildState(axiosErrorWithApiBody))).toBe(404);
|
|
75
|
+
expect(selectAPIErrorCode(buildState(forbiddenError))).toBe(403);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('selectAPIErrorCode returns undefined when the error response has no status', () => {
|
|
79
|
+
expect(selectAPIErrorCode(buildState(plainError))).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -33,8 +33,9 @@ import {
|
|
|
33
33
|
selectExternalId,
|
|
34
34
|
selectDynflowEnableConsole,
|
|
35
35
|
selectCanEdit,
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
selectAPIStatus,
|
|
37
|
+
selectAPIErrorMessage,
|
|
38
|
+
selectAPIErrorCode,
|
|
38
39
|
selectIsLoading,
|
|
39
40
|
selectDependsOn,
|
|
40
41
|
selectBlocks,
|
|
@@ -70,8 +71,9 @@ const mapStateToProps = state => ({
|
|
|
70
71
|
externalId: selectExternalId(state),
|
|
71
72
|
dynflowEnableConsole: selectDynflowEnableConsole(state),
|
|
72
73
|
canEdit: selectCanEdit(state),
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
apiStatus: selectAPIStatus(state),
|
|
75
|
+
apiErrorMessage: selectAPIErrorMessage(state),
|
|
76
|
+
apiErrorCode: selectAPIErrorCode(state),
|
|
75
77
|
isLoading: selectIsLoading(state),
|
|
76
78
|
dependsOn: selectDependsOn(state),
|
|
77
79
|
blocks: selectBlocks(state),
|
|
@@ -1,13 +1,100 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
2
4
|
|
|
5
|
+
import { TASKS_DASHBOARD_AVAILABLE_TIMES } from '../TasksDashboardConstants';
|
|
6
|
+
import { getQueryFromUrl } from '../TasksDashboardHelper';
|
|
3
7
|
import TasksDashboard from '../TasksDashboard';
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
'
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
+
jest.mock('../TasksDashboardHelper', () => ({
|
|
10
|
+
...jest.requireActual('../TasksDashboardHelper'),
|
|
11
|
+
getQueryFromUrl: jest.fn(() => ({})),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock(
|
|
15
|
+
'../Components/TasksCardsGrid/Components/TasksDonutChart/TasksDonutChart',
|
|
16
|
+
() => {
|
|
17
|
+
const React = require('react');
|
|
18
|
+
const Stub = () => <div data-testid="tasks-donut-chart-stub" />;
|
|
19
|
+
Stub.displayName = 'TasksDonutChart';
|
|
20
|
+
|
|
21
|
+
return Stub;
|
|
22
|
+
}
|
|
23
|
+
);
|
|
9
24
|
|
|
10
25
|
describe('TasksDashboard', () => {
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
const cardIds = [
|
|
27
|
+
'running-tasks-card',
|
|
28
|
+
'paused-tasks-card',
|
|
29
|
+
'stopped-tasks-card',
|
|
30
|
+
'scheduled-tasks-card',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
getQueryFromUrl.mockReturnValue({});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders the dashboard grid with time picker and task cards', () => {
|
|
39
|
+
const { container } = render(<TasksDashboard history={{}} />);
|
|
40
|
+
|
|
41
|
+
expect(container.querySelector('.tasks-dashboard-grid')).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByText('With focus on last')).toBeInTheDocument();
|
|
43
|
+
expect(
|
|
44
|
+
screen.getByRole('button', { name: /24h/i })
|
|
45
|
+
).toBeInTheDocument();
|
|
46
|
+
|
|
47
|
+
cardIds.forEach(id => {
|
|
48
|
+
expect(container.querySelector(`#${id}`)).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('initializes the dashboard and fetches the summary on mount', () => {
|
|
53
|
+
const initializeDashboard = jest.fn();
|
|
54
|
+
const fetchTasksSummary = jest.fn();
|
|
55
|
+
|
|
56
|
+
render(
|
|
57
|
+
<TasksDashboard
|
|
58
|
+
history={{}}
|
|
59
|
+
initializeDashboard={initializeDashboard}
|
|
60
|
+
fetchTasksSummary={fetchTasksSummary}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(initializeDashboard).toHaveBeenCalledWith({
|
|
65
|
+
time: undefined,
|
|
66
|
+
query: {},
|
|
67
|
+
});
|
|
68
|
+
expect(fetchTasksSummary).toHaveBeenCalledWith(
|
|
69
|
+
TASKS_DASHBOARD_AVAILABLE_TIMES.H24,
|
|
70
|
+
''
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('fetches the summary again when time changes', () => {
|
|
75
|
+
const fetchTasksSummary = jest.fn();
|
|
76
|
+
|
|
77
|
+
const { rerender } = render(
|
|
78
|
+
<TasksDashboard
|
|
79
|
+
history={{}}
|
|
80
|
+
fetchTasksSummary={fetchTasksSummary}
|
|
81
|
+
time={TASKS_DASHBOARD_AVAILABLE_TIMES.H24}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
fetchTasksSummary.mockClear();
|
|
86
|
+
|
|
87
|
+
rerender(
|
|
88
|
+
<TasksDashboard
|
|
89
|
+
history={{}}
|
|
90
|
+
fetchTasksSummary={fetchTasksSummary}
|
|
91
|
+
time={TASKS_DASHBOARD_AVAILABLE_TIMES.H12}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(fetchTasksSummary).toHaveBeenCalledWith(
|
|
96
|
+
TASKS_DASHBOARD_AVAILABLE_TIMES.H12,
|
|
97
|
+
''
|
|
98
|
+
);
|
|
99
|
+
});
|
|
13
100
|
});
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { testActionSnapshotWithFixtures } from '@theforeman/test';
|
|
2
1
|
import { API } from 'foremanReact/redux/API';
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
FOREMAN_TASKS_DASHBOARD_INIT,
|
|
4
|
+
FOREMAN_TASKS_DASHBOARD_UPDATE_TIME,
|
|
5
|
+
FOREMAN_TASKS_DASHBOARD_UPDATE_QUERY,
|
|
6
|
+
FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_REQUEST,
|
|
7
|
+
FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_SUCCESS,
|
|
8
|
+
FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_FAILURE,
|
|
9
|
+
} from '../TasksDashboardConstants';
|
|
10
|
+
import { timeToHoursNumber, resolveQuery } from '../TasksDashboardHelper';
|
|
4
11
|
import {
|
|
5
12
|
initializeDashboard,
|
|
6
13
|
updateTime,
|
|
@@ -12,25 +19,99 @@ import {
|
|
|
12
19
|
wrongTime,
|
|
13
20
|
parentTaskID,
|
|
14
21
|
apiGetMock,
|
|
22
|
+
taskSummary,
|
|
23
|
+
subtaskSummary,
|
|
15
24
|
} from './TaskDashboard.fixtures';
|
|
16
25
|
|
|
17
26
|
jest.mock('foremanReact/redux/API');
|
|
18
27
|
jest.mock('../TasksDashboardHelper');
|
|
19
28
|
|
|
20
29
|
timeToHoursNumber.mockImplementation(arg => arg);
|
|
30
|
+
resolveQuery.mockImplementation(() => {});
|
|
21
31
|
API.get.mockImplementation(apiGetMock);
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
describe('TasksDashboard - Actions', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
timeToHoursNumber.mockImplementation(arg => arg);
|
|
37
|
+
resolveQuery.mockImplementation(() => {});
|
|
38
|
+
API.get.mockImplementation(apiGetMock);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should initialize-dashboard', () => {
|
|
42
|
+
expect(
|
|
43
|
+
initializeDashboard({ time: 'some-time', query: 'some-query' })
|
|
44
|
+
).toEqual({
|
|
45
|
+
type: FOREMAN_TASKS_DASHBOARD_INIT,
|
|
46
|
+
payload: { time: 'some-time', query: 'some-query' },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should update-time', () => {
|
|
51
|
+
expect(updateTime('some-time')).toEqual({
|
|
52
|
+
type: FOREMAN_TASKS_DASHBOARD_UPDATE_TIME,
|
|
53
|
+
payload: 'some-time',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should update-query', () => {
|
|
58
|
+
const dispatch = jest.fn();
|
|
59
|
+
const getState = jest.fn();
|
|
60
|
+
const history = { push: jest.fn(), replace: jest.fn() };
|
|
61
|
+
const query = { state: 'running' };
|
|
62
|
+
|
|
63
|
+
updateQuery(query, history)(dispatch, getState);
|
|
64
|
+
|
|
65
|
+
expect(resolveQuery).toHaveBeenCalledWith(query, history);
|
|
66
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
67
|
+
type: FOREMAN_TASKS_DASHBOARD_UPDATE_QUERY,
|
|
68
|
+
payload: query,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should fetch-tasks-summary and success', async () => {
|
|
73
|
+
const dispatch = jest.fn();
|
|
74
|
+
|
|
75
|
+
await fetchTasksSummary(correctTime)(dispatch);
|
|
76
|
+
|
|
77
|
+
expect(dispatch).toHaveBeenCalledTimes(2);
|
|
78
|
+
expect(dispatch.mock.calls[0][0]).toEqual({
|
|
79
|
+
type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_REQUEST,
|
|
80
|
+
});
|
|
81
|
+
expect(dispatch.mock.calls[1][0]).toEqual({
|
|
82
|
+
type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_SUCCESS,
|
|
83
|
+
payload: taskSummary,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should fetch-tasks-summary for subtasks and success', async () => {
|
|
88
|
+
const dispatch = jest.fn();
|
|
89
|
+
|
|
90
|
+
await fetchTasksSummary(correctTime, parentTaskID)(dispatch);
|
|
91
|
+
|
|
92
|
+
expect(dispatch).toHaveBeenCalledTimes(2);
|
|
93
|
+
expect(dispatch.mock.calls[0][0]).toEqual({
|
|
94
|
+
type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_REQUEST,
|
|
95
|
+
});
|
|
96
|
+
expect(dispatch.mock.calls[1][0]).toEqual({
|
|
97
|
+
type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_SUCCESS,
|
|
98
|
+
payload: subtaskSummary,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should fetch-tasks-summary and fail', async () => {
|
|
103
|
+
const dispatch = jest.fn();
|
|
104
|
+
|
|
105
|
+
await fetchTasksSummary(wrongTime)(dispatch);
|
|
106
|
+
|
|
107
|
+
expect(dispatch).toHaveBeenCalledTimes(2);
|
|
108
|
+
expect(dispatch.mock.calls[0][0]).toEqual({
|
|
109
|
+
type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_REQUEST,
|
|
110
|
+
});
|
|
111
|
+
expect(dispatch.mock.calls[1][0].type).toBe(
|
|
112
|
+
FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_FAILURE
|
|
113
|
+
);
|
|
114
|
+
expect(dispatch.mock.calls[1][0].payload).toEqual(expect.any(Error));
|
|
115
|
+
expect(dispatch.mock.calls[1][0].payload.message).toBe('wrong time');
|
|
116
|
+
});
|
|
117
|
+
});
|