Skip to content

Commit 6f1c42b

Browse files
committed
added better targets for tests and implemented unit tests for login panel
1 parent a70d5eb commit 6f1c42b

File tree

2 files changed

+184
-10
lines changed

2 files changed

+184
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2+
import '@testing-library/jest-dom';
3+
import LoginPanel from './LoginPanel';
4+
import AuthService from '@/service/authService';
5+
import useAuth from '@/hooks/useAuth';
6+
import { AxiosError, InternalAxiosRequestConfig } from 'axios';
7+
8+
9+
jest.mock('@/service/authService');
10+
jest.mock('@/hooks/useAuth');
11+
jest.mock('../../config', () => ({
12+
API_DOMAIN: 'http://localhost:3000',
13+
}));
14+
15+
describe('LoginPanel Component', () => {
16+
beforeEach(() => {
17+
(useAuth as jest.Mock).mockReturnValue({
18+
saveToken: jest.fn(),
19+
});
20+
});
21+
22+
// login routes
23+
test('renders LoginPanel with login button', () => {
24+
render(<LoginPanel />);
25+
expect(screen.getByTestId('open-login-button')).toBeInTheDocument();
26+
});
27+
28+
test('opens dialog when clicking LOGIN button', () => {
29+
render(<LoginPanel />);
30+
fireEvent.click(screen.getByTestId('open-login-button'));
31+
expect(screen.getByText(/Please enter your credentials to continue/i)).toBeInTheDocument();
32+
});
33+
34+
test('renders login form with username and password fields', () => {
35+
render(<LoginPanel />);
36+
fireEvent.click(screen.getByTestId('open-login-button'));
37+
expect(screen.getByTestId('username-input')).toBeInTheDocument();
38+
expect(screen.getByTestId('password-input')).toBeInTheDocument();
39+
});
40+
41+
test('displays error message when no username or password is provided', async () => {
42+
render(<LoginPanel />);
43+
fireEvent.click(screen.getByTestId('open-login-button'));
44+
fireEvent.click(screen.getByTestId('submit-button'));
45+
46+
expect(screen.getByTestId('error-message')).toHaveTextContent('Please enter a username and password');
47+
});
48+
49+
test('logs in user successfully', async () => {
50+
(AuthService.login as jest.Mock).mockResolvedValue({ token: 'test-token' });
51+
render(<LoginPanel />);
52+
fireEvent.click(screen.getByTestId('open-login-button'));
53+
fireEvent.change(screen.getByTestId('username-input'), { target: { value: 'testuser' } });
54+
fireEvent.change(screen.getByTestId('password-input'), { target: { value: '6@:;£V64YmvC' } });
55+
fireEvent.click(screen.getByTestId('submit-button'));
56+
57+
await waitFor(() => {
58+
expect(AuthService.login).toHaveBeenCalledWith('testuser', '6@:;£V64YmvC');
59+
expect(useAuth().saveToken).toHaveBeenCalledWith('test-token');
60+
});
61+
});
62+
63+
test('displays error when login fails', async () => {
64+
const errorResponse = {
65+
data: { error: 'Invalid credentials' },
66+
};
67+
68+
const axiosError = new AxiosError(
69+
'Request failed with status code 401',
70+
'ERR_BAD_REQUEST',
71+
{} as InternalAxiosRequestConfig,
72+
null,
73+
{
74+
...errorResponse,
75+
status: 401,
76+
statusText: 'Unauthorized',
77+
headers: {},
78+
config: {} as InternalAxiosRequestConfig,
79+
}
80+
);
81+
82+
(AuthService.login as jest.Mock).mockRejectedValue(axiosError);
83+
84+
render(<LoginPanel />);
85+
fireEvent.click(screen.getByTestId('open-login-button'));
86+
fireEvent.change(screen.getByTestId('username-input'), { target: { value: 'wronguser' } });
87+
fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'wrongpass' } });
88+
fireEvent.click(screen.getByTestId('submit-button'));
89+
90+
await waitFor(() => {
91+
expect(screen.getByTestId('error-message')).toHaveTextContent('Invalid credentials');
92+
});
93+
});
94+
95+
// register routes
96+
test('toggles between login and register mode', () => {
97+
render(<LoginPanel />);
98+
fireEvent.click(screen.getByTestId('open-login-button'));
99+
100+
// initially in login mode
101+
expect(screen.getByTestId('submit-button')).toHaveTextContent('Login');
102+
103+
// toggle to register mode
104+
fireEvent.click(screen.getByTestId('toggle-register-button'));
105+
expect(screen.getByTestId('submit-button')).toHaveTextContent('Register');
106+
107+
// toggle back to login mode
108+
fireEvent.click(screen.getByTestId('toggle-register-button'));
109+
expect(screen.getByTestId('submit-button')).toHaveTextContent('Login');
110+
});
111+
112+
test('registers a new user successfully', async () => {
113+
(AuthService.register as jest.Mock).mockResolvedValue({ token: 'register-token' });
114+
render(<LoginPanel />);
115+
fireEvent.click(screen.getByTestId('open-login-button'));
116+
117+
// toggle to Register mode
118+
fireEvent.click(screen.getByTestId('toggle-register-button'));
119+
120+
fireEvent.change(screen.getByTestId('username-input'), { target: { value: 'newuser' } });
121+
fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'P@ssword1' } });
122+
fireEvent.click(screen.getByTestId('submit-button'));
123+
124+
await waitFor(() => {
125+
expect(AuthService.register).toHaveBeenCalledWith('newuser', 'P@ssword1');
126+
expect(useAuth().saveToken).toHaveBeenCalledWith('register-token');
127+
});
128+
});
129+
130+
test('displays error when registration fails', async () => {
131+
const errorResponse = {
132+
data: { error: 'Username already exists' },
133+
};
134+
135+
const axiosError = new AxiosError(
136+
'Request failed with status code 409',
137+
'ERR_CONFLICT',
138+
{} as InternalAxiosRequestConfig,
139+
null,
140+
{
141+
...errorResponse,
142+
status: 409,
143+
statusText: 'Conflict',
144+
headers: {},
145+
config: {} as InternalAxiosRequestConfig,
146+
}
147+
);
148+
149+
(AuthService.register as jest.Mock).mockRejectedValue(axiosError);
150+
151+
render(<LoginPanel />);
152+
fireEvent.click(screen.getByTestId('open-login-button'));
153+
154+
// toggle to Register mode
155+
fireEvent.click(screen.getByTestId('toggle-register-button'));
156+
157+
fireEvent.change(screen.getByTestId('username-input'), { target: { value: 'zcog' } });
158+
fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'P@ssword1' } });
159+
fireEvent.click(screen.getByTestId('submit-button'));
160+
161+
await waitFor(() => {
162+
expect(screen.getByTestId('error-message')).toHaveTextContent('Username already exists');
163+
});
164+
});
165+
});

src/components/LoginPanel/LoginPanel.tsx

+19-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useEffect, useState } from "react";
2+
import { AxiosError } from "axios";
23

34
import {
45
Dialog,
@@ -9,12 +10,11 @@ import {
910
DialogTitle,
1011
DialogTrigger,
1112
} from "@/components/ui/dialog"
12-
import AuthService from "@/service/authService";
1313
import { Input } from "../ui/input";
14-
import { AxiosError } from "axios";
14+
15+
import AuthService from "@/service/authService";
1516
import useAuth from "@/hooks/useAuth";
1617

17-
//todo: login vs register
1818
const LoginPanel = () => {
1919
const [username, setUsername] = useState("");
2020
const [password, setPassword] = useState("");
@@ -72,6 +72,8 @@ const LoginPanel = () => {
7272
<button
7373
className=""
7474
onClick={() => setOpen(true)}
75+
aria-label="Open login dialog"
76+
data-testid="open-login-button"
7577
>
7678
LOGIN
7779
</button>
@@ -88,17 +90,19 @@ const LoginPanel = () => {
8890
Please enter your credentials to continue
8991
</DialogDescription>
9092
</DialogHeader>
91-
<form className="flex flex-col" onSubmit={handleSubmit}>
93+
<form className="flex flex-col" onSubmit={handleSubmit} data-testid="login-form">
9294
<div className="p-2">
9395
<label htmlFor="username" className="sr-only">
9496
Username
9597
</label>
9698
<Input
9799
id="username"
98-
type="username"
100+
type="text"
99101
placeholder="username"
100102
value={username}
101103
onChange={(e) => setUsername(e.target.value)}
104+
aria-label="Username"
105+
data-testid="username-input"
102106
/>
103107
</div>
104108
<div className="p-2">
@@ -111,18 +115,22 @@ const LoginPanel = () => {
111115
placeholder="password"
112116
value={password}
113117
onChange={(e) => setPassword(e.target.value)}
118+
aria-label="Password"
119+
data-testid="password-input"
114120
/>
115121
</div>
116122
<div className="flex justify-end">
117123
<div className="flex flex-col items-end">
118124
{error && (
119-
<p className="text-right text-red-500">
125+
<p className="text-right text-red-500" data-testid="error-message">
120126
{error}
121127
</p>
122128
)}
123129
<button
124130
type="submit"
125131
className="p-2 mt-2 text-white transition-colors rounded-md bg-slate-500 hover:bg-slate-400"
132+
aria-label={isRegistering ? "Register" : "Login"}
133+
data-testid="submit-button"
126134
>
127135
{isRegistering ? "Register" : "Login"}
128136
</button>
@@ -133,14 +141,15 @@ const LoginPanel = () => {
133141
<button
134142
className="border-none text-muted-foreground bg-none test-sm"
135143
onClick={() => setIsRegistering(!isRegistering)}
144+
aria-label={isRegistering ? "Switch to Login" : "Switch to Register"}
145+
data-testid="toggle-register-button"
136146
>
137-
{isRegistering ? "Login" : "Register"}
147+
{isRegistering ? "Click here to Login instead" : "Click here to Register instead"}
138148
</button>
139149
</DialogFooter>
140150
</DialogContent>
141151
</Dialog>
142-
143-
)
144-
}
152+
);
153+
};
145154

146155
export default LoginPanel;

0 commit comments

Comments
 (0)