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
changegets called with the correct data - presenter: test after
changegets 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));
}