foreman_remote_execution 16.6.5 → 16.7.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.
@@ -1,5 +1,6 @@
1
+ /* eslint-disable max-lines */
1
2
  import PropTypes from 'prop-types';
2
- import React, { useEffect, useState } from 'react';
3
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
3
4
  import { useDispatch, useSelector } from 'react-redux';
4
5
  import { Button, Split, SplitItem } from '@patternfly/react-core';
5
6
  import { UndoIcon } from '@patternfly/react-icons';
@@ -12,6 +13,7 @@ import {
12
13
  } from '@patternfly/react-core/deprecated';
13
14
  import { translate as __ } from 'foremanReact/common/I18n';
14
15
  import { foremanUrl } from 'foremanReact/common/helpers';
16
+ import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
15
17
  import { get } from 'foremanReact/redux/API';
16
18
  import {
17
19
  cancelJob,
@@ -23,10 +25,7 @@ import {
23
25
  GET_REPORT_TEMPLATES,
24
26
  GET_REPORT_TEMPLATE_INPUTS,
25
27
  } from './JobInvocationConstants';
26
- import {
27
- selectTaskCancelable,
28
- selectHasPermission,
29
- } from './JobInvocationSelectors';
28
+ import { selectTaskCancelable } from './JobInvocationSelectors';
30
29
 
31
30
  const JobInvocationToolbarButtons = ({ jobId, data }) => {
32
31
  const { succeeded, failed, task, recurrence, permissions } = data;
@@ -38,165 +37,238 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
38
37
  ? permissions.edit_recurring_logics
39
38
  : false;
40
39
  const isTaskCancelable = useSelector(selectTaskCancelable);
41
- const useHasPermission = permissionRequired =>
42
- useSelector(selectHasPermission(permissionRequired));
40
+ const canCreateJobInvocations = usePermissions(['create_job_invocations']);
41
+ const canCancelJobInvocations = usePermissions(['cancel_job_invocations']);
42
+ const canGenerateReportTemplates = usePermissions([
43
+ 'generate_report_templates',
44
+ ]);
43
45
  const [isActionOpen, setIsActionOpen] = useState(false);
44
46
  const [reportTemplateJobId, setReportTemplateJobId] = useState(undefined);
45
47
  const [templateInputId, setTemplateInputId] = useState(undefined);
46
- const queryParams = new URLSearchParams({
47
- [`report_template_report[input_values][${templateInputId}][value]`]: jobId,
48
- });
49
48
  const dispatch = useDispatch();
49
+ const reportHref = useMemo(() => {
50
+ if (reportTemplateJobId === undefined || templateInputId === undefined) {
51
+ return undefined;
52
+ }
53
+ const queryParams = new URLSearchParams({
54
+ [`report_template_report[input_values][${templateInputId}][value]`]: jobId,
55
+ });
56
+ return foremanUrl(
57
+ `/templates/report_templates/${reportTemplateJobId}/generate?${queryParams.toString()}`
58
+ );
59
+ }, [jobId, reportTemplateJobId, templateInputId]);
50
60
 
51
- const onActionFocus = () => {
61
+ const onActionFocus = useCallback(() => {
52
62
  const element = document.getElementById(
53
63
  `toggle-split-button-action-primary-${jobId}`
54
64
  );
55
- element.focus();
56
- };
57
- const onActionSelect = () => {
65
+ if (element) {
66
+ element.focus();
67
+ }
68
+ }, [jobId]);
69
+ const onActionSelect = useCallback(() => {
58
70
  setIsActionOpen(false);
59
71
  onActionFocus();
60
- };
72
+ }, [onActionFocus]);
73
+ const onActionToggle = useCallback((_event, val) => setIsActionOpen(val), []);
61
74
 
62
75
  useEffect(() => {
76
+ let isMounted = true;
63
77
  dispatch(
64
78
  get({
65
79
  key: GET_REPORT_TEMPLATES,
66
80
  url: '/api/report_templates',
67
81
  handleSuccess: ({ data: { results } }) => {
68
- setReportTemplateJobId(
69
- results.find(result => result.name === 'Job - Invocation Report')
70
- ?.id
71
- );
82
+ if (isMounted) {
83
+ setReportTemplateJobId(
84
+ results.find(result => result.name === 'Job - Invocation Report')
85
+ ?.id
86
+ );
87
+ }
72
88
  },
73
89
  handleError: () => {
74
- setReportTemplateJobId(undefined);
90
+ if (isMounted) {
91
+ setReportTemplateJobId(undefined);
92
+ }
75
93
  },
76
94
  })
77
95
  );
96
+ return () => {
97
+ isMounted = false;
98
+ };
78
99
  }, [dispatch]);
79
100
  useEffect(() => {
101
+ let isMounted = true;
80
102
  if (reportTemplateJobId !== undefined) {
81
103
  dispatch(
82
104
  get({
83
105
  key: GET_REPORT_TEMPLATE_INPUTS,
84
106
  url: `/api/templates/${reportTemplateJobId}/template_inputs`,
85
107
  handleSuccess: ({ data: { results } }) => {
86
- setTemplateInputId(
87
- results.find(result => result.name === 'job_id')?.id
88
- );
108
+ if (isMounted) {
109
+ setTemplateInputId(
110
+ results.find(result => result.name === 'job_id')?.id
111
+ );
112
+ }
89
113
  },
90
114
  handleError: () => {
91
- setTemplateInputId(undefined);
115
+ if (isMounted) {
116
+ setTemplateInputId(undefined);
117
+ }
92
118
  },
93
119
  })
94
120
  );
95
121
  }
122
+ return () => {
123
+ isMounted = false;
124
+ };
96
125
  }, [dispatch, reportTemplateJobId]);
97
126
 
98
- const recurrenceDropdownItems = recurrence
99
- ? [
100
- <DropdownSeparator ouiaId="dropdown-separator-1" key="separator-1" />,
101
- <DropdownItem
102
- ouiaId="change-enabled-recurring-dropdown-item"
103
- onClick={() =>
104
- dispatch(
105
- enableRecurringLogic(recurrence?.id, recurringEnabled, jobId)
106
- )
107
- }
108
- key="change-enabled-recurring"
109
- component="button"
110
- isDisabled={
111
- recurrence?.id === undefined ||
112
- recurrence?.state === 'cancelled' ||
113
- !canEditRecurringLogic
114
- }
115
- >
116
- {recurringEnabled ? __('Disable recurring') : __('Enable recurring')}
117
- </DropdownItem>,
118
- <DropdownItem
119
- ouiaId="cancel-recurring-dropdown-item"
120
- onClick={() => dispatch(cancelRecurringLogic(recurrence?.id, jobId))}
121
- key="cancel-recurring"
122
- component="button"
123
- isDisabled={
124
- recurrence?.id === undefined ||
125
- recurrence?.state === 'cancelled' ||
126
- !canEditRecurringLogic
127
- }
128
- >
129
- {__('Cancel recurring')}
130
- </DropdownItem>,
131
- ]
132
- : [];
127
+ const recurrenceDropdownItems = useMemo(
128
+ () =>
129
+ recurrence
130
+ ? [
131
+ <DropdownSeparator
132
+ ouiaId="dropdown-separator-1"
133
+ key="separator-1"
134
+ />,
135
+ <DropdownItem
136
+ ouiaId="change-enabled-recurring-dropdown-item"
137
+ onClick={() =>
138
+ dispatch(
139
+ enableRecurringLogic(recurrence?.id, recurringEnabled, jobId)
140
+ )
141
+ }
142
+ key="change-enabled-recurring"
143
+ component="button"
144
+ isDisabled={
145
+ recurrence?.id === undefined ||
146
+ recurrence?.state === 'cancelled' ||
147
+ !canEditRecurringLogic
148
+ }
149
+ >
150
+ {recurringEnabled
151
+ ? __('Disable recurring')
152
+ : __('Enable recurring')}
153
+ </DropdownItem>,
154
+ <DropdownItem
155
+ ouiaId="cancel-recurring-dropdown-item"
156
+ onClick={() =>
157
+ dispatch(cancelRecurringLogic(recurrence?.id, jobId))
158
+ }
159
+ key="cancel-recurring"
160
+ component="button"
161
+ isDisabled={
162
+ recurrence?.id === undefined ||
163
+ recurrence?.state === 'cancelled' ||
164
+ !canEditRecurringLogic
165
+ }
166
+ >
167
+ {__('Cancel recurring')}
168
+ </DropdownItem>,
169
+ ]
170
+ : [],
171
+ [recurrence, recurringEnabled, canEditRecurringLogic, dispatch, jobId]
172
+ );
173
+
174
+ const dropdownItems = useMemo(
175
+ () => [
176
+ <DropdownItem
177
+ ouiaId="rerun-succeeded-dropdown-item"
178
+ href={foremanUrl(`/job_invocations/${jobId}/rerun?succeeded_only=1`)}
179
+ key="rerun-succeeded"
180
+ isDisabled={!canCreateJobInvocations || !(succeeded > 0)}
181
+ description="Rerun job on successful hosts"
182
+ >
183
+ {__('Rerun successful')}
184
+ </DropdownItem>,
185
+ <DropdownItem
186
+ ouiaId="rerun-failed-dropdown-item"
187
+ href={foremanUrl(`/job_invocations/${jobId}/rerun?failed_only=1`)}
188
+ key="rerun-failed"
189
+ isDisabled={!canCreateJobInvocations || !(failed > 0)}
190
+ description="Rerun job on failed hosts"
191
+ >
192
+ {__('Rerun failed')}
193
+ </DropdownItem>,
194
+ <DropdownItem
195
+ ouiaId="view-task-dropdown-item"
196
+ href={foremanUrl(`/foreman_tasks/tasks/${task?.id}`)}
197
+ key="view-task"
198
+ isDisabled={!canViewForemanTasks || task === undefined}
199
+ description="See details of latest task"
200
+ >
201
+ {__('View task')}
202
+ </DropdownItem>,
203
+ <DropdownSeparator ouiaId="dropdown-separator-0" key="separator-0" />,
204
+ <DropdownItem
205
+ ouiaId="cancel-dropdown-item"
206
+ onClick={() => dispatch(cancelJob(jobId, false))}
207
+ key="cancel"
208
+ component="button"
209
+ isDisabled={!canCancelJobInvocations || !isTaskCancelable}
210
+ description="Cancel job gracefully"
211
+ >
212
+ {__('Cancel')}
213
+ </DropdownItem>,
214
+ <DropdownItem
215
+ ouiaId="abort-dropdown-item"
216
+ onClick={() => dispatch(cancelJob(jobId, true))}
217
+ key="abort"
218
+ component="button"
219
+ isDisabled={!canCancelJobInvocations || !isTaskCancelable}
220
+ description="Cancel job immediately"
221
+ >
222
+ {__('Abort')}
223
+ </DropdownItem>,
224
+ ...recurrenceDropdownItems,
225
+ <DropdownSeparator ouiaId="dropdown-separator-2" key="separator-2" />,
226
+ <DropdownItem
227
+ ouiaId="legacy-ui-dropdown-item"
228
+ icon={<UndoIcon />}
229
+ href={`/legacy/job_invocations/${jobId}`}
230
+ key="legacy-ui"
231
+ >
232
+ {__('Legacy UI')}
233
+ </DropdownItem>,
234
+ ],
235
+ [
236
+ canCancelJobInvocations,
237
+ canCreateJobInvocations,
238
+ canViewForemanTasks,
239
+ dispatch,
240
+ failed,
241
+ isTaskCancelable,
242
+ jobId,
243
+ recurrenceDropdownItems,
244
+ succeeded,
245
+ task,
246
+ ]
247
+ );
133
248
 
134
- const dropdownItems = [
135
- <DropdownItem
136
- ouiaId="rerun-succeeded-dropdown-item"
137
- href={foremanUrl(`/job_invocations/${jobId}/rerun?succeeded_only=1`)}
138
- key="rerun-succeeded"
139
- isDisabled={
140
- !useHasPermission('create_job_invocations') || !(succeeded > 0)
141
- }
142
- description="Rerun job on successful hosts"
143
- >
144
- {__('Rerun successful')}
145
- </DropdownItem>,
146
- <DropdownItem
147
- ouiaId="rerun-failed-dropdown-item"
148
- href={foremanUrl(`/job_invocations/${jobId}/rerun?failed_only=1`)}
149
- key="rerun-failed"
150
- isDisabled={!useHasPermission('create_job_invocations') || !(failed > 0)}
151
- description="Rerun job on failed hosts"
152
- >
153
- {__('Rerun failed')}
154
- </DropdownItem>,
155
- <DropdownItem
156
- ouiaId="view-task-dropdown-item"
157
- href={foremanUrl(`/foreman_tasks/tasks/${task?.id}`)}
158
- key="view-task"
159
- isDisabled={!canViewForemanTasks || task === undefined}
160
- description="See details of latest task"
161
- >
162
- {__('View task')}
163
- </DropdownItem>,
164
- <DropdownSeparator ouiaId="dropdown-separator-0" key="separator-0" />,
165
- <DropdownItem
166
- ouiaId="cancel-dropdown-item"
167
- onClick={() => dispatch(cancelJob(jobId, false))}
168
- key="cancel"
169
- component="button"
170
- isDisabled={
171
- !useHasPermission('cancel_job_invocations') || !isTaskCancelable
172
- }
173
- description="Cancel job gracefully"
174
- >
175
- {__('Cancel')}
176
- </DropdownItem>,
177
- <DropdownItem
178
- ouiaId="abort-dropdown-item"
179
- onClick={() => dispatch(cancelJob(jobId, true))}
180
- key="abort"
181
- component="button"
182
- isDisabled={
183
- !useHasPermission('cancel_job_invocations') || !isTaskCancelable
184
- }
185
- description="Cancel job immediately"
186
- >
187
- {__('Abort')}
188
- </DropdownItem>,
189
- ...recurrenceDropdownItems,
190
- <DropdownSeparator ouiaId="dropdown-separator-2" key="separator-2" />,
191
- <DropdownItem
192
- ouiaId="legacy-ui-dropdown-item"
193
- icon={<UndoIcon />}
194
- href={`/legacy/job_invocations/${jobId}`}
195
- key="legacy-ui"
196
- >
197
- {__('Legacy UI')}
198
- </DropdownItem>,
199
- ];
249
+ const dropdownToggle = useMemo(
250
+ () => (
251
+ <DropdownToggle
252
+ ouiaId="toggle-button-action-primary"
253
+ id={`toggle-split-button-action-primary-${jobId}`}
254
+ splitButtonItems={[
255
+ <Button
256
+ component="a"
257
+ ouiaId="button-rerun-all"
258
+ key="rerun"
259
+ href={foremanUrl(`/job_invocations/${jobId}/rerun`)}
260
+ variant="control"
261
+ isDisabled={!canCreateJobInvocations}
262
+ >
263
+ {__(`Rerun all`)}
264
+ </Button>,
265
+ ]}
266
+ splitButtonVariant="action"
267
+ onToggle={onActionToggle}
268
+ />
269
+ ),
270
+ [canCreateJobInvocations, jobId, onActionToggle]
271
+ );
200
272
 
201
273
  return (
202
274
  <>
@@ -206,14 +278,12 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
206
278
  component="a"
207
279
  ouiaId="button-create-report"
208
280
  className="button-create-report"
209
- href={foremanUrl(
210
- `/templates/report_templates/${reportTemplateJobId}/generate?${queryParams.toString()}`
211
- )}
281
+ href={reportHref}
212
282
  variant="secondary"
213
283
  isDisabled={
214
- !useHasPermission('generate_report_templates') ||
284
+ !canGenerateReportTemplates ||
215
285
  task?.state === STATUS.PENDING ||
216
- templateInputId === undefined
286
+ reportHref === undefined
217
287
  }
218
288
  >
219
289
  {__(`Create report`)}
@@ -224,26 +294,7 @@ const JobInvocationToolbarButtons = ({ jobId, data }) => {
224
294
  ouiaId="job-invocation-global-actions-dropdown"
225
295
  onSelect={onActionSelect}
226
296
  position={DropdownPosition.right}
227
- toggle={
228
- <DropdownToggle
229
- ouiaId="toggle-button-action-primary"
230
- id={`toggle-split-button-action-primary-${jobId}`}
231
- splitButtonItems={[
232
- <Button
233
- component="a"
234
- ouiaId="button-rerun-all"
235
- key="rerun"
236
- href={foremanUrl(`/job_invocations/${jobId}/rerun`)}
237
- variant="control"
238
- isDisabled={!useHasPermission('create_job_invocations')}
239
- >
240
- {__(`Rerun all`)}
241
- </Button>,
242
- ]}
243
- splitButtonVariant="action"
244
- onToggle={(_event, val) => setIsActionOpen(val)}
245
- />
246
- }
297
+ toggle={dropdownToggle}
247
298
  isOpen={isActionOpen}
248
299
  dropdownItems={dropdownItems}
249
300
  />
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import { createMemoryHistory } from 'history';
5
+ import { Router } from 'react-router-dom';
6
+ import JobInvocationEmptyState from '../JobInvocationEmptyState';
7
+ import { createForemanContextWrapper } from './foremanTestHelpers';
8
+
9
+ describe('JobInvocationEmptyState', () => {
10
+ it('renders the failed load empty state for a job invocation', () => {
11
+ const jobInvocationId = '99';
12
+ const errorMessage = 'Record not found';
13
+ const history = createMemoryHistory();
14
+ const ForemanContextWrapper = createForemanContextWrapper();
15
+
16
+ render(
17
+ <Router history={history}>
18
+ <ForemanContextWrapper>
19
+ <JobInvocationEmptyState
20
+ jobInvocationId={jobInvocationId}
21
+ httpStatus={404}
22
+ errorMessage={errorMessage}
23
+ />
24
+ </ForemanContextWrapper>
25
+ </Router>
26
+ );
27
+
28
+ expect(
29
+ screen.getByRole('heading', {
30
+ name: 'Unable to load job invocation',
31
+ level: 5,
32
+ })
33
+ ).toBeInTheDocument();
34
+ expect(
35
+ screen.getByText(
36
+ (_, element) =>
37
+ element?.tagName === 'P' &&
38
+ element.textContent?.includes(
39
+ `The job invocation with id ${jobInvocationId} could not be found. It may have been deleted or may not be available in your current organization or location scope.`
40
+ )
41
+ )
42
+ ).toBeInTheDocument();
43
+ expect(
44
+ screen.getByText(`Server returned: ${errorMessage}`)
45
+ ).toBeInTheDocument();
46
+ expect(
47
+ screen.getByRole('button', { name: 'Go to job invocations' })
48
+ ).toBeInTheDocument();
49
+ expect(
50
+ screen.getByRole('button', { name: 'Create a new job invocation' })
51
+ ).toBeInTheDocument();
52
+ });
53
+ });
@@ -1,9 +1,13 @@
1
+ /* eslint-disable max-lines */
1
2
  import React from 'react';
2
3
  import configureMockStore from 'redux-mock-store';
3
4
  import { fireEvent, render, screen, act } from '@testing-library/react';
4
5
  import '@testing-library/jest-dom/extend-expect';
6
+ import { createMemoryHistory } from 'history';
7
+ import { Router } from 'react-router-dom';
5
8
  import { Provider } from 'react-redux';
6
9
  import thunk from 'redux-thunk';
10
+ import { STATUS } from 'foremanReact/constants';
7
11
  import { foremanUrl } from 'foremanReact/common/helpers';
8
12
  import * as api from 'foremanReact/redux/API';
9
13
  import JobInvocationDetailPage from '../index';
@@ -11,7 +15,6 @@ import {
11
15
  jobInvocationData,
12
16
  jobInvocationDataScheduled,
13
17
  jobInvocationDataRecurring,
14
- mockPermissionsData,
15
18
  mockReportTemplatesResponse,
16
19
  mockReportTemplateInputsResponse,
17
20
  } from './fixtures';
@@ -26,8 +29,10 @@ import {
26
29
  CHANGE_ENABLED_RECURRING_LOGIC,
27
30
  GET_REPORT_TEMPLATES,
28
31
  GET_REPORT_TEMPLATE_INPUTS,
32
+ GET_TASK,
29
33
  JOB_INVOCATION_KEY,
30
34
  } from '../JobInvocationConstants';
35
+ import { createForemanContextWrapper } from './foremanTestHelpers';
31
36
 
32
37
  jest.spyOn(api, 'get');
33
38
 
@@ -35,8 +40,11 @@ jest.spyOn(api, 'get');
35
40
  const originalToLocaleString = Date.prototype.toLocaleString;
36
41
  beforeAll(() => {
37
42
  // eslint-disable-next-line no-extend-native
38
- Date.prototype.toLocaleString = function (locale, options) {
39
- return originalToLocaleString.call(this, locale, { ...options, timeZone: 'UTC' });
43
+ Date.prototype.toLocaleString = function toLocaleStringUTC(locale, options) {
44
+ return originalToLocaleString.call(this, locale, {
45
+ ...options,
46
+ timeZone: 'UTC',
47
+ });
40
48
  };
41
49
  });
42
50
  afterAll(() => {
@@ -44,12 +52,6 @@ afterAll(() => {
44
52
  Date.prototype.toLocaleString = originalToLocaleString;
45
53
  });
46
54
 
47
- jest.mock('foremanReact/common/hooks/API/APIHooks', () => ({
48
- useAPI: jest.fn(() => ({
49
- response: mockPermissionsData,
50
- })),
51
- }));
52
-
53
55
  jest.mock('foremanReact/routes/common/PageLayout/PageLayout', () =>
54
56
  jest.fn(props => (
55
57
  <div>
@@ -63,6 +65,7 @@ jest.mock('foremanReact/routes/common/PageLayout/PageLayout', () =>
63
65
  const initialState = {
64
66
  JOB_INVOCATION_KEY: {
65
67
  response: jobInvocationData,
68
+ status: STATUS.RESOLVED,
66
69
  },
67
70
  GET_REPORT_TEMPLATES: mockReportTemplatesResponse,
68
71
  extendable: {},
@@ -71,6 +74,7 @@ const initialState = {
71
74
  const initialStateScheduled = {
72
75
  JOB_INVOCATION_KEY: {
73
76
  response: jobInvocationDataScheduled,
77
+ status: STATUS.RESOLVED,
74
78
  },
75
79
  extendable: {},
76
80
  };
@@ -91,6 +95,15 @@ api.get.mockImplementation(({ handleSuccess, ...action }) => {
91
95
  return { type: 'get', ...action };
92
96
  });
93
97
 
98
+ const initialStateRecurring = {
99
+ JOB_INVOCATION_KEY: {
100
+ response: jobInvocationDataRecurring,
101
+ status: STATUS.RESOLVED,
102
+ },
103
+ GET_REPORT_TEMPLATES: mockReportTemplatesResponse,
104
+ extendable: {},
105
+ };
106
+
94
107
  jest.mock('../JobInvocationHostTable.js', () => () => (
95
108
  <div data-testid="mock-table">Mock Table</div>
96
109
  ));
@@ -198,6 +211,29 @@ describe('JobInvocationDetailPage', () => {
198
211
  ).toEqual(`/legacy/job_invocations/${jobId}`);
199
212
  });
200
213
 
214
+ it('keeps toolbar buttons mounted while job invocation data is refreshing', async () => {
215
+ const jobId = jobInvocationData.id;
216
+ const store = mockStore({
217
+ ...initialState,
218
+ JOB_INVOCATION_KEY: {
219
+ response: jobInvocationData,
220
+ status: STATUS.PENDING,
221
+ },
222
+ });
223
+
224
+ render(
225
+ <Provider store={store}>
226
+ <JobInvocationDetailPage
227
+ match={{ params: { id: `${jobId}` } }}
228
+ {...props}
229
+ />
230
+ </Provider>
231
+ );
232
+
233
+ expect(screen.getByText('Create report')).toBeInTheDocument();
234
+ expect(screen.getByText('Rerun all')).toBeInTheDocument();
235
+ });
236
+
201
237
  it('shows scheduled date', async () => {
202
238
  const store = mockStore(initialStateScheduled);
203
239
  render(
@@ -217,8 +253,9 @@ describe('JobInvocationDetailPage', () => {
217
253
  it('should dispatch global actions', async () => {
218
254
  // recurring in the future
219
255
  const jobId = jobInvocationDataRecurring.id;
256
+ const taskId = jobInvocationDataRecurring.task.id;
220
257
  const recurrenceId = jobInvocationDataRecurring.recurrence.id;
221
- const store = mockStore(jobInvocationDataRecurring);
258
+ const store = mockStore(initialStateRecurring);
222
259
  render(
223
260
  <Provider store={store}>
224
261
  <JobInvocationDetailPage
@@ -232,7 +269,11 @@ describe('JobInvocationDetailPage', () => {
232
269
  { key: GET_REPORT_TEMPLATES, url: '/api/report_templates' },
233
270
  {
234
271
  key: JOB_INVOCATION_KEY,
235
- url: `/api/job_invocations/${jobId}?host_status=true`,
272
+ url: `/api/job_invocations/${jobId}`,
273
+ },
274
+ {
275
+ key: GET_TASK,
276
+ url: `/foreman_tasks/api/tasks/${taskId}`,
236
277
  },
237
278
  {
238
279
  key: GET_REPORT_TEMPLATE_INPUTS,
@@ -281,4 +322,61 @@ describe('JobInvocationDetailPage', () => {
281
322
  }
282
323
  });
283
324
  });
325
+
326
+ it('renders empty state when the job invocation fails to load', () => {
327
+ const jobId = '99';
328
+ const errorMessage = 'Record not found';
329
+ const history = createMemoryHistory();
330
+ const ForemanContextWrapper = createForemanContextWrapper();
331
+ const store = mockStore({
332
+ JOB_INVOCATION_KEY: {
333
+ status: STATUS.ERROR,
334
+ response: {
335
+ message: errorMessage,
336
+ response: { status: 404 },
337
+ },
338
+ },
339
+ extendable: {},
340
+ });
341
+
342
+ render(
343
+ <Router history={history}>
344
+ <ForemanContextWrapper>
345
+ <Provider store={store}>
346
+ <JobInvocationDetailPage
347
+ match={{ params: { id: jobId } }}
348
+ history={history}
349
+ />
350
+ </Provider>
351
+ </ForemanContextWrapper>
352
+ </Router>
353
+ );
354
+
355
+ expect(
356
+ screen.getByRole('heading', {
357
+ name: 'Unable to load job invocation',
358
+ level: 5,
359
+ })
360
+ ).toBeInTheDocument();
361
+ expect(
362
+ screen.getByText(
363
+ (_, element) =>
364
+ element?.tagName === 'P' &&
365
+ element.textContent?.includes(
366
+ `The job invocation with id ${jobId} could not be found. It may have been deleted or may not be available in your current organization or location scope.`
367
+ )
368
+ )
369
+ ).toBeInTheDocument();
370
+ expect(
371
+ screen.getByText(`Server returned: ${errorMessage}`)
372
+ ).toBeInTheDocument();
373
+ expect(
374
+ screen.getByRole('button', { name: 'Go to job invocations' })
375
+ ).toBeInTheDocument();
376
+ expect(
377
+ screen.getByRole('button', { name: 'Create a new job invocation' })
378
+ ).toBeInTheDocument();
379
+ expect(screen.queryByText('Description')).not.toBeInTheDocument();
380
+ expect(screen.queryByTestId('mock-table')).not.toBeInTheDocument();
381
+ });
284
382
  });