Skip to content

Commit a327287

Browse files
authored
Merge pull request #33 from ruiqi7/qa/test-cases
Write test cases for question list, sign up and log in
2 parents 8b69b75 + 729d6c9 commit a327287

File tree

13 files changed

+578
-60
lines changed

13 files changed

+578
-60
lines changed

backend/question-service/src/controllers/questionController.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Request, Response } from "express";
22
import Question, { IQuestion } from "../models/Question.ts";
3-
import { checkIsExistingQuestion } from "../utils/utils.ts";
3+
import { checkIsExistingQuestion, sortAlphabetically } from "../utils/utils.ts";
44
import {
55
DUPLICATE_QUESTION_MESSAGE,
66
QN_DESC_EXCEED_CHAR_LIMIT_MESSAGE,
@@ -212,7 +212,12 @@ export const readQuestionsList = async (
212212
res.status(200).json({
213213
message: QN_RETRIEVED_MESSAGE,
214214
questionCount: filteredQuestionCount,
215-
questions: filteredQuestions.map(formatQuestionResponse),
215+
questions: filteredQuestions
216+
.map(formatQuestionResponse)
217+
.map((question) => ({
218+
...question,
219+
categories: sortAlphabetically(question.categories),
220+
})),
216221
});
217222
} catch (error) {
218223
res.status(500).json({ message: SERVER_ERROR_MESSAGE, error });
@@ -250,7 +255,7 @@ export const readCategories = async (
250255

251256
res.status(200).json({
252257
message: CATEGORIES_RETRIEVED_MESSAGE,
253-
categories: uniqueCats,
258+
categories: sortAlphabetically(uniqueCats),
254259
});
255260
} catch (error) {
256261
res.status(500).json({ message: SERVER_ERROR_MESSAGE, error });

backend/question-service/src/utils/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ export const uploadFileToFirebase = async (
4848
blobStream.end(file.buffer);
4949
});
5050
};
51+
52+
export const sortAlphabetically = (arr: string[]) => {
53+
return [...arr].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
54+
};

backend/question-service/swagger.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ paths:
327327
tags:
328328
- questions
329329
summary: Returns question categories
330-
description: Returns list of unique question categories
330+
description: Returns list of unique question categories sorted alphabetically
331331
responses:
332332
200:
333333
description: Successful Response

backend/user-service/package-lock.json

Lines changed: 51 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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
// "@uiw/react-md-editor":
9797
// "<rootDir>/node_modules/@uiw/react-md-editor/esm/index",
9898
},

frontend/src/components/CustomTextField/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,21 @@ const CustomTextField: React.FC<CustomTextFieldProps> = ({
7777
helperText={error}
7878
type={showPassword ? "text" : "password"}
7979
slotProps={{
80+
htmlInput: {
81+
"data-testid": label
82+
},
8083
input: {
8184
endAdornment: isPasswordField && (
8285
<InputAdornment position="end">
8386
<IconButton
8487
onClick={() => setShowPassword(!showPassword)}
8588
edge="end"
8689
>
87-
{showPassword ? <VisibilityOff /> : <Visibility />}
90+
{showPassword ? (
91+
<VisibilityOff sx={(theme) => ({ fontSize: theme.spacing(2.5) })} />
92+
) : (
93+
<Visibility sx={(theme) => ({ fontSize: theme.spacing(2.5) })} />
94+
)}
8895
</IconButton>
8996
</InputAdornment>
9097
),
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
@@ -123,6 +123,8 @@ const LogIn: React.FC = () => {
123123
</Typography>
124124
<Typography
125125
component="span"
126+
role="button"
127+
tabIndex={0}
126128
sx={{
127129
fontSize: 14,
128130
cursor: "pointer",

0 commit comments

Comments
 (0)