Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

userEvent.type only types last character or gives act warning when using Formik #539

Closed
Hyllesen opened this issue Jan 4, 2021 · 7 comments

Comments

@Hyllesen
Copy link

Hyllesen commented Jan 4, 2021

Either

  1. Act wrap missing warning when using userEvent.type with Formik, or
  2. Wrap userEvent with act, and have the tests failing due to only the last character being entered.

The issue has been mentioned here, but it seems to have been closed?

I'm able to reproduce the error after creating a new CRA project yesterday, but not on CodeSandbox?

App.js

import React from "react";
import { useFormik } from "formik";

export default function App() {
  const formik = useFormik({
    initialValues: {
      firstName: "",
    },
  });

  const { values, handleChange, handleSubmit } = formik;

  return (
    <div className="App">
      <form onSubmit={handleSubmit}>
        <input
          data-testid="firstName"
          value={values.firstName}
          onChange={handleChange}
          id="firstName"
          type="text"
        />
      </form>
    </div>
  );
}

App.test.js

import React from "react";
import App from "./App";
import { render, screen, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

describe("App", () => {
  test("Input text in formik form passes but with act warning", () => {
    render(<App />);

    const firstNameInput = screen.getByTestId("firstName");

    userEvent.type(firstNameInput, "MyFirstName");

    expect(firstNameInput.value).toBe("MyFirstName");
  });

  test("Input text in formik form doesn't pass, but without act warning", () => {
    render(<App />);

    const firstNameInput = screen.getByTestId("firstName");

    act(() => {
      userEvent.type(firstNameInput, "MyFirstName");
    });

    expect(firstNameInput.value).toBe("MyFirstName");
  });
});

What you did:
Created a new project using npx create-react-app, added formik

What happened:

 FAIL  src/App.test.js
  App
    ✓ Input text in formik form passes but with act warning (137 ms)
    ✕ Input text in formik form doesn't pass, but without act warning (45 ms)

  ● App › Input text in formik form doesn't pass, but without act warning

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

    Expected: "MyFirstName"
    Received: "e"

      24 |     });
      25 | 
    > 26 |     expect(firstNameInput.value).toBe("MyFirstName");
         |                                  ^
      27 |   });
      28 | });
      29 | 

      at Object.<anonymous> (src/App.test.js:26:34)

  console.error
    Warning: An update to App inside a test was not wrapped in act(...).
    
    When testing, code that causes React state updates should be wrapped into act(...):
    
    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */
    
    This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
        at App (/Users/stefan/formik-testing-library-bug/src/App.js:5:18)

      at printWarning (node_modules/react-dom/cjs/react-dom.development.js:67:30)
      at error (node_modules/react-dom/cjs/react-dom.development.js:43:5)
      at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-dom/cjs/react-dom.development.js:24064:9)
      at dispatch (node_modules/react-dom/cjs/react-dom.development.js:16135:9)
      at node_modules/formik/src/Formik.tsx:327:11  

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        1.382 s
Ran all test suites.

Reproduction repository:
https://github.com/Hyllesen/testing-library-formik-bug
I could not reproduce the error in codesandbox, https://codesandbox.io/s/youthful-river-0wxrz

@testing-library/user-event: v12.6.0
@testing-library/jest-dom: ^5.11.4
@testing-library/react: ^11.1.0
@testing-library/user-event: 12.6.0
formik": "^2.2.6
node@v14.15.1

@ph-fritsche
Copy link
Member

You could not reproduce the error in codesandbox because your codesandbox example does not contain the failing test.;)

This looks like an issue with Formik because this test does pass:

  test("Delayed change to unexpected value", () => {
    render(<App />);
    const firstNameInput = screen.getByTestId("firstName");

    userEvent.type(firstNameInput, "MyFirstName");

    expect(firstNameInput.value).toBe("MyFirstName");
    waitFor(() => expect(firstNameInput.value).toBe("e"));
  });

@Hyllesen
Copy link
Author

Hyllesen commented Jan 5, 2021

You could not reproduce the error in codesandbox because your codesandbox example does not contain the failing test.;)

This looks like an issue with Formik because this test does pass:

  test("Delayed change to unexpected value", () => {
    render(<App />);
    const firstNameInput = screen.getByTestId("firstName");

    userEvent.type(firstNameInput, "MyFirstName");

    expect(firstNameInput.value).toBe("MyFirstName");
    waitFor(() => expect(firstNameInput.value).toBe("e"));
  });

What makes think you this is a Formik issue? I see the test passing without act warnings, it's quite strange. I'm not sure how to pinpoint whether this is a userEvent issue or Formik issue. We could throw this back and forth forever, and it seems like a pretty basic issue.

@ph-fritsche
Copy link
Member

Sry, stupid mistake on my part - didn't await/return the Promise. The test does fail when written correctly:

return waitFor(() => expect(firstNameInput.value).toBe("e"));

This is the fix for your test:

  test("fixed", async () => {
    render(<App />);

    const firstNameInput = screen.getByTestId("firstName");

    await userEvent.type(firstNameInput, "MyFirstName", { delay: 1 });

    expect(firstNameInput.value).toBe("MyFirstName");
  });

Formik applies the state change asynchronously. That's why they happen out of the act-wrapped event handler.
If wrapped in extra act React defers the state changes, so userEvent.type picks up the unchanged value and applies the next character. So you only end up with "e".
delay solves both problems.

@Hyllesen
Copy link
Author

Hyllesen commented Jan 5, 2021

  test("fixed", async () => {
    render(<App />);

    const firstNameInput = screen.getByTestId("firstName");

    await userEvent.type(firstNameInput, "MyFirstName", { delay: 1 });

    expect(firstNameInput.value).toBe("MyFirstName");
  });

Formik applies the state change asynchronously. That's why they happen out of the act-wrapped event handler.
If wrapped in extra act React defers the state changes, so userEvent.type picks up the unchanged value and applies the next character. So you only end up with "e".
delay solves both problems.

Seems like a bit of a hacky and non-obvious solution, but I'm glad I can move on at least. Do you still believe this is a Formik issue and not a userEvent issue?

@ph-fritsche
Copy link
Member

ph-fritsche commented Jan 5, 2021

I thought so because when trying to reproduce the described behavior the test above succeeded although it should fail. And it does fail (without the slip of the pen)...

If there is a Formik issue or if this is the expected Formik behavior, I don't know.
I don't see any issue with userEvent here as userEvent can not figure out how your system applies changes. It can just work with what it can see. And that is the elements value property - which obviously does not change instantly when the input event is fired.
If Formik just pushes the change down the event loop by creating promises ({delay: 1} works) or if there are some debouncing functions or timeouts involved ({delay: 1} works just by luck and can break any moment), you should ask at Formik or examine the code there.

The whole idea to simulate browser behavior without using a browser is hacky...

@kelly-tock
Copy link

Sry, stupid mistake on my part - didn't await/return the Promise. The test does fail when written correctly:

return waitFor(() => expect(firstNameInput.value).toBe("e"));

This is the fix for your test:

  test("fixed", async () => {
    render(<App />);

    const firstNameInput = screen.getByTestId("firstName");

    await userEvent.type(firstNameInput, "MyFirstName", { delay: 1 });

    expect(firstNameInput.value).toBe("MyFirstName");
  });

Formik applies the state change asynchronously. That's why they happen out of the act-wrapped event handler.
If wrapped in extra act React defers the state changes, so userEvent.type picks up the unchanged value and applies the next character. So you only end up with "e".
delay solves both problems.

I needed to do

await userEvent.type

vs just userEvent.type

and the delay.

@cmacdonnacha
Copy link

FYI await userEvent.type worked for me without the delay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants