Merge pull request #766 from PicPeak/fix/date-input-invalid-crash
fix(admin): stop the event-date field crashing the page on backspace
This commit is contained in:
@@ -90,6 +90,13 @@ export const LocalizedDateInput: React.FC<LocalizedDateInputProps> = ({
|
|||||||
[d, mo, y] = [a, b, c];
|
[d, mo, y] = [a, b, c];
|
||||||
}
|
}
|
||||||
if (!/^\d{1,2}$/.test(d) || !/^\d{1,2}$/.test(mo) || !/^\d{4}$/.test(y)) return '';
|
if (!/^\d{1,2}$/.test(d) || !/^\d{1,2}$/.test(mo) || !/^\d{4}$/.test(y)) return '';
|
||||||
|
// Reject impossible calendar dates (day 00, month 13, 31 Feb…) — a partial
|
||||||
|
// value mid-backspace like "0/07/2026" is otherwise coerced to "2026-07-00",
|
||||||
|
// which is a valid string but an Invalid Date that crashes date-fns format()
|
||||||
|
// downstream. Round-trip through Date to confirm the components survive.
|
||||||
|
const yy = Number(y), mm = Number(mo), dd = Number(d);
|
||||||
|
const probe = new Date(yy, mm - 1, dd);
|
||||||
|
if (probe.getFullYear() !== yy || probe.getMonth() !== mm - 1 || probe.getDate() !== dd) return '';
|
||||||
return `${y}-${mo.padStart(2, '0')}-${d.padStart(2, '0')}`;
|
return `${y}-${mo.padStart(2, '0')}-${d.padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
import { LocalizedDateInput } from '../LocalizedDateInput';
|
||||||
|
|
||||||
|
// Stub the settings hook so the component doesn't need a QueryClient; falls
|
||||||
|
// back to the default DD.MM.YYYY display format. The parse/validation under
|
||||||
|
// test is separator-independent.
|
||||||
|
vi.mock('../../../hooks/usePublicSettings', () => ({
|
||||||
|
usePublicSettings: () => ({ settings: {} }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('LocalizedDateInput', () => {
|
||||||
|
const renderInput = (value = '2026-07-07') => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
render(<LocalizedDateInput value={value} onChange={onChange} />);
|
||||||
|
const input = screen.getByDisplayValue('07.07.2026') as HTMLInputElement;
|
||||||
|
return { input, onChange };
|
||||||
|
};
|
||||||
|
|
||||||
|
it('does not commit an impossible date mid-edit (regression: backspacing the day → "2026-07-00" crashed the page)', () => {
|
||||||
|
const { input, onChange } = renderInput();
|
||||||
|
// Backspacing a day digit leaves "0.07.2026" — a syntactically complete but
|
||||||
|
// invalid date. It must NOT propagate (used to coerce to "2026-07-00",
|
||||||
|
// which crashed date-fns format() downstream).
|
||||||
|
fireEvent.change(input, { target: { value: '0.07.2026' } });
|
||||||
|
expect(onChange).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Nor may an out-of-range calendar date (31 Feb).
|
||||||
|
fireEvent.change(input, { target: { value: '31.02.2026' } });
|
||||||
|
expect(onChange).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('commits a complete, valid date as ISO', () => {
|
||||||
|
const { input, onChange } = renderInput();
|
||||||
|
fireEvent.change(input, { target: { value: '15.08.2026' } });
|
||||||
|
expect(onChange).toHaveBeenCalledWith('2026-08-15');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { format as dateFnsFormat, formatDistanceToNow as dateFnsFormatDistanceToNow } from 'date-fns';
|
import { format as dateFnsFormat, formatDistanceToNow as dateFnsFormatDistanceToNow, isValid } from 'date-fns';
|
||||||
import { de, enUS, ptBR, fr } from 'date-fns/locale';
|
import { de, enUS, ptBR, fr } from 'date-fns/locale';
|
||||||
import { usePublicSettings } from './usePublicSettings';
|
import { usePublicSettings } from './usePublicSettings';
|
||||||
|
|
||||||
@@ -32,6 +32,11 @@ export const useLocalizedDate = () => {
|
|||||||
|
|
||||||
const format = (date: Date | string, formatStr?: string) => {
|
const format = (date: Date | string, formatStr?: string) => {
|
||||||
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
||||||
|
// date-fns `format` throws RangeError on an Invalid Date, which crashes the
|
||||||
|
// whole page when a call site renders a transient/partial date (e.g. the
|
||||||
|
// event-date field mid-edit). Return '' instead so a bad value degrades to
|
||||||
|
// blank rather than tearing down the tree.
|
||||||
|
if (!isValid(dateObj)) return '';
|
||||||
// Use admin-configured date format if available and no format string provided
|
// Use admin-configured date format if available and no format string provided
|
||||||
let dateFormat = formatStr;
|
let dateFormat = formatStr;
|
||||||
if (!dateFormat && settings?.general_date_format) {
|
if (!dateFormat && settings?.general_date_format) {
|
||||||
@@ -50,6 +55,7 @@ export const useLocalizedDate = () => {
|
|||||||
|
|
||||||
const formatDistanceToNow = (date: Date | string, options?: { addSuffix?: boolean }) => {
|
const formatDistanceToNow = (date: Date | string, options?: { addSuffix?: boolean }) => {
|
||||||
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
||||||
|
if (!isValid(dateObj)) return '';
|
||||||
return dateFnsFormatDistanceToNow(dateObj, { ...options, locale: getLocale() });
|
return dateFnsFormatDistanceToNow(dateObj, { ...options, locale: getLocale() });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user