Search code examples
javascriptreactjsjestjsreact-testing-libraryformik

React Testing Library: Input Field Not Updating Value in Tests


I'm working with a custom React component that wraps an Office UI Fabric TextField within a Formik form. Despite following typical patterns for handling input fields with React and Formik, I'm encountering issues when testing the component using React Testing Library. The input's value does not update after simulating a change event in Jest.

Here's the relevant part of my component:

const FMIconButtonTextField: React.FC<Props> = props => {
  const { label, styles, name, callOutSpanText } = props;
  const [isCalloutVisible, setIsCalloutVisible] = useState(false);
  const inputId = getId(name);
  const [field, { touched, error }] = useField(name);

  return (
    <>
      <TextField
        {...field}
        {...props}
        label={label}
        errorMessage={touched && error ? error : null}
        id={inputId}
      />
    </>
  );
};

And here's how I'm testing it:

it('allows input into the voucher code field', async () => {
  const initialValues = { voucherCode: '' };
  const { container, findByDisplayValue } = render(
    <Formik initialValues={initialValues} onSubmit={jest.fn()}>
      <Form>
        <FMIconButtonTextField
          name="voucherCode"
          label="Voucher Code *"
          callOutSpanText="Voucher Code must contain at least 5 characters and no more than 20 characters, and is allowed A to Z, 0 to 9, and _."
          placeholder="Only A-Z or 0-9 or _"
        />
      </Form>
    </Formik>
  );

  const input = container.querySelector('input[name="voucherCode"]');
  fireEvent.change(input, { target: { value: 'ABC123' } });

  // Wait for React's state update to propagate
  await findByDisplayValue('ABC123'); // This ensures the input has received and displays the new value

  expect(input.value).toBe('ABC123');
});

Despite these efforts, the test fails with the input value not updating:

expect(received).toBe(expected) // Object.is equality

Expected: "ABC123"
Received: ""

Solution

  • After encountering issues with the input's value not updating when simulating change events in Jest, I found that the missing link was the onChange handler in the custom TextField component that integrates Formik with the Office UI Fabric UI library. The original component did not include an explicit onChange handler to manage state updates which are essential for Formik's setValue function to properly propagate the new value.

    Here's the modified component with the onChange handler:

    import React, { useState, useCallback } from 'react';
    import { TextField, Label, Stack, IconButton, Callout } from '@fluentui/react';
    import { useField, useFormikContext } from 'formik';
    import { getId } from '@fluentui/react';
    
    const FMIconButtonTextField = props => {
      const { label, styles, name, callOutSpanText } = props;
      const [isCalloutVisible, setIsCalloutVisible] = useState(false);
      const [field, meta, helpers] = useField(name);
      const { setValue } = helpers;
    
      const onRenderLabel = useCallback(() => (
        <Stack horizontal verticalAlign="center">
          <Label id={getId('label')} styles={styles.label}>
            {label}
          </Label>
          <IconButton
            id={getId('iconButton')}
            iconProps={{ iconName: 'Info' }}
            title="Info"
            ariaLabel="Info"
            onClick={() => setIsCalloutVisible(true)}
          />
          {isCalloutVisible && (
            <Callout target={`#${getId('iconButton')}`} onDismiss={() => setIsCalloutVisible(false)}>
              <Stack tokens={{ childrenGap: 20, maxWidth: 300 }} styles={styles.callout}>
                <span>{callOutSpanText}</span>
              </Stack>
            </Callout>
          )}
        </Stack>
      ), [label, styles, callOutSpanText, isCalloutVisible]);
    
      return (
        <TextField
          {...field}
          {...props}
          label={label}
          onRenderLabel={onRenderLabel}
          errorMessage={meta.touched && meta.error ? meta.error : undefined}
          onChange={(_, newValue) => setValue(newValue || '')}
        />
      );
    };
    
    export default FMIconButtonTextField;
    

    Key Changes:

    • Added an onChange handler to the TextField component. This handler uses Formik's setValue to update the field's value, ensuring that changes in the input propagate back to Formik's state management.
    • Spread the field object before other props to ensure proper override if necessary.