ensured password reset and account verification works as well as support dashboard
This commit is contained in:
2436
ditch-the-agent/package-lock.json
generated
2436
ditch-the-agent/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@
|
|||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"predeploy": "vite build && cp ./dist/index.html ./dist/404.html",
|
"predeploy": "vite build && cp ./dist/index.html ./dist/404.html",
|
||||||
"deploy": "gh-pages -d dist"
|
"deploy": "gh-pages -d dist",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
@@ -43,6 +44,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/react": "^4.1.1",
|
"@iconify/react": "^4.1.1",
|
||||||
|
"@testing-library/jest-dom": "^6.4.2",
|
||||||
|
"@testing-library/react": "^14.2.1",
|
||||||
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
@@ -52,8 +56,10 @@
|
|||||||
"eslint-plugin-prettier": "^5.5.3",
|
"eslint-plugin-prettier": "^5.5.3",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
|
"jsdom": "^24.0.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
|
"vitest": "^1.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,9 +72,9 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
text: item.description,
|
text: item.description,
|
||||||
created_at: item.created_at,
|
created_at: item.created_at,
|
||||||
updated_at: item.updated_at,
|
updated_at: item.updated_at,
|
||||||
user: -1, // Placeholder
|
user: item.user_email,
|
||||||
user_first_name: 'System',
|
user_first_name: item.user_first_name,
|
||||||
user_last_name: 'Description'
|
user_last_name: item.user_last_name,
|
||||||
}]
|
}]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
result = result.filter(item =>
|
result = result.filter(item =>
|
||||||
item.title.toLowerCase().includes(lowerSearch) ||
|
item.title.toLowerCase().includes(lowerSearch) ||
|
||||||
item.description.toLowerCase().includes(lowerSearch)
|
item.description.toLowerCase().includes(lowerSearch)
|
||||||
// || item.user?.email?.toLowerCase().includes(lowerSearch) // If user object existed
|
|| item.user_email?.toLowerCase().includes(lowerSearch) // If user object existed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +215,26 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseCase = async () => {
|
||||||
|
if (!selectedSupportCaseId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data }: AxiosResponse<SupportCaseApi> = await axiosInstance.patch(
|
||||||
|
`/support/cases/${selectedSupportCaseId}/`,
|
||||||
|
{ status: 'closed' }
|
||||||
|
);
|
||||||
|
|
||||||
|
setSupportCase(prev => prev ? { ...prev, status: data.status, updated_at: data.updated_at } : null);
|
||||||
|
setSupportCases(prevCases =>
|
||||||
|
prevCases.map(c =>
|
||||||
|
c.id === selectedSupportCaseId ? { ...c, status: data.status, updated_at: data.updated_at } : c
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to close case", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <PageLoader />;
|
return <PageLoader />;
|
||||||
}
|
}
|
||||||
@@ -233,7 +253,8 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
sx={{ py: 4, height: '90vh', width: 'fill', display: 'flex', flexDirection: 'column' }}
|
maxWidth={false}
|
||||||
|
sx={{ py: 4, height: '90vh', width: '100%', display: 'flex', flexDirection: 'column' }}
|
||||||
>
|
>
|
||||||
<Paper
|
<Paper
|
||||||
elevation={3}
|
elevation={3}
|
||||||
@@ -242,7 +263,9 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
<Grid container sx={{ height: '100%', width: '100%' }}>
|
<Grid container sx={{ height: '100%', width: '100%' }}>
|
||||||
{/* Left Panel: List & Filters */}
|
{/* Left Panel: List & Filters */}
|
||||||
<Grid
|
<Grid
|
||||||
size={{ xs: 12, md: 4 }}
|
item
|
||||||
|
xs={12}
|
||||||
|
md={4}
|
||||||
sx={{
|
sx={{
|
||||||
borderRight: { md: '1px solid', borderColor: 'grey.200' },
|
borderRight: { md: '1px solid', borderColor: 'grey.200' },
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -337,6 +360,7 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -354,12 +378,21 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
borderColor: 'grey.200',
|
borderColor: 'grey.200',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" component="h3" sx={{ fontWeight: 'bold' }}>
|
<Typography variant="h6" component="h3" sx={{ fontWeight: 'bold' }}>
|
||||||
{supportCase.title} | {supportCase.category}
|
{supportCase.title} | {supportCase.category}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={handleCloseCase}
|
||||||
|
disabled={supportCase.status === 'closed'}
|
||||||
|
>
|
||||||
|
{supportCase.status === 'closed' ? 'Closed' : 'Close Case'}
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3, bgcolor: 'grey.50' }}>
|
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3, bgcolor: 'grey.50' }}>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
import ForgotPassword from './ForgotPassword';
|
||||||
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
|
|
||||||
|
// Mock axiosInstance
|
||||||
|
vi.mock('../../axiosApi.js', () => ({
|
||||||
|
axiosInstance: {
|
||||||
|
post: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ForgotPassword', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
render(<ForgotPassword />);
|
||||||
|
expect(screen.getByText('Forgot Password')).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: /Send Password Reset Link/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates email input', () => {
|
||||||
|
render(<ForgotPassword />);
|
||||||
|
const emailInput = screen.getByLabelText('Email');
|
||||||
|
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
||||||
|
expect(emailInput).toHaveValue('test@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls API and shows success message on valid submission', async () => {
|
||||||
|
(axiosInstance.post as any).mockResolvedValue({ status: 200 });
|
||||||
|
render(<ForgotPassword />);
|
||||||
|
|
||||||
|
const emailInput = screen.getByLabelText('Email');
|
||||||
|
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
||||||
|
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Send Password Reset Link/i });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(submitButton).toBeDisabled();
|
||||||
|
expect(screen.getByText('Sending...')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(axiosInstance.post).toHaveBeenCalledWith('password-reset/', { email: 'test@example.com' });
|
||||||
|
expect(screen.getByText('Password reset link has been sent to your email.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error message on API failure', async () => {
|
||||||
|
const errorMessage = 'User not found';
|
||||||
|
(axiosInstance.post as any).mockRejectedValue({
|
||||||
|
response: {
|
||||||
|
data: {
|
||||||
|
detail: errorMessage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ForgotPassword />);
|
||||||
|
|
||||||
|
const emailInput = screen.getByLabelText('Email');
|
||||||
|
fireEvent.change(emailInput, { target: { value: 'wrong@example.com' } });
|
||||||
|
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Send Password Reset Link/i });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
@@ -10,12 +11,36 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import Image from 'components/base/Image';
|
import Image from 'components/base/Image';
|
||||||
import { Suspense } from 'react';
|
import { Suspense, useState } from 'react';
|
||||||
import forgotPassword from 'assets/authentication-banners/green.png';
|
import forgotPassword from 'assets/authentication-banners/green.png';
|
||||||
import IconifyIcon from 'components/base/IconifyIcon';
|
import IconifyIcon from 'components/base/IconifyIcon';
|
||||||
import logo from 'assets/logo/favicon-logo.png';
|
import logo from 'assets/logo/favicon-logo.png';
|
||||||
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
|
|
||||||
const ForgotPassword = () => {
|
const ForgotPassword = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleResetPassword = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setSuccessMessage(null);
|
||||||
|
setErrorMessage(null);
|
||||||
|
try {
|
||||||
|
await axiosInstance.post('password-reset/', { email });
|
||||||
|
setSuccessMessage('Password reset link has been sent to your email.');
|
||||||
|
} catch (error: any) {
|
||||||
|
setErrorMessage(
|
||||||
|
error.response?.data?.email?.[0] ||
|
||||||
|
error.response?.data?.detail ||
|
||||||
|
'An error occurred. Please try again.',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
@@ -30,14 +55,18 @@ const ForgotPassword = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
<Stack alignItems="center" gap={6.5} width={330} mx="auto">
|
<Stack alignItems="center" gap={6.5} width={330} mx="auto">
|
||||||
<Typography variant="h3">Forgot Password</Typography>
|
<Typography variant="h3">Forgot Password</Typography>
|
||||||
|
{successMessage && <Alert severity="success">{successMessage}</Alert>}
|
||||||
|
{errorMessage && <Alert severity="error">{errorMessage}</Alert>}
|
||||||
<FormControl variant="standard" fullWidth>
|
<FormControl variant="standard" fullWidth>
|
||||||
<InputLabel shrink htmlFor="new-password">
|
<InputLabel shrink htmlFor="email">
|
||||||
Email
|
Email
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<TextField
|
<TextField
|
||||||
variant="filled"
|
variant="filled"
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
id="email"
|
id="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
@@ -47,8 +76,13 @@ const ForgotPassword = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Button variant="contained" fullWidth>
|
<Button
|
||||||
Send Password Reset Link
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
onClick={handleResetPassword}
|
||||||
|
disabled={loading || !email}
|
||||||
|
>
|
||||||
|
{loading ? 'Sending...' : 'Send Password Reset Link'}
|
||||||
</Button>
|
</Button>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Back to{' '}
|
Back to{' '}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { axiosInstance } from '../../axiosApi.js';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Form, Formik } from 'formik';
|
import { Form, Formik } from 'formik';
|
||||||
import { AuthContext } from 'contexts/AuthContext.js';
|
import { AuthContext } from 'contexts/AuthContext.js';
|
||||||
|
import paths from 'routes/paths';
|
||||||
|
|
||||||
type loginValues = {
|
type loginValues = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -50,7 +51,15 @@ const Login = (): ReactElement => {
|
|||||||
|
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const hasErrors = Object.keys(error.response.data).length > 0;
|
if (
|
||||||
|
error.response &&
|
||||||
|
error.response.status === 401 &&
|
||||||
|
error.response.data.detail === 'No active account found with the given credentials'
|
||||||
|
) {
|
||||||
|
navigate(paths.verifyAccount, { state: { email } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hasErrors = error.response && Object.keys(error.response.data).length > 0;
|
||||||
if (hasErrors) {
|
if (hasErrors) {
|
||||||
setErrorMessage(error.response.data);
|
setErrorMessage(error.response.data);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
157
ditch-the-agent/src/pages/authentication/ResetPassword.test.tsx
Normal file
157
ditch-the-agent/src/pages/authentication/ResetPassword.test.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
import ResetPassword from './ResetPassword';
|
||||||
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Mock axiosInstance
|
||||||
|
vi.mock('../../axiosApi.js', () => ({
|
||||||
|
axiosInstance: {
|
||||||
|
post: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock useSearchParams
|
||||||
|
const mockGet = vi.fn();
|
||||||
|
vi.mock('react-router-dom', async () => {
|
||||||
|
const actual = await vi.importActual('react-router-dom');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useSearchParams: () => [{ get: mockGet }],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ResetPassword', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockGet.mockImplementation((key) => {
|
||||||
|
if (key === 'email') return 'test@example.com';
|
||||||
|
if (key === 'code') return '123456';
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<ResetPassword />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.getByRole('heading', { name: /Reset Password/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Reset Code')).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Confirm Password')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: /Reset Password/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates inputs', () => {
|
||||||
|
renderComponent();
|
||||||
|
const emailInput = screen.getByLabelText('Email');
|
||||||
|
const codeInput = screen.getByLabelText('Reset Code');
|
||||||
|
const passwordInput = screen.getByLabelText('Password');
|
||||||
|
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||||
|
|
||||||
|
fireEvent.change(emailInput, { target: { value: 'new@example.com' } });
|
||||||
|
fireEvent.change(codeInput, { target: { value: '654321' } });
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||||
|
|
||||||
|
expect(emailInput).toHaveValue('new@example.com');
|
||||||
|
expect(codeInput).toHaveValue('654321');
|
||||||
|
expect(passwordInput).toHaveValue('password123');
|
||||||
|
expect(confirmInput).toHaveValue('password123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error if passwords do not match', async () => {
|
||||||
|
renderComponent();
|
||||||
|
const passwordInput = screen.getByLabelText('Password');
|
||||||
|
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||||
|
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.change(confirmInput, { target: { value: 'password456' } });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(screen.getByText("Passwords don't match")).toBeInTheDocument();
|
||||||
|
expect(axiosInstance.post).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls API and shows success message on valid submission', async () => {
|
||||||
|
(axiosInstance.post as any).mockResolvedValue({ status: 200 });
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
// Email and Code are pre-filled from mock
|
||||||
|
const passwordInput = screen.getByLabelText('Password');
|
||||||
|
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||||
|
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(submitButton).toBeDisabled();
|
||||||
|
expect(screen.getByText('Resetting...')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(axiosInstance.post).toHaveBeenCalledWith('password-reset/confirm/', {
|
||||||
|
email: 'test@example.com',
|
||||||
|
code: '123456',
|
||||||
|
new_password: 'password123',
|
||||||
|
new_password2: 'password123',
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Reset Successfully')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error message on API failure', async () => {
|
||||||
|
const errorMessage = 'Invalid token';
|
||||||
|
(axiosInstance.post as any).mockRejectedValue({
|
||||||
|
response: {
|
||||||
|
data: {
|
||||||
|
detail: errorMessage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const passwordInput = screen.getByLabelText('Password');
|
||||||
|
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||||
|
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error if email or code is missing', async () => {
|
||||||
|
mockGet.mockReturnValue(null); // No URL params
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
// Inputs should be empty
|
||||||
|
const emailInput = screen.getByLabelText('Email');
|
||||||
|
const codeInput = screen.getByLabelText('Reset Code');
|
||||||
|
expect(emailInput).toHaveValue('');
|
||||||
|
expect(codeInput).toHaveValue('');
|
||||||
|
|
||||||
|
const passwordInput = screen.getByLabelText('Password');
|
||||||
|
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||||
|
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(screen.getByText('Please provide both email and reset code.')).toBeInTheDocument();
|
||||||
|
expect(axiosInstance.post).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ReactElement, Suspense, useState } from 'react';
|
import { ReactElement, Suspense, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -17,30 +18,55 @@ import passwordUpdated from 'assets/authentication-banners/password-updated.png'
|
|||||||
import successTick from 'assets/authentication-banners/successTick.png';
|
import successTick from 'assets/authentication-banners/successTick.png';
|
||||||
import Image from 'components/base/Image';
|
import Image from 'components/base/Image';
|
||||||
import IconifyIcon from 'components/base/IconifyIcon';
|
import IconifyIcon from 'components/base/IconifyIcon';
|
||||||
import PasswordStrengthChecker from '../../components/PasswordStrengthChecker';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
|
|
||||||
const ResetPassword = (): ReactElement => {
|
const ResetPassword = (): ReactElement => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [showNewPassword, setShowNewPassword] = useState(false);
|
const [showNewPassword, setShowNewPassword] = useState(false);
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [email, setEmail] = useState(searchParams.get('email') || '');
|
||||||
|
const [code, setCode] = useState(searchParams.get('code') || '');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [resetSuccessful, setResetSuccessful] = useState(false);
|
||||||
|
|
||||||
const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword);
|
const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword);
|
||||||
const handleClickShowConfirmPassword = () => setShowConfirmPassword(!showConfirmPassword);
|
const handleClickShowConfirmPassword = () => setShowConfirmPassword(!showConfirmPassword);
|
||||||
const [resetSuccessful, setResetSuccessful] = useState(false);
|
|
||||||
|
|
||||||
const handleResetPassword = () => {
|
const handleResetPassword = async () => {
|
||||||
const passwordField: HTMLInputElement = document.getElementById(
|
if (password !== confirmPassword) {
|
||||||
'new-password',
|
setErrorMessage("Passwords don't match");
|
||||||
) as HTMLInputElement;
|
|
||||||
const confirmPasswordField: HTMLInputElement = document.getElementById(
|
|
||||||
'confirm-password',
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
if (passwordField.value !== confirmPasswordField.value) {
|
|
||||||
alert("Passwords don't match");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!email || !code) {
|
||||||
|
setErrorMessage('Please provide both email and reset code.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axiosInstance.post('password-reset/confirm/', {
|
||||||
|
email,
|
||||||
|
code,
|
||||||
|
new_password: password,
|
||||||
|
new_password2: confirmPassword,
|
||||||
|
});
|
||||||
setResetSuccessful(true);
|
setResetSuccessful(true);
|
||||||
|
} catch (error: any) {
|
||||||
|
setErrorMessage(
|
||||||
|
error.response?.data?.detail ||
|
||||||
|
error.response?.data?.non_field_errors?.[0] ||
|
||||||
|
'An error occurred. Please try again.',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,6 +84,45 @@ const ResetPassword = (): ReactElement => {
|
|||||||
{!resetSuccessful ? (
|
{!resetSuccessful ? (
|
||||||
<Stack alignItems="center" gap={3.75} width={330} mx="auto">
|
<Stack alignItems="center" gap={3.75} width={330} mx="auto">
|
||||||
<Typography variant="h3">Reset Password</Typography>
|
<Typography variant="h3">Reset Password</Typography>
|
||||||
|
{errorMessage && <Alert severity="error">{errorMessage}</Alert>}
|
||||||
|
<FormControl variant="standard" fullWidth>
|
||||||
|
<InputLabel shrink htmlFor="email">
|
||||||
|
Email
|
||||||
|
</InputLabel>
|
||||||
|
<TextField
|
||||||
|
variant="filled"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
id="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconifyIcon icon="ic:baseline-email" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl variant="standard" fullWidth>
|
||||||
|
<InputLabel shrink htmlFor="code">
|
||||||
|
Reset Code
|
||||||
|
</InputLabel>
|
||||||
|
<TextField
|
||||||
|
variant="filled"
|
||||||
|
placeholder="Enter reset code"
|
||||||
|
id="code"
|
||||||
|
value={code}
|
||||||
|
onChange={(e) => setCode(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconifyIcon icon="ic:baseline-lock" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
<FormControl variant="standard" fullWidth>
|
<FormControl variant="standard" fullWidth>
|
||||||
<InputLabel shrink htmlFor="new-password">
|
<InputLabel shrink htmlFor="new-password">
|
||||||
Password
|
Password
|
||||||
@@ -67,6 +132,7 @@ const ResetPassword = (): ReactElement => {
|
|||||||
placeholder="Enter new password"
|
placeholder="Enter new password"
|
||||||
type={showNewPassword ? 'text' : 'password'}
|
type={showNewPassword ? 'text' : 'password'}
|
||||||
id="new-password"
|
id="new-password"
|
||||||
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
@@ -89,17 +155,18 @@ const ResetPassword = (): ReactElement => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/*<PasswordStrengthChecker password={password} />*/}
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl variant="standard" fullWidth>
|
<FormControl variant="standard" fullWidth>
|
||||||
<InputLabel shrink htmlFor="confirm-password">
|
<InputLabel shrink htmlFor="confirm-password">
|
||||||
Password
|
Confirm Password
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<TextField
|
<TextField
|
||||||
variant="filled"
|
variant="filled"
|
||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
type={showConfirmPassword ? 'text' : 'password'}
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
id="confirm-password"
|
id="confirm-password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
@@ -122,8 +189,13 @@ const ResetPassword = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Button variant="contained" fullWidth onClick={handleResetPassword}>
|
<Button
|
||||||
Reset Password
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
onClick={handleResetPassword}
|
||||||
|
disabled={loading || !password || !confirmPassword}
|
||||||
|
>
|
||||||
|
{loading ? 'Resetting...' : 'Reset Password'}
|
||||||
</Button>
|
</Button>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Back to{' '}
|
Back to{' '}
|
||||||
@@ -140,7 +212,7 @@ const ResetPassword = (): ReactElement => {
|
|||||||
<Stack alignItems="center" gap={3.75} width={330} mx="auto">
|
<Stack alignItems="center" gap={3.75} width={330} mx="auto">
|
||||||
<Image src={successTick} />
|
<Image src={successTick} />
|
||||||
<Typography variant="h3">Reset Successfully</Typography>
|
<Typography variant="h3">Reset Successfully</Typography>
|
||||||
<Typography variant="body1" textAlign="center" color="text.secndary">
|
<Typography variant="body1" textAlign="center" color="text.secondary">
|
||||||
Your Ditch the Agent log in password has been updated successfully
|
Your Ditch the Agent log in password has been updated successfully
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button variant="contained" fullWidth LinkComponent={Link} href="/authentication/login">
|
<Button variant="contained" fullWidth LinkComponent={Link} href="/authentication/login">
|
||||||
|
|||||||
203
ditch-the-agent/src/pages/authentication/VerifyAccount.tsx
Normal file
203
ditch-the-agent/src/pages/authentication/VerifyAccount.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { ReactElement, Suspense, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
InputAdornment,
|
||||||
|
InputLabel,
|
||||||
|
Link,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import loginBanner from 'assets/authentication-banners/green.png';
|
||||||
|
import IconifyIcon from 'components/base/IconifyIcon';
|
||||||
|
import logo from 'assets/logo/favicon-logo.png';
|
||||||
|
import Image from 'components/base/Image';
|
||||||
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { Form, Formik } from 'formik';
|
||||||
|
import paths from 'routes/paths';
|
||||||
|
|
||||||
|
type verifyValues = {
|
||||||
|
email: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VerifyAccount = (): ReactElement => {
|
||||||
|
const [errorMessage, setErrorMessage] = useState<any | null>(null);
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const initialEmail = location.state?.email || '';
|
||||||
|
|
||||||
|
const handleVerify = async ({ email, code }: verifyValues): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await axiosInstance.post('/check-passcode/', {
|
||||||
|
email: email,
|
||||||
|
code: code,
|
||||||
|
});
|
||||||
|
navigate('/');
|
||||||
|
} catch (error) {
|
||||||
|
const hasErrors = error.response && Object.keys(error.response.data).length > 0;
|
||||||
|
if (hasErrors) {
|
||||||
|
setErrorMessage(error.response.data);
|
||||||
|
} else {
|
||||||
|
setErrorMessage('An unexpected error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
bgcolor="background.paper"
|
||||||
|
boxShadow={(theme) => theme.shadows[3]}
|
||||||
|
height={560}
|
||||||
|
width={{ md: 960 }}
|
||||||
|
>
|
||||||
|
<Stack width={{ md: 0.5 }} m={2.5} gap={10}>
|
||||||
|
<Link href="/" height="fit-content">
|
||||||
|
<Image src={logo} width={82.6} />
|
||||||
|
</Link>
|
||||||
|
<Stack alignItems="center" gap={2.5} width={330} mx="auto">
|
||||||
|
<Typography variant="h3">Verify Account</Typography>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
email: initialEmail,
|
||||||
|
code: '',
|
||||||
|
}}
|
||||||
|
onSubmit={handleVerify}
|
||||||
|
enableReinitialize
|
||||||
|
>
|
||||||
|
{({ setFieldValue, values }) => (
|
||||||
|
<Form>
|
||||||
|
<FormControl variant="standard" fullWidth>
|
||||||
|
{successMessage ? (
|
||||||
|
<Alert severity="success" sx={{ mb: 2 }}>
|
||||||
|
<Typography>{successMessage}</Typography>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
{errorMessage ? (
|
||||||
|
<Alert severity="error">
|
||||||
|
{errorMessage.detail ? (
|
||||||
|
<Typography>{errorMessage.detail}</Typography>
|
||||||
|
) : (
|
||||||
|
<ul>
|
||||||
|
{Object.entries(errorMessage).map(([fieldName, errorMessages]) => (
|
||||||
|
<li key={fieldName}>
|
||||||
|
<strong>{fieldName}</strong>
|
||||||
|
{Array.isArray(errorMessages) ? (
|
||||||
|
<ul>
|
||||||
|
{errorMessages.map((message, index) => (
|
||||||
|
<li key={`${fieldName}-${index}`}>{message}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<span>: {String(errorMessages)}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
<InputLabel shrink htmlFor="email">
|
||||||
|
Email
|
||||||
|
</InputLabel>
|
||||||
|
<TextField
|
||||||
|
variant="filled"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
id="email"
|
||||||
|
value={values.email}
|
||||||
|
onChange={(event) => setFieldValue('email', event.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconifyIcon icon="ic:baseline-email" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl variant="standard" fullWidth sx={{ mt: 2 }}>
|
||||||
|
<InputLabel shrink htmlFor="code">
|
||||||
|
Verification Code
|
||||||
|
</InputLabel>
|
||||||
|
<TextField
|
||||||
|
variant="filled"
|
||||||
|
placeholder="Enter verification code"
|
||||||
|
id="code"
|
||||||
|
value={values.code}
|
||||||
|
onChange={(event) => setFieldValue('code', event.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconifyIcon icon="ic:baseline-lock" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button variant="contained" type={'submit'} fullWidth sx={{ mt: 3 }}>
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
fullWidth
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
onClick={async () => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
setSuccessMessage(null);
|
||||||
|
if (!values.email) {
|
||||||
|
setErrorMessage({ detail: 'Email is required to resend code.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await axiosInstance.post('/resend-registration-email/', {
|
||||||
|
email: values.email,
|
||||||
|
});
|
||||||
|
setSuccessMessage('Registration email sent.');
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage({ detail: 'There was an error sending the verification link.' });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resend Verification Code
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Back to{' '}
|
||||||
|
<Link
|
||||||
|
href={paths.login}
|
||||||
|
underline="hover"
|
||||||
|
fontSize={(theme) => theme.typography.body1.fontSize}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<Skeleton variant="rectangular" height={1} width={1} sx={{ bgcolor: 'primary.main' }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
alt="Login banner"
|
||||||
|
src={loginBanner}
|
||||||
|
sx={{
|
||||||
|
width: 0.5,
|
||||||
|
display: { xs: 'none', md: 'block' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VerifyAccount;
|
||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
home: `/${rootPaths.homeRoot}`,
|
home: `/${rootPaths.homeRoot}`,
|
||||||
dashboard: `/${rootPaths.dashboardRoot}`,
|
dashboard: `/${rootPaths.dashboardRoot}`,
|
||||||
login: `/${rootPaths.authRoot}/login`,
|
login: `/${rootPaths.authRoot}/login`,
|
||||||
|
verifyAccount: `/${rootPaths.authRoot}/verify-account`,
|
||||||
signup: `/${rootPaths.authRoot}/sign-up`,
|
signup: `/${rootPaths.authRoot}/sign-up`,
|
||||||
resetPassword: `/${rootPaths.authRoot}/reset-password`,
|
resetPassword: `/${rootPaths.authRoot}/reset-password`,
|
||||||
forgotPassword: `/${rootPaths.authRoot}/forgot-password`,
|
forgotPassword: `/${rootPaths.authRoot}/forgot-password`,
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const Error404 = lazy(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const Login = lazy(async () => import('pages/authentication/Login'));
|
const Login = lazy(async () => import('pages/authentication/Login'));
|
||||||
|
const VerifyAccount = lazy(async () => import('pages/authentication/VerifyAccount'));
|
||||||
const SignUp = lazy(async () => import('pages/authentication/SignUp'));
|
const SignUp = lazy(async () => import('pages/authentication/SignUp'));
|
||||||
|
|
||||||
const ResetPassword = lazy(async () => import('pages/authentication/ResetPassword'));
|
const ResetPassword = lazy(async () => import('pages/authentication/ResetPassword'));
|
||||||
@@ -179,6 +180,10 @@ const routes: RouteObject[] = [
|
|||||||
path: paths.login,
|
path: paths.login,
|
||||||
element: <Login />,
|
element: <Login />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: paths.verifyAccount,
|
||||||
|
element: <VerifyAccount />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: paths.signup,
|
path: paths.signup,
|
||||||
element: <SignUp />,
|
element: <SignUp />,
|
||||||
|
|||||||
1
ditch-the-agent/src/setupTests.ts
Normal file
1
ditch-the-agent/src/setupTests.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -786,5 +786,8 @@ export interface SupportCaseApi {
|
|||||||
messages: SupportMessageApi[];
|
messages: SupportMessageApi[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
user_email: string;
|
||||||
|
user_first_name: string;
|
||||||
|
user_last_name: string;
|
||||||
}
|
}
|
||||||
// Walk Score API Type Definitions
|
// Walk Score API Type Definitions
|
||||||
|
|||||||
@@ -16,4 +16,9 @@ export default defineConfig({
|
|||||||
port: 3000,
|
port: 3000,
|
||||||
},
|
},
|
||||||
base: '/',
|
base: '/',
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: './src/setupTests.ts',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user