Skip to content

Commit 808e87e

Browse files
Fixes #39412 - replace messagebox with empty state and update to pf5 components inside PreupgradeReports (#175)
* Fixes #39412 - replace messagebox with empty state and update to pf5 components inside PreupgradeReports * Refs #39412 - fix snapshot failing * Refs #39412 - bump foreman core dependency
1 parent af94e75 commit 808e87e

6 files changed

Lines changed: 208 additions & 172 deletions

File tree

lib/foreman_leapp/engine.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Engine < ::Rails::Engine
1717
initializer 'foreman_leapp.register_plugin', before: :finisher_hook do |app|
1818
app.reloader.to_prepare do
1919
Foreman::Plugin.register :foreman_leapp do
20-
requires_foreman '>= 3.16'
20+
requires_foreman '>= 5.0'
2121
register_gettext
2222

2323
apipie_documented_controllers ["#{ForemanLeapp::Engine.root}/app/controllers/api/v2/*.rb"]

webpack/components/PreupgradeReports/PreupgradeReports.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React, { useState } from 'react';
2-
import MessageBox from 'foremanReact/components/common/MessageBox';
2+
import { Icon } from '@patternfly/react-core';
3+
import { ExclamationCircleIcon } from '@patternfly/react-icons';
4+
import EmptyState from 'foremanReact/components/common/EmptyState';
35
import { LoadingState, Row } from 'patternfly-react';
46
import PropTypes from 'prop-types';
57
import { sprintf, translate as __ } from 'foremanReact/common/I18n';
@@ -116,10 +118,15 @@ const withLoadingState = Component => componentProps => {
116118

117119
if (!isEmpty(error)) {
118120
return (
119-
<MessageBox
121+
<EmptyState
120122
key="preupgrade-reports-error"
121-
icontype="error-circle-o"
122-
msg={sprintf(__('Could not retrieve data: %(status)s - %(msg)s'), {
123+
variant="xs"
124+
icon={
125+
<Icon iconSize="lg">
126+
<ExclamationCircleIcon />
127+
</Icon>
128+
}
129+
header={sprintf(__('Could not retrieve data: %(status)s - %(msg)s'), {
123130
status: error.statusText,
124131
msg: error.errorMsg,
125132
})}

webpack/components/PreupgradeReports/__tests__/PreupgradeReports.fixtures.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,49 @@ export const preupgradeReports = [
6060
},
6161
];
6262

63+
export const preupgradeReportsWithFixableEntries = [
64+
{
65+
hostId: 5,
66+
entries: [
67+
{
68+
title: 'Fixable entry',
69+
severity: 'high',
70+
id: 100,
71+
hostId: 5,
72+
hostname: 'host.example.com',
73+
flags: [],
74+
detail: {
75+
remediations: [{ type: 'command', context: ['echo', 'fix'] }],
76+
},
77+
},
78+
{
79+
title: 'Not fixable entry',
80+
severity: 'low',
81+
id: 101,
82+
hostId: 5,
83+
hostname: 'host.example.com',
84+
flags: [],
85+
},
86+
],
87+
},
88+
{
89+
hostId: 6,
90+
entries: [
91+
{
92+
title: 'Another fixable entry',
93+
severity: 'medium',
94+
id: 102,
95+
hostId: 6,
96+
hostname: 'foo.example.com',
97+
flags: [],
98+
detail: {
99+
remediations: [{ type: 'command', context: ['echo', 'fix2'] }],
100+
},
101+
},
102+
],
103+
},
104+
];
105+
63106
export const reportsWithRemediations = [
64107
{
65108
hostId: 5,
Lines changed: 151 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,159 @@
1-
import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
1+
import React from 'react';
2+
import { Provider } from 'react-redux';
3+
import configureMockStore from 'redux-mock-store';
4+
import { fireEvent, render, screen } from '@testing-library/react';
5+
import '@testing-library/jest-dom';
26

37
import PreupgradeReports from '../PreupgradeReports';
8+
import {
9+
preupgradeReports,
10+
preupgradeReportsWithFixableEntries,
11+
} from './PreupgradeReports.fixtures';
412

5-
import { preupgradeReports } from './PreupgradeReports.fixtures';
13+
jest.mock('foremanReact/components/Pagination', () => {
14+
const MockPagination = () => <div data-testid="pagination">Pagination</div>;
15+
return MockPagination;
16+
});
17+
18+
jest.mock('../../PreupgradeReportsList/components/images/i_severity-high.svg', () => 'severity-high.svg');
19+
jest.mock('../../PreupgradeReportsList/components/images/i_severity-med.svg', () => 'severity-med.svg');
20+
jest.mock('../../PreupgradeReportsList/components/images/i_severity-low.svg', () => 'severity-low.svg');
21+
22+
const mockStore = configureMockStore([]);
623

724
const csrfToken = 'xyz';
825
const newJobInvocationUrl = '/job_invocations/new';
9-
const getPreupgradeReports = () => {};
10-
11-
const fixtures = {
12-
'should render when loaded with reports': {
13-
loading: false,
14-
error: {},
15-
preupgradeReports,
16-
csrfToken,
17-
newJobInvocationUrl,
18-
getPreupgradeReports,
19-
reportsExpected: true,
20-
},
21-
'should render when loaded without reports': {
22-
loading: false,
23-
error: {},
24-
preupgradeReports: [],
25-
csrfToken,
26-
newJobInvocationUrl,
27-
getPreupgradeReports,
28-
reportsExpected: true,
29-
},
30-
'should render when loading': {
31-
loading: true,
32-
error: {},
33-
preupgradeReports: [],
34-
csrfToken,
35-
newJobInvocationUrl,
36-
getPreupgradeReports,
37-
reportsExpected: false,
38-
},
39-
'should render error': {
40-
loading: false,
41-
error: {
42-
statusText: 'Internal server error',
43-
errorMsg: 'Well, this is embarassing',
44-
},
45-
preupgradeReports: [],
46-
csrfToken,
47-
newJobInvocationUrl,
48-
getPreupgradeReports,
49-
reportsExpected: false,
50-
},
26+
27+
const defaultProps = {
28+
loading: false,
29+
error: {},
30+
preupgradeReports,
31+
csrfToken,
32+
newJobInvocationUrl,
33+
reportsExpected: true,
5134
};
5235

53-
describe('PreupgradeReports', () =>
54-
testComponentSnapshotsWithFixtures(PreupgradeReports, fixtures));
36+
const renderComponent = (props = {}) =>
37+
render(
38+
<Provider store={mockStore({})}>
39+
<PreupgradeReports {...defaultProps} {...props} />
40+
</Provider>
41+
);
42+
43+
const getFixSelectedForm = () =>
44+
screen.getByRole('button', { name: 'Fix Selected' }).closest('form');
45+
46+
describe('PreupgradeReports', () => {
47+
// withLoadingState wraps content in patternfly-react LoadingState, which
48+
// delays showing the spinner by 300ms and always schedules that timeout on
49+
// mount — even when loading=false. Without fake timers the timeout fires
50+
// after the test unmounts and React logs a setState-on-unmounted warning.
51+
beforeEach(() => {
52+
jest.useFakeTimers();
53+
});
54+
55+
afterEach(() => {
56+
jest.runOnlyPendingTimers();
57+
jest.useRealTimers();
58+
});
59+
60+
it('renders report entries when loaded with reports', () => {
61+
renderComponent();
62+
63+
expect(screen.getByText('Fix me!')).toBeInTheDocument();
64+
expect(screen.getByText('I am broken too')).toBeInTheDocument();
65+
expect(screen.getByText('Octocat is not happy')).toBeInTheDocument();
66+
});
67+
68+
it('renders empty state when loaded without reports', () => {
69+
renderComponent({ preupgradeReports: [] });
70+
71+
expect(
72+
screen.getByRole('heading', {
73+
name: 'No Preupgrade Report Available',
74+
level: 5,
75+
})
76+
).toBeInTheDocument();
77+
expect(
78+
screen.getByText(
79+
'The preupgrade report could not be generated, check the job details for the reason'
80+
)
81+
).toBeInTheDocument();
82+
});
83+
84+
it('renders loading state while data is being fetched', () => {
85+
renderComponent({
86+
loading: true,
87+
preupgradeReports: [],
88+
reportsExpected: false,
89+
});
90+
91+
// LoadingState renders nothing until its 300ms timeout elapses.
92+
jest.advanceTimersByTime(300);
93+
94+
expect(screen.getByText('Loading')).toBeInTheDocument();
95+
expect(
96+
screen.queryByRole('heading', {
97+
name: 'No Preupgrade Report Available',
98+
})
99+
).not.toBeInTheDocument();
100+
});
101+
102+
it('renders error empty state when data retrieval fails', () => {
103+
renderComponent({
104+
error: {
105+
statusText: 'Internal server error',
106+
errorMsg: 'Unexpected error',
107+
},
108+
preupgradeReports: [],
109+
reportsExpected: false,
110+
});
111+
112+
expect(
113+
screen.getByRole('heading', {
114+
name: 'Could not retrieve data: Internal server error - Unexpected error',
115+
level: 5,
116+
})
117+
).toBeInTheDocument();
118+
});
119+
120+
it('disables Fix Selected when no fixable entries are selected', () => {
121+
renderComponent({ preupgradeReports: preupgradeReportsWithFixableEntries });
122+
123+
expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeDisabled();
124+
});
125+
126+
it('enables Fix Selected and passes selected entry ids when a fixable entry is selected', () => {
127+
renderComponent({ preupgradeReports: preupgradeReportsWithFixableEntries });
128+
129+
const [, fixableEntryCheckbox] = screen.getAllByRole('checkbox');
130+
131+
fireEvent.click(fixableEntryCheckbox);
132+
133+
expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeEnabled();
134+
expect(
135+
getFixSelectedForm().querySelector('input[name="inputs[remediation_ids]"]')
136+
).toHaveValue('100');
137+
});
138+
139+
it('selects all fixable entries when header checkbox is clicked', () => {
140+
renderComponent({ preupgradeReports: preupgradeReportsWithFixableEntries });
141+
142+
const [selectAllCheckbox] = screen.getAllByRole('checkbox');
143+
144+
fireEvent.click(selectAllCheckbox);
145+
146+
expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeEnabled();
147+
148+
const form = getFixSelectedForm();
149+
150+
expect(
151+
form.querySelector('input[name="inputs[remediation_ids]"]')
152+
).toHaveValue('100,102');
153+
expect(
154+
[...form.querySelectorAll('input[name="host_ids[]"]')].map(
155+
input => input.value
156+
)
157+
).toEqual(['5', '6']);
158+
});
159+
});

0 commit comments

Comments
 (0)