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/
|
||||
.env
|
||||
.env.*
|
||||
uploads/
|
||||
*.log
|
||||
.DS_Store
|
||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -11,6 +11,9 @@
|
||||
# production
|
||||
/build
|
||||
|
||||
# Vite
|
||||
.vite
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
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>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||
@@ -23,15 +23,6 @@
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
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`.
|
||||
-->
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</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",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@googlemaps/js-api-loader": "^1.16.10",
|
||||
"@stripe/connect-js": "^3.3.31",
|
||||
@@ -12,8 +13,7 @@
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.126",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
@@ -24,51 +24,32 @@
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"stripe": "^18.4.0",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start:dev": "dotenv -e .env.dev react-scripts start",
|
||||
"start:qa": "dotenv -e .env.qa react-scripts start",
|
||||
"start:prod": "dotenv -e .env.prod react-scripts start",
|
||||
"build:dev": "dotenv -e .env.dev react-scripts build",
|
||||
"build:qa": "dotenv -e .env.qa react-scripts build",
|
||||
"build:prod": "dotenv -e .env.prod react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"test:coverage": "react-scripts test --coverage --watchAll=false --maxWorkers=4"
|
||||
},
|
||||
"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"
|
||||
]
|
||||
"start:dev": "vite --mode dev",
|
||||
"start:qa": "vite --mode qa",
|
||||
"start:prod": "vite --mode prod",
|
||||
"build:dev": "vite build --mode dev",
|
||||
"build:qa": "vite build --mode qa",
|
||||
"build:prod": "vite build --mode prod",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google.maps": "^3.58.1",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"cross-fetch": "^4.1.0",
|
||||
"dotenv-cli": "^9.0.0",
|
||||
"msw": "^2.11.2"
|
||||
},
|
||||
"overrides": {
|
||||
"nth-check": "^2.1.1",
|
||||
"postcss": "^8.4.31",
|
||||
"svgo": "^3.0.0",
|
||||
"webpack-dev-server": "^5.2.1"
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"msw": "^2.11.2",
|
||||
"vite": "^7.3.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import PrivateRoute from './components/PrivateRoute';
|
||||
import axios from 'axios';
|
||||
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 { showAuthModal, authModalMode, closeAuthModal, user } = useAuth();
|
||||
@@ -77,7 +77,7 @@ const AppContent: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const checkAlphaAccess = async () => {
|
||||
// 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);
|
||||
setCheckingAccess(false);
|
||||
return;
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
/**
|
||||
* Manual axios mock for Jest
|
||||
* Manual axios mock for Vitest
|
||||
* This avoids ESM transformation issues with the axios package
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
|
||||
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: {} })),
|
||||
get: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
post: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
put: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
delete: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
patch: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
request: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
interceptors: {
|
||||
request: {
|
||||
use: jest.fn(() => 0),
|
||||
eject: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
use: vi.fn(() => 0),
|
||||
eject: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
},
|
||||
response: {
|
||||
use: jest.fn(() => 0),
|
||||
eject: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
use: vi.fn(() => 0),
|
||||
eject: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
@@ -35,33 +37,33 @@ const mockAxiosInstance = {
|
||||
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: {} })),
|
||||
getUri: vi.fn(),
|
||||
head: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
options: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
postForm: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
putForm: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
patchForm: vi.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(),
|
||||
create: vi.fn(() => ({ ...mockAxiosInstance })),
|
||||
isAxiosError: vi.fn((error: any) => error?.isAxiosError === true),
|
||||
isCancel: vi.fn(() => false),
|
||||
all: vi.fn((promises: Promise<any>[]) => Promise.all(promises)),
|
||||
spread: vi.fn((callback: Function) => (arr: any[]) => callback(...arr)),
|
||||
toFormData: vi.fn(),
|
||||
formToJSON: vi.fn(),
|
||||
CancelToken: {
|
||||
source: jest.fn(() => ({
|
||||
source: vi.fn(() => ({
|
||||
token: {},
|
||||
cancel: jest.fn(),
|
||||
cancel: vi.fn(),
|
||||
})),
|
||||
},
|
||||
Axios: jest.fn(),
|
||||
AxiosError: jest.fn(),
|
||||
Cancel: jest.fn(),
|
||||
CanceledError: jest.fn(),
|
||||
Axios: vi.fn(),
|
||||
AxiosError: vi.fn(),
|
||||
Cancel: vi.fn(),
|
||||
CanceledError: vi.fn(),
|
||||
VERSION: '1.0.0',
|
||||
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 { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { vi } from 'vitest';
|
||||
import AuthModal from '../../components/AuthModal';
|
||||
|
||||
// Mock the auth context
|
||||
const mockLogin = jest.fn();
|
||||
const mockRegister = jest.fn();
|
||||
const mockLogin = vi.fn();
|
||||
const mockRegister = vi.fn();
|
||||
|
||||
jest.mock('../../contexts/AuthContext', () => ({
|
||||
...jest.requireActual('../../contexts/AuthContext'),
|
||||
useAuth: () => ({
|
||||
login: mockLogin,
|
||||
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>;
|
||||
vi.mock('../../contexts/AuthContext', async () => {
|
||||
const actual = await vi.importActual('../../contexts/AuthContext');
|
||||
return {
|
||||
...actual,
|
||||
useAuth: () => ({
|
||||
login: mockLogin,
|
||||
register: mockRegister,
|
||||
user: null,
|
||||
loading: false,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../components/PasswordInput', () => {
|
||||
return function MockPasswordInput({
|
||||
// Mock child components
|
||||
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,
|
||||
label,
|
||||
value,
|
||||
@@ -59,11 +63,11 @@ jest.mock('../../components/PasswordInput', () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../components/ForgotPasswordModal', () => {
|
||||
return function MockForgotPasswordModal({
|
||||
vi.mock('../../components/ForgotPasswordModal', () => ({
|
||||
default: function MockForgotPasswordModal({
|
||||
show,
|
||||
onHide,
|
||||
onBackToLogin
|
||||
@@ -79,11 +83,11 @@ jest.mock('../../components/ForgotPasswordModal', () => {
|
||||
<button onClick={onHide}>Close</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../components/VerificationCodeModal', () => {
|
||||
return function MockVerificationCodeModal({
|
||||
vi.mock('../../components/VerificationCodeModal', () => ({
|
||||
default: function MockVerificationCodeModal({
|
||||
show,
|
||||
onHide,
|
||||
email,
|
||||
@@ -102,17 +106,17 @@ jest.mock('../../components/VerificationCodeModal', () => {
|
||||
<button onClick={onHide}>Close</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
describe('AuthModal', () => {
|
||||
const defaultProps = {
|
||||
show: true,
|
||||
onHide: jest.fn(),
|
||||
onHide: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// Helper to get email input (it's a textbox with type email)
|
||||
@@ -371,20 +375,33 @@ describe('AuthModal', () => {
|
||||
|
||||
describe('Google OAuth', () => {
|
||||
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;
|
||||
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} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /continue with google/i }));
|
||||
|
||||
// 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
|
||||
window.location = originalLocation;
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: originalLocation,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,16 +8,19 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { vi, type MockedFunction } from 'vitest';
|
||||
import ItemCard from '../../components/ItemCard';
|
||||
import { Item } from '../../types';
|
||||
import { getPublicImageUrl } from '../../services/uploadService';
|
||||
import { getImageUrl, getPublicImageUrl } from '../../services/uploadService';
|
||||
|
||||
// Mock the uploadService
|
||||
jest.mock('../../services/uploadService', () => ({
|
||||
getPublicImageUrl: jest.fn(),
|
||||
vi.mock('../../services/uploadService', () => ({
|
||||
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
|
||||
const renderWithRouter = (component: React.ReactElement) => {
|
||||
@@ -31,10 +34,15 @@ beforeEach(() => {
|
||||
if (imagePath.startsWith('https://')) return 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(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// Mock item data
|
||||
|
||||
@@ -8,33 +8,34 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { vi, type Mock } from 'vitest';
|
||||
import Navbar from '../../components/Navbar';
|
||||
import { rentalAPI, messageAPI } from '../../services/api';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../services/api', () => ({
|
||||
vi.mock('../../services/api', () => ({
|
||||
rentalAPI: {
|
||||
getPendingRequestsCount: jest.fn(),
|
||||
getPendingRequestsCount: vi.fn(),
|
||||
},
|
||||
messageAPI: {
|
||||
getUnreadCount: jest.fn(),
|
||||
getUnreadCount: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock socket context
|
||||
jest.mock('../../contexts/SocketContext', () => ({
|
||||
vi.mock('../../contexts/SocketContext', () => ({
|
||||
useSocket: () => ({
|
||||
onNewMessage: jest.fn(() => () => {}),
|
||||
onMessageRead: jest.fn(() => () => {}),
|
||||
onNewMessage: vi.fn(() => () => {}),
|
||||
onMessageRead: vi.fn(() => () => {}),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Variable to control auth state per test
|
||||
let mockUser: any = null;
|
||||
const mockLogout = jest.fn();
|
||||
const mockOpenAuthModal = jest.fn();
|
||||
const mockLogout = vi.fn();
|
||||
const mockOpenAuthModal = vi.fn();
|
||||
|
||||
jest.mock('../../contexts/AuthContext', () => ({
|
||||
vi.mock('../../contexts/AuthContext', () => ({
|
||||
useAuth: () => ({
|
||||
user: mockUser,
|
||||
logout: mockLogout,
|
||||
@@ -43,11 +44,14 @@ jest.mock('../../contexts/AuthContext', () => ({
|
||||
}));
|
||||
|
||||
// Mock useNavigate
|
||||
const mockNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockNavigate,
|
||||
}));
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
// Helper to render with Router
|
||||
const renderWithRouter = (component: React.ReactElement) => {
|
||||
@@ -56,12 +60,12 @@ const renderWithRouter = (component: React.ReactElement) => {
|
||||
|
||||
describe('Navbar', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
mockUser = null;
|
||||
|
||||
// Default mock implementations
|
||||
(rentalAPI.getPendingRequestsCount as jest.Mock).mockResolvedValue({ data: { count: 0 } });
|
||||
(messageAPI.getUnreadCount as jest.Mock).mockResolvedValue({ data: { count: 0 } });
|
||||
(rentalAPI.getPendingRequestsCount as Mock).mockResolvedValue({ data: { count: 0 } });
|
||||
(messageAPI.getUnreadCount as Mock).mockResolvedValue({ data: { count: 0 } });
|
||||
});
|
||||
|
||||
describe('Branding', () => {
|
||||
@@ -193,42 +197,70 @@ describe('Navbar', () => {
|
||||
it('should show profile link in dropdown', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('should show renting link in dropdown', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('should show owning link in dropdown', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('should show messages link in dropdown', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('should show forum link in dropdown', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('should show earnings link in dropdown', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('should call logout and navigate home when logout is clicked', () => {
|
||||
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 });
|
||||
fireEvent.click(logoutButton);
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import React from '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 { mockUser } from '../../mocks/handlers';
|
||||
|
||||
// Mock the API module
|
||||
jest.mock('../../services/api', () => {
|
||||
vi.mock('../../services/api', () => {
|
||||
const mockAuthAPI = {
|
||||
login: jest.fn(),
|
||||
register: jest.fn(),
|
||||
googleLogin: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
getStatus: jest.fn(),
|
||||
login: vi.fn(),
|
||||
register: vi.fn(),
|
||||
googleLogin: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
getStatus: vi.fn(),
|
||||
};
|
||||
|
||||
const mockFetchCSRFToken = jest.fn().mockResolvedValue('test-csrf-token');
|
||||
const mockResetCSRFToken = jest.fn();
|
||||
const mockFetchCSRFToken = vi.fn().mockResolvedValue('test-csrf-token');
|
||||
const mockResetCSRFToken = vi.fn();
|
||||
|
||||
return {
|
||||
authAPI: mockAuthAPI,
|
||||
@@ -26,9 +27,18 @@ jest.mock('../../services/api', () => {
|
||||
// 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>;
|
||||
const mockAuthAPI = authAPI as Mocked<typeof authAPI>;
|
||||
const mockFetchCSRFToken = fetchCSRFToken as MockedFunction<typeof fetchCSRFToken>;
|
||||
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
|
||||
const TestComponent: React.FC = () => {
|
||||
@@ -64,17 +74,17 @@ const renderWithAuth = (ui: React.ReactElement = <TestComponent />) => {
|
||||
|
||||
describe('AuthContext', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
// Default: user is authenticated
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: true, user: mockUser },
|
||||
});
|
||||
mockAuthAPI.getStatus.mockResolvedValue(
|
||||
mockAxiosResponse({ 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(() => {});
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
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 () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
mockAuthAPI.getStatus.mockResolvedValue(
|
||||
mockAxiosResponse({ authenticated: false, user: null })
|
||||
);
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
@@ -128,13 +138,13 @@ describe('AuthContext', () => {
|
||||
|
||||
describe('Login', () => {
|
||||
it('logs in successfully with valid credentials', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
mockAuthAPI.getStatus.mockResolvedValue(
|
||||
mockAxiosResponse({ authenticated: false, user: null })
|
||||
);
|
||||
|
||||
mockAuthAPI.login.mockResolvedValue({
|
||||
data: { user: mockUser },
|
||||
});
|
||||
mockAuthAPI.login.mockResolvedValue(
|
||||
mockAxiosResponse({ user: mockUser })
|
||||
);
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
@@ -159,9 +169,9 @@ describe('AuthContext', () => {
|
||||
});
|
||||
|
||||
it('keeps user as null when login fails', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
mockAuthAPI.getStatus.mockResolvedValue(
|
||||
mockAxiosResponse({ authenticated: false, user: null })
|
||||
);
|
||||
|
||||
mockAuthAPI.login.mockRejectedValue(new Error('Invalid credentials'));
|
||||
|
||||
@@ -213,13 +223,13 @@ describe('AuthContext', () => {
|
||||
|
||||
describe('Registration', () => {
|
||||
it('registers a new user successfully', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
mockAuthAPI.getStatus.mockResolvedValue(
|
||||
mockAxiosResponse({ authenticated: false, user: null })
|
||||
);
|
||||
|
||||
mockAuthAPI.register.mockResolvedValue({
|
||||
data: { user: { ...mockUser, email: 'new@example.com', isVerified: false } },
|
||||
});
|
||||
mockAuthAPI.register.mockResolvedValue(
|
||||
mockAxiosResponse({ user: { ...mockUser, email: 'new@example.com', isVerified: false } })
|
||||
);
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
@@ -239,13 +249,13 @@ describe('AuthContext', () => {
|
||||
|
||||
describe('Google Login', () => {
|
||||
it('logs in with Google successfully', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
mockAuthAPI.getStatus.mockResolvedValue(
|
||||
mockAxiosResponse({ authenticated: false, user: null })
|
||||
);
|
||||
|
||||
mockAuthAPI.googleLogin.mockResolvedValue({
|
||||
data: { user: mockUser },
|
||||
});
|
||||
mockAuthAPI.googleLogin.mockResolvedValue(
|
||||
mockAxiosResponse({ user: mockUser })
|
||||
);
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
@@ -267,7 +277,7 @@ describe('AuthContext', () => {
|
||||
|
||||
describe('Logout', () => {
|
||||
it('logs out successfully', async () => {
|
||||
mockAuthAPI.logout.mockResolvedValue({ data: { message: 'Logged out' } });
|
||||
mockAuthAPI.logout.mockResolvedValue(mockAxiosResponse({ message: 'Logged out' }));
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
@@ -403,9 +413,9 @@ describe('AuthContext', () => {
|
||||
mockAuthAPI.getStatus.mockImplementation(() => {
|
||||
callCount++;
|
||||
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();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { vi } from 'vitest';
|
||||
import { useAddressAutocomplete, usStates } from '../../hooks/useAddressAutocomplete';
|
||||
import { PlaceDetails } from '../../services/placesService';
|
||||
|
||||
@@ -153,7 +154,7 @@ describe('useAddressAutocomplete', () => {
|
||||
});
|
||||
|
||||
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 = {
|
||||
formattedAddress: '123 Street, City, XX 12345, USA',
|
||||
@@ -236,7 +237,7 @@ describe('useAddressAutocomplete', () => {
|
||||
});
|
||||
|
||||
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
|
||||
const invalidPlace = {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* direct uploads, and signed URL generation for private content.
|
||||
*/
|
||||
|
||||
import { vi, type Mocked } from 'vitest';
|
||||
import api from '../../services/api';
|
||||
import {
|
||||
getPublicImageUrl,
|
||||
@@ -13,15 +14,14 @@ import {
|
||||
uploadToS3,
|
||||
confirmUploads,
|
||||
uploadFile,
|
||||
uploadFiles,
|
||||
getSignedUrl,
|
||||
PresignedUrlResponse,
|
||||
} from '../../services/uploadService';
|
||||
|
||||
// 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
|
||||
class MockXMLHttpRequest {
|
||||
@@ -93,11 +93,11 @@ const originalXMLHttpRequest = global.XMLHttpRequest;
|
||||
|
||||
describe('Upload Service', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
MockXMLHttpRequest.reset();
|
||||
// Reset environment variables
|
||||
process.env.REACT_APP_S3_BUCKET = 'test-bucket';
|
||||
process.env.REACT_APP_AWS_REGION = 'us-east-1';
|
||||
// Reset environment variables using stubEnv for Vitest
|
||||
vi.stubEnv('VITE_S3_BUCKET', 'test-bucket');
|
||||
vi.stubEnv('VITE_AWS_REGION', 'us-east-1');
|
||||
// Mock XMLHttpRequest globally
|
||||
(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';
|
||||
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', () => {
|
||||
@@ -153,6 +147,7 @@ describe('Upload Service', () => {
|
||||
const mockResponse: PresignedUrlResponse = {
|
||||
uploadUrl: 'https://presigned-url.s3.amazonaws.com/items/uuid.jpg?signature=abc',
|
||||
key: 'items/550e8400-e29b-41d4-a716-446655440000.jpg',
|
||||
stagingKey: null,
|
||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/550e8400-e29b-41d4-a716-446655440000.jpg',
|
||||
expiresAt: new Date().toISOString(),
|
||||
};
|
||||
@@ -213,19 +208,21 @@ describe('Upload Service', () => {
|
||||
{
|
||||
uploadUrl: 'https://presigned-url1.s3.amazonaws.com',
|
||||
key: 'items/uuid1.jpg',
|
||||
stagingKey: null,
|
||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
|
||||
expiresAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
uploadUrl: 'https://presigned-url2.s3.amazonaws.com',
|
||||
key: 'items/uuid2.png',
|
||||
stagingKey: null,
|
||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
|
||||
expiresAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
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);
|
||||
|
||||
@@ -236,15 +233,15 @@ describe('Upload Service', () => {
|
||||
{ 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 () => {
|
||||
mockedApi.post.mockResolvedValue({ data: { uploads: [] } });
|
||||
mockedApi.post.mockResolvedValue({ data: { uploads: [], baseKey: undefined } });
|
||||
|
||||
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 () => {
|
||||
const onProgress = jest.fn();
|
||||
const onProgress = vi.fn();
|
||||
|
||||
await uploadToS3(mockFile, mockUploadUrl, { onProgress });
|
||||
|
||||
@@ -318,6 +315,7 @@ describe('Upload Service', () => {
|
||||
const presignResponse: PresignedUrlResponse = {
|
||||
uploadUrl: 'https://presigned.s3.amazonaws.com/items/uuid.jpg',
|
||||
key: 'items/uuid.jpg',
|
||||
stagingKey: null,
|
||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid.jpg',
|
||||
expiresAt: new Date().toISOString(),
|
||||
};
|
||||
@@ -362,7 +360,7 @@ describe('Upload Service', () => {
|
||||
});
|
||||
|
||||
it('should pass onProgress to uploadToS3', async () => {
|
||||
const onProgress = jest.fn();
|
||||
const onProgress = vi.fn();
|
||||
|
||||
mockedApi.post.mockResolvedValueOnce({ data: presignResponse });
|
||||
mockedApi.post.mockResolvedValueOnce({
|
||||
@@ -396,150 +394,8 @@ describe('Upload Service', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadFiles', () => {
|
||||
const mockFiles = [
|
||||
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',
|
||||
}));
|
||||
});
|
||||
});
|
||||
// Note: uploadFiles function was removed from uploadService and replaced with uploadImagesWithVariants
|
||||
// Tests for batch uploads would need to be updated to test the new function
|
||||
|
||||
describe('getSignedUrl', () => {
|
||||
it('should request signed URL for private content', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
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 [code, setCode] = useState("");
|
||||
|
||||
@@ -101,7 +101,7 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
||||
};
|
||||
|
||||
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 scope = "openid email profile";
|
||||
const responseType = "code";
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { stripeAPI, rentalAPI } from "../services/api";
|
||||
|
||||
const stripePromise = loadStripe(
|
||||
process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY || ""
|
||||
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || ""
|
||||
);
|
||||
|
||||
interface EmbeddedStripeCheckoutProps {
|
||||
|
||||
@@ -32,8 +32,8 @@ const GoogleMapWithRadius: React.FC<GoogleMapWithRadiusProps> = ({
|
||||
const { zoom = 12 } = mapOptions;
|
||||
|
||||
// Get API key and Map ID from environment
|
||||
const apiKey = process.env.REACT_APP_GOOGLE_MAPS_PUBLIC_API_KEY;
|
||||
const mapId = process.env.REACT_APP_GOOGLE_MAPS_MAP_ID;
|
||||
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_PUBLIC_API_KEY;
|
||||
const mapId = import.meta.env.VITE_GOOGLE_MAPS_MAP_ID;
|
||||
|
||||
// Refs for map container and instances
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -45,8 +45,8 @@ const SearchResultsMap: React.FC<SearchResultsMapProps> = ({
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const apiKey = process.env.REACT_APP_GOOGLE_MAPS_PUBLIC_API_KEY;
|
||||
const mapId = process.env.REACT_APP_GOOGLE_MAPS_MAP_ID;
|
||||
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_PUBLIC_API_KEY;
|
||||
const mapId = import.meta.env.VITE_GOOGLE_MAPS_MAP_ID;
|
||||
|
||||
// Clean up markers
|
||||
const clearMarkers = useCallback(() => {
|
||||
|
||||
@@ -31,7 +31,7 @@ const StripeConnectOnboarding: React.FC<StripeConnectOnboardingProps> = ({
|
||||
|
||||
const initializeStripeConnect = useCallback(async () => {
|
||||
try {
|
||||
const publishableKey = process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY;
|
||||
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||
if (!publishableKey) {
|
||||
throw new Error("Stripe publishable key not configured");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { stripeAPI, rentalAPI } from "../services/api";
|
||||
|
||||
const stripePromise = loadStripe(
|
||||
process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY || ""
|
||||
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || ""
|
||||
);
|
||||
|
||||
interface UpdatePaymentMethodProps {
|
||||
|
||||
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
@@ -13,8 +12,3 @@ root.render(
|
||||
<App />
|
||||
</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.
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import { mockUser, mockUnverifiedUser, mockItem, mockRental } from './handlers';
|
||||
|
||||
// Re-export mock data
|
||||
@@ -10,23 +11,23 @@ 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(),
|
||||
listen: vi.fn(),
|
||||
resetHandlers: vi.fn(),
|
||||
close: vi.fn(),
|
||||
use: vi.fn(),
|
||||
};
|
||||
|
||||
// Setup axios mock
|
||||
jest.mock('axios', () => {
|
||||
vi.mock('axios', () => {
|
||||
const mockAxiosInstance = {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
interceptors: {
|
||||
request: { use: jest.fn(), eject: jest.fn() },
|
||||
response: { use: jest.fn(), eject: jest.fn() },
|
||||
request: { use: vi.fn(), eject: vi.fn() },
|
||||
response: { use: vi.fn(), eject: vi.fn() },
|
||||
},
|
||||
defaults: {
|
||||
headers: {
|
||||
@@ -36,7 +37,7 @@ jest.mock('axios', () => {
|
||||
};
|
||||
|
||||
return {
|
||||
create: jest.fn(() => mockAxiosInstance),
|
||||
create: vi.fn(() => mockAxiosInstance),
|
||||
default: mockAxiosInstance,
|
||||
...mockAxiosInstance,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { rentalAPI } from "../services/api";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
|
||||
const stripePromise = loadStripe(
|
||||
process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY || ""
|
||||
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || ""
|
||||
);
|
||||
|
||||
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";
|
||||
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL;
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
// CSRF token management
|
||||
let csrfToken: string | null = null;
|
||||
|
||||
@@ -43,7 +43,7 @@ class SocketService {
|
||||
*/
|
||||
private getSocketUrl(): string {
|
||||
// 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)
|
||||
*/
|
||||
export const getPublicImageUrl = (
|
||||
imagePath: string | null | undefined
|
||||
imagePath: string | null | undefined,
|
||||
): string => {
|
||||
if (!imagePath) return "";
|
||||
|
||||
@@ -19,8 +19,8 @@ export const getPublicImageUrl = (
|
||||
}
|
||||
|
||||
// S3 key (e.g., "profiles/uuid.jpg", "items/uuid.jpg", "forum/uuid.jpg")
|
||||
const s3Bucket = process.env.REACT_APP_S3_BUCKET || "";
|
||||
const s3Region = process.env.REACT_APP_AWS_REGION || "us-east-1";
|
||||
const s3Bucket = import.meta.env.VITE_S3_BUCKET;
|
||||
const s3Region = import.meta.env.VITE_AWS_REGION;
|
||||
return `https://${s3Bucket}.s3.${s3Region}.amazonaws.com/${imagePath}`;
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ interface UploadOptions {
|
||||
*/
|
||||
export async function getPresignedUrl(
|
||||
uploadType: UploadType,
|
||||
file: File
|
||||
file: File,
|
||||
): Promise<PresignedUrlResponse> {
|
||||
const response = await api.post("/upload/presign", {
|
||||
uploadType,
|
||||
@@ -71,7 +71,7 @@ interface BatchPresignResponse {
|
||||
*/
|
||||
export async function getPresignedUrls(
|
||||
uploadType: UploadType,
|
||||
files: File[]
|
||||
files: File[],
|
||||
): Promise<BatchPresignResponse> {
|
||||
const response = await api.post("/upload/presign-batch", {
|
||||
uploadType,
|
||||
@@ -90,7 +90,7 @@ export async function getPresignedUrls(
|
||||
export async function uploadToS3(
|
||||
file: File,
|
||||
uploadUrl: string,
|
||||
options: UploadOptions = {}
|
||||
options: UploadOptions = {},
|
||||
): Promise<void> {
|
||||
const { onProgress, maxRetries = 3 } = options;
|
||||
|
||||
@@ -133,7 +133,7 @@ export async function uploadToS3(
|
||||
* Confirm that files have been uploaded to S3
|
||||
*/
|
||||
export async function confirmUploads(
|
||||
keys: string[]
|
||||
keys: string[],
|
||||
): Promise<{ confirmed: string[]; total: number }> {
|
||||
const response = await api.post("/upload/confirm", { keys });
|
||||
return response.data;
|
||||
@@ -145,7 +145,7 @@ export async function confirmUploads(
|
||||
export async function uploadFile(
|
||||
uploadType: UploadType,
|
||||
file: File,
|
||||
options: UploadOptions = {}
|
||||
options: UploadOptions = {},
|
||||
): Promise<{ key: string; publicUrl: string }> {
|
||||
// Get presigned URL
|
||||
const presigned = await getPresignedUrl(uploadType, file);
|
||||
@@ -170,7 +170,7 @@ export async function uploadFile(
|
||||
*/
|
||||
export async function getSignedUrl(key: string): Promise<string> {
|
||||
const response = await api.get(
|
||||
`/upload/signed-url/${encodeURIComponent(key)}`
|
||||
`/upload/signed-url/${encodeURIComponent(key)}`,
|
||||
);
|
||||
return response.data.url;
|
||||
}
|
||||
@@ -181,7 +181,7 @@ export async function getSignedUrl(key: string): Promise<string> {
|
||||
*/
|
||||
export async function getSignedImageUrl(
|
||||
baseKey: string,
|
||||
size: "thumbnail" | "medium" | "original" = "original"
|
||||
size: "thumbnail" | "medium" | "original" = "original",
|
||||
): Promise<string> {
|
||||
const suffix = getSizeSuffix(size);
|
||||
const variantKey = getVariantKey(baseKey, suffix);
|
||||
@@ -194,7 +194,7 @@ export async function getSignedImageUrl(
|
||||
*/
|
||||
export function getImageUrl(
|
||||
baseKey: string | null | undefined,
|
||||
size: "thumbnail" | "medium" | "original" = "original"
|
||||
size: "thumbnail" | "medium" | "original" = "original",
|
||||
): string {
|
||||
if (!baseKey) return "";
|
||||
|
||||
@@ -215,14 +215,18 @@ export interface UploadWithResizeOptions extends UploadOptions {
|
||||
export async function uploadImageWithVariants(
|
||||
uploadType: UploadType,
|
||||
file: File,
|
||||
options: UploadWithResizeOptions = {}
|
||||
options: UploadWithResizeOptions = {},
|
||||
): Promise<{ baseKey: string; publicUrl: string; variants: string[] }> {
|
||||
const { onProgress, skipResize } = options;
|
||||
|
||||
// If skipping resize, use regular upload
|
||||
if (skipResize) {
|
||||
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
|
||||
@@ -247,13 +251,20 @@ export async function uploadImageWithVariants(
|
||||
if (onProgress) {
|
||||
const fileContribution = (variantFile.size / totalBytes) * percent;
|
||||
// Approximate combined progress
|
||||
onProgress(Math.min(99, Math.round(uploadedBytes / totalBytes * 100 + fileContribution)));
|
||||
onProgress(
|
||||
Math.min(
|
||||
99,
|
||||
Math.round(
|
||||
(uploadedBytes / totalBytes) * 100 + fileContribution,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
}).then(() => {
|
||||
uploadedBytes += files[i].size;
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// 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)
|
||||
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 {
|
||||
baseKey: originalKey,
|
||||
@@ -280,12 +293,12 @@ export async function uploadImageWithVariants(
|
||||
export async function uploadImagesWithVariants(
|
||||
uploadType: UploadType,
|
||||
files: File[],
|
||||
options: UploadWithResizeOptions = {}
|
||||
options: UploadWithResizeOptions = {},
|
||||
): Promise<{ baseKey: string; publicUrl: string }[]> {
|
||||
if (files.length === 0) return [];
|
||||
|
||||
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 }));
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
// Vitest setup file
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { vi, beforeAll, afterAll } from 'vitest';
|
||||
|
||||
// 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(),
|
||||
assign: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
reload: vi.fn(),
|
||||
};
|
||||
|
||||
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": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": 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": [
|
||||
"src"
|
||||
]
|
||||
"include": ["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