foreman_remote_execution 14.1.4 → 15.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.
- 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
|
+
});
|