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,5 +1,3 @@
1
- import { testReducerSnapshotWithFixtures } from '@theforeman/test';
2
-
3
1
  import {
4
2
  FOREMAN_TASKS_DASHBOARD_INIT,
5
3
  FOREMAN_TASKS_DASHBOARD_UPDATE_TIME,
@@ -7,52 +5,120 @@ import {
7
5
  FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_REQUEST,
8
6
  FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_SUCCESS,
9
7
  FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_FAILURE,
8
+ TASKS_DASHBOARD_AVAILABLE_QUERY_STATES,
9
+ TASKS_DASHBOARD_AVAILABLE_TIMES,
10
+ TASKS_SUMMARY_ZERO,
10
11
  } from '../TasksDashboardConstants';
11
12
  import reducer from '../TasksDashboardReducer';
12
13
 
13
- const fixtures = {
14
- 'should return the initial state': {},
15
- 'should handle FOREMAN_TASKS_DASHBOARD_INIT': {
16
- action: {
17
- type: FOREMAN_TASKS_DASHBOARD_INIT,
18
- },
19
- },
20
- 'should handle FOREMAN_TASKS_DASHBOARD_INIT with data': {
21
- action: {
22
- type: FOREMAN_TASKS_DASHBOARD_INIT,
23
- payload: { time: 'some-time', query: 'some-query' },
24
- },
25
- },
26
- 'should handle FOREMAN_TASKS_DASHBOARD_UPDATE_TIME': {
27
- action: {
28
- type: FOREMAN_TASKS_DASHBOARD_UPDATE_TIME,
29
- payload: 'some-time',
30
- },
31
- },
32
- 'should handle FOREMAN_TASKS_DASHBOARD_UPDATE_QUERY': {
33
- action: {
34
- type: FOREMAN_TASKS_DASHBOARD_UPDATE_QUERY,
35
- payload: 'some-query',
36
- },
37
- },
38
- 'should handle FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_REQUEST': {
39
- action: {
40
- type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_REQUEST,
41
- },
42
- },
43
- 'should handle FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_SUCCESS': {
44
- action: {
45
- type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_SUCCESS,
46
- payload: 'some-payload',
47
- },
48
- },
49
- 'should handle FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_FAILURE': {
50
- action: {
51
- type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_FAILURE,
52
- payload: new Error('some error'),
53
- },
54
- },
14
+ const dashboardQuery = {
15
+ state: TASKS_DASHBOARD_AVAILABLE_QUERY_STATES.RUNNING,
16
+ };
17
+
18
+ const initialState = {
19
+ time: TASKS_DASHBOARD_AVAILABLE_TIMES.H24,
20
+ isLoadingTasksSummary: false,
21
+ error: null,
22
+ tasksSummary: TASKS_SUMMARY_ZERO,
55
23
  };
56
24
 
57
- describe('BreadcrumbBar reducer', () =>
58
- testReducerSnapshotWithFixtures(reducer, fixtures));
25
+ const runReducer = (state, action) =>
26
+ reducer(state, action).asMutable({ deep: true });
27
+
28
+ describe('TasksDashboard reducer', () => {
29
+ it('should return the initial state', () => {
30
+ expect(runReducer(undefined, {})).toEqual(initialState);
31
+ });
32
+
33
+ it('should handle FOREMAN_TASKS_DASHBOARD_INIT', () => {
34
+ expect(
35
+ runReducer(undefined, {
36
+ type: FOREMAN_TASKS_DASHBOARD_INIT,
37
+ })
38
+ ).toEqual({
39
+ ...initialState,
40
+ query: undefined,
41
+ });
42
+ });
43
+
44
+ it('should handle FOREMAN_TASKS_DASHBOARD_INIT with data', () => {
45
+ expect(
46
+ runReducer(undefined, {
47
+ type: FOREMAN_TASKS_DASHBOARD_INIT,
48
+ payload: {
49
+ time: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK,
50
+ query: dashboardQuery,
51
+ },
52
+ })
53
+ ).toEqual({
54
+ ...initialState,
55
+ time: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK,
56
+ query: dashboardQuery,
57
+ });
58
+ });
59
+
60
+ it('should handle FOREMAN_TASKS_DASHBOARD_UPDATE_TIME', () => {
61
+ expect(
62
+ runReducer(undefined, {
63
+ type: FOREMAN_TASKS_DASHBOARD_UPDATE_TIME,
64
+ payload: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK,
65
+ })
66
+ ).toEqual({
67
+ ...initialState,
68
+ time: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK,
69
+ });
70
+ });
71
+
72
+ it('should handle FOREMAN_TASKS_DASHBOARD_UPDATE_QUERY', () => {
73
+ expect(
74
+ runReducer(undefined, {
75
+ type: FOREMAN_TASKS_DASHBOARD_UPDATE_QUERY,
76
+ payload: dashboardQuery,
77
+ })
78
+ ).toEqual({
79
+ ...initialState,
80
+ query: dashboardQuery,
81
+ });
82
+ });
83
+
84
+ it('should handle FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_REQUEST', () => {
85
+ expect(
86
+ runReducer(undefined, {
87
+ type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_REQUEST,
88
+ })
89
+ ).toEqual({
90
+ ...initialState,
91
+ isLoading: true,
92
+ });
93
+ });
94
+
95
+ it('should handle FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_SUCCESS', () => {
96
+ expect(
97
+ runReducer(undefined, {
98
+ type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_SUCCESS,
99
+ payload: 'some-payload',
100
+ })
101
+ ).toEqual({
102
+ ...initialState,
103
+ tasksSummary: 'some-payload',
104
+ isLoading: false,
105
+ error: null,
106
+ });
107
+ });
108
+
109
+ it('should handle FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_FAILURE', () => {
110
+ const error = new Error('some error');
111
+
112
+ expect(
113
+ runReducer(undefined, {
114
+ type: FOREMAN_TASKS_DASHBOARD_FETCH_TASKS_SUMMARY_FAILURE,
115
+ payload: error,
116
+ })
117
+ ).toEqual({
118
+ ...initialState,
119
+ tasksSummary: TASKS_SUMMARY_ZERO,
120
+ isLoading: false,
121
+ error,
122
+ });
123
+ });
124
+ });
@@ -1,4 +1,3 @@
1
- import { testSelectorsSnapshotWithFixtures } from '@theforeman/test';
2
1
  import {
3
2
  selectTasksDashboard,
4
3
  selectTime,
@@ -6,6 +5,7 @@ import {
6
5
  selectTasksSummary,
7
6
  calcStoppedOther,
8
7
  } from '../TasksDashboardSelectors';
8
+ import { TASKS_DASHBOARD_AVAILABLE_TIMES } from '../TasksDashboardConstants';
9
9
 
10
10
  const stoppedResult = {
11
11
  error: {
@@ -55,19 +55,107 @@ const state = {
55
55
  },
56
56
  };
57
57
 
58
- const fixtures = {
59
- 'should select tasks-dashboard': () => selectTasksDashboard(state),
60
- 'should select tasks-dashboard when state is empty': () =>
61
- selectTasksDashboard({}),
62
- 'should select time': () => selectTime(state),
63
- 'should select time when state is empty': () => selectTime({}),
64
- 'should select query': () => selectQuery(state),
65
- 'should select query when state is empty': () => selectQuery({}),
66
- 'should select tasks-summary': () => selectTasksSummary(state),
67
- 'should select tasks-summary when state is empty': () =>
68
- selectTasksSummary({}),
69
- 'should calcStoppedOther': () => calcStoppedOther(stoppedResult),
58
+ const emptyTasksSummary = {
59
+ paused: {
60
+ last: 0,
61
+ older: 0,
62
+ },
63
+ running: {
64
+ last: 0,
65
+ older: 0,
66
+ },
67
+ scheduled: 0,
68
+ stopped: {
69
+ other: 0,
70
+ results: {
71
+ error: {
72
+ last: 0,
73
+ total: 0,
74
+ },
75
+ success: {
76
+ last: 0,
77
+ total: 0,
78
+ },
79
+ warning: {
80
+ last: 0,
81
+ total: 0,
82
+ },
83
+ },
84
+ },
70
85
  };
71
86
 
72
- describe('TasksDashboard - Selectors', () =>
73
- testSelectorsSnapshotWithFixtures(fixtures));
87
+ describe('TasksDashboard - Selectors', () => {
88
+ it('should select tasks-dashboard', () => {
89
+ expect(selectTasksDashboard(state)).toEqual(
90
+ state.foremanTasks.tasksDashboard
91
+ );
92
+ });
93
+
94
+ it('should select tasks-dashboard when state is empty', () => {
95
+ expect(selectTasksDashboard({})).toEqual({});
96
+ });
97
+
98
+ it('should select time', () => {
99
+ expect(selectTime(state)).toBe('some-time');
100
+ });
101
+
102
+ it('should select time when state is empty', () => {
103
+ expect(selectTime({})).toBe(TASKS_DASHBOARD_AVAILABLE_TIMES.H24);
104
+ });
105
+
106
+ it('should select query', () => {
107
+ const query = { state: 'running' };
108
+ const stateWithQuery = {
109
+ ...state,
110
+ foremanTasks: {
111
+ ...state.foremanTasks,
112
+ tasksDashboard: { ...state.foremanTasks.tasksDashboard, query },
113
+ },
114
+ };
115
+
116
+ expect(selectQuery(stateWithQuery)).toEqual(query);
117
+ });
118
+
119
+ it('should select query when state is empty', () => {
120
+ expect(selectQuery({})).toEqual({});
121
+ });
122
+
123
+ it('should select tasks-summary', () => {
124
+ expect(selectTasksSummary(state)).toEqual({
125
+ paused: {
126
+ last: 2,
127
+ older: 7,
128
+ },
129
+ running: {
130
+ last: 3,
131
+ older: 5,
132
+ },
133
+ scheduled: 6,
134
+ stopped: {
135
+ other: 16,
136
+ results: {
137
+ error: {
138
+ last: 1,
139
+ total: 9,
140
+ },
141
+ success: {
142
+ last: 3,
143
+ total: 7,
144
+ },
145
+ warning: {
146
+ last: 2,
147
+ total: 8,
148
+ },
149
+ },
150
+ },
151
+ });
152
+ });
153
+
154
+ it('should select tasks-summary when state is empty', () => {
155
+ expect(selectTasksSummary({})).toEqual(emptyTasksSummary);
156
+ });
157
+
158
+ it('should calcStoppedOther', () => {
159
+ expect(calcStoppedOther(stoppedResult)).toBe(16);
160
+ });
161
+ });
@@ -5,7 +5,7 @@ export const BULK_RESUME_PATH = 'bulk_resume';
5
5
  export const BULK_FORCE_CANCEL_PATH = 'bulk_stop';
6
6
 
7
7
  export const TASKS_SEARCH_PROPS = {
8
- ...getControllerSearchProps('tasks'),
8
+ ...getControllerSearchProps('/foreman_tasks/tasks'),
9
9
  controller: 'foreman_tasks_tasks',
10
10
  };
11
11
 
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { Icon } from '@patternfly/react-core';
3
+ import {
4
+ CheckCircleIcon,
5
+ ExclamationCircleIcon,
6
+ ExclamationTriangleIcon,
7
+ QuestionCircleIcon,
8
+ } from '@patternfly/react-icons';
9
+ import { translate as __ } from 'foremanReact/common/I18n';
10
+
11
+ /**
12
+ * Icon reflecting task state/result (aligned with TaskInfo / tasks table).
13
+ *
14
+ * @param {string} state Dynflow task state (e.g. stopped, running).
15
+ * @param {string} [result] Result when stopped (success, error, warning, …).
16
+ * @returns {React.ReactElement}
17
+ */
18
+ export const taskResultIconEl = (state, result) => {
19
+ if (state && state !== 'stopped') {
20
+ return (
21
+ <Icon title={__('Running')}>
22
+ <QuestionCircleIcon />
23
+ </Icon>
24
+ );
25
+ }
26
+
27
+ switch (result) {
28
+ case 'success':
29
+ return (
30
+ <Icon status="success" title={__('Success')}>
31
+ <CheckCircleIcon />
32
+ </Icon>
33
+ );
34
+ case 'error':
35
+ return (
36
+ <Icon status="danger" title={__('Error')}>
37
+ <ExclamationCircleIcon />
38
+ </Icon>
39
+ );
40
+ case 'warning':
41
+ return (
42
+ <Icon status="warning" title={__('Warning')}>
43
+ <ExclamationTriangleIcon />
44
+ </Icon>
45
+ );
46
+ default:
47
+ return (
48
+ <Icon title={__('Unknown')}>
49
+ <QuestionCircleIcon />
50
+ </Icon>
51
+ );
52
+ }
53
+ };
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useSelector } from 'react-redux';
4
+ import { Flex, FlexItem, TextContent, Text } from '@patternfly/react-core';
5
+ import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
6
+ 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';
14
+
15
+ const TaskDetailsPage = props => {
16
+ const { id } = props.match.params;
17
+ const action = useSelector(selectAction);
18
+ const taskState = useSelector(selectState);
19
+ const taskResult = useSelector(selectResult);
20
+ const headerText = action
21
+ ? sprintf(__('Details of %s task'), action)
22
+ : __('Task Details');
23
+
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
+ return (
44
+ <PageLayout
45
+ customHeader={header}
46
+ header={headerText}
47
+ searchable={false}
48
+ breadcrumbOptions={{
49
+ breadcrumbItems: [
50
+ { caption: __('Tasks'), url: '/foreman_tasks/tasks' },
51
+ { caption: action || id },
52
+ ],
53
+ isSwitchable: true,
54
+ resource: {
55
+ nameField: 'action',
56
+ resourceUrl: '/foreman_tasks/api/tasks',
57
+ switcherItemUrl: '/foreman_tasks/tasks/:id',
58
+ },
59
+ }}
60
+ >
61
+ <TaskDetails {...props} />
62
+ </PageLayout>
63
+ );
64
+ };
65
+
66
+ TaskDetailsPage.propTypes = {
67
+ match: PropTypes.shape({
68
+ params: PropTypes.shape({
69
+ id: PropTypes.string.isRequired,
70
+ }).isRequired,
71
+ }).isRequired,
72
+ };
73
+
74
+ export default TaskDetailsPage;
@@ -0,0 +1,265 @@
1
+ import React from 'react';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { combineReducers, configureStore } from '@reduxjs/toolkit';
5
+ import { createMemoryHistory } from 'history';
6
+ import { Router } from 'react-router-dom';
7
+ import { IntlProvider } from 'react-intl';
8
+ import { Provider } from 'react-redux';
9
+
10
+ import breadcrumbBarReducer from 'foremanReact/components/BreadcrumbBar/BreadcrumbBarReducer';
11
+ import { STATUS } from 'foremanReact/constants';
12
+ import intervalsReducer from 'foremanReact/redux/middlewares/IntervalMiddleware/IntervalReducer';
13
+
14
+ import { FOREMAN_TASK_DETAILS, VIEW_FOREMAN_TASKS } from '../../../Components/TaskDetails/TaskDetailsConstants';
15
+ import TaskDetailsPage from '../TaskDetailsPage';
16
+
17
+ const mockUseForemanPermissions = jest.fn(
18
+ () => new Set(['view_foreman_tasks'])
19
+ );
20
+
21
+ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
22
+ ...jest.requireActual('foremanReact/Root/Context/ForemanContext'),
23
+ useForemanPermissions: (...args) => mockUseForemanPermissions(...args),
24
+ }));
25
+
26
+ const TASK_DETAILS_TITLE_ROW_OUIA_ID = 'foreman-tasks-task-details-title-row';
27
+
28
+ const routerPropsBase = {
29
+ history: { push: jest.fn(), replace: jest.fn(), go: jest.fn() },
30
+ location: {
31
+ pathname: '/foreman_tasks/tasks/task-route-id',
32
+ search: '',
33
+ hash: '',
34
+ state: undefined,
35
+ },
36
+ };
37
+
38
+ const matchDefault = {
39
+ params: { id: 'task-route-id' },
40
+ path: '/foreman_tasks/tasks/:id',
41
+ url: '/foreman_tasks/tasks/task-route-id',
42
+ isExact: true,
43
+ };
44
+
45
+ const baseTaskPayload = {
46
+ action: '',
47
+ input: [],
48
+ output: {},
49
+ locks: [],
50
+ links: [],
51
+ depends_on: [],
52
+ blocks: [],
53
+ failed_steps: [],
54
+ running_steps: [],
55
+ execution_plan: {},
56
+ state: 'running',
57
+ result: '',
58
+ };
59
+
60
+ const createStoreForTaskPayload = overrides => ({
61
+ API: {
62
+ [FOREMAN_TASK_DETAILS]: {
63
+ response: { ...baseTaskPayload, ...overrides },
64
+ status: STATUS.RESOLVED,
65
+ payload: {},
66
+ },
67
+ },
68
+ });
69
+
70
+ const rootReducer = combineReducers({
71
+ API: (state = {}, action) => state,
72
+ intervals: intervalsReducer,
73
+ breadcrumbBar: breadcrumbBarReducer,
74
+ foremanTasks: (state = {}, action) => state,
75
+ });
76
+
77
+ const renderPage = (apiPayloadOverrides = {}, propsOverrides = {}) => {
78
+ const history = createMemoryHistory({
79
+ initialEntries: [`/foreman_tasks/tasks/${matchDefault.params.id}`],
80
+ });
81
+ const store = configureStore({
82
+ reducer: rootReducer,
83
+ preloadedState: createStoreForTaskPayload(apiPayloadOverrides),
84
+ middleware: getDefaultMiddleware =>
85
+ getDefaultMiddleware({
86
+ immutableCheck: false,
87
+ serializableCheck: false,
88
+ }),
89
+ });
90
+
91
+ window.history.pushState(
92
+ {},
93
+ '',
94
+ `/foreman_tasks/tasks/${matchDefault.params.id}`
95
+ );
96
+
97
+ return render(
98
+ <Router history={history}>
99
+ <Provider store={store}>
100
+ <IntlProvider locale="en">
101
+ <TaskDetailsPage
102
+ {...routerPropsBase}
103
+ history={history}
104
+ match={matchDefault}
105
+ {...propsOverrides}
106
+ />
107
+ </IntlProvider>
108
+ </Provider>
109
+ </Router>
110
+ );
111
+ };
112
+
113
+ const breadcrumbTitleHeadings = () =>
114
+ screen.getAllByRole('heading', { level: 1 }).filter(
115
+ heading => heading.getAttribute('data-ouia-component-id') === 'breadcrumb_title'
116
+ );
117
+
118
+ /**
119
+ * Title row (`customHeader` root `Flex`): same OUIA pattern as `Locks.test.js`.
120
+ */
121
+ const taskDetailsTitleRegion = container => {
122
+ const el = container.querySelector(
123
+ `[data-ouia-component-id="${TASK_DETAILS_TITLE_ROW_OUIA_ID}"]`
124
+ );
125
+
126
+ expect(el).toBeTruthy();
127
+
128
+ return el;
129
+ };
130
+
131
+ describe('TaskDetailsPage', () => {
132
+ beforeEach(() => {
133
+ mockUseForemanPermissions.mockImplementation(() => new Set(['view_foreman_tasks']));
134
+ });
135
+
136
+ describe('permissions', () => {
137
+ it(`renders the task details chrome when ${VIEW_FOREMAN_TASKS} is present`, () => {
138
+ mockUseForemanPermissions.mockImplementation(
139
+ () => new Set(['edit_foreman_tasks', VIEW_FOREMAN_TASKS])
140
+ );
141
+
142
+ renderPage({ action: 'Run job' });
143
+
144
+ expect(
145
+ screen.getByRole('navigation', { name: 'Breadcrumb' })
146
+ ).toBeInTheDocument();
147
+ expect(
148
+ screen.getByRole('heading', {
149
+ level: 1,
150
+ name: /Details of Run job task/,
151
+ })
152
+ ).toBeInTheDocument();
153
+ expect(screen.queryByRole('heading', { name: /Permission denied/i })).not.toBeInTheDocument();
154
+ expect(mockUseForemanPermissions).toHaveBeenCalled();
155
+ });
156
+
157
+ it(`shows ResourceLoadFailedEmptyState and lists ${VIEW_FOREMAN_TASKS} when it is absent`, () => {
158
+ mockUseForemanPermissions.mockImplementation(() => new Set());
159
+
160
+ renderPage({ action: 'Hidden task' });
161
+
162
+ expect(
163
+ screen.getByRole('heading', { name: /Permission denied/i })
164
+ ).toBeInTheDocument();
165
+ expect(
166
+ screen.getByText(
167
+ /You do not have permission to view the task with id task-route-id/
168
+ )
169
+ ).toBeInTheDocument();
170
+ expect(screen.getByText(VIEW_FOREMAN_TASKS)).toBeInTheDocument();
171
+ expect(
172
+ screen.getByRole('button', { name: /Back to tasks/i })
173
+ ).toBeInTheDocument();
174
+ expect(
175
+ screen.getByRole('navigation', { name: 'Breadcrumb' })
176
+ ).toBeInTheDocument();
177
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
178
+ });
179
+
180
+ it('denies access when user only has edit_foreman_tasks without view', () => {
181
+ mockUseForemanPermissions.mockImplementation(
182
+ () => new Set(['edit_foreman_tasks'])
183
+ );
184
+
185
+ renderPage({});
186
+
187
+ expect(
188
+ screen.getByRole('heading', { name: /Permission denied/i })
189
+ ).toBeInTheDocument();
190
+ expect(screen.getByText(VIEW_FOREMAN_TASKS)).toBeInTheDocument();
191
+ expect(
192
+ screen.getByRole('navigation', { name: 'Breadcrumb' })
193
+ ).toBeInTheDocument();
194
+ expect(screen.queryByRole('tab', { name: /^task$/i })).not.toBeInTheDocument();
195
+ });
196
+ });
197
+
198
+
199
+ const expectToolbarHeadingText = substring => {
200
+ const headings = breadcrumbTitleHeadings();
201
+
202
+ expect(headings.length).toBeGreaterThan(0);
203
+
204
+ headings.forEach(heading => {
205
+ expect(heading).toHaveTextContent(substring);
206
+ });
207
+ };
208
+
209
+ it('shows generic title and breadcrumb from route id when action is unset', () => {
210
+ const page = renderPage({});
211
+ expectToolbarHeadingText('Task Details');
212
+
213
+ expect(screen.getByRole('navigation', { name: 'Breadcrumb' })).toHaveTextContent(
214
+ 'Tasks'
215
+ );
216
+ expect(
217
+ within(screen.getByRole('navigation', { name: 'Breadcrumb' })).getByText(
218
+ /task-route-id/
219
+ )
220
+ ).toBeInTheDocument();
221
+
222
+ const titleRegion = taskDetailsTitleRegion(page.container);
223
+
224
+ expect(
225
+ within(titleRegion).getAllByRole('img', { hidden: true }).length
226
+ ).toBeGreaterThan(0);
227
+ expect(titleRegion.querySelector('[class*="danger"]')).toBeNull();
228
+ });
229
+
230
+ it('uses task action for title and breadcrumb when loaded', () => {
231
+ renderPage({ action: 'Refresh hosts' });
232
+
233
+ expectToolbarHeadingText('Details of Refresh hosts task');
234
+
235
+ expect(screen.getByRole('link', { name: /^Tasks$/ })).toHaveAttribute(
236
+ 'href',
237
+ '/foreman_tasks/tasks'
238
+ );
239
+ expect(
240
+ within(screen.getByRole('navigation', { name: 'Breadcrumb' })).getByText(
241
+ 'Refresh hosts'
242
+ )
243
+ ).toBeInTheDocument();
244
+ });
245
+
246
+ it('shows error status styling in the heading when task is stopped with error result', () => {
247
+ const page = renderPage({
248
+ action: 'Some action',
249
+ state: 'stopped',
250
+ result: 'error',
251
+ });
252
+
253
+ expectToolbarHeadingText('Details of Some action task');
254
+
255
+ expect(
256
+ taskDetailsTitleRegion(page.container).querySelector('[class*="danger"]')
257
+ ).toBeTruthy();
258
+
259
+ expect(
260
+ within(screen.getByRole('navigation', { name: 'Breadcrumb' })).getByText(
261
+ 'Some action'
262
+ )
263
+ ).toBeInTheDocument();
264
+ });
265
+ });