migration to vite and cleaned up /uploads
This commit is contained in:
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -1,6 +1,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
uploads/
|
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -11,6 +11,9 @@
|
|||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
.vite
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Village Share - Life is too expensive. Rent or borrow from your neighbors"
|
content="Village Share - Life is too expensive. Rent or borrow from your neighbors"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<title>Village Share</title>
|
<title>Village Share</title>
|
||||||
<link
|
<link
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||||
@@ -23,15 +23,6 @@
|
|||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!--
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
preset: 'react-app',
|
|
||||||
collectCoverageFrom: [
|
|
||||||
'src/**/*.{js,jsx,ts,tsx}',
|
|
||||||
'!src/index.tsx',
|
|
||||||
'!src/reportWebVitals.ts',
|
|
||||||
'!src/**/*.d.ts',
|
|
||||||
'!src/setupTests.ts',
|
|
||||||
'!src/test-polyfills.js',
|
|
||||||
'!src/mocks/**'
|
|
||||||
],
|
|
||||||
coverageReporters: ['text', 'lcov', 'html'],
|
|
||||||
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
|
|
||||||
testMatch: [
|
|
||||||
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
|
|
||||||
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
|
|
||||||
],
|
|
||||||
transformIgnorePatterns: [
|
|
||||||
'/node_modules/(?!(axios|@stripe)/).*'
|
|
||||||
],
|
|
||||||
moduleNameMapper: {
|
|
||||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
|
|
||||||
},
|
|
||||||
testTimeout: 10000,
|
|
||||||
coverageThreshold: {
|
|
||||||
global: {
|
|
||||||
lines: 80,
|
|
||||||
statements: 80
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
18090
frontend/package-lock.json
generated
18090
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googlemaps/js-api-loader": "^1.16.10",
|
"@googlemaps/js-api-loader": "^1.16.10",
|
||||||
"@stripe/connect-js": "^3.3.31",
|
"@stripe/connect-js": "^3.3.31",
|
||||||
@@ -12,8 +13,7 @@
|
|||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/node": "^20.0.0",
|
||||||
"@types/node": "^16.18.126",
|
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
@@ -24,51 +24,32 @@
|
|||||||
"react-datepicker": "^9.1.0",
|
"react-datepicker": "^9.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^6.30.1",
|
"react-router-dom": "^6.30.1",
|
||||||
"react-scripts": "^5.0.1",
|
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"stripe": "^18.4.0",
|
"stripe": "^18.4.0",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5"
|
||||||
"web-vitals": "^2.1.4"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start:dev": "dotenv -e .env.dev react-scripts start",
|
"start:dev": "vite --mode dev",
|
||||||
"start:qa": "dotenv -e .env.qa react-scripts start",
|
"start:qa": "vite --mode qa",
|
||||||
"start:prod": "dotenv -e .env.prod react-scripts start",
|
"start:prod": "vite --mode prod",
|
||||||
"build:dev": "dotenv -e .env.dev react-scripts build",
|
"build:dev": "vite build --mode dev",
|
||||||
"build:qa": "dotenv -e .env.qa react-scripts build",
|
"build:qa": "vite build --mode qa",
|
||||||
"build:prod": "dotenv -e .env.prod react-scripts build",
|
"build:prod": "vite build --mode prod",
|
||||||
"test": "react-scripts test",
|
"preview": "vite preview",
|
||||||
"eject": "react-scripts eject",
|
"test": "vitest run",
|
||||||
"test:coverage": "react-scripts test --coverage --watchAll=false --maxWorkers=4"
|
"test:watch": "vitest",
|
||||||
},
|
"test:coverage": "vitest run --coverage"
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/google.maps": "^3.58.1",
|
"@types/google.maps": "^3.58.1",
|
||||||
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
|
"@vitest/coverage-v8": "^4.0.17",
|
||||||
"cross-fetch": "^4.1.0",
|
"cross-fetch": "^4.1.0",
|
||||||
"dotenv-cli": "^9.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"msw": "^2.11.2"
|
"jsdom": "^27.4.0",
|
||||||
},
|
"msw": "^2.11.2",
|
||||||
"overrides": {
|
"vite": "^7.3.1",
|
||||||
"nth-check": "^2.1.1",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"postcss": "^8.4.31",
|
"vitest": "^4.0.17"
|
||||||
"svgo": "^3.0.0",
|
|
||||||
"webpack-dev-server": "^5.2.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import PrivateRoute from './components/PrivateRoute';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5001';
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5001';
|
||||||
|
|
||||||
const AppContent: React.FC = () => {
|
const AppContent: React.FC = () => {
|
||||||
const { showAuthModal, authModalMode, closeAuthModal, user } = useAuth();
|
const { showAuthModal, authModalMode, closeAuthModal, user } = useAuth();
|
||||||
@@ -77,7 +77,7 @@ const AppContent: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAlphaAccess = async () => {
|
const checkAlphaAccess = async () => {
|
||||||
// Bypass alpha access check if feature is disabled
|
// Bypass alpha access check if feature is disabled
|
||||||
if (process.env.REACT_APP_ALPHA_TESTING_ENABLED !== 'true') {
|
if (import.meta.env.VITE_ALPHA_TESTING_ENABLED !== 'true') {
|
||||||
setHasAlphaAccess(true);
|
setHasAlphaAccess(true);
|
||||||
setCheckingAccess(false);
|
setCheckingAccess(false);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
/**
|
/**
|
||||||
* Manual axios mock for Jest
|
* Manual axios mock for Vitest
|
||||||
* This avoids ESM transformation issues with the axios package
|
* This avoids ESM transformation issues with the axios package
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
const mockAxiosInstance = {
|
const mockAxiosInstance = {
|
||||||
get: jest.fn(() => Promise.resolve({ data: {} })),
|
get: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
post: jest.fn(() => Promise.resolve({ data: {} })),
|
post: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
put: jest.fn(() => Promise.resolve({ data: {} })),
|
put: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
delete: jest.fn(() => Promise.resolve({ data: {} })),
|
delete: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
patch: jest.fn(() => Promise.resolve({ data: {} })),
|
patch: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
request: jest.fn(() => Promise.resolve({ data: {} })),
|
request: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
interceptors: {
|
interceptors: {
|
||||||
request: {
|
request: {
|
||||||
use: jest.fn(() => 0),
|
use: vi.fn(() => 0),
|
||||||
eject: jest.fn(),
|
eject: vi.fn(),
|
||||||
clear: jest.fn(),
|
clear: vi.fn(),
|
||||||
},
|
},
|
||||||
response: {
|
response: {
|
||||||
use: jest.fn(() => 0),
|
use: vi.fn(() => 0),
|
||||||
eject: jest.fn(),
|
eject: vi.fn(),
|
||||||
clear: jest.fn(),
|
clear: vi.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -35,33 +37,33 @@ const mockAxiosInstance = {
|
|||||||
timeout: 0,
|
timeout: 0,
|
||||||
withCredentials: false,
|
withCredentials: false,
|
||||||
},
|
},
|
||||||
getUri: jest.fn(),
|
getUri: vi.fn(),
|
||||||
head: jest.fn(() => Promise.resolve({ data: {} })),
|
head: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
options: jest.fn(() => Promise.resolve({ data: {} })),
|
options: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
postForm: jest.fn(() => Promise.resolve({ data: {} })),
|
postForm: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
putForm: jest.fn(() => Promise.resolve({ data: {} })),
|
putForm: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
patchForm: jest.fn(() => Promise.resolve({ data: {} })),
|
patchForm: vi.fn(() => Promise.resolve({ data: {} })),
|
||||||
};
|
};
|
||||||
|
|
||||||
const axios = {
|
const axios = {
|
||||||
...mockAxiosInstance,
|
...mockAxiosInstance,
|
||||||
create: jest.fn(() => ({ ...mockAxiosInstance })),
|
create: vi.fn(() => ({ ...mockAxiosInstance })),
|
||||||
isAxiosError: jest.fn((error: any) => error?.isAxiosError === true),
|
isAxiosError: vi.fn((error: any) => error?.isAxiosError === true),
|
||||||
isCancel: jest.fn(() => false),
|
isCancel: vi.fn(() => false),
|
||||||
all: jest.fn((promises: Promise<any>[]) => Promise.all(promises)),
|
all: vi.fn((promises: Promise<any>[]) => Promise.all(promises)),
|
||||||
spread: jest.fn((callback: Function) => (arr: any[]) => callback(...arr)),
|
spread: vi.fn((callback: Function) => (arr: any[]) => callback(...arr)),
|
||||||
toFormData: jest.fn(),
|
toFormData: vi.fn(),
|
||||||
formToJSON: jest.fn(),
|
formToJSON: vi.fn(),
|
||||||
CancelToken: {
|
CancelToken: {
|
||||||
source: jest.fn(() => ({
|
source: vi.fn(() => ({
|
||||||
token: {},
|
token: {},
|
||||||
cancel: jest.fn(),
|
cancel: vi.fn(),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
Axios: jest.fn(),
|
Axios: vi.fn(),
|
||||||
AxiosError: jest.fn(),
|
AxiosError: vi.fn(),
|
||||||
Cancel: jest.fn(),
|
Cancel: vi.fn(),
|
||||||
CanceledError: jest.fn(),
|
CanceledError: vi.fn(),
|
||||||
VERSION: '1.0.0',
|
VERSION: '1.0.0',
|
||||||
default: mockAxiosInstance,
|
default: mockAxiosInstance,
|
||||||
};
|
};
|
||||||
|
|||||||
1
frontend/src/__mocks__/fileMock.js
Normal file
1
frontend/src/__mocks__/fileMock.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = 'test-file-stub';
|
||||||
@@ -8,31 +8,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { vi } from 'vitest';
|
||||||
import AuthModal from '../../components/AuthModal';
|
import AuthModal from '../../components/AuthModal';
|
||||||
|
|
||||||
// Mock the auth context
|
// Mock the auth context
|
||||||
const mockLogin = jest.fn();
|
const mockLogin = vi.fn();
|
||||||
const mockRegister = jest.fn();
|
const mockRegister = vi.fn();
|
||||||
|
|
||||||
jest.mock('../../contexts/AuthContext', () => ({
|
vi.mock('../../contexts/AuthContext', async () => {
|
||||||
...jest.requireActual('../../contexts/AuthContext'),
|
const actual = await vi.importActual('../../contexts/AuthContext');
|
||||||
useAuth: () => ({
|
return {
|
||||||
login: mockLogin,
|
...actual,
|
||||||
register: mockRegister,
|
useAuth: () => ({
|
||||||
user: null,
|
login: mockLogin,
|
||||||
loading: false,
|
register: mockRegister,
|
||||||
}),
|
user: null,
|
||||||
}));
|
loading: false,
|
||||||
|
}),
|
||||||
// Mock child components
|
|
||||||
jest.mock('../../components/PasswordStrengthMeter', () => {
|
|
||||||
return function MockPasswordStrengthMeter({ password }: { password: string }) {
|
|
||||||
return <div data-testid="password-strength-meter">Strength: {password.length > 8 ? 'Strong' : 'Weak'}</div>;
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('../../components/PasswordInput', () => {
|
// Mock child components
|
||||||
return function MockPasswordInput({
|
vi.mock('../../components/PasswordStrengthMeter', () => ({
|
||||||
|
default: function MockPasswordStrengthMeter({ password }: { password: string }) {
|
||||||
|
return <div data-testid="password-strength-meter">Strength: {password.length > 8 ? 'Strong' : 'Weak'}</div>;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../components/PasswordInput', () => ({
|
||||||
|
default: function MockPasswordInput({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
@@ -59,11 +63,11 @@ jest.mock('../../components/PasswordInput', () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
jest.mock('../../components/ForgotPasswordModal', () => {
|
vi.mock('../../components/ForgotPasswordModal', () => ({
|
||||||
return function MockForgotPasswordModal({
|
default: function MockForgotPasswordModal({
|
||||||
show,
|
show,
|
||||||
onHide,
|
onHide,
|
||||||
onBackToLogin
|
onBackToLogin
|
||||||
@@ -79,11 +83,11 @@ jest.mock('../../components/ForgotPasswordModal', () => {
|
|||||||
<button onClick={onHide}>Close</button>
|
<button onClick={onHide}>Close</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
jest.mock('../../components/VerificationCodeModal', () => {
|
vi.mock('../../components/VerificationCodeModal', () => ({
|
||||||
return function MockVerificationCodeModal({
|
default: function MockVerificationCodeModal({
|
||||||
show,
|
show,
|
||||||
onHide,
|
onHide,
|
||||||
email,
|
email,
|
||||||
@@ -102,17 +106,17 @@ jest.mock('../../components/VerificationCodeModal', () => {
|
|||||||
<button onClick={onHide}>Close</button>
|
<button onClick={onHide}>Close</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
describe('AuthModal', () => {
|
describe('AuthModal', () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
show: true,
|
show: true,
|
||||||
onHide: jest.fn(),
|
onHide: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper to get email input (it's a textbox with type email)
|
// Helper to get email input (it's a textbox with type email)
|
||||||
@@ -371,20 +375,33 @@ describe('AuthModal', () => {
|
|||||||
|
|
||||||
describe('Google OAuth', () => {
|
describe('Google OAuth', () => {
|
||||||
it('should redirect to Google OAuth when Google button is clicked', () => {
|
it('should redirect to Google OAuth when Google button is clicked', () => {
|
||||||
// Mock window.location
|
// Mock window.location.href using Object.defineProperty
|
||||||
|
let mockHref = '';
|
||||||
const originalLocation = window.location;
|
const originalLocation = window.location;
|
||||||
delete (window as any).location;
|
|
||||||
window.location = { ...originalLocation, href: '' } as Location;
|
Object.defineProperty(window, 'location', {
|
||||||
|
value: {
|
||||||
|
...originalLocation,
|
||||||
|
get href() { return mockHref; },
|
||||||
|
set href(value: string) { mockHref = value; },
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
render(<AuthModal {...defaultProps} />);
|
render(<AuthModal {...defaultProps} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /continue with google/i }));
|
fireEvent.click(screen.getByRole('button', { name: /continue with google/i }));
|
||||||
|
|
||||||
// Check that window.location.href was set to Google OAuth URL
|
// Check that window.location.href was set to Google OAuth URL
|
||||||
expect(window.location.href).toContain('accounts.google.com');
|
expect(mockHref).toContain('accounts.google.com');
|
||||||
|
|
||||||
// Restore
|
// Restore
|
||||||
window.location = originalLocation;
|
Object.defineProperty(window, 'location', {
|
||||||
|
value: originalLocation,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { vi, type MockedFunction } from 'vitest';
|
||||||
import ItemCard from '../../components/ItemCard';
|
import ItemCard from '../../components/ItemCard';
|
||||||
import { Item } from '../../types';
|
import { Item } from '../../types';
|
||||||
import { getPublicImageUrl } from '../../services/uploadService';
|
import { getImageUrl, getPublicImageUrl } from '../../services/uploadService';
|
||||||
|
|
||||||
// Mock the uploadService
|
// Mock the uploadService
|
||||||
jest.mock('../../services/uploadService', () => ({
|
vi.mock('../../services/uploadService', () => ({
|
||||||
getPublicImageUrl: jest.fn(),
|
getPublicImageUrl: vi.fn(),
|
||||||
|
getImageUrl: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockedGetPublicImageUrl = getPublicImageUrl as jest.MockedFunction<typeof getPublicImageUrl>;
|
const mockedGetPublicImageUrl = getPublicImageUrl as MockedFunction<typeof getPublicImageUrl>;
|
||||||
|
const mockedGetImageUrl = getImageUrl as MockedFunction<typeof getImageUrl>;
|
||||||
|
|
||||||
// Helper to render with Router
|
// Helper to render with Router
|
||||||
const renderWithRouter = (component: React.ReactElement) => {
|
const renderWithRouter = (component: React.ReactElement) => {
|
||||||
@@ -31,10 +34,15 @@ beforeEach(() => {
|
|||||||
if (imagePath.startsWith('https://')) return imagePath;
|
if (imagePath.startsWith('https://')) return imagePath;
|
||||||
return `https://test-bucket.s3.us-east-1.amazonaws.com/${imagePath}`;
|
return `https://test-bucket.s3.us-east-1.amazonaws.com/${imagePath}`;
|
||||||
});
|
});
|
||||||
|
mockedGetImageUrl.mockImplementation((imagePath: string | null | undefined) => {
|
||||||
|
if (!imagePath) return '';
|
||||||
|
if (imagePath.startsWith('https://')) return imagePath;
|
||||||
|
return `https://test-bucket.s3.us-east-1.amazonaws.com/${imagePath}`;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock item data
|
// Mock item data
|
||||||
|
|||||||
@@ -8,33 +8,34 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { vi, type Mock } from 'vitest';
|
||||||
import Navbar from '../../components/Navbar';
|
import Navbar from '../../components/Navbar';
|
||||||
import { rentalAPI, messageAPI } from '../../services/api';
|
import { rentalAPI, messageAPI } from '../../services/api';
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
jest.mock('../../services/api', () => ({
|
vi.mock('../../services/api', () => ({
|
||||||
rentalAPI: {
|
rentalAPI: {
|
||||||
getPendingRequestsCount: jest.fn(),
|
getPendingRequestsCount: vi.fn(),
|
||||||
},
|
},
|
||||||
messageAPI: {
|
messageAPI: {
|
||||||
getUnreadCount: jest.fn(),
|
getUnreadCount: vi.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock socket context
|
// Mock socket context
|
||||||
jest.mock('../../contexts/SocketContext', () => ({
|
vi.mock('../../contexts/SocketContext', () => ({
|
||||||
useSocket: () => ({
|
useSocket: () => ({
|
||||||
onNewMessage: jest.fn(() => () => {}),
|
onNewMessage: vi.fn(() => () => {}),
|
||||||
onMessageRead: jest.fn(() => () => {}),
|
onMessageRead: vi.fn(() => () => {}),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Variable to control auth state per test
|
// Variable to control auth state per test
|
||||||
let mockUser: any = null;
|
let mockUser: any = null;
|
||||||
const mockLogout = jest.fn();
|
const mockLogout = vi.fn();
|
||||||
const mockOpenAuthModal = jest.fn();
|
const mockOpenAuthModal = vi.fn();
|
||||||
|
|
||||||
jest.mock('../../contexts/AuthContext', () => ({
|
vi.mock('../../contexts/AuthContext', () => ({
|
||||||
useAuth: () => ({
|
useAuth: () => ({
|
||||||
user: mockUser,
|
user: mockUser,
|
||||||
logout: mockLogout,
|
logout: mockLogout,
|
||||||
@@ -43,11 +44,14 @@ jest.mock('../../contexts/AuthContext', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock useNavigate
|
// Mock useNavigate
|
||||||
const mockNavigate = jest.fn();
|
const mockNavigate = vi.fn();
|
||||||
jest.mock('react-router-dom', () => ({
|
vi.mock('react-router-dom', async () => {
|
||||||
...jest.requireActual('react-router-dom'),
|
const actual = await vi.importActual('react-router-dom');
|
||||||
useNavigate: () => mockNavigate,
|
return {
|
||||||
}));
|
...actual,
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Helper to render with Router
|
// Helper to render with Router
|
||||||
const renderWithRouter = (component: React.ReactElement) => {
|
const renderWithRouter = (component: React.ReactElement) => {
|
||||||
@@ -56,12 +60,12 @@ const renderWithRouter = (component: React.ReactElement) => {
|
|||||||
|
|
||||||
describe('Navbar', () => {
|
describe('Navbar', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockUser = null;
|
mockUser = null;
|
||||||
|
|
||||||
// Default mock implementations
|
// Default mock implementations
|
||||||
(rentalAPI.getPendingRequestsCount as jest.Mock).mockResolvedValue({ data: { count: 0 } });
|
(rentalAPI.getPendingRequestsCount as Mock).mockResolvedValue({ data: { count: 0 } });
|
||||||
(messageAPI.getUnreadCount as jest.Mock).mockResolvedValue({ data: { count: 0 } });
|
(messageAPI.getUnreadCount as Mock).mockResolvedValue({ data: { count: 0 } });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Branding', () => {
|
describe('Branding', () => {
|
||||||
@@ -193,42 +197,70 @@ describe('Navbar', () => {
|
|||||||
it('should show profile link in dropdown', () => {
|
it('should show profile link in dropdown', () => {
|
||||||
renderWithRouter(<Navbar />);
|
renderWithRouter(<Navbar />);
|
||||||
|
|
||||||
|
// Click the dropdown toggle to expand the menu
|
||||||
|
const avatarButton = screen.getByRole('button', { name: /John Doe's avatar/i });
|
||||||
|
fireEvent.click(avatarButton);
|
||||||
|
|
||||||
expect(screen.getByRole('link', { name: /Profile/i })).toHaveAttribute('href', '/profile');
|
expect(screen.getByRole('link', { name: /Profile/i })).toHaveAttribute('href', '/profile');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show renting link in dropdown', () => {
|
it('should show renting link in dropdown', () => {
|
||||||
renderWithRouter(<Navbar />);
|
renderWithRouter(<Navbar />);
|
||||||
|
|
||||||
|
// Click the dropdown toggle to expand the menu
|
||||||
|
const avatarButton = screen.getByRole('button', { name: /John Doe's avatar/i });
|
||||||
|
fireEvent.click(avatarButton);
|
||||||
|
|
||||||
expect(screen.getByRole('link', { name: /Renting/i })).toHaveAttribute('href', '/renting');
|
expect(screen.getByRole('link', { name: /Renting/i })).toHaveAttribute('href', '/renting');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show owning link in dropdown', () => {
|
it('should show owning link in dropdown', () => {
|
||||||
renderWithRouter(<Navbar />);
|
renderWithRouter(<Navbar />);
|
||||||
|
|
||||||
|
// Click the dropdown toggle to expand the menu
|
||||||
|
const avatarButton = screen.getByRole('button', { name: /John Doe's avatar/i });
|
||||||
|
fireEvent.click(avatarButton);
|
||||||
|
|
||||||
expect(screen.getByRole('link', { name: /Owning/i })).toHaveAttribute('href', '/owning');
|
expect(screen.getByRole('link', { name: /Owning/i })).toHaveAttribute('href', '/owning');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show messages link in dropdown', () => {
|
it('should show messages link in dropdown', () => {
|
||||||
renderWithRouter(<Navbar />);
|
renderWithRouter(<Navbar />);
|
||||||
|
|
||||||
|
// Click the dropdown toggle to expand the menu
|
||||||
|
const avatarButton = screen.getByRole('button', { name: /John Doe's avatar/i });
|
||||||
|
fireEvent.click(avatarButton);
|
||||||
|
|
||||||
expect(screen.getByRole('link', { name: /Messages/i })).toHaveAttribute('href', '/messages');
|
expect(screen.getByRole('link', { name: /Messages/i })).toHaveAttribute('href', '/messages');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show forum link in dropdown', () => {
|
it('should show forum link in dropdown', () => {
|
||||||
renderWithRouter(<Navbar />);
|
renderWithRouter(<Navbar />);
|
||||||
|
|
||||||
|
// Click the dropdown toggle to expand the menu
|
||||||
|
const avatarButton = screen.getByRole('button', { name: /John Doe's avatar/i });
|
||||||
|
fireEvent.click(avatarButton);
|
||||||
|
|
||||||
expect(screen.getByRole('link', { name: /Forum/i })).toHaveAttribute('href', '/forum');
|
expect(screen.getByRole('link', { name: /Forum/i })).toHaveAttribute('href', '/forum');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show earnings link in dropdown', () => {
|
it('should show earnings link in dropdown', () => {
|
||||||
renderWithRouter(<Navbar />);
|
renderWithRouter(<Navbar />);
|
||||||
|
|
||||||
|
// Click the dropdown toggle to expand the menu
|
||||||
|
const avatarButton = screen.getByRole('button', { name: /John Doe's avatar/i });
|
||||||
|
fireEvent.click(avatarButton);
|
||||||
|
|
||||||
expect(screen.getByRole('link', { name: /Earnings/i })).toHaveAttribute('href', '/earnings');
|
expect(screen.getByRole('link', { name: /Earnings/i })).toHaveAttribute('href', '/earnings');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call logout and navigate home when logout is clicked', () => {
|
it('should call logout and navigate home when logout is clicked', () => {
|
||||||
renderWithRouter(<Navbar />);
|
renderWithRouter(<Navbar />);
|
||||||
|
|
||||||
|
// Click the dropdown toggle to expand the menu
|
||||||
|
const avatarButton = screen.getByRole('button', { name: /John Doe's avatar/i });
|
||||||
|
fireEvent.click(avatarButton);
|
||||||
|
|
||||||
const logoutButton = screen.getByRole('button', { name: /Logout/i });
|
const logoutButton = screen.getByRole('button', { name: /Logout/i });
|
||||||
fireEvent.click(logoutButton);
|
fireEvent.click(logoutButton);
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
|
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
|
||||||
|
import { vi, type Mocked, type MockedFunction } from 'vitest';
|
||||||
import { AuthProvider, useAuth } from '../../contexts/AuthContext';
|
import { AuthProvider, useAuth } from '../../contexts/AuthContext';
|
||||||
import { mockUser } from '../../mocks/handlers';
|
import { mockUser } from '../../mocks/handlers';
|
||||||
|
|
||||||
// Mock the API module
|
// Mock the API module
|
||||||
jest.mock('../../services/api', () => {
|
vi.mock('../../services/api', () => {
|
||||||
const mockAuthAPI = {
|
const mockAuthAPI = {
|
||||||
login: jest.fn(),
|
login: vi.fn(),
|
||||||
register: jest.fn(),
|
register: vi.fn(),
|
||||||
googleLogin: jest.fn(),
|
googleLogin: vi.fn(),
|
||||||
logout: jest.fn(),
|
logout: vi.fn(),
|
||||||
getStatus: jest.fn(),
|
getStatus: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockFetchCSRFToken = jest.fn().mockResolvedValue('test-csrf-token');
|
const mockFetchCSRFToken = vi.fn().mockResolvedValue('test-csrf-token');
|
||||||
const mockResetCSRFToken = jest.fn();
|
const mockResetCSRFToken = vi.fn();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authAPI: mockAuthAPI,
|
authAPI: mockAuthAPI,
|
||||||
@@ -26,9 +27,18 @@ jest.mock('../../services/api', () => {
|
|||||||
// Get mocked modules
|
// Get mocked modules
|
||||||
import { authAPI, fetchCSRFToken, resetCSRFToken } from '../../services/api';
|
import { authAPI, fetchCSRFToken, resetCSRFToken } from '../../services/api';
|
||||||
|
|
||||||
const mockAuthAPI = authAPI as jest.Mocked<typeof authAPI>;
|
const mockAuthAPI = authAPI as Mocked<typeof authAPI>;
|
||||||
const mockFetchCSRFToken = fetchCSRFToken as jest.MockedFunction<typeof fetchCSRFToken>;
|
const mockFetchCSRFToken = fetchCSRFToken as MockedFunction<typeof fetchCSRFToken>;
|
||||||
const mockResetCSRFToken = resetCSRFToken as jest.MockedFunction<typeof resetCSRFToken>;
|
const mockResetCSRFToken = resetCSRFToken as MockedFunction<typeof resetCSRFToken>;
|
||||||
|
|
||||||
|
// Helper to create mock Axios response
|
||||||
|
const mockAxiosResponse = <T,>(data: T) => ({
|
||||||
|
data,
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
headers: {},
|
||||||
|
config: {} as any,
|
||||||
|
});
|
||||||
|
|
||||||
// Test component that uses the auth context
|
// Test component that uses the auth context
|
||||||
const TestComponent: React.FC = () => {
|
const TestComponent: React.FC = () => {
|
||||||
@@ -64,17 +74,17 @@ const renderWithAuth = (ui: React.ReactElement = <TestComponent />) => {
|
|||||||
|
|
||||||
describe('AuthContext', () => {
|
describe('AuthContext', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Default: user is authenticated
|
// Default: user is authenticated
|
||||||
mockAuthAPI.getStatus.mockResolvedValue({
|
mockAuthAPI.getStatus.mockResolvedValue(
|
||||||
data: { authenticated: true, user: mockUser },
|
mockAxiosResponse({ authenticated: true, user: mockUser })
|
||||||
});
|
);
|
||||||
mockFetchCSRFToken.mockResolvedValue('test-csrf-token');
|
mockFetchCSRFToken.mockResolvedValue('test-csrf-token');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useAuth hook', () => {
|
describe('useAuth hook', () => {
|
||||||
it('throws error when used outside AuthProvider', () => {
|
it('throws error when used outside AuthProvider', () => {
|
||||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
expect(() => render(<TestComponent />)).toThrow('useAuth must be used within an AuthProvider');
|
expect(() => render(<TestComponent />)).toThrow('useAuth must be used within an AuthProvider');
|
||||||
|
|
||||||
@@ -100,9 +110,9 @@ describe('AuthContext', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sets user to null when not authenticated', async () => {
|
it('sets user to null when not authenticated', async () => {
|
||||||
mockAuthAPI.getStatus.mockResolvedValue({
|
mockAuthAPI.getStatus.mockResolvedValue(
|
||||||
data: { authenticated: false, user: null },
|
mockAxiosResponse({ authenticated: false, user: null })
|
||||||
});
|
);
|
||||||
|
|
||||||
renderWithAuth();
|
renderWithAuth();
|
||||||
|
|
||||||
@@ -128,13 +138,13 @@ describe('AuthContext', () => {
|
|||||||
|
|
||||||
describe('Login', () => {
|
describe('Login', () => {
|
||||||
it('logs in successfully with valid credentials', async () => {
|
it('logs in successfully with valid credentials', async () => {
|
||||||
mockAuthAPI.getStatus.mockResolvedValue({
|
mockAuthAPI.getStatus.mockResolvedValue(
|
||||||
data: { authenticated: false, user: null },
|
mockAxiosResponse({ authenticated: false, user: null })
|
||||||
});
|
);
|
||||||
|
|
||||||
mockAuthAPI.login.mockResolvedValue({
|
mockAuthAPI.login.mockResolvedValue(
|
||||||
data: { user: mockUser },
|
mockAxiosResponse({ user: mockUser })
|
||||||
});
|
);
|
||||||
|
|
||||||
renderWithAuth();
|
renderWithAuth();
|
||||||
|
|
||||||
@@ -159,9 +169,9 @@ describe('AuthContext', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('keeps user as null when login fails', async () => {
|
it('keeps user as null when login fails', async () => {
|
||||||
mockAuthAPI.getStatus.mockResolvedValue({
|
mockAuthAPI.getStatus.mockResolvedValue(
|
||||||
data: { authenticated: false, user: null },
|
mockAxiosResponse({ authenticated: false, user: null })
|
||||||
});
|
);
|
||||||
|
|
||||||
mockAuthAPI.login.mockRejectedValue(new Error('Invalid credentials'));
|
mockAuthAPI.login.mockRejectedValue(new Error('Invalid credentials'));
|
||||||
|
|
||||||
@@ -213,13 +223,13 @@ describe('AuthContext', () => {
|
|||||||
|
|
||||||
describe('Registration', () => {
|
describe('Registration', () => {
|
||||||
it('registers a new user successfully', async () => {
|
it('registers a new user successfully', async () => {
|
||||||
mockAuthAPI.getStatus.mockResolvedValue({
|
mockAuthAPI.getStatus.mockResolvedValue(
|
||||||
data: { authenticated: false, user: null },
|
mockAxiosResponse({ authenticated: false, user: null })
|
||||||
});
|
);
|
||||||
|
|
||||||
mockAuthAPI.register.mockResolvedValue({
|
mockAuthAPI.register.mockResolvedValue(
|
||||||
data: { user: { ...mockUser, email: 'new@example.com', isVerified: false } },
|
mockAxiosResponse({ user: { ...mockUser, email: 'new@example.com', isVerified: false } })
|
||||||
});
|
);
|
||||||
|
|
||||||
renderWithAuth();
|
renderWithAuth();
|
||||||
|
|
||||||
@@ -239,13 +249,13 @@ describe('AuthContext', () => {
|
|||||||
|
|
||||||
describe('Google Login', () => {
|
describe('Google Login', () => {
|
||||||
it('logs in with Google successfully', async () => {
|
it('logs in with Google successfully', async () => {
|
||||||
mockAuthAPI.getStatus.mockResolvedValue({
|
mockAuthAPI.getStatus.mockResolvedValue(
|
||||||
data: { authenticated: false, user: null },
|
mockAxiosResponse({ authenticated: false, user: null })
|
||||||
});
|
);
|
||||||
|
|
||||||
mockAuthAPI.googleLogin.mockResolvedValue({
|
mockAuthAPI.googleLogin.mockResolvedValue(
|
||||||
data: { user: mockUser },
|
mockAxiosResponse({ user: mockUser })
|
||||||
});
|
);
|
||||||
|
|
||||||
renderWithAuth();
|
renderWithAuth();
|
||||||
|
|
||||||
@@ -267,7 +277,7 @@ describe('AuthContext', () => {
|
|||||||
|
|
||||||
describe('Logout', () => {
|
describe('Logout', () => {
|
||||||
it('logs out successfully', async () => {
|
it('logs out successfully', async () => {
|
||||||
mockAuthAPI.logout.mockResolvedValue({ data: { message: 'Logged out' } });
|
mockAuthAPI.logout.mockResolvedValue(mockAxiosResponse({ message: 'Logged out' }));
|
||||||
|
|
||||||
renderWithAuth();
|
renderWithAuth();
|
||||||
|
|
||||||
@@ -403,9 +413,9 @@ describe('AuthContext', () => {
|
|||||||
mockAuthAPI.getStatus.mockImplementation(() => {
|
mockAuthAPI.getStatus.mockImplementation(() => {
|
||||||
callCount++;
|
callCount++;
|
||||||
if (callCount === 1) {
|
if (callCount === 1) {
|
||||||
return Promise.resolve({ data: { authenticated: false, user: null } });
|
return Promise.resolve(mockAxiosResponse({ authenticated: false, user: null }));
|
||||||
}
|
}
|
||||||
return Promise.resolve({ data: { authenticated: true, user: mockUser } });
|
return Promise.resolve(mockAxiosResponse({ authenticated: true, user: mockUser }));
|
||||||
});
|
});
|
||||||
|
|
||||||
renderWithAuth();
|
renderWithAuth();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { vi } from 'vitest';
|
||||||
import { useAddressAutocomplete, usStates } from '../../hooks/useAddressAutocomplete';
|
import { useAddressAutocomplete, usStates } from '../../hooks/useAddressAutocomplete';
|
||||||
import { PlaceDetails } from '../../services/placesService';
|
import { PlaceDetails } from '../../services/placesService';
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ describe('useAddressAutocomplete', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sets state to empty string for unknown states', () => {
|
it('sets state to empty string for unknown states', () => {
|
||||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
const place: PlaceDetails = {
|
const place: PlaceDetails = {
|
||||||
formattedAddress: '123 Street, City, XX 12345, USA',
|
formattedAddress: '123 Street, City, XX 12345, USA',
|
||||||
@@ -236,7 +237,7 @@ describe('useAddressAutocomplete', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns null and logs error when parsing fails', () => {
|
it('returns null and logs error when parsing fails', () => {
|
||||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
// Pass an object that will cause an error when accessing nested properties
|
// Pass an object that will cause an error when accessing nested properties
|
||||||
const invalidPlace = {
|
const invalidPlace = {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* direct uploads, and signed URL generation for private content.
|
* direct uploads, and signed URL generation for private content.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { vi, type Mocked } from 'vitest';
|
||||||
import api from '../../services/api';
|
import api from '../../services/api';
|
||||||
import {
|
import {
|
||||||
getPublicImageUrl,
|
getPublicImageUrl,
|
||||||
@@ -13,15 +14,14 @@ import {
|
|||||||
uploadToS3,
|
uploadToS3,
|
||||||
confirmUploads,
|
confirmUploads,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
uploadFiles,
|
|
||||||
getSignedUrl,
|
getSignedUrl,
|
||||||
PresignedUrlResponse,
|
PresignedUrlResponse,
|
||||||
} from '../../services/uploadService';
|
} from '../../services/uploadService';
|
||||||
|
|
||||||
// Mock the api module
|
// Mock the api module
|
||||||
jest.mock('../../services/api');
|
vi.mock('../../services/api');
|
||||||
|
|
||||||
const mockedApi = api as jest.Mocked<typeof api>;
|
const mockedApi = api as Mocked<typeof api>;
|
||||||
|
|
||||||
// Mock XMLHttpRequest for uploadToS3 tests
|
// Mock XMLHttpRequest for uploadToS3 tests
|
||||||
class MockXMLHttpRequest {
|
class MockXMLHttpRequest {
|
||||||
@@ -93,11 +93,11 @@ const originalXMLHttpRequest = global.XMLHttpRequest;
|
|||||||
|
|
||||||
describe('Upload Service', () => {
|
describe('Upload Service', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
MockXMLHttpRequest.reset();
|
MockXMLHttpRequest.reset();
|
||||||
// Reset environment variables
|
// Reset environment variables using stubEnv for Vitest
|
||||||
process.env.REACT_APP_S3_BUCKET = 'test-bucket';
|
vi.stubEnv('VITE_S3_BUCKET', 'test-bucket');
|
||||||
process.env.REACT_APP_AWS_REGION = 'us-east-1';
|
vi.stubEnv('VITE_AWS_REGION', 'us-east-1');
|
||||||
// Mock XMLHttpRequest globally
|
// Mock XMLHttpRequest globally
|
||||||
(global as unknown as { XMLHttpRequest: typeof MockXMLHttpRequest }).XMLHttpRequest = MockXMLHttpRequest;
|
(global as unknown as { XMLHttpRequest: typeof MockXMLHttpRequest }).XMLHttpRequest = MockXMLHttpRequest;
|
||||||
});
|
});
|
||||||
@@ -140,12 +140,6 @@ describe('Upload Service', () => {
|
|||||||
const key = 'forum/550e8400-e29b-41d4-a716-446655440000.jpg';
|
const key = 'forum/550e8400-e29b-41d4-a716-446655440000.jpg';
|
||||||
expect(getPublicImageUrl(key)).toContain('forum/');
|
expect(getPublicImageUrl(key)).toContain('forum/');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use default region when not set', () => {
|
|
||||||
delete process.env.REACT_APP_AWS_REGION;
|
|
||||||
const key = 'items/uuid.jpg';
|
|
||||||
expect(getPublicImageUrl(key)).toContain('us-east-1');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getPresignedUrl', () => {
|
describe('getPresignedUrl', () => {
|
||||||
@@ -153,6 +147,7 @@ describe('Upload Service', () => {
|
|||||||
const mockResponse: PresignedUrlResponse = {
|
const mockResponse: PresignedUrlResponse = {
|
||||||
uploadUrl: 'https://presigned-url.s3.amazonaws.com/items/uuid.jpg?signature=abc',
|
uploadUrl: 'https://presigned-url.s3.amazonaws.com/items/uuid.jpg?signature=abc',
|
||||||
key: 'items/550e8400-e29b-41d4-a716-446655440000.jpg',
|
key: 'items/550e8400-e29b-41d4-a716-446655440000.jpg',
|
||||||
|
stagingKey: null,
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/550e8400-e29b-41d4-a716-446655440000.jpg',
|
publicUrl: 'https://bucket.s3.amazonaws.com/items/550e8400-e29b-41d4-a716-446655440000.jpg',
|
||||||
expiresAt: new Date().toISOString(),
|
expiresAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -213,19 +208,21 @@ describe('Upload Service', () => {
|
|||||||
{
|
{
|
||||||
uploadUrl: 'https://presigned-url1.s3.amazonaws.com',
|
uploadUrl: 'https://presigned-url1.s3.amazonaws.com',
|
||||||
key: 'items/uuid1.jpg',
|
key: 'items/uuid1.jpg',
|
||||||
|
stagingKey: null,
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
|
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
|
||||||
expiresAt: new Date().toISOString(),
|
expiresAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
uploadUrl: 'https://presigned-url2.s3.amazonaws.com',
|
uploadUrl: 'https://presigned-url2.s3.amazonaws.com',
|
||||||
key: 'items/uuid2.png',
|
key: 'items/uuid2.png',
|
||||||
|
stagingKey: null,
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
|
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
|
||||||
expiresAt: new Date().toISOString(),
|
expiresAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
it('should request batch presigned URLs', async () => {
|
it('should request batch presigned URLs', async () => {
|
||||||
mockedApi.post.mockResolvedValue({ data: { uploads: mockResponses } });
|
mockedApi.post.mockResolvedValue({ data: { uploads: mockResponses, baseKey: 'base-key' } });
|
||||||
|
|
||||||
const result = await getPresignedUrls('item', mockFiles);
|
const result = await getPresignedUrls('item', mockFiles);
|
||||||
|
|
||||||
@@ -236,15 +233,15 @@ describe('Upload Service', () => {
|
|||||||
{ contentType: 'image/png', fileName: 'photo2.png', fileSize: mockFiles[1].size },
|
{ contentType: 'image/png', fileName: 'photo2.png', fileSize: mockFiles[1].size },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(result).toEqual(mockResponses);
|
expect(result).toEqual({ uploads: mockResponses, baseKey: 'base-key' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty file array', async () => {
|
it('should handle empty file array', async () => {
|
||||||
mockedApi.post.mockResolvedValue({ data: { uploads: [] } });
|
mockedApi.post.mockResolvedValue({ data: { uploads: [], baseKey: undefined } });
|
||||||
|
|
||||||
const result = await getPresignedUrls('item', []);
|
const result = await getPresignedUrls('item', []);
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual({ uploads: [], baseKey: undefined });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -262,7 +259,7 @@ describe('Upload Service', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call onProgress callback during upload', async () => {
|
it('should call onProgress callback during upload', async () => {
|
||||||
const onProgress = jest.fn();
|
const onProgress = vi.fn();
|
||||||
|
|
||||||
await uploadToS3(mockFile, mockUploadUrl, { onProgress });
|
await uploadToS3(mockFile, mockUploadUrl, { onProgress });
|
||||||
|
|
||||||
@@ -318,6 +315,7 @@ describe('Upload Service', () => {
|
|||||||
const presignResponse: PresignedUrlResponse = {
|
const presignResponse: PresignedUrlResponse = {
|
||||||
uploadUrl: 'https://presigned.s3.amazonaws.com/items/uuid.jpg',
|
uploadUrl: 'https://presigned.s3.amazonaws.com/items/uuid.jpg',
|
||||||
key: 'items/uuid.jpg',
|
key: 'items/uuid.jpg',
|
||||||
|
stagingKey: null,
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid.jpg',
|
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid.jpg',
|
||||||
expiresAt: new Date().toISOString(),
|
expiresAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -362,7 +360,7 @@ describe('Upload Service', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should pass onProgress to uploadToS3', async () => {
|
it('should pass onProgress to uploadToS3', async () => {
|
||||||
const onProgress = jest.fn();
|
const onProgress = vi.fn();
|
||||||
|
|
||||||
mockedApi.post.mockResolvedValueOnce({ data: presignResponse });
|
mockedApi.post.mockResolvedValueOnce({ data: presignResponse });
|
||||||
mockedApi.post.mockResolvedValueOnce({
|
mockedApi.post.mockResolvedValueOnce({
|
||||||
@@ -396,150 +394,8 @@ describe('Upload Service', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('uploadFiles', () => {
|
// Note: uploadFiles function was removed from uploadService and replaced with uploadImagesWithVariants
|
||||||
const mockFiles = [
|
// Tests for batch uploads would need to be updated to test the new function
|
||||||
new File(['test1'], 'photo1.jpg', { type: 'image/jpeg' }),
|
|
||||||
new File(['test2'], 'photo2.png', { type: 'image/png' }),
|
|
||||||
];
|
|
||||||
|
|
||||||
const presignResponses: PresignedUrlResponse[] = [
|
|
||||||
{
|
|
||||||
uploadUrl: 'https://presigned1.s3.amazonaws.com/items/uuid1.jpg',
|
|
||||||
key: 'items/uuid1.jpg',
|
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
|
|
||||||
expiresAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uploadUrl: 'https://presigned2.s3.amazonaws.com/items/uuid2.png',
|
|
||||||
key: 'items/uuid2.png',
|
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
|
|
||||||
expiresAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it('should return empty array for empty files array', async () => {
|
|
||||||
const result = await uploadFiles('item', []);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should complete full batch upload flow successfully', async () => {
|
|
||||||
mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
|
|
||||||
mockedApi.post.mockResolvedValueOnce({
|
|
||||||
data: {
|
|
||||||
confirmed: presignResponses.map((p) => p.key),
|
|
||||||
total: 2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await uploadFiles('item', mockFiles);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
expect(result[0]).toEqual({
|
|
||||||
key: 'items/uuid1.jpg',
|
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
|
|
||||||
});
|
|
||||||
expect(result[1]).toEqual({
|
|
||||||
key: 'items/uuid2.png',
|
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify batch presign was called
|
|
||||||
expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign-batch', {
|
|
||||||
uploadType: 'item',
|
|
||||||
files: [
|
|
||||||
{ contentType: 'image/jpeg', fileName: 'photo1.jpg', fileSize: mockFiles[0].size },
|
|
||||||
{ contentType: 'image/png', fileName: 'photo2.png', fileSize: mockFiles[1].size },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify confirm was called with all keys
|
|
||||||
expect(mockedApi.post).toHaveBeenCalledWith('/upload/confirm', {
|
|
||||||
keys: ['items/uuid1.jpg', 'items/uuid2.png'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should filter out unconfirmed uploads', async () => {
|
|
||||||
mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
|
|
||||||
// Only first file confirmed
|
|
||||||
mockedApi.post.mockResolvedValueOnce({
|
|
||||||
data: {
|
|
||||||
confirmed: ['items/uuid1.jpg'],
|
|
||||||
total: 2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
||||||
|
|
||||||
const result = await uploadFiles('item', mockFiles);
|
|
||||||
|
|
||||||
// Only confirmed uploads should be returned
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].key).toBe('items/uuid1.jpg');
|
|
||||||
|
|
||||||
// Should log warning about failed verification
|
|
||||||
expect(consoleSpy).toHaveBeenCalledWith('1 uploads failed verification');
|
|
||||||
|
|
||||||
consoleSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle all uploads failing verification', async () => {
|
|
||||||
mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
|
|
||||||
mockedApi.post.mockResolvedValueOnce({
|
|
||||||
data: {
|
|
||||||
confirmed: [],
|
|
||||||
total: 2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
||||||
|
|
||||||
const result = await uploadFiles('item', mockFiles);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(0);
|
|
||||||
expect(consoleSpy).toHaveBeenCalledWith('2 uploads failed verification');
|
|
||||||
|
|
||||||
consoleSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should upload all files in parallel', async () => {
|
|
||||||
mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
|
|
||||||
mockedApi.post.mockResolvedValueOnce({
|
|
||||||
data: {
|
|
||||||
confirmed: presignResponses.map((p) => p.key),
|
|
||||||
total: 2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await uploadFiles('item', mockFiles);
|
|
||||||
|
|
||||||
// Should have created 2 XHR instances for parallel uploads
|
|
||||||
expect(MockXMLHttpRequest.instances.length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with different upload types', async () => {
|
|
||||||
const forumResponses = presignResponses.map((r) => ({
|
|
||||||
...r,
|
|
||||||
key: r.key.replace('items/', 'forum/'),
|
|
||||||
publicUrl: r.publicUrl.replace('items/', 'forum/'),
|
|
||||||
}));
|
|
||||||
|
|
||||||
mockedApi.post.mockResolvedValueOnce({ data: { uploads: forumResponses } });
|
|
||||||
mockedApi.post.mockResolvedValueOnce({
|
|
||||||
data: {
|
|
||||||
confirmed: forumResponses.map((p) => p.key),
|
|
||||||
total: 2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await uploadFiles('forum', mockFiles);
|
|
||||||
|
|
||||||
expect(result[0].key).toBe('forum/uuid1.jpg');
|
|
||||||
expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign-batch', expect.objectContaining({
|
|
||||||
uploadType: 'forum',
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getSignedUrl', () => {
|
describe('getSignedUrl', () => {
|
||||||
it('should request signed URL for private content', async () => {
|
it('should request signed URL for private content', async () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const API_URL = process.env.REACT_APP_API_URL || "http://localhost:5001";
|
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:5001";
|
||||||
|
|
||||||
const AlphaGate: React.FC = () => {
|
const AlphaGate: React.FC = () => {
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGoogleLogin = () => {
|
const handleGoogleLogin = () => {
|
||||||
const clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID;
|
const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||||
const redirectUri = `${window.location.origin}/auth/google/callback`;
|
const redirectUri = `${window.location.origin}/auth/google/callback`;
|
||||||
const scope = "openid email profile";
|
const scope = "openid email profile";
|
||||||
const responseType = "code";
|
const responseType = "code";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { stripeAPI, rentalAPI } from "../services/api";
|
import { stripeAPI, rentalAPI } from "../services/api";
|
||||||
|
|
||||||
const stripePromise = loadStripe(
|
const stripePromise = loadStripe(
|
||||||
process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY || ""
|
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || ""
|
||||||
);
|
);
|
||||||
|
|
||||||
interface EmbeddedStripeCheckoutProps {
|
interface EmbeddedStripeCheckoutProps {
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ const GoogleMapWithRadius: React.FC<GoogleMapWithRadiusProps> = ({
|
|||||||
const { zoom = 12 } = mapOptions;
|
const { zoom = 12 } = mapOptions;
|
||||||
|
|
||||||
// Get API key and Map ID from environment
|
// Get API key and Map ID from environment
|
||||||
const apiKey = process.env.REACT_APP_GOOGLE_MAPS_PUBLIC_API_KEY;
|
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_PUBLIC_API_KEY;
|
||||||
const mapId = process.env.REACT_APP_GOOGLE_MAPS_MAP_ID;
|
const mapId = import.meta.env.VITE_GOOGLE_MAPS_MAP_ID;
|
||||||
|
|
||||||
// Refs for map container and instances
|
// Refs for map container and instances
|
||||||
const mapRef = useRef<HTMLDivElement>(null);
|
const mapRef = useRef<HTMLDivElement>(null);
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ const SearchResultsMap: React.FC<SearchResultsMapProps> = ({
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const apiKey = process.env.REACT_APP_GOOGLE_MAPS_PUBLIC_API_KEY;
|
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_PUBLIC_API_KEY;
|
||||||
const mapId = process.env.REACT_APP_GOOGLE_MAPS_MAP_ID;
|
const mapId = import.meta.env.VITE_GOOGLE_MAPS_MAP_ID;
|
||||||
|
|
||||||
// Clean up markers
|
// Clean up markers
|
||||||
const clearMarkers = useCallback(() => {
|
const clearMarkers = useCallback(() => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const StripeConnectOnboarding: React.FC<StripeConnectOnboardingProps> = ({
|
|||||||
|
|
||||||
const initializeStripeConnect = useCallback(async () => {
|
const initializeStripeConnect = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const publishableKey = process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY;
|
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||||
if (!publishableKey) {
|
if (!publishableKey) {
|
||||||
throw new Error("Stripe publishable key not configured");
|
throw new Error("Stripe publishable key not configured");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { stripeAPI, rentalAPI } from "../services/api";
|
import { stripeAPI, rentalAPI } from "../services/api";
|
||||||
|
|
||||||
const stripePromise = loadStripe(
|
const stripePromise = loadStripe(
|
||||||
process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY || ""
|
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || ""
|
||||||
);
|
);
|
||||||
|
|
||||||
interface UpdatePaymentMethodProps {
|
interface UpdatePaymentMethodProps {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import reportWebVitals from './reportWebVitals';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
@@ -13,8 +12,3 @@ root.render(
|
|||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||||
reportWebVitals();
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Mock server using Jest mocks instead of MSW.
|
* Mock server using Vitest mocks instead of MSW.
|
||||||
* This provides a simpler setup that works with all Node versions.
|
* This provides a simpler setup that works with all Node versions.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { vi } from 'vitest';
|
||||||
import { mockUser, mockUnverifiedUser, mockItem, mockRental } from './handlers';
|
import { mockUser, mockUnverifiedUser, mockItem, mockRental } from './handlers';
|
||||||
|
|
||||||
// Re-export mock data
|
// Re-export mock data
|
||||||
@@ -10,23 +11,23 @@ export { mockUser, mockUnverifiedUser, mockItem, mockRental };
|
|||||||
|
|
||||||
// Mock server interface for compatibility with setup
|
// Mock server interface for compatibility with setup
|
||||||
export const server = {
|
export const server = {
|
||||||
listen: jest.fn(),
|
listen: vi.fn(),
|
||||||
resetHandlers: jest.fn(),
|
resetHandlers: vi.fn(),
|
||||||
close: jest.fn(),
|
close: vi.fn(),
|
||||||
use: jest.fn(),
|
use: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup axios mock
|
// Setup axios mock
|
||||||
jest.mock('axios', () => {
|
vi.mock('axios', () => {
|
||||||
const mockAxiosInstance = {
|
const mockAxiosInstance = {
|
||||||
get: jest.fn(),
|
get: vi.fn(),
|
||||||
post: jest.fn(),
|
post: vi.fn(),
|
||||||
put: jest.fn(),
|
put: vi.fn(),
|
||||||
delete: jest.fn(),
|
delete: vi.fn(),
|
||||||
patch: jest.fn(),
|
patch: vi.fn(),
|
||||||
interceptors: {
|
interceptors: {
|
||||||
request: { use: jest.fn(), eject: jest.fn() },
|
request: { use: vi.fn(), eject: vi.fn() },
|
||||||
response: { use: jest.fn(), eject: jest.fn() },
|
response: { use: vi.fn(), eject: vi.fn() },
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -36,7 +37,7 @@ jest.mock('axios', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create: jest.fn(() => mockAxiosInstance),
|
create: vi.fn(() => mockAxiosInstance),
|
||||||
default: mockAxiosInstance,
|
default: mockAxiosInstance,
|
||||||
...mockAxiosInstance,
|
...mockAxiosInstance,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { rentalAPI } from "../services/api";
|
|||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|
||||||
const stripePromise = loadStripe(
|
const stripePromise = loadStripe(
|
||||||
process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY || ""
|
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || ""
|
||||||
);
|
);
|
||||||
|
|
||||||
const CompletePayment: React.FC = () => {
|
const CompletePayment: React.FC = () => {
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { ReportHandler } from 'web-vitals';
|
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios, { AxiosError, AxiosRequestConfig } from "axios";
|
import axios, { AxiosError, AxiosRequestConfig } from "axios";
|
||||||
|
|
||||||
const API_BASE_URL = process.env.REACT_APP_API_URL;
|
const API_BASE_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
// CSRF token management
|
// CSRF token management
|
||||||
let csrfToken: string | null = null;
|
let csrfToken: string | null = null;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class SocketService {
|
|||||||
*/
|
*/
|
||||||
private getSocketUrl(): string {
|
private getSocketUrl(): string {
|
||||||
// Use environment variable or default to localhost:5001 (matches backend)
|
// Use environment variable or default to localhost:5001 (matches backend)
|
||||||
return process.env.REACT_APP_BASE_URL || "http://localhost:5001";
|
return import.meta.env.VITE_BASE_URL || "http://localhost:5001";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
* Get the public URL for an image (S3 only)
|
* Get the public URL for an image (S3 only)
|
||||||
*/
|
*/
|
||||||
export const getPublicImageUrl = (
|
export const getPublicImageUrl = (
|
||||||
imagePath: string | null | undefined
|
imagePath: string | null | undefined,
|
||||||
): string => {
|
): string => {
|
||||||
if (!imagePath) return "";
|
if (!imagePath) return "";
|
||||||
|
|
||||||
@@ -19,8 +19,8 @@ export const getPublicImageUrl = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// S3 key (e.g., "profiles/uuid.jpg", "items/uuid.jpg", "forum/uuid.jpg")
|
// S3 key (e.g., "profiles/uuid.jpg", "items/uuid.jpg", "forum/uuid.jpg")
|
||||||
const s3Bucket = process.env.REACT_APP_S3_BUCKET || "";
|
const s3Bucket = import.meta.env.VITE_S3_BUCKET;
|
||||||
const s3Region = process.env.REACT_APP_AWS_REGION || "us-east-1";
|
const s3Region = import.meta.env.VITE_AWS_REGION;
|
||||||
return `https://${s3Bucket}.s3.${s3Region}.amazonaws.com/${imagePath}`;
|
return `https://${s3Bucket}.s3.${s3Region}.amazonaws.com/${imagePath}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ interface UploadOptions {
|
|||||||
*/
|
*/
|
||||||
export async function getPresignedUrl(
|
export async function getPresignedUrl(
|
||||||
uploadType: UploadType,
|
uploadType: UploadType,
|
||||||
file: File
|
file: File,
|
||||||
): Promise<PresignedUrlResponse> {
|
): Promise<PresignedUrlResponse> {
|
||||||
const response = await api.post("/upload/presign", {
|
const response = await api.post("/upload/presign", {
|
||||||
uploadType,
|
uploadType,
|
||||||
@@ -71,7 +71,7 @@ interface BatchPresignResponse {
|
|||||||
*/
|
*/
|
||||||
export async function getPresignedUrls(
|
export async function getPresignedUrls(
|
||||||
uploadType: UploadType,
|
uploadType: UploadType,
|
||||||
files: File[]
|
files: File[],
|
||||||
): Promise<BatchPresignResponse> {
|
): Promise<BatchPresignResponse> {
|
||||||
const response = await api.post("/upload/presign-batch", {
|
const response = await api.post("/upload/presign-batch", {
|
||||||
uploadType,
|
uploadType,
|
||||||
@@ -90,7 +90,7 @@ export async function getPresignedUrls(
|
|||||||
export async function uploadToS3(
|
export async function uploadToS3(
|
||||||
file: File,
|
file: File,
|
||||||
uploadUrl: string,
|
uploadUrl: string,
|
||||||
options: UploadOptions = {}
|
options: UploadOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { onProgress, maxRetries = 3 } = options;
|
const { onProgress, maxRetries = 3 } = options;
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ export async function uploadToS3(
|
|||||||
* Confirm that files have been uploaded to S3
|
* Confirm that files have been uploaded to S3
|
||||||
*/
|
*/
|
||||||
export async function confirmUploads(
|
export async function confirmUploads(
|
||||||
keys: string[]
|
keys: string[],
|
||||||
): Promise<{ confirmed: string[]; total: number }> {
|
): Promise<{ confirmed: string[]; total: number }> {
|
||||||
const response = await api.post("/upload/confirm", { keys });
|
const response = await api.post("/upload/confirm", { keys });
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -145,7 +145,7 @@ export async function confirmUploads(
|
|||||||
export async function uploadFile(
|
export async function uploadFile(
|
||||||
uploadType: UploadType,
|
uploadType: UploadType,
|
||||||
file: File,
|
file: File,
|
||||||
options: UploadOptions = {}
|
options: UploadOptions = {},
|
||||||
): Promise<{ key: string; publicUrl: string }> {
|
): Promise<{ key: string; publicUrl: string }> {
|
||||||
// Get presigned URL
|
// Get presigned URL
|
||||||
const presigned = await getPresignedUrl(uploadType, file);
|
const presigned = await getPresignedUrl(uploadType, file);
|
||||||
@@ -170,7 +170,7 @@ export async function uploadFile(
|
|||||||
*/
|
*/
|
||||||
export async function getSignedUrl(key: string): Promise<string> {
|
export async function getSignedUrl(key: string): Promise<string> {
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
`/upload/signed-url/${encodeURIComponent(key)}`
|
`/upload/signed-url/${encodeURIComponent(key)}`,
|
||||||
);
|
);
|
||||||
return response.data.url;
|
return response.data.url;
|
||||||
}
|
}
|
||||||
@@ -181,7 +181,7 @@ export async function getSignedUrl(key: string): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
export async function getSignedImageUrl(
|
export async function getSignedImageUrl(
|
||||||
baseKey: string,
|
baseKey: string,
|
||||||
size: "thumbnail" | "medium" | "original" = "original"
|
size: "thumbnail" | "medium" | "original" = "original",
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const suffix = getSizeSuffix(size);
|
const suffix = getSizeSuffix(size);
|
||||||
const variantKey = getVariantKey(baseKey, suffix);
|
const variantKey = getVariantKey(baseKey, suffix);
|
||||||
@@ -194,7 +194,7 @@ export async function getSignedImageUrl(
|
|||||||
*/
|
*/
|
||||||
export function getImageUrl(
|
export function getImageUrl(
|
||||||
baseKey: string | null | undefined,
|
baseKey: string | null | undefined,
|
||||||
size: "thumbnail" | "medium" | "original" = "original"
|
size: "thumbnail" | "medium" | "original" = "original",
|
||||||
): string {
|
): string {
|
||||||
if (!baseKey) return "";
|
if (!baseKey) return "";
|
||||||
|
|
||||||
@@ -215,14 +215,18 @@ export interface UploadWithResizeOptions extends UploadOptions {
|
|||||||
export async function uploadImageWithVariants(
|
export async function uploadImageWithVariants(
|
||||||
uploadType: UploadType,
|
uploadType: UploadType,
|
||||||
file: File,
|
file: File,
|
||||||
options: UploadWithResizeOptions = {}
|
options: UploadWithResizeOptions = {},
|
||||||
): Promise<{ baseKey: string; publicUrl: string; variants: string[] }> {
|
): Promise<{ baseKey: string; publicUrl: string; variants: string[] }> {
|
||||||
const { onProgress, skipResize } = options;
|
const { onProgress, skipResize } = options;
|
||||||
|
|
||||||
// If skipping resize, use regular upload
|
// If skipping resize, use regular upload
|
||||||
if (skipResize) {
|
if (skipResize) {
|
||||||
const result = await uploadFile(uploadType, file, { onProgress });
|
const result = await uploadFile(uploadType, file, { onProgress });
|
||||||
return { baseKey: result.key, publicUrl: result.publicUrl, variants: [result.key] };
|
return {
|
||||||
|
baseKey: result.key,
|
||||||
|
publicUrl: result.publicUrl,
|
||||||
|
variants: [result.key],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate resized variants
|
// Generate resized variants
|
||||||
@@ -247,13 +251,20 @@ export async function uploadImageWithVariants(
|
|||||||
if (onProgress) {
|
if (onProgress) {
|
||||||
const fileContribution = (variantFile.size / totalBytes) * percent;
|
const fileContribution = (variantFile.size / totalBytes) * percent;
|
||||||
// Approximate combined progress
|
// Approximate combined progress
|
||||||
onProgress(Math.min(99, Math.round(uploadedBytes / totalBytes * 100 + fileContribution)));
|
onProgress(
|
||||||
|
Math.min(
|
||||||
|
99,
|
||||||
|
Math.round(
|
||||||
|
(uploadedBytes / totalBytes) * 100 + fileContribution,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
uploadedBytes += files[i].size;
|
uploadedBytes += files[i].size;
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Confirm all uploads - use stagingKey if present (image processing enabled), else key
|
// Confirm all uploads - use stagingKey if present (image processing enabled), else key
|
||||||
@@ -264,7 +275,9 @@ export async function uploadImageWithVariants(
|
|||||||
|
|
||||||
// Use the final keys for database storage (not staging keys)
|
// Use the final keys for database storage (not staging keys)
|
||||||
const finalKeys = presignedUrls.map((p) => p.key);
|
const finalKeys = presignedUrls.map((p) => p.key);
|
||||||
const originalKey = finalKeys.find((k) => !k.includes("_th") && !k.includes("_md")) || finalKeys[0];
|
const originalKey =
|
||||||
|
finalKeys.find((k) => !k.includes("_th") && !k.includes("_md")) ||
|
||||||
|
finalKeys[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baseKey: originalKey,
|
baseKey: originalKey,
|
||||||
@@ -280,12 +293,12 @@ export async function uploadImageWithVariants(
|
|||||||
export async function uploadImagesWithVariants(
|
export async function uploadImagesWithVariants(
|
||||||
uploadType: UploadType,
|
uploadType: UploadType,
|
||||||
files: File[],
|
files: File[],
|
||||||
options: UploadWithResizeOptions = {}
|
options: UploadWithResizeOptions = {},
|
||||||
): Promise<{ baseKey: string; publicUrl: string }[]> {
|
): Promise<{ baseKey: string; publicUrl: string }[]> {
|
||||||
if (files.length === 0) return [];
|
if (files.length === 0) return [];
|
||||||
|
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
files.map((file) => uploadImageWithVariants(uploadType, file, options))
|
files.map((file) => uploadImageWithVariants(uploadType, file, options)),
|
||||||
);
|
);
|
||||||
|
|
||||||
return results.map((r) => ({ baseKey: r.baseKey, publicUrl: r.publicUrl }));
|
return results.map((r) => ({ baseKey: r.baseKey, publicUrl: r.publicUrl }));
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
// Vitest setup file
|
||||||
// allows you to do things like:
|
import '@testing-library/jest-dom/vitest';
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
import { vi, beforeAll, afterAll } from 'vitest';
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
|
|
||||||
// Mock window.location for tests that use navigation
|
// Mock window.location for tests that use navigation
|
||||||
const mockLocation = {
|
const mockLocation = {
|
||||||
...window.location,
|
...window.location,
|
||||||
href: 'http://localhost:3000',
|
href: 'http://localhost:3000',
|
||||||
pathname: '/',
|
pathname: '/',
|
||||||
assign: jest.fn(),
|
assign: vi.fn(),
|
||||||
replace: jest.fn(),
|
replace: vi.fn(),
|
||||||
reload: jest.fn(),
|
reload: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.defineProperty(window, 'location', {
|
Object.defineProperty(window, 'location', {
|
||||||
|
|||||||
19
frontend/src/vite-env.d.ts
vendored
Normal file
19
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_URL: string;
|
||||||
|
readonly VITE_BASE_URL: string;
|
||||||
|
readonly VITE_ENV: string;
|
||||||
|
readonly VITE_STRIPE_PUBLISHABLE_KEY: string;
|
||||||
|
readonly VITE_GOOGLE_MAPS_PUBLIC_API_KEY: string;
|
||||||
|
readonly VITE_GOOGLE_MAPS_MAP_ID: string;
|
||||||
|
readonly VITE_GOOGLE_CLIENT_ID: string;
|
||||||
|
readonly VITE_ALPHA_TESTING_ENABLED: string;
|
||||||
|
readonly VITE_S3_BUCKET: string;
|
||||||
|
readonly VITE_S3_ENABLED: string;
|
||||||
|
readonly VITE_AWS_REGION: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
@@ -1,26 +1,21 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "ES2020",
|
||||||
"lib": [
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"dom",
|
"module": "ESNext",
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"types": ["vite/client", "node", "vitest/globals", "google.maps"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"],
|
||||||
"src"
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
frontend/tsconfig.node.json
Normal file
11
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
44
frontend/vite.config.ts
Normal file
44
frontend/vite.config.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/// <reference types="vitest" />
|
||||||
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
|
return {
|
||||||
|
plugins: [react(), tsconfigPaths()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: false,
|
||||||
|
proxy: {
|
||||||
|
'/api': { target: env.VITE_BASE_URL || 'http://localhost:5001', changeOrigin: true },
|
||||||
|
'/socket.io': { target: env.VITE_BASE_URL || 'http://localhost:5001', changeOrigin: true, ws: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: { outDir: 'build', sourcemap: true },
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./src/setupTests.ts'],
|
||||||
|
include: ['src/**/*.{test,spec}.{js,jsx,ts,tsx}', 'src/**/__tests__/**/*.{js,jsx,ts,tsx}'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'lcov', 'html'],
|
||||||
|
include: ['src/**/*.{js,jsx,ts,tsx}'],
|
||||||
|
exclude: [
|
||||||
|
'src/index.tsx',
|
||||||
|
'src/**/*.d.ts',
|
||||||
|
'src/setupTests.ts',
|
||||||
|
'src/setupEnv.ts',
|
||||||
|
'src/test-polyfills.js',
|
||||||
|
'src/mocks/**',
|
||||||
|
'src/__mocks__/**',
|
||||||
|
],
|
||||||
|
thresholds: {
|
||||||
|
lines: 80,
|
||||||
|
statements: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user