foreman-tasks 13.0.0 → 13.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
  3. data/lib/foreman_tasks/version.rb +1 -1
  4. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
  5. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
  6. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
  7. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
  8. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +236 -131
  9. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -33
  10. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -67
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
  13. data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
  14. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +48 -51
  15. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +0 -39
  16. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +0 -5
  17. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
  18. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +99 -0
  19. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +129 -8
  20. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
  21. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
  22. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +10 -33
  23. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
  24. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +100 -88
  25. data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +7 -8
  26. data/webpack/Routes/routes.js +1 -1
  27. data/webpack/Routes/routes.test.js +1 -1
  28. data/webpack/index.js +0 -5
  29. metadata +8 -2
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
2
3
  import { render, screen } from '@testing-library/react';
3
4
  import '@testing-library/jest-dom';
4
5
  import { configureStore } from '@reduxjs/toolkit';
@@ -7,7 +8,13 @@ import { createMemoryHistory } from 'history';
7
8
  import { Router } from 'react-router-dom';
8
9
 
9
10
  import TaskDetails from '../TaskDetails';
10
- import { minProps } from './TaskDetails.fixtures';
11
+ import {
12
+ fixtureFailedExecutionDetail,
13
+ fixtureWithDependencies,
14
+ fixtureWithOverviewMessages,
15
+ minProps,
16
+ taskDetailsWithExecutionTabDefaults,
17
+ } from './TaskDetails.fixtures';
11
18
  import { VIEW_FOREMAN_TASKS } from '../TaskDetailsConstants';
12
19
 
13
20
  const mockUseForemanPermissions = jest.fn(
@@ -45,6 +52,10 @@ describe('TaskDetails', () => {
45
52
  );
46
53
  });
47
54
 
55
+ afterEach(() => {
56
+ jest.clearAllMocks();
57
+ });
58
+
48
59
  it(`shows ResourceLoadFailedEmptyState when ${VIEW_FOREMAN_TASKS} is absent`, () => {
49
60
  mockUseForemanPermissions.mockImplementation(() => new Set());
50
61
 
@@ -111,25 +122,135 @@ describe('TaskDetails', () => {
111
122
  expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
112
123
  });
113
124
 
114
- it('shows skeleton while loading on the Task tab', () => {
125
+ it('shows skeleton in the overview while loading', () => {
115
126
  const { container } = renderTaskDetails({ isLoading: true });
116
127
  expect(
117
128
  container.querySelector('.react-loading-skeleton')
118
129
  ).toBeInTheDocument();
119
130
  });
120
131
 
121
- it('renders six tabs with expected labels', () => {
132
+ it('renders four tabs with expected labels', () => {
122
133
  renderTaskDetails();
123
- expect(document.getElementById('task-details-tabs')).toBeInTheDocument();
124
- expect(screen.getByRole('tab', { name: /^task$/i })).toBeInTheDocument();
125
134
  expect(
126
- screen.getByRole('tab', { name: /running steps/i })
135
+ screen.getByRole('tab', { name: /execution details/i })
127
136
  ).toBeInTheDocument();
128
- expect(screen.getByRole('tab', { name: /errors/i })).toBeInTheDocument();
129
- expect(screen.getByRole('tab', { name: /locks/i })).toBeInTheDocument();
130
137
  expect(
131
138
  screen.getByRole('tab', { name: /dependencies/i })
132
139
  ).toBeInTheDocument();
140
+ expect(screen.getByRole('tab', { name: /locks/i })).toBeInTheDocument();
133
141
  expect(screen.getByRole('tab', { name: /raw/i })).toBeInTheDocument();
134
142
  });
143
+
144
+ it('exposes stable ouiaIds on tabs for automation', () => {
145
+ renderTaskDetails();
146
+
147
+ expect(
148
+ document.querySelector(
149
+ '[data-ouia-component-id="task-details-tab-execution-details"]'
150
+ )
151
+ ).toBeInTheDocument();
152
+ expect(
153
+ document.querySelector(
154
+ '[data-ouia-component-id="task-details-tab-dependencies"]'
155
+ )
156
+ ).toBeInTheDocument();
157
+ expect(
158
+ document.querySelector(
159
+ '[data-ouia-component-id="task-details-tab-locks"]'
160
+ )
161
+ ).toBeInTheDocument();
162
+ expect(
163
+ document.querySelector('[data-ouia-component-id="task-details-tab-raw"]')
164
+ ).toBeInTheDocument();
165
+ });
166
+
167
+ it('shows failed step errors on the default execution details tab', () => {
168
+ renderTaskDetails({
169
+ ...fixtureFailedExecutionDetail,
170
+ executionPlan: fixtureFailedExecutionDetail.executionPlan,
171
+ failedSteps: fixtureFailedExecutionDetail.failedSteps,
172
+ });
173
+
174
+ expect(
175
+ screen.getByRole('tab', {
176
+ name: /action actions::katello::eventqueue::monitor is already active/i,
177
+ })
178
+ ).toBeInTheDocument();
179
+ expect(screen.getByLabelText(/failed task errors/i)).toBeInTheDocument();
180
+ });
181
+
182
+ it('starts task reload on mount and stops on unmount', () => {
183
+ const taskReloadStart = jest.fn();
184
+ const taskReloadStop = jest.fn();
185
+
186
+ const { unmount } = renderTaskDetails({
187
+ ...taskDetailsWithExecutionTabDefaults,
188
+ taskReloadStart,
189
+ taskReloadStop,
190
+ });
191
+
192
+ expect(taskReloadStart).toHaveBeenCalledWith(
193
+ 'a15dd820-32f1-4ced-9ab7-c0fab8234c47'
194
+ );
195
+ unmount();
196
+ expect(taskReloadStop).toHaveBeenCalled();
197
+ });
198
+
199
+ it('shows execution details panel on the default tab when loaded', () => {
200
+ renderTaskDetails({ ...taskDetailsWithExecutionTabDefaults });
201
+
202
+ expect(
203
+ screen.getByRole('heading', { name: /^no errors found$/i })
204
+ ).toBeInTheDocument();
205
+ });
206
+
207
+ it('disables tabs and hides execution panel while loading', () => {
208
+ renderTaskDetails({
209
+ ...taskDetailsWithExecutionTabDefaults,
210
+ isLoading: true,
211
+ });
212
+
213
+ expect(
214
+ screen.queryByRole('heading', { name: /^no errors found$/i })
215
+ ).not.toBeInTheDocument();
216
+ expect(
217
+ screen.getByRole('tab', { name: /execution details/i })
218
+ ).toBeDisabled();
219
+ expect(screen.getByRole('tab', { name: /dependencies/i })).toBeDisabled();
220
+ expect(screen.getByRole('tab', { name: /locks/i })).toBeDisabled();
221
+ expect(screen.getByRole('tab', { name: /raw/i })).toBeDisabled();
222
+ });
223
+
224
+ it('renders troubleshooting help in overview after expanding details', async () => {
225
+ renderTaskDetails({ ...fixtureWithOverviewMessages });
226
+
227
+ await userEvent.click(
228
+ screen.getByRole('button', { name: /show more details/i })
229
+ );
230
+
231
+ expect(screen.getByText('Troubleshooting')).toBeInTheDocument();
232
+ expect(screen.getByText('See logs')).toBeInTheDocument();
233
+ });
234
+
235
+ it('renders raw output when the Raw tab is selected', async () => {
236
+ renderTaskDetails({ ...fixtureWithOverviewMessages });
237
+
238
+ await userEvent.click(screen.getByRole('tab', { name: /raw/i }));
239
+
240
+ expect(screen.getByText(/raw output/i)).toBeInTheDocument();
241
+ expect(screen.getByText(/partial output/i)).toBeInTheDocument();
242
+ });
243
+
244
+ it('renders dependency tables when the Dependencies tab is selected', async () => {
245
+ renderTaskDetails({ ...fixtureWithDependencies });
246
+
247
+ await userEvent.click(screen.getByRole('tab', { name: /dependencies/i }));
248
+
249
+ expect(
250
+ screen.getByRole('link', { name: 'Foo Bar Action' })
251
+ ).toBeInTheDocument();
252
+ expect(
253
+ screen.getByRole('link', { name: 'Test Action' })
254
+ ).toBeInTheDocument();
255
+ });
135
256
  });
@@ -0,0 +1,174 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ Flex,
5
+ FlexItem,
6
+ Split,
7
+ SplitItem,
8
+ Title,
9
+ } from '@patternfly/react-core';
10
+ import { translate as __ } from 'foremanReact/common/I18n';
11
+ import { STATUS } from 'foremanReact/constants';
12
+ import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
13
+ import { TaskButtons } from '../../Components/TaskDetails/Components/TaskButtons';
14
+ import { VIEW_FOREMAN_TASKS } from '../../Components/TaskDetails/TaskDetailsConstants';
15
+ import {
16
+ ForceUnlockConfirmationModal,
17
+ UnlockConfirmationModal,
18
+ } from '../../Components/common/ClickConfirmation';
19
+ import { taskResultIconEl } from '../../Components/common/taskResultIcon';
20
+ import './TaskDetailsHeader.scss';
21
+
22
+ const TitleComponent = ({ action, state, result }) => (
23
+ <Flex
24
+ alignItems={{ default: 'alignItemsCenter' }}
25
+ spaceItems={{ default: 'spaceItemsXs' }}
26
+ >
27
+ <FlexItem>
28
+ <Title
29
+ headingLevel="h1"
30
+ className="foreman-tasks-task-details-header__title"
31
+ ouiaId="task-overview-title"
32
+ >
33
+ {action || __('Task Details')}
34
+ </Title>
35
+ </FlexItem>
36
+ <FlexItem>{taskResultIconEl(state, result)}</FlexItem>
37
+ </Flex>
38
+ );
39
+
40
+ TitleComponent.propTypes = {
41
+ action: PropTypes.string,
42
+ state: PropTypes.string,
43
+ result: PropTypes.string,
44
+ };
45
+
46
+ TitleComponent.defaultProps = {
47
+ action: '',
48
+ state: '',
49
+ result: '',
50
+ };
51
+
52
+ const TaskDetailsHeader = props => {
53
+ const {
54
+ taskReload,
55
+ id,
56
+ forceCancelTaskRequest,
57
+ unlockTaskRequest,
58
+ action,
59
+ taskReloadStart,
60
+ taskReloadStop,
61
+ executionPlan,
62
+ apiStatus,
63
+ } = props;
64
+ const hasViewPermission = usePermissions([VIEW_FOREMAN_TASKS]);
65
+ const showActions = hasViewPermission && apiStatus !== STATUS.ERROR;
66
+ const resumable = executionPlan ? executionPlan.state === 'paused' : false;
67
+ const cancellable = executionPlan ? executionPlan.cancellable : false;
68
+
69
+ const forceUnlock = () => {
70
+ if (!taskReload) {
71
+ taskReloadStart(id);
72
+ }
73
+
74
+ forceCancelTaskRequest(id, action);
75
+ };
76
+
77
+ const unlock = () => {
78
+ if (!taskReload) {
79
+ taskReloadStart(id);
80
+ }
81
+
82
+ unlockTaskRequest(id, action);
83
+ };
84
+
85
+ const taskProgressToggle = () => {
86
+ if (taskReload) {
87
+ taskReloadStop();
88
+ } else {
89
+ taskReloadStart(id);
90
+ }
91
+ };
92
+
93
+ const [unlockModalOpen, setUnlockModalOpen] = useState(false);
94
+ const [forceUnlockModalOpen, setForceUnlockModalOpen] = useState(false);
95
+
96
+ return (
97
+ <React.Fragment>
98
+ {showActions && (
99
+ <>
100
+ <UnlockConfirmationModal
101
+ onClick={unlock}
102
+ isOpen={unlockModalOpen}
103
+ setModalClosed={() => setUnlockModalOpen(false)}
104
+ />
105
+ <ForceUnlockConfirmationModal
106
+ onClick={forceUnlock}
107
+ isOpen={forceUnlockModalOpen}
108
+ setModalClosed={() => setForceUnlockModalOpen(false)}
109
+ />
110
+ </>
111
+ )}
112
+ <Split
113
+ className="foreman-tasks-task-details-header"
114
+ hasGutter
115
+ isWrappable
116
+ data-ouia-component-id="foreman-tasks-task-details-title-row"
117
+ >
118
+ <SplitItem isFilled>
119
+ <TitleComponent
120
+ action={props.action}
121
+ state={props.state}
122
+ result={props.result}
123
+ />
124
+ </SplitItem>
125
+ {showActions && (
126
+ <SplitItem>
127
+ <TaskButtons
128
+ {...props}
129
+ taskReloadStart={taskReloadStart}
130
+ taskProgressToggle={taskProgressToggle}
131
+ setUnlockModalOpen={setUnlockModalOpen}
132
+ setForceUnlockModalOpen={setForceUnlockModalOpen}
133
+ resumable={resumable}
134
+ cancellable={cancellable}
135
+ />
136
+ </SplitItem>
137
+ )}
138
+ </Split>
139
+ </React.Fragment>
140
+ );
141
+ };
142
+
143
+ TaskDetailsHeader.propTypes = {
144
+ taskReload: PropTypes.bool,
145
+ id: PropTypes.string,
146
+ forceCancelTaskRequest: PropTypes.func,
147
+ unlockTaskRequest: PropTypes.func,
148
+ action: PropTypes.string,
149
+ state: PropTypes.string,
150
+ result: PropTypes.string,
151
+ taskReloadStart: PropTypes.func,
152
+ taskReloadStop: PropTypes.func,
153
+ executionPlan: PropTypes.shape({
154
+ state: PropTypes.string,
155
+ cancellable: PropTypes.bool,
156
+ }),
157
+ apiStatus: PropTypes.oneOf(Object.keys(STATUS)),
158
+ };
159
+
160
+ TaskDetailsHeader.defaultProps = {
161
+ taskReload: false,
162
+ id: '',
163
+ forceCancelTaskRequest: () => null,
164
+ unlockTaskRequest: () => null,
165
+ action: '',
166
+ state: '',
167
+ result: '',
168
+ taskReloadStart: () => null,
169
+ taskReloadStop: () => null,
170
+ executionPlan: {},
171
+ apiStatus: STATUS.RESOLVED,
172
+ };
173
+
174
+ export default TaskDetailsHeader;
@@ -0,0 +1,5 @@
1
+ .foreman-tasks-task-details-header {
2
+ .foreman-tasks-task-details-header__title {
3
+ margin: 0;
4
+ }
5
+ }
@@ -1,48 +1,20 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { useSelector } from 'react-redux';
4
- import { Flex, FlexItem, TextContent, Text } from '@patternfly/react-core';
5
3
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
6
4
  import { translate as __, sprintf } from 'foremanReact/common/I18n';
7
- import TaskDetails from '../../Components/TaskDetails';
8
- import {
9
- selectAction,
10
- selectResult,
11
- selectState,
12
- } from '../../Components/TaskDetails/TaskDetailsSelectors';
13
- import { taskResultIconEl } from '../../Components/common/taskResultIcon';
5
+ import TaskDetails from '../../Components/TaskDetails/TaskDetails';
6
+ import TaskDetailsHeader from './TaskDetailsHeader';
14
7
 
15
8
  const TaskDetailsPage = props => {
16
- const { id } = props.match.params;
17
- const action = useSelector(selectAction);
18
- const taskState = useSelector(selectState);
19
- const taskResult = useSelector(selectResult);
9
+ const { match, action } = props;
10
+ const { id } = match.params;
20
11
  const headerText = action
21
12
  ? sprintf(__('Details of %s task'), action)
22
13
  : __('Task Details');
23
14
 
24
- const header = (
25
- <Flex
26
- component="span"
27
- data-ouia-component-id="foreman-tasks-task-details-title-row"
28
- display={{ default: 'inlineFlex' }}
29
- alignItems={{ default: 'alignItemsCenter' }}
30
- gap={{ default: 'gapSm' }}
31
- >
32
- <TextContent>
33
- <Text ouiaId="breadcrumb_title" component="h1">
34
- {headerText}
35
- </Text>
36
- </TextContent>
37
- <FlexItem component="span">
38
- {taskResultIconEl(taskState, taskResult)}
39
- </FlexItem>
40
- </Flex>
41
- );
42
-
43
15
  return (
44
16
  <PageLayout
45
- customHeader={header}
17
+ customHeader={<TaskDetailsHeader {...props} />}
46
18
  header={headerText}
47
19
  searchable={false}
48
20
  breadcrumbOptions={{
@@ -69,6 +41,11 @@ TaskDetailsPage.propTypes = {
69
41
  id: PropTypes.string.isRequired,
70
42
  }).isRequired,
71
43
  }).isRequired,
44
+ action: PropTypes.string,
45
+ };
46
+
47
+ TaskDetailsPage.defaultProps = {
48
+ action: '',
72
49
  };
73
50
 
74
51
  export default TaskDetailsPage;
@@ -0,0 +1,142 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { STATUS } from 'foremanReact/constants';
5
+
6
+ import TaskDetailsHeader from '../TaskDetailsHeader';
7
+ import { VIEW_FOREMAN_TASKS } from '../../../Components/TaskDetails/TaskDetailsConstants';
8
+
9
+ const mockUseForemanPermissions = jest.fn(
10
+ () => new Set(['view_foreman_tasks'])
11
+ );
12
+
13
+ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
14
+ ...jest.requireActual('foremanReact/Root/Context/ForemanContext'),
15
+ useForemanPermissions: (...args) => mockUseForemanPermissions(...args),
16
+ }));
17
+
18
+ const baseHeaderProps = {
19
+ id: 'test-id',
20
+ action: 'Refresh hosts',
21
+ state: 'running',
22
+ result: 'pending',
23
+ taskReload: false,
24
+ taskReloadStart: jest.fn(),
25
+ taskReloadStop: jest.fn(),
26
+ forceCancelTaskRequest: jest.fn(),
27
+ unlockTaskRequest: jest.fn(),
28
+ cancelTaskRequest: jest.fn(),
29
+ resumeTaskRequest: jest.fn(),
30
+ canEdit: true,
31
+ dynflowEnableConsole: true,
32
+ externalId: 'ext-99',
33
+ executionPlan: {},
34
+ apiStatus: STATUS.RESOLVED,
35
+ };
36
+
37
+ const expectHeaderActionsVisible = () => {
38
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
39
+ expect(
40
+ screen.getByRole('link', { name: /dynflow console/i })
41
+ ).toBeInTheDocument();
42
+ expect(
43
+ screen.getByRole('button', { name: /^task actions$/i })
44
+ ).toBeInTheDocument();
45
+ };
46
+
47
+ const expectHeaderActionsHidden = () => {
48
+ expect(
49
+ screen.queryByRole('button', { name: /cancel/i })
50
+ ).not.toBeInTheDocument();
51
+ expect(
52
+ screen.queryByRole('link', { name: /dynflow console/i })
53
+ ).not.toBeInTheDocument();
54
+ expect(
55
+ screen.queryByRole('button', { name: /^task actions$/i })
56
+ ).not.toBeInTheDocument();
57
+ };
58
+
59
+ describe('TaskDetailsHeader', () => {
60
+ beforeEach(() => {
61
+ mockUseForemanPermissions.mockImplementation(
62
+ () => new Set(['view_foreman_tasks'])
63
+ );
64
+ });
65
+
66
+ afterEach(() => {
67
+ jest.clearAllMocks();
68
+ });
69
+
70
+ it('renders action heading and exposes cancel, dynflow link, and task actions toggle', () => {
71
+ render(<TaskDetailsHeader {...baseHeaderProps} />);
72
+
73
+ expect(
74
+ screen.getByRole('heading', { level: 1, name: 'Refresh hosts' })
75
+ ).toBeInTheDocument();
76
+ expectHeaderActionsVisible();
77
+ });
78
+
79
+ it('shows running status icon next to the title when task state is running', () => {
80
+ render(
81
+ <TaskDetailsHeader
82
+ {...baseHeaderProps}
83
+ action="Running job"
84
+ state="running"
85
+ result="pending"
86
+ />
87
+ );
88
+
89
+ expect(
90
+ screen.getByRole('heading', { level: 1, name: 'Running job' })
91
+ ).toBeInTheDocument();
92
+ expect(screen.getByTitle('Running')).toBeInTheDocument();
93
+ });
94
+
95
+ it('shows error status icon next to the title when task stopped with error', () => {
96
+ render(
97
+ <TaskDetailsHeader
98
+ {...baseHeaderProps}
99
+ action="Failed job"
100
+ state="stopped"
101
+ result="error"
102
+ />
103
+ );
104
+
105
+ expect(
106
+ screen.getByRole('heading', { level: 1, name: 'Failed job' })
107
+ ).toBeInTheDocument();
108
+ expect(screen.getByTitle('Error')).toBeInTheDocument();
109
+ });
110
+
111
+ it('uses generic title when action is unset', () => {
112
+ render(<TaskDetailsHeader {...baseHeaderProps} action="" />);
113
+
114
+ expect(
115
+ screen.getByRole('heading', { level: 1, name: 'Task Details' })
116
+ ).toBeInTheDocument();
117
+ });
118
+
119
+ it.each([
120
+ [
121
+ `${VIEW_FOREMAN_TASKS} is absent`,
122
+ () => {
123
+ mockUseForemanPermissions.mockImplementation(() => new Set());
124
+ },
125
+ {},
126
+ ],
127
+ [
128
+ 'apiStatus is ERROR',
129
+ () => {},
130
+ { apiStatus: STATUS.ERROR },
131
+ ],
132
+ ])('hides task actions when %s', (_label, setupPermissions, propOverrides) => {
133
+ setupPermissions();
134
+
135
+ render(<TaskDetailsHeader {...baseHeaderProps} {...propOverrides} />);
136
+
137
+ expect(
138
+ screen.getByRole('heading', { level: 1, name: 'Refresh hosts' })
139
+ ).toBeInTheDocument();
140
+ expectHeaderActionsHidden();
141
+ });
142
+ });