Formik: Formik doesn't seem to work with Enzyme's .simulate('change')

Created on 15 Feb 2019  ·  6Comments  ·  Source: formium/formik

🐛 Bug report

Current Behavior

If testing a component that uses Formik using Enzyme's .simulate('change' method, Formik warns there is no name (and receives undefined as the element?)

Expected behavior

The same code works in an actual browser. Not really sure that this is a Formik issue, it might be what Enzyme passes as the event that isn't correct? 🤷‍♂️

Reproducible example

I think this is as simple a reproduction case as I can make.

import React from 'react';
import {mount} from 'enzyme';
import {Formik} from 'formik';

describe('Sign In View', () => {  it('works?', () => {
    const component = mount(
      <Formik initialValues={{email: ''}}>
        {({handleChange}) => (
          <form noValidate>
            <input id="email" type="email" name="email" onChange={handleChange} />
          </form>
        )}
      </Formik>,
    );

    component
      .find('#email')
      .first()
      .simulate('change', {target: {value: ''}});

    expect(component.find('input')).toHaveLength(1);
  });
});

The test itself passes because I'm not really testing anything interesting yet, but during the test I get:

    console.warn node_modules/formik/dist/formik.cjs.development.js:543
      Warning: Formik called `handleChange`, but you forgot to pass an `id` or `name` attribute to your input:

          undefined

          Formik cannot determine which value to update. For more info see https://github.com/jaredpalmer/formik#handlechange-e-reactchangeeventany--void

Suggested solution(s)

If I log what is passed to handleChange it seems to be:

    SyntheticEvent {
      dispatchConfig:
       { phasedRegistrationNames: { bubbled: 'onChange', captured: 'onChangeCapture' },
         dependencies:
          [ 'blur',
            'change',
            'click',
            'focus',
            'input',
            'keydown',
            'keyup',
            'selectionchange' ] },
      _targetInst:
       FiberNode {
         tag: 5,
         key: null,
         elementType: 'input',
         type: 'input',
         stateNode:
          HTMLInputElement {
            '__reactInternalInstance$j3zzgmq2kne': [Circular],
            '__reactEventHandlers$j3zzgmq2kne': [Object],
            _wrapperState: [Object],
            value: [Getter/Setter],
            _valueTracker: [Object] },
         return:
          FiberNode {
            tag: 5,
            key: null,
            elementType: 'form',
            type: 'form',
            stateNode: [Object],
            return: [Object],
            child: [Circular],
            sibling: null,
            index: 0,
            ref: null,
            pendingProps: [Object],
            memoizedProps: [Object],
            updateQueue: null,
            memoizedState: null,
            firstContextDependency: null,
            mode: 0,
            effectTag: 0,
            nextEffect: null,
            firstEffect: null,
            lastEffect: null,
            expirationTime: 0,
            childExpirationTime: 0,
            alternate: null,
            actualDuration: 0,
            actualStartTime: -1,
            selfBaseDuration: 0,
            treeBaseDuration: 0,
            _debugID: 55,
            _debugSource: null,
            _debugOwner: [Object],
            _debugIsCurrentlyTiming: false },
         child: null,
         sibling: null,
         index: 0,
         ref: null,
         pendingProps:
          { id: 'email',
            type: 'email',
            name: 'email',
            onChange: [Function: logChange] },
         memoizedProps:
          { id: 'email',
            type: 'email',
            name: 'email',
            onChange: [Function: logChange] },
         updateQueue: null,
         memoizedState: null,
         firstContextDependency: null,
         mode: 0,
         effectTag: 0,
         nextEffect: null,
         firstEffect: null,
         lastEffect: null,
         expirationTime: 0,
         childExpirationTime: 0,
         alternate: null,
         actualDuration: 0,
         actualStartTime: -1,
         selfBaseDuration: 0,
         treeBaseDuration: 0,
         _debugID: 56,
         _debugSource:
          { fileName: '/Users/jvalore/Projects/business_continuity/management_ui/src/views/sign-in.spec.js',
            lineNumber: 73 },
         _debugOwner:
          FiberNode {
            tag: 1,
            key: null,
            elementType: [Object],
            type: [Object],
            stateNode: [Object],
            return: [Object],
            child: [Object],
            sibling: null,
            index: 0,
            ref: null,
            pendingProps: [Object],
            memoizedProps: [Object],
            updateQueue: null,
            memoizedState: [Object],
            firstContextDependency: null,
            mode: 0,
            effectTag: 5,
            nextEffect: [Object],
            firstEffect: null,
            lastEffect: null,
            expirationTime: 0,
            childExpirationTime: 0,
            alternate: null,
            actualDuration: 0,
            actualStartTime: -1,
            selfBaseDuration: 0,
            treeBaseDuration: 0,
            _debugID: 50,
            _debugSource: null,
            _debugOwner: [Object],
            _debugIsCurrentlyTiming: false },
         _debugIsCurrentlyTiming: false },
      nativeEvent:
       Event {
         target:
          HTMLInputElement {
            '__reactInternalInstance$j3zzgmq2kne': [Object],
            '__reactEventHandlers$j3zzgmq2kne': [Object],
            _wrapperState: [Object],
            value: [Getter/Setter],
            _valueTracker: [Object] },
         type: 'change' },
      type: 'change',
      target: { value: '' },
      currentTarget:
       HTMLInputElement {
         '__reactInternalInstance$j3zzgmq2kne':
          FiberNode {
            tag: 5,
            key: null,
            elementType: 'input',
            type: 'input',
            stateNode: [Circular],
            return: [Object],
            child: null,
            sibling: null,
            index: 0,
            ref: null,
            pendingProps: [Object],
            memoizedProps: [Object],
            updateQueue: null,
            memoizedState: null,
            firstContextDependency: null,
            mode: 0,
            effectTag: 0,
            nextEffect: null,
            firstEffect: null,
            lastEffect: null,
            expirationTime: 0,
            childExpirationTime: 0,
            alternate: null,
            actualDuration: 0,
            actualStartTime: -1,
            selfBaseDuration: 0,
            treeBaseDuration: 0,
            _debugID: 56,
            _debugSource: [Object],
            _debugOwner: [Object],
            _debugIsCurrentlyTiming: false },
         '__reactEventHandlers$j3zzgmq2kne':
          { id: 'email',
            type: 'email',
            name: 'email',
            onChange: [Function: logChange] },
         _wrapperState:
          { initialChecked: undefined,
            initialValue: '',
            controlled: false },
         value: [Getter/Setter],
         _valueTracker:
          { getValue: [Function: getValue],
            setValue: [Function: setValue],
            stopTracking: [Function: stopTracking] } },
      eventPhase: undefined,
      bubbles: undefined,
      cancelable: undefined,
      timeStamp: 1550263479021,
      defaultPrevented: undefined,
      isTrusted: undefined,
      isDefaultPrevented: [Function: functionThatReturnsFalse],
      isPropagationStopped: [Function: functionThatReturnsFalse],
      isPersistent: [Function: functionThatReturnsTrue],
      _dispatchListeners: [Function: logChange],
      _dispatchInstances:
       FiberNode {
         tag: 5,
         key: null,
         elementType: 'input',
         type: 'input',
         stateNode:
          HTMLInputElement {
            '__reactInternalInstance$j3zzgmq2kne': [Circular],
            '__reactEventHandlers$j3zzgmq2kne': [Object],
            _wrapperState: [Object],
            value: [Getter/Setter],
            _valueTracker: [Object] },
         return:
          FiberNode {
            tag: 5,
            key: null,
            elementType: 'form',
            type: 'form',
            stateNode: [Object],
            return: [Object],
            child: [Circular],
            sibling: null,
            index: 0,
            ref: null,
            pendingProps: [Object],
            memoizedProps: [Object],
            updateQueue: null,
            memoizedState: null,
            firstContextDependency: null,
            mode: 0,
            effectTag: 0,
            nextEffect: null,
            firstEffect: null,
            lastEffect: null,
            expirationTime: 0,
            childExpirationTime: 0,
            alternate: null,
            actualDuration: 0,
            actualStartTime: -1,
            selfBaseDuration: 0,
            treeBaseDuration: 0,
            _debugID: 55,
            _debugSource: null,
            _debugOwner: [Object],
            _debugIsCurrentlyTiming: false },
         child: null,
         sibling: null,
         index: 0,
         ref: null,
         pendingProps:
          { id: 'email',
            type: 'email',
            name: 'email',
            onChange: [Function: logChange] },
         memoizedProps:
          { id: 'email',
            type: 'email',
            name: 'email',
            onChange: [Function: logChange] },
         updateQueue: null,
         memoizedState: null,
         firstContextDependency: null,
         mode: 0,
         effectTag: 0,
         nextEffect: null,
         firstEffect: null,
         lastEffect: null,
         expirationTime: 0,
         childExpirationTime: 0,
         alternate: null,
         actualDuration: 0,
         actualStartTime: -1,
         selfBaseDuration: 0,
         treeBaseDuration: 0,
          FiberNode { ...

.. rest of output omitted

Not sure if what enzyme's .simulate passes is different compared to what react would normally send.

Additional context

Your environment

Relevant dependencies:

"formik": "1.5.0",
"react": "16.8.0-alpha.1",
"react-dom": "16.8.0-alpha.1",
"enzyme": "3.8.0",
"enzyme-adapter-react-16": "1.8.0",
"jest": "23.6.0",
"jest-enzyme": "7.0.1",

| Software | Version(s) |
| ---------------- | ---------- |
| Formik | 1.5.0
| React | 16.8.0-alpha.1
| TypeScript | no
| Browser | n/a
| npm/Yarn | Yarn 1.13.0
| Operating System | OSX

Formik Testing Duplicate

Most helpful comment

I noticed that I can resolve this by doing:

        const input = component.find('#email').first();
        input.instance().value = '[email protected]';
        input.simulate('change');

So i'm guessing that Enzyme isn't merging the {target: ...} into the event correctly.


Edit: The above doesn't quite seem to work either. All validations now appear during the test, not just the 1 field that has been changed.

So if I have multiple input defined:

                <Form.Group controlId="email">
                  <Form.Label>
                    <I18n t="email" />
                  </Form.Label>
                  <Form.Control
                    type="email"
                    autoFocus={true}
                    name="email"
                    value={values.email}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    isInvalid={touched.email && !!errors.email}
                  />
                  <Form.Control.Feedback type="invalid">{errors.email}</Form.Control.Feedback>
                </Form.Group>

                <Form.Group controlId="password">
                  <Form.Label>
                    <I18n t="password" />
                  </Form.Label>
                  <Form.Control
                    type="password"
                    className="form-control"
                    name="password"
                    value={values.password}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    isInvalid={touched.password && !!errors.password}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.password}
                  </Form.Control.Feedback>
                </Form.Group>

if I run the code for real in the browser it works fine.

In my test if I

        const input = component.find('input[name="email"]').first();
        input.instance().value = '[email protected]';
        input.simulate('change');

then the "invalid" messages appear for both the email and password fields. 🤷‍♂️

All 6 comments

I noticed that I can resolve this by doing:

        const input = component.find('#email').first();
        input.instance().value = '[email protected]';
        input.simulate('change');

So i'm guessing that Enzyme isn't merging the {target: ...} into the event correctly.


Edit: The above doesn't quite seem to work either. All validations now appear during the test, not just the 1 field that has been changed.

So if I have multiple input defined:

                <Form.Group controlId="email">
                  <Form.Label>
                    <I18n t="email" />
                  </Form.Label>
                  <Form.Control
                    type="email"
                    autoFocus={true}
                    name="email"
                    value={values.email}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    isInvalid={touched.email && !!errors.email}
                  />
                  <Form.Control.Feedback type="invalid">{errors.email}</Form.Control.Feedback>
                </Form.Group>

                <Form.Group controlId="password">
                  <Form.Label>
                    <I18n t="password" />
                  </Form.Label>
                  <Form.Control
                    type="password"
                    className="form-control"
                    name="password"
                    value={values.password}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    isInvalid={touched.password && !!errors.password}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.password}
                  </Form.Control.Feedback>
                </Form.Group>

if I run the code for real in the browser it works fine.

In my test if I

        const input = component.find('input[name="email"]').first();
        input.instance().value = '[email protected]';
        input.simulate('change');

then the "invalid" messages appear for both the email and password fields. 🤷‍♂️

Duplicate of #937 #1262

This is because the enzyme's change event is synchronous and Formik's handlers are asynchronous causing pain and sadness for all of us.

Thanks for the reply. This is unfortunate indeed. I guess I'll close this issue then since it's a duplicate.

We have on response you can use asynchrone test with enzyme and it's done (thanks @jaredpalmer)

We have on response you can use asynchrone test with enzyme and it's done (thanks @jaredpalmer)

@thomasrobin94 how can I use asynchronous test with enzyme? can you please provide more details?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

pmonty picture pmonty  ·  3Comments

jaredpalmer picture jaredpalmer  ·  3Comments

Jungwoo-An picture Jungwoo-An  ·  3Comments

outaTiME picture outaTiME  ·  3Comments

dearcodes picture dearcodes  ·  3Comments