Skip to content

Commit 0dc5dea

Browse files
authored
Continued tests (#55)
1 parent 593afd2 commit 0dc5dea

File tree

5 files changed

+339
-5
lines changed

5 files changed

+339
-5
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
| Statements | Branches | Functions | Lines |
66
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
7-
| ![Statements](https://img.shields.io/badge/statements-74.38%25-red.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-79.47%25-red.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-70.63%25-red.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-74.38%25-red.svg?style=flat) |
7+
| ![Statements](https://img.shields.io/badge/statements-74.44%25-red.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-79.93%25-red.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-71.53%25-red.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-74.44%25-red.svg?style=flat) |
88

99
## Requirements
1010

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
import ShowHideTextAdornment from '~/core/components/ShowHideTextAdornment';
4+
import { fireEvent, render, screen } from '~/vitest/utils';
5+
6+
describe('ShowHideTextAdornment', () => {
7+
it('renders the Visibility icon when visible is true', () => {
8+
const mockChange = vi.fn();
9+
render(<ShowHideTextAdornment change={mockChange} visible={true} />);
10+
const button = screen.getByLabelText('toggle password visibility');
11+
// Check if the rendered icon contains "Visibility"
12+
expect(button.innerHTML).toContain('Visibility');
13+
});
14+
15+
it('renders the VisibilityOff icon when visible is false', () => {
16+
const mockChange = vi.fn();
17+
render(<ShowHideTextAdornment change={mockChange} visible={false} />);
18+
const button = screen.getByLabelText('toggle password visibility');
19+
expect(button.innerHTML).toContain('VisibilityOff');
20+
});
21+
22+
it('calls change callback on click', () => {
23+
const mockChange = vi.fn();
24+
render(<ShowHideTextAdornment change={mockChange} visible={false} />);
25+
const button = screen.getByLabelText('toggle password visibility');
26+
fireEvent.click(button);
27+
expect(mockChange).toHaveBeenCalled();
28+
});
29+
30+
it('calls change callback on mouse down', () => {
31+
const mockChange = vi.fn();
32+
render(<ShowHideTextAdornment change={mockChange} visible={true} />);
33+
const button = screen.getByLabelText('toggle password visibility');
34+
fireEvent.mouseDown(button);
35+
expect(mockChange).toHaveBeenCalled();
36+
});
37+
38+
it('spreads additional props to the InputAdornment component', () => {
39+
const mockChange = vi.fn();
40+
render(<ShowHideTextAdornment change={mockChange} data-testid="adornment" visible={true} />);
41+
const adornment = screen.getByTestId('adornment');
42+
expect(adornment).toBeInTheDocument();
43+
});
44+
});

app/src/features/authentication/public/__tests__/LoginView.test.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import userEvent from '@testing-library/user-event';
22
import { act } from 'react';
3-
import { Route, createRoutesFromChildren } from 'react-router';
3+
import { createRoutesFromChildren, Route } from 'react-router';
44
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
55

66
import useWindowSize from '~/core/hooks/useWindowSize';
@@ -15,7 +15,16 @@ vi.mock('~/core/components/G-splash', () => ({
1515
default: () => <div data-testid="g-splash" />,
1616
}));
1717

18-
describe.skip('LoginView', () => {
18+
vi.mock('react-router', async () => {
19+
const router = await vi.importActual<typeof import('react-router')>('react-router');
20+
return {
21+
...router,
22+
useLocation: vi.fn().mockReturnValue(vi.fn()),
23+
useNavigate: vi.fn().mockReturnValue(vi.fn()),
24+
};
25+
});
26+
27+
describe('LoginView', () => {
1928
beforeEach(() => {
2029
vi.mocked(useWindowSize).mockReturnValue({
2130
isDesktop: true,
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { act } from 'react';
2+
import Result, { isOk, ok } from 'true-myth/result';
3+
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
import type { ReactHookForm } from '~/core/hooks/useSubmit';
6+
import useForgotPasswordForm from '~/features/authentication/public/hooks/useForgotPasswordForm';
7+
import { ForgotPasswordSchema } from '~/features/schemas/ForgoPassword';
8+
import { renderHook } from '~/vitest/utils';
9+
10+
// Capture submit callbacks passed to useSubmit
11+
let capturedSubmitOptions: null | {
12+
onInvalidSubmit: (form: ReactHookForm<any>) => string;
13+
onValidSubmit: (data: any) => Promise<any>;
14+
} = null;
15+
16+
// Mock useSubmit to capture the callbacks and return onValidSubmit as onSubmit
17+
vi.mock('~/core/hooks/useSubmit', () => ({
18+
__esModule: true,
19+
default: (options: { form: any; onInvalidSubmit: any; onValidSubmit: any }) => {
20+
capturedSubmitOptions = options;
21+
return options.onValidSubmit;
22+
},
23+
}));
24+
25+
// Mock useNavigate from react-router
26+
const mockNavigate = vi.fn();
27+
vi.mock('react-router', () => ({
28+
useNavigate: () => mockNavigate,
29+
}));
30+
31+
// Mock useToast
32+
const mockToast = {
33+
error: vi.fn(),
34+
};
35+
vi.mock('~/core/hooks/useToast', () => ({
36+
default: () => ({ toast: mockToast }),
37+
}));
38+
39+
// Mock useVerifyEmail
40+
const mockVerifyEmail = vi.fn();
41+
vi.mock('~/features/authentication/public/hooks/useVerifyEmail', () => ({
42+
__esModule: true,
43+
default: () => ({
44+
verifyEmail: mockVerifyEmail,
45+
}),
46+
}));
47+
48+
describe('useForgotPasswordForm', () => {
49+
beforeEach(() => {
50+
mockNavigate.mockReset();
51+
mockToast.error.mockReset();
52+
capturedSubmitOptions = null;
53+
mockVerifyEmail.mockReset();
54+
});
55+
56+
it('should return expected properties', () => {
57+
const { result } = renderHook(() => useForgotPasswordForm());
58+
59+
expect(result.current).toHaveProperty('control');
60+
expect(result.current).toHaveProperty('errors');
61+
expect(result.current).toHaveProperty('onSubmit');
62+
expect(result.current).toHaveProperty('register');
63+
expect(result.current).toHaveProperty('schema', ForgotPasswordSchema);
64+
});
65+
66+
it('should navigate with success true when verifyEmail returns Ok', async () => {
67+
// Arrange: simulate verifyEmail returning an Ok result
68+
mockVerifyEmail.mockResolvedValue(ok(true));
69+
renderHook(() => useForgotPasswordForm());
70+
71+
const formData = { username: 'testuser' };
72+
const submissionResult = await act(async () => {
73+
return await capturedSubmitOptions!.onValidSubmit(formData);
74+
});
75+
76+
expect(mockVerifyEmail).toHaveBeenCalledWith('testuser');
77+
expect(mockNavigate).toHaveBeenCalledWith('/forgot-password?success=true');
78+
expect(isOk(submissionResult)).toBe(true);
79+
});
80+
81+
it('should navigate with success false when verifyEmail returns Err', async () => {
82+
// Arrange: simulate verifyEmail returning an Err result
83+
const errorResult = { _tag: 'Err', error: new Error('Invalid email') };
84+
mockVerifyEmail.mockResolvedValue(errorResult);
85+
renderHook(() => useForgotPasswordForm());
86+
87+
const formData = { username: 'testuser' };
88+
89+
const submissionResult = await act(async () => {
90+
return await capturedSubmitOptions!.onValidSubmit(formData);
91+
});
92+
expect(mockVerifyEmail).toHaveBeenCalledWith('testuser');
93+
expect(mockNavigate).toHaveBeenCalledWith('/forgot-password?success=false');
94+
expect(isOk(submissionResult)).toBe(true);
95+
});
96+
97+
it('should call toast.error on invalid submit and return error message', () => {
98+
// Arrange: create dummy form with an error on the username field
99+
const dummyForm = {
100+
formState: {
101+
errors: {
102+
username: { message: 'Username is required', type: 'required' },
103+
},
104+
},
105+
} as unknown as ReactHookForm<any>;
106+
107+
renderHook(() => useForgotPasswordForm());
108+
const errorMessage = capturedSubmitOptions?.onInvalidSubmit(dummyForm);
109+
110+
expect(mockToast.error).toHaveBeenCalledWith('Username is required');
111+
expect(errorMessage).toBe('Username is required');
112+
});
113+
});
Lines changed: 170 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,171 @@
1-
import { describe } from 'vitest';
1+
import { act } from 'react';
2+
import { isOk } from 'true-myth/result';
3+
import { beforeEach, describe, expect, it, vi } from 'vitest';
24

3-
describe.todo('useAuth', () => {});
5+
import type { ReactHookForm, UnknownMouseEvent } from '~/core/hooks/useSubmit';
6+
import useLoginForm from '~/features/authentication/public/hooks/useLoginForm';
7+
import { LoginSchema } from '~/features/schemas/Login';
8+
import { renderHook } from '~/vitest/utils';
9+
10+
// Variables to capture the callbacks passed to useSubmit
11+
let capturedSubmitOptions: null | {
12+
onInvalidSubmit: (form: ReactHookForm<any>) => string;
13+
onValidSubmit: (data: any) => Promise<any>;
14+
} = null;
15+
16+
// Mock useSubmit to simply capture the callbacks and return the onValidSubmit callback as onSubmit
17+
vi.mock('~/core/hooks/useSubmit', () => ({
18+
__esModule: true,
19+
default: (options: { onInvalidSubmit: any; onValidSubmit: any }) => {
20+
capturedSubmitOptions = options;
21+
return options.onValidSubmit;
22+
},
23+
}));
24+
25+
// Create a mock for useNavigate
26+
const mockNavigate = vi.fn();
27+
vi.mock('react-router', () => ({
28+
useNavigate: () => mockNavigate,
29+
}));
30+
31+
// Prepare a mock for useAuth to control the authentication.signIn behavior
32+
const mockSignIn = vi.fn();
33+
const mockAuth = {
34+
authenticate: {
35+
signIn: mockSignIn,
36+
},
37+
};
38+
39+
vi.mock('~/features/authentication/public/hooks/useAuth', () => ({
40+
__esModule: true,
41+
default: () => mockAuth,
42+
}));
43+
44+
// Import the hook under test
45+
46+
describe('useLoginForm', () => {
47+
const redirectUrl = '/dashboard';
48+
49+
beforeEach(() => {
50+
mockNavigate.mockReset();
51+
mockSignIn.mockReset();
52+
capturedSubmitOptions = null;
53+
});
54+
55+
it('should return expected properties', () => {
56+
const { result } = renderHook(() => useLoginForm(redirectUrl));
57+
58+
expect(result.current).toHaveProperty('control');
59+
expect(result.current).toHaveProperty('dirtyFields');
60+
expect(result.current).toHaveProperty('errors');
61+
expect(result.current).toHaveProperty('isDirty');
62+
expect(result.current).toHaveProperty('isSubmitSuccessful');
63+
expect(result.current).toHaveProperty('isSubmitted');
64+
expect(result.current).toHaveProperty('isSubmitting');
65+
expect(result.current).toHaveProperty('isValid');
66+
expect(result.current).toHaveProperty('onSubmit');
67+
expect(result.current).toHaveProperty('register');
68+
expect(result.current).toHaveProperty('schema', LoginSchema);
69+
});
70+
71+
it('should call navigate on valid submission', async () => {
72+
// Arrange: mock signIn to return an Ok result
73+
mockSignIn.mockResolvedValue({ _tag: 'Ok' });
74+
75+
const { result } = renderHook(() => useLoginForm(redirectUrl));
76+
77+
// Act: call the onSubmit callback (which is onValidSubmit from our mocked useSubmit)
78+
await act(async () => {
79+
const res = await result.current.onSubmit(new MouseEvent('click') as unknown as UnknownMouseEvent);
80+
81+
// And if signIn returns Ok, then navigate should be called
82+
if (isOk(res)) {
83+
expect(mockNavigate).toHaveBeenCalledWith(redirectUrl ?? '/');
84+
}
85+
});
86+
});
87+
88+
it('should return the error result when sign in fails', async () => {
89+
// Arrange: mock signIn to return an Err result
90+
const errorResult = { _tag: 'Err', error: new Error('Invalid credentials') };
91+
mockSignIn.mockResolvedValue(errorResult);
92+
93+
const { result } = renderHook(() => useLoginForm(redirectUrl));
94+
95+
// Act: call the onSubmit callback
96+
let returnedResult;
97+
await act(async () => {
98+
returnedResult = await result.current.onSubmit(new MouseEvent('click') as unknown as UnknownMouseEvent);
99+
});
100+
101+
expect(mockNavigate).not.toHaveBeenCalled();
102+
expect(returnedResult).toEqual(errorResult);
103+
});
104+
105+
describe('onInvalidSubmit (extracted from useSubmit)', () => {
106+
it('should return "Username and password are required" when both errors exist', () => {
107+
// Build a dummy ReactHookForm with errors on both fields
108+
const dummyForm = {
109+
formState: {
110+
errors: {
111+
password: { type: 'required' },
112+
username: { type: 'required' },
113+
},
114+
isSubmitted: true,
115+
isValid: false,
116+
},
117+
} as unknown as ReactHookForm<any>;
118+
119+
// Ensure that our captured callback exists by rendering the hook
120+
renderHook(() => useLoginForm(redirectUrl));
121+
const errorMessage = capturedSubmitOptions?.onInvalidSubmit(dummyForm);
122+
expect(errorMessage).toBe('Username and password are required');
123+
});
124+
125+
it('should return "Password is required" when only password has error', () => {
126+
const dummyForm = {
127+
formState: {
128+
errors: {
129+
password: { type: 'required' },
130+
},
131+
isSubmitted: true,
132+
isValid: false,
133+
},
134+
} as unknown as ReactHookForm<any>;
135+
136+
renderHook(() => useLoginForm(redirectUrl));
137+
const errorMessage = capturedSubmitOptions?.onInvalidSubmit(dummyForm);
138+
expect(errorMessage).toBe('Password is required');
139+
});
140+
141+
it('should return "Username is required" when only username has error', () => {
142+
const dummyForm = {
143+
formState: {
144+
errors: {
145+
username: { type: 'required' },
146+
},
147+
isSubmitted: true,
148+
isValid: false,
149+
},
150+
} as unknown as ReactHookForm<any>;
151+
152+
renderHook(() => useLoginForm(redirectUrl));
153+
const errorMessage = capturedSubmitOptions?.onInvalidSubmit(dummyForm);
154+
expect(errorMessage).toBe('Username is required');
155+
});
156+
157+
it('should return "Invalid username or password" when form is submitted and valid', () => {
158+
const dummyForm = {
159+
formState: {
160+
errors: {},
161+
isSubmitted: true,
162+
isValid: true,
163+
},
164+
} as unknown as ReactHookForm<any>;
165+
166+
renderHook(() => useLoginForm(redirectUrl));
167+
const errorMessage = capturedSubmitOptions?.onInvalidSubmit(dummyForm);
168+
expect(errorMessage).toBe('Invalid username or password');
169+
});
170+
});
171+
});

0 commit comments

Comments
 (0)