mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Add tests for useProfileSettings hook functionality
This commit is contained in:
517
app/src/hooks/useProfileSettings.test.ts
Normal file
517
app/src/hooks/useProfileSettings.test.ts
Normal file
@@ -0,0 +1,517 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useProfileSettings } from './useProfileSettings';
|
||||
import * as userApi from '@/api/user';
|
||||
import type { UpdateProfileRequest } from '@/types/api';
|
||||
import { UserRole, type User } from '@/types/models';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/api/user');
|
||||
vi.mock('@mantine/notifications', () => ({
|
||||
notifications: {
|
||||
show: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Import notifications for assertions
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
// Mock user data
|
||||
const mockUser: User = {
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
role: UserRole.Editor,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
|
||||
describe('useProfileSettings', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('returns correct initial loading state', () => {
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(typeof result.current.updateProfile).toBe('function');
|
||||
expect(typeof result.current.deleteAccount).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateProfile', () => {
|
||||
it('updates profile successfully', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
const updatedUser: User = {
|
||||
...mockUser,
|
||||
displayName: 'Updated Name',
|
||||
};
|
||||
mockUpdateProfile.mockResolvedValue(updatedUser);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
const updateRequest: UpdateProfileRequest = {
|
||||
displayName: 'Updated Name',
|
||||
};
|
||||
|
||||
let returnedUser: User | null = null;
|
||||
await act(async () => {
|
||||
returnedUser = await result.current.updateProfile(updateRequest);
|
||||
});
|
||||
|
||||
expect(returnedUser).toEqual(updatedUser);
|
||||
expect(mockUpdateProfile).toHaveBeenCalledWith(updateRequest);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Success',
|
||||
message: 'Profile updated successfully',
|
||||
color: 'green',
|
||||
});
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('updates email successfully', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
const updatedUser: User = {
|
||||
...mockUser,
|
||||
email: 'newemail@example.com',
|
||||
};
|
||||
mockUpdateProfile.mockResolvedValue(updatedUser);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
const updateRequest: UpdateProfileRequest = {
|
||||
email: 'newemail@example.com',
|
||||
currentPassword: 'current123',
|
||||
};
|
||||
|
||||
let returnedUser: User | null = null;
|
||||
await act(async () => {
|
||||
returnedUser = await result.current.updateProfile(updateRequest);
|
||||
});
|
||||
|
||||
expect(returnedUser).toEqual(updatedUser);
|
||||
expect(mockUpdateProfile).toHaveBeenCalledWith(updateRequest);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Success',
|
||||
message: 'Profile updated successfully',
|
||||
color: 'green',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates password successfully', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
mockUpdateProfile.mockResolvedValue(mockUser);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
const updateRequest: UpdateProfileRequest = {
|
||||
currentPassword: 'oldpass123',
|
||||
newPassword: 'newpass456',
|
||||
};
|
||||
|
||||
let returnedUser: User | null = null;
|
||||
await act(async () => {
|
||||
returnedUser = await result.current.updateProfile(updateRequest);
|
||||
});
|
||||
|
||||
expect(returnedUser).toEqual(mockUser);
|
||||
expect(mockUpdateProfile).toHaveBeenCalledWith(updateRequest);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Success',
|
||||
message: 'Profile updated successfully',
|
||||
color: 'green',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates multiple fields successfully', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
const updatedUser: User = {
|
||||
...mockUser,
|
||||
displayName: 'New Display Name',
|
||||
email: 'updated@example.com',
|
||||
};
|
||||
mockUpdateProfile.mockResolvedValue(updatedUser);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
const updateRequest: UpdateProfileRequest = {
|
||||
displayName: 'New Display Name',
|
||||
email: 'updated@example.com',
|
||||
currentPassword: 'current123',
|
||||
};
|
||||
|
||||
let returnedUser: User | null = null;
|
||||
await act(async () => {
|
||||
returnedUser = await result.current.updateProfile(updateRequest);
|
||||
});
|
||||
|
||||
expect(returnedUser).toEqual(updatedUser);
|
||||
expect(mockUpdateProfile).toHaveBeenCalledWith(updateRequest);
|
||||
});
|
||||
|
||||
it('shows loading state during update', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
let resolveUpdate: (value: User) => void;
|
||||
const updatePromise = new Promise<User>((resolve) => {
|
||||
resolveUpdate = resolve;
|
||||
});
|
||||
mockUpdateProfile.mockReturnValue(updatePromise);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
// Start update
|
||||
act(() => {
|
||||
void result.current.updateProfile({ displayName: 'Test' });
|
||||
});
|
||||
|
||||
// Should be loading
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
// Resolve the promise
|
||||
await act(async () => {
|
||||
if (resolveUpdate) resolveUpdate(mockUser);
|
||||
await updatePromise;
|
||||
});
|
||||
|
||||
// Should no longer be loading
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('handles password errors specifically', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
mockUpdateProfile.mockRejectedValue(
|
||||
new Error('Current password is incorrect')
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
const updateRequest: UpdateProfileRequest = {
|
||||
currentPassword: 'wrongpass',
|
||||
newPassword: 'newpass123',
|
||||
};
|
||||
|
||||
let returnedUser: User | null = null;
|
||||
await act(async () => {
|
||||
returnedUser = await result.current.updateProfile(updateRequest);
|
||||
});
|
||||
|
||||
expect(returnedUser).toBeNull();
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Current password is incorrect',
|
||||
color: 'red',
|
||||
});
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('handles email errors specifically', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
mockUpdateProfile.mockRejectedValue(new Error('Email already exists'));
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
const updateRequest: UpdateProfileRequest = {
|
||||
email: 'existing@example.com',
|
||||
};
|
||||
|
||||
let returnedUser: User | null = null;
|
||||
await act(async () => {
|
||||
returnedUser = await result.current.updateProfile(updateRequest);
|
||||
});
|
||||
|
||||
expect(returnedUser).toBeNull();
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Email is already in use',
|
||||
color: 'red',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles generic update errors', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
mockUpdateProfile.mockRejectedValue(new Error('Server error'));
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
const updateRequest: UpdateProfileRequest = {
|
||||
displayName: 'Test Name',
|
||||
};
|
||||
|
||||
let returnedUser: User | null = null;
|
||||
await act(async () => {
|
||||
returnedUser = await result.current.updateProfile(updateRequest);
|
||||
});
|
||||
|
||||
expect(returnedUser).toBeNull();
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Failed to update profile',
|
||||
color: 'red',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-Error rejection', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
mockUpdateProfile.mockRejectedValue('String error');
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
let returnedUser: User | null = null;
|
||||
await act(async () => {
|
||||
returnedUser = await result.current.updateProfile({
|
||||
displayName: 'Test',
|
||||
});
|
||||
});
|
||||
|
||||
expect(returnedUser).toBeNull();
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Failed to update profile',
|
||||
color: 'red',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAccount', () => {
|
||||
it('deletes account successfully', async () => {
|
||||
const mockDeleteUser = vi.mocked(userApi.deleteUser);
|
||||
mockDeleteUser.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
let deleteResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
deleteResult = await result.current.deleteAccount('password123');
|
||||
});
|
||||
|
||||
expect(deleteResult).toBe(true);
|
||||
expect(mockDeleteUser).toHaveBeenCalledWith('password123');
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Success',
|
||||
message: 'Account deleted successfully',
|
||||
color: 'green',
|
||||
});
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('shows loading state during deletion', async () => {
|
||||
const mockDeleteUser = vi.mocked(userApi.deleteUser);
|
||||
let resolveDelete: () => void;
|
||||
const deletePromise = new Promise<void>((resolve) => {
|
||||
resolveDelete = resolve;
|
||||
});
|
||||
mockDeleteUser.mockReturnValue(deletePromise);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
// Start deletion
|
||||
act(() => {
|
||||
void result.current.deleteAccount('password123');
|
||||
});
|
||||
|
||||
// Should be loading
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
// Resolve the promise
|
||||
await act(async () => {
|
||||
if (resolveDelete) resolveDelete();
|
||||
await deletePromise;
|
||||
});
|
||||
|
||||
// Should no longer be loading
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('handles delete errors with error message', async () => {
|
||||
const mockDeleteUser = vi.mocked(userApi.deleteUser);
|
||||
mockDeleteUser.mockRejectedValue(new Error('Invalid password'));
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
let deleteResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
deleteResult = await result.current.deleteAccount('wrongpass');
|
||||
});
|
||||
|
||||
expect(deleteResult).toBe(false);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Invalid password',
|
||||
color: 'red',
|
||||
});
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('handles generic delete errors', async () => {
|
||||
const mockDeleteUser = vi.mocked(userApi.deleteUser);
|
||||
mockDeleteUser.mockRejectedValue(new Error('Server error'));
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
let deleteResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
deleteResult = await result.current.deleteAccount('password123');
|
||||
});
|
||||
|
||||
expect(deleteResult).toBe(false);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Server error',
|
||||
color: 'red',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-Error rejection in delete', async () => {
|
||||
const mockDeleteUser = vi.mocked(userApi.deleteUser);
|
||||
mockDeleteUser.mockRejectedValue('String error');
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
let deleteResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
deleteResult = await result.current.deleteAccount('password123');
|
||||
});
|
||||
|
||||
expect(deleteResult).toBe(false);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Failed to delete account',
|
||||
color: 'red',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty password', async () => {
|
||||
const mockDeleteUser = vi.mocked(userApi.deleteUser);
|
||||
mockDeleteUser.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
let deleteResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
deleteResult = await result.current.deleteAccount('');
|
||||
});
|
||||
|
||||
expect(deleteResult).toBe(true);
|
||||
expect(mockDeleteUser).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('concurrent operations', () => {
|
||||
it('handles concurrent profile updates', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
mockUpdateProfile
|
||||
.mockResolvedValueOnce({ ...mockUser, displayName: 'Name 1' })
|
||||
.mockResolvedValueOnce({ ...mockUser, displayName: 'Name 2' });
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
let results: (User | null)[] = [];
|
||||
await act(async () => {
|
||||
const promises = [
|
||||
result.current.updateProfile({ displayName: 'Name 1' }),
|
||||
result.current.updateProfile({ displayName: 'Name 2' }),
|
||||
];
|
||||
results = await Promise.all(promises);
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]?.displayName).toBe('Name 1');
|
||||
expect(results[1]?.displayName).toBe('Name 2');
|
||||
expect(mockUpdateProfile).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('handles update followed by delete', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
const mockDeleteUser = vi.mocked(userApi.deleteUser);
|
||||
|
||||
mockUpdateProfile.mockResolvedValue(mockUser);
|
||||
mockDeleteUser.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
let updateResult: User | null = null;
|
||||
let deleteResult: boolean | undefined;
|
||||
|
||||
await act(async () => {
|
||||
updateResult = await result.current.updateProfile({
|
||||
displayName: 'Updated',
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
deleteResult = await result.current.deleteAccount('password123');
|
||||
});
|
||||
|
||||
expect(updateResult).toEqual(mockUser);
|
||||
expect(deleteResult).toBe(true);
|
||||
expect(mockUpdateProfile).toHaveBeenCalledWith({
|
||||
displayName: 'Updated',
|
||||
});
|
||||
expect(mockDeleteUser).toHaveBeenCalledWith('password123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hook interface', () => {
|
||||
it('returns correct interface', () => {
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
expect(typeof result.current.loading).toBe('boolean');
|
||||
expect(typeof result.current.updateProfile).toBe('function');
|
||||
expect(typeof result.current.deleteAccount).toBe('function');
|
||||
});
|
||||
|
||||
it('functions are stable across re-renders', () => {
|
||||
const { result, rerender } = renderHook(() => useProfileSettings());
|
||||
|
||||
const initialFunctions = {
|
||||
updateProfile: result.current.updateProfile,
|
||||
deleteAccount: result.current.deleteAccount,
|
||||
};
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current.updateProfile).toBe(initialFunctions.updateProfile);
|
||||
expect(result.current.deleteAccount).toBe(initialFunctions.deleteAccount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles empty update request', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
mockUpdateProfile.mockResolvedValue(mockUser);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
let returnedUser: User | null = null;
|
||||
await act(async () => {
|
||||
returnedUser = await result.current.updateProfile({});
|
||||
});
|
||||
|
||||
expect(returnedUser).toEqual(mockUser);
|
||||
expect(mockUpdateProfile).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('handles update with undefined values', async () => {
|
||||
const mockUpdateProfile = vi.mocked(userApi.updateProfile);
|
||||
mockUpdateProfile.mockResolvedValue(mockUser);
|
||||
|
||||
const { result } = renderHook(() => useProfileSettings());
|
||||
|
||||
const updateRequest: UpdateProfileRequest = {};
|
||||
|
||||
let returnedUser: User | null = null;
|
||||
await act(async () => {
|
||||
returnedUser = await result.current.updateProfile(updateRequest);
|
||||
});
|
||||
|
||||
expect(returnedUser).toEqual(mockUser);
|
||||
expect(mockUpdateProfile).toHaveBeenCalledWith(updateRequest);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user