feat(trial-user-limits): implement trial member limit checks in project and team controllers
- Added TRIAL_MEMBER_LIMIT constant to enforce a maximum number of trial users in project and team member controllers. - Implemented logic to check current trial members against the limit during user addition, providing appropriate responses for exceeding limits. - Updated relevant controllers to utilize the new trial member limit functionality, enhancing subscription management for trial users. - Enhanced error messaging to guide users on upgrading their subscription for additional members.
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import AuthenticatingPage from '../AuthenticatingPage';
|
||||
import { verifyAuthentication } from '@/features/auth/authSlice';
|
||||
import { setUser } from '@/features/user/userSlice';
|
||||
import { setSession } from '@/utils/session-helper';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/features/auth/authSlice', () => ({
|
||||
verifyAuthentication: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/user/userSlice', () => ({
|
||||
setUser: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/session-helper', () => ({
|
||||
setSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/errorLogger', () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/shared/constants', () => ({
|
||||
WORKLENZ_REDIRECT_PROJ_KEY: 'worklenz_redirect_proj',
|
||||
}));
|
||||
|
||||
// Mock navigation
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock dispatch
|
||||
const mockDispatch = vi.fn();
|
||||
vi.mock('@/hooks/useAppDispatch', () => ({
|
||||
useAppDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
// Setup i18n for testing
|
||||
i18n.init({
|
||||
lng: 'en',
|
||||
resources: {
|
||||
en: {
|
||||
'auth/auth-common': {
|
||||
authenticating: 'Authenticating...',
|
||||
gettingThingsReady: 'Getting things ready for you...',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create test store
|
||||
const createTestStore = () => {
|
||||
return configureStore({
|
||||
reducer: {
|
||||
auth: (state = {}) => state,
|
||||
user: (state = {}) => state,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderWithProviders = (component: React.ReactElement) => {
|
||||
const store = createTestStore();
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{component}
|
||||
</I18nextProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('AuthenticatingPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
localStorage.clear();
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { href: '' },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders loading state correctly', () => {
|
||||
renderWithProviders(<AuthenticatingPage />);
|
||||
|
||||
expect(screen.getByText('Authenticating...')).toBeInTheDocument();
|
||||
expect(screen.getByText('Getting things ready for you...')).toBeInTheDocument();
|
||||
expect(screen.getByRole('generic', { busy: true })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects to login when authentication fails', async () => {
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({ authenticated: false }),
|
||||
});
|
||||
|
||||
renderWithProviders(<AuthenticatingPage />);
|
||||
|
||||
// Run all pending timers
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
|
||||
it('redirects to setup when user setup is not completed', async () => {
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
setup_completed: false,
|
||||
};
|
||||
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({
|
||||
authenticated: true,
|
||||
user: mockUser,
|
||||
}),
|
||||
});
|
||||
|
||||
renderWithProviders(<AuthenticatingPage />);
|
||||
|
||||
// Run all pending timers
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(setSession).toHaveBeenCalledWith(mockUser);
|
||||
expect(setUser).toHaveBeenCalledWith(mockUser);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/worklenz/setup');
|
||||
});
|
||||
|
||||
it('redirects to home after successful authentication', async () => {
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
setup_completed: true,
|
||||
};
|
||||
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({
|
||||
authenticated: true,
|
||||
user: mockUser,
|
||||
}),
|
||||
});
|
||||
|
||||
renderWithProviders(<AuthenticatingPage />);
|
||||
|
||||
// Run all pending timers
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(setSession).toHaveBeenCalledWith(mockUser);
|
||||
expect(setUser).toHaveBeenCalledWith(mockUser);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/worklenz/home');
|
||||
});
|
||||
|
||||
it('redirects to project when redirect key is present in localStorage', async () => {
|
||||
const projectId = 'test-project-123';
|
||||
localStorage.setItem('worklenz_redirect_proj', projectId);
|
||||
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
setup_completed: true,
|
||||
};
|
||||
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({
|
||||
authenticated: true,
|
||||
user: mockUser,
|
||||
}),
|
||||
});
|
||||
|
||||
// Mock window.location with a proper setter
|
||||
let hrefValue = '';
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
get href() {
|
||||
return hrefValue;
|
||||
},
|
||||
set href(value) {
|
||||
hrefValue = value;
|
||||
},
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
renderWithProviders(<AuthenticatingPage />);
|
||||
|
||||
// Run all pending timers
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(setSession).toHaveBeenCalledWith(mockUser);
|
||||
expect(setUser).toHaveBeenCalledWith(mockUser);
|
||||
expect(hrefValue).toBe(`/worklenz/projects/${projectId}?tab=tasks-list`);
|
||||
expect(localStorage.getItem('worklenz_redirect_proj')).toBeNull();
|
||||
});
|
||||
|
||||
it('handles authentication errors and redirects to login', async () => {
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockRejectedValue(new Error('Authentication failed')),
|
||||
});
|
||||
|
||||
renderWithProviders(<AuthenticatingPage />);
|
||||
|
||||
// Run all pending timers
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import ForgotPasswordPage from '../ForgotPasswordPage';
|
||||
import { resetPassword, verifyAuthentication } from '@/features/auth/authSlice';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/features/auth/authSlice', () => ({
|
||||
resetPassword: vi.fn(),
|
||||
verifyAuthentication: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/user/userSlice', () => ({
|
||||
setUser: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/session-helper', () => ({
|
||||
setSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/errorLogger', () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useMixpanelTracking', () => ({
|
||||
useMixpanelTracking: () => ({
|
||||
trackMixpanelEvent: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useDoumentTItle', () => ({
|
||||
useDocumentTitle: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('react-responsive', () => ({
|
||||
useMediaQuery: () => false,
|
||||
}));
|
||||
|
||||
// Mock navigation
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock dispatch
|
||||
const mockDispatch = vi.fn();
|
||||
vi.mock('@/hooks/useAppDispatch', () => ({
|
||||
useAppDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
// Setup i18n for testing
|
||||
i18n.init({
|
||||
lng: 'en',
|
||||
resources: {
|
||||
en: {
|
||||
'auth/forgot-password': {
|
||||
headerDescription: 'Enter your email to reset your password',
|
||||
emailRequired: 'Please input your email!',
|
||||
emailPlaceholder: 'Enter your email',
|
||||
resetPasswordButton: 'Reset Password',
|
||||
returnToLoginButton: 'Return to Login',
|
||||
orText: 'or',
|
||||
successTitle: 'Password Reset Email Sent',
|
||||
successMessage: 'Please check your email for instructions to reset your password.',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create test store
|
||||
const createTestStore = () => {
|
||||
return configureStore({
|
||||
reducer: {
|
||||
auth: (state = {}) => state,
|
||||
user: (state = {}) => state,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderWithProviders = (component: React.ReactElement) => {
|
||||
const store = createTestStore();
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{component}
|
||||
</I18nextProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('ForgotPasswordPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Mock URL search params
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '' },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders forgot password form correctly', () => {
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
expect(screen.getByText('Enter your email to reset your password')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Enter your email')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Reset Password' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Return to Login' })).toBeInTheDocument();
|
||||
expect(screen.getByText('or')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('validates required email field', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Please input your email!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('validates email format', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByPlaceholderText('Enter your email');
|
||||
await user.type(emailInput, 'invalid-email');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Please input your email!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('submits form with valid email', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({ done: true }),
|
||||
});
|
||||
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByPlaceholderText('Enter your email');
|
||||
await user.type(emailInput, 'test@example.com');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resetPassword).toHaveBeenCalledWith('test@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows success message after successful submission', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({ done: true }),
|
||||
});
|
||||
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByPlaceholderText('Enter your email');
|
||||
await user.type(emailInput, 'test@example.com');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password Reset Email Sent')).toBeInTheDocument();
|
||||
expect(screen.getByText('Please check your email for instructions to reset your password.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows loading state during submission', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockImplementation(() => new Promise(() => {})), // Never resolves
|
||||
});
|
||||
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByPlaceholderText('Enter your email');
|
||||
await user.type(emailInput, 'test@example.com');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('img', { name: /loading/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles submission errors gracefully', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockRejectedValue(new Error('Reset failed')),
|
||||
});
|
||||
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByPlaceholderText('Enter your email');
|
||||
await user.type(emailInput, 'test@example.com');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should not show success message
|
||||
expect(screen.queryByText('Password Reset Email Sent')).not.toBeInTheDocument();
|
||||
// Should still show the form
|
||||
expect(screen.getByPlaceholderText('Enter your email')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to login page when return button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
const returnButton = screen.getByRole('button', { name: 'Return to Login' });
|
||||
await user.click(returnButton);
|
||||
|
||||
expect(returnButton.closest('a')).toHaveAttribute('href', '/auth/login');
|
||||
});
|
||||
|
||||
it('handles team parameter from URL', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?team=test-team-id' },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
// Component should render normally even with team parameter
|
||||
expect(screen.getByText('Enter your email to reset your password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects authenticated users to home', async () => {
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({
|
||||
authenticated: true,
|
||||
user: { id: '1', email: 'test@example.com' },
|
||||
}),
|
||||
});
|
||||
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/worklenz/home');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not submit with empty email after trimming', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({ done: true }),
|
||||
});
|
||||
|
||||
renderWithProviders(<ForgotPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByPlaceholderText('Enter your email');
|
||||
await user.type(emailInput, ' '); // Only whitespace
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
// Should not call resetPassword with empty string
|
||||
expect(resetPassword).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import LoggingOutPage from '../LoggingOutPage';
|
||||
import { authApiService } from '@/api/auth/auth.api.service';
|
||||
import CacheCleanup from '@/utils/cache-cleanup';
|
||||
|
||||
// Mock dependencies
|
||||
const mockAuthService = {
|
||||
signOut: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('@/hooks/useAuth', () => ({
|
||||
useAuthService: () => mockAuthService,
|
||||
}));
|
||||
|
||||
vi.mock('@/api/auth/auth.api.service', () => ({
|
||||
authApiService: {
|
||||
logout: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/cache-cleanup', () => ({
|
||||
default: {
|
||||
clearAllCaches: vi.fn(),
|
||||
forceReload: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('react-responsive', () => ({
|
||||
useMediaQuery: () => false,
|
||||
}));
|
||||
|
||||
// Mock navigation
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
// Setup i18n for testing
|
||||
i18n.init({
|
||||
lng: 'en',
|
||||
resources: {
|
||||
en: {
|
||||
'auth/auth-common': {
|
||||
loggingOut: 'Logging Out...',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const renderWithProviders = (component: React.ReactElement) => {
|
||||
return render(
|
||||
<BrowserRouter>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{component}
|
||||
</I18nextProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
describe('LoggingOutPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
|
||||
// Mock console.error to avoid noise in tests
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
vi.mock('@/hooks/useAuth', () => ({
|
||||
useAuthService: () => mockAuthService,
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders loading state correctly', () => {
|
||||
renderWithProviders(<LoggingOutPage />);
|
||||
|
||||
expect(screen.getByText('Logging Out...')).toBeInTheDocument();
|
||||
expect(screen.getByRole('img', { name: /loading/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('performs complete logout sequence successfully', async () => {
|
||||
mockAuthService.signOut.mockResolvedValue(undefined);
|
||||
(authApiService.logout as any).mockResolvedValue(undefined);
|
||||
(CacheCleanup.clearAllCaches as any).mockResolvedValue(undefined);
|
||||
|
||||
renderWithProviders(<LoggingOutPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAuthService.signOut).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(authApiService.logout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(CacheCleanup.clearAllCaches).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Fast-forward time to trigger the setTimeout
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(CacheCleanup.forceReload).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles auth service signOut failure', async () => {
|
||||
mockAuthService.signOut.mockRejectedValue(new Error('SignOut failed'));
|
||||
(authApiService.logout as any).mockResolvedValue(undefined);
|
||||
(CacheCleanup.clearAllCaches as any).mockResolvedValue(undefined);
|
||||
|
||||
renderWithProviders(<LoggingOutPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAuthService.signOut).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(console.error).toHaveBeenCalledWith('Logout error:', expect.any(Error));
|
||||
expect(CacheCleanup.forceReload).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles backend logout failure', async () => {
|
||||
mockAuthService.signOut.mockResolvedValue(undefined);
|
||||
(authApiService.logout as any).mockRejectedValue(new Error('Backend logout failed'));
|
||||
(CacheCleanup.clearAllCaches as any).mockResolvedValue(undefined);
|
||||
|
||||
renderWithProviders(<LoggingOutPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAuthService.signOut).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(authApiService.logout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(console.error).toHaveBeenCalledWith('Logout error:', expect.any(Error));
|
||||
expect(CacheCleanup.forceReload).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles cache cleanup failure', async () => {
|
||||
mockAuthService.signOut.mockResolvedValue(undefined);
|
||||
(authApiService.logout as any).mockResolvedValue(undefined);
|
||||
(CacheCleanup.clearAllCaches as any).mockRejectedValue(new Error('Cache cleanup failed'));
|
||||
|
||||
renderWithProviders(<LoggingOutPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAuthService.signOut).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(authApiService.logout).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(CacheCleanup.clearAllCaches).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(console.error).toHaveBeenCalledWith('Logout error:', expect.any(Error));
|
||||
expect(CacheCleanup.forceReload).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers logout sequence immediately on mount', () => {
|
||||
renderWithProviders(<LoggingOutPage />);
|
||||
|
||||
expect(mockAuthService.signOut).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows consistent loading UI throughout logout process', async () => {
|
||||
mockAuthService.signOut.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
|
||||
(authApiService.logout as any).mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
|
||||
(CacheCleanup.clearAllCaches as any).mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
|
||||
|
||||
renderWithProviders(<LoggingOutPage />);
|
||||
|
||||
// Should show loading state immediately
|
||||
expect(screen.getByText('Logging Out...')).toBeInTheDocument();
|
||||
expect(screen.getByRole('img', { name: /loading/i })).toBeInTheDocument();
|
||||
|
||||
// Should continue showing loading state during the process
|
||||
vi.advanceTimersByTime(50);
|
||||
|
||||
expect(screen.getByText('Logging Out...')).toBeInTheDocument();
|
||||
expect(screen.getByRole('img', { name: /loading/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls forceReload with correct path after timeout', async () => {
|
||||
mockAuthService.signOut.mockResolvedValue(undefined);
|
||||
(authApiService.logout as any).mockResolvedValue(undefined);
|
||||
(CacheCleanup.clearAllCaches as any).mockResolvedValue(undefined);
|
||||
|
||||
renderWithProviders(<LoggingOutPage />);
|
||||
|
||||
// Wait for all async operations to complete
|
||||
await waitFor(() => {
|
||||
expect(CacheCleanup.clearAllCaches).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Fast-forward exactly 1000ms
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(CacheCleanup.forceReload).toHaveBeenCalledWith('/auth/login');
|
||||
expect(CacheCleanup.forceReload).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles complete failure of all logout steps', async () => {
|
||||
mockAuthService.signOut.mockRejectedValue(new Error('SignOut failed'));
|
||||
(authApiService.logout as any).mockRejectedValue(new Error('Backend logout failed'));
|
||||
(CacheCleanup.clearAllCaches as any).mockRejectedValue(new Error('Cache cleanup failed'));
|
||||
|
||||
renderWithProviders(<LoggingOutPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(console.error).toHaveBeenCalledWith('Logout error:', expect.any(Error));
|
||||
expect(CacheCleanup.forceReload).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
});
|
||||
});
|
||||
317
worklenz-frontend/src/pages/auth/__tests__/LoginPage.test.tsx
Normal file
317
worklenz-frontend/src/pages/auth/__tests__/LoginPage.test.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import LoginPage from '../LoginPage';
|
||||
import { login, verifyAuthentication } from '@/features/auth/authSlice';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/features/auth/authSlice', () => ({
|
||||
login: vi.fn(),
|
||||
verifyAuthentication: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/user/userSlice', () => ({
|
||||
setUser: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/session-helper', () => ({
|
||||
setSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/errorLogger', () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useMixpanelTracking', () => ({
|
||||
useMixpanelTracking: () => ({
|
||||
trackMixpanelEvent: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useDoumentTItle', () => ({
|
||||
useDocumentTitle: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useAuth', () => ({
|
||||
useAuthService: () => ({
|
||||
getCurrentSession: () => null,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/services/alerts/alertService', () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('react-responsive', () => ({
|
||||
useMediaQuery: () => false,
|
||||
}));
|
||||
|
||||
// Mock navigation
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock dispatch
|
||||
const mockDispatch = vi.fn();
|
||||
vi.mock('@/hooks/useAppDispatch', () => ({
|
||||
useAppDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
// Setup i18n for testing
|
||||
i18n.init({
|
||||
lng: 'en',
|
||||
resources: {
|
||||
en: {
|
||||
'auth/login': {
|
||||
headerDescription: 'Sign in to your account',
|
||||
emailRequired: 'Please input your email!',
|
||||
passwordRequired: 'Please input your password!',
|
||||
emailPlaceholder: 'Email',
|
||||
passwordPlaceholder: 'Password',
|
||||
loginButton: 'Sign In',
|
||||
rememberMe: 'Remember me',
|
||||
forgotPasswordButton: 'Forgot password?',
|
||||
signInWithGoogleButton: 'Sign in with Google',
|
||||
orText: 'or',
|
||||
dontHaveAccountText: "Don't have an account?",
|
||||
signupButton: 'Sign up',
|
||||
successMessage: 'Login successful!',
|
||||
'validationMessages.email': 'Please enter a valid email!',
|
||||
'validationMessages.password': 'Password must be at least 8 characters!',
|
||||
'errorMessages.loginErrorTitle': 'Login Failed',
|
||||
'errorMessages.loginErrorMessage': 'Invalid email or password',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create test store
|
||||
const createTestStore = (initialState: any = {}) => {
|
||||
return configureStore({
|
||||
reducer: {
|
||||
auth: (state = { isLoading: false, ...initialState.auth }) => state,
|
||||
user: (state = {}) => state,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderWithProviders = (component: React.ReactElement, initialState: any = {}) => {
|
||||
const store = createTestStore(initialState);
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{component}
|
||||
</I18nextProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('LoginPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Mock environment variables
|
||||
vi.stubEnv('VITE_ENABLE_GOOGLE_LOGIN', 'true');
|
||||
vi.stubEnv('VITE_API_URL', 'http://localhost:3000');
|
||||
});
|
||||
|
||||
it('renders login form correctly', () => {
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
expect(screen.getByText('Sign in to your account')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Remember me')).toBeInTheDocument();
|
||||
expect(screen.getByText('Forgot password?')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Google login button when enabled', () => {
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
expect(screen.getByText('Sign in with Google')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('validates required fields', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign In' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Please input your email!')).toBeInTheDocument();
|
||||
expect(screen.getByText('Please input your password!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('validates email format', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
const emailInput = screen.getByPlaceholderText('Email');
|
||||
await user.type(emailInput, 'invalid-email');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign In' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Please enter a valid email!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('validates password minimum length', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
const emailInput = screen.getByPlaceholderText('Email');
|
||||
const passwordInput = screen.getByPlaceholderText('Password');
|
||||
|
||||
await user.type(emailInput, 'test@example.com');
|
||||
await user.type(passwordInput, '123');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign In' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password must be at least 8 characters!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('submits form with valid credentials', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({
|
||||
authenticated: true,
|
||||
user: { id: '1', email: 'test@example.com' },
|
||||
}),
|
||||
});
|
||||
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
const emailInput = screen.getByPlaceholderText('Email');
|
||||
const passwordInput = screen.getByPlaceholderText('Password');
|
||||
|
||||
await user.type(emailInput, 'test@example.com');
|
||||
await user.type(passwordInput, 'password123');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign In' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(login).toHaveBeenCalledWith({
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
remember: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows loading state during login', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<LoginPage />, { auth: { isLoading: true } });
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign In' });
|
||||
expect(submitButton).toBeDisabled();
|
||||
expect(screen.getByRole('img', { name: /loading/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles Google login click', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { href: '' },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const googleButton = screen.getByText('Sign in with Google');
|
||||
await user.click(googleButton);
|
||||
|
||||
expect(window.location.href).toBe('http://localhost:3000/secure/google');
|
||||
});
|
||||
|
||||
it('navigates to signup page', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
const signupLink = screen.getByText('Sign up');
|
||||
await user.click(signupLink);
|
||||
|
||||
// Link navigation is handled by React Router, so we just check the element exists
|
||||
expect(signupLink.closest('a')).toHaveAttribute('href', '/auth/signup');
|
||||
});
|
||||
|
||||
it('navigates to forgot password page', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
const forgotPasswordLink = screen.getByText('Forgot password?');
|
||||
await user.click(forgotPasswordLink);
|
||||
|
||||
expect(forgotPasswordLink.closest('a')).toHaveAttribute('href', '/auth/forgot-password');
|
||||
});
|
||||
|
||||
it('toggles remember me checkbox', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
const rememberMeCheckbox = screen.getByRole('checkbox', { name: 'Remember me' });
|
||||
expect(rememberMeCheckbox).toBeChecked(); // Default is true
|
||||
|
||||
await user.click(rememberMeCheckbox);
|
||||
expect(rememberMeCheckbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('redirects already authenticated users', async () => {
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({
|
||||
authenticated: true,
|
||||
user: { id: '1', email: 'test@example.com', setup_completed: true },
|
||||
}),
|
||||
});
|
||||
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/worklenz/home');
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects to setup for users with incomplete setup', async () => {
|
||||
const mockCurrentSession = {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
setup_completed: false,
|
||||
};
|
||||
|
||||
vi.mock('@/hooks/useAuth', () => ({
|
||||
useAuthService: () => ({
|
||||
getCurrentSession: () => mockCurrentSession,
|
||||
}),
|
||||
}));
|
||||
|
||||
renderWithProviders(<LoginPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/worklenz/setup');
|
||||
});
|
||||
});
|
||||
});
|
||||
359
worklenz-frontend/src/pages/auth/__tests__/SignupPage.test.tsx
Normal file
359
worklenz-frontend/src/pages/auth/__tests__/SignupPage.test.tsx
Normal file
@@ -0,0 +1,359 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import SignupPage from '../SignupPage';
|
||||
import { signUp } from '@/features/auth/authSlice';
|
||||
import { authApiService } from '@/api/auth/auth.api.service';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/features/auth/authSlice', () => ({
|
||||
signUp: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/auth/auth.api.service', () => ({
|
||||
authApiService: {
|
||||
signUpCheck: vi.fn(),
|
||||
verifyRecaptchaToken: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useMixpanelTracking', () => ({
|
||||
useMixpanelTracking: () => ({
|
||||
trackMixpanelEvent: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useDoumentTItle', () => ({
|
||||
useDocumentTitle: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/errorLogger', () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/services/alerts/alertService', () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('react-responsive', () => ({
|
||||
useMediaQuery: () => false,
|
||||
}));
|
||||
|
||||
// Mock navigation
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock dispatch
|
||||
const mockDispatch = vi.fn();
|
||||
vi.mock('@/hooks/useAppDispatch', () => ({
|
||||
useAppDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
// Setup i18n for testing
|
||||
i18n.init({
|
||||
lng: 'en',
|
||||
resources: {
|
||||
en: {
|
||||
'auth/signup': {
|
||||
headerDescription: 'Sign up to get started',
|
||||
nameLabel: 'Full Name',
|
||||
emailLabel: 'Email',
|
||||
passwordLabel: 'Password',
|
||||
nameRequired: 'Please input your name!',
|
||||
emailRequired: 'Please input your email!',
|
||||
passwordRequired: 'Please input your password!',
|
||||
nameMinCharacterRequired: 'Name must be at least 4 characters!',
|
||||
passwordMinCharacterRequired: 'Password must be at least 8 characters!',
|
||||
passwordMaxCharacterRequired: 'Password must be no more than 32 characters!',
|
||||
passwordPatternRequired: 'Password must contain uppercase, lowercase, number and special character!',
|
||||
namePlaceholder: 'Enter your full name',
|
||||
emailPlaceholder: 'Enter your email',
|
||||
strongPasswordPlaceholder: 'Enter a strong password',
|
||||
passwordGuideline: 'Password must be at least 8 characters, include uppercase and lowercase letters, a number, and a special character.',
|
||||
signupButton: 'Sign Up',
|
||||
signInWithGoogleButton: 'Sign up with Google',
|
||||
orText: 'or',
|
||||
alreadyHaveAccountText: 'Already have an account?',
|
||||
loginButton: 'Log in',
|
||||
bySigningUpText: 'By signing up, you agree to our',
|
||||
privacyPolicyLink: 'Privacy Policy',
|
||||
andText: 'and',
|
||||
termsOfUseLink: 'Terms of Use',
|
||||
reCAPTCHAVerificationError: 'reCAPTCHA Verification Failed',
|
||||
reCAPTCHAVerificationErrorMessage: 'Please try again',
|
||||
'passwordChecklist.minLength': 'At least 8 characters',
|
||||
'passwordChecklist.uppercase': 'One uppercase letter',
|
||||
'passwordChecklist.lowercase': 'One lowercase letter',
|
||||
'passwordChecklist.number': 'One number',
|
||||
'passwordChecklist.special': 'One special character',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create test store
|
||||
const createTestStore = () => {
|
||||
return configureStore({
|
||||
reducer: {
|
||||
themeReducer: (state = { mode: 'light' }) => state,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderWithProviders = (component: React.ReactElement) => {
|
||||
const store = createTestStore();
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{component}
|
||||
</I18nextProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('SignupPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
// Mock environment variables
|
||||
vi.stubEnv('VITE_ENABLE_GOOGLE_LOGIN', 'true');
|
||||
vi.stubEnv('VITE_ENABLE_RECAPTCHA', 'false');
|
||||
vi.stubEnv('VITE_API_URL', 'http://localhost:3000');
|
||||
|
||||
// Mock URL search params
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '' },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders signup form correctly', () => {
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
expect(screen.getByText('Sign up to get started')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Enter your full name')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Enter your email')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Enter a strong password')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Sign Up' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Google signup button when enabled', () => {
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
expect(screen.getByText('Sign up with Google')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('validates required fields', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign Up' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Please input your name!')).toBeInTheDocument();
|
||||
expect(screen.getByText('Please input your email!')).toBeInTheDocument();
|
||||
expect(screen.getByText('Please input your password!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('validates name minimum length', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('Enter your full name');
|
||||
await user.type(nameInput, 'Jo');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign Up' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Name must be at least 4 characters!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('validates email format', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
const emailInput = screen.getByPlaceholderText('Enter your email');
|
||||
await user.type(emailInput, 'invalid-email');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign Up' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Please input your email!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('validates password requirements', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('Enter a strong password');
|
||||
await user.type(passwordInput, 'weak');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign Up' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password must be at least 8 characters!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows password checklist when password field is focused', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('Enter a strong password');
|
||||
await user.click(passwordInput);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('At least 8 characters')).toBeInTheDocument();
|
||||
expect(screen.getByText('One uppercase letter')).toBeInTheDocument();
|
||||
expect(screen.getByText('One lowercase letter')).toBeInTheDocument();
|
||||
expect(screen.getByText('One number')).toBeInTheDocument();
|
||||
expect(screen.getByText('One special character')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('updates password checklist based on input', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('Enter a strong password');
|
||||
await user.click(passwordInput);
|
||||
await user.type(passwordInput, 'Password123!');
|
||||
|
||||
await waitFor(() => {
|
||||
// All checklist items should be visible and some should be checked
|
||||
expect(screen.getByText('At least 8 characters')).toBeInTheDocument();
|
||||
expect(screen.getByText('One uppercase letter')).toBeInTheDocument();
|
||||
expect(screen.getByText('One lowercase letter')).toBeInTheDocument();
|
||||
expect(screen.getByText('One number')).toBeInTheDocument();
|
||||
expect(screen.getByText('One special character')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('submits form with valid data', async () => {
|
||||
const user = userEvent.setup();
|
||||
(authApiService.signUpCheck as any).mockResolvedValue({ done: true });
|
||||
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('Enter your full name');
|
||||
const emailInput = screen.getByPlaceholderText('Enter your email');
|
||||
const passwordInput = screen.getByPlaceholderText('Enter a strong password');
|
||||
|
||||
await user.type(nameInput, 'John Doe');
|
||||
await user.type(emailInput, 'john@example.com');
|
||||
await user.type(passwordInput, 'Password123!');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign Up' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(authApiService.signUpCheck).toHaveBeenCalledWith({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'Password123!',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles Google signup click', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { href: '' },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const googleButton = screen.getByText('Sign up with Google');
|
||||
await user.click(googleButton);
|
||||
|
||||
expect(window.location.href).toBe('http://localhost:3000/secure/google?');
|
||||
});
|
||||
|
||||
it('pre-fills form from URL parameters', () => {
|
||||
// Mock URLSearchParams
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { search: '?email=test@example.com&name=Test User' },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('Enter your full name') as HTMLInputElement;
|
||||
const emailInput = screen.getByPlaceholderText('Enter your email') as HTMLInputElement;
|
||||
|
||||
expect(nameInput.value).toBe('Test User');
|
||||
expect(emailInput.value).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('shows terms of use and privacy policy links', () => {
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
expect(screen.getByText('Privacy Policy')).toBeInTheDocument();
|
||||
expect(screen.getByText('Terms of Use')).toBeInTheDocument();
|
||||
|
||||
const privacyLink = screen.getByText('Privacy Policy').closest('a');
|
||||
const termsLink = screen.getByText('Terms of Use').closest('a');
|
||||
|
||||
expect(privacyLink).toHaveAttribute('href', 'https://worklenz.com/privacy/');
|
||||
expect(termsLink).toHaveAttribute('href', 'https://worklenz.com/terms/');
|
||||
});
|
||||
|
||||
it('navigates to login page', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
const loginLink = screen.getByText('Log in');
|
||||
await user.click(loginLink);
|
||||
|
||||
expect(loginLink.closest('a')).toHaveAttribute('href', '/auth/login');
|
||||
});
|
||||
|
||||
it('shows loading state during signup', async () => {
|
||||
const user = userEvent.setup();
|
||||
(authApiService.signUpCheck as any).mockResolvedValue({ done: true });
|
||||
|
||||
renderWithProviders(<SignupPage />);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('Enter your full name');
|
||||
const emailInput = screen.getByPlaceholderText('Enter your email');
|
||||
const passwordInput = screen.getByPlaceholderText('Enter a strong password');
|
||||
|
||||
await user.type(nameInput, 'John Doe');
|
||||
await user.type(emailInput, 'john@example.com');
|
||||
await user.type(passwordInput, 'Password123!');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign Up' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('img', { name: /loading/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { BrowserRouter, MemoryRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import VerifyResetEmailPage from '../VerifyResetEmailPage';
|
||||
import { updatePassword } from '@/features/auth/authSlice';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/features/auth/authSlice', () => ({
|
||||
updatePassword: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/errorLogger', () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useMixpanelTracking', () => ({
|
||||
useMixpanelTracking: () => ({
|
||||
trackMixpanelEvent: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useDoumentTItle', () => ({
|
||||
useDocumentTitle: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('react-responsive', () => ({
|
||||
useMediaQuery: () => false,
|
||||
}));
|
||||
|
||||
// Mock navigation
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock dispatch
|
||||
const mockDispatch = vi.fn();
|
||||
vi.mock('@/hooks/useAppDispatch', () => ({
|
||||
useAppDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
// Setup i18n for testing
|
||||
i18n.init({
|
||||
lng: 'en',
|
||||
resources: {
|
||||
en: {
|
||||
'auth/verify-reset-email': {
|
||||
description: 'Enter your new password',
|
||||
passwordRequired: 'Please input your password!',
|
||||
confirmPasswordRequired: 'Please confirm your password!',
|
||||
placeholder: 'Enter new password',
|
||||
confirmPasswordPlaceholder: 'Confirm new password',
|
||||
resetPasswordButton: 'Reset Password',
|
||||
resendResetEmail: 'Resend Reset Email',
|
||||
orText: 'or',
|
||||
passwordMismatch: 'The two passwords do not match!',
|
||||
successTitle: 'Password Reset Successful',
|
||||
successMessage: 'Your password has been reset successfully.',
|
||||
'passwordChecklist.minLength': 'At least 8 characters',
|
||||
'passwordChecklist.uppercase': 'One uppercase letter',
|
||||
'passwordChecklist.lowercase': 'One lowercase letter',
|
||||
'passwordChecklist.number': 'One number',
|
||||
'passwordChecklist.special': 'One special character',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Create test store
|
||||
const createTestStore = () => {
|
||||
return configureStore({
|
||||
reducer: {
|
||||
themeReducer: (state = { mode: 'light' }) => state,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderWithProviders = (component: React.ReactElement, route = '/verify-reset/test-hash/test-user') => {
|
||||
const store = createTestStore();
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<MemoryRouter initialEntries={[route]}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{component}
|
||||
</I18nextProvider>
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('VerifyResetEmailPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders password reset form correctly', () => {
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
expect(screen.getByText('Enter your new password')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Enter new password')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Confirm new password')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Reset Password' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Resend Reset Email' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows password checklist immediately', () => {
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
expect(screen.getByText('At least 8 characters')).toBeInTheDocument();
|
||||
expect(screen.getByText('One uppercase letter')).toBeInTheDocument();
|
||||
expect(screen.getByText('One lowercase letter')).toBeInTheDocument();
|
||||
expect(screen.getByText('One number')).toBeInTheDocument();
|
||||
expect(screen.getByText('One special character')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('validates required password fields', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Please input your password!')).toBeInTheDocument();
|
||||
expect(screen.getByText('Please confirm your password!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('validates password confirmation match', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('Enter new password');
|
||||
const confirmPasswordInput = screen.getByPlaceholderText('Confirm new password');
|
||||
|
||||
await user.type(passwordInput, 'Password123!');
|
||||
await user.type(confirmPasswordInput, 'DifferentPassword123!');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('The two passwords do not match!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('updates password checklist based on input', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('Enter new password');
|
||||
await user.type(passwordInput, 'Password123!');
|
||||
|
||||
// All checklist items should be visible (this component shows them by default)
|
||||
expect(screen.getByText('At least 8 characters')).toBeInTheDocument();
|
||||
expect(screen.getByText('One uppercase letter')).toBeInTheDocument();
|
||||
expect(screen.getByText('One lowercase letter')).toBeInTheDocument();
|
||||
expect(screen.getByText('One number')).toBeInTheDocument();
|
||||
expect(screen.getByText('One special character')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('submits form with valid matching passwords', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({ done: true }),
|
||||
});
|
||||
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('Enter new password');
|
||||
const confirmPasswordInput = screen.getByPlaceholderText('Confirm new password');
|
||||
|
||||
await user.type(passwordInput, 'Password123!');
|
||||
await user.type(confirmPasswordInput, 'Password123!');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updatePassword).toHaveBeenCalledWith({
|
||||
hash: 'test-hash',
|
||||
user: 'test-user',
|
||||
password: 'Password123!',
|
||||
confirmPassword: 'Password123!',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows success message after successful password reset', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({ done: true }),
|
||||
});
|
||||
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('Enter new password');
|
||||
const confirmPasswordInput = screen.getByPlaceholderText('Confirm new password');
|
||||
|
||||
await user.type(passwordInput, 'Password123!');
|
||||
await user.type(confirmPasswordInput, 'Password123!');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Password Reset Successful')).toBeInTheDocument();
|
||||
expect(screen.getByText('Your password has been reset successfully.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows loading state during submission', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockImplementation(() => new Promise(() => {})), // Never resolves
|
||||
});
|
||||
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('Enter new password');
|
||||
const confirmPasswordInput = screen.getByPlaceholderText('Confirm new password');
|
||||
|
||||
await user.type(passwordInput, 'Password123!');
|
||||
await user.type(confirmPasswordInput, 'Password123!');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('img', { name: /loading/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles submission errors gracefully', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockRejectedValue(new Error('Reset failed')),
|
||||
});
|
||||
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('Enter new password');
|
||||
const confirmPasswordInput = screen.getByPlaceholderText('Confirm new password');
|
||||
|
||||
await user.type(passwordInput, 'Password123!');
|
||||
await user.type(confirmPasswordInput, 'Password123!');
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should not show success message
|
||||
expect(screen.queryByText('Password Reset Successful')).not.toBeInTheDocument();
|
||||
// Should still show the form
|
||||
expect(screen.getByPlaceholderText('Enter new password')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to forgot password page when resend button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
const resendButton = screen.getByRole('button', { name: 'Resend Reset Email' });
|
||||
await user.click(resendButton);
|
||||
|
||||
expect(resendButton.closest('a')).toHaveAttribute('href', '/auth/forgot-password');
|
||||
});
|
||||
|
||||
it('prevents pasting in confirm password field', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
const confirmPasswordInput = screen.getByPlaceholderText('Confirm new password');
|
||||
|
||||
await user.click(confirmPasswordInput);
|
||||
|
||||
// Try to paste - should be prevented
|
||||
const pasteEvent = new ClipboardEvent('paste', {
|
||||
clipboardData: new DataTransfer(),
|
||||
});
|
||||
|
||||
fireEvent(confirmPasswordInput, pasteEvent);
|
||||
|
||||
// The preventDefault should be called (we can't easily test this directly,
|
||||
// but we can ensure the input behavior remains consistent)
|
||||
expect(confirmPasswordInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not submit with empty passwords after trimming', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDispatch.mockReturnValue({
|
||||
unwrap: vi.fn().mockResolvedValue({ done: true }),
|
||||
});
|
||||
|
||||
renderWithProviders(<VerifyResetEmailPage />);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('Enter new password');
|
||||
const confirmPasswordInput = screen.getByPlaceholderText('Confirm new password');
|
||||
|
||||
await user.type(passwordInput, ' '); // Only whitespace
|
||||
await user.type(confirmPasswordInput, ' '); // Only whitespace
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: 'Reset Password' });
|
||||
await user.click(submitButton);
|
||||
|
||||
// Should not call updatePassword with empty strings
|
||||
expect(updatePassword).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('extracts hash and user from URL params', () => {
|
||||
renderWithProviders(<VerifyResetEmailPage />, '/verify-reset/my-hash/my-user');
|
||||
|
||||
// Component should render normally, indicating it received the params
|
||||
expect(screen.getByText('Enter your new password')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
83
worklenz-frontend/src/test/setup.ts
Normal file
83
worklenz-frontend/src/test/setup.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock environment variables
|
||||
Object.defineProperty(process, 'env', {
|
||||
value: {
|
||||
NODE_ENV: 'test',
|
||||
VITE_API_URL: 'http://localhost:3000',
|
||||
VITE_ENABLE_GOOGLE_LOGIN: 'true',
|
||||
VITE_ENABLE_RECAPTCHA: 'false',
|
||||
VITE_RECAPTCHA_SITE_KEY: 'test-site-key',
|
||||
},
|
||||
});
|
||||
|
||||
// Mock window.matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), // deprecated
|
||||
removeListener: vi.fn(), // deprecated
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock IntersectionObserver
|
||||
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock ResizeObserver
|
||||
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock window.getSelection
|
||||
Object.defineProperty(window, 'getSelection', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(() => ({
|
||||
rangeCount: 0,
|
||||
getRangeAt: vi.fn(),
|
||||
removeAllRanges: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
vi.stubGlobal('localStorage', localStorageMock);
|
||||
|
||||
// Mock sessionStorage
|
||||
const sessionStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
vi.stubGlobal('sessionStorage', sessionStorageMock);
|
||||
|
||||
// Suppress console warnings during tests
|
||||
const originalConsoleWarn = console.warn;
|
||||
console.warn = (...args) => {
|
||||
// Suppress specific warnings that are not relevant for tests
|
||||
if (
|
||||
args[0]?.includes?.('React Router Future Flag Warning') ||
|
||||
args[0]?.includes?.('validateDOMNesting')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalConsoleWarn(...args);
|
||||
};
|
||||
Reference in New Issue
Block a user