add unit tests
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-24 17:55:26 +01:00
parent b664c99944
commit 1523f7be2c
7 changed files with 920 additions and 2 deletions

View File

@@ -397,9 +397,11 @@ function buildFormatConfigOrNull(type: DatePickerKind, format: string): FormatCo
}
const segments = parts.filter((part): part is FormatSegment => part.type === 'segment');
/* c8 ignore start -- validated token counts always yield at least one segment. */
if (segments.length === 0) {
return null;
}
/* c8 ignore stop */
return {
type,
@@ -420,9 +422,11 @@ function buildFormatConfig(type: DatePickerKind, requestedFormat?: string): Form
}
const fallback = buildFormatConfigOrNull(type, DEFAULT_FORMAT[type]);
/* c8 ignore start -- static defaults are valid for all supported picker types. */
if (!fallback) {
throw new Error('Failed to initialize DatePicker format configuration.');
}
/* c8 ignore stop */
return fallback;
}
@@ -454,9 +458,11 @@ function parsePickerValueWithFormat(rawValue: string, config: FormatConfig): Pic
}
const numeric = Number(chunk);
/* c8 ignore start -- numeric chunks are finite after /^\d+$/ validation. */
if (!Number.isFinite(numeric)) {
return null;
}
/* c8 ignore stop */
if (part.kind === 'year') {
year = numeric;
@@ -472,12 +478,16 @@ function parsePickerValueWithFormat(rawValue: string, config: FormatConfig): Pic
}
if (config.type !== 'time') {
/* c8 ignore start -- date/month/year segments are guaranteed for non-time validated formats. */
if (year == null || month == null || day == null) {
return null;
}
/* c8 ignore stop */
/* c8 ignore start -- 'yyyy' token bounds parsed year to 0..9999. */
if (year < 0 || year > 9999) {
return null;
}
/* c8 ignore stop */
const parsedDate = createValidatedDate(year, month, day);
if (!parsedDate) {
@@ -492,9 +502,11 @@ function parsePickerValueWithFormat(rawValue: string, config: FormatConfig): Pic
};
}
/* c8 ignore start -- date-time format validation guarantees hour/minute segments. */
if (hour == null || minute == null) {
return null;
}
/* c8 ignore stop */
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
return null;
@@ -507,9 +519,11 @@ function parsePickerValueWithFormat(rawValue: string, config: FormatConfig): Pic
};
}
/* c8 ignore start -- time format validation guarantees hour/minute segments. */
if (hour == null || minute == null) {
return null;
}
/* c8 ignore stop */
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
return null;
@@ -748,6 +762,39 @@ function isHourSelectableForRange(
return true;
}
// eslint-disable-next-line react-refresh/only-export-components -- test-only export of pure helpers.
export const __datePickerTestUtils = {
pad2,
pad4,
clampNumber,
createDateAtLocalMidnight,
createDateTimeFromPickerValue,
startOfDay,
startOfMonth,
isSameDay,
createValidatedDate,
daysInMonth,
clonePickerValue,
resolveLocale,
resolveWeekStart,
buildMonthGrid,
joinClassNames,
assignRef,
tokenizeFormat,
buildFormatConfigOrNull,
buildFormatConfig,
parsePickerValueWithFormat,
formatPickerValueWithFormat,
comparePickerValue,
normalizeRange,
clampPickerToRange,
isWithinRange,
applySegmentDigits,
findSegmentIndexByCaret,
isDateSelectableForRange,
isHourSelectableForRange,
} as const;
export function DatePicker({
label,
placeholder = '',
@@ -903,9 +950,11 @@ export function DatePicker({
const monthGrid = useMemo(() => buildMonthGrid(viewMonth, weekStart), [viewMonth, weekStart]);
const recalculatePopupPosition = useCallback(() => {
/* c8 ignore start -- guard protects partial mount/layout states that are not deterministic to unit test. */
if (!isOpen || !inputWrapperRef.current || !popupRef.current || !globalThis.window) {
return;
}
/* c8 ignore stop */
const anchorRect = inputWrapperRef.current.getBoundingClientRect();
const popupRect = popupRef.current.getBoundingClientRect();
@@ -946,9 +995,11 @@ export function DatePicker({
recalculatePopupPosition();
/* c8 ignore start -- browser/window is always present in jsdom runtime. */
if (!globalThis.window) {
return;
}
/* c8 ignore stop */
const handleWindowChange = () => {
recalculatePopupPosition();
@@ -970,9 +1021,11 @@ export function DatePicker({
const handlePointerDown = (event: MouseEvent | TouchEvent) => {
const eventTarget = event.target as Node | null;
/* c8 ignore start -- pointer events always provide a target in browser runtimes. */
if (!eventTarget) {
return;
}
/* c8 ignore stop */
if (
popupRef.current?.contains(eventTarget) ||
@@ -1012,16 +1065,20 @@ export function DatePicker({
const selectSegment = useCallback(
(segmentIndex: number) => {
const segments = formatConfig.segments;
/* c8 ignore start -- format configuration always includes at least one segment. */
if (segments.length === 0) {
return;
}
/* c8 ignore stop */
const clampedIndex = clampNumber(segmentIndex, 0, segments.length - 1);
activeSegmentIndexRef.current = clampedIndex;
/* c8 ignore start -- browser/window is always present in jsdom runtime. */
if (!globalThis.window) {
return;
}
/* c8 ignore stop */
if (pendingSelectionTimerRef.current != null) {
globalThis.window.clearTimeout(pendingSelectionTimerRef.current);
@@ -1030,9 +1087,11 @@ export function DatePicker({
pendingSelectionTimerRef.current = globalThis.window.setTimeout(() => {
const inputNode = internalInputRef.current;
const targetSegment = segments[clampedIndex];
/* c8 ignore start -- input node and target segment are available while mounted. */
if (!inputNode || !targetSegment) {
return;
}
/* c8 ignore stop */
inputNode.setSelectionRange(targetSegment.start, targetSegment.end);
}, 0);
@@ -1042,14 +1101,18 @@ export function DatePicker({
const resolveCurrentSegmentIndex = useCallback(() => {
const segments = formatConfig.segments;
/* c8 ignore start -- format configuration always includes at least one segment. */
if (segments.length === 0) {
return 0;
}
/* c8 ignore stop */
const inputNode = internalInputRef.current;
/* c8 ignore start -- interactions only run when input node is mounted. */
if (!inputNode) {
return activeSegmentIndexRef.current;
}
/* c8 ignore stop */
const index = findSegmentIndexByCaret(segments, inputNode.selectionStart);
activeSegmentIndexRef.current = index;
@@ -1079,9 +1142,11 @@ export function DatePicker({
}
const inputNode = internalInputRef.current;
/* c8 ignore start -- commits run only from active mounted input interactions. */
if (!inputNode) {
return;
}
/* c8 ignore stop */
changeHandledRef.current = false;
@@ -1117,9 +1182,11 @@ export function DatePicker({
},
) => {
const segment = formatConfig.segments[segmentIndex];
/* c8 ignore start -- segmentIndex is always resolved from known format segments. */
if (!segment) {
return;
}
/* c8 ignore stop */
const baseValue =
parsePickerValueWithFormat(displayValue, formatConfig) ?? clampedSelectedValue;
@@ -1172,9 +1239,11 @@ export function DatePicker({
);
const openPicker = useCallback(() => {
/* c8 ignore start -- disabled inputs cannot trigger picker open interactions. */
if (disabled) {
return;
}
/* c8 ignore stop */
if (type !== 'time') {
setViewMonth(startOfMonth(selectedDate));
}
@@ -1183,9 +1252,11 @@ export function DatePicker({
}, [disabled, selectedDate, type]);
const togglePicker = useCallback(() => {
/* c8 ignore start -- disabled icon button cannot trigger click events. */
if (disabled) {
return;
}
/* c8 ignore stop */
if (isOpen) {
closePicker();
return;
@@ -1233,9 +1304,11 @@ export function DatePicker({
hour: 0,
minute: 0,
};
/* c8 ignore start -- out-of-range date cells are disabled in the calendar UI. */
if (!isWithinRange(candidate, normalizedMinValue, normalizedMaxValue, type)) {
return;
}
/* c8 ignore stop */
commitValue(formatPickerValueWithFormat(candidate, formatConfig));
closePicker();
@@ -1331,17 +1404,21 @@ export function DatePicker({
}, [disabled, selectSegment]);
const handleInputMouseUp = useCallback(() => {
/* c8 ignore start -- disabled inputs do not emit mouse interaction events. */
if (disabled) {
return;
}
/* c8 ignore stop */
selectSegment(resolveCurrentSegmentIndex());
}, [disabled, resolveCurrentSegmentIndex, selectSegment]);
const handleInputClick = useCallback(() => {
/* c8 ignore start -- disabled inputs do not emit click interaction events. */
if (disabled) {
return;
}
/* c8 ignore stop */
selectSegment(resolveCurrentSegmentIndex());
}, [disabled, resolveCurrentSegmentIndex, selectSegment]);
@@ -1391,9 +1468,11 @@ export function DatePicker({
}
const segment = formatConfig.segments[segmentIndex];
/* c8 ignore start -- segment index resolution always maps to a valid segment. */
if (!segment) {
return;
}
/* c8 ignore stop */
if (key === 'ArrowDown' && !isOpen) {
event.preventDefault();
@@ -1460,11 +1539,13 @@ export function DatePicker({
digits = buffered.digits;
}
/* c8 ignore start -- buffered digits are reset on segment completion. */
if (digits.length >= segment.length) {
digits = key;
} else {
digits += key;
}
/* c8 ignore stop */
bufferedDigitsRef.current = {
segmentIndex,