Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build/*
.ideaDataSources/
data-test/*
bin/*
.kiro/

# IntelliJ customizations
!.idea/checkstyle-idea.xml
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"snyk.advanced.autoSelectOrganization": true
}
190 changes: 190 additions & 0 deletions admin-wcc-app/__tests__/components/mentors/CreateMentorForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import '@testing-library/jest-dom';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CreateMentorForm from '@/components/CreateMentor/CreateMentorForm';
import * as api from '@/lib/api';
import mockRouter from 'next-router-mock';

jest.mock('next/router', () => jest.requireActual('next-router-mock'));
jest.mock('@/lib/api');
jest.mock('@/lib/auth', () => ({
getStoredToken: jest.fn(() => 'mock-token'),
}));

global.scrollTo = jest.fn();

const mockApiFetch = api.apiFetch as jest.MockedFunction<typeof api.apiFetch>;

const fillRequiredFields = async (user: ReturnType<typeof userEvent.setup>) => {
await user.type(screen.getByLabelText(/full name/i), 'Jane Doe');
await user.type(screen.getByLabelText(/email/i), 'jane@example.com');
await user.type(screen.getByLabelText(/position/i), 'Developer');
await user.type(screen.getByLabelText(/slack display name/i), 'janedoe');
await user.type(screen.getByLabelText(/bio/i), 'Experienced developer');
await user.type(screen.getByLabelText(/years of experience/i), '5');
await user.type(screen.getByLabelText(/ideal mentee/i), 'Eager learners');

const countryInput = screen.getByLabelText(/country/i);
await user.click(countryInput);
await user.type(countryInput, 'United');
const countryOption = await screen.findByRole('option', { name: /united states \(us\)/i });
await user.click(countryOption);

const profileStatusLabel = screen.getByText('Profile Status');
const profileStatusFormControl = profileStatusLabel.closest('.MuiFormControl-root');
const profileStatusSelect = profileStatusFormControl?.querySelector(
'[role="combobox"]'
) as HTMLElement;
fireEvent.mouseDown(profileStatusSelect);
const activeOption = await screen.findByRole('option', { name: /active/i });
await user.click(activeOption);

const technicalAreasInput = screen.getByLabelText(/technical areas/i);
await user.click(technicalAreasInput);
const backendOption = await screen.findByRole('option', { name: /backend/i });
await user.click(backendOption);
await user.click(screen.getByText('Skills & Experience'));

const progLangInput = screen.getByLabelText(/programming languages/i);
await user.click(progLangInput);
const javaOption = await screen.findByRole('option', { name: /^java$/i });
await user.click(javaOption);
await user.click(screen.getByText('Skills & Experience'));

const focusInput = screen.getByLabelText(/mentorship focus/i);
await user.click(focusInput);
const careerOption = await screen.findByRole('option', { name: /career/i });
await user.click(careerOption);
await user.click(screen.getByText('Mentorship Preferences'));

const typeInput = screen.getByLabelText(/mentorship type/i);
await user.click(typeInput);
const adHocOption = await screen.findByRole('option', { name: /ad hoc/i });
await user.click(adHocOption);
await user.click(screen.getByText('Mentorship Preferences'));
};

describe('CreateMentorForm', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it.each([
{ field: 'Full Name', errorMessage: 'Full name is required' },
{ field: 'Email', errorMessage: 'Email is required' },
{ field: 'Position', errorMessage: 'Position is required' },
{ field: 'Slack Display Name', errorMessage: 'Slack display name is required' },
{ field: 'Bio', errorMessage: 'Bio is required' },
{ field: 'Years of Experience', errorMessage: 'Years of experience is required' },
{ field: 'Ideal Mentee', errorMessage: 'Ideal mentee description is required' },
])('does not submit form when $field is empty', async ({ errorMessage }) => {
render(<CreateMentorForm />);

const submitButton = screen.getByRole('button', { name: /create mentor/i });
fireEvent.click(submitButton);

await screen.findByText(errorMessage);
expect(mockApiFetch).not.toHaveBeenCalled();
});

it('shows email validation error for invalid email format', async () => {
const user = userEvent.setup();
render(<CreateMentorForm />);

const emailInput = screen.getByLabelText(/email/i);
await user.type(emailInput, 'invalid-email');
await user.tab();

await screen.findByText('Invalid email format');
expect(mockApiFetch).not.toHaveBeenCalled();
});

it('shows success message when API call succeeds', async () => {
mockApiFetch.mockResolvedValueOnce({});
const user = userEvent.setup();
render(<CreateMentorForm />);

await fillRequiredFields(user);

const submitButton = screen.getByRole('button', { name: /create mentor/i });
await user.click(submitButton);

await screen.findByText('Mentor created successfully!');

expect(mockApiFetch).toHaveBeenCalledWith(
'/api/platform/v1/mentors',
expect.objectContaining({
method: 'POST',
token: 'mock-token',
})
);
});

it('shows error message when API call fails', async () => {
mockApiFetch.mockRejectedValueOnce(new Error('Server error'));
const user = userEvent.setup();
render(<CreateMentorForm />);

await fillRequiredFields(user);

const submitButton = screen.getByRole('button', { name: /create mentor/i });
await user.click(submitButton);

await screen.findByText('Server error');
});

it('sends correct payload to API when form is submitted', async () => {
mockApiFetch.mockResolvedValueOnce({});
const user = userEvent.setup();
render(<CreateMentorForm />);

await fillRequiredFields(user);

const submitButton = screen.getByRole('button', { name: /create mentor/i });
await user.click(submitButton);

await waitFor(() => {
expect(mockApiFetch).toHaveBeenCalled();
});

const callArgs = mockApiFetch.mock.calls[0];
const payload = callArgs[1]?.body;

expect(payload).toMatchObject({
fullName: 'Jane Doe',
email: 'jane@example.com',
position: 'Developer',
slackDisplayName: 'janedoe',
country: {
countryCode: 'US',
countryName: 'United States',
},
memberTypes: ['MENTOR'],
profileStatus: 'ACTIVE',
bio: 'Experienced developer',
skills: {
yearsExperience: 5,
areas: expect.arrayContaining(['BACKEND']),
languages: expect.arrayContaining(['JAVA']),
mentorshipFocus: expect.any(Array),
},
menteeSection: {
mentorshipType: expect.arrayContaining(['AD_HOC']),
availability: [],
idealMentee: 'Eager learners',
additional: '',
},
});
});

it('navigates to mentors list when cancel button is clicked', async () => {
const user = userEvent.setup();
mockRouter.setCurrentUrl('/admin/mentors/create');
render(<CreateMentorForm />);

const cancelButton = screen.getByRole('button', { name: /cancel/i });
await user.click(cancelButton);

expect(mockRouter.pathname).toBe('/admin/mentors');
});
});
126 changes: 126 additions & 0 deletions admin-wcc-app/components/CreateMentor/BasicInfoSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { TextField, Autocomplete, Typography } from '@mui/material';
import Grid from '@mui/material/Grid2';
import { Controller } from 'react-hook-form';
import { COUNTRIES } from '@/lib/countries';
import { FormSectionProps } from './types';

export default function BasicInfoSection({ control, errors }: FormSectionProps) {
return (
<Grid size={12}>
<Typography variant="h6" sx={{ mb: 2 }}>
Basic Information
</Typography>
<Grid container spacing={3}>
<Grid size={12}>
<Controller
name="fullName"
control={control}
render={({ field }) => (
<TextField
{...field}
fullWidth
required
label="Full Name"
error={!!errors.fullName}
helperText={errors.fullName?.message}
/>
)}
/>
</Grid>

<Grid size={{ xs: 12, sm: 6 }}>
<Controller
name="email"
control={control}
render={({ field }) => (
<TextField
{...field}
fullWidth
required
label="Email"
type="email"
error={!!errors.email}
helperText={errors.email?.message}
/>
)}
/>
</Grid>

<Grid size={{ xs: 12, sm: 6 }}>
<Controller
name="slackDisplayName"
control={control}
render={({ field }) => (
<TextField
{...field}
fullWidth
required
label="Slack Display Name"
error={!!errors.slackDisplayName}
helperText={errors.slackDisplayName?.message}
/>
)}
/>
</Grid>

<Grid size={{ xs: 12, sm: 6 }}>
<Controller
name="position"
control={control}
render={({ field }) => (
<TextField
{...field}
fullWidth
required
label="Position"
error={!!errors.position}
helperText={errors.position?.message}
/>
)}
/>
</Grid>

<Grid size={{ xs: 12, sm: 6 }}>
<Controller
name="companyName"
control={control}
render={({ field }) => <TextField {...field} fullWidth label="Company Name" />}
/>
</Grid>

<Grid size={{ xs: 12, sm: 6 }}>
<Controller
name="city"
control={control}
render={({ field }) => <TextField {...field} fullWidth label="City" />}
/>
</Grid>

<Grid size={{ xs: 12, sm: 6 }}>
<Controller
name="country"
control={control}
render={({ field: { onChange, value } }) => (
<Autocomplete
options={COUNTRIES}
value={value}
onChange={(_, newValue) => onChange(newValue)}
getOptionLabel={(option) => `${option.countryName} (${option.countryCode})`}
isOptionEqualToValue={(option, val) => option.countryCode === val.countryCode}
renderInput={(params) => (
<TextField
{...params}
label="Country *"
placeholder="Search country"
error={!!errors.country}
helperText={errors.country?.message}
/>
)}
/>
)}
/>
</Grid>
</Grid>
</Grid>
);
}
Loading
Loading