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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/foreman_tasks/tasks_controller.rb +0 -5
  3. data/config/routes.rb +3 -2
  4. data/lib/foreman_tasks/engine.rb +2 -2
  5. data/lib/foreman_tasks/version.rb +1 -1
  6. data/test/controllers/tasks_controller_test.rb +0 -9
  7. data/test/foreman_tasks_test_helper.rb +2 -2
  8. data/test/integration/tasks_test.rb +17 -0
  9. data/test/test_plugin_helper.rb +8 -0
  10. data/webpack/ForemanTasks/Components/TaskActions/TaskAction.test.js +212 -42
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/Errors.js +224 -55
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/Errors.scss +41 -0
  13. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +6 -51
  14. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js +1 -6
  15. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Errors.test.js +159 -11
  16. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +0 -2
  17. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +0 -2
  18. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +29 -15
  19. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -5
  20. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -1
  21. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +20 -5
  22. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +1 -1
  23. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +97 -10
  24. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js +81 -0
  25. data/webpack/ForemanTasks/Components/TaskDetails/index.js +6 -4
  26. data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboard.test.js +94 -7
  27. data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardActions.test.js +97 -16
  28. data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardReducer.test.js +112 -46
  29. data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardSelectors.test.js +103 -15
  30. data/webpack/ForemanTasks/Components/TasksTable/TasksTableConstants.js +1 -1
  31. data/webpack/ForemanTasks/Components/common/taskResultIcon.js +53 -0
  32. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +74 -0
  33. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +265 -0
  34. data/webpack/Routes/routes.js +6 -0
  35. data/webpack/Routes/routes.test.js +16 -5
  36. metadata +10 -7
  37. data/app/views/foreman_tasks/tasks/show.html.erb +0 -18
  38. data/webpack/ForemanTasks/Components/TaskActions/__snapshots__/TaskAction.test.js.snap +0 -233
  39. data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboard.test.js.snap +0 -51
  40. data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboardActions.test.js.snap +0 -151
  41. data/webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboardReducer.test.js.snap +0 -275
  42. 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 __, 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
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
- APIerror,
31
+ apiStatus,
32
+ apiErrorMessage,
33
+ apiErrorCode,
30
34
  ...props
31
35
  }) => {
32
36
  const id = getTaskID();
33
- const { taskReload, status, isLoading, result } = props;
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 (status === STATUS.ERROR) {
56
+ if (apiStatus === STATUS.ERROR || !hasViewPermission) {
52
57
  return (
53
- <MessageBox
54
- key="task-details-error"
55
- icontype="error-circle-o"
56
- msg={sprintf(__('Could not receive data: %s'), APIerror?.message)}
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 well">
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
- status: PropTypes.oneOf(Object.keys(STATUS)),
163
- APIerror: PropTypes.object,
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
- APIerror: null,
179
- status: STATUS.PENDING,
193
+ apiErrorMessage: '',
180
194
  links: [],
181
195
  dependsOn: [],
182
196
  blocks: [],
@@ -1,10 +1,6 @@
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 {
@@ -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';
@@ -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 selectStatus = state => selectTaskDetailsResponse(state).status;
105
+ export const selectAPIStatus = state =>
106
+ selectAPIStatusByKey(state, FOREMAN_TASK_DETAILS);
105
107
 
106
108
  export const selectAPIError = state =>
107
- selectTaskDetailsResponse(state)?.APIerror;
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
- !!selectAPIByKey(state, FOREMAN_TASK_DETAILS).response &&
111
- selectStatus(state) === STATUS.PENDING;
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 || [];
@@ -4,5 +4,5 @@ export const minProps = {
4
4
  taskReloadStop: jest.fn(),
5
5
  taskReloadStart: jest.fn(),
6
6
  taskProgressToggle: jest.fn(),
7
- status: 'RESOLVED',
7
+ apiStatus: 'RESOLVED',
8
8
  };
@@ -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
- it('shows error message when status is ERROR', () => {
15
- render(
16
- <TaskDetails
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(/could not receive data: some-error/i)
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 } = render(<TaskDetails {...minProps} isLoading />);
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
- render(<TaskDetails {...minProps} />);
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
- selectStatus,
37
- selectAPIError,
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
- status: selectStatus(state),
74
- APIerror: selectAPIError(state),
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 { testComponentSnapshotsWithFixtures } from '@theforeman/test';
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
- const fixtures = {
6
- 'render without Props': { history: {} },
7
- /** fixtures, props for the component */
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
- describe('rendering', () =>
12
- testComponentSnapshotsWithFixtures(TasksDashboard, fixtures));
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 { timeToHoursNumber } from '../TasksDashboardHelper';
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
- const fixtures = {
24
- 'should initialize-dashboard': () =>
25
- initializeDashboard({ time: 'some-time', query: 'some-query' }),
26
- 'should update-time': () => updateTime('some-time'),
27
- 'should update-query': () => updateQuery('some-query'),
28
- 'should fetch-tasks-summary and success': () =>
29
- fetchTasksSummary(correctTime),
30
- 'should fetch-tasks-summary for subtasks and success': () =>
31
- fetchTasksSummary(correctTime, parentTaskID),
32
- 'should fetch-tasks-summary and fail': () => fetchTasksSummary(wrongTime),
33
- };
34
-
35
- describe('TasksDashboard - Actions', () =>
36
- testActionSnapshotWithFixtures(fixtures));
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
+ });