From 68d69da6c1852f2cc9e128227228db6249081c4a Mon Sep 17 00:00:00 2001 From: Ian Ballou Date: Thu, 16 Jan 2025 22:27:47 +0000 Subject: [PATCH] Refs #38107 - fix empty state, fix page caps, do more testing --- lib/katello/plugin.rb | 2 +- .../BootedContainerImagesPage.js | 6 +- .../bootedContainerImages.fixtures.js | 8 +- .../bootedContainerImagesPage.test.js | 212 +++++++++++++++++- 4 files changed, 209 insertions(+), 19 deletions(-) diff --git a/lib/katello/plugin.rb b/lib/katello/plugin.rb index 87b0f6f9337..6f2d2b3ff76 100644 --- a/lib/katello/plugin.rb +++ b/lib/katello/plugin.rb @@ -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', diff --git a/webpack/scenes/BootedContainerImages/BootedContainerImagesPage.js b/webpack/scenes/BootedContainerImages/BootedContainerImagesPage.js index 39e31b1a7d9..862590012a9 100644 --- a/webpack/scenes/BootedContainerImages/BootedContainerImagesPage.js +++ b/webpack/scenes/BootedContainerImages/BootedContainerImagesPage.js @@ -145,7 +145,7 @@ const BootedContainerImagesPage = () => { )} - {!status === STATUS.PENDING && + {!(status === STATUS.PENDING) && results.length === 0 && !errorMessage && ( @@ -200,8 +200,8 @@ const BootedContainerImagesPage = () => { - {__('Image digest')} - {__('Hosts')} + {__('Image digest')} + {__('Hosts')} diff --git a/webpack/scenes/BootedContainerImages/__tests__/bootedContainerImages.fixtures.js b/webpack/scenes/BootedContainerImages/__tests__/bootedContainerImages.fixtures.js index 2abdebe0547..ad589d98c24 100644 --- a/webpack/scenes/BootedContainerImages/__tests__/bootedContainerImages.fixtures.js +++ b/webpack/scenes/BootedContainerImages/__tests__/bootedContainerImages.fixtures.js @@ -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, }, ], }, @@ -32,7 +32,7 @@ const bootedContainerImagesResponse = Immutable({ digests: [ { bootc_booted_digest: 'sha256:54256a998f0c62e16f3927c82b570f90bd8449a52e03daabd5fd16d6419fd576', - host_count: 1, + host_count: 6, }, ], }, diff --git a/webpack/scenes/BootedContainerImages/__tests__/bootedContainerImagesPage.test.js b/webpack/scenes/BootedContainerImages/__tests__/bootedContainerImagesPage.test.js index b31e5875172..8a29f4efdca 100644 --- a/webpack/scenes/BootedContainerImages/__tests__/bootedContainerImagesPage.test.js +++ b/webpack/scenes/BootedContainerImages/__tests__/bootedContainerImagesPage.test.js @@ -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) @@ -30,14 +64,170 @@ test('BootedContainerImagesPage renders correctly', async (done) => { .times(2) .reply(200, bootcImagesData); - const { queryByText, queryAllByText } = renderWithRedux(); - expect(queryByText(firstImage.bootc_booted_image)).toBeNull(); + const { + queryByText, queryAllByText, queryAllByRole, + } = renderWithRedux(); + + 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(); + + 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(); + + 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(); + // 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); +});