Skip to content
Open
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
@@ -1,85 +1,219 @@
import type { ComponentProps } from 'react';
import { screen, fireEvent } from '@testing-library/react';
import type { FormikProps, FormikValues } from 'formik';
import { formikFormProps } from '@console/shared/src/test-utils/formik-props-utils';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Formik } from 'formik';
import type { ObjectSchema } from 'yup';
import * as yup from 'yup';
import { limitsValidationSchema } from '@console/dev-console/src/components/import/validation-schema';
import type { K8sResourceKind } from '@console/internal/module/k8s';
import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
import { getLimitsDataFromResource } from '@console/shared/src/utils/resource-utils';
import { t } from '../../../../../../../__mocks__/i18next';
import ResourceLimitsModal from '../ResourceLimitsModal';

jest.mock('@console/dev-console/src/components/import/advanced/ResourceLimitSection', () => ({
default: () => null,
}));
jest.mock('@patternfly/react-topology', () => ({}));

type ResourceLimitsModalProps = ComponentProps<typeof ResourceLimitsModal>;
const emptyLimits = {
cpu: {
request: '',
requestUnit: '',
defaultRequestUnit: '',
limit: '',
limitUnit: '',
defaultLimitUnit: '',
},
memory: {
request: '',
requestUnit: 'Mi',
defaultRequestUnit: 'Mi',
limit: '',
limitUnit: 'Mi',
defaultLimitUnit: 'Mi',
},
};

describe('ResourceLimitsModal Form', () => {
let formProps: ResourceLimitsModalProps;
const resourceLimitsSchema = yup.object().shape({
limits: limitsValidationSchema(t),
});

type Props = FormikProps<FormikValues> & ResourceLimitsModalProps;
const limitsFormValues = {
limits: emptyLimits,
container: 'hello-openshift',
};

beforeEach(() => {
jest.clearAllMocks();
formProps = {
...formikFormProps,
isSubmitting: false,
cancel: jest.fn(),
resource: {
apiVersion: 'apps/v1',
kind: 'Deployment',
const baseDeployment = (): K8sResourceKind =>
({
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
name: 'xyz-deployment',
},
spec: {
selector: {
matchLabels: {
app: 'hello-openshift',
},
},
replicas: 1,
template: {
metadata: {
name: 'xyz-deployment',
labels: {
app: 'hello-openshift',
},
},
spec: {
selector: {
matchLabels: {
app: 'hello-openshift',
},
},
replicas: 1,
template: {
metadata: {
labels: {
app: 'hello-openshift',
},
},
spec: {
containers: [
containers: [
{
name: 'hello-openshift',
image: 'openshift/hello-openshift',
ports: [
{
name: 'hello-openshift',
image: 'openshift/hello-openshift',
ports: [
{
containerPort: 8080,
},
],
containerPort: 8080,
},
],
},
},
],
},
},
} as Props;
},
} as K8sResourceKind);

type RenderResourceLimitsModalOptions = {
resource?: K8sResourceKind;
validationSchema?: ObjectSchema<unknown>;
onSubmit?: jest.Mock;
cancel?: jest.Mock;
};

const renderResourceLimitsModal = ({
resource,
validationSchema,
onSubmit: onSubmitOption,
cancel: cancelOption,
}: RenderResourceLimitsModalOptions = {}) => {
const onSubmit = onSubmitOption ?? jest.fn();
const cancel = cancelOption ?? jest.fn();
const initialValues = resource
? {
limits: getLimitsDataFromResource(resource),
container: resource.spec.template.spec.containers[0].name,
}
: limitsFormValues;

return {
onSubmit,
cancel,
...renderWithProviders(
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
{(formikProps) => (
<ResourceLimitsModal {...formikProps} cancel={cancel} isSubmitting={false} />
)}
</Formik>,
),
};
};

describe('ResourceLimitsModal Form', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders the modal with the correct title and initial elements', () => {
renderWithProviders(<ResourceLimitsModal {...formProps} />);
it('renders the modal title', () => {
renderResourceLimitsModal();

expect(screen.getByText('Edit resource limits')).toBeVisible();
});

it('renders the form with Cancel and Save actions', () => {
renderResourceLimitsModal();

expect(screen.getByRole('form')).toBeVisible();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Save' })).toBeVisible();
});

it('calls the cancel function when the Cancel button is clicked', async () => {
renderWithProviders(<ResourceLimitsModal {...formProps} />);
const user = userEvent.setup();
const cancel = jest.fn();
renderResourceLimitsModal({ cancel });

await fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(formProps.cancel).toHaveBeenCalledTimes(1);
await user.click(screen.getByRole('button', { name: 'Cancel' }));
expect(cancel).toHaveBeenCalledTimes(1);
});

it('calls the handleSubmit function when the form is submitted', async () => {
renderWithProviders(<ResourceLimitsModal {...formProps} />);
it('submits the form when Save is clicked', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
renderResourceLimitsModal({ onSubmit });

await user.click(screen.getByRole('button', { name: 'Save' }));
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});

describe('ResourceLimitsModal with validation (resource limits schema)', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('populates CPU and Memory request/limit fields from the workload resource', () => {
const resource = baseDeployment();
resource.spec.template.spec.containers[0].resources = {
requests: { cpu: '100m', memory: '128Mi' },
limits: { cpu: '500m', memory: '256Mi' },
};

renderResourceLimitsModal({ resource, validationSchema: resourceLimitsSchema });

expect(screen.getByDisplayValue('100')).toBeVisible();
expect(screen.getByDisplayValue('500')).toBeVisible();
expect(screen.getByDisplayValue('128')).toBeVisible();
expect(screen.getByDisplayValue('256')).toBeVisible();
});

it('disables Save when CPU request is greater than CPU limit', async () => {
const user = userEvent.setup();
const resource = baseDeployment();
resource.spec.template.spec.containers[0].resources = {
requests: { cpu: '100m', memory: '128Mi' },
limits: { cpu: '200m', memory: '256Mi' },
};

renderResourceLimitsModal({ resource, validationSchema: resourceLimitsSchema });

const save = screen.getByRole('button', { name: 'Save' });
expect(save).not.toBeDisabled();

const cpuRequest = screen.getByRole('spinbutton', { name: 'CPU request' });
await user.click(cpuRequest);
await user.keyboard('{Control>}a{/Control}');
await user.keyboard('300');

await waitFor(() => expect(save).toBeDisabled());
expect(
await screen.findByText('CPU request must be less than or equal to limit.'),
).toBeVisible();
});

it('disables Save when Memory request is greater than Memory limit', async () => {
const user = userEvent.setup();
const resource = baseDeployment();
resource.spec.template.spec.containers[0].resources = {
requests: { cpu: '100m', memory: '128Mi' },
limits: { cpu: '500m', memory: '256Mi' },
};

renderResourceLimitsModal({ resource, validationSchema: resourceLimitsSchema });

const save = screen.getByRole('button', { name: 'Save' });
const memoryRequest = screen.getByRole('spinbutton', { name: 'Memory request' });

await user.click(memoryRequest);
await user.keyboard('{Control>}a{/Control}');
await user.keyboard('512');

await fireEvent.submit(screen.getByRole('form'));
expect(formProps.handleSubmit).toHaveBeenCalledTimes(1);
await waitFor(() => expect(save).toBeDisabled());
expect(
await screen.findByText('Memory request must be less than or equal to limit.'),
).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -1,33 +1,83 @@
import { render } from '@testing-library/react';
import { Button } from '@patternfly/react-core';
import { screen } from '@testing-library/react';
import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
import { AccessDenied, EmptyBox, ConsoleEmptyState } from '..';

const TestIcon = () => 'TestIcon';

describe('EmptyBox', () => {
it('should render without label', () => {
const { getByText } = render(<EmptyBox />);
getByText('Not found');
it('renders default "Not found" message without label', () => {
renderWithProviders(<EmptyBox />);
expect(screen.getByText('Not found')).toBeVisible();
});

it('should render with label', () => {
const { getByText } = render(<EmptyBox label="test-label" />);
getByText('No test-label found');
it('renders message with label when provided', () => {
renderWithProviders(<EmptyBox label="resources" />);
expect(screen.getByText('No resources found')).toBeVisible();
});
});

describe('MsgBox', () => {
it('should render title', () => {
const { getByText } = render(<ConsoleEmptyState title="test-title" />);
getByText('test-title');
describe('ConsoleEmptyState', () => {
it('renders title and children in body', () => {
renderWithProviders(
<ConsoleEmptyState title="Empty State Title">Body content</ConsoleEmptyState>,
);
expect(screen.getByText('Empty State Title')).toBeVisible();
expect(screen.getByText('Body content')).toBeVisible();
});

it('renders Icon when provided', () => {
renderWithProviders(<ConsoleEmptyState Icon={TestIcon} title="With Icon" />);
expect(screen.getByText('TestIcon')).toBeVisible();
});

it('should render children', () => {
const { getByText } = render(<ConsoleEmptyState>test-child</ConsoleEmptyState>);
getByText('test-child');
it('renders primary and secondary actions when provided', () => {
const primaryActions = [<Button key="create">Create Resource</Button>];
const secondaryActions = [
<Button key="learn" variant="link">
Learn more
</Button>,
];
renderWithProviders(
<ConsoleEmptyState
title="Test"
primaryActions={primaryActions}
secondaryActions={secondaryActions}
/>,
);
expect(screen.getByRole('button', { name: 'Create Resource' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Learn more' })).toBeVisible();
});

it('does not render body or footer when not provided', () => {
renderWithProviders(<ConsoleEmptyState title="No Body" />);
expect(screen.queryByTestId('console-empty-state-body')).not.toBeInTheDocument();
expect(screen.queryByTestId('console-empty-state-footer')).not.toBeInTheDocument();
});
});

describe('AccessDenied', () => {
it('should render message', () => {
const { getByText } = render(<AccessDenied>test-message</AccessDenied>);
getByText('test-message');
it('renders restricted access title and message', () => {
renderWithProviders(<AccessDenied />);
expect(screen.getByText('Restricted access')).toBeVisible();
expect(
screen.getByText("You don't have access to this section due to cluster policy"),
).toBeVisible();
});

it('renders error details alert when children provided', () => {
renderWithProviders(<AccessDenied>Permission denied for resource xyz</AccessDenied>);
expect(screen.getByText('Error details')).toBeVisible();
expect(screen.getByText('Permission denied for resource xyz')).toBeVisible();
});

it('does not render error alert when no children provided', () => {
renderWithProviders(<AccessDenied />);
expect(screen.queryByText('Error details')).not.toBeInTheDocument();
});

it('renders restricted sign icon', () => {
renderWithProviders(<AccessDenied />);
expect(screen.getByAltText('Restricted access')).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ interface NumberSpinnerFieldProps extends FieldProps {
setOutputAsIntegerFlag?: boolean;
label?: React.ReactNode;
helpText?: React.ReactNode;
min?: number;
max?: number;
}

const NumberSpinnerField: FC<NumberSpinnerFieldProps> = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const ResourceLimitField: FC<ResourceLimitFieldProps> = ({
unitName,
unitOptions,
helpText,
inputAriaLabel,
...props
}) => {
const [field, { touched, error }] = useField(props.name);
Expand All @@ -27,6 +28,8 @@ const ResourceLimitField: FC<ResourceLimitFieldProps> = ({
<FormGroup fieldId={fieldId} label={label} isRequired={props.required}>
<RequestSizeInput
{...props}
ariaLabel={inputAriaLabel}
inputID={fieldId}
onChange={(val) => {
setFieldValue(props.name, val.value);
setFieldTouched(props.name, true);
Expand Down
Loading