Skip to content

Commit f852b6d

Browse files
committed
Extend RTL Test Coverage for Components Migrated from Enzyme
1 parent 62dd642 commit f852b6d

18 files changed

Lines changed: 1009 additions & 410 deletions

File tree

Lines changed: 187 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,219 @@
1-
import type { ComponentProps } from 'react';
2-
import { screen, fireEvent } from '@testing-library/react';
3-
import type { FormikProps, FormikValues } from 'formik';
4-
import { formikFormProps } from '@console/shared/src/test-utils/formik-props-utils';
1+
import { screen, waitFor } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { Formik } from 'formik';
4+
import type { ObjectSchema } from 'yup';
5+
import * as yup from 'yup';
6+
import { limitsValidationSchema } from '@console/dev-console/src/components/import/validation-schema';
7+
import type { K8sResourceKind } from '@console/internal/module/k8s';
58
import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
9+
import { getLimitsDataFromResource } from '@console/shared/src/utils/resource-utils';
10+
import { t } from '../../../../../../../__mocks__/i18next';
611
import ResourceLimitsModal from '../ResourceLimitsModal';
712

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

12-
type ResourceLimitsModalProps = ComponentProps<typeof ResourceLimitsModal>;
15+
const emptyLimits = {
16+
cpu: {
17+
request: '',
18+
requestUnit: '',
19+
defaultRequestUnit: '',
20+
limit: '',
21+
limitUnit: '',
22+
defaultLimitUnit: '',
23+
},
24+
memory: {
25+
request: '',
26+
requestUnit: 'Mi',
27+
defaultRequestUnit: 'Mi',
28+
limit: '',
29+
limitUnit: 'Mi',
30+
defaultLimitUnit: 'Mi',
31+
},
32+
};
1333

14-
describe('ResourceLimitsModal Form', () => {
15-
let formProps: ResourceLimitsModalProps;
34+
const resourceLimitsSchema = yup.object().shape({
35+
limits: limitsValidationSchema(t),
36+
});
1637

17-
type Props = FormikProps<FormikValues> & ResourceLimitsModalProps;
38+
const limitsFormValues = {
39+
limits: emptyLimits,
40+
container: 'hello-openshift',
41+
};
1842

19-
beforeEach(() => {
20-
jest.clearAllMocks();
21-
formProps = {
22-
...formikFormProps,
23-
isSubmitting: false,
24-
cancel: jest.fn(),
25-
resource: {
26-
apiVersion: 'apps/v1',
27-
kind: 'Deployment',
43+
const baseDeployment = (): K8sResourceKind =>
44+
({
45+
apiVersion: 'apps/v1',
46+
kind: 'Deployment',
47+
metadata: {
48+
name: 'xyz-deployment',
49+
},
50+
spec: {
51+
selector: {
52+
matchLabels: {
53+
app: 'hello-openshift',
54+
},
55+
},
56+
replicas: 1,
57+
template: {
2858
metadata: {
29-
name: 'xyz-deployment',
59+
labels: {
60+
app: 'hello-openshift',
61+
},
3062
},
3163
spec: {
32-
selector: {
33-
matchLabels: {
34-
app: 'hello-openshift',
35-
},
36-
},
37-
replicas: 1,
38-
template: {
39-
metadata: {
40-
labels: {
41-
app: 'hello-openshift',
42-
},
43-
},
44-
spec: {
45-
containers: [
64+
containers: [
65+
{
66+
name: 'hello-openshift',
67+
image: 'openshift/hello-openshift',
68+
ports: [
4669
{
47-
name: 'hello-openshift',
48-
image: 'openshift/hello-openshift',
49-
ports: [
50-
{
51-
containerPort: 8080,
52-
},
53-
],
70+
containerPort: 8080,
5471
},
5572
],
5673
},
57-
},
74+
],
5875
},
5976
},
60-
} as Props;
77+
},
78+
} as K8sResourceKind);
79+
80+
type RenderResourceLimitsModalOptions = {
81+
resource?: K8sResourceKind;
82+
validationSchema?: ObjectSchema<unknown>;
83+
onSubmit?: jest.Mock;
84+
cancel?: jest.Mock;
85+
};
86+
87+
const renderResourceLimitsModal = ({
88+
resource,
89+
validationSchema,
90+
onSubmit: onSubmitOption,
91+
cancel: cancelOption,
92+
}: RenderResourceLimitsModalOptions = {}) => {
93+
const onSubmit = onSubmitOption ?? jest.fn();
94+
const cancel = cancelOption ?? jest.fn();
95+
const initialValues = resource
96+
? {
97+
limits: getLimitsDataFromResource(resource),
98+
container: resource.spec.template.spec.containers[0].name,
99+
}
100+
: limitsFormValues;
101+
102+
return {
103+
onSubmit,
104+
cancel,
105+
...renderWithProviders(
106+
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit}>
107+
{(formikProps) => (
108+
<ResourceLimitsModal {...formikProps} cancel={cancel} isSubmitting={false} />
109+
)}
110+
</Formik>,
111+
),
112+
};
113+
};
114+
115+
describe('ResourceLimitsModal Form', () => {
116+
beforeEach(() => {
117+
jest.clearAllMocks();
61118
});
62119

63-
it('renders the modal with the correct title and initial elements', () => {
64-
renderWithProviders(<ResourceLimitsModal {...formProps} />);
120+
it('renders the modal title', () => {
121+
renderResourceLimitsModal();
65122

66123
expect(screen.getByText('Edit resource limits')).toBeVisible();
124+
});
125+
126+
it('renders the form with Cancel and Save actions', () => {
127+
renderResourceLimitsModal();
128+
67129
expect(screen.getByRole('form')).toBeVisible();
68130
expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
69131
expect(screen.getByRole('button', { name: 'Save' })).toBeVisible();
70132
});
71133

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

75-
await fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
76-
expect(formProps.cancel).toHaveBeenCalledTimes(1);
139+
await user.click(screen.getByRole('button', { name: 'Cancel' }));
140+
expect(cancel).toHaveBeenCalledTimes(1);
77141
});
78142

79-
it('calls the handleSubmit function when the form is submitted', async () => {
80-
renderWithProviders(<ResourceLimitsModal {...formProps} />);
143+
it('submits the form when Save is clicked', async () => {
144+
const user = userEvent.setup();
145+
const onSubmit = jest.fn();
146+
renderResourceLimitsModal({ onSubmit });
147+
148+
await user.click(screen.getByRole('button', { name: 'Save' }));
149+
expect(onSubmit).toHaveBeenCalledTimes(1);
150+
});
151+
});
152+
153+
describe('ResourceLimitsModal with validation (resource limits schema)', () => {
154+
beforeEach(() => {
155+
jest.clearAllMocks();
156+
});
157+
158+
it('populates CPU and Memory request/limit fields from the workload resource', () => {
159+
const resource = baseDeployment();
160+
resource.spec.template.spec.containers[0].resources = {
161+
requests: { cpu: '100m', memory: '128Mi' },
162+
limits: { cpu: '500m', memory: '256Mi' },
163+
};
164+
165+
renderResourceLimitsModal({ resource, validationSchema: resourceLimitsSchema });
166+
167+
expect(screen.getByDisplayValue('100')).toBeVisible();
168+
expect(screen.getByDisplayValue('500')).toBeVisible();
169+
expect(screen.getByDisplayValue('128')).toBeVisible();
170+
expect(screen.getByDisplayValue('256')).toBeVisible();
171+
});
172+
173+
it('disables Save when CPU request is greater than CPU limit', async () => {
174+
const user = userEvent.setup();
175+
const resource = baseDeployment();
176+
resource.spec.template.spec.containers[0].resources = {
177+
requests: { cpu: '100m', memory: '128Mi' },
178+
limits: { cpu: '200m', memory: '256Mi' },
179+
};
180+
181+
renderResourceLimitsModal({ resource, validationSchema: resourceLimitsSchema });
182+
183+
const save = screen.getByRole('button', { name: 'Save' });
184+
expect(save).not.toBeDisabled();
185+
186+
const cpuRequest = screen.getByRole('spinbutton', { name: 'CPU request' });
187+
await user.click(cpuRequest);
188+
await user.keyboard('{Control>}a{/Control}');
189+
await user.keyboard('300');
190+
191+
await waitFor(() => expect(save).toBeDisabled());
192+
expect(
193+
await screen.findByText('CPU request must be less than or equal to limit.'),
194+
).toBeVisible();
195+
});
196+
197+
it('disables Save when Memory request is greater than Memory limit', async () => {
198+
const user = userEvent.setup();
199+
const resource = baseDeployment();
200+
resource.spec.template.spec.containers[0].resources = {
201+
requests: { cpu: '100m', memory: '128Mi' },
202+
limits: { cpu: '500m', memory: '256Mi' },
203+
};
204+
205+
renderResourceLimitsModal({ resource, validationSchema: resourceLimitsSchema });
206+
207+
const save = screen.getByRole('button', { name: 'Save' });
208+
const memoryRequest = screen.getByRole('spinbutton', { name: 'Memory request' });
209+
210+
await user.click(memoryRequest);
211+
await user.keyboard('{Control>}a{/Control}');
212+
await user.keyboard('512');
81213

82-
await fireEvent.submit(screen.getByRole('form'));
83-
expect(formProps.handleSubmit).toHaveBeenCalledTimes(1);
214+
await waitFor(() => expect(save).toBeDisabled());
215+
expect(
216+
await screen.findByText('Memory request must be less than or equal to limit.'),
217+
).toBeVisible();
84218
});
85219
});
Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,83 @@
1-
import { render } from '@testing-library/react';
1+
import { Button } from '@patternfly/react-core';
2+
import { screen } from '@testing-library/react';
3+
import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils';
24
import { AccessDenied, EmptyBox, ConsoleEmptyState } from '..';
35

6+
const TestIcon = () => 'TestIcon';
7+
48
describe('EmptyBox', () => {
5-
it('should render without label', () => {
6-
const { getByText } = render(<EmptyBox />);
7-
getByText('Not found');
9+
it('renders default "Not found" message without label', () => {
10+
renderWithProviders(<EmptyBox />);
11+
expect(screen.getByText('Not found')).toBeVisible();
812
});
913

10-
it('should render with label', () => {
11-
const { getByText } = render(<EmptyBox label="test-label" />);
12-
getByText('No test-label found');
14+
it('renders message with label when provided', () => {
15+
renderWithProviders(<EmptyBox label="resources" />);
16+
expect(screen.getByText('No resources found')).toBeVisible();
1317
});
1418
});
1519

16-
describe('MsgBox', () => {
17-
it('should render title', () => {
18-
const { getByText } = render(<ConsoleEmptyState title="test-title" />);
19-
getByText('test-title');
20+
describe('ConsoleEmptyState', () => {
21+
it('renders title and children in body', () => {
22+
renderWithProviders(
23+
<ConsoleEmptyState title="Empty State Title">Body content</ConsoleEmptyState>,
24+
);
25+
expect(screen.getByText('Empty State Title')).toBeVisible();
26+
expect(screen.getByText('Body content')).toBeVisible();
27+
});
28+
29+
it('renders Icon when provided', () => {
30+
renderWithProviders(<ConsoleEmptyState Icon={TestIcon} title="With Icon" />);
31+
expect(screen.getByText('TestIcon')).toBeVisible();
2032
});
2133

22-
it('should render children', () => {
23-
const { getByText } = render(<ConsoleEmptyState>test-child</ConsoleEmptyState>);
24-
getByText('test-child');
34+
it('renders primary and secondary actions when provided', () => {
35+
const primaryActions = [<Button key="create">Create Resource</Button>];
36+
const secondaryActions = [
37+
<Button key="learn" variant="link">
38+
Learn more
39+
</Button>,
40+
];
41+
renderWithProviders(
42+
<ConsoleEmptyState
43+
title="Test"
44+
primaryActions={primaryActions}
45+
secondaryActions={secondaryActions}
46+
/>,
47+
);
48+
expect(screen.getByRole('button', { name: 'Create Resource' })).toBeVisible();
49+
expect(screen.getByRole('button', { name: 'Learn more' })).toBeVisible();
50+
});
51+
52+
it('does not render body or footer when not provided', () => {
53+
renderWithProviders(<ConsoleEmptyState title="No Body" />);
54+
expect(screen.queryByTestId('console-empty-state-body')).not.toBeInTheDocument();
55+
expect(screen.queryByTestId('console-empty-state-footer')).not.toBeInTheDocument();
2556
});
2657
});
2758

2859
describe('AccessDenied', () => {
29-
it('should render message', () => {
30-
const { getByText } = render(<AccessDenied>test-message</AccessDenied>);
31-
getByText('test-message');
60+
it('renders restricted access title and message', () => {
61+
renderWithProviders(<AccessDenied />);
62+
expect(screen.getByText('Restricted access')).toBeVisible();
63+
expect(
64+
screen.getByText("You don't have access to this section due to cluster policy"),
65+
).toBeVisible();
66+
});
67+
68+
it('renders error details alert when children provided', () => {
69+
renderWithProviders(<AccessDenied>Permission denied for resource xyz</AccessDenied>);
70+
expect(screen.getByText('Error details')).toBeVisible();
71+
expect(screen.getByText('Permission denied for resource xyz')).toBeVisible();
72+
});
73+
74+
it('does not render error alert when no children provided', () => {
75+
renderWithProviders(<AccessDenied />);
76+
expect(screen.queryByText('Error details')).not.toBeInTheDocument();
77+
});
78+
79+
it('renders restricted sign icon', () => {
80+
renderWithProviders(<AccessDenied />);
81+
expect(screen.getByAltText('Restricted access')).toBeVisible();
3282
});
3383
});

frontend/packages/console-shared/src/components/formik-fields/NumberSpinnerField.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ interface NumberSpinnerFieldProps extends FieldProps {
1313
setOutputAsIntegerFlag?: boolean;
1414
label?: React.ReactNode;
1515
helpText?: React.ReactNode;
16+
min?: number;
17+
max?: number;
1618
}
1719

1820
const NumberSpinnerField: FC<NumberSpinnerFieldProps> = ({

frontend/packages/console-shared/src/components/formik-fields/ResourceLimitField.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const ResourceLimitField: FC<ResourceLimitFieldProps> = ({
1212
unitName,
1313
unitOptions,
1414
helpText,
15+
inputAriaLabel,
1516
...props
1617
}) => {
1718
const [field, { touched, error }] = useField(props.name);
@@ -27,6 +28,8 @@ const ResourceLimitField: FC<ResourceLimitFieldProps> = ({
2728
<FormGroup fieldId={fieldId} label={label} isRequired={props.required}>
2829
<RequestSizeInput
2930
{...props}
31+
ariaLabel={inputAriaLabel}
32+
inputID={fieldId}
3033
onChange={(val) => {
3134
setFieldValue(props.name, val.value);
3235
setFieldTouched(props.name, true);

0 commit comments

Comments
 (0)