Skip to content

Commit 90b24c1

Browse files
authored
Create cron schedule input (#1038)
* Create cron schedule input Signed-off-by: Assem Hafez <[email protected]> * refactor popover and other cron input components --------- Signed-off-by: Assem Hafez <[email protected]>
1 parent 3236871 commit 90b24c1

10 files changed

+852
-0
lines changed
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
import React from 'react';
2+
3+
import { render, screen, userEvent } from '@/test-utils/rtl';
4+
5+
import CronScheduleInput from '../cron-schedule-input';
6+
import { type CronScheduleInputProps } from '../cron-schedule-input.types';
7+
8+
// Mock the CronScheduleInputPopover component
9+
jest.mock('../cron-schedule-input-popover/cron-schedule-input-popover', () => {
10+
return function MockCronScheduleInputPopover({
11+
fieldType,
12+
}: {
13+
fieldType: string;
14+
}) {
15+
return (
16+
<div data-testid={`cron-schedule-input-popover-${fieldType}`}>
17+
Popover for {fieldType}
18+
</div>
19+
);
20+
};
21+
});
22+
23+
const defaultProps: CronScheduleInputProps = {
24+
value: {
25+
minutes: '0',
26+
hours: '9',
27+
daysOfMonth: '1',
28+
months: '1',
29+
daysOfWeek: '1',
30+
},
31+
onChange: jest.fn(),
32+
onBlur: jest.fn(),
33+
onFocus: jest.fn(),
34+
};
35+
36+
describe(CronScheduleInput.name, () => {
37+
beforeEach(() => {
38+
jest.clearAllMocks();
39+
});
40+
41+
it('should render all cron fields with correct labels', () => {
42+
setup();
43+
44+
expect(screen.getByText('Minute')).toBeInTheDocument();
45+
expect(screen.getByText('Hour')).toBeInTheDocument();
46+
expect(screen.getByText('Day of Month')).toBeInTheDocument();
47+
expect(screen.getByText('Month')).toBeInTheDocument();
48+
expect(screen.getByText('Day of Week')).toBeInTheDocument();
49+
});
50+
51+
it('should render input fields with correct values', () => {
52+
setup();
53+
54+
expect(screen.getByLabelText('Minute')).toHaveValue('0');
55+
expect(screen.getByLabelText('Hour')).toHaveValue('9');
56+
expect(screen.getByLabelText('Day of Month')).toHaveValue('1');
57+
expect(screen.getByLabelText('Month')).toHaveValue('1');
58+
expect(screen.getByLabelText('Day of Week')).toHaveValue('1');
59+
});
60+
61+
it('should render empty inputs when no value is provided', () => {
62+
setup({ value: undefined });
63+
64+
expect(screen.getByLabelText('Minute')).toHaveValue('');
65+
expect(screen.getByLabelText('Hour')).toHaveValue('');
66+
expect(screen.getByLabelText('Day of Month')).toHaveValue('');
67+
expect(screen.getByLabelText('Month')).toHaveValue('');
68+
expect(screen.getByLabelText('Day of Week')).toHaveValue('');
69+
});
70+
71+
it('should render with partial values', () => {
72+
const partialValue = {
73+
minutes: '30',
74+
hours: '14',
75+
};
76+
setup({ value: partialValue });
77+
78+
expect(screen.getByLabelText('Minute')).toHaveValue('30');
79+
expect(screen.getByLabelText('Hour')).toHaveValue('14');
80+
81+
expect(screen.getByLabelText('Day of Month')).toHaveValue('');
82+
expect(screen.getByLabelText('Month')).toHaveValue('');
83+
expect(screen.getByLabelText('Day of Week')).toHaveValue('');
84+
});
85+
86+
it('should call onChange when input value changes', async () => {
87+
const { user, mockOnChange } = setup({ value: { minutes: '' } });
88+
89+
const minutesInput = screen.getByLabelText('Minute');
90+
await user.type(minutesInput, '15');
91+
92+
expect(mockOnChange).toHaveBeenCalled();
93+
94+
const calls = mockOnChange.mock.calls;
95+
expect(calls[0][0].minutes).toBe('1');
96+
expect(calls[1][0].minutes).toBe('5');
97+
});
98+
99+
it('should call onFocus when input is focused', async () => {
100+
const { user, mockOnFocus } = setup();
101+
102+
const minutesInput = screen.getByLabelText('Minute');
103+
await user.click(minutesInput);
104+
105+
expect(mockOnFocus).toHaveBeenCalled();
106+
});
107+
108+
it('should call onBlur when input loses focus', async () => {
109+
const { user, mockOnBlur } = setup();
110+
111+
const minutesInput = screen.getByLabelText('Minute');
112+
await user.click(minutesInput);
113+
await user.tab();
114+
115+
expect(mockOnBlur).toHaveBeenCalled();
116+
});
117+
118+
it('should handle multiple field changes correctly', async () => {
119+
const { user, mockOnChange } = setup({ value: { minutes: '', hours: '' } });
120+
121+
const minutesInput = screen.getByLabelText('Minute');
122+
const hoursInput = screen.getByLabelText('Hour');
123+
124+
await user.type(minutesInput, '1');
125+
126+
await user.type(hoursInput, '2');
127+
128+
expect(mockOnChange).toHaveBeenCalled();
129+
130+
const calls = mockOnChange.mock.calls;
131+
expect(calls[0][0].minutes).toBe('1');
132+
expect(calls[1][0].hours).toBe('2');
133+
});
134+
135+
it('should open popover when input is focused', async () => {
136+
const { user } = setup();
137+
138+
const minutesInput = screen.getByLabelText('Minute');
139+
await user.click(minutesInput);
140+
141+
expect(
142+
screen.getByTestId('cron-schedule-input-popover-minutes')
143+
).toBeInTheDocument();
144+
});
145+
146+
it('should close popover when input loses focus', async () => {
147+
const { user } = setup();
148+
149+
const minutesInput = screen.getByLabelText('Minute');
150+
await user.click(minutesInput);
151+
152+
expect(
153+
screen.getByTestId('cron-schedule-input-popover-minutes')
154+
).toBeInTheDocument();
155+
156+
await user.tab();
157+
158+
expect(
159+
screen.queryByTestId('cron-schedule-input-popover-minutes')
160+
).not.toBeInTheDocument();
161+
});
162+
163+
it('should open correct popover for each field', async () => {
164+
const { user } = setup();
165+
166+
// Focus minutes field
167+
const minutesInput = screen.getByLabelText('Minute');
168+
await user.click(minutesInput);
169+
expect(
170+
screen.getByTestId('cron-schedule-input-popover-minutes')
171+
).toBeInTheDocument();
172+
173+
// Focus hours field - this should close the minutes popover and open hours popover
174+
const hoursInput = screen.getByLabelText('Hour');
175+
await user.click(hoursInput);
176+
177+
// The minutes popover should be closed and hours popover should be open
178+
expect(
179+
screen.getByTestId('cron-schedule-input-popover-hours')
180+
).toBeInTheDocument();
181+
});
182+
183+
it('should show error state for string error', () => {
184+
setup({ error: 'Invalid cron expression' });
185+
186+
const inputs = screen.getAllByRole('textbox');
187+
inputs.forEach((input) => {
188+
expect(input).toHaveAttribute('aria-invalid', 'true');
189+
});
190+
});
191+
192+
it('should show error state for specific field errors', () => {
193+
const fieldErrors = {
194+
minutes: 'Invalid minutes value',
195+
hours: 'Invalid hours value',
196+
daysOfMonth: '',
197+
months: '',
198+
daysOfWeek: '',
199+
seconds: '',
200+
years: '',
201+
};
202+
203+
setup({ error: fieldErrors });
204+
205+
// Verify error states by selecting inputs by their labels
206+
expect(screen.getByLabelText('Minute')).toHaveAttribute(
207+
'aria-invalid',
208+
'true'
209+
);
210+
expect(screen.getByLabelText('Hour')).toHaveAttribute(
211+
'aria-invalid',
212+
'true'
213+
);
214+
expect(screen.getByLabelText('Day of Month')).not.toHaveAttribute(
215+
'aria-invalid',
216+
'true'
217+
);
218+
expect(screen.getByLabelText('Month')).not.toHaveAttribute(
219+
'aria-invalid',
220+
'true'
221+
);
222+
expect(screen.getByLabelText('Day of Week')).not.toHaveAttribute(
223+
'aria-invalid',
224+
'true'
225+
);
226+
});
227+
228+
it('should not show error state when no error is provided', () => {
229+
setup();
230+
231+
const inputs = screen.getAllByRole('textbox');
232+
inputs.forEach((input) => {
233+
expect(input).not.toHaveAttribute('aria-invalid', 'true');
234+
});
235+
});
236+
237+
it('should disable all inputs when disabled prop is true', () => {
238+
setup({ disabled: true });
239+
240+
const inputs = screen.getAllByRole('textbox');
241+
inputs.forEach((input) => {
242+
expect(input).toBeDisabled();
243+
});
244+
});
245+
246+
it('should enable all inputs when disabled prop is false', () => {
247+
setup({ disabled: false });
248+
249+
const inputs = screen.getAllByRole('textbox');
250+
inputs.forEach((input) => {
251+
expect(input).not.toBeDisabled();
252+
});
253+
});
254+
255+
it('should enable inputs by default when disabled prop is not provided', () => {
256+
setup();
257+
258+
const inputs = screen.getAllByRole('textbox');
259+
inputs.forEach((input) => {
260+
expect(input).not.toBeDisabled();
261+
});
262+
});
263+
264+
it('should not trigger onChange when disabled', async () => {
265+
const { user, mockOnChange } = setup({ disabled: true });
266+
267+
const minutesInput = screen.getByLabelText('Minute');
268+
await user.click(minutesInput);
269+
await user.type(minutesInput, '15');
270+
271+
expect(mockOnChange).not.toHaveBeenCalled();
272+
});
273+
274+
it('should handle undefined onChange gracefully', async () => {
275+
const { user } = setup({ onChange: undefined });
276+
277+
const minutesInput = screen.getByLabelText('Minute');
278+
await user.type(minutesInput, '5');
279+
280+
// Should not throw an error
281+
// Note: The input value won't update because there's no state management
282+
// when onChange is undefined, but the component should not crash
283+
expect(minutesInput).toBeInTheDocument();
284+
});
285+
286+
it('should handle undefined onFocus gracefully', async () => {
287+
const { user } = setup({ onFocus: undefined });
288+
289+
const minutesInput = screen.getByLabelText('Minute');
290+
await user.click(minutesInput);
291+
292+
// Should not throw an error
293+
expect(
294+
screen.getByTestId('cron-schedule-input-popover-minutes')
295+
).toBeInTheDocument();
296+
});
297+
298+
it('should handle undefined onBlur gracefully', async () => {
299+
const { user } = setup({ onBlur: undefined });
300+
301+
const minutesInput = screen.getByLabelText('Minute');
302+
await user.click(minutesInput);
303+
await user.tab();
304+
305+
// Should not throw an error
306+
expect(
307+
screen.queryByTestId('cron-schedule-input-popover-minutes')
308+
).not.toBeInTheDocument();
309+
});
310+
311+
it('should maintain focus management between fields', async () => {
312+
const { user } = setup();
313+
314+
const minutesInput = screen.getByLabelText('Minute');
315+
const hoursInput = screen.getByLabelText('Hour');
316+
317+
await user.click(minutesInput);
318+
expect(minutesInput).toHaveFocus();
319+
320+
await user.tab();
321+
expect(hoursInput).toHaveFocus();
322+
});
323+
});
324+
325+
function setup(props: Partial<CronScheduleInputProps> = {}) {
326+
const mockOnChange = jest.fn();
327+
const mockOnBlur = jest.fn();
328+
const mockOnFocus = jest.fn();
329+
const user = userEvent.setup();
330+
331+
const mergedProps = {
332+
...defaultProps,
333+
onChange: mockOnChange,
334+
onBlur: mockOnBlur,
335+
onFocus: mockOnFocus,
336+
...props,
337+
};
338+
339+
render(<CronScheduleInput {...mergedProps} />);
340+
341+
return {
342+
user,
343+
mockOnChange,
344+
mockOnBlur,
345+
mockOnFocus,
346+
};
347+
}

0 commit comments

Comments
 (0)