Skip to content

Commit d760d8a

Browse files
committed
Add test cases for sign up and login pages
1 parent 4f8e7ea commit d760d8a

File tree

7 files changed

+327
-10
lines changed

7 files changed

+327
-10
lines changed

frontend/__mocks__/svgMock.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const ReactComponent = () => null;
2+
3+
export default ReactComponent;

frontend/jest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const config: Config = {
9292
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
9393
moduleNameMapper: {
9494
'\\.(css)$': '<rootDir>/__mocks__/styleMock.ts',
95-
'\\.(svg)$': '<rootDir>/__mocks__/styleMock.ts',
95+
'\\.(svg\\?react)$': '<rootDir>/__mocks__/svgMock.ts',
9696
},
9797

9898
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader

frontend/src/components/CustomTextField/index.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { Visibility, VisibilityOff } from "@mui/icons-material";
2-
import { IconButton, InputAdornment, TextField, TextFieldPropsSizeOverrides, TextFieldVariants, Tooltip } from "@mui/material";
3-
import { OverridableStringUnion } from '@mui/types';
2+
import {
3+
IconButton,
4+
InputAdornment,
5+
TextField,
6+
TextFieldPropsSizeOverrides,
7+
TextFieldVariants,
8+
Tooltip,
9+
} from "@mui/material";
10+
import { OverridableStringUnion } from "@mui/types";
411
import { useState } from "react";
512

613
const passwordRequirements = (
@@ -16,8 +23,11 @@ const passwordRequirements = (
1623
// Adapted from https://muhimasri.com/blogs/mui-validation/
1724
type CustomTextFieldProps = {
1825
label: string;
19-
variant?: TextFieldVariants;
20-
size?: OverridableStringUnion<"small" | "medium", TextFieldPropsSizeOverrides>;
26+
variant?: TextFieldVariants;
27+
size?: OverridableStringUnion<
28+
"small" | "medium",
29+
TextFieldPropsSizeOverrides
30+
>;
2131
required?: boolean;
2232
emptyField?: boolean;
2333
validator?: (value: string) => string;
@@ -40,7 +50,7 @@ const CustomTextField: React.FC<CustomTextFieldProps> = ({
4050

4151
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
4252
const input = event.target.value;
43-
53+
4454
let errorMessage = "";
4555
if (validator) {
4656
errorMessage = validator(input);
@@ -51,7 +61,11 @@ const CustomTextField: React.FC<CustomTextFieldProps> = ({
5161
};
5262

5363
return (
54-
<Tooltip title={isPasswordField && passwordRequirements} placement="right" arrow>
64+
<Tooltip
65+
title={isPasswordField && passwordRequirements}
66+
placement="right"
67+
arrow
68+
>
5569
<TextField
5670
label={label}
5771
variant={variant}
@@ -63,15 +77,25 @@ const CustomTextField: React.FC<CustomTextFieldProps> = ({
6377
helperText={error}
6478
type={showPassword ? "text" : "password"}
6579
slotProps={{
80+
htmlInput: {
81+
"data-testid": label
82+
},
6683
input: {
6784
endAdornment: isPasswordField && (
6885
<InputAdornment position="end">
69-
<IconButton onClick={() => setShowPassword(!showPassword)} edge="end">
70-
{showPassword ? <VisibilityOff /> : <Visibility />}
86+
<IconButton
87+
onClick={() => setShowPassword(!showPassword)}
88+
edge="end"
89+
>
90+
{showPassword ? (
91+
<VisibilityOff sx={(theme) => ({ fontSize: theme.spacing(2.5) })} />
92+
) : (
93+
<Visibility sx={(theme) => ({ fontSize: theme.spacing(2.5) })} />
94+
)}
7195
</IconButton>
7296
</InputAdornment>
7397
),
74-
}
98+
},
7599
}}
76100
/>
77101
</Tooltip>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2+
import "@testing-library/jest-dom";
3+
import * as hooks from "../../contexts/AuthContext";
4+
import LogIn from ".";
5+
import { userClient } from "../../utils/api";
6+
7+
/* Mock useNavigate */
8+
const mockUseNavigate = jest.fn();
9+
jest.mock("react-router-dom", () => ({
10+
...jest.requireActual("react-router-dom"),
11+
useNavigate: () => mockUseNavigate,
12+
}));
13+
14+
/* Mock userClient APIs */
15+
jest.mock("../../utils/api", () => ({
16+
userClient: {
17+
post: jest.fn(),
18+
},
19+
}));
20+
const mockedPost = userClient.post as jest.MockedFunction<typeof userClient.post>;
21+
22+
describe("Log In Components", () => {
23+
beforeEach(() => {
24+
jest.spyOn(hooks, "useAuth").mockImplementation(() => ({
25+
signup: jest.fn(),
26+
login: jest.fn(),
27+
logout: jest.fn(),
28+
user: null,
29+
}));
30+
});
31+
32+
it("App name is rendered", () => {
33+
render(<LogIn />);
34+
expect(screen.getByText("PeerPrep")).toBeInTheDocument();
35+
});
36+
37+
it("Email field is rendered", () => {
38+
render(<LogIn />);
39+
expect(screen.getByTestId("Email")).toBeInTheDocument();
40+
});
41+
42+
it("Password field is rendered", () => {
43+
render(<LogIn />);
44+
expect(screen.getByTestId("Password")).toBeInTheDocument();
45+
});
46+
47+
it("Log in button is rendered", () => {
48+
render(<LogIn />);
49+
expect(screen.getByRole("button", { name: "Log in" })).toBeInTheDocument();
50+
});
51+
52+
it("Prompt to sign up is rendered", () => {
53+
render(<LogIn />);
54+
const signUpButton = screen.getByRole("button", { name: "Sign up" });
55+
expect(signUpButton).toBeInTheDocument();
56+
57+
fireEvent.click(signUpButton);
58+
expect(mockUseNavigate).toHaveBeenCalledWith("/signup");
59+
});
60+
});
61+
62+
describe("Log In Events", () => {
63+
// valid inputs
64+
const email = "[email protected]";
65+
const password = "Password@123";
66+
67+
beforeEach(() => {
68+
jest.spyOn(hooks, "useAuth").mockImplementation(() => ({
69+
signup: jest.fn(),
70+
login: (email, password) => {
71+
return mockedPost("/auth/login", {
72+
email,
73+
password,
74+
});
75+
},
76+
logout: jest.fn(),
77+
user: null,
78+
}));
79+
});
80+
81+
it("Successful log in with valid inputs", async () => {
82+
mockedPost.mockResolvedValue({});
83+
84+
render(<LogIn />);
85+
86+
fireEvent.change(screen.getByTestId("Email"), { target: { value: email } });
87+
fireEvent.change(screen.getByTestId("Password"), { target: { value: password } });
88+
fireEvent.click(screen.getByRole("button", { name: "Log in" }));
89+
90+
await waitFor(() => {
91+
expect(mockedPost).toHaveBeenCalledWith("/auth/login", {
92+
email: email,
93+
password: password,
94+
});
95+
});
96+
});
97+
98+
it("Unsuccessful log in with invalid email", async () => {
99+
const invalidEmail = "invalidEmail";
100+
101+
render(<LogIn />);
102+
103+
fireEvent.change(screen.getByTestId("Email"), { target: { value: invalidEmail } });
104+
fireEvent.change(screen.getByTestId("Password"), { target: { value: password } });
105+
fireEvent.click(screen.getByRole("button", { name: "Log in" }));
106+
107+
await waitFor(() => {
108+
expect(mockedPost).not.toHaveBeenCalled();
109+
});
110+
});
111+
});

frontend/src/pages/LogIn/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ const LogIn: React.FC = () => {
118118
</Typography>
119119
<Typography
120120
component="span"
121+
role="button"
122+
tabIndex={0}
121123
sx={{
122124
fontSize: 14,
123125
cursor: "pointer",
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2+
import "@testing-library/jest-dom";
3+
import * as hooks from "../../contexts/AuthContext";
4+
import SignUp from ".";
5+
import { userClient } from "../../utils/api";
6+
7+
/* Mock useNavigate */
8+
const mockUseNavigate = jest.fn();
9+
jest.mock("react-router-dom", () => ({
10+
...jest.requireActual("react-router-dom"),
11+
useNavigate: () => mockUseNavigate,
12+
}));
13+
14+
/* Mock userClient APIs */
15+
jest.mock("../../utils/api", () => ({
16+
userClient: {
17+
post: jest.fn(),
18+
},
19+
}));
20+
const mockedPost = userClient.post as jest.MockedFunction<typeof userClient.post>;
21+
22+
describe("Sign Up Components", () => {
23+
beforeEach(() => {
24+
jest.spyOn(hooks, "useAuth").mockImplementation(() => ({
25+
signup: jest.fn(),
26+
login: jest.fn(),
27+
logout: jest.fn(),
28+
user: null,
29+
}));
30+
});
31+
32+
it("App name is rendered", () => {
33+
render(<SignUp />);
34+
expect(screen.getByText("PeerPrep")).toBeInTheDocument();
35+
});
36+
37+
it("First name field is rendered", () => {
38+
render(<SignUp />);
39+
expect(screen.getByTestId("First Name")).toBeInTheDocument();
40+
});
41+
42+
it("Last name field is rendered", () => {
43+
render(<SignUp />);
44+
expect(screen.getByTestId("Last Name")).toBeInTheDocument();
45+
});
46+
47+
it("Username field is rendered", () => {
48+
render(<SignUp />);
49+
expect(screen.getByTestId("Username")).toBeInTheDocument();
50+
});
51+
52+
it("Email field is rendered", () => {
53+
render(<SignUp />);
54+
expect(screen.getByTestId("Email")).toBeInTheDocument();
55+
});
56+
57+
it("Password field is rendered", () => {
58+
render(<SignUp />);
59+
expect(screen.getByTestId("Password")).toBeInTheDocument();
60+
});
61+
62+
it("Sign up button is rendered", () => {
63+
render(<SignUp />);
64+
expect(screen.getByRole("button", { name: "Sign up" })).toBeInTheDocument();
65+
});
66+
67+
it("Prompt to log in is rendered", () => {
68+
render(<SignUp />);
69+
const logInButton = screen.getByRole("button", { name: "Log in" });
70+
expect(logInButton).toBeInTheDocument();
71+
72+
fireEvent.click(logInButton);
73+
expect(mockUseNavigate).toHaveBeenCalledWith("/login");
74+
});
75+
});
76+
77+
describe("Sign Up Events", () => {
78+
// valid inputs
79+
const firstName = "Test First Name";
80+
const lastName = "Test Last Name";
81+
const username = ".test_username";
82+
const email = "[email protected]";
83+
const password = "Password@123";
84+
85+
beforeEach(() => {
86+
jest.spyOn(hooks, "useAuth").mockImplementation(() => ({
87+
signup: (firstName, lastName, username, email, password) => {
88+
return mockedPost("/users", {
89+
firstName,
90+
lastName,
91+
username,
92+
email,
93+
password,
94+
});
95+
},
96+
login: jest.fn(),
97+
logout: jest.fn(),
98+
user: null,
99+
}));
100+
});
101+
102+
it("Successful sign up with valid inputs", async () => {
103+
mockedPost.mockResolvedValue({});
104+
105+
render(<SignUp />);
106+
107+
fireEvent.change(screen.getByTestId("First Name"), { target: { value: firstName } });
108+
fireEvent.change(screen.getByTestId("Last Name"), { target: { value: lastName } });
109+
fireEvent.change(screen.getByTestId("Username"), { target: { value: username } });
110+
fireEvent.change(screen.getByTestId("Email"), { target: { value: email } });
111+
fireEvent.change(screen.getByTestId("Password"), { target: { value: password } });
112+
fireEvent.click(screen.getByRole("button", { name: "Sign up" }));
113+
114+
await waitFor(() => {
115+
expect(mockedPost).toHaveBeenCalledWith("/users", {
116+
firstName: firstName,
117+
lastName: lastName,
118+
username: username,
119+
email: email,
120+
password: password,
121+
});
122+
});
123+
});
124+
125+
it("Unsuccessful sign up with invalid username", async () => {
126+
const invalidUsername = "test";
127+
128+
render(<SignUp />);
129+
130+
fireEvent.change(screen.getByTestId("First Name"), { target: { value: firstName } });
131+
fireEvent.change(screen.getByTestId("Last Name"), { target: { value: lastName } });
132+
fireEvent.change(screen.getByTestId("Username"), { target: { value: invalidUsername } });
133+
fireEvent.change(screen.getByTestId("Email"), { target: { value: email } });
134+
fireEvent.change(screen.getByTestId("Password"), { target: { value: password } });
135+
fireEvent.click(screen.getByRole("button", { name: "Sign up" }));
136+
137+
await waitFor(() => {
138+
expect(mockedPost).not.toHaveBeenCalled();
139+
});
140+
});
141+
142+
it("Unsuccessful sign up with invalid email", async () => {
143+
const invalidEmail = "invalidEmail";
144+
145+
render(<SignUp />);
146+
147+
fireEvent.change(screen.getByTestId("First Name"), { target: { value: firstName } });
148+
fireEvent.change(screen.getByTestId("Last Name"), { target: { value: lastName } });
149+
fireEvent.change(screen.getByTestId("Username"), { target: { value: username } });
150+
fireEvent.change(screen.getByTestId("Email"), { target: { value: invalidEmail } });
151+
fireEvent.change(screen.getByTestId("Password"), { target: { value: password } });
152+
fireEvent.click(screen.getByRole("button", { name: "Sign up" }));
153+
154+
await waitFor(() => {
155+
expect(mockedPost).not.toHaveBeenCalled();
156+
});
157+
});
158+
159+
it("Unsuccessful sign up with invalid password", async () => {
160+
const invalidPassword = "invalidPassword";
161+
162+
render(<SignUp />);
163+
164+
fireEvent.change(screen.getByTestId("First Name"), { target: { value: firstName } });
165+
fireEvent.change(screen.getByTestId("Last Name"), { target: { value: lastName } });
166+
fireEvent.change(screen.getByTestId("Username"), { target: { value: username } });
167+
fireEvent.change(screen.getByTestId("Email"), { target: { value: email } });
168+
fireEvent.change(screen.getByTestId("Password"), { target: { value: invalidPassword } });
169+
fireEvent.click(screen.getByRole("button", { name: "Sign up" }));
170+
171+
await waitFor(() => {
172+
expect(mockedPost).not.toHaveBeenCalled();
173+
});
174+
});
175+
});

frontend/src/pages/SignUp/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ const SignUp: React.FC = () => {
146146
</Typography>
147147
<Typography
148148
component="span"
149+
role="button"
150+
tabIndex={0}
149151
sx={{
150152
fontSize: 14,
151153
cursor: "pointer",

0 commit comments

Comments
 (0)