Skip to content

Commit 5fbe2b9

Browse files
committed
more tests
1 parent dda6e47 commit 5fbe2b9

11 files changed

Lines changed: 656 additions & 34 deletions

File tree

packages/gamut/src/DatePicker/DatePicker.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,12 @@ import type { DatePickerProps } from './types';
1818
import { useResolvedLocale } from './utils/locale';
1919
import { DEFAULT_DATE_PICKER_TRANSLATIONS } from './utils/translations';
2020

21-
/**
22-
* DatePicker: single-date or range. Holds shared state and provides it via context.
23-
* Single: selectedDate, setSelectedDate. Range: startDate, endDate, setStartDate, setEndDate.
24-
* With no children, renders default layout (input + calendar popover).
25-
*/
2621
export const DatePicker: React.FC<DatePickerProps> = (props) => {
2722
const {
2823
locale,
2924
shouldDisableDate,
30-
mode,
3125
children,
26+
mode,
3227
translations: translationsProp,
3328
inputSize,
3429
quickActions,
@@ -73,9 +68,10 @@ export const DatePicker: React.FC<DatePickerProps> = (props) => {
7368
...translationsProp,
7469
};
7570
const resolvedQuickActions =
76-
quickActions ?? mode === 'range'
71+
quickActions ??
72+
(mode === 'range'
7773
? getDefaultRangeQuickActions(translations)
78-
: getDefaultSingleQuickActions(resolvedLocale);
74+
: getDefaultSingleQuickActions(resolvedLocale));
7975
const base = {
8076
isCalendarOpen,
8177
openCalendar,
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { MockGamutProvider, setupRtl } from '@codecademy/gamut-tests';
2+
import { render } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import type { FC } from 'react';
5+
6+
import { DatePickerProvider } from '../../DatePickerContext';
7+
import {
8+
createMockRangeContext,
9+
createMockSingleContext,
10+
} from '../../DatePickerContext/__tests__/mockContexts';
11+
import type { DatePickerContextValue } from '../../DatePickerContext/types';
12+
import { DatePickerCalendar } from '..';
13+
14+
jest.mock('react-use', () => {
15+
const actual = jest.requireActual<typeof import('react-use')>('react-use');
16+
return {
17+
...actual,
18+
/** One-month layout in tests (stable gridcell queries). */
19+
useMedia: jest.fn(() => false),
20+
};
21+
});
22+
23+
type CalendarHarnessProps = { context: DatePickerContextValue };
24+
25+
const DatePickerCalendarHarness: FC<CalendarHarnessProps> = ({ context }) => (
26+
<DatePickerProvider value={context}>
27+
<DatePickerCalendar dialogId="test-cal-dialog" />
28+
</DatePickerProvider>
29+
);
30+
31+
const renderCalendar = setupRtl(DatePickerCalendarHarness, {
32+
context: createMockSingleContext({
33+
isCalendarOpen: true,
34+
selectedDate: new Date(2024, 2, 1),
35+
}),
36+
});
37+
38+
describe('DatePickerCalendar', () => {
39+
it('throws when rendered without DatePickerProvider', () => {
40+
expect(() =>
41+
render(
42+
<MockGamutProvider>
43+
<DatePickerCalendar dialogId="orphan" />
44+
</MockGamutProvider>
45+
)
46+
).toThrow(/useDatePickerContext must be used within a DatePicker/);
47+
});
48+
49+
it('renders a calendar grid when the picker context is open', () => {
50+
const { view } = renderCalendar();
51+
52+
expect(view.getByRole('grid')).toBeInTheDocument();
53+
});
54+
55+
it('calls setSelection when a day cell is activated in single mode', async () => {
56+
const onSelection = jest.fn();
57+
const { view } = renderCalendar({
58+
context: createMockSingleContext({
59+
isCalendarOpen: true,
60+
selectedDate: new Date(2024, 2, 1),
61+
onSelection,
62+
}),
63+
});
64+
65+
const march20 = view.getByRole('gridcell', { name: /March 20, 2024/i });
66+
await userEvent.click(march20);
67+
68+
expect(onSelection).toHaveBeenCalledWith(new Date(2024, 2, 20));
69+
});
70+
71+
it('calls onSelection(null) when the already-selected day is clicked again in single mode', async () => {
72+
const onSelection = jest.fn();
73+
const selected = new Date(2024, 2, 20);
74+
const { view } = renderCalendar({
75+
context: createMockSingleContext({
76+
isCalendarOpen: true,
77+
selectedDate: selected,
78+
onSelection,
79+
}),
80+
});
81+
82+
const march20 = view.getByRole('gridcell', { name: /March 20, 2024/i });
83+
await userEvent.click(march20);
84+
85+
expect(onSelection).toHaveBeenCalledWith(null);
86+
});
87+
88+
it('calls closeCalendar after selecting a date in single mode', async () => {
89+
const closeCalendar = jest.fn();
90+
const { view } = renderCalendar({
91+
context: createMockSingleContext({
92+
isCalendarOpen: true,
93+
selectedDate: new Date(2024, 2, 1),
94+
closeCalendar,
95+
}),
96+
});
97+
98+
const march20 = view.getByRole('gridcell', { name: /March 20, 2024/i });
99+
await userEvent.click(march20);
100+
101+
expect(closeCalendar).toHaveBeenCalled();
102+
});
103+
104+
it('invokes closeCalendar when Escape is pressed on the grid', async () => {
105+
const closeCalendar = jest.fn();
106+
const { view } = renderCalendar({
107+
context: createMockSingleContext({
108+
isCalendarOpen: true,
109+
selectedDate: new Date(2024, 2, 1),
110+
closeCalendar,
111+
}),
112+
});
113+
114+
const march15 = view.getByRole('gridcell', { name: /March 15, 2024/i });
115+
march15.focus();
116+
await userEvent.keyboard('{Escape}');
117+
118+
expect(closeCalendar).toHaveBeenCalled();
119+
});
120+
121+
it('calls onRangeSelection with a new start when the start field is active and a day is chosen', async () => {
122+
const onRangeSelection = jest.fn();
123+
const { view } = renderCalendar({
124+
context: createMockRangeContext({
125+
isCalendarOpen: true,
126+
startDate: new Date(2024, 2, 1),
127+
endDate: null,
128+
activeRangePart: 'start',
129+
onRangeSelection,
130+
}),
131+
});
132+
133+
const march20 = view.getByRole('gridcell', { name: /March 20, 2024/i });
134+
await userEvent.click(march20);
135+
136+
expect(onRangeSelection).toHaveBeenCalledWith(new Date(2024, 2, 20), null);
137+
});
138+
139+
it('calls setSelection in range mode when choosing an end date with the end field active', async () => {
140+
const onRangeSelection = jest.fn();
141+
const { view } = renderCalendar({
142+
context: createMockRangeContext({
143+
isCalendarOpen: true,
144+
startDate: new Date(2024, 2, 1),
145+
endDate: null,
146+
activeRangePart: 'end',
147+
onRangeSelection,
148+
}),
149+
});
150+
151+
const march20 = view.getByRole('gridcell', { name: /March 20, 2024/i });
152+
await userEvent.click(march20);
153+
154+
expect(onRangeSelection).toHaveBeenCalledWith(
155+
new Date(2024, 2, 1),
156+
new Date(2024, 2, 20)
157+
);
158+
});
159+
160+
it('calls closeCalendar in range mode when a full range is selected', async () => {
161+
const closeCalendar = jest.fn();
162+
const { view } = renderCalendar({
163+
context: createMockRangeContext({
164+
isCalendarOpen: true,
165+
startDate: new Date(2024, 2, 1),
166+
endDate: null,
167+
activeRangePart: 'end',
168+
closeCalendar,
169+
}),
170+
});
171+
172+
const march20 = view.getByRole('gridcell', { name: /March 20, 2024/i });
173+
await userEvent.click(march20);
174+
175+
expect(closeCalendar).toHaveBeenCalled();
176+
});
177+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { MockGamutProvider } from '@codecademy/gamut-tests';
2+
import { render } from '@testing-library/react';
3+
4+
import { DatePickerProvider, useDatePicker } from '..';
5+
import {
6+
createMockRangeContext,
7+
createMockSingleContext,
8+
} from './mockContexts';
9+
10+
const Consumer = () => {
11+
const ctx = useDatePicker();
12+
return <span data-testid="mode">{ctx.mode}</span>;
13+
};
14+
15+
const RangeDatesConsumer = () => {
16+
const ctx = useDatePicker();
17+
if (ctx.mode !== 'range') return null;
18+
return (
19+
<span data-testid="range-dates">
20+
{ctx.startDate?.toDateString() ?? 'no-start'}|
21+
{ctx.endDate?.toDateString() ?? 'no-end'}
22+
</span>
23+
);
24+
};
25+
26+
describe('DatePickerContext', () => {
27+
it('throws when useDatePicker is used outside DatePickerProvider', () => {
28+
expect(() =>
29+
render(
30+
<MockGamutProvider>
31+
<Consumer />
32+
</MockGamutProvider>
33+
)
34+
).toThrow('useDatePickerContext must be used within a DatePicker.');
35+
});
36+
37+
it('returns single-mode context from useDatePicker when wrapped in DatePickerProvider', () => {
38+
const { getByTestId } = render(
39+
<MockGamutProvider>
40+
<DatePickerProvider value={createMockSingleContext()}>
41+
<Consumer />
42+
</DatePickerProvider>
43+
</MockGamutProvider>
44+
);
45+
46+
expect(getByTestId('mode')).toHaveTextContent('single');
47+
});
48+
49+
it('exposes range fields when the provider value is range mode', () => {
50+
const { getByTestId } = render(
51+
<MockGamutProvider>
52+
<DatePickerProvider value={createMockRangeContext()}>
53+
<Consumer />
54+
</DatePickerProvider>
55+
</MockGamutProvider>
56+
);
57+
58+
expect(getByTestId('mode')).toHaveTextContent('range');
59+
});
60+
61+
it('passes startDate and endDate through to consumers in range mode', () => {
62+
const start = new Date(2026, 3, 10);
63+
const end = new Date(2026, 3, 20);
64+
const { getByTestId } = render(
65+
<MockGamutProvider>
66+
<DatePickerProvider
67+
value={createMockRangeContext({ startDate: start, endDate: end })}
68+
>
69+
<RangeDatesConsumer />
70+
</DatePickerProvider>
71+
</MockGamutProvider>
72+
);
73+
74+
expect(getByTestId('range-dates')).toHaveTextContent(
75+
`${start.toDateString()}|${end.toDateString()}`
76+
);
77+
});
78+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { DEFAULT_DATE_PICKER_TRANSLATIONS } from '../../utils/translations';
2+
import type {
3+
DatePickerRangeContextValue,
4+
DatePickerSingleContextValue,
5+
} from '../types';
6+
7+
export function createMockSingleContext(
8+
overrides: Partial<DatePickerSingleContextValue> = {}
9+
): DatePickerSingleContextValue {
10+
return {
11+
mode: 'single',
12+
locale: new Intl.Locale('en-US'),
13+
isCalendarOpen: false,
14+
openCalendar: jest.fn(),
15+
focusCalendar: jest.fn(),
16+
focusGridSignal: false,
17+
gridFocusRequested: false,
18+
clearGridFocusRequest: jest.fn(),
19+
closeCalendar: jest.fn(),
20+
calendarDialogId: 'test-datepicker-dialog',
21+
translations: { ...DEFAULT_DATE_PICKER_TRANSLATIONS },
22+
quickActions: [],
23+
selectedDate: new Date(2024, 2, 15),
24+
onSelection: jest.fn(),
25+
...overrides,
26+
};
27+
}
28+
29+
export function createMockRangeContext(
30+
overrides: Partial<DatePickerRangeContextValue> = {}
31+
): DatePickerRangeContextValue {
32+
return {
33+
mode: 'range',
34+
locale: new Intl.Locale('en-US'),
35+
isCalendarOpen: false,
36+
openCalendar: jest.fn(),
37+
focusCalendar: jest.fn(),
38+
focusGridSignal: false,
39+
gridFocusRequested: false,
40+
clearGridFocusRequest: jest.fn(),
41+
closeCalendar: jest.fn(),
42+
calendarDialogId: 'test-datepicker-dialog',
43+
translations: { ...DEFAULT_DATE_PICKER_TRANSLATIONS },
44+
quickActions: [],
45+
startDate: null,
46+
endDate: null,
47+
onRangeSelection: jest.fn(),
48+
activeRangePart: null,
49+
setActiveRangePart: jest.fn(),
50+
...overrides,
51+
};
52+
}

packages/gamut/src/DatePicker/DatePickerInput/Segment/__tests__/DatePickerInputSegment.test.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
/* eslint-disable simple-import-sort/imports -- import sort vs consistent-type-imports for relative paths */
2-
3-
import { useCallback, useState, type FC } from 'react';
4-
51
import { setupRtl } from '@codecademy/gamut-tests';
62
import userEvent from '@testing-library/user-event';
3+
import { type FC, useCallback, useState } from 'react';
74

8-
import { DatePickerInputSegment } from '../DatePickerInputSegment';
95
import type { DatePartKind } from '../../utils';
10-
import type { AssignSegmentRef } from '../DatePickerInputSegment';
11-
import type { SegmentValues } from '../segmentUtils';
6+
import { type AssignSegmentRef, DatePickerInputSegment } from '..';
7+
import type { SegmentValues } from '../utils';
128

139
const noop = () => undefined;
1410

@@ -46,13 +42,13 @@ const SegmentHarness: FC<HarnessProps> = ({
4642
disabled={disabled}
4743
error={error}
4844
field={field}
49-
focusOrOpenCalendarGrid={focusOrOpenCalendarGrid}
50-
focusSegmentField={noopFocusSegmentField}
51-
handleOnFocus={noop}
5245
nextField={null}
5346
prevField={null}
5447
segments={segments}
5548
setSegments={setSegments}
49+
onAltArrowDown={focusOrOpenCalendarGrid}
50+
onFocus={noop}
51+
onSiblingFocus={noopFocusSegmentField}
5652
/>
5753
);
5854
};
@@ -90,6 +86,17 @@ describe('DatePickerInputSegment', () => {
9086
expect(month).toHaveAttribute('tabIndex', '-1');
9187
});
9288

89+
it('ignores digit input when disabled', async () => {
90+
const user = userEvent.setup();
91+
const { view } = renderView({ disabled: true });
92+
93+
const month = view.getByRole('spinbutton', { name: 'month' });
94+
await user.click(month);
95+
await user.keyboard('5');
96+
97+
expect(month).toHaveAttribute('aria-valuetext', 'MM');
98+
});
99+
93100
it('increments month with ArrowUp from empty', async () => {
94101
const user = userEvent.setup();
95102
const { view } = renderView({});

0 commit comments

Comments
 (0)