Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@
white-space: pre-wrap;
}
}

.leapp-expanded-tbody {
border: 1px solid var(--pf-v5-global--BorderColor--100);
box-shadow: var(--pf-v5-global--BoxShadow--sm);

tr:first-child > td {
background-color: var(--pf-v5-global--primary-color--light-background, #e7f1fa);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';

import {
fireEvent,
render,
screen,
waitFor,
fireEvent,
within,
} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';

import { APIActions } from 'foremanReact/redux/API';
import PreupgradeReportsTable from '../index';
import { Provider } from 'react-redux';
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { APIActions } from 'foremanReact/redux/API';
import PreupgradeReportsTable from '../index';

jest.mock('foremanReact/redux/API');

Expand All @@ -24,17 +26,25 @@ const mockJobData = {
template_name: 'Run preupgrade via Leapp',
};

// Entry 0 (id=1): command remediation + inhibitor flag → fixable + selectable
// Entry 1 (id=2): hint-only remediation → has_remediation=Yes, NOT selectable
// Entries 2-11: no remediations → has_remediation=No, NOT selectable
const mockEntries = Array.from({ length: 12 }, (_, i) => ({
id: i + 1,
title: `Report Entry ${i + 1}`,
hostname: 'example.com',
host_id: 100 + i,
severity: i === 0 ? 'high' : 'low',
summary: `Summary for report entry ${i + 1}`,
tags: i === 0 ? ['security', 'network'] : [],
flags: i === 0 ? ['inhibitor'] : [],
detail: {
remediations:
i === 0 ? [{ type: 'command', context: ['echo', 'fix_command'] }] : [],
i === 0
? [{ type: 'command', context: ['echo', 'fix_command'] }]
: i === 1
? [{ type: 'hint', context: 'Do something manually' }]
: null,
external:
i === 0 ? [{ url: 'http://example.com', title: 'External Link' }] : [],
},
Expand All @@ -59,6 +69,8 @@ describe('PreupgradeReportsTable', () => {
return { type: 'MOCK_API_SUCCESS' };
};
});

APIActions.post.mockImplementation(() => () => ({ type: 'MOCK_API_POST' }));
});

const renderComponent = (data = mockJobData) =>
Expand All @@ -68,28 +80,52 @@ describe('PreupgradeReportsTable', () => {
</Provider>
);

const expandSection = () => {
const expandSection = () =>
fireEvent.click(screen.getByText('Leapp preupgrade report'));
};

const waitForTable = () =>
waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));

it('renders data', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));
await waitForTable();
expect(
screen.getByText('Report Entry 1', { selector: 'td' })
).toBeInTheDocument();
});

it('expands a row and shows details', async () => {
it('does not render anything for non-Leapp jobs', () => {
renderComponent({ id: 55, template_name: 'Standard RHEL Update' });
expect(
screen.queryByText('Leapp preupgrade report')
).not.toBeInTheDocument();
});

it('renders empty state message when no issues found', async () => {
APIActions.get.mockImplementation(({ key, handleSuccess }) => {
return () => {
if (key.includes('GET_LEAPP_REPORT_LIST'))
handleSuccess({ results: [{ id: mockReportId }] });
if (key.includes('GET_LEAPP_REPORT_DETAIL'))
handleSuccess({ id: mockReportId, preupgrade_report_entries: [] });
return { type: 'EMPTY' };
};
});
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));

const rowExpandButtons = screen.getAllByLabelText('Details');
fireEvent.click(rowExpandButtons[0]);
await waitFor(() =>
expect(
screen.getByText('The preupgrade report shows no issues.')
).toBeInTheDocument()
);
});

expect(await screen.findByText('Summary')).toBeInTheDocument();
it('expands a row and shows details', async () => {
renderComponent();
expandSection();
await waitForTable();
fireEvent.click(screen.getAllByLabelText('Details')[0]);
expect(
await screen.findByText('Summary for report entry 1')
).toBeInTheDocument();
Expand All @@ -98,11 +134,8 @@ describe('PreupgradeReportsTable', () => {
it('expands all rows', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));

const expandAllButton = screen.getByLabelText('Expand all rows');
fireEvent.click(expandAllButton);

await waitForTable();
fireEvent.click(screen.getByLabelText('Expand all rows'));
expect(
await screen.findByText('Summary for report entry 1')
).toBeInTheDocument();
Expand All @@ -114,11 +147,9 @@ describe('PreupgradeReportsTable', () => {
it('paginates to the next page', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));

await waitForTable();
fireEvent.click(screen.getAllByLabelText('Go to next page')[0]);
await waitFor(() => screen.getByText('Report Entry 6', { selector: 'td' }));

expect(
screen.getByText('Report Entry 10', { selector: 'td' })
).toBeInTheDocument();
Expand All @@ -127,19 +158,168 @@ describe('PreupgradeReportsTable', () => {
it('changes perPage limit to 10', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));

await waitForTable();
fireEvent.click(screen.getAllByLabelText('Items per page')[0]);
fireEvent.click(screen.getAllByText('10 per page')[0]);

await waitFor(() => {
await waitFor(() =>
expect(
screen.getByText('Report Entry 10', { selector: 'td' })
).toBeInTheDocument();
});
).toBeInTheDocument()
);
});

it('renders empty state message when no issues found', async () => {
it('displays correct inhibitor status based on flags', async () => {
renderComponent();
expandSection();
await waitForTable();

const row1 = screen
.getByText('Report Entry 1', { selector: 'td' })
.closest('tr');
expect(
within(row1.querySelector('td[data-label="Inhibitor?"]')).getByText('Yes')
).toBeInTheDocument();

const row2 = screen
.getByText('Report Entry 2', { selector: 'td' })
.closest('tr');
expect(
within(row2.querySelector('td[data-label="Inhibitor?"]')).getByText('No')
).toBeInTheDocument();
});

it('shows Has Remediation? Yes for any remediation type, not only command', async () => {
renderComponent();
expandSection();
await waitForTable();

// id=1: command → Yes
const row1 = screen
.getByText('Report Entry 1', { selector: 'td' })
.closest('tr');
expect(
within(row1.querySelector('td[data-label="Has Remediation?"]')).getByText(
'Yes'
)
).toBeInTheDocument();

// id=2: hint-only → still Yes (display column shows any remediations)
const row2 = screen
.getByText('Report Entry 2', { selector: 'td' })
.closest('tr');
expect(
within(row2.querySelector('td[data-label="Has Remediation?"]')).getByText(
'Yes'
)
).toBeInTheDocument();

// id=3: no remediations → No
const row3 = screen
.getByText('Report Entry 3', { selector: 'td' })
.closest('tr');
expect(
within(row3.querySelector('td[data-label="Has Remediation?"]')).getByText(
'No'
)
).toBeInTheDocument();
});

it('renders Fix Selected button disabled initially', async () => {
renderComponent();
expandSection();
await waitForTable();
expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeDisabled();
});

it('enables Fix Selected after selecting a fixable (command) row', async () => {
renderComponent();
expandSection();
await waitForTable();

// checkboxes[0] = SelectAll, checkboxes[1] = entry id=1 (command remediation)
fireEvent.click(screen.getAllByRole('checkbox')[1]);

await waitFor(() =>
expect(
screen.getByRole('button', { name: 'Fix Selected' })
).not.toBeDisabled()
);
});

it('Fix Selected dispatches APIActions.post with correct feature, host_ids and remediation_ids', async () => {
renderComponent();
expandSection();
await waitForTable();

fireEvent.click(screen.getAllByRole('checkbox')[1]); // select entry id=1 (host_id=100)

await waitFor(() =>
expect(
screen.getByRole('button', { name: 'Fix Selected' })
).not.toBeDisabled()
);

fireEvent.click(screen.getByRole('button', { name: 'Fix Selected' }));

expect(APIActions.post).toHaveBeenCalledWith(
expect.objectContaining({
url: expect.stringContaining('/api/job_invocations'),
params: {
job_invocation: {
feature: 'leapp_remediation_plan',
host_ids: [100],
inputs: { remediation_ids: '1' },
},
},
})
);
});

it('hint-only row checkbox is disabled and does not enable Fix Selected', async () => {
renderComponent();
expandSection();
await waitForTable();

// checkboxes[2] = entry id=2, which has hint-only remediation — must be disabled
const hintCheckbox = screen.getAllByRole('checkbox')[2];
expect(hintCheckbox).toBeDisabled();

fireEvent.click(hintCheckbox);
expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeDisabled();
});

it('renders Run Upgrade button enabled when entries are present', async () => {
renderComponent();
expandSection();
await waitForTable();
expect(
screen.getByRole('button', { name: 'Run Upgrade' })
).not.toBeDisabled();
});

it('Run Upgrade dispatches APIActions.post with correct feature and all host_ids', async () => {
renderComponent();
expandSection();
await waitForTable();

fireEvent.click(screen.getByRole('button', { name: 'Run Upgrade' }));

expect(APIActions.post).toHaveBeenCalledWith(
expect.objectContaining({
url: expect.stringContaining('/api/job_invocations'),
params: {
job_invocation: {
feature: 'leapp_upgrade',
host_ids: expect.arrayContaining([100, 101, 102]),
},
},
})
);
const callParams = APIActions.post.mock.calls[0][0].params.job_invocation;
expect(callParams.inputs).toBeUndefined();
});

it('does not render toolbar buttons when report has no entries', async () => {
APIActions.get.mockImplementation(({ key, handleSuccess }) => {
return () => {
if (key.includes('GET_LEAPP_REPORT_LIST'))
Expand All @@ -151,36 +331,35 @@ describe('PreupgradeReportsTable', () => {
});
renderComponent();
expandSection();
await waitFor(() => {
expect(
screen.getByText('The preupgrade report shows no issues.')
).toBeInTheDocument();
});
});

it('does not render anything for non-Leapp jobs', () => {
const nonLeappData = { id: 55, template_name: 'Standard RHEL Update' };
renderComponent(nonLeappData);
await waitFor(() =>
screen.getByText('The preupgrade report shows no issues.')
);
expect(
screen.queryByText('Leapp preupgrade report')
screen.queryByRole('button', { name: 'Fix Selected' })
).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'Run Upgrade' })
).not.toBeInTheDocument();
});

it('displays correct inhibitor status based on flags', async () => {
it('renders the SelectAll checkbox', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));
await waitForTable();
expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0);
});

const row1 = screen
.getByText('Report Entry 1', { selector: 'td' })
.closest('tr');
const inhibitorCell1 = row1.querySelector('td[data-label="Inhibitor?"]');
expect(within(inhibitorCell1).getByText('Yes')).toBeInTheDocument();
it('selecting all fixable rows enables Fix Selected', async () => {
renderComponent();
expandSection();
await waitForTable();

const row2 = screen
.getByText('Report Entry 2', { selector: 'td' })
.closest('tr');
const inhibitorCell2 = row2.querySelector('td[data-label="Inhibitor?"]');
expect(within(inhibitorCell2).getByText('No')).toBeInTheDocument();
fireEvent.click(screen.getByLabelText('Select all'));

await waitFor(() =>
expect(
screen.getByRole('button', { name: 'Fix Selected' })
).not.toBeDisabled()
);
});
});
Loading
Loading