mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
app: number fields no longer throw "e.preventDefault is not a function" (#735)
`useForm`'s input handlers called `e.preventDefault()` unconditionally, but the
Mantine-backed `NumberField` (Sleep between requests, Depth, Maximum Pages)
hands them a bare `{target}` object because NumberInput emits a value, not an
event. Every keystroke in those fields threw.
`handleInputEvent` now only calls `preventDefault` when the argument actually
has one, and `getInputProps`' wrapper no longer repeats the call. Audited every
other `preventDefault` call site: all are bound to real DOM/React events or a
guarded no-argument call.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
eead78007d
commit
20541d7184
3 changed files with 64 additions and 4 deletions
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import {render, screen, waitFor} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {DestinationForm} from './Download';
|
||||
import {createTestForm} from '../test-utils';
|
||||
import {DepthInputForm, DestinationForm} from './Download';
|
||||
import {useForm} from '../hooks/useForm';
|
||||
import {createTestForm, renderWithProviders} from '../test-utils';
|
||||
|
||||
// Mock DirectorySearch component - simplified to avoid useState/useEffect warnings
|
||||
jest.mock('./Common', () => {
|
||||
|
|
@ -251,3 +252,26 @@ describe('DestinationForm', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('NumberField', () => {
|
||||
// Renders a NumberField against a real useForm: the Mantine NumberInput passes a value (not an
|
||||
// event) to onChange, which used to reach `e.preventDefault()` and throw while typing.
|
||||
function DepthHarness({onData}) {
|
||||
const form = useForm({defaultFormData: {settings: {depth: ''}}, submitter: jest.fn()});
|
||||
onData(form.formData);
|
||||
return <DepthInputForm form={form}/>;
|
||||
}
|
||||
|
||||
it('typing into a NumberField updates the form without throwing', async () => {
|
||||
let latest = null;
|
||||
renderWithProviders(<DepthHarness onData={(d) => latest = d}/>);
|
||||
|
||||
const input = screen.getByRole('textbox', {name: /Depth/});
|
||||
await userEvent.type(input, '3');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(latest.settings.depth).toBe(3);
|
||||
});
|
||||
expect(input).toHaveValue('3');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -188,7 +188,9 @@ export function useForm({
|
|||
}, 300), [validators]);
|
||||
|
||||
const handleInputEvent = (e) => {
|
||||
if (e) e.preventDefault();
|
||||
// Mantine inputs (NumberInput, etc.) hand back a value rather than a DOM event, so callers
|
||||
// wrap it as a bare `{target: {...}}` object. Only a real event has `preventDefault`.
|
||||
if (e && typeof e.preventDefault === 'function') e.preventDefault();
|
||||
let {type, value} = e.target;
|
||||
let {path} = e.target.dataset;
|
||||
path = path || e.target.name;
|
||||
|
|
@ -286,7 +288,8 @@ export function useForm({
|
|||
const [customProps, inputAttrs] = getCustomProps({name, validator, path, type, required, afterChange});
|
||||
|
||||
const localHandleInputEvent = async (e) => {
|
||||
if (e) e.preventDefault();
|
||||
// `handleInputEvent` already handles a real event's preventDefault, and tolerates the
|
||||
// value-shaped objects Mantine-backed fields pass.
|
||||
const value = handleInputEvent(e);
|
||||
if (onChange) {
|
||||
await onChange(value);
|
||||
|
|
|
|||
|
|
@ -465,6 +465,39 @@ describe('useForm', () => {
|
|||
expect(result.current.formData.sleep_requests).toBe(0.75);
|
||||
});
|
||||
|
||||
it('handleInputEvent accepts a value-shaped event with no preventDefault (Mantine NumberInput)', () => {
|
||||
// Mantine's NumberInput hands back the value, so NumberField wraps it in a bare
|
||||
// {target} object. Typing must not throw "e.preventDefault is not a function".
|
||||
const {result} = renderHook(() => useForm({
|
||||
defaultFormData: {settings: {sleep_requests: ''}},
|
||||
submitter: jest.fn(),
|
||||
}));
|
||||
|
||||
act(() => {
|
||||
result.current.handleInputEvent({
|
||||
target: {type: 'number', value: 0.75, dataset: {path: 'settings.sleep_requests'}},
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.formData.settings.sleep_requests).toBe(0.75);
|
||||
});
|
||||
|
||||
it('getInputProps onChange accepts a value-shaped event with no preventDefault', async () => {
|
||||
const {result} = renderHook(() => useForm({
|
||||
defaultFormData: {settings: {depth: ''}},
|
||||
submitter: jest.fn(),
|
||||
}));
|
||||
|
||||
await act(async () => {
|
||||
const [inputProps] = result.current.getInputProps({name: 'depth', path: 'settings.depth', type: 'number'});
|
||||
await inputProps.onChange({
|
||||
target: {type: 'number', value: 3, dataset: {path: 'settings.depth'}},
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.formData.settings.depth).toBe(3);
|
||||
});
|
||||
|
||||
it('handleInputEvent uses data-path attribute when available', () => {
|
||||
const {result} = renderHook(() => useForm({
|
||||
defaultFormData: {nested: {field: ''}},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue