foreman_remote_execution 14.1.4 → 15.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/job_invocations_controller.rb +8 -0
- data/app/controllers/template_invocations_controller.rb +57 -0
- data/app/controllers/ui_job_wizard_controller.rb +6 -3
- data/app/helpers/remote_execution_helper.rb +5 -6
- data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/hosts.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/show.json.rabl +18 -0
- data/app/views/templates/script/convert2rhel_analyze.erb +4 -4
- data/config/routes.rb +2 -0
- data/lib/foreman_remote_execution/engine.rb +3 -3
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/webpack/JobInvocationDetail/JobAdditionInfo.js +214 -0
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +40 -2
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +70 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +177 -80
- data/webpack/JobInvocationDetail/JobInvocationHostTableToolbar.js +63 -0
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +8 -1
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +61 -10
- data/webpack/JobInvocationDetail/OpenAlInvocations.js +111 -0
- data/webpack/JobInvocationDetail/TemplateInvocation.js +202 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +124 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js +156 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/PreviewTemplate.js +50 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +224 -0
- data/webpack/JobInvocationDetail/TemplateInvocationPage.js +53 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +110 -0
- data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +69 -0
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +131 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +130 -0
- data/webpack/JobInvocationDetail/index.js +18 -3
- data/webpack/JobWizard/JobWizard.js +38 -16
- data/webpack/JobWizard/{StartsBeforeErrorAlert.js → StartsErrorAlert.js} +16 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +1 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +5 -3
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +1 -1
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +3 -3
- data/webpack/JobWizard/steps/form/DateTimePicker.js +13 -0
- data/webpack/JobWizard/steps/form/Formatter.js +1 -0
- data/webpack/JobWizard/steps/form/ResourceSelect.js +34 -9
- data/webpack/Routes/routes.js +6 -0
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +1 -0
- data/webpack/react_app/components/RegistrationExtension/RexPull.js +27 -2
- data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +1 -1
- metadata +15 -3
@@ -0,0 +1,224 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { useDispatch, useSelector } from 'react-redux';
|
4
|
+
import { Flex, FlexItem, Button } from '@patternfly/react-core';
|
5
|
+
import { ActionsColumn } from '@patternfly/react-table';
|
6
|
+
import { APIActions } from 'foremanReact/redux/API';
|
7
|
+
import { addToast } from 'foremanReact/components/ToastsList';
|
8
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
9
|
+
import { selectTemplateInvocation } from '../JobInvocationSelectors';
|
10
|
+
|
11
|
+
const actions = ({
|
12
|
+
taskID,
|
13
|
+
jobID,
|
14
|
+
hostID,
|
15
|
+
taskCancellable,
|
16
|
+
permissions,
|
17
|
+
dispatch,
|
18
|
+
}) => ({
|
19
|
+
rerun: {
|
20
|
+
name: 'template-invocation-rerun-job',
|
21
|
+
href: `/job_invocations/${jobID}/rerun?host_ids[]=${hostID}`,
|
22
|
+
component: 'a',
|
23
|
+
text: __('Rerun'),
|
24
|
+
permission: permissions.execute_jobs,
|
25
|
+
},
|
26
|
+
details: {
|
27
|
+
name: 'template-invocation-task-details',
|
28
|
+
href: `/foreman_tasks/tasks/${taskID}`,
|
29
|
+
component: 'a',
|
30
|
+
text: __('Task Details'),
|
31
|
+
permission: permissions.view_foreman_tasks,
|
32
|
+
},
|
33
|
+
cancel: {
|
34
|
+
name: 'template-invocation-cancel-job',
|
35
|
+
text: __('Cancel Task'),
|
36
|
+
permission: permissions.cancel_job_invocations,
|
37
|
+
onClick: () => {
|
38
|
+
dispatch(
|
39
|
+
addToast({
|
40
|
+
key: `cancel-job-info`,
|
41
|
+
type: 'info',
|
42
|
+
message: __('Trying to cancel the task for the host'),
|
43
|
+
})
|
44
|
+
);
|
45
|
+
dispatch(
|
46
|
+
APIActions.post({
|
47
|
+
url: `/foreman_tasks/tasks/${taskID}/cancel`,
|
48
|
+
key: 'CANCEL_TASK',
|
49
|
+
errorToast: ({ response }) => response.data.message,
|
50
|
+
successToast: () => __('Task for the host cancelled succesfully'),
|
51
|
+
})
|
52
|
+
);
|
53
|
+
},
|
54
|
+
isDisabled: !taskCancellable,
|
55
|
+
},
|
56
|
+
abort: {
|
57
|
+
name: 'template-invocation-abort-job',
|
58
|
+
text: __('Abort task'),
|
59
|
+
permission: permissions.cancel_job_invocations,
|
60
|
+
onClick: () => {
|
61
|
+
dispatch(
|
62
|
+
addToast({
|
63
|
+
key: `abort-job-info`,
|
64
|
+
type: 'info',
|
65
|
+
message: __('Trying to abort the task for the host'),
|
66
|
+
})
|
67
|
+
);
|
68
|
+
dispatch(
|
69
|
+
APIActions.post({
|
70
|
+
url: `/foreman_tasks/tasks/${taskID}/abort`,
|
71
|
+
key: 'ABORT_TASK',
|
72
|
+
errorToast: ({ response }) => response.data.message,
|
73
|
+
successToast: () => __('task aborted succesfully'),
|
74
|
+
})
|
75
|
+
);
|
76
|
+
},
|
77
|
+
isDisabled: !taskCancellable,
|
78
|
+
},
|
79
|
+
});
|
80
|
+
|
81
|
+
export const RowActions = ({ hostID, jobID }) => {
|
82
|
+
const dispatch = useDispatch();
|
83
|
+
const response = useSelector(selectTemplateInvocation);
|
84
|
+
if (!response?.permissions) return null;
|
85
|
+
const {
|
86
|
+
task_id: taskID,
|
87
|
+
task_cancellable: taskCancellable,
|
88
|
+
permissions,
|
89
|
+
} = response;
|
90
|
+
|
91
|
+
const getActions = actions({
|
92
|
+
taskID,
|
93
|
+
jobID,
|
94
|
+
hostID,
|
95
|
+
taskCancellable,
|
96
|
+
permissions,
|
97
|
+
dispatch,
|
98
|
+
});
|
99
|
+
const rowActions = Object.values(getActions)
|
100
|
+
.map(({ text, onClick, href, permission, isDisabled }) =>
|
101
|
+
permission
|
102
|
+
? {
|
103
|
+
title: href ? (
|
104
|
+
<a href={href} target="_blank" rel="noreferrer">
|
105
|
+
{text}
|
106
|
+
</a>
|
107
|
+
) : (
|
108
|
+
text
|
109
|
+
),
|
110
|
+
onClick,
|
111
|
+
isDisabled,
|
112
|
+
}
|
113
|
+
: null
|
114
|
+
)
|
115
|
+
.filter(Boolean);
|
116
|
+
|
117
|
+
return <ActionsColumn items={rowActions} />;
|
118
|
+
};
|
119
|
+
|
120
|
+
export const TemplateActionButtons = ({
|
121
|
+
taskID,
|
122
|
+
jobID,
|
123
|
+
hostID,
|
124
|
+
taskCancellable,
|
125
|
+
permissions,
|
126
|
+
}) => {
|
127
|
+
const dispatch = useDispatch();
|
128
|
+
const { rerun, details, cancel, abort } = actions({
|
129
|
+
taskID,
|
130
|
+
jobID,
|
131
|
+
hostID,
|
132
|
+
taskCancellable,
|
133
|
+
permissions,
|
134
|
+
dispatch,
|
135
|
+
});
|
136
|
+
return (
|
137
|
+
<Flex align={{ default: 'alignRight' }}>
|
138
|
+
{rerun.permission && (
|
139
|
+
<FlexItem spacer={{ default: 'spacerXs' }}>
|
140
|
+
<Button
|
141
|
+
isSmall
|
142
|
+
variant="secondary"
|
143
|
+
isInline
|
144
|
+
ouiaId={rerun.name}
|
145
|
+
href={rerun.href}
|
146
|
+
component="a"
|
147
|
+
target="_blank"
|
148
|
+
>
|
149
|
+
{rerun.text}
|
150
|
+
</Button>
|
151
|
+
</FlexItem>
|
152
|
+
)}
|
153
|
+
{details.permission && (
|
154
|
+
<FlexItem spacer={{ default: 'spacerXs' }}>
|
155
|
+
<Button
|
156
|
+
isSmall
|
157
|
+
variant="secondary"
|
158
|
+
isInline
|
159
|
+
ouiaId={details.name}
|
160
|
+
href={details.href}
|
161
|
+
component="a"
|
162
|
+
target="_blank"
|
163
|
+
>
|
164
|
+
{details.text}
|
165
|
+
</Button>
|
166
|
+
</FlexItem>
|
167
|
+
)}
|
168
|
+
{cancel.permission && (
|
169
|
+
<FlexItem spacer={{ default: 'spacerXs' }}>
|
170
|
+
<Button
|
171
|
+
isSmall
|
172
|
+
variant="danger"
|
173
|
+
isInline
|
174
|
+
ouiaId={cancel.name}
|
175
|
+
onClick={cancel.onClick}
|
176
|
+
isDisabled={cancel.isDisabled}
|
177
|
+
>
|
178
|
+
{cancel.text}
|
179
|
+
</Button>
|
180
|
+
</FlexItem>
|
181
|
+
)}
|
182
|
+
{abort.permission && (
|
183
|
+
<FlexItem spacer={{ default: 'spacerXs' }}>
|
184
|
+
<Button
|
185
|
+
isSmall
|
186
|
+
variant="danger"
|
187
|
+
isInline
|
188
|
+
ouiaId={abort.name}
|
189
|
+
onClick={abort.onClick}
|
190
|
+
isDisabled={abort.isDisabled}
|
191
|
+
>
|
192
|
+
{abort.text}
|
193
|
+
</Button>
|
194
|
+
</FlexItem>
|
195
|
+
)}
|
196
|
+
</Flex>
|
197
|
+
);
|
198
|
+
};
|
199
|
+
TemplateActionButtons.propTypes = {
|
200
|
+
taskID: PropTypes.string,
|
201
|
+
jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
202
|
+
hostID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
203
|
+
taskCancellable: PropTypes.bool,
|
204
|
+
permissions: PropTypes.shape({
|
205
|
+
view_foreman_tasks: PropTypes.bool,
|
206
|
+
cancel_job_invocations: PropTypes.bool,
|
207
|
+
execute_jobs: PropTypes.bool,
|
208
|
+
}),
|
209
|
+
};
|
210
|
+
|
211
|
+
TemplateActionButtons.defaultProps = {
|
212
|
+
taskID: null,
|
213
|
+
taskCancellable: false,
|
214
|
+
permissions: {
|
215
|
+
view_foreman_tasks: false,
|
216
|
+
cancel_job_invocations: false,
|
217
|
+
execute_jobs: false,
|
218
|
+
},
|
219
|
+
};
|
220
|
+
|
221
|
+
RowActions.propTypes = {
|
222
|
+
hostID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
223
|
+
jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
224
|
+
};
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { useSelector } from 'react-redux';
|
4
|
+
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
5
|
+
import { translate as __, sprintf } from 'foremanReact/common/I18n';
|
6
|
+
import { TemplateInvocation } from './TemplateInvocation';
|
7
|
+
import { selectTemplateInvocation } from './JobInvocationSelectors';
|
8
|
+
import { jobInvocationDetailsUrl } from './JobInvocationConstants';
|
9
|
+
|
10
|
+
const TemplateInvocationPage = ({
|
11
|
+
match: {
|
12
|
+
params: { hostID, jobID },
|
13
|
+
},
|
14
|
+
}) => {
|
15
|
+
const {
|
16
|
+
job_invocation_description: jobDescription,
|
17
|
+
host_name: hostName,
|
18
|
+
} = useSelector(selectTemplateInvocation);
|
19
|
+
const description = sprintf(__('Template Invocation for %s'), hostName);
|
20
|
+
const breadcrumbOptions = {
|
21
|
+
breadcrumbItems: [
|
22
|
+
{ caption: __('Jobs'), url: `/job_invocations` },
|
23
|
+
{ caption: jobDescription, url: jobInvocationDetailsUrl(jobID) },
|
24
|
+
{ caption: hostName },
|
25
|
+
],
|
26
|
+
isPf4: true,
|
27
|
+
};
|
28
|
+
return (
|
29
|
+
<PageLayout
|
30
|
+
header={description}
|
31
|
+
breadcrumbOptions={breadcrumbOptions}
|
32
|
+
searchable={false}
|
33
|
+
>
|
34
|
+
<TemplateInvocation
|
35
|
+
hostID={hostID}
|
36
|
+
jobID={jobID}
|
37
|
+
isInTableView={false}
|
38
|
+
hostName={hostName}
|
39
|
+
/>
|
40
|
+
</PageLayout>
|
41
|
+
);
|
42
|
+
};
|
43
|
+
|
44
|
+
TemplateInvocationPage.propTypes = {
|
45
|
+
match: PropTypes.shape({
|
46
|
+
params: PropTypes.shape({
|
47
|
+
jobID: PropTypes.string.isRequired,
|
48
|
+
hostID: PropTypes.string.isRequired,
|
49
|
+
}).isRequired,
|
50
|
+
}).isRequired,
|
51
|
+
};
|
52
|
+
|
53
|
+
export default TemplateInvocationPage;
|
@@ -105,7 +105,7 @@ describe('JobInvocationDetailPage', () => {
|
|
105
105
|
expect(screen.getByText('Succeeded: 2')).toBeInTheDocument();
|
106
106
|
expect(screen.getByText('Failed: 4')).toBeInTheDocument();
|
107
107
|
expect(screen.getByText('In Progress: 0')).toBeInTheDocument();
|
108
|
-
expect(screen.getByText('
|
108
|
+
expect(screen.getByText('Cancelled: 0')).toBeInTheDocument();
|
109
109
|
|
110
110
|
const informationToCheck = {
|
111
111
|
'Effective user:': jobInvocationData.effective_user,
|
@@ -0,0 +1,110 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
3
|
+
import '@testing-library/jest-dom/extend-expect';
|
4
|
+
import { OpenAlInvocations, PopupAlert } from '../OpenAlInvocations';
|
5
|
+
import { templateInvocationPageUrl } from '../JobInvocationConstants';
|
6
|
+
|
7
|
+
// Mock the templateInvocationPageUrl function
|
8
|
+
jest.mock('../JobInvocationConstants', () => ({
|
9
|
+
...jest.requireActual('../JobInvocationConstants'),
|
10
|
+
templateInvocationPageUrl: jest.fn((resultId, id) => `url/${resultId}/${id}`),
|
11
|
+
}));
|
12
|
+
|
13
|
+
describe('OpenAlInvocations', () => {
|
14
|
+
const mockResults = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
15
|
+
const mockSetShowAlert = jest.fn();
|
16
|
+
let windowSpy;
|
17
|
+
const windowOpen = window.open;
|
18
|
+
|
19
|
+
beforeAll(() => {
|
20
|
+
window.open = () => {};
|
21
|
+
});
|
22
|
+
afterAll(() => {
|
23
|
+
windowSpy.mockRestore();
|
24
|
+
jest.clearAllMocks();
|
25
|
+
window.open = windowOpen;
|
26
|
+
});
|
27
|
+
|
28
|
+
test('renders without crashing', () => {
|
29
|
+
render(
|
30
|
+
<OpenAlInvocations
|
31
|
+
results={mockResults}
|
32
|
+
id="test-id"
|
33
|
+
setShowAlert={mockSetShowAlert}
|
34
|
+
/>
|
35
|
+
);
|
36
|
+
});
|
37
|
+
|
38
|
+
test('opens links when results length is less than or equal to 3', () => {
|
39
|
+
render(
|
40
|
+
<OpenAlInvocations
|
41
|
+
results={mockResults}
|
42
|
+
id="test-id"
|
43
|
+
setShowAlert={mockSetShowAlert}
|
44
|
+
/>
|
45
|
+
);
|
46
|
+
|
47
|
+
const button = screen.getByRole('button', { name: /open all/i });
|
48
|
+
fireEvent.click(button);
|
49
|
+
|
50
|
+
expect(templateInvocationPageUrl).toHaveBeenCalledTimes(mockResults.length);
|
51
|
+
mockResults.forEach(result => {
|
52
|
+
expect(templateInvocationPageUrl).toHaveBeenCalledWith(
|
53
|
+
result.id,
|
54
|
+
'test-id'
|
55
|
+
);
|
56
|
+
});
|
57
|
+
});
|
58
|
+
|
59
|
+
test('shows modal when results length is greater than 3', () => {
|
60
|
+
const largeResults = [...mockResults, { id: 4 }];
|
61
|
+
render(
|
62
|
+
<OpenAlInvocations
|
63
|
+
results={largeResults}
|
64
|
+
id="test-id"
|
65
|
+
setShowAlert={mockSetShowAlert}
|
66
|
+
/>
|
67
|
+
);
|
68
|
+
|
69
|
+
const button = screen.getByRole('button', { name: /open all/i });
|
70
|
+
fireEvent.click(button);
|
71
|
+
|
72
|
+
expect(
|
73
|
+
screen.getAllByText(/open all invocations in new tabs/i)
|
74
|
+
).toHaveLength(2);
|
75
|
+
});
|
76
|
+
|
77
|
+
test('shows alert when popups are blocked', () => {
|
78
|
+
window.open = jest.fn().mockReturnValueOnce(null);
|
79
|
+
|
80
|
+
render(
|
81
|
+
<OpenAlInvocations
|
82
|
+
results={mockResults}
|
83
|
+
id="test-id"
|
84
|
+
setShowAlert={mockSetShowAlert}
|
85
|
+
/>
|
86
|
+
);
|
87
|
+
|
88
|
+
const button = screen.getByRole('button', { name: /open all/i });
|
89
|
+
fireEvent.click(button);
|
90
|
+
|
91
|
+
expect(mockSetShowAlert).toHaveBeenCalledWith(true);
|
92
|
+
});
|
93
|
+
});
|
94
|
+
|
95
|
+
describe('PopupAlert', () => {
|
96
|
+
const mockSetShowAlert = jest.fn();
|
97
|
+
|
98
|
+
test('renders without crashing', () => {
|
99
|
+
render(<PopupAlert setShowAlert={mockSetShowAlert} />);
|
100
|
+
});
|
101
|
+
|
102
|
+
test('closes alert when close button is clicked', () => {
|
103
|
+
render(<PopupAlert setShowAlert={mockSetShowAlert} />);
|
104
|
+
|
105
|
+
const closeButton = screen.getByRole('button', { name: /close/i });
|
106
|
+
fireEvent.click(closeButton);
|
107
|
+
|
108
|
+
expect(mockSetShowAlert).toHaveBeenCalledWith(false);
|
109
|
+
});
|
110
|
+
});
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
3
|
+
import '@testing-library/jest-dom/extend-expect';
|
4
|
+
import { OutputCodeBlock } from '../TemplateInvocationComponents/OutputCodeBlock';
|
5
|
+
import { jobInvocationOutput } from './fixtures';
|
6
|
+
|
7
|
+
const mockShowOutputType = {
|
8
|
+
stdout: true,
|
9
|
+
stderr: true,
|
10
|
+
debug: true,
|
11
|
+
};
|
12
|
+
|
13
|
+
describe('OutputCodeBlock', () => {
|
14
|
+
beforeAll(() => {
|
15
|
+
Element.prototype.scrollTo = () => {};
|
16
|
+
});
|
17
|
+
afterAll(() => {
|
18
|
+
delete Element.prototype.scrollTo;
|
19
|
+
});
|
20
|
+
test('displays the correct output', () => {
|
21
|
+
render(
|
22
|
+
<OutputCodeBlock
|
23
|
+
code={jobInvocationOutput}
|
24
|
+
showOutputType={mockShowOutputType}
|
25
|
+
scrollElement="body"
|
26
|
+
/>
|
27
|
+
);
|
28
|
+
|
29
|
+
expect(screen.getByText('3:')).toBeInTheDocument();
|
30
|
+
expect(screen.getByText('This is red text')).toHaveStyle('color: red');
|
31
|
+
expect(screen.getByText('This is green text')).toHaveStyle(
|
32
|
+
'color: lightgreen'
|
33
|
+
);
|
34
|
+
});
|
35
|
+
|
36
|
+
test('displays no output message when filtered', () => {
|
37
|
+
render(
|
38
|
+
<OutputCodeBlock
|
39
|
+
code={jobInvocationOutput}
|
40
|
+
showOutputType={{ stdout: false, stderr: false, debug: false }}
|
41
|
+
scrollElement="body"
|
42
|
+
/>
|
43
|
+
);
|
44
|
+
|
45
|
+
expect(
|
46
|
+
screen.getByText('No output for the selected filters')
|
47
|
+
).toBeInTheDocument();
|
48
|
+
});
|
49
|
+
|
50
|
+
test('scrolls to top and bottom', async () => {
|
51
|
+
render(
|
52
|
+
<div className="template-invocation">
|
53
|
+
<div className="invocation-output" style={{ overflow: 'auto' }}>
|
54
|
+
<OutputCodeBlock
|
55
|
+
code={jobInvocationOutput}
|
56
|
+
showOutputType={mockShowOutputType}
|
57
|
+
scrollElement=".invocation-output"
|
58
|
+
/>
|
59
|
+
</div>
|
60
|
+
</div>
|
61
|
+
);
|
62
|
+
|
63
|
+
const scrollToTopButton = screen.getByText('Scroll to top');
|
64
|
+
const scrollToBottomButton = screen.getByText('Scroll to bottom');
|
65
|
+
|
66
|
+
fireEvent.click(scrollToTopButton);
|
67
|
+
fireEvent.click(scrollToBottomButton);
|
68
|
+
});
|
69
|
+
});
|
@@ -0,0 +1,131 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import configureMockStore from 'redux-mock-store';
|
3
|
+
import { Provider } from 'react-redux';
|
4
|
+
import { render, screen, act, fireEvent } from '@testing-library/react';
|
5
|
+
import '@testing-library/jest-dom/extend-expect';
|
6
|
+
import * as APIHooks from 'foremanReact/common/hooks/API/APIHooks';
|
7
|
+
import * as api from 'foremanReact/redux/API';
|
8
|
+
import { TemplateInvocation } from '../TemplateInvocation';
|
9
|
+
import { mockTemplateInvocationResponse } from './fixtures';
|
10
|
+
|
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
|
+
|
18
|
+
const mockStore = configureMockStore([]);
|
19
|
+
const store = mockStore({
|
20
|
+
HOSTS_API: {
|
21
|
+
response: {
|
22
|
+
subtotal: 3,
|
23
|
+
},
|
24
|
+
},
|
25
|
+
});
|
26
|
+
describe('TemplateInvocation', () => {
|
27
|
+
test('render', async () => {
|
28
|
+
render(
|
29
|
+
<Provider store={store}>
|
30
|
+
<TemplateInvocation
|
31
|
+
hostID="1"
|
32
|
+
jobID="1"
|
33
|
+
isInTableView={false}
|
34
|
+
hostName="example-host"
|
35
|
+
/>
|
36
|
+
</Provider>
|
37
|
+
);
|
38
|
+
|
39
|
+
expect(screen.getByText('Target:')).toBeInTheDocument();
|
40
|
+
expect(screen.getByText('example-host')).toBeInTheDocument();
|
41
|
+
|
42
|
+
expect(screen.getByText('This is red text')).toBeInTheDocument();
|
43
|
+
expect(screen.getByText('This is default text')).toBeInTheDocument();
|
44
|
+
});
|
45
|
+
test('filtering toggles', () => {
|
46
|
+
render(
|
47
|
+
<Provider store={store}>
|
48
|
+
<TemplateInvocation
|
49
|
+
hostID="1"
|
50
|
+
jobID="1"
|
51
|
+
isInTableView={false}
|
52
|
+
hostName="example-host"
|
53
|
+
/>
|
54
|
+
</Provider>
|
55
|
+
);
|
56
|
+
|
57
|
+
act(() => {
|
58
|
+
fireEvent.click(screen.getByText('STDOUT'));
|
59
|
+
fireEvent.click(screen.getByText('DEBUG'));
|
60
|
+
fireEvent.click(screen.getByText('STDERR'));
|
61
|
+
});
|
62
|
+
expect(
|
63
|
+
screen.queryAllByText('No output for the selected filters')
|
64
|
+
).toHaveLength(1);
|
65
|
+
expect(screen.queryAllByText('Exit status: 1')).toHaveLength(0);
|
66
|
+
expect(
|
67
|
+
screen.queryAllByText('StandardError: Job execution failed')
|
68
|
+
).toHaveLength(0);
|
69
|
+
|
70
|
+
act(() => {
|
71
|
+
fireEvent.click(screen.getByText('STDOUT'));
|
72
|
+
});
|
73
|
+
expect(
|
74
|
+
screen.queryAllByText('No output for the selected filters')
|
75
|
+
).toHaveLength(0);
|
76
|
+
expect(screen.queryAllByText('Exit status: 1')).toHaveLength(1);
|
77
|
+
expect(
|
78
|
+
screen.queryAllByText('StandardError: Job execution failed')
|
79
|
+
).toHaveLength(0);
|
80
|
+
|
81
|
+
act(() => {
|
82
|
+
fireEvent.click(screen.getByText('DEBUG'));
|
83
|
+
});
|
84
|
+
expect(
|
85
|
+
screen.queryAllByText('No output for the selected filters')
|
86
|
+
).toHaveLength(0);
|
87
|
+
expect(screen.queryAllByText('Exit status: 1')).toHaveLength(1);
|
88
|
+
expect(
|
89
|
+
screen.queryAllByText('StandardError: Job execution failed')
|
90
|
+
).toHaveLength(1);
|
91
|
+
});
|
92
|
+
test('displays an error alert when there is an error', async () => {
|
93
|
+
APIHooks.useAPI.mockImplementation(() => ({
|
94
|
+
response: { response: { data: { error: 'Error message' } } },
|
95
|
+
status: 'ERROR',
|
96
|
+
}));
|
97
|
+
|
98
|
+
render(
|
99
|
+
<TemplateInvocation
|
100
|
+
hostID="1"
|
101
|
+
jobID="1"
|
102
|
+
isInTableView={false}
|
103
|
+
hostName="example-host"
|
104
|
+
/>
|
105
|
+
);
|
106
|
+
|
107
|
+
expect(
|
108
|
+
screen.getByText(
|
109
|
+
'An error occurred while fetching the template invocation details.'
|
110
|
+
)
|
111
|
+
).toBeInTheDocument();
|
112
|
+
expect(screen.getByText('Error message')).toBeInTheDocument();
|
113
|
+
});
|
114
|
+
|
115
|
+
test('displays a skeleton while loading', async () => {
|
116
|
+
APIHooks.useAPI.mockImplementation(() => ({
|
117
|
+
response: {},
|
118
|
+
status: 'PENDING',
|
119
|
+
}));
|
120
|
+
render(
|
121
|
+
<TemplateInvocation
|
122
|
+
hostID="1"
|
123
|
+
jobID="1"
|
124
|
+
isInTableView={false}
|
125
|
+
hostName="example-host"
|
126
|
+
/>
|
127
|
+
);
|
128
|
+
|
129
|
+
expect(document.querySelectorAll('.pf-c-skeleton')).toHaveLength(1);
|
130
|
+
});
|
131
|
+
});
|