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
.