Skip to content

Commit

Permalink
Refs #38107 - fix empty state, fix page caps, do more testing
Browse files Browse the repository at this point in the history
  • Loading branch information
ianballou committed Jan 22, 2025
1 parent 6928bdf commit 7edac87
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 21 deletions.
2 changes: 1 addition & 1 deletion lib/katello/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@

menu :top_menu,
:booted_container_images,
:caption => N_('Booted container images'),
:caption => N_('Booted Container Images'),
:url_hash => {:controller => 'katello/api/v2/host_bootc_images',
:action => 'bootc_images'},
:url => '/booted_container_images',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import {
import Pagination from 'foremanReact/components/Pagination';
import EmptyPage from 'foremanReact/routes/common/EmptyPage';
import { translate as __ } from 'foremanReact/common/I18n';
import { useForemanHostsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
import BOOTED_CONTAINER_IMAGES_KEY, { BOOTED_CONTAINER_IMAGES_API_PATH } from './BootedContainerImagesConstants';

const BootedContainerImagesPage = () => {
const foremanHostsPageUrl = useForemanHostsPageUrl();
const columns = {
bootc_booted_image: {
title: __('Image name'),
Expand All @@ -33,7 +35,7 @@ const BootedContainerImagesPage = () => {
hosts: {
title: __('Hosts'),
wrapper: ({ bootc_booted_image: bootcBootedImage, digests }) => (
<a href={`/hosts?search=bootc_booted_image%20=%20${bootcBootedImage}`}>{digests.reduce((total, digest) => total + digest.host_count, 0)}</a>
<a href={`${foremanHostsPageUrl}?search=bootc_booted_image%20=%20${bootcBootedImage}`}>{digests.reduce((total, digest) => total + digest.host_count, 0)}</a>
),
},
};
Expand Down Expand Up @@ -145,7 +147,7 @@ const BootedContainerImagesPage = () => {
</Td>
</Tr>
)}
{!status === STATUS.PENDING &&
{!(status === STATUS.PENDING) &&
results.length === 0 &&
!errorMessage && (
<Tr ouiaId="table-empty">
Expand Down Expand Up @@ -200,16 +202,16 @@ const BootedContainerImagesPage = () => {
<TableComposable variant="compact" isStriped ouiaId={`table-composable-expanded-${rowIndex}`}>
<Thead>
<Tr ouiaId={`table-row-inner-expandable-${rowIndex}`}>
<Th width={50}>{__('Image digest')}</Th>
<Th width={50}>{__('Hosts')}</Th>
<Th width={55}>{__('Image digest')}</Th>
<Th width={45}>{__('Hosts')}</Th>
</Tr>
</Thead>
<Tbody>
{digests.map((digest, index) => (
<Tr key={digest.bootc_booted_digest} ouiaId={`table-row-expandable-content-${index}`}>
<Td>{digest.bootc_booted_digest}</Td>
<Td>
<a href={`/hosts?search=bootc_booted_digest%20=%20${digest.bootc_booted_digest}`}>{digest.host_count}</a>
<a href={`${foremanHostsPageUrl}?search=bootc_booted_digest%20=%20${digest.bootc_booted_digest}`}>{digest.host_count}</a>
</Td>
</Tr>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ const bootedContainerImagesResponse = Immutable({
},
{
bootc_booted_digest: 'sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd573',
host_count: 1,
host_count: 2,
},
{
bootc_booted_digest: 'sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd574',
host_count: 1,
host_count: 3,
},
{
bootc_booted_digest: 'sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd575',
host_count: 1,
host_count: 4,
},
],
},
Expand All @@ -32,7 +32,7 @@ const bootedContainerImagesResponse = Immutable({
digests: [
{
bootc_booted_digest: 'sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd576',
host_count: 1,
host_count: 6,
},
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,61 @@
import React from 'react';
import { renderWithRedux, patientlyWaitFor, act } from 'react-testing-lib-wrapper';
import { nockInstance, assertNockRequest, mockAutocomplete } from '../../../test-utils/nockWrapper';
import api from '../../../services/api';
import BOOTED_CONTAINER_IMAGES_KEY from '../BootedContainerImagesConstants';
import BootedContainerImagesPage from '../BootedContainerImagesPage';
import bootcImagesData from './bootedContainerImages.fixtures';

// const bootedContainerImagesIndexPath = api.getApiUrl('/booted_container_images');
// const renderOptions = { apiNamespace: BOOTED_CONTAINER_IMAGES_KEY };
const bootcImagesUrl = '/api/v2/hosts/bootc_images';
const autocompleteUrl = '/host_bootc_images/auto_complete_search';
const autocompleteQuery = {
search: '',
};

let firstImage;
let secondImage;
const buildBootedImage = id => ({
bootc_booted_image: `quay.io/centos-bootc/centos-bootc:stream${id}`,
digests: [
{
bootc_booted_digest: `sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd57${id}`,
host_count: 1,
},
],
});

const createBootedImages = (amount) => {
const response = {
total: amount,
subtotal: amount,
page: 1,
per_page: 20,
error: null,
search: null,
sort: {
by: 'bootc_booted_image',
order: 'asc',
},
results: [],
};

[...Array(amount).keys()].forEach((_, i) => response.results.push(buildBootedImage(i + 1)));

return response;
};

let centos10Image;
let centos9Image;
let stream10Digest1;
let stream10Digest2;
let stream10Digest3;
let stream10Digest4;
let stream9Digest;
beforeEach(() => {
const { results } = bootcImagesData;
[firstImage, secondImage] = results;
[centos10Image, centos9Image] = results;
[stream10Digest1, stream10Digest2, stream10Digest3, stream10Digest4] =
centos10Image.digests.map(digest => digest.bootc_booted_digest);
stream9Digest = centos9Image.digests[0].bootc_booted_digest;
});

test('BootedContainerImagesPage renders correctly', async (done) => {
test('BootedContainerImagesPage renders correctly expanded', async (done) => {
const autocompleteScope = mockAutocomplete(nockInstance, autocompleteUrl, autocompleteQuery);
const scope = nockInstance
.get(bootcImagesUrl)
Expand All @@ -30,14 +64,170 @@ test('BootedContainerImagesPage renders correctly', async (done) => {
.times(2)
.reply(200, bootcImagesData);

const { queryByText, queryAllByText } = renderWithRedux(<BootedContainerImagesPage />);
expect(queryByText(firstImage.bootc_booted_image)).toBeNull();
const {
queryByText, queryAllByText, queryAllByRole,
} = renderWithRedux(<BootedContainerImagesPage />);

expect(queryByText(centos10Image.bootc_booted_image)).toBeNull();

await patientlyWaitFor(() => {
expect(queryByText(firstImage.bootc_booted_image)).toBeInTheDocument();
// Expand the rows
queryAllByRole('button').find(btn => btn.getAttribute('aria-labelledby') ===
'simple-node1 booted-containers-expander-quay.io/centos-bootc/centos-bootc:stream91').click();
queryAllByRole('button').find(btn => btn.getAttribute('aria-labelledby') ===
'simple-node0 booted-containers-expander-quay.io/centos-bootc/centos-bootc:stream100').click();

// Check that the digest host count links appear
expect(queryAllByText('1').find(link => String(link.getAttribute('href')).includes(stream10Digest1))).toBeVisible();
expect(queryAllByText('2').find(link => String(link.getAttribute('href')).includes(stream10Digest2))).toBeVisible();
expect(queryAllByText('3').find(link => String(link.getAttribute('href')).includes(stream10Digest3))).toBeVisible();
expect(queryAllByText('4').find(link => String(link.getAttribute('href')).includes(stream10Digest4))).toBeVisible();
expect(queryAllByText('6').find(link => String(link.getAttribute('href')).includes(stream9Digest))).toBeVisible();

// Check that the image host count links appear
const links = queryAllByRole('link');
const stream10Link = links.find(link => link.getAttribute('href') === `/hosts?search=bootc_booted_image%20=%20${centos10Image.bootc_booted_image}`);
const stream9Link = links.find(link => link.getAttribute('href') === `/hosts?search=bootc_booted_image%20=%20${centos9Image.bootc_booted_image}`);
expect(stream10Link).toBeVisible();
expect(stream9Link).toBeVisible();

// Check that the image names appear
expect(queryByText(centos10Image.bootc_booted_image)).toBeVisible();
expect(queryByText(centos9Image.bootc_booted_image)).toBeVisible();

// Check that the digest counts appear
// console.log(queryAllByText('4')[0].closest('td'));
expect(queryAllByText('4')[0].closest('td')).toBeVisible();
expect(queryAllByText('1')[1].closest('td')).toBeVisible();

// Check that the digest names appear
expect(queryByText(stream10Digest1).closest('td')).toBeVisible();
expect(queryByText(stream10Digest2).closest('td')).toBeVisible();
expect(queryByText(stream10Digest3).closest('td')).toBeVisible();
expect(queryByText(stream10Digest4).closest('td')).toBeVisible();
expect(queryByText(stream9Digest).closest('td')).toBeVisible();
});

assertNockRequest(autocompleteScope);
assertNockRequest(scope, done);
act(done);
});

test('BootedContainerImagesPage renders correctly unexpanded', async (done) => {
const autocompleteScope = mockAutocomplete(nockInstance, autocompleteUrl, autocompleteQuery);
const scope = nockInstance
.get(bootcImagesUrl)
.query(true)
// Why does the page load twice?
.times(2)
.reply(200, bootcImagesData);

const {
queryByText, queryAllByText, queryAllByRole,
} = renderWithRedux(<BootedContainerImagesPage />);

expect(queryByText(centos10Image.bootc_booted_image)).toBeNull();

await patientlyWaitFor(() => {
// Check that the digest host count links don't appear
expect(queryAllByText('1').find(link => String(link.getAttribute('href')).includes(stream10Digest1))).not.toBeVisible();
expect(queryAllByText('2').find(link => String(link.getAttribute('href')).includes(stream10Digest2))).not.toBeVisible();
expect(queryAllByText('3').find(link => String(link.getAttribute('href')).includes(stream10Digest3))).not.toBeVisible();
expect(queryAllByText('4').find(link => String(link.getAttribute('href')).includes(stream10Digest4))).not.toBeVisible();
expect(queryAllByText('6').find(link => String(link.getAttribute('href')).includes(stream9Digest))).not.toBeVisible();

// Check that the image host count links appear
const links = queryAllByRole('link');
const stream10Link = links.find(link => link.getAttribute('href') === `/hosts?search=bootc_booted_image%20=%20${centos10Image.bootc_booted_image}`);
const stream9Link = links.find(link => link.getAttribute('href') === `/hosts?search=bootc_booted_image%20=%20${centos9Image.bootc_booted_image}`);
expect(stream10Link).toBeVisible();
expect(stream9Link).toBeVisible();

// Check that the image names appear
expect(queryByText(centos10Image.bootc_booted_image)).toBeVisible();
expect(queryByText(centos9Image.bootc_booted_image)).toBeVisible();

// Check that the digest counts appear
expect(queryAllByText('4')[0].closest('td')).toBeVisible();
expect(queryAllByText('1')[1].closest('td')).toBeVisible();

// Check that the digest names don't appear
expect(queryByText(stream10Digest1).closest('td')).not.toBeVisible();
expect(queryByText(stream10Digest2).closest('td')).not.toBeVisible();
expect(queryByText(stream10Digest3).closest('td')).not.toBeVisible();
expect(queryByText(stream10Digest4).closest('td')).not.toBeVisible();
expect(queryByText(stream9Digest).closest('td')).not.toBeVisible();
});

assertNockRequest(autocompleteScope);
assertNockRequest(scope, done);
act(done);
});

test('Can handle no booted images being present', async (done) => {
const autocompleteScope = mockAutocomplete(nockInstance, autocompleteUrl, autocompleteQuery);
const noResults = {
total: 0,
subtotal: 0,
page: 1,
per_page: 20,
results: [],
};
const scope = nockInstance
.get(bootcImagesUrl)
.query(true)
// Why does the page load twice?
.times(2)
.reply(200, noResults);
const { queryByText } = renderWithRedux(<BootedContainerImagesPage />);

expect(queryByText(centos10Image.bootc_booted_image)).toBeNull();
await patientlyWaitFor(() => expect(queryByText('No Results')).toBeInTheDocument());
assertNockRequest(autocompleteScope);
assertNockRequest(scope, done);
});

test('Can handle pagination', async (done) => {
const largeBootcData = createBootedImages(100);
const { results } = largeBootcData;
const bootcFirstPage = { ...largeBootcData, ...{ results: results.slice(0, 20) } };
const bootcSecondPage = { ...largeBootcData, page: 2, results: results.slice(20, 40) };
const autocompleteScope = mockAutocomplete(nockInstance, autocompleteUrl, autocompleteQuery);

// Match first page API request
const firstPageScope = nockInstance
.get(bootcImagesUrl)
.query(true)
.times(2)
.reply(200, bootcFirstPage);

// Match second page API request
const secondPageScope = nockInstance
.get(bootcImagesUrl)
// Using a custom query params matcher because parameters can be strings
.query(actualQueryObject => (parseInt(actualQueryObject.page, 10) === 2))
.reply(200, bootcSecondPage);
const { queryByText, getAllByLabelText } = renderWithRedux(<BootedContainerImagesPage />);
// Wait for first paginated page to load and assert only the first page of results are present
await patientlyWaitFor(() => {
expect(queryByText(results[0].bootc_booted_image)).toBeInTheDocument();
expect(queryByText(results[19].bootc_booted_image)).toBeInTheDocument();
expect(queryByText(results[21].bootc_booted_image)).not.toBeInTheDocument();
});

// Label comes from patternfly, if this test fails, check if patternfly updated the label.
const [top, bottom] = getAllByLabelText('Go to next page');
expect(top).toBeInTheDocument();
expect(bottom).toBeInTheDocument();
bottom.click();
// Wait for second paginated page to load and assert only the second page of results are present
await patientlyWaitFor(() => {
expect(queryByText(results[20].bootc_booted_image)).toBeInTheDocument();
expect(queryByText(results[39].bootc_booted_image)).toBeInTheDocument();
expect(queryByText(results[41].bootc_booted_image)).not.toBeInTheDocument();
});
assertNockRequest(autocompleteScope);
assertNockRequest(firstPageScope);
assertNockRequest(secondPageScope, done);
act(done);
});

0 comments on commit 7edac87

Please sign in to comment.