foreman_remote_execution 16.0.4 → 16.0.5

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/job_invocations_controller.rb +28 -1
  3. data/app/controllers/template_invocations_controller.rb +1 -1
  4. data/config/routes.rb +1 -0
  5. data/lib/foreman_remote_execution/engine.rb +9 -256
  6. data/lib/foreman_remote_execution/plugin.rb +246 -0
  7. data/lib/foreman_remote_execution/version.rb +1 -1
  8. data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
  9. data/test/unit/job_invocation_report_template_test.rb +6 -6
  10. data/webpack/JobInvocationDetail/CheckboxesActions.js +196 -0
  11. data/webpack/JobInvocationDetail/{JobInvocationHostTableToolbar.js → DropdownFilter.js} +3 -6
  12. data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -6
  13. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +18 -0
  14. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +205 -89
  15. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +30 -3
  16. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +20 -27
  17. data/webpack/JobInvocationDetail/OpenAllInvocationsModal.js +118 -0
  18. data/webpack/JobInvocationDetail/TemplateInvocation.js +54 -24
  19. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +8 -10
  20. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +1 -1
  21. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
  22. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +202 -0
  23. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +34 -28
  24. data/webpack/JobInvocationDetail/index.js +61 -30
  25. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +26 -7
  26. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +1 -1
  27. metadata +8 -6
  28. data/webpack/JobInvocationDetail/OpenAlInvocations.js +0 -111
  29. 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: [
@@ -97,7 +97,7 @@ describe('JobInvocationDetailPage', () => {
97
97
 
98
98
  expect(screen.getByText('Description')).toBeInTheDocument();
99
99
  expect(
100
- container.querySelector('.chart-donut .pf-c-chart') // todo: change to pf5 once we update @patternfly/react-charts to v7
100
+ container.querySelector('.chart-donut .pf-v5-c-chart')
101
101
  ).toBeInTheDocument();
102
102
  expect(screen.getByText('2/6')).toBeInTheDocument();
103
103
  expect(screen.getByText('Systems')).toBeInTheDocument();
@@ -0,0 +1,202 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import configureStore from 'redux-mock-store';
4
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
5
+ import '@testing-library/jest-dom/extend-expect';
6
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
7
+ import { CheckboxesActions } from '../CheckboxesActions';
8
+ import * as selectors from '../JobInvocationSelectors';
9
+ import { PopupAlert } from '../OpenAllInvocationsModal';
10
+
11
+ jest.mock('foremanReact/common/hooks/API/APIHooks');
12
+
13
+ jest.mock('../JobInvocationSelectors');
14
+ jest.mock('../JobInvocationConstants', () => ({
15
+ ...jest.requireActual('../JobInvocationConstants'),
16
+ templateInvocationPageUrl: jest.fn(
17
+ (hostId, jobId) => `url/${hostId}/${jobId}`
18
+ ),
19
+ DIRECT_OPEN_HOST_LIMIT: 3,
20
+ }));
21
+ selectors.selectItems.mockImplementation(() => ({
22
+ targeting: { search_query: 'name~*' },
23
+ }));
24
+ selectors.selectHasPermission.mockImplementation(() => () => () => true);
25
+ const mockStore = configureStore([]);
26
+ const store = mockStore({
27
+ templateInvocation: {
28
+ permissions: {
29
+ execute_jobs: true,
30
+ },
31
+ },
32
+ });
33
+
34
+ describe('TableToolbarActions', () => {
35
+ const jobID = '42';
36
+ let openSpy;
37
+
38
+ beforeEach(() => {
39
+ openSpy = jest.spyOn(window, 'open').mockImplementation(jest.fn());
40
+ useAPI.mockClear();
41
+ useAPI.mockReturnValue({
42
+ response: null,
43
+ status: 'initial',
44
+ });
45
+ });
46
+
47
+ afterEach(() => {
48
+ openSpy.mockRestore();
49
+ jest.clearAllMocks();
50
+ });
51
+
52
+ describe('Opening selected in new tabs', () => {
53
+ test('opens links when results length is less than or equal to 3', () => {
54
+ const selectedIds = [1, 2, 3];
55
+ render(
56
+ <Provider store={store}>
57
+ <CheckboxesActions
58
+ selectedIds={selectedIds}
59
+ failedCount={0}
60
+ jobID={jobID}
61
+ />
62
+ </Provider>
63
+ );
64
+ const openAllButton = screen.getByLabelText(
65
+ /open all template invocations in new tab/i
66
+ );
67
+ fireEvent.click(openAllButton);
68
+ expect(openSpy).toHaveBeenCalledTimes(selectedIds.length);
69
+ });
70
+
71
+ test('shows modal when results length is greater than 3', () => {
72
+ const selectedIds = [1, 2, 3, 4];
73
+ render(
74
+ <Provider store={store}>
75
+ <CheckboxesActions
76
+ selectedIds={selectedIds}
77
+ failedCount={0}
78
+ jobID={jobID}
79
+ />
80
+ </Provider>
81
+ );
82
+ fireEvent.click(
83
+ screen.getByLabelText(/open all template invocations in new tab/i)
84
+ );
85
+ expect(
86
+ screen.getByRole('heading', {
87
+ name: /open all %s invocations in new tabs \+ selected/i,
88
+ })
89
+ ).toBeInTheDocument();
90
+ });
91
+
92
+ test('shows alert when popups are blocked', () => {
93
+ openSpy.mockReturnValue(null);
94
+ const selectedIds = [1, 2];
95
+ render(
96
+ <Provider store={store}>
97
+ <CheckboxesActions
98
+ selectedIds={selectedIds}
99
+ failedCount={0}
100
+ jobID={jobID}
101
+ />
102
+ </Provider>
103
+ );
104
+ fireEvent.click(
105
+ screen.getByLabelText(/open all template invocations in new tab/i)
106
+ );
107
+ expect(
108
+ screen.getByText(/Popups are blocked by your browser/)
109
+ ).toBeInTheDocument();
110
+ });
111
+ });
112
+
113
+ describe('Opening failed in new tabs', () => {
114
+ test('opens links when results length is less than or equal to 3', async () => {
115
+ const failedHosts = [{ id: 301 }, { id: 302 }];
116
+ useAPI.mockReturnValue({
117
+ response: { results: failedHosts },
118
+ status: 'success',
119
+ });
120
+ render(
121
+ <Provider store={store}>
122
+ <CheckboxesActions selectedIds={[]} failedCount={2} jobID={jobID} />
123
+ </Provider>
124
+ );
125
+ fireEvent.click(screen.getByLabelText(/actions dropdown toggle/i));
126
+ fireEvent.click(screen.getByText(/open all failed runs/i));
127
+ await waitFor(() => {
128
+ expect(openSpy).toHaveBeenCalledTimes(failedHosts.length);
129
+ });
130
+ });
131
+
132
+ test('shows modal when results length is greater than 3', () => {
133
+ render(
134
+ <Provider store={store}>
135
+ <CheckboxesActions selectedIds={[]} failedCount={4} jobID={jobID} />
136
+ </Provider>
137
+ );
138
+ fireEvent.click(screen.getByLabelText(/actions dropdown toggle/i));
139
+ fireEvent.click(screen.getByText(/open all failed runs/i));
140
+ expect(
141
+ screen.getByRole('heading', {
142
+ name: /open all %s invocations in new tabs \+ failed/i,
143
+ })
144
+ ).toBeInTheDocument();
145
+ });
146
+
147
+ test('calls useApi with skip: true when failedCount is 0', () => {
148
+ render(
149
+ <Provider store={store}>
150
+ <CheckboxesActions selectedIds={[]} failedCount={0} jobID={jobID} />
151
+ </Provider>
152
+ );
153
+ expect(useAPI).toHaveBeenCalledWith(
154
+ 'get',
155
+ `foreman/api/job_invocations/${jobID}/hosts`,
156
+ expect.objectContaining({
157
+ skip: true,
158
+ })
159
+ );
160
+ });
161
+ });
162
+
163
+ describe('PopupAlert', () => {
164
+ test('renders without crashing', () => {
165
+ const mockSetShowAlert = jest.fn();
166
+ render(<PopupAlert setShowAlert={mockSetShowAlert} />);
167
+ expect(
168
+ screen.getByText(/Popups are blocked by your browser/)
169
+ ).toBeInTheDocument();
170
+ });
171
+
172
+ test('closes alert when close button is clicked', () => {
173
+ const mockSetShowAlert = jest.fn();
174
+ render(<PopupAlert setShowAlert={mockSetShowAlert} />);
175
+ const closeButton = screen.getByRole('button', { name: /close/i });
176
+ fireEvent.click(closeButton);
177
+ expect(mockSetShowAlert).toHaveBeenCalledWith(false);
178
+ });
179
+ });
180
+
181
+ describe('Rerun button', () => {
182
+ test('is enabled when permissions and ids are valid', () => {
183
+ const selectedIds = [101, 102, 103];
184
+ render(
185
+ <Provider store={store}>
186
+ <CheckboxesActions
187
+ bulkParams="(id ^ (101, 102, 103))"
188
+ selectedIds={selectedIds}
189
+ failedCount={1}
190
+ jobID={jobID}
191
+ />
192
+ </Provider>
193
+ );
194
+ const rerunLink = screen.getByRole('link', { name: /rerun/i });
195
+ expect(rerunLink).toBeEnabled();
196
+ const expectedSearchParams = new URLSearchParams();
197
+ selectedIds.forEach(id => expectedSearchParams.append('host_ids[]', id));
198
+ const expectedHref = `foreman/job_invocations/42/rerun?search=(name~*) AND ((id ^ (101, 102, 103)))`;
199
+ expect(rerunLink).toHaveAttribute('href', expectedHref);
200
+ });
201
+ });
202
+ });
@@ -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);