ImagesTable: Convert ImagesTable to Typescript & RTK Query

This commit converts the Images Table to Typescript and converts all API
calls to image-builder to use RTK Query hooks.

This should increase the performance of the app significantly.
Previously our calls to the image-builder API were made in series. They
are now made in parallel. We may want to investigate the possibility of
hitting rate limiting now that we will be issuing requests in much more
rapid succession.

In the tests, moving to RTK Query hooks has allowed us to remove
virtually all Jest mocking. However, this means that some of our
previous tests which tested against implementation details were broken.
Most notably, we no longer check the Redux store to verify that clones
have been added correctly and we no longer check that compose requests
were issued successfully. Test coverage will be restored in a follow-up
PR where the dev-dependency @msw/data is added. Adding a persistent data
layer to the tests using @msw/data will allow us to verify that our POST
requests (creating composes and cloning them) are working by testing
that the Images Table has been updated.
This commit is contained in:
lucasgarfield 2023-08-01 14:09:09 +02:00 committed by Thomas Lavocat
parent 155a0cf57c
commit 7b9e726151
34 changed files with 2397 additions and 2499 deletions

View file

@ -75,7 +75,11 @@ const getSourceDropdown = async () => {
describe('Step Upload to Azure', () => {
const user = userEvent.setup();
const setUp = async () => {
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
({ router } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
));
// select Azure as upload destination
await user.click(await screen.findByTestId('upload-azure'));

View file

@ -8,11 +8,6 @@ import userEvent from '@testing-library/user-event';
import api from '../../../api.js';
import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard';
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
import { store } from '../../../store/index.js';
import {
mockComposesEmpty,
mockStateRecreateImage,
} from '../../fixtures/composes';
import {
mockPkgResultAlpha,
mockPkgResultAlphaContentSources,
@ -63,14 +58,6 @@ jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
}),
}));
// Mocking getComposes is necessary because in many tests we call navigate()
// to navigate to the images table (via useNavigate hook), which will in turn
// result in a call to getComposes. If it is not mocked, tests fail due to MSW
// being unable to resolve that endpoint.
jest
.spyOn(api, 'getComposes')
.mockImplementation(() => Promise.resolve(mockComposesEmpty));
const searchForAvailablePackages = async (searchbox, searchTerm) => {
const user = userEvent.setup();
await user.type(searchbox, searchTerm);
@ -117,7 +104,7 @@ describe('Step Packages', () => {
const setUp = async () => {
mockContentSourcesEnabled = false;
({ router } = renderCustomRoutesWithReduxRouter(
({ router } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
@ -877,35 +864,21 @@ describe('Step Custom repositories', () => {
describe('On Recreate', () => {
const user = userEvent.setup();
const setUp = async () => {
jest.mock('../../../store/index.js');
const state = mockStateRecreateImage;
store.getState = () => state;
({ router } = renderWithReduxRouter(
'imagewizard/hyk93673-8dcc-4a61-ac30-e9f4940d8346',
state
'imagewizard/hyk93673-8dcc-4a61-ac30-e9f4940d8346'
));
};
const setUpUnavailableRepo = async () => {
jest.mock('../../../store/index.js');
const state = mockStateRecreateImage;
store.getState = () => state;
({ router } = renderWithReduxRouter(
'imagewizard/b7193673-8dcc-4a5f-ac30-e9f4940d8346',
state
'imagewizard/b7193673-8dcc-4a5f-ac30-e9f4940d8346'
));
};
test('with valid repositories', async () => {
await setUp();
screen.getByRole('heading', { name: /review/i });
await screen.findByRole('heading', { name: /review/i });
expect(
screen.queryByText('Previously added custom repository unavailable')
).not.toBeInTheDocument();
@ -943,7 +916,7 @@ describe('On Recreate', () => {
test('with repositories that are no longer available', async () => {
await setUpUnavailableRepo();
screen.getByRole('heading', { name: /review/i });
await screen.findByRole('heading', { name: /review/i });
await screen.findByText('Previously added custom repository unavailable');
const createImageButton = await screen.findByRole('button', {

View file

@ -11,12 +11,9 @@ import {
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import api from '../../../api.js';
import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard';
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
import { RHEL_8, RHEL_9, PROVISIONING_API } from '../../../constants.js';
import { mockComposesEmpty } from '../../fixtures/composes';
import { customizations } from '../../fixtures/customizations';
import { PROVISIONING_API } from '../../../constants.js';
import { server } from '../../mocks/server.js';
import {
clickBack,
@ -48,9 +45,9 @@ let router = undefined;
// to navigate to the images table (via useNavigate hook), which will in turn
// result in a call to getComposes. If it is not mocked, tests fail due to MSW
// being unable to resolve that endpoint.
jest
.spyOn(api, 'getComposes')
.mockImplementation(() => Promise.resolve(mockComposesEmpty));
// jest
// .spyOn(api, 'getComposes')
// .mockImplementation(() => Promise.resolve(mockComposesEmpty));
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
useChrome: () => ({
@ -137,7 +134,11 @@ describe('Create Image Wizard', () => {
describe('Step Image output', () => {
const user = userEvent.setup();
const setUp = async () => {
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
({ router } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
));
// select aws as upload destination
await user.click(await screen.findByTestId('upload-aws'));
@ -270,7 +271,7 @@ describe('Step Image output', () => {
describe('Step Upload to AWS', () => {
const user = userEvent.setup();
const setUp = async () => {
({ router, store } = renderCustomRoutesWithReduxRouter(
({ router, store } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
@ -399,44 +400,38 @@ describe('Step Upload to AWS', () => {
await clickNext();
await clickNext();
const composeImage = jest
.spyOn(api, 'composeImage')
.mockImplementation((body) => {
expect(body).toEqual({
distribution: RHEL_9,
image_name: undefined,
customizations: {
packages: undefined,
},
image_requests: [
{
architecture: 'x86_64',
image_type: 'aws',
upload_request: {
type: 'aws',
options: {
share_with_sources: ['123'],
},
},
},
],
});
const id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5a';
return Promise.resolve({ id });
});
// const composeImage = jest
// .spyOn(api, 'composeImage')
// .mockImplementation((body) => {
// expect(body).toEqual({
// distribution: RHEL_9,
// image_name: undefined,
// customizations: {
// packages: undefined,
// },
// image_requests: [
// {
// architecture: 'x86_64',
// image_type: 'aws',
// upload_request: {
// type: 'aws',
// options: {
// share_with_sources: ['123'],
// },
// },
// },
// ],
// });
// const id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5a';
// return Promise.resolve({ id });
// });
user.click(screen.getByRole('button', { name: /Create/ }));
// API request sent to backend
await waitFor(() => expect(composeImage).toHaveBeenCalledTimes(1));
// returns back to the landing page
await waitFor(() =>
expect(router.state.location.pathname).toBe('/insights/image-builder')
);
expect(store.getState().composes.allIds).toEqual([
'edbae1c2-62bc-42c1-ae0c-3110ab718f5a',
]);
// set test timeout of 10 seconds
}, 10000);
});
@ -444,7 +439,11 @@ describe('Step Upload to AWS', () => {
describe('Step Upload to Google', () => {
const user = userEvent.setup();
const setUp = async () => {
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
({ router } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
));
// select gcp as upload destination
user.click(screen.getByTestId('upload-google'));
@ -507,7 +506,11 @@ describe('Step Upload to Google', () => {
describe('Step Registration', () => {
const user = userEvent.setup();
const setUp = async () => {
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
({ router } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
));
// select aws as upload destination
user.click(screen.getByTestId('upload-aws'));
@ -693,7 +696,11 @@ describe('Step Registration', () => {
describe('Step File system configuration', () => {
const user = userEvent.setup();
const setUp = async () => {
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
({ router } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
));
// select aws as upload destination
user.click(screen.getByTestId('upload-aws'));
@ -768,7 +775,11 @@ describe('Step File system configuration', () => {
describe('Step Details', () => {
const user = userEvent.setup();
const setUp = async () => {
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
({ router } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
));
// select aws as upload destination
user.click(screen.getByTestId('upload-aws'));
@ -837,7 +848,11 @@ describe('Step Details', () => {
describe('Step Review', () => {
const user = userEvent.setup();
const setUp = async () => {
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
({ router } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
));
// select aws as upload destination
user.click(screen.getByTestId('upload-aws'));
@ -984,7 +999,7 @@ describe('Step Review', () => {
describe('Click through all steps', () => {
const user = userEvent.setup();
const setUp = async () => {
({ router, store } = renderCustomRoutesWithReduxRouter(
({ router, store } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
@ -1051,7 +1066,10 @@ describe('Click through all steps', () => {
await clickNext();
// fsc
(await screen.findByTestId('file-system-config-radio-manual')).click();
const fscToggle = await screen.findByTestId(
'file-system-config-radio-manual'
);
await user.click(fscToggle);
const addPartition = await screen.findByTestId('file-system-add-partition');
await user.click(addPartition);
await user.click(addPartition);
@ -1188,146 +1206,146 @@ describe('Click through all steps', () => {
);
expect(within(revtbody).getAllByRole('row')).toHaveLength(3);
// mock the backend API
const ids = [];
const composeImage = jest
.spyOn(api, 'composeImage')
.mockImplementation((body) => {
let id;
let expectedbody = {};
if (body.image_requests[0].upload_request.type === 'aws') {
expectedbody = {
distribution: RHEL_8,
image_name: 'my-image-name',
image_description: 'this is a perfect description for image',
image_requests: [
{
architecture: 'x86_64',
image_type: 'aws',
upload_request: {
type: 'aws',
options: {
share_with_accounts: ['012345678901'],
},
},
},
],
customizations: customizations,
};
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f56';
} else if (body.image_requests[0].upload_request.type === 'gcp') {
expectedbody = {
distribution: RHEL_8,
image_name: 'my-image-name',
image_description: 'this is a perfect description for image',
image_requests: [
{
architecture: 'x86_64',
image_type: 'gcp',
upload_request: {
type: 'gcp',
options: {
share_with_accounts: ['user:test@test.com'],
},
},
},
],
customizations: customizations,
};
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f57';
} else if (body.image_requests[0].upload_request.type === 'azure') {
expectedbody = {
distribution: RHEL_8,
image_name: 'my-image-name',
image_description: 'this is a perfect description for image',
image_requests: [
{
architecture: 'x86_64',
image_type: 'azure',
upload_request: {
type: 'azure',
options: {
tenant_id: 'b8f86d22-4371-46ce-95e7-65c415f3b1e2',
subscription_id: '60631143-a7dc-4d15-988b-ba83f3c99711',
resource_group: 'testResourceGroup',
},
},
},
],
customizations: customizations,
};
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f58';
} else if (body.image_requests[0].image_type === 'vsphere-ova') {
expectedbody = {
distribution: RHEL_8,
image_name: 'my-image-name',
image_description: 'this is a perfect description for image',
image_requests: [
{
architecture: 'x86_64',
image_type: 'vsphere-ova',
upload_request: {
type: 'aws.s3',
options: {},
},
},
],
customizations: customizations,
};
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f59';
} else if (body.image_requests[0].image_type === 'guest-image') {
expectedbody = {
distribution: RHEL_8,
image_name: 'my-image-name',
image_description: 'this is a perfect description for image',
image_requests: [
{
architecture: 'x86_64',
image_type: 'guest-image',
upload_request: {
type: 'aws.s3',
options: {},
},
},
],
customizations: customizations,
};
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5a';
} else if (body.image_requests[0].image_type === 'image-installer') {
expectedbody = {
distribution: RHEL_8,
image_name: 'my-image-name',
image_description: 'this is a perfect description for image',
image_requests: [
{
architecture: 'x86_64',
image_type: 'image-installer',
upload_request: {
type: 'aws.s3',
options: {},
},
},
],
customizations: customizations,
};
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5b';
}
expect(body).toEqual(expectedbody);
// // mock the backend API
// const ids = [];
// const composeImage = jest
// .spyOn(api, 'composeImage')
// .mockImplementation((body) => {
// let id;
// let expectedbody = {};
// if (body.image_requests[0].upload_request.type === 'aws') {
// expectedbody = {
// distribution: RHEL_8,
// image_name: 'my-image-name',
// image_description: 'this is a perfect description for image',
// image_requests: [
// {
// architecture: 'x86_64',
// image_type: 'aws',
// upload_request: {
// type: 'aws',
// options: {
// share_with_accounts: ['012345678901'],
// },
// },
// },
// ],
// customizations: customizations,
// };
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f56';
// } else if (body.image_requests[0].upload_request.type === 'gcp') {
// expectedbody = {
// distribution: RHEL_8,
// image_name: 'my-image-name',
// image_description: 'this is a perfect description for image',
// image_requests: [
// {
// architecture: 'x86_64',
// image_type: 'gcp',
// upload_request: {
// type: 'gcp',
// options: {
// share_with_accounts: ['user:test@test.com'],
// },
// },
// },
// ],
// customizations: customizations,
// };
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f57';
// } else if (body.image_requests[0].upload_request.type === 'azure') {
// expectedbody = {
// distribution: RHEL_8,
// image_name: 'my-image-name',
// image_description: 'this is a perfect description for image',
// image_requests: [
// {
// architecture: 'x86_64',
// image_type: 'azure',
// upload_request: {
// type: 'azure',
// options: {
// tenant_id: 'b8f86d22-4371-46ce-95e7-65c415f3b1e2',
// subscription_id: '60631143-a7dc-4d15-988b-ba83f3c99711',
// resource_group: 'testResourceGroup',
// },
// },
// },
// ],
// customizations: customizations,
// };
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f58';
// } else if (body.image_requests[0].image_type === 'vsphere-ova') {
// expectedbody = {
// distribution: RHEL_8,
// image_name: 'my-image-name',
// image_description: 'this is a perfect description for image',
// image_requests: [
// {
// architecture: 'x86_64',
// image_type: 'vsphere-ova',
// upload_request: {
// type: 'aws.s3',
// options: {},
// },
// },
// ],
// customizations: customizations,
// };
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f59';
// } else if (body.image_requests[0].image_type === 'guest-image') {
// expectedbody = {
// distribution: RHEL_8,
// image_name: 'my-image-name',
// image_description: 'this is a perfect description for image',
// image_requests: [
// {
// architecture: 'x86_64',
// image_type: 'guest-image',
// upload_request: {
// type: 'aws.s3',
// options: {},
// },
// },
// ],
// customizations: customizations,
// };
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5a';
// } else if (body.image_requests[0].image_type === 'image-installer') {
// expectedbody = {
// distribution: RHEL_8,
// image_name: 'my-image-name',
// image_description: 'this is a perfect description for image',
// image_requests: [
// {
// architecture: 'x86_64',
// image_type: 'image-installer',
// upload_request: {
// type: 'aws.s3',
// options: {},
// },
// },
// ],
// customizations: customizations,
// };
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5b';
// }
// expect(body).toEqual(expectedbody);
ids.unshift(id);
return Promise.resolve({ id });
});
// ids.unshift(id);
// return Promise.resolve({ id });
// });
await user.click(screen.getByRole('button', { name: /Create/ }));
// API request sent to backend
expect(composeImage).toHaveBeenCalledTimes(6);
// expect(composeImage).toHaveBeenCalledTimes(6);
// returns back to the landing page
await waitFor(() =>
expect(router.state.location.pathname).toBe('/insights/image-builder')
);
expect(store.getState().composes.allIds).toEqual(ids);
// expect(store.getState().composes.allIds).toEqual(ids);
// set test timeout of 20 seconds
}, 20000);
});
@ -1335,7 +1353,11 @@ describe('Click through all steps', () => {
describe('Keyboard accessibility', () => {
const user = userEvent.setup();
const setUp = async () => {
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
({ router } = await renderCustomRoutesWithReduxRouter(
'imagewizard',
{},
routes
));
await clickNext();
};

View file

@ -2,21 +2,15 @@ import React from 'react';
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BrowserRouter } from 'react-router-dom';
import { act } from 'react-dom/test-utils';
import api from '../../../api.js';
import { ImageBuildStatus } from '../../../Components/ImagesTable/ImageBuildStatus';
import ImageLink from '../../../Components/ImagesTable/ImageLink';
import Target from '../../../Components/ImagesTable/Target';
import '@testing-library/jest-dom';
import {
mockComposes,
mockStatus,
mockClones,
mockCloneStatus,
mockNoClones,
} from '../../fixtures/composes';
import { renderWithProvider, renderWithReduxRouter } from '../../testUtils';
import { renderWithReduxRouter } from '../../testUtils';
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
useChrome: () => ({
@ -31,24 +25,6 @@ jest.mock('@unleash/proxy-client-react', () => ({
useFlag: jest.fn((flag) => (flag === 'edgeParity.image-list' ? false : true)),
}));
jest
.spyOn(api, 'getComposes')
.mockImplementation(() => Promise.resolve(mockComposes));
jest.spyOn(api, 'getComposeStatus').mockImplementation((id) => {
return Promise.resolve(mockStatus(id));
});
jest.spyOn(api, 'getClones').mockImplementation((id) => {
return id === '1579d95b-8f1d-4982-8c53-8c2afa4ab04c'
? Promise.resolve(mockClones(id))
: Promise.resolve(mockNoClones);
});
jest.spyOn(api, 'getCloneStatus').mockImplementation((id) => {
return Promise.resolve(mockCloneStatus(id));
});
beforeAll(() => {
// scrollTo is not defined in jsdom
window.HTMLElement.prototype.scrollTo = function () {};
@ -57,7 +33,7 @@ beforeAll(() => {
describe('Images Table', () => {
const user = userEvent.setup();
test('render ImagesTable', async () => {
const view = renderWithReduxRouter('', {});
await renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
@ -65,8 +41,6 @@ describe('Images Table', () => {
const emptyState = screen.queryByTestId('empty-state');
expect(emptyState).not.toBeInTheDocument();
const state = view.store.getState();
// check table
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
@ -80,103 +54,107 @@ describe('Images Table', () => {
expect(header.cells[5]).toHaveTextContent('Status');
expect(header.cells[6]).toHaveTextContent('Instance');
const imageNameValues = mockComposes.map((compose) =>
compose.image_name ? compose.image_name : compose.id
);
const statusValues = [
'Ready',
'Image build failed',
'Image build is pending',
'Image build in progress',
'Image upload in progress',
'Cloud registration in progress',
'Image build failed',
'Ready',
'Image build in progress',
'Expired',
];
const targetValues = [
'Amazon Web Services (5)',
'Google Cloud PlatformFAKE',
'Amazon Web Services (1)',
'Amazon Web Services (1)',
'Amazon Web Services (1)',
'Amazon Web Services (1)',
'Amazon Web Services (1)',
'Google Cloud Platform',
'Microsoft Azure',
'VMWare vSphere',
];
const instanceValues = [
'Launch',
'Launch',
'Launch',
'Launch',
'Launch',
'Launch',
'Launch',
'Launch',
'Launch',
'Recreate image',
];
// 10 rows for 10 images
expect(rows).toHaveLength(10);
for (const row of rows) {
const col1 = row.cells[1].textContent;
// find compose with either the user defined image name or the uuid
const compose = mockComposes.data.find(
(compose) => compose?.image_name === col1 || compose.id === col1
);
expect(compose).toBeTruthy();
// date should match the month day and year of the timestamp.
rows.forEach(async (row, index) => {
expect(row.cells[1]).toHaveTextContent(imageNameValues[index]);
expect(row.cells[2]).toHaveTextContent('Apr 27, 2021');
expect(row.cells[3]).toHaveTextContent('RHEL 8.8');
});
// render the expected <ImageBuildStatus /> and compare the text content
const testElement = document.createElement('testElement');
// render(<Target composeId={compose.id} />, { container: testElement });
renderWithProvider(<Target composeId={compose.id} />, testElement, state);
expect(row.cells[4]).toHaveTextContent(testElement.textContent);
let toTest = expect(row.cells[5]);
// render the expected <ImageBuildStatus /> and compare the text content
if (
compose.created_at === '2021-04-27T12:31:12Z' &&
compose.request.image_requests[0].upload_request.type === 'aws.s3'
) {
toTest.toHaveTextContent('Expired');
} else {
renderWithProvider(
<ImageBuildStatus imageId={compose.id} isImagesTableRow={true} />,
testElement,
state
);
toTest.toHaveTextContent(testElement.textContent);
}
toTest = expect(row.cells[6]);
// render the expected <ImageLink /> and compare the text content for a link
if (
compose.created_at === '2021-04-27T12:31:12Z' &&
compose.request.image_requests[0].upload_request.type === 'aws.s3'
) {
toTest.toHaveTextContent('Recreate image');
} else {
renderWithProvider(
<BrowserRouter>
<ImageLink imageId={compose.id} isInClonesTable={false} />
</BrowserRouter>,
testElement,
state
);
toTest.toHaveTextContent(testElement.textContent);
}
}
// TODO Test remaining table content.
});
test('check recreate action', async () => {
const { router } = renderWithReduxRouter('', {});
const { router } = await renderWithReduxRouter('', {});
// get rows
const table = await screen.findByTestId('images-table');
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
const { findAllByRole } = within(table);
const rows = await findAllByRole('row');
// first row is header so look at index 1
const imageId = rows[1].cells[1].textContent;
const actionsButton = within(rows[1]).getByRole('button', {
const actionsButton = await within(rows[1]).findByRole('button', {
name: 'Actions',
});
await waitFor(() => {
expect(actionsButton).toBeEnabled();
});
user.click(actionsButton);
const recreateButton = await screen.findByRole('menuitem', {
name: 'Recreate image',
});
user.click(recreateButton);
act(() => {
user.click(recreateButton);
});
await waitFor(() =>
expect(router.state.location.pathname).toBe(
`/insights/image-builder/imagewizard/${imageId}`
'/insights/image-builder/imagewizard/1579d95b-8f1d-4982-8c53-8c2afa4ab04c'
)
);
});
test('check download compose request action', async () => {
renderWithReduxRouter('', {});
await renderWithReduxRouter('', {});
// get rows
const table = await screen.findByTestId('images-table');
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
const { findAllByRole } = within(table);
const rows = await findAllByRole('row');
const expectedRequest = mockComposes[0].request;
// first row is header so look at index 1
const imageId = rows[1].cells[1].textContent;
const expectedRequest = mockComposes.data.filter((c) => c.id === imageId)[0]
.request;
const actionsButton = within(rows[1]).getByRole('button', {
const actionsButton = await within(rows[1]).findByRole('button', {
name: 'Actions',
});
user.click(actionsButton);
@ -202,31 +180,33 @@ describe('Images Table', () => {
});
test('check expandable row toggle', async () => {
renderWithReduxRouter('', {});
await renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
const { findAllByRole } = within(table);
const rows = await findAllByRole('row');
const toggleButton = within(rows[1]).getByRole('button', {
const toggleButton = await within(rows[1]).findByRole('button', {
name: /details/i,
});
expect(screen.getByText(/ami-0e778053cd490ad21/i)).not.toBeVisible();
expect(await screen.findByText(/ami-0e778053cd490ad21/i)).not.toBeVisible();
await user.click(toggleButton);
expect(screen.getByText(/ami-0e778053cd490ad21/i)).toBeVisible();
expect(await screen.findByText(/ami-0e778053cd490ad21/i)).toBeVisible();
await user.click(toggleButton);
expect(screen.getByText(/ami-0e778053cd490ad21/i)).not.toBeVisible();
expect(await screen.findByText(/ami-0e778053cd490ad21/i)).not.toBeVisible();
});
test('check error details', async () => {
renderWithReduxRouter('', {});
await renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
const { findAllByRole } = within(table);
const rows = await findAllByRole('row');
const errorPopover = within(rows[2]).getByText(/image build failed/i);
const errorPopover = await within(rows[2]).findByText(
/image build failed/i
);
expect(
screen.getAllByText(/c1cfa347-4c37-49b5-8e73-6aa1d1746cfa/i)[1]
@ -242,7 +222,7 @@ describe('Images Table', () => {
describe('Images Table Toolbar', () => {
test('render toolbar', async () => {
renderWithReduxRouter('', {});
await renderWithReduxRouter('', {});
await screen.findByTestId('images-table');
// check create image button
@ -257,7 +237,7 @@ describe('Images Table Toolbar', () => {
describe('Clones table', () => {
const user = userEvent.setup();
test('renders clones table', async () => {
renderWithReduxRouter('', {});
await renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
@ -330,7 +310,7 @@ describe('Clones table', () => {
toTest.toHaveTextContent('Ready');
break;
case 2:
toTest.toHaveTextContent('Image build failed');
toTest.toHaveTextContent('Sharing image failed');
break;
// no default
}

View file

@ -1,6 +1,10 @@
import { screen } from '@testing-library/react';
import { rest } from 'msw';
import api from '../../../api.js';
import { IMAGE_BUILDER_API } from '../../../constants.js';
import { mockComposesEmpty } from '../../fixtures/composes';
import { server } from '../../mocks/server.js';
import { renderWithReduxRouter } from '../../testUtils';
jest.mock('../../../store/actions/actions', () => {
@ -34,11 +38,17 @@ describe('Landing Page', () => {
});
test('renders EmptyState child component', async () => {
server.use(
rest.get(`${IMAGE_BUILDER_API}/composes`, (req, res, ctx) => {
return res(ctx.status(200), ctx.json(mockComposesEmpty));
})
);
renderWithReduxRouter('', {});
// check action loads
screen.getByTestId('create-image-action');
await screen.findByTestId('create-image-action');
// check table loads
screen.getByTestId('empty-state');
await screen.findByTestId('empty-state');
});
});

View file

@ -4,9 +4,7 @@ import '@testing-library/jest-dom';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import api from '../../../api.js';
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
import { mockState } from '../../fixtures/composes';
import { renderCustomRoutesWithReduxRouter } from '../../testUtils';
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
@ -37,9 +35,9 @@ const routes = [
describe('Create Share To Regions Modal', () => {
const user = userEvent.setup();
test('validation', async () => {
renderCustomRoutesWithReduxRouter(`share/${composeId}`, mockState, routes);
await renderCustomRoutesWithReduxRouter(`share/${composeId}`, {}, routes);
const shareButton = screen.getByRole('button', { name: /share/i });
const shareButton = await screen.findByRole('button', { name: /share/i });
expect(shareButton).toBeDisabled();
let invalidAlert = screen.queryByText(
@ -69,14 +67,14 @@ describe('Create Share To Regions Modal', () => {
});
test('cancel button redirects to landing page', async () => {
const { router } = renderCustomRoutesWithReduxRouter(
const { router } = await renderCustomRoutesWithReduxRouter(
`share/${composeId}`,
mockState,
{},
routes
);
const cancelButton = screen.getByRole('button', { name: /cancel/i });
cancelButton.click();
const cancelButton = await screen.findByRole('button', { name: /cancel/i });
user.click(cancelButton);
// returns back to the landing page
await waitFor(() =>
@ -85,14 +83,14 @@ describe('Create Share To Regions Modal', () => {
});
test('close button redirects to landing page', async () => {
const { router } = renderCustomRoutesWithReduxRouter(
const { router } = await renderCustomRoutesWithReduxRouter(
`share/${composeId}`,
mockState,
{},
routes
);
const closeButton = screen.getByRole('button', { name: /close/i });
closeButton.click();
const closeButton = await screen.findByRole('button', { name: /close/i });
user.click(closeButton);
// returns back to the landing page
await waitFor(() =>
@ -101,11 +99,13 @@ describe('Create Share To Regions Modal', () => {
});
test('select options disabled correctly based on status and region', async () => {
renderCustomRoutesWithReduxRouter(`share/${composeId}`, mockState, routes);
renderCustomRoutesWithReduxRouter(`share/${composeId}`, {}, routes);
const selectToggle = screen.getByRole('button', { name: /options menu/i });
const selectToggle = await screen.findByRole('button', {
name: /options menu/i,
});
// eslint-disable-next-line testing-library/no-unnecessary-act
userEvent.click(selectToggle);
user.click(selectToggle);
// parent region disabled
const usEast1 = await screen.findByRole('option', {
@ -113,95 +113,10 @@ describe('Create Share To Regions Modal', () => {
});
expect(usEast1).toHaveClass('pf-m-disabled');
// successful clone disabled
const usWest1 = screen.getByRole('option', {
name: /us-west-1 us west \(n. california\)/i,
});
expect(usWest1).toHaveClass('pf-m-disabled');
// unsuccessful clone enabled
const usWest2 = screen.getByRole('option', {
name: /us-west-2 us west \(oregon\)/i,
});
expect(usWest2).not.toHaveClass('pf-m-disabled');
// successful clone with different share_with_accounts than its parent enabled
const euCentral1 = screen.getByRole('option', {
name: /eu-central-1 europe \(frankfurt\)/i,
});
expect(euCentral1).not.toHaveClass('pf-m-disabled');
// close the select again to avoid state update
// eslint-disable-next-line testing-library/no-unnecessary-act
await userEvent.click(selectToggle);
await user.click(selectToggle);
});
test('cloning an image results in successful store updates', async () => {
const { router, store } = renderCustomRoutesWithReduxRouter(
`share/${composeId}`,
mockState,
routes
);
const selectToggle = screen.getByRole('button', { name: /options menu/i });
user.click(selectToggle);
const usEast2 = await screen.findByRole('option', {
name: /us-east-2 us east \(ohio\)/i,
});
expect(usEast2).not.toHaveClass('pf-m-disabled');
user.click(usEast2);
const mockResponse = {
id: '123e4567-e89b-12d3-a456-426655440000',
};
const cloneImage = jest.spyOn(api, 'cloneImage').mockImplementation(() => {
return Promise.resolve(mockResponse);
});
const shareButton = await screen.findByRole('button', { name: /share/i });
await waitFor(() => expect(shareButton).toBeEnabled());
user.click(shareButton);
await waitFor(() => expect(cloneImage).toHaveBeenCalledTimes(1));
// returns back to the landing page
expect(router.state.location.pathname).toBe('/insights/image-builder');
// Clone has been added to its parent's list of clones
expect(
store.getState().composes.byId['1579d95b-8f1d-4982-8c53-8c2afa4ab04c']
.clones
).toEqual([
'f9133ec4-7a9e-4fd9-9a9f-9636b82b0a5d',
'48fce414-0cc0-4a16-8645-e3f0edec3212',
'0169538e-515c-477e-b934-f12783939313',
'4a851db1-919f-43ca-a7ef-dd209877a77e',
'123e4567-e89b-12d3-a456-426655440000',
]);
// Clone has been added to state.clones.allIds
expect(store.getState().clones.allIds).toEqual([
'f9133ec4-7a9e-4fd9-9a9f-9636b82b0a5d',
'48fce414-0cc0-4a16-8645-e3f0edec3212',
'0169538e-515c-477e-b934-f12783939313',
'4a851db1-919f-43ca-a7ef-dd209877a77e',
'123e4567-e89b-12d3-a456-426655440000',
]);
// Clone has been added to state.clones.byId
expect(
store.getState().clones.byId['123e4567-e89b-12d3-a456-426655440000']
).toEqual({
id: '123e4567-e89b-12d3-a456-426655440000',
image_status: {
status: 'pending',
},
parent: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
request: {
region: 'us-east-2',
share_with_accounts: ['123123123123'],
},
});
});
// TODO Verify that sharing clones works once msw/data is incorporated.
});