foreman_remote_execution 16.0.4 → 16.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +33 -6
  3. data/app/controllers/job_invocations_controller.rb +28 -1
  4. data/app/controllers/template_invocations_controller.rb +1 -1
  5. data/app/helpers/remote_execution_helper.rb +1 -1
  6. data/config/routes.rb +4 -2
  7. data/lib/foreman_remote_execution/engine.rb +9 -256
  8. data/lib/foreman_remote_execution/plugin.rb +240 -0
  9. data/lib/foreman_remote_execution/version.rb +1 -1
  10. data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
  11. data/test/unit/job_invocation_report_template_test.rb +6 -6
  12. data/webpack/JobInvocationDetail/CheckboxesActions.js +327 -0
  13. data/webpack/JobInvocationDetail/{JobInvocationHostTableToolbar.js → DropdownFilter.js} +3 -6
  14. data/webpack/JobInvocationDetail/JobInvocationConstants.js +8 -9
  15. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +18 -0
  16. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +205 -89
  17. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +30 -3
  18. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +23 -28
  19. data/webpack/JobInvocationDetail/OpenAllInvocationsModal.js +118 -0
  20. data/webpack/JobInvocationDetail/TemplateInvocation.js +54 -24
  21. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +8 -10
  22. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +1 -1
  23. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +2 -4
  24. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +204 -0
  25. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +34 -28
  26. data/webpack/JobInvocationDetail/index.js +71 -41
  27. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +26 -7
  28. data/webpack/Routes/routes.js +1 -1
  29. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +1 -1
  30. metadata +8 -6
  31. data/webpack/JobInvocationDetail/OpenAlInvocations.js +0 -111
  32. data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +0 -110
@@ -0,0 +1,118 @@
1
+ import {
2
+ Alert,
3
+ AlertActionCloseButton,
4
+ Button,
5
+ Modal,
6
+ } from '@patternfly/react-core';
7
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
8
+ import PropTypes from 'prop-types';
9
+ import React from 'react';
10
+ import {
11
+ templateInvocationPageUrl,
12
+ MAX_HOSTS_API_SIZE,
13
+ } from './JobInvocationConstants';
14
+
15
+ export const PopupAlert = ({ setShowAlert }) => (
16
+ <Alert
17
+ ouiaId="template-invocation-new-tab-popup-alert"
18
+ variant="warning"
19
+ actionClose={<AlertActionCloseButton onClose={() => setShowAlert(false)} />}
20
+ title={__(
21
+ 'Popups are blocked by your browser. Please allow popups for this site to open all invocations in new tabs.'
22
+ )}
23
+ />
24
+ );
25
+
26
+ const OpenAllInvocationsModal = ({
27
+ failedCount,
28
+ failedHosts,
29
+ isOpen,
30
+ isOpenFailed,
31
+ jobID,
32
+ onClose,
33
+ setShowAlert,
34
+ selectedIds,
35
+ }) => {
36
+ const modalText = isOpenFailed ? 'failed' : 'selected';
37
+
38
+ const openLink = url => {
39
+ const newWin = window.open(url);
40
+ if (!newWin || newWin.closed || typeof newWin.closed === 'undefined') {
41
+ setShowAlert(true);
42
+ }
43
+ };
44
+
45
+ return (
46
+ <Modal
47
+ className="open-all-modal"
48
+ isOpen={isOpen}
49
+ onClose={onClose}
50
+ ouiaId="template-invocation-new-tab-modal"
51
+ title={sprintf(__('Open all %s invocations in new tabs'), modalText)}
52
+ titleIconVariant="warning"
53
+ width={590}
54
+ actions={[
55
+ <Button
56
+ ouiaId="template-invocation-new-tab-modal-confirm"
57
+ key="confirm"
58
+ variant="primary"
59
+ onClick={() => {
60
+ const hostsToOpen = isOpenFailed
61
+ ? failedHosts
62
+ : selectedIds.map(id => ({ id }));
63
+
64
+ hostsToOpen
65
+ .slice(0, MAX_HOSTS_API_SIZE)
66
+ .forEach(({ id }) =>
67
+ openLink(templateInvocationPageUrl(id, jobID), '_blank')
68
+ );
69
+
70
+ onClose();
71
+ }}
72
+ >
73
+ {__('Open in new tabs')}
74
+ </Button>,
75
+ <Button
76
+ ouiaId="template-invocation-new-tab-modal-cancel"
77
+ key="cancel"
78
+ variant="link"
79
+ onClick={onClose}
80
+ >
81
+ {__('Cancel')}
82
+ </Button>,
83
+ ]}
84
+ >
85
+ {sprintf(
86
+ __('Are you sure you want to open all %s invocations in new tabs?'),
87
+ modalText
88
+ )}
89
+ <br />
90
+ {__('This will open a new tab for each invocation. The maximum is 100.')}
91
+ <br />
92
+ {sprintf(__('The number of %s invocations is:'), modalText)}{' '}
93
+ <b>{isOpenFailed ? failedCount : selectedIds.length}</b>
94
+ </Modal>
95
+ );
96
+ };
97
+
98
+ OpenAllInvocationsModal.propTypes = {
99
+ failedCount: PropTypes.number.isRequired,
100
+ failedHosts: PropTypes.array,
101
+ isOpen: PropTypes.bool.isRequired,
102
+ isOpenFailed: PropTypes.bool,
103
+ jobID: PropTypes.string.isRequired,
104
+ onClose: PropTypes.func.isRequired,
105
+ setShowAlert: PropTypes.func.isRequired,
106
+ selectedIds: PropTypes.array.isRequired,
107
+ };
108
+
109
+ OpenAllInvocationsModal.defaultProps = {
110
+ failedHosts: [],
111
+ isOpenFailed: false,
112
+ };
113
+
114
+ PopupAlert.propTypes = {
115
+ setShowAlert: PropTypes.func.isRequired,
116
+ };
117
+
118
+ export default OpenAllInvocationsModal;
@@ -1,8 +1,9 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState, useEffect, useRef } from 'react';
2
2
  import { isEmpty } from 'lodash';
3
3
  import PropTypes from 'prop-types';
4
4
  import { ClipboardCopyButton, Alert, Skeleton } from '@patternfly/react-core';
5
- import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
5
+ import { useDispatch, useSelector } from 'react-redux';
6
+ import { APIActions } from 'foremanReact/redux/API';
6
7
  import { translate as __ } from 'foremanReact/common/I18n';
7
8
  import { useForemanHostDetailsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
8
9
  import { STATUS } from 'foremanReact/constants';
@@ -11,6 +12,10 @@ import {
11
12
  templateInvocationPageUrl,
12
13
  GET_TEMPLATE_INVOCATION,
13
14
  } from './JobInvocationConstants';
15
+ import {
16
+ selectTemplateInvocationStatus,
17
+ selectTemplateInvocation,
18
+ } from './JobInvocationSelectors';
14
19
  import { OutputToggleGroup } from './TemplateInvocationComponents/OutputToggleGroup';
15
20
  import { PreviewTemplate } from './TemplateInvocationComponents/PreviewTemplate';
16
21
  import { OutputCodeBlock } from './TemplateInvocationComponents/OutputCodeBlock';
@@ -47,41 +52,64 @@ const CopyToClipboard = ({ fullOutput }) => {
47
52
  </ClipboardCopyButton>
48
53
  );
49
54
  };
50
- let intervalId;
55
+
51
56
  export const TemplateInvocation = ({
52
57
  hostID,
53
58
  jobID,
54
59
  isInTableView,
55
60
  hostName,
56
61
  hostProxy,
62
+ isExpanded,
57
63
  }) => {
64
+ const intervalRef = useRef(null);
58
65
  const templateURL = showTemplateInvocationUrl(hostID, jobID);
59
66
  const hostDetailsPageUrl = useForemanHostDetailsPageUrl();
60
- const { response, status, setAPIOptions } = useAPI('get', templateURL, {
61
- key: GET_TEMPLATE_INVOCATION,
62
- headers: { Accept: 'application/json' },
63
- handleError: () => {
64
- if (intervalId) clearInterval(intervalId);
65
- },
66
- });
67
- const { finished, auto_refresh: autoRefresh } = response;
67
+
68
+ const status = useSelector(selectTemplateInvocationStatus(hostID));
69
+ const response = useSelector(selectTemplateInvocation(hostID));
70
+ const finished = response.finished ?? true;
71
+ const autoRefresh = response.auto_refresh || false;
72
+ const dispatch = useDispatch();
68
73
 
69
74
  useEffect(() => {
70
- if (!finished || autoRefresh) {
71
- intervalId = setInterval(() => {
72
- // Re call api
73
- setAPIOptions(prevOptions => ({
74
- ...prevOptions,
75
- }));
75
+ const getData = async () => {
76
+ if (
77
+ (!isInTableView || (isInTableView && isExpanded)) &&
78
+ (Object.keys(response).length === 0 || autoRefresh)
79
+ ) {
80
+ dispatch(
81
+ APIActions.get({
82
+ url: templateURL,
83
+ key: `${GET_TEMPLATE_INVOCATION}_${hostID}`,
84
+ handleError: () => {
85
+ if (intervalRef.current) clearInterval(intervalRef.current);
86
+ },
87
+ })
88
+ );
89
+ }
90
+ };
91
+ getData();
92
+ if (!finished && autoRefresh) {
93
+ intervalRef.current = setInterval(() => {
94
+ getData();
76
95
  }, 5000);
77
96
  }
78
- if (intervalId && finished && !autoRefresh) {
79
- clearInterval(intervalId);
80
- }
97
+
81
98
  return () => {
82
- clearInterval(intervalId);
99
+ if (intervalRef.current) {
100
+ clearInterval(intervalRef.current);
101
+ intervalRef.current = null;
102
+ }
83
103
  };
84
- }, [finished, autoRefresh, setAPIOptions]);
104
+ }, [
105
+ dispatch,
106
+ templateURL,
107
+ isExpanded,
108
+ isInTableView,
109
+ finished,
110
+ autoRefresh,
111
+ hostID,
112
+ ]);
85
113
 
86
114
  const errorMessage =
87
115
  response?.response?.data?.error?.message ||
@@ -91,10 +119,10 @@ export const TemplateInvocation = ({
91
119
  preview,
92
120
  output,
93
121
  input_values: inputValues,
94
- task_id: taskID,
95
- task_cancellable: taskCancellable,
122
+ task,
96
123
  permissions,
97
124
  } = response;
125
+ const { id: taskID, cancellable: taskCancellable } = task || {};
98
126
  const [showOutputType, setShowOutputType] = useState({
99
127
  stderr: true,
100
128
  stdout: true,
@@ -197,12 +225,14 @@ TemplateInvocation.propTypes = {
197
225
  hostProxy: PropTypes.object, // only used when isInTableView is false
198
226
  jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
199
227
  isInTableView: PropTypes.bool,
228
+ isExpanded: PropTypes.bool,
200
229
  };
201
230
 
202
231
  TemplateInvocation.defaultProps = {
203
232
  isInTableView: true,
204
233
  hostName: '',
205
234
  hostProxy: {},
235
+ isExpanded: false,
206
236
  };
207
237
 
208
238
  CopyToClipboard.propTypes = {
@@ -6,7 +6,7 @@ import { ActionsColumn } from '@patternfly/react-table';
6
6
  import { APIActions } from 'foremanReact/redux/API';
7
7
  import { addToast } from 'foremanReact/components/ToastsList';
8
8
  import { translate as __ } from 'foremanReact/common/I18n';
9
- import { selectTemplateInvocation } from '../JobInvocationSelectors';
9
+ import { selectTemplateInvocationList } from '../JobInvocationSelectors';
10
10
  import './index.scss';
11
11
 
12
12
  const actions = ({
@@ -47,7 +47,8 @@ const actions = ({
47
47
  APIActions.post({
48
48
  url: `/foreman_tasks/tasks/${taskID}/cancel`,
49
49
  key: 'CANCEL_TASK',
50
- errorToast: ({ response }) => response.data.message,
50
+ errorToast: ({ response }) =>
51
+ response?.data?.message || __('Could not cancel the task'),
51
52
  successToast: () => __('Task for the host cancelled succesfully'),
52
53
  })
53
54
  );
@@ -70,7 +71,8 @@ const actions = ({
70
71
  APIActions.post({
71
72
  url: `/foreman_tasks/tasks/${taskID}/abort`,
72
73
  key: 'ABORT_TASK',
73
- errorToast: ({ response }) => response.data.message,
74
+ errorToast: ({ response }) =>
75
+ response?.data?.message || __('Could not abort the task'),
74
76
  successToast: () => __('task aborted succesfully'),
75
77
  })
76
78
  );
@@ -81,14 +83,10 @@ const actions = ({
81
83
 
82
84
  export const RowActions = ({ hostID, jobID }) => {
83
85
  const dispatch = useDispatch();
84
- const response = useSelector(selectTemplateInvocation);
86
+ const response = useSelector(selectTemplateInvocationList)?.[hostID];
85
87
  if (!response?.permissions) return null;
86
- const {
87
- task_id: taskID,
88
- task_cancellable: taskCancellable,
89
- permissions,
90
- } = response;
91
-
88
+ const { task, permissions } = response;
89
+ const { id: taskID, cancellable: taskCancellable } = task || {};
92
90
  const getActions = actions({
93
91
  taskID,
94
92
  jobID,
@@ -16,7 +16,7 @@ const TemplateInvocationPage = ({
16
16
  job_invocation_description: jobDescription,
17
17
  host_name: hostName,
18
18
  proxy: hostProxy,
19
- } = useSelector(selectTemplateInvocation);
19
+ } = useSelector(selectTemplateInvocation(hostID));
20
20
  const description = sprintf(__('Template Invocation for %s'), hostName);
21
21
  const breadcrumbOptions = {
22
22
  breadcrumbItems: [
@@ -96,9 +96,7 @@ describe('JobInvocationDetailPage', () => {
96
96
  );
97
97
 
98
98
  expect(screen.getByText('Description')).toBeInTheDocument();
99
- expect(
100
- container.querySelector('.chart-donut .pf-c-chart') // todo: change to pf5 once we update @patternfly/react-charts to v7
101
- ).toBeInTheDocument();
99
+ expect(container.querySelector('.chart-donut')).toBeInTheDocument();
102
100
  expect(screen.getByText('2/6')).toBeInTheDocument();
103
101
  expect(screen.getByText('Systems')).toBeInTheDocument();
104
102
  expect(screen.getByText('System status')).toBeInTheDocument();
@@ -178,7 +176,7 @@ describe('JobInvocationDetailPage', () => {
178
176
  .getByText('Legacy UI')
179
177
  .closest('a')
180
178
  .getAttribute('href')
181
- ).toEqual(`/job_invocations/${jobId}`);
179
+ ).toEqual(`/legacy/job_invocations/${jobId}`);
182
180
  });
183
181
 
184
182
  it('shows scheduled date', async () => {
@@ -0,0 +1,204 @@
1
+ import '@testing-library/jest-dom/extend-expect';
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
3
+ import axios from 'axios';
4
+ import { foremanUrl } from 'foremanReact/common/helpers';
5
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
6
+ import React from 'react';
7
+ import { Provider } from 'react-redux';
8
+ import configureStore from 'redux-mock-store';
9
+
10
+ import { CheckboxesActions } from '../CheckboxesActions';
11
+ import * as selectors from '../JobInvocationSelectors';
12
+ import { PopupAlert } from '../OpenAllInvocationsModal';
13
+
14
+ jest.mock('axios');
15
+ jest.mock('foremanReact/common/hooks/API/APIHooks');
16
+ jest.mock('../JobInvocationSelectors');
17
+
18
+ jest.mock('../JobInvocationConstants', () => ({
19
+ ...jest.requireActual('../JobInvocationConstants'),
20
+ templateInvocationPageUrl: jest.fn(
21
+ (hostId, jobId) => `url/${hostId}/${jobId}`
22
+ ),
23
+ DIRECT_OPEN_HOST_LIMIT: 3,
24
+ }));
25
+
26
+ selectors.selectItems.mockImplementation(() => ({
27
+ targeting: { search_query: 'name~*' },
28
+ }));
29
+ selectors.selectHasPermission.mockImplementation(() => () => true);
30
+ selectors.selectTaskCancelable.mockImplementation(() => true);
31
+
32
+ const mockStore = configureStore([]);
33
+ const store = mockStore({});
34
+
35
+ describe('TableToolbarActions', () => {
36
+ const jobID = '42';
37
+ let openSpy;
38
+
39
+ beforeEach(() => {
40
+ openSpy = jest.spyOn(window, 'open').mockImplementation(jest.fn());
41
+ useAPI.mockClear();
42
+ axios.post.mockResolvedValue({ data: {} });
43
+ useAPI.mockReturnValue({
44
+ response: null,
45
+ status: 'initial',
46
+ });
47
+ });
48
+
49
+ afterEach(() => {
50
+ openSpy.mockRestore();
51
+ jest.clearAllMocks();
52
+ });
53
+
54
+ describe('Opening selected in new tabs', () => {
55
+ test('opens links when results length is less than or equal to 3', () => {
56
+ const selectedIds = [1, 2, 3];
57
+ render(
58
+ <Provider store={store}>
59
+ <CheckboxesActions
60
+ selectedIds={selectedIds}
61
+ failedCount={0}
62
+ jobID={jobID}
63
+ />
64
+ </Provider>
65
+ );
66
+ const openAllButton = screen.getByLabelText(
67
+ /open all template invocations in new tab/i
68
+ );
69
+ fireEvent.click(openAllButton);
70
+ expect(openSpy).toHaveBeenCalledTimes(selectedIds.length);
71
+ });
72
+
73
+ test('shows modal when results length is greater than 3', async () => {
74
+ const selectedIds = [1, 2, 3, 4];
75
+ render(
76
+ <Provider store={store}>
77
+ <CheckboxesActions
78
+ selectedIds={selectedIds}
79
+ failedCount={0}
80
+ jobID={jobID}
81
+ />
82
+ </Provider>
83
+ );
84
+ fireEvent.click(
85
+ screen.getByLabelText(/open all template invocations in new tab/i)
86
+ );
87
+ expect(
88
+ await screen.findByRole('heading', {
89
+ name: /open all.*invocations in new tabs \+ selected/i,
90
+ })
91
+ ).toBeInTheDocument();
92
+ });
93
+
94
+ test('shows alert when popups are blocked', async () => {
95
+ openSpy.mockReturnValue(null);
96
+ const selectedIds = [1, 2];
97
+ render(
98
+ <Provider store={store}>
99
+ <CheckboxesActions
100
+ selectedIds={selectedIds}
101
+ failedCount={0}
102
+ jobID={jobID}
103
+ />
104
+ </Provider>
105
+ );
106
+ fireEvent.click(
107
+ screen.getByLabelText(/open all template invocations in new tab/i)
108
+ );
109
+ expect(
110
+ await screen.findByText(/Popups are blocked by your browser/)
111
+ ).toBeInTheDocument();
112
+ });
113
+ });
114
+
115
+ describe('Opening failed in new tabs', () => {
116
+ test('opens links when results length is less than or equal to 3', async () => {
117
+ const failedHosts = [{ id: 301 }, { id: 302 }];
118
+ useAPI.mockReturnValue({
119
+ response: { results: failedHosts },
120
+ status: 'success',
121
+ });
122
+ render(
123
+ <Provider store={store}>
124
+ <CheckboxesActions selectedIds={[]} failedCount={2} jobID={jobID} />
125
+ </Provider>
126
+ );
127
+ fireEvent.click(screen.getByLabelText(/actions dropdown toggle/i));
128
+ fireEvent.click(await screen.findByText(/open all failed runs/i));
129
+ await waitFor(() => {
130
+ expect(openSpy).toHaveBeenCalledTimes(failedHosts.length);
131
+ });
132
+ });
133
+
134
+ test('shows modal when results length is greater than 3', async () => {
135
+ render(
136
+ <Provider store={store}>
137
+ <CheckboxesActions selectedIds={[]} failedCount={4} jobID={jobID} />
138
+ </Provider>
139
+ );
140
+ fireEvent.click(screen.getByLabelText(/actions dropdown toggle/i));
141
+ fireEvent.click(await screen.findByText(/open all failed runs/i));
142
+ expect(
143
+ await screen.findByRole('heading', {
144
+ name: /open all.*invocations in new tabs \+ failed/i,
145
+ })
146
+ ).toBeInTheDocument();
147
+ });
148
+
149
+ test('calls useApi with skip: true when failedCount is 0', () => {
150
+ render(
151
+ <Provider store={store}>
152
+ <CheckboxesActions selectedIds={[]} failedCount={0} jobID={jobID} />
153
+ </Provider>
154
+ );
155
+ expect(useAPI).toHaveBeenCalledWith(
156
+ 'get',
157
+ foremanUrl(`/api/job_invocations/${jobID}/hosts`),
158
+ expect.objectContaining({
159
+ skip: true,
160
+ })
161
+ );
162
+ });
163
+ });
164
+
165
+ describe('PopupAlert', () => {
166
+ test('renders without crashing', () => {
167
+ const mockSetShowAlert = jest.fn();
168
+ render(<PopupAlert setShowAlert={mockSetShowAlert} />);
169
+ expect(
170
+ screen.getByText(/Popups are blocked by your browser/)
171
+ ).toBeInTheDocument();
172
+ });
173
+
174
+ test('closes alert when close button is clicked', () => {
175
+ const mockSetShowAlert = jest.fn();
176
+ render(<PopupAlert setShowAlert={mockSetShowAlert} />);
177
+ const closeButton = screen.getByRole('button', { name: /close/i });
178
+ fireEvent.click(closeButton);
179
+ expect(mockSetShowAlert).toHaveBeenCalledWith(false);
180
+ });
181
+ });
182
+
183
+ describe('Rerun button', () => {
184
+ test('is enabled when permissions and ids are valid', () => {
185
+ const selectedIds = [101, 102, 103];
186
+ render(
187
+ <Provider store={store}>
188
+ <CheckboxesActions
189
+ bulkParams="(id ^ (101, 102, 103))"
190
+ selectedIds={selectedIds}
191
+ failedCount={1}
192
+ jobID={jobID}
193
+ />
194
+ </Provider>
195
+ );
196
+ const rerunLink = screen.getByRole('link', { name: /rerun/i });
197
+ expect(rerunLink).toBeEnabled();
198
+ const expectedHref = foremanUrl(
199
+ `/job_invocations/42/rerun?search=(name~*) AND ((id ^ (101, 102, 103)))`
200
+ );
201
+ expect(rerunLink).toHaveAttribute('href', expectedHref);
202
+ });
203
+ });
204
+ });
@@ -3,18 +3,19 @@ import configureMockStore from 'redux-mock-store';
3
3
  import { Provider } from 'react-redux';
4
4
  import { render, screen, act, fireEvent } from '@testing-library/react';
5
5
  import '@testing-library/jest-dom/extend-expect';
6
- import * as APIHooks from 'foremanReact/common/hooks/API/APIHooks';
7
6
  import * as api from 'foremanReact/redux/API';
7
+ import * as selectors from '../JobInvocationSelectors';
8
8
  import { TemplateInvocation } from '../TemplateInvocation';
9
9
  import { mockTemplateInvocationResponse } from './fixtures';
10
10
 
11
11
  jest.spyOn(api, 'get');
12
- jest.mock('foremanReact/common/hooks/API/APIHooks');
13
- APIHooks.useAPI.mockImplementation(() => ({
14
- response: mockTemplateInvocationResponse,
15
- status: 'RESOLVED',
16
- }));
17
-
12
+ jest.mock('../JobInvocationSelectors');
13
+ selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
14
+ 'RESOLVED'
15
+ );
16
+ selectors.selectTemplateInvocation.mockImplementation(() => () =>
17
+ mockTemplateInvocationResponse
18
+ );
18
19
  const mockStore = configureMockStore([]);
19
20
  const store = mockStore({
20
21
  HOSTS_API: {
@@ -95,19 +96,22 @@ describe('TemplateInvocation', () => {
95
96
  ).toHaveLength(1);
96
97
  });
97
98
  test('displays an error alert when there is an error', async () => {
98
- APIHooks.useAPI.mockImplementation(() => ({
99
- response: { response: { data: { error: 'Error message' } } },
100
- status: 'ERROR',
99
+ selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
100
+ 'ERROR'
101
+ );
102
+ selectors.selectTemplateInvocation.mockImplementation(() => () => ({
103
+ response: { data: { error: 'Error message' } },
101
104
  }));
102
-
103
105
  render(
104
- <TemplateInvocation
105
- hostID="1"
106
- jobID="1"
107
- isInTableView={false}
108
- hostName="example-host"
109
- hostProxy={{ name: 'example-proxy', href: '#' }}
110
- />
106
+ <Provider store={store}>
107
+ <TemplateInvocation
108
+ hostID="1"
109
+ jobID="1"
110
+ isInTableView={false}
111
+ hostName="example-host"
112
+ hostProxy={{ name: 'example-proxy', href: '#' }}
113
+ />
114
+ </Provider>
111
115
  );
112
116
 
113
117
  expect(
@@ -119,17 +123,19 @@ describe('TemplateInvocation', () => {
119
123
  });
120
124
 
121
125
  test('displays a skeleton while loading', async () => {
122
- APIHooks.useAPI.mockImplementation(() => ({
123
- response: {},
124
- status: 'PENDING',
125
- }));
126
+ selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
127
+ 'PENDING'
128
+ );
129
+ selectors.selectTemplateInvocation.mockImplementation(() => () => ({}));
126
130
  render(
127
- <TemplateInvocation
128
- hostID="1"
129
- jobID="1"
130
- isInTableView={false}
131
- hostName="example-host"
132
- />
131
+ <Provider store={store}>
132
+ <TemplateInvocation
133
+ hostID="1"
134
+ jobID="1"
135
+ isInTableView={false}
136
+ hostName="example-host"
137
+ />
138
+ </Provider>
133
139
  );
134
140
 
135
141
  expect(document.querySelectorAll('.pf-v5-c-skeleton')).toHaveLength(1);