Home > Net >  Jest & React Testing Library:Type of Events dont match
Jest & React Testing Library:Type of Events dont match

Time:12-23

im trying some adventures in the testing area, especially i wanted to test some basic react components but i get stuck at the first step. This is a simplified form that i am using in my application.

The form is working. Im struggling with the test of the view atm.

What i want:

I would like to test the view without the container and they container with the view.

I wrote some basic test, the way i think they should look like e.g.

  • view: test if change gets called with the correct data
  • presenter: test after change gets called what the value of the input is <-this is working, thus not in the code but in the gist

What i expect to happen:

If i call fireEvent in the test, i would like he test to pass.

What is happening:

The first test is working(obviously), because the component gets initialized with empty values. onChange test in the container component is working aswell. The onChange test in my view is broken, the types of the events don't match.

How can i test this or achieve the correct type?

Code:

LoginView.ts (Presenter)

import { ChangeEvent, createElement, FunctionComponent } from "react";

export interface LoginViewProps {
    username: string;
    password: string;
    onChange: (event: ChangeEvent<HTMLInputElement>) => void;
    onSubmit: () => void;
}

export const LoginView: FunctionComponent<LoginViewProps> = (props: LoginViewProps) => {

    return createElement("div", {},
        createElement("label", {
            htmlFor: "username",
        },
            "Username:",
        ),
        createElement("input", {
            "data-testid": "username",
            type: "text",
            name: "username",
            id: "username",
            value: props.username,
            onChange: props.onChange,
        }),
        createElement("label", {
            htmlFor: "password",
        },
            "Password:",
        ),
        createElement("input", {
            "data-testid": "password",
            type: "password",
            name: "password",
            id: "password",
            value: props.password,
            onChange: props.onChange,
        }),
        createElement("button", {
            type: "button",
            "data-testid": "submit",
            onClick: props.onSubmit,
        },
            "Sign in",
        ),
    );
};

export default LoginView;

LoginView.test.ts - Test for the View

import "@testing-library/jest-dom";
import { createElement } from "react";
import { render, fireEvent, cleanup } from "@testing-library/react";
import LoginView, { LoginViewProps } from "./LoginView";

afterEach(cleanup);

describe("Login Presenter", () => {

    /**
     * This works, more coincidence than knowledge
     */
    it("should display div with blank values", async () => {
        const { findByTestId } = renderLoginForm();

        const username = await findByTestId("username");
        const password = await findByTestId("password");

        expect(username).toHaveValue("");
        expect(password).toHaveValue("");
    });

    /**
     * This is not working
     */
    it("should allow entering a username", async () => {
        const onChange = jest.fn();
        const { findByTestId } = renderLoginForm({
            onChange,
        });
        const username = await findByTestId("username");

        fireEvent.change(username, {
            target: {
                id: "username",
                value: "test",
            },
        });

        /**
         * This expect is wrong,
         * received: Object {...}
         * expected: SyntheticBaseEvent {...}
         */
        expect(onChange).toHaveBeenCalledWith({
            target: {
                id: "username",
                value: "test",
            },
        });
    });

    /**
     * This is not working
     */
    it("should allow entering a password", async () => {
        const onChange = jest.fn();
        const { findByTestId } = renderLoginForm({
            onChange,
        });
        const password = await findByTestId("password");

        fireEvent.change(password, {
            target: {
                id: "password",
                value: "test",
            },
        });

        /**
         * This expect is wrong,
         * received: Object {...}
         * expected: SyntheticBaseEvent {...}
         */
        expect(onChange).toHaveBeenCalledWith({
            target: {
                id: "password",
                value: "test",
            },
        });
    });

    it("should submit the form with username, password", async () => {
        /**
         * What to write here?
         *
         * How can i test the values that i provided
         */
    });
});


function renderLoginForm(props: Partial<LoginViewProps> = {}) {
    const defaultProps: LoginViewProps = {
        username: "",
        password: "",
        onChange() {
            return;
        },
        onSubmit() {
            return;
        },
    };
    return render(createElement(LoginView, {
        ...defaultProps,
        ...props,
    }));
}

Error:

> jest

 FAIL  src/react/LoginForm/LoginView.test.ts
  ● Login Presenter › should allow entering a username

    expect(jest.fn()).toHaveBeenCalledWith(...expected)

    - Expected
      Received

    - Object {
    -   "target": Object {
    -     "id": "username",
    -     "value": "test",
      SyntheticBaseEvent {
        "_reactName": "onChange",
        "_targetInst": null,
        "bubbles": true,
        "cancelable": false,
        "currentTarget": null,
        "defaultPrevented": false,
        "eventPhase": 3,
        "isDefaultPrevented": [Function functionThatReturnsFalse],
        "isPropagationStopped": [Function functionThatReturnsFalse],
        "isTrusted": false,
        "nativeEvent": Event {
          "isTrusted": false,
        },
        "target": <input
          data-testid="username"
          id="username"
          name="username"
          type="text"
          value=""
        />,
        "timeStamp": 1640165302072,
        "type": "change",
      },

    Number of calls: 1

      31 |              });
      32 |
    > 33 |              expect(onChange).toHaveBeenCalledWith({
         |                               ^
      34 |                      target: {
      35 |                              id: "username",
      36 |                              value: "test",

      at _callee2$ (src/react/LoginForm/LoginView.test.ts:33:20)
      at tryCatch (node_modules/regenerator-runtime/runtime.js:63:40)
      at Generator.invoke [as _invoke] (node_modules/regenerator-runtime/runtime.js:294:22)
      at Generator.next (node_modules/regenerator-runtime/runtime.js:119:21)
      at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
      at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:25:9)

  ● Login Presenter › should allow entering a password

    expect(jest.fn()).toHaveBeenCalledWith(...expected)

    - Expected
      Received

    - Object {
    -   "target": Object {
    -     "id": "password",
    -     "value": "test",
      SyntheticBaseEvent {
        "_reactName": "onChange",
        "_targetInst": null,
        "bubbles": true,
        "cancelable": false,
        "currentTarget": null,
        "defaultPrevented": false,
        "eventPhase": 3,
        "isDefaultPrevented": [Function functionThatReturnsFalse],
        "isPropagationStopped": [Function functionThatReturnsFalse],
        "isTrusted": false,
        "nativeEvent": Event {
          "isTrusted": false,
        },
        "target": <input
          data-testid="password"
          id="password"
          name="password"
          type="password"
          value=""
        />,
        "timeStamp": 1640165302102,
        "type": "change",
      },

    Number of calls: 1

      53 |              });
      54 |
    > 55 |              expect(onChange).toHaveBeenCalledWith({
         |                               ^
      56 |                      target: {
      57 |                              id: "password",
      58 |                              value: "test",

      at _callee3$ (src/react/LoginForm/LoginView.test.ts:55:20)
      at tryCatch (node_modules/regenerator-runtime/runtime.js:63:40)
      at Generator.invoke [as _invoke] (node_modules/regenerator-runtime/runtime.js:294:22)
      at Generator.next (node_modules/regenerator-runtime/runtime.js:119:21)
      at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
      at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:25:9)

 PASS  src/react/LoginForm/Login.test.ts

Test Suites: 1 failed, 1 passed, 2 total
Tests:       2 failed, 6 passed, 8 total
Snapshots:   0 total
Time:        4.035 s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

Gist:

https://gist.github.com/simann/9cbf01f28602d59ba988ef608df99bc0

Final remarks:

Besides the error above, i am still in need for some tests of the submit function, if anyone could provide me with some information i would be really grateful.

Any other hints or improvemets are welcome aswell.

EDIT

For clarification i will add the code of the container aswell

Login.ts

import { ChangeEvent, Component, createElement } from "react";
import LoginForm from "./LoginView";

interface LoginState {
    password: string;
    username: string;
}

export class Login extends Component<null, LoginState> {

    constructor(props: null) {
        super(props);
        this.state = {
            password: "",
            username: "",
        };

        this.onChange = this.onChange.bind(this);
        this.onSubmit = this.onSubmit.bind(this);
    }

    onChange(event: ChangeEvent<HTMLInputElement>) {
        const { id, value } = event.target;
        this.setState({
            ...this.state,
            [id]: value,
        });
    }


    onSubmit() {
        console.log(this.state.username, this.state.password);
        /**
         * Do a websocket request with the values
         */
    }

    render() {
        return createElement(LoginForm, {
            password: this.state.password,
            username: this.state.username,
            onChange: this.onChange,
            onSubmit: this.onSubmit,
        });
    }

}

export default Login;

Login.test.ts

import "@testing-library/jest-dom";
import { createElement } from "react";
import { render, fireEvent, cleanup } from "@testing-library/react";
import Login from "./Login";

afterEach(cleanup);

describe("Login Container", () => {
    it("should display a blank login form with blank values", async () => {
        const { findByTestId } = renderLogin();

        const username = await findByTestId("username");
        const password = await findByTestId("password");

        expect(username).toHaveValue("");
        expect(password).toHaveValue("");
    });

    it("should allow entering a username", async () => {
        const { findByTestId } = renderLogin();
        const username = await findByTestId("username");

        fireEvent.change(username, {
            target: {
                id: "username",
                value: "test",
            },
        });

        expect(username).toHaveValue("test");
    });

    it("should allow entering a password", async () => {
        const { findByTestId } = renderLogin();
        const password = await findByTestId("password");

        fireEvent.change(password, {
            target: {
                id: "password",
                value: "test",
            },
        });

        expect(password).toHaveValue("test");
    });

    it("should submit the form with username, password", async () => {
        /**
         * What to write here?
         *
         * How do i test the values that are in my state?
         */
    });
});


function renderLogin() {
    return render(createElement(Login));
}

CodePudding user response:

The tests are mainly failing because you're comparing two different objects.

The object passed by React to the event handler is an instance of SyntheticEvent which is a wrapper around the browser's native event, and is more complex than the object you're comparing it to.

Despite having a similar interface to the native event, the SyntheticEvent class is an implementation detail of React and you should avoid implementing your tests around its structure, especially for object matching tests.

A better approach is it to wrap your onChange prop inside another function which will call it passing only the event value.

...
createElement("input", {
    "data-testid": "username",
    type: "text",
    name: "username",
    id: "username",
    value: props.username,
    onChange: (event) => {
        props.onChange(event.target.value);
    },
})
...

Then, on your test you can write:

...
fireEvent.change(password, {
    target: {
        value: "test",
    },
});

expect(onChange).toHaveBeenCalledWith("test");
...

To test submitted values, use React useState to save the values of your input fields.

...
const [username, setUsername] = React.useState("");
const [password, setPassword] = React.useState("");
...
createElement("input", {
    "data-testid": "username",
    type: "text",
    name: "username",
    id: "username",
    value: props.username,
    onChange: (event) => {
        setUsername(event.target.value);
        props.onChange(event.target.value);
    },
}),
createElement("input", {
    "data-testid": "password",
    type: "password",
    name: "password",
    id: "password",
    value: props.password,
    onChange: () => {
        setPassword(event.target.value);
        props.onChange(event.target.value);
    },
}),
createElement("button", {
    type: "button",
    "data-testid": "submit",
    onClick: () => {
        props.onSubmit(username, password)
    },
},
    "Sign in",
)

Then in your test you ca

it("should submit the form with username, password", async () => {
    const onSubmit = jest.fn();
    const { findByTestId } = renderLoginForm({
        onSubmit,
    });

    const username = await findByTestId("username");
    const password = await findByTestId("password");
    const submit = await findByTestId("submit");

    fireEvent.change(username, { target: { value: "username" } });
    fireEvent.change(password, { target: { value: "password" } });
    fireEvent.click(submit);

    expect(onSubmit).toHaveBeenCalledWith("username", "password");
});

Aside from that, you should use JSX to create components instead of createElement.

  • Related