imageFilenames and imageFilename, backend integration tests, frontend tests, removed username references
This commit is contained in:
@@ -6,7 +6,8 @@ module.exports = {
|
||||
'!src/reportWebVitals.ts',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/setupTests.ts',
|
||||
'!src/test-polyfills.js'
|
||||
'!src/test-polyfills.js',
|
||||
'!src/mocks/**'
|
||||
],
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
|
||||
@@ -15,7 +16,7 @@ module.exports = {
|
||||
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
|
||||
],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(axios|@stripe)/)'
|
||||
'/node_modules/(?!(axios|@stripe)/).*'
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
|
||||
|
||||
25
frontend/jest.env.js
Normal file
25
frontend/jest.env.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const JSDOMEnvironment = require('jest-environment-jsdom').default;
|
||||
const { TextEncoder, TextDecoder } = require('util');
|
||||
|
||||
class CustomEnvironment extends JSDOMEnvironment {
|
||||
constructor(config, context) {
|
||||
super(config, context);
|
||||
|
||||
// Add polyfills to global before any test code runs
|
||||
this.global.TextEncoder = TextEncoder;
|
||||
this.global.TextDecoder = TextDecoder;
|
||||
|
||||
// BroadcastChannel polyfill
|
||||
this.global.BroadcastChannel = class BroadcastChannel {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
postMessage() {}
|
||||
close() {}
|
||||
addEventListener() {}
|
||||
removeEventListener() {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CustomEnvironment;
|
||||
57
frontend/package-lock.json
generated
57
frontend/package-lock.json
generated
@@ -33,6 +33,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google.maps": "^3.58.1",
|
||||
"cross-fetch": "^4.1.0",
|
||||
"dotenv-cli": "^9.0.0",
|
||||
"msw": "^2.11.2"
|
||||
}
|
||||
@@ -6238,6 +6239,16 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
|
||||
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -12174,6 +12185,52 @@
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google.maps": "^3.58.1",
|
||||
"cross-fetch": "^4.1.0",
|
||||
"dotenv-cli": "^9.0.0",
|
||||
"msw": "^2.11.2"
|
||||
}
|
||||
|
||||
82
frontend/src/__mocks__/axios.ts
Normal file
82
frontend/src/__mocks__/axios.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Manual axios mock for Jest
|
||||
* This avoids ESM transformation issues with the axios package
|
||||
*/
|
||||
|
||||
const mockAxiosInstance = {
|
||||
get: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
post: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
put: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
delete: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
patch: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
request: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
interceptors: {
|
||||
request: {
|
||||
use: jest.fn(() => 0),
|
||||
eject: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
},
|
||||
response: {
|
||||
use: jest.fn(() => 0),
|
||||
eject: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
headers: {
|
||||
common: {},
|
||||
get: {},
|
||||
post: {},
|
||||
put: {},
|
||||
delete: {},
|
||||
patch: {},
|
||||
},
|
||||
baseURL: '',
|
||||
timeout: 0,
|
||||
withCredentials: false,
|
||||
},
|
||||
getUri: jest.fn(),
|
||||
head: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
options: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
postForm: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
putForm: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
patchForm: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
};
|
||||
|
||||
const axios = {
|
||||
...mockAxiosInstance,
|
||||
create: jest.fn(() => ({ ...mockAxiosInstance })),
|
||||
isAxiosError: jest.fn((error: any) => error?.isAxiosError === true),
|
||||
isCancel: jest.fn(() => false),
|
||||
all: jest.fn((promises: Promise<any>[]) => Promise.all(promises)),
|
||||
spread: jest.fn((callback: Function) => (arr: any[]) => callback(...arr)),
|
||||
toFormData: jest.fn(),
|
||||
formToJSON: jest.fn(),
|
||||
CancelToken: {
|
||||
source: jest.fn(() => ({
|
||||
token: {},
|
||||
cancel: jest.fn(),
|
||||
})),
|
||||
},
|
||||
Axios: jest.fn(),
|
||||
AxiosError: jest.fn(),
|
||||
Cancel: jest.fn(),
|
||||
CanceledError: jest.fn(),
|
||||
VERSION: '1.0.0',
|
||||
default: mockAxiosInstance,
|
||||
};
|
||||
|
||||
export default axios;
|
||||
export const AxiosError = class extends Error {
|
||||
isAxiosError = true;
|
||||
response?: any;
|
||||
request?: any;
|
||||
config?: any;
|
||||
code?: string;
|
||||
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = 'AxiosError';
|
||||
}
|
||||
};
|
||||
export type { AxiosRequestConfig } from 'axios';
|
||||
461
frontend/src/__tests__/contexts/AuthContext.test.tsx
Normal file
461
frontend/src/__tests__/contexts/AuthContext.test.tsx
Normal file
@@ -0,0 +1,461 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
|
||||
import { AuthProvider, useAuth } from '../../contexts/AuthContext';
|
||||
import { mockUser } from '../../mocks/handlers';
|
||||
|
||||
// Mock the API module
|
||||
jest.mock('../../services/api', () => {
|
||||
const mockAuthAPI = {
|
||||
login: jest.fn(),
|
||||
register: jest.fn(),
|
||||
googleLogin: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
getStatus: jest.fn(),
|
||||
};
|
||||
|
||||
const mockFetchCSRFToken = jest.fn().mockResolvedValue('test-csrf-token');
|
||||
const mockResetCSRFToken = jest.fn();
|
||||
|
||||
return {
|
||||
authAPI: mockAuthAPI,
|
||||
fetchCSRFToken: mockFetchCSRFToken,
|
||||
resetCSRFToken: mockResetCSRFToken,
|
||||
};
|
||||
});
|
||||
|
||||
// Get mocked modules
|
||||
import { authAPI, fetchCSRFToken, resetCSRFToken } from '../../services/api';
|
||||
|
||||
const mockAuthAPI = authAPI as jest.Mocked<typeof authAPI>;
|
||||
const mockFetchCSRFToken = fetchCSRFToken as jest.MockedFunction<typeof fetchCSRFToken>;
|
||||
const mockResetCSRFToken = resetCSRFToken as jest.MockedFunction<typeof resetCSRFToken>;
|
||||
|
||||
// Test component that uses the auth context
|
||||
const TestComponent: React.FC = () => {
|
||||
const auth = useAuth();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
|
||||
<div data-testid="user">{auth.user ? auth.user.email : 'no-user'}</div>
|
||||
<div data-testid="verified">{auth.user?.isVerified ? 'verified' : 'not-verified'}</div>
|
||||
<div data-testid="modal-open">{auth.showAuthModal ? 'open' : 'closed'}</div>
|
||||
<div data-testid="modal-mode">{auth.authModalMode}</div>
|
||||
<button onClick={() => auth.login('test@example.com', 'password123')}>Login</button>
|
||||
<button onClick={() => auth.register({ email: 'new@example.com', username: 'newuser', password: 'password123' })}>Register</button>
|
||||
<button onClick={() => auth.googleLogin('valid-google-code')}>Google Login</button>
|
||||
<button onClick={() => auth.logout()}>Logout</button>
|
||||
<button onClick={() => auth.openAuthModal('login')}>Open Login Modal</button>
|
||||
<button onClick={() => auth.openAuthModal('signup')}>Open Signup Modal</button>
|
||||
<button onClick={() => auth.closeAuthModal()}>Close Modal</button>
|
||||
<button onClick={() => auth.checkAuth()}>Check Auth</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Wrapper component for testing
|
||||
const renderWithAuth = (ui: React.ReactElement = <TestComponent />) => {
|
||||
return render(
|
||||
<AuthProvider>
|
||||
{ui}
|
||||
</AuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('AuthContext', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Default: user is authenticated
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: true, user: mockUser },
|
||||
});
|
||||
mockFetchCSRFToken.mockResolvedValue('test-csrf-token');
|
||||
});
|
||||
|
||||
describe('useAuth hook', () => {
|
||||
it('throws error when used outside AuthProvider', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
expect(() => render(<TestComponent />)).toThrow('useAuth must be used within an AuthProvider');
|
||||
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('starts with loading state', () => {
|
||||
renderWithAuth();
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('loading');
|
||||
});
|
||||
|
||||
it('checks authentication status on mount', async () => {
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
expect(mockAuthAPI.getStatus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets user to null when not authenticated', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
});
|
||||
|
||||
it('handles network errors gracefully', async () => {
|
||||
mockAuthAPI.getStatus.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Login', () => {
|
||||
it('logs in successfully with valid credentials', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
|
||||
mockAuthAPI.login.mockResolvedValue({
|
||||
data: { user: mockUser },
|
||||
});
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Login'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
|
||||
expect(mockAuthAPI.login).toHaveBeenCalledWith({
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps user as null when login fails', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
|
||||
mockAuthAPI.login.mockRejectedValue(new Error('Invalid credentials'));
|
||||
|
||||
// Create a test component that captures login errors
|
||||
const LoginErrorTestComponent: React.FC = () => {
|
||||
const auth = useAuth();
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
await auth.login('test@example.com', 'wrongpassword');
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
|
||||
<div data-testid="user">{auth.user ? auth.user.email : 'no-user'}</div>
|
||||
<div data-testid="error">{error || 'no-error'}</div>
|
||||
<button onClick={handleLogin}>Login</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<LoginErrorTestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Login'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('error')).toHaveTextContent('Invalid credentials');
|
||||
});
|
||||
|
||||
// User should still be null after failed login
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Registration', () => {
|
||||
it('registers a new user successfully', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
|
||||
mockAuthAPI.register.mockResolvedValue({
|
||||
data: { user: { ...mockUser, email: 'new@example.com', isVerified: false } },
|
||||
});
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Register'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('new@example.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google Login', () => {
|
||||
it('logs in with Google successfully', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
|
||||
mockAuthAPI.googleLogin.mockResolvedValue({
|
||||
data: { user: mockUser },
|
||||
});
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Google Login'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
|
||||
expect(mockAuthAPI.googleLogin).toHaveBeenCalledWith('valid-google-code');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logout', () => {
|
||||
it('logs out successfully', async () => {
|
||||
mockAuthAPI.logout.mockResolvedValue({ data: { message: 'Logged out' } });
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Logout'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
});
|
||||
|
||||
expect(mockResetCSRFToken).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clears user state even if logout API fails', async () => {
|
||||
mockAuthAPI.logout.mockRejectedValue(new Error('Server error'));
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Logout'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auth Modal', () => {
|
||||
it('opens login modal', async () => {
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('modal-open')).toHaveTextContent('closed');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Open Login Modal'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('modal-open')).toHaveTextContent('open');
|
||||
expect(screen.getByTestId('modal-mode')).toHaveTextContent('login');
|
||||
});
|
||||
|
||||
it('opens signup modal', async () => {
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Open Signup Modal'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('modal-open')).toHaveTextContent('open');
|
||||
expect(screen.getByTestId('modal-mode')).toHaveTextContent('signup');
|
||||
});
|
||||
|
||||
it('closes modal', async () => {
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Open Login Modal'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('modal-open')).toHaveTextContent('open');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Close Modal'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('modal-open')).toHaveTextContent('closed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('updates user state', async () => {
|
||||
const TestComponentWithUpdate: React.FC = () => {
|
||||
const auth = useAuth();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
|
||||
<div data-testid="user-email">{auth.user?.email || 'no-user'}</div>
|
||||
<div data-testid="user-name">{auth.user?.firstName || 'no-name'}</div>
|
||||
<button onClick={() => auth.updateUser({
|
||||
...auth.user!,
|
||||
firstName: 'Updated',
|
||||
lastName: 'Name',
|
||||
})}>
|
||||
Update User
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderWithAuth(<TestComponentWithUpdate />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user-name')).toHaveTextContent('Test');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Update User'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user-name')).toHaveTextContent('Updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkAuth', () => {
|
||||
it('refreshes authentication status', async () => {
|
||||
let callCount = 0;
|
||||
|
||||
mockAuthAPI.getStatus.mockImplementation(() => {
|
||||
callCount++;
|
||||
if (callCount === 1) {
|
||||
return Promise.resolve({ data: { authenticated: false, user: null } });
|
||||
}
|
||||
return Promise.resolve({ data: { authenticated: true, user: mockUser } });
|
||||
});
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Check Auth'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth Callback Handling', () => {
|
||||
it('skips auth check on OAuth callback page', async () => {
|
||||
// Mock being on the OAuth callback page
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
...window.location,
|
||||
pathname: '/auth/google/callback',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
mockAuthAPI.getStatus.mockClear();
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
// Status should not be called on OAuth callback page
|
||||
expect(mockAuthAPI.getStatus).not.toHaveBeenCalled();
|
||||
|
||||
// Reset location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
...window.location,
|
||||
pathname: '/',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
346
frontend/src/__tests__/hooks/useAddressAutocomplete.test.ts
Normal file
346
frontend/src/__tests__/hooks/useAddressAutocomplete.test.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useAddressAutocomplete, usStates } from '../../hooks/useAddressAutocomplete';
|
||||
import { PlaceDetails } from '../../services/placesService';
|
||||
|
||||
describe('useAddressAutocomplete', () => {
|
||||
describe('usStates', () => {
|
||||
it('contains all 50 US states', () => {
|
||||
expect(usStates).toHaveLength(50);
|
||||
});
|
||||
|
||||
it('includes common states', () => {
|
||||
expect(usStates).toContain('California');
|
||||
expect(usStates).toContain('New York');
|
||||
expect(usStates).toContain('Texas');
|
||||
expect(usStates).toContain('Florida');
|
||||
});
|
||||
|
||||
it('states are in alphabetical order', () => {
|
||||
const sorted = [...usStates].sort();
|
||||
expect(usStates).toEqual(sorted);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsePlace', () => {
|
||||
const { result } = renderHook(() => useAddressAutocomplete());
|
||||
|
||||
it('parses a complete place correctly', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '123 Main Street, Los Angeles, CA 90210, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '123',
|
||||
route: 'Main Street',
|
||||
locality: 'Los Angeles',
|
||||
administrativeAreaLevel1: 'CA',
|
||||
administrativeAreaLevel1Long: 'California',
|
||||
postalCode: '90210',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 34.0522,
|
||||
longitude: -118.2437,
|
||||
},
|
||||
placeId: 'test-place-id',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.address1).toBe('123 Main Street');
|
||||
expect(parsed!.city).toBe('Los Angeles');
|
||||
expect(parsed!.state).toBe('California');
|
||||
expect(parsed!.zipCode).toBe('90210');
|
||||
expect(parsed!.country).toBe('US');
|
||||
expect(parsed!.latitude).toBe(34.0522);
|
||||
expect(parsed!.longitude).toBe(-118.2437);
|
||||
});
|
||||
|
||||
it('converts state codes to full names', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '456 Oak Ave, New York, NY 10001, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '456',
|
||||
route: 'Oak Ave',
|
||||
locality: 'New York',
|
||||
administrativeAreaLevel1: 'NY',
|
||||
administrativeAreaLevel1Long: 'New York',
|
||||
postalCode: '10001',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 40.7128,
|
||||
longitude: -74.006,
|
||||
},
|
||||
placeId: 'test-place-id-2',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.state).toBe('New York');
|
||||
});
|
||||
|
||||
it('uses formatted address when street number and route are missing', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: 'Some Place, Austin, TX 78701, USA',
|
||||
addressComponents: {
|
||||
locality: 'Austin',
|
||||
administrativeAreaLevel1: 'TX',
|
||||
administrativeAreaLevel1Long: 'Texas',
|
||||
postalCode: '78701',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 30.2672,
|
||||
longitude: -97.7431,
|
||||
},
|
||||
placeId: 'test-place-id-3',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.address1).toBe('Some Place, Austin, TX 78701, USA');
|
||||
});
|
||||
|
||||
it('handles missing postal code gracefully', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '789 Pine St, Seattle, WA, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '789',
|
||||
route: 'Pine St',
|
||||
locality: 'Seattle',
|
||||
administrativeAreaLevel1: 'WA',
|
||||
administrativeAreaLevel1Long: 'Washington',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 47.6062,
|
||||
longitude: -122.3321,
|
||||
},
|
||||
placeId: 'test-place-id-4',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.zipCode).toBe('');
|
||||
expect(parsed!.state).toBe('Washington');
|
||||
});
|
||||
|
||||
it('handles missing city gracefully', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '100 Rural Road, CO 80000, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '100',
|
||||
route: 'Rural Road',
|
||||
administrativeAreaLevel1: 'CO',
|
||||
administrativeAreaLevel1Long: 'Colorado',
|
||||
postalCode: '80000',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 39.5501,
|
||||
longitude: -105.7821,
|
||||
},
|
||||
placeId: 'test-place-id-5',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.city).toBe('');
|
||||
});
|
||||
|
||||
it('sets state to empty string for unknown states', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '123 Street, City, XX 12345, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '123',
|
||||
route: 'Street',
|
||||
locality: 'City',
|
||||
administrativeAreaLevel1: 'XX',
|
||||
administrativeAreaLevel1Long: 'Unknown State',
|
||||
postalCode: '12345',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
},
|
||||
placeId: 'test-place-id-6',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.state).toBe('');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('State not found in dropdown options')
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('handles DC (District of Columbia) correctly', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '1600 Pennsylvania Ave, Washington, DC 20500, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '1600',
|
||||
route: 'Pennsylvania Ave',
|
||||
locality: 'Washington',
|
||||
administrativeAreaLevel1: 'DC',
|
||||
administrativeAreaLevel1Long: 'District of Columbia',
|
||||
postalCode: '20500',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 38.8977,
|
||||
longitude: -77.0365,
|
||||
},
|
||||
placeId: 'test-place-id-dc',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
// DC is not in the 50 states list, so state should be empty
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.city).toBe('Washington');
|
||||
});
|
||||
|
||||
it('uses long state name from administrativeAreaLevel1Long when available', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '500 Beach Blvd, Miami, FL 33101, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '500',
|
||||
route: 'Beach Blvd',
|
||||
locality: 'Miami',
|
||||
administrativeAreaLevel1: 'FL',
|
||||
administrativeAreaLevel1Long: 'Florida',
|
||||
postalCode: '33101',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 25.7617,
|
||||
longitude: -80.1918,
|
||||
},
|
||||
placeId: 'test-place-id-fl',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.state).toBe('Florida');
|
||||
});
|
||||
|
||||
it('returns null and logs error when parsing fails', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
// Pass an object that will cause an error when accessing nested properties
|
||||
const invalidPlace = {
|
||||
formattedAddress: 'Test',
|
||||
addressComponents: null,
|
||||
geometry: { latitude: 0, longitude: 0 },
|
||||
placeId: 'test',
|
||||
} as unknown as PlaceDetails;
|
||||
|
||||
const parsed = result.current.parsePlace(invalidPlace);
|
||||
|
||||
expect(parsed).toBeNull();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error parsing place details:',
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('handles only street number without route', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '42, Chicago, IL 60601, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '42',
|
||||
locality: 'Chicago',
|
||||
administrativeAreaLevel1: 'IL',
|
||||
administrativeAreaLevel1Long: 'Illinois',
|
||||
postalCode: '60601',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 41.8781,
|
||||
longitude: -87.6298,
|
||||
},
|
||||
placeId: 'test-place-id-number-only',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.address1).toBe('42');
|
||||
});
|
||||
|
||||
it('handles only route without street number', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: 'Main Street, Boston, MA 02101, USA',
|
||||
addressComponents: {
|
||||
route: 'Main Street',
|
||||
locality: 'Boston',
|
||||
administrativeAreaLevel1: 'MA',
|
||||
administrativeAreaLevel1Long: 'Massachusetts',
|
||||
postalCode: '02101',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 42.3601,
|
||||
longitude: -71.0589,
|
||||
},
|
||||
placeId: 'test-place-id-route-only',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.address1).toBe('Main Street');
|
||||
});
|
||||
|
||||
it('defaults country to US when not provided', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '999 Test St, Denver, CO 80202',
|
||||
addressComponents: {
|
||||
streetNumber: '999',
|
||||
route: 'Test St',
|
||||
locality: 'Denver',
|
||||
administrativeAreaLevel1: 'CO',
|
||||
administrativeAreaLevel1Long: 'Colorado',
|
||||
postalCode: '80202',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
},
|
||||
placeId: 'test-place-id-no-country',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.country).toBe('US');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hook stability', () => {
|
||||
it('returns stable parsePlace function', () => {
|
||||
const { result, rerender } = renderHook(() => useAddressAutocomplete());
|
||||
|
||||
const firstParsePlace = result.current.parsePlace;
|
||||
|
||||
rerender();
|
||||
|
||||
const secondParsePlace = result.current.parsePlace;
|
||||
|
||||
expect(firstParsePlace).toBe(secondParsePlace);
|
||||
});
|
||||
});
|
||||
});
|
||||
211
frontend/src/__tests__/services/api.test.ts
Normal file
211
frontend/src/__tests__/services/api.test.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* API Service Tests
|
||||
*
|
||||
* Tests the API service module structure and exported functions.
|
||||
* API interceptor behavior is tested in integration with AuthContext.
|
||||
*/
|
||||
|
||||
import {
|
||||
authAPI,
|
||||
userAPI,
|
||||
itemAPI,
|
||||
rentalAPI,
|
||||
messageAPI,
|
||||
mapsAPI,
|
||||
stripeAPI,
|
||||
addressAPI,
|
||||
conditionCheckAPI,
|
||||
forumAPI,
|
||||
feedbackAPI,
|
||||
fetchCSRFToken,
|
||||
resetCSRFToken,
|
||||
getMessageImageUrl,
|
||||
getForumImageUrl,
|
||||
} from '../../services/api';
|
||||
import api from '../../services/api';
|
||||
|
||||
describe('API Service', () => {
|
||||
describe('Module Exports', () => {
|
||||
it('exports authAPI with correct methods', () => {
|
||||
expect(authAPI).toBeDefined();
|
||||
expect(typeof authAPI.login).toBe('function');
|
||||
expect(typeof authAPI.register).toBe('function');
|
||||
expect(typeof authAPI.logout).toBe('function');
|
||||
expect(typeof authAPI.googleLogin).toBe('function');
|
||||
expect(typeof authAPI.getStatus).toBe('function');
|
||||
expect(typeof authAPI.verifyEmail).toBe('function');
|
||||
expect(typeof authAPI.forgotPassword).toBe('function');
|
||||
expect(typeof authAPI.resetPassword).toBe('function');
|
||||
});
|
||||
|
||||
it('exports userAPI with correct methods', () => {
|
||||
expect(userAPI).toBeDefined();
|
||||
expect(typeof userAPI.getProfile).toBe('function');
|
||||
expect(typeof userAPI.updateProfile).toBe('function');
|
||||
expect(typeof userAPI.uploadProfileImage).toBe('function');
|
||||
});
|
||||
|
||||
it('exports itemAPI with correct methods', () => {
|
||||
expect(itemAPI).toBeDefined();
|
||||
expect(typeof itemAPI.getItems).toBe('function');
|
||||
expect(typeof itemAPI.getItem).toBe('function');
|
||||
expect(typeof itemAPI.createItem).toBe('function');
|
||||
expect(typeof itemAPI.updateItem).toBe('function');
|
||||
expect(typeof itemAPI.deleteItem).toBe('function');
|
||||
});
|
||||
|
||||
it('exports rentalAPI with correct methods', () => {
|
||||
expect(rentalAPI).toBeDefined();
|
||||
expect(typeof rentalAPI.createRental).toBe('function');
|
||||
expect(typeof rentalAPI.getRentals).toBe('function');
|
||||
expect(typeof rentalAPI.getListings).toBe('function');
|
||||
expect(typeof rentalAPI.updateRentalStatus).toBe('function');
|
||||
expect(typeof rentalAPI.cancelRental).toBe('function');
|
||||
});
|
||||
|
||||
it('exports messageAPI with correct methods', () => {
|
||||
expect(messageAPI).toBeDefined();
|
||||
expect(typeof messageAPI.getMessages).toBe('function');
|
||||
expect(typeof messageAPI.getConversations).toBe('function');
|
||||
expect(typeof messageAPI.sendMessage).toBe('function');
|
||||
expect(typeof messageAPI.getUnreadCount).toBe('function');
|
||||
});
|
||||
|
||||
it('exports mapsAPI with correct methods', () => {
|
||||
expect(mapsAPI).toBeDefined();
|
||||
expect(typeof mapsAPI.placesAutocomplete).toBe('function');
|
||||
expect(typeof mapsAPI.placeDetails).toBe('function');
|
||||
expect(typeof mapsAPI.geocode).toBe('function');
|
||||
});
|
||||
|
||||
it('exports stripeAPI with correct methods', () => {
|
||||
expect(stripeAPI).toBeDefined();
|
||||
expect(typeof stripeAPI.getCheckoutSession).toBe('function');
|
||||
expect(typeof stripeAPI.createConnectedAccount).toBe('function');
|
||||
expect(typeof stripeAPI.createAccountLink).toBe('function');
|
||||
expect(typeof stripeAPI.getAccountStatus).toBe('function');
|
||||
});
|
||||
|
||||
it('exports CSRF token management functions', () => {
|
||||
expect(typeof fetchCSRFToken).toBe('function');
|
||||
expect(typeof resetCSRFToken).toBe('function');
|
||||
});
|
||||
|
||||
it('exports helper functions for image URLs', () => {
|
||||
expect(typeof getMessageImageUrl).toBe('function');
|
||||
expect(typeof getForumImageUrl).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Helper Functions', () => {
|
||||
it('getMessageImageUrl constructs correct URL', () => {
|
||||
const url = getMessageImageUrl('test-image.jpg');
|
||||
expect(url).toContain('/messages/images/test-image.jpg');
|
||||
});
|
||||
|
||||
it('getForumImageUrl constructs correct URL', () => {
|
||||
const url = getForumImageUrl('forum-image.jpg');
|
||||
expect(url).toContain('/uploads/forum/forum-image.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSRF Token Management', () => {
|
||||
it('resetCSRFToken clears the token', () => {
|
||||
// Should not throw
|
||||
expect(() => resetCSRFToken()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Configuration', () => {
|
||||
it('creates axios instance with correct base URL', () => {
|
||||
expect(api).toBeDefined();
|
||||
expect(api.defaults).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Namespaces', () => {
|
||||
describe('authAPI', () => {
|
||||
it('has all authentication methods', () => {
|
||||
const expectedMethods = [
|
||||
'register',
|
||||
'login',
|
||||
'googleLogin',
|
||||
'logout',
|
||||
'refresh',
|
||||
'getCSRFToken',
|
||||
'getStatus',
|
||||
'verifyEmail',
|
||||
'resendVerification',
|
||||
'forgotPassword',
|
||||
'verifyResetToken',
|
||||
'resetPassword',
|
||||
];
|
||||
|
||||
expectedMethods.forEach((method) => {
|
||||
expect((authAPI as any)[method]).toBeDefined();
|
||||
expect(typeof (authAPI as any)[method]).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addressAPI', () => {
|
||||
it('has all address management methods', () => {
|
||||
const expectedMethods = [
|
||||
'getAddresses',
|
||||
'createAddress',
|
||||
'updateAddress',
|
||||
'deleteAddress',
|
||||
];
|
||||
|
||||
expectedMethods.forEach((method) => {
|
||||
expect((addressAPI as any)[method]).toBeDefined();
|
||||
expect(typeof (addressAPI as any)[method]).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('conditionCheckAPI', () => {
|
||||
it('has all condition check methods', () => {
|
||||
const expectedMethods = [
|
||||
'submitConditionCheck',
|
||||
'getConditionChecks',
|
||||
'getConditionCheckTimeline',
|
||||
'getAvailableChecks',
|
||||
];
|
||||
|
||||
expectedMethods.forEach((method) => {
|
||||
expect((conditionCheckAPI as any)[method]).toBeDefined();
|
||||
expect(typeof (conditionCheckAPI as any)[method]).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('forumAPI', () => {
|
||||
it('has all forum methods', () => {
|
||||
const expectedMethods = [
|
||||
'getPosts',
|
||||
'getPost',
|
||||
'createPost',
|
||||
'updatePost',
|
||||
'deletePost',
|
||||
'createComment',
|
||||
'updateComment',
|
||||
'deleteComment',
|
||||
'getTags',
|
||||
];
|
||||
|
||||
expectedMethods.forEach((method) => {
|
||||
expect((forumAPI as any)[method]).toBeDefined();
|
||||
expect(typeof (forumAPI as any)[method]).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('feedbackAPI', () => {
|
||||
it('has feedback submission method', () => {
|
||||
expect(feedbackAPI.submitFeedback).toBeDefined();
|
||||
expect(typeof feedbackAPI.submitFeedback).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -437,9 +437,9 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
{/* Header */}
|
||||
<div className="bg-primary text-white p-3 d-flex align-items-center justify-content-between flex-shrink-0">
|
||||
<div className="d-flex align-items-center">
|
||||
{recipient.profileImage ? (
|
||||
{recipient.imageFilename ? (
|
||||
<img
|
||||
src={recipient.profileImage}
|
||||
src={recipient.imageFilename}
|
||||
alt={`${recipient.firstName} ${recipient.lastName}`}
|
||||
className="rounded-circle me-2"
|
||||
style={{ width: "35px", height: "35px", objectFit: "cover" }}
|
||||
@@ -525,10 +525,10 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{message.imagePath && (
|
||||
{message.imageFilename && (
|
||||
<div className="mb-2">
|
||||
<img
|
||||
src={getMessageImageUrl(message.imagePath)}
|
||||
src={getMessageImageUrl(message.imageFilename)}
|
||||
alt="Shared image"
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -539,7 +539,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
}}
|
||||
onClick={() =>
|
||||
window.open(
|
||||
getMessageImageUrl(message.imagePath!),
|
||||
getMessageImageUrl(message.imageFilename!),
|
||||
"_blank"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -212,9 +212,9 @@ const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
<p className="card-text mb-2" style={{ whiteSpace: "pre-wrap" }}>
|
||||
{comment.content}
|
||||
</p>
|
||||
{comment.images && comment.images.length > 0 && (
|
||||
{comment.imageFilenames && comment.imageFilenames.length > 0 && (
|
||||
<div className="row g-2 mb-2">
|
||||
{comment.images.map((image, index) => (
|
||||
{comment.imageFilenames.map((image, index) => (
|
||||
<div key={index} className="col-4 col-md-3">
|
||||
<img
|
||||
src={getForumImageUrl(image)}
|
||||
|
||||
@@ -47,9 +47,9 @@ const ItemCard: React.FC<ItemCardProps> = ({
|
||||
return (
|
||||
<Link to={`/items/${item.id}`} className="text-decoration-none">
|
||||
<div className="card h-100" style={{ cursor: 'pointer' }}>
|
||||
{item.images && item.images[0] ? (
|
||||
{item.imageFilenames && item.imageFilenames[0] ? (
|
||||
<img
|
||||
src={item.images[0]}
|
||||
src={item.imageFilenames[0]}
|
||||
className="card-img-top"
|
||||
alt={item.name}
|
||||
style={{
|
||||
|
||||
@@ -29,9 +29,9 @@ const ItemMarkerInfo: React.FC<ItemMarkerInfoProps> = ({ item, onViewDetails })
|
||||
return (
|
||||
<div style={{ width: 'min(280px, 90vw)', maxWidth: '280px' }}>
|
||||
<div className="card border-0">
|
||||
{item.images && item.images[0] ? (
|
||||
{item.imageFilenames && item.imageFilenames[0] ? (
|
||||
<img
|
||||
src={item.images[0]}
|
||||
src={item.imageFilenames[0]}
|
||||
className="card-img-top"
|
||||
alt={item.name}
|
||||
style={{
|
||||
|
||||
@@ -85,9 +85,9 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
||||
onClick={() => rental.renter && navigate(`/users/${rental.renterId}`)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{rental.renter?.profileImage ? (
|
||||
{rental.renter?.imageFilename ? (
|
||||
<img
|
||||
src={rental.renter.profileImage}
|
||||
src={rental.renter.imageFilename}
|
||||
alt={`${rental.renter.firstName} ${rental.renter.lastName}`}
|
||||
className="rounded-circle"
|
||||
style={{
|
||||
|
||||
@@ -102,9 +102,9 @@ const ReviewItemModal: React.FC<ReviewItemModalProps> = ({
|
||||
{rental.owner && rental.item && (
|
||||
<div className="mb-4 text-center">
|
||||
<div className="d-flex justify-content-center mb-3">
|
||||
{rental.owner.profileImage ? (
|
||||
{rental.owner.imageFilename ? (
|
||||
<img
|
||||
src={rental.owner.profileImage}
|
||||
src={rental.owner.imageFilename}
|
||||
alt={`${rental.owner.firstName} ${rental.owner.lastName}`}
|
||||
className="rounded-circle"
|
||||
style={{
|
||||
|
||||
@@ -102,9 +102,9 @@ const ReviewRenterModal: React.FC<ReviewRenterModalProps> = ({
|
||||
{rental.renter && rental.item && (
|
||||
<div className="mb-4 text-center">
|
||||
<div className="d-flex justify-content-center mb-3">
|
||||
{rental.renter.profileImage ? (
|
||||
{rental.renter.imageFilename ? (
|
||||
<img
|
||||
src={rental.renter.profileImage}
|
||||
src={rental.renter.imageFilename}
|
||||
alt={`${rental.renter.firstName} ${rental.renter.lastName}`}
|
||||
className="rounded-circle"
|
||||
style={{
|
||||
|
||||
72
frontend/src/mocks/handlers.ts
Normal file
72
frontend/src/mocks/handlers.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Mock data for tests
|
||||
*/
|
||||
|
||||
// Mock user data
|
||||
export const mockUser = {
|
||||
id: '1',
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isVerified: true,
|
||||
role: 'user' as const,
|
||||
};
|
||||
|
||||
export const mockUnverifiedUser = {
|
||||
...mockUser,
|
||||
id: '2',
|
||||
email: 'unverified@example.com',
|
||||
isVerified: false,
|
||||
};
|
||||
|
||||
// Mock item data
|
||||
export const mockItem = {
|
||||
id: '1',
|
||||
name: 'Test Item',
|
||||
description: 'A test item for rental',
|
||||
pricePerDay: 25,
|
||||
replacementCost: 500,
|
||||
condition: 'excellent' as const,
|
||||
isAvailable: true,
|
||||
images: ['image1.jpg'],
|
||||
ownerId: '2',
|
||||
city: 'Test City',
|
||||
state: 'California',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Mock rental data
|
||||
export const mockRental = {
|
||||
id: '1',
|
||||
itemId: '1',
|
||||
renterId: '1',
|
||||
ownerId: '2',
|
||||
startDateTime: new Date().toISOString(),
|
||||
endDateTime: new Date(Date.now() + 86400000).toISOString(),
|
||||
totalAmount: 25,
|
||||
status: 'pending' as const,
|
||||
paymentStatus: 'pending' as const,
|
||||
deliveryMethod: 'pickup' as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Mock API response helpers
|
||||
export const createMockResponse = <T>(data: T, status = 200) => ({
|
||||
data,
|
||||
status,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: {},
|
||||
});
|
||||
|
||||
export const createMockError = (message: string, status: number, code?: string) => {
|
||||
const error = new Error(message) as any;
|
||||
error.response = {
|
||||
status,
|
||||
data: { message, code },
|
||||
};
|
||||
return error;
|
||||
};
|
||||
43
frontend/src/mocks/server.ts
Normal file
43
frontend/src/mocks/server.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Mock server using Jest mocks instead of MSW.
|
||||
* This provides a simpler setup that works with all Node versions.
|
||||
*/
|
||||
|
||||
import { mockUser, mockUnverifiedUser, mockItem, mockRental } from './handlers';
|
||||
|
||||
// Re-export mock data
|
||||
export { mockUser, mockUnverifiedUser, mockItem, mockRental };
|
||||
|
||||
// Mock server interface for compatibility with setup
|
||||
export const server = {
|
||||
listen: jest.fn(),
|
||||
resetHandlers: jest.fn(),
|
||||
close: jest.fn(),
|
||||
use: jest.fn(),
|
||||
};
|
||||
|
||||
// Setup axios mock
|
||||
jest.mock('axios', () => {
|
||||
const mockAxiosInstance = {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
interceptors: {
|
||||
request: { use: jest.fn(), eject: jest.fn() },
|
||||
response: { use: jest.fn(), eject: jest.fn() },
|
||||
},
|
||||
defaults: {
|
||||
headers: {
|
||||
common: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
create: jest.fn(() => mockAxiosInstance),
|
||||
default: mockAxiosInstance,
|
||||
...mockAxiosInstance,
|
||||
};
|
||||
});
|
||||
@@ -162,8 +162,8 @@ const EditItem: React.FC = () => {
|
||||
});
|
||||
|
||||
// Set existing images as previews
|
||||
if (item.images && item.images.length > 0) {
|
||||
setImagePreviews(item.images);
|
||||
if (item.imageFilenames && item.imageFilenames.length > 0) {
|
||||
setImagePreviews(item.imageFilenames);
|
||||
}
|
||||
|
||||
// Determine which pricing unit to select based on existing data
|
||||
|
||||
@@ -343,9 +343,9 @@ const ForumPostDetail: React.FC = () => {
|
||||
{post.content}
|
||||
</div>
|
||||
|
||||
{post.images && post.images.length > 0 && (
|
||||
{post.imageFilenames && post.imageFilenames.length > 0 && (
|
||||
<div className="row g-2 mb-3">
|
||||
{post.images.map((image, index) => (
|
||||
{post.imageFilenames.map((image, index) => (
|
||||
<div key={index} className="col-6 col-md-4">
|
||||
<img
|
||||
src={getForumImageUrl(image)}
|
||||
|
||||
@@ -414,10 +414,10 @@ const ItemDetail: React.FC = () => {
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
{/* Images */}
|
||||
{item.images.length > 0 ? (
|
||||
{item.imageFilenames.length > 0 ? (
|
||||
<div className="mb-4">
|
||||
<img
|
||||
src={item.images[selectedImage]}
|
||||
src={item.imageFilenames[selectedImage]}
|
||||
alt={item.name}
|
||||
className="img-fluid rounded mb-3"
|
||||
style={{
|
||||
@@ -426,9 +426,9 @@ const ItemDetail: React.FC = () => {
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
{item.images.length > 1 && (
|
||||
{item.imageFilenames.length > 1 && (
|
||||
<div className="d-flex gap-2 overflow-auto justify-content-center">
|
||||
{item.images.map((image, index) => (
|
||||
{item.imageFilenames.map((image, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={image}
|
||||
@@ -478,9 +478,9 @@ const ItemDetail: React.FC = () => {
|
||||
onClick={() => navigate(`/users/${item.ownerId}`)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{item.owner.profileImage ? (
|
||||
{item.owner.imageFilename ? (
|
||||
<img
|
||||
src={item.owner.profileImage}
|
||||
src={item.owner.imageFilename}
|
||||
alt={`${item.owner.firstName} ${item.owner.lastName}`}
|
||||
className="rounded-circle me-2"
|
||||
style={{
|
||||
|
||||
@@ -230,9 +230,9 @@ const Messages: React.FC = () => {
|
||||
<div className="d-flex w-100 justify-content-between align-items-start">
|
||||
<div className="d-flex align-items-center flex-grow-1">
|
||||
{/* Profile Picture */}
|
||||
{conversation.partner.profileImage ? (
|
||||
{conversation.partner.imageFilename ? (
|
||||
<img
|
||||
src={conversation.partner.profileImage}
|
||||
src={conversation.partner.imageFilename}
|
||||
alt={`${conversation.partner.firstName} ${conversation.partner.lastName}`}
|
||||
className="rounded-circle me-3"
|
||||
style={{
|
||||
|
||||
@@ -306,9 +306,9 @@ const Owning: React.FC = () => {
|
||||
{allOwnerRentals.map((rental) => (
|
||||
<div key={rental.id} className="col-md-6 col-lg-4 mb-4">
|
||||
<div className="card h-100">
|
||||
{rental.item?.images && rental.item.images[0] && (
|
||||
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
|
||||
<img
|
||||
src={rental.item.images[0]}
|
||||
src={rental.item.imageFilenames[0]}
|
||||
className="card-img-top"
|
||||
alt={rental.item.name}
|
||||
style={{ height: "200px", objectFit: "cover" }}
|
||||
@@ -527,9 +527,9 @@ const Owning: React.FC = () => {
|
||||
navigate(`/items/${item.id}`);
|
||||
}}
|
||||
>
|
||||
{item.images && item.images[0] && (
|
||||
{item.imageFilenames && item.imageFilenames[0] && (
|
||||
<img
|
||||
src={item.images[0]}
|
||||
src={item.imageFilenames[0]}
|
||||
className="card-img-top"
|
||||
alt={item.name}
|
||||
style={{ height: "200px", objectFit: "cover" }}
|
||||
|
||||
@@ -39,7 +39,7 @@ const Profile: React.FC = () => {
|
||||
state: string;
|
||||
zipCode: string;
|
||||
country: string;
|
||||
profileImage: string;
|
||||
imageFilename: string;
|
||||
itemRequestNotificationRadius: number | null;
|
||||
}>({
|
||||
firstName: "",
|
||||
@@ -52,7 +52,7 @@ const Profile: React.FC = () => {
|
||||
state: "",
|
||||
zipCode: "",
|
||||
country: "",
|
||||
profileImage: "",
|
||||
imageFilename: "",
|
||||
itemRequestNotificationRadius: 10,
|
||||
});
|
||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||
@@ -156,12 +156,12 @@ const Profile: React.FC = () => {
|
||||
state: response.data.state || "",
|
||||
zipCode: response.data.zipCode || "",
|
||||
country: response.data.country || "",
|
||||
profileImage: response.data.profileImage || "",
|
||||
imageFilename: response.data.imageFilename || "",
|
||||
itemRequestNotificationRadius:
|
||||
response.data.itemRequestNotificationRadius || 10,
|
||||
});
|
||||
if (response.data.profileImage) {
|
||||
setImagePreview(getImageUrl(response.data.profileImage));
|
||||
if (response.data.imageFilename) {
|
||||
setImagePreview(getImageUrl(response.data.imageFilename));
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Failed to fetch profile");
|
||||
@@ -304,14 +304,14 @@ const Profile: React.FC = () => {
|
||||
// Upload image immediately
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("profileImage", file);
|
||||
formData.append("imageFilename", file);
|
||||
|
||||
const response = await userAPI.uploadProfileImage(formData);
|
||||
|
||||
// Update the profileImage in formData with the new filename
|
||||
// Update the imageFilename in formData with the new filename
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
profileImage: response.data.filename,
|
||||
imageFilename: response.data.filename,
|
||||
}));
|
||||
|
||||
// Update preview to use the uploaded image URL
|
||||
@@ -322,8 +322,8 @@ const Profile: React.FC = () => {
|
||||
// Reset on error
|
||||
setImageFile(null);
|
||||
setImagePreview(
|
||||
profileData?.profileImage
|
||||
? getImageUrl(profileData.profileImage)
|
||||
profileData?.imageFilename
|
||||
? getImageUrl(profileData.imageFilename)
|
||||
: null
|
||||
);
|
||||
}
|
||||
@@ -336,8 +336,8 @@ const Profile: React.FC = () => {
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
// Don't send profileImage in the update data as it's handled separately
|
||||
const { profileImage, ...updateData } = formData;
|
||||
// Don't send imageFilename in the update data as it's handled separately
|
||||
const { imageFilename, ...updateData } = formData;
|
||||
|
||||
const response = await userAPI.updateProfile(updateData);
|
||||
setProfileData(response.data);
|
||||
@@ -379,12 +379,12 @@ const Profile: React.FC = () => {
|
||||
state: profileData.state || "",
|
||||
zipCode: profileData.zipCode || "",
|
||||
country: profileData.country || "",
|
||||
profileImage: profileData.profileImage || "",
|
||||
imageFilename: profileData.imageFilename || "",
|
||||
itemRequestNotificationRadius:
|
||||
profileData.itemRequestNotificationRadius || 10,
|
||||
});
|
||||
setImagePreview(
|
||||
profileData.profileImage ? getImageUrl(profileData.profileImage) : null
|
||||
profileData.imageFilename ? getImageUrl(profileData.imageFilename) : null
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -774,7 +774,7 @@ const Profile: React.FC = () => {
|
||||
)}
|
||||
{editing && (
|
||||
<label
|
||||
htmlFor="profileImageOverview"
|
||||
htmlFor="imageFilenameOverview"
|
||||
className="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle"
|
||||
style={{
|
||||
width: "35px",
|
||||
@@ -785,7 +785,7 @@ const Profile: React.FC = () => {
|
||||
<i className="bi bi-camera-fill"></i>
|
||||
<input
|
||||
type="file"
|
||||
id="profileImageOverview"
|
||||
id="imageFilenameOverview"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className="d-none"
|
||||
@@ -1222,9 +1222,9 @@ const Profile: React.FC = () => {
|
||||
className="col-md-6 col-lg-4 mb-4"
|
||||
>
|
||||
<div className="card h-100">
|
||||
{rental.item?.images && rental.item.images[0] && (
|
||||
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
|
||||
<img
|
||||
src={rental.item.images[0]}
|
||||
src={rental.item.imageFilenames[0]}
|
||||
className="card-img-top"
|
||||
alt={rental.item.name}
|
||||
style={{
|
||||
@@ -1359,9 +1359,9 @@ const Profile: React.FC = () => {
|
||||
className="col-md-6 col-lg-4 mb-4"
|
||||
>
|
||||
<div className="card h-100">
|
||||
{rental.item?.images && rental.item.images[0] && (
|
||||
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
|
||||
<img
|
||||
src={rental.item.images[0]}
|
||||
src={rental.item.imageFilenames[0]}
|
||||
className="card-img-top"
|
||||
alt={rental.item.name}
|
||||
style={{
|
||||
|
||||
@@ -71,9 +71,9 @@ const PublicProfile: React.FC = () => {
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="text-center mb-4">
|
||||
{user.profileImage ? (
|
||||
{user.imageFilename ? (
|
||||
<img
|
||||
src={user.profileImage}
|
||||
src={user.imageFilename}
|
||||
alt={`${user.firstName} ${user.lastName}`}
|
||||
className="rounded-circle mb-3"
|
||||
style={{ width: '150px', height: '150px', objectFit: 'cover' }}
|
||||
@@ -111,9 +111,9 @@ const PublicProfile: React.FC = () => {
|
||||
onClick={() => navigate(`/items/${item.id}`)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{item.images.length > 0 ? (
|
||||
{item.imageFilenames.length > 0 ? (
|
||||
<img
|
||||
src={item.images[0]}
|
||||
src={item.imageFilenames[0]}
|
||||
className="card-img-top"
|
||||
alt={item.name}
|
||||
style={{ height: '200px', objectFit: 'cover' }}
|
||||
|
||||
@@ -341,9 +341,9 @@ const RentItem: React.FC = () => {
|
||||
<div className="col-md-4">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
{item.images && item.images[0] && (
|
||||
{item.imageFilenames && item.imageFilenames[0] && (
|
||||
<img
|
||||
src={item.images[0]}
|
||||
src={item.imageFilenames[0]}
|
||||
alt={item.name}
|
||||
className="img-fluid rounded mb-3"
|
||||
style={{
|
||||
|
||||
@@ -230,9 +230,9 @@ const Renting: React.FC = () => {
|
||||
className="card h-100"
|
||||
style={{ cursor: rental.item ? "pointer" : "default" }}
|
||||
>
|
||||
{rental.item?.images && rental.item.images[0] && (
|
||||
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
|
||||
<img
|
||||
src={rental.item.images[0]}
|
||||
src={rental.item.imageFilenames[0]}
|
||||
className="card-img-top"
|
||||
alt={rental.item.name}
|
||||
style={{ height: "200px", objectFit: "cover" }}
|
||||
|
||||
@@ -3,3 +3,53 @@
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Mock window.location for tests that use navigation
|
||||
const mockLocation = {
|
||||
...window.location,
|
||||
href: 'http://localhost:3000',
|
||||
pathname: '/',
|
||||
assign: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: mockLocation,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// Suppress console errors during tests (optional, comment out for debugging)
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
beforeAll(() => {
|
||||
console.error = (...args: any[]) => {
|
||||
// Filter out known React warnings during tests
|
||||
if (
|
||||
typeof args[0] === 'string' &&
|
||||
(args[0].includes('Warning: ReactDOM.render is no longer supported') ||
|
||||
args[0].includes('Warning: An update to') ||
|
||||
args[0].includes('act(...)'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalConsoleError.call(console, ...args);
|
||||
};
|
||||
|
||||
console.warn = (...args: any[]) => {
|
||||
// Filter out known warnings
|
||||
if (
|
||||
typeof args[0] === 'string' &&
|
||||
args[0].includes('componentWillReceiveProps')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalConsoleWarn.call(console, ...args);
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.error = originalConsoleError;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
26
frontend/src/test-polyfills.js
Normal file
26
frontend/src/test-polyfills.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Polyfills for MSW 2.x - must be loaded before MSW
|
||||
const { TextEncoder, TextDecoder } = require('util');
|
||||
|
||||
global.TextEncoder = TextEncoder;
|
||||
global.TextDecoder = TextDecoder;
|
||||
|
||||
// Polyfill for fetch, Request, Response, Headers
|
||||
const { fetch, Headers, Request, Response } = require('cross-fetch');
|
||||
|
||||
global.fetch = fetch;
|
||||
global.Headers = Headers;
|
||||
global.Request = Request;
|
||||
global.Response = Response;
|
||||
|
||||
// BroadcastChannel polyfill for MSW
|
||||
class BroadcastChannel {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
postMessage() {}
|
||||
close() {}
|
||||
addEventListener() {}
|
||||
removeEventListener() {}
|
||||
}
|
||||
|
||||
global.BroadcastChannel = BroadcastChannel;
|
||||
@@ -27,7 +27,7 @@ export interface User {
|
||||
state?: string;
|
||||
zipCode?: string;
|
||||
country?: string;
|
||||
profileImage?: string;
|
||||
imageFilename?: string;
|
||||
isVerified: boolean;
|
||||
role?: "user" | "admin";
|
||||
stripeConnectedAccountId?: string;
|
||||
@@ -41,7 +41,7 @@ export interface Message {
|
||||
receiverId: string;
|
||||
content: string;
|
||||
isRead: boolean;
|
||||
imagePath?: string;
|
||||
imageFilename?: string;
|
||||
sender?: User;
|
||||
receiver?: User;
|
||||
createdAt: string;
|
||||
@@ -84,7 +84,7 @@ export interface Item {
|
||||
country?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
images: string[];
|
||||
imageFilenames: string[];
|
||||
condition: "excellent" | "good" | "fair" | "poor";
|
||||
isAvailable: boolean;
|
||||
rules?: string;
|
||||
@@ -187,7 +187,7 @@ export interface ConditionCheck {
|
||||
| "rental_start_renter"
|
||||
| "rental_end_renter"
|
||||
| "post_rental_owner";
|
||||
photos: string[];
|
||||
imageFilenames: string[];
|
||||
notes?: string;
|
||||
submittedBy: string;
|
||||
submittedAt: string;
|
||||
@@ -212,7 +212,7 @@ export interface DamageAssessment {
|
||||
needsReplacement: boolean;
|
||||
replacementCost?: number;
|
||||
proofOfOwnership?: string[];
|
||||
photos?: string[];
|
||||
imageFilenames?: string[];
|
||||
assessedAt: string;
|
||||
assessedBy: string;
|
||||
feeCalculation: {
|
||||
@@ -265,7 +265,7 @@ export interface ForumPost {
|
||||
commentCount: number;
|
||||
isPinned: boolean;
|
||||
acceptedAnswerId?: string;
|
||||
images?: string[];
|
||||
imageFilenames?: string[];
|
||||
isDeleted?: boolean;
|
||||
deletedBy?: string;
|
||||
deletedAt?: string;
|
||||
@@ -287,7 +287,7 @@ export interface ForumComment {
|
||||
content: string;
|
||||
parentCommentId?: string;
|
||||
isDeleted: boolean;
|
||||
images?: string[];
|
||||
imageFilenames?: string[];
|
||||
deletedBy?: string;
|
||||
deletedAt?: string;
|
||||
author?: User;
|
||||
|
||||
Reference in New Issue
Block a user