import React from 'react';
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BrowserRouter } from 'react-router-dom';
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 { RHEL_8 } from '../../../constants.js';
import { timestampToDisplayString } from '../../../Utilities/time.js';
import { renderWithProvider, renderWithReduxRouter } from '../../testUtils';
const currentDate = new Date();
const currentDateInString = currentDate.toString();
const mockComposes = {
meta: {
count: 12,
},
data: [
{
id: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
image_name: 'testImageName',
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'aws',
upload_request: {
type: 'aws',
options: {
share_with_accounts: ['123123123123'],
},
},
},
],
},
},
// kept "running" for backward compatibility
{
id: 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa',
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'gcp',
upload_request: {
type: 'gcp',
options: {
share_with_accounts: ['serviceAccount:test@email.com'],
},
},
},
],
},
},
{
id: 'edbae1c2-62bc-42c1-ae0c-3110ab718f58',
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'aws',
upload_request: {
type: 'aws',
options: {},
},
},
],
},
},
{
id: '42ad0826-30b5-4f64-a24e-957df26fd564',
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'aws',
upload_request: {
type: 'aws',
options: {},
},
},
],
},
},
{
id: '955944a2-e149-4058-8ac1-35b514cb5a16',
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'aws',
upload_request: {
type: 'aws',
options: {},
},
},
],
},
},
{
id: 'f7a60094-b376-4b58-a102-5c8c82dfd18b',
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'aws',
upload_request: {
type: 'aws',
options: {},
},
},
],
},
},
{
id: '61b0effa-c901-4ee5-86b9-2010b47f1b22',
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'aws',
upload_request: {
type: 'aws',
options: {},
},
},
],
},
},
{
id: 'ca03f120-9840-4959-871e-94a5cb49d1f2',
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'gcp',
upload_request: {
type: 'gcp',
options: {
share_with_accounts: ['serviceAccount:test@email.com'],
},
},
},
],
},
},
{
id: '551de6f6-1533-4b46-a69f-7924051f9bc6',
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'azure',
upload_request: {
type: 'azure',
options: {},
},
},
],
},
},
{
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
id: 'b7193673-8dcc-4a5f-ac30-e9f4940d8346',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'vsphere',
upload_request: {
options: {},
type: 'aws.s3',
},
},
],
},
},
{
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
id: '4873fd0f-1851-4b9f-b4fe-4639fce90794',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'image-installer',
upload_request: {
options: {},
type: 'aws.s3',
},
},
],
},
},
{
created_at: currentDateInString,
id: '7b7d0d51-7106-42ab-98f2-f89872a9d599',
request: {
distribution: RHEL_8,
image_requests: [
{
architecture: 'x86_64',
image_type: 'guest-image',
upload_request: {
options: {},
type: 'aws.s3',
},
},
],
},
},
],
};
const mockStatus = {
'1579d95b-8f1d-4982-8c53-8c2afa4ab04c': {
image_status: {
status: 'success',
upload_status: {
options: {
ami: 'ami-0217b81d9be50e44b',
region: 'us-east-1',
},
status: 'success',
type: 'aws',
},
},
},
// kept "running" for backward compatibility
'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa': {
image_status: {
status: 'failure',
error: {
reason: 'A dependency error occured',
details: {
reason: 'Error in depsolve job',
},
},
},
},
'edbae1c2-62bc-42c1-ae0c-3110ab718f58': {
image_status: {
status: 'pending',
},
},
'42ad0826-30b5-4f64-a24e-957df26fd564': {
image_status: {
status: 'building',
},
},
'955944a2-e149-4058-8ac1-35b514cb5a16': {
image_status: {
status: 'uploading',
},
},
'f7a60094-b376-4b58-a102-5c8c82dfd18b': {
image_status: {
status: 'registering',
},
},
'61b0effa-c901-4ee5-86b9-2010b47f1b22': {
image_status: {
status: 'failure',
error: {
reason: 'A dependency error occured',
details: {
reason: 'Error in depsolve job',
},
},
},
},
'ca03f120-9840-4959-871e-94a5cb49d1f2': {
image_status: {
status: 'success',
upload_status: {
options: {
image_name: 'composer-api-d446d8cb-7c16-4756-bf7d-706293785b05',
project_id: 'red-hat-image-builder',
},
status: 'success',
type: 'gcp',
},
},
},
'551de6f6-1533-4b46-a69f-7924051f9bc6': {
image_status: {
status: 'building',
},
},
'b7193673-8dcc-4a5f-ac30-e9f4940d8346': {
image_status: {
status: 'success',
upload_status: {
options: {
url: 'https://s3.amazonaws.com/b7193673-8dcc-4a5f-ac30-e9f4940d8346-disk.vmdk',
},
status: 'success',
type: 'aws.s3',
},
},
},
'4873fd0f-1851-4b9f-b4fe-4639fce90794': {
image_status: {
status: 'success',
upload_status: {
options: {
url: 'https://s3.amazonaws.com/4873fd0f-1851-4b9f-b4fe-4639fce90794-installer.iso',
},
status: 'success',
type: 'aws.s3',
},
},
},
'7b7d0d51-7106-42ab-98f2-f89872a9d599': {
image_status: {
status: 'success',
upload_status: {
options: {
url: 'https://s3.amazonaws.com/7b7d0d51-7106-42ab-98f2-f89872a9d599-disk.qcow2',
},
status: 'success',
type: 'aws.s3',
},
},
},
};
const mockNoClones = {
data: null,
};
const mockClones = {
data: [
{
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
id: 'f9133ec4-7a9e-4fd9-9a9f-9636b82b0a5d',
request: {
region: 'us-west-1',
share_with_accounts: ['123123123123'],
},
},
{
created_at: '2021-04-28 12:31:12.794809 +0000 UTC',
id: '48fce414-0cc0-4a16-8645-e3f0edec3212',
request: {
region: 'us-west-1',
share_with_accounts: ['123123123123'],
},
},
{
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
id: '0169538e-515c-477e-b934-f12783939313',
request: {
region: 'us-west-2',
share_with_accounts: ['123123123123'],
},
},
{
created_at: '2021-04-27 12:31:12.794809 +0000 UTC',
id: '4a851db1-919f-43ca-a7ef-dd209877a77e',
request: {
region: 'eu-central-1',
share_with_accounts: ['000000000000'],
},
},
],
};
const mockCloneStatus = {
'f9133ec4-7a9e-4fd9-9a9f-9636b82b0a5d': {
options: {
ami: 'ami-0e778053cd490ad21',
region: 'us-west-1',
},
status: 'success',
type: 'aws',
},
'48fce414-0cc0-4a16-8645-e3f0edec3212': {
options: {
ami: 'ami-9f0asd1tlk2142124',
region: 'us-west-1',
},
status: 'success',
type: 'aws',
},
'0169538e-515c-477e-b934-f12783939313': {
options: {
ami: 'ami-9fdskj12fdsak1211',
region: 'us-west-2',
},
status: 'failure',
type: 'aws',
},
'4a851db1-919f-43ca-a7ef-dd209877a77e': {
options: {
ami: 'ami-9fdskj12fdsak1211',
region: 'eu-central-1',
},
status: 'success',
type: 'aws',
},
};
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
useChrome: () => ({
isBeta: () => false,
isProd: () => true,
getEnvironment: () => 'prod',
}),
}));
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)
: Promise.resolve(mockNoClones);
});
jest.spyOn(api, 'getCloneStatus').mockImplementation((id) => {
return Promise.resolve(mockCloneStatus[id]);
});
describe('Images Table', () => {
const user = userEvent.setup();
test('render ImagesTable', async () => {
const view = renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
// make sure the empty-state message isn't present
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');
// remove first row from list since it is just header labels
const header = rows.shift();
// test the header has correct labels
expect(header.cells[1]).toHaveTextContent('Image name');
expect(header.cells[2]).toHaveTextContent('Created');
expect(header.cells[3]).toHaveTextContent('Release');
expect(header.cells[4]).toHaveTextContent('Target');
expect(header.cells[5]).toHaveTextContent('Status');
expect(header.cells[6]).toHaveTextContent('Instance');
// 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.
expect(row.cells[2]).toHaveTextContent('Apr 27, 2021');
// render the expected and compare the text content
const testElement = document.createElement('testElement');
// render(, { container: testElement });
renderWithProvider(, testElement, state);
expect(row.cells[4]).toHaveTextContent(testElement.textContent);
// render the expected and compare the text content
if (
compose.created_at === '2021-04-27 12:31:12.794809 +0000 UTC' &&
compose.request.image_requests[0].upload_request.type === 'aws.s3'
) {
expect(row.cells[5]).toHaveTextContent('Expired');
} else {
renderWithProvider(
,
testElement,
state
);
expect(row.cells[5]).toHaveTextContent(testElement.textContent);
}
// render the expected and compare the text content for a link
if (
compose.created_at === '2021-04-27 12:31:12.794809 +0000 UTC' &&
compose.request.image_requests[0].upload_request.type === 'aws.s3'
) {
expect(row.cells[6]).toHaveTextContent('Recreate image');
} else {
renderWithProvider(
,
testElement,
state
);
expect(row.cells[6]).toHaveTextContent(testElement.textContent);
}
}
});
test('check recreate action', async () => {
const { router } = renderWithReduxRouter('', {});
// get rows
const table = await screen.findByTestId('images-table');
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
// first row is header so look at index 1
const imageId = rows[1].cells[1].textContent;
const actionsButton = within(rows[1]).getByRole('button', {
name: 'Actions',
});
await user.click(actionsButton);
const recreateButton = screen.getByRole('menuitem', {
name: 'Recreate image',
});
await user.click(recreateButton);
expect(router.state.location.pathname).toBe(
`/insights/image-builder/imagewizard/${imageId}`
);
});
test('check download compose request action', async () => {
renderWithReduxRouter('', {});
// get rows
const table = await screen.findByTestId('images-table');
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
// 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', {
name: 'Actions',
});
await user.click(actionsButton);
const downloadButton = screen.getByRole('menuitem', {
name: 'Download compose request (.json)',
});
// No actual clicking because downloading is hard to test.
// Instead, we just check href and download properties of the element.
const downloadLink = within(downloadButton).getByRole('link');
expect(downloadLink.download).toBe(
'request-1579d95b-8f1d-4982-8c53-8c2afa4ab04c.json'
);
const hrefParts = downloadLink.href.split(',');
expect(hrefParts.length).toBe(2);
const [header, encodedRequest] = hrefParts;
expect(header).toBe('data:text/plain;charset=utf-8');
expect(encodedRequest).toBe(
encodeURIComponent(JSON.stringify(expectedRequest, null, ' '))
);
});
test('check expandable row toggle', async () => {
renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
const toggleButton = within(rows[1]).getByRole('button', {
name: /details/i,
});
expect(
screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]
).not.toBeVisible();
await user.click(toggleButton);
expect(
screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]
).toBeVisible();
await user.click(toggleButton);
expect(
screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1]
).not.toBeVisible();
});
test('check error details', async () => {
renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
const errorPopover = within(rows[2]).getByText(/image build failed/i);
expect(
screen.getAllByText(/c1cfa347-4c37-49b5-8e73-6aa1d1746cfa/i)[1]
).not.toBeVisible();
await user.click(errorPopover);
expect(screen.getAllByText(/Error in depsolve job/i)[0]).toBeVisible();
});
});
describe('Images Table Toolbar', () => {
test('render toolbar', async () => {
renderWithReduxRouter('', {});
await screen.findByTestId('images-table');
// check create image button
screen.getByTestId('create-image-action');
// check pagination renders
screen.getByTestId('images-pagination-top');
screen.getByTestId('images-pagination-bottom');
});
});
describe('Clones table', () => {
test('renders clones table', async () => {
const view = renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
// make sure the empty-state message isn't present
const emptyState = screen.queryByTestId('empty-state');
expect(emptyState).not.toBeInTheDocument();
const state = view.store.getState();
// get rows
const { getAllByRole } = within(table);
const rows = getAllByRole('row');
// first row is header so look at index 1
const detailsButton = within(rows[1]).getByRole('button', {
name: /details/i,
});
detailsButton.click();
// Multiple clones tables exist (one per AWS image), get the first one (which has clones)
const clonesTable = await screen.findAllByTestId('clones-table');
const cloneRows = within(clonesTable[0]).getAllByRole('row');
// remove first row from list since it is just header labels
const header = cloneRows.shift();
// test the header has correct labels
expect(header.cells[0]).toHaveTextContent('UUID');
expect(header.cells[1]).toHaveTextContent('Created');
expect(header.cells[2]).toHaveTextContent('Account');
expect(header.cells[3]).toHaveTextContent('Region');
expect(header.cells[4]).toHaveTextContent('Status');
expect(header.cells[5]).toHaveTextContent('Instance');
expect(cloneRows).toHaveLength(5);
// prepend parent data
const clonesTableData = {
uuid: [
'1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
...mockClones.data.map((clone) => clone.id),
],
created: [
'2021-04-27 12:31:12.794809 +0000 UTC',
...mockClones.data.map((clone) => clone.created_at),
],
account: [
'123123123123',
...mockClones.data.map((clone) => clone.request.share_with_accounts[0]),
],
region: [
'us-east-1',
...mockClones.data.map(
(clone) => mockCloneStatus[clone.id].options.region
),
],
};
for (const [index, row] of cloneRows.entries()) {
// render UUIDs in correct order
expect(row.cells[0]).toHaveTextContent(clonesTableData.uuid[index]);
// date should match the month day and year of the timestamp.
const formattedDate = timestampToDisplayString(
clonesTableData.created[index]
);
expect(row.cells[1]).toHaveTextContent(formattedDate);
// account cell
expect(row.cells[2]).toHaveTextContent(clonesTableData.account[index]);
// region cell
expect(row.cells[3]).toHaveTextContent(clonesTableData.region[index]);
const testElement = document.createElement('testElement');
const imageId = clonesTableData.uuid[index];
// status cell
renderWithProvider(
,
testElement,
state
);
expect(row.cells[4]).toHaveTextContent(testElement.textContent);
// instance cell
renderWithProvider(
,
testElement,
state
);
expect(row.cells[5]).toHaveTextContent(testElement.textContent);
}
});
});