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
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,16 @@ describe('EditMentorForm', () => {
});
});

it('Given a valid mentorId, when form loads, then books array is joined as newline-separated text', async () => {
mockGetMentorById.mockResolvedValue(fakeMentor);

render(<EditMentorForm mentorId="7" />);

await waitFor(() => {
const booksField = screen.getByLabelText(/recommend books/i);
expect(booksField).toHaveValue('Clean Code\nRefactoring');
});
});
// it.skip('Given a valid mentorId, when form loads, then books array is joined as newline-separated text', async () => {
// mockGetMentorById.mockResolvedValue(fakeMentor);
//
// render(<EditMentorForm mentorId="7" />);
//
// await waitFor(() => {
// const booksField = screen.getByLabelText(/recommend books/i);
// expect(booksField).toHaveValue('Clean Code\nRefactoring');
// });
// });

it('Given a valid mentorId, when form loads, then profile picture section is shown', async () => {
mockGetMentorById.mockResolvedValue(fakeMentor);
Expand Down
176 changes: 95 additions & 81 deletions admin-wcc-app/__tests__/components/mentors/CreateMentorForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jest.mock('@/lib/auth', () => ({
getStoredToken: jest.fn(() => 'mock-token'),
}));

const timeout = 30000;

global.scrollTo = jest.fn();

const mockApiFetch = api.apiFetch as jest.MockedFunction<typeof api.apiFetch>;
Expand Down Expand Up @@ -105,87 +107,99 @@ describe('CreateMentorForm', () => {
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([
expect.objectContaining({ technicalArea: 'BACKEND', proficiencyLevel: 'INTERMEDIATE' }),
]),
languages: expect.arrayContaining([
expect.objectContaining({ language: 'JAVA', proficiencyLevel: 'INTERMEDIATE' }),
]),
mentorshipFocus: expect.any(Array),
},
menteeSection: {
mentorshipType: expect.arrayContaining(['AD_HOC']),
availability: [],
idealMentee: 'Eager learners',
additional: '',
},
});
});
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',
})
);
},
timeout
);

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');
},
timeout
);

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([
expect.objectContaining({ technicalArea: 'BACKEND', proficiencyLevel: 'INTERMEDIATE' }),
]),
languages: expect.arrayContaining([
expect.objectContaining({ language: 'JAVA', proficiencyLevel: 'INTERMEDIATE' }),
]),
mentorshipFocus: expect.any(Array),
},
menteeSection: {
mentorshipType: expect.arrayContaining(['AD_HOC']),
availability: [],
idealMentee: 'Eager learners',
additional: '',
},
});
},
timeout
);

it('navigates to mentors list when cancel button is clicked', async () => {
const user = userEvent.setup();
Expand Down
5 changes: 5 additions & 0 deletions admin-wcc-app/components/AdminLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
Mentor Dashboard
</Button>
)}
{isMentor && (
<Button component={Link} href="/admin/mentor/profile" color="inherit">
My Profile
</Button>
)}
{(isAdmin || isMentorshipAdmin || isLeader) && (
<Button component={Link} href="/admin/mentors" color="inherit">
Mentors
Expand Down
18 changes: 9 additions & 9 deletions admin-wcc-app/components/CreateMentor/CreateMentorForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,21 @@ export default function CreateMentorForm() {
});

const transformFormData = (data: MentorFormData) => ({
fullName: data.fullName,
position: data.position,
email: data.email,
slackDisplayName: data.slackDisplayName,
fullName: data.fullName.trim(),
position: data.position.trim(),
email: data.email.trim(),
slackDisplayName: data.slackDisplayName.trim(),
country: {
countryCode: data.country?.countryCode,
countryName: data.country?.countryName,
},
city: data.city,
companyName: data.companyName,
city: (data.city ?? '').trim(),
companyName: (data.companyName ?? '').trim(),
memberTypes: data.memberTypes,
images: data.images,
network: data.network,
profileStatus: data.profileStatus,
bio: data.bio,
bio: data.bio.trim(),
spokenLanguages: data.spokenLanguages,
skills: {
yearsExperience: Number(data.yearsExperience),
Expand All @@ -80,8 +80,8 @@ export default function CreateMentorForm() {
menteeSection: {
mentorshipType: data.mentorshipType,
availability: [],
idealMentee: data.idealMentee,
additional: data.additionalInfo,
idealMentee: data.idealMentee.trim(),
additional: (data.additionalInfo ?? '').trim(),
},
});

Expand Down
44 changes: 32 additions & 12 deletions admin-wcc-app/components/EditMentor/EditMentorForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ function buildDefaultValues(mentor: MentorItem): EditMentorFormData {
})),
mentorshipFocus: mentor.skills?.mentorshipFocus ?? [],
mentorshipType: deriveMentorshipType(mentor.menteeSection),
longTermNumMentee: mentor.menteeSection?.longTerm?.numMentee ?? 1,
longTermHours: mentor.menteeSection?.longTerm?.hours ?? 2,
idealMentee: mentor.menteeSection?.idealMentee ?? '',
additionalInfo: mentor.menteeSection?.additional ?? '',
monthAvailability: MONTHS.map((month) => ({
Expand All @@ -96,11 +98,13 @@ export default function EditMentorForm({ mentorId }: EditMentorFormProps) {
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [profilePictureUrl, setProfilePictureUrl] = useState<string | undefined>(undefined);
const [profilePictureUploading, setProfilePictureUploading] = useState(false);
const [adHocWarning, setAdHocWarning] = useState<string | null>(null);

const {
control,
handleSubmit,
reset,
setValue,
formState: { errors },
} = useForm<EditMentorFormData>({
resolver: zodResolver(editMentorSchema),
Expand Down Expand Up @@ -139,19 +143,19 @@ export default function EditMentorForm({ mentorId }: EditMentorFormProps) {
}, [mentorId, reset]);

const transformFormData = (data: EditMentorFormData) => ({
fullName: data.fullName,
position: data.position,
email: data.email,
slackDisplayName: data.slackDisplayName,
fullName: data.fullName.trim(),
position: data.position.trim(),
email: data.email.trim(),
slackDisplayName: data.slackDisplayName.trim(),
country: {
countryCode: data.country?.countryCode,
countryName: data.country?.countryName,
},
city: data.city,
companyName: data.companyName,
city: data.city.trim(),
companyName: (data.companyName ?? '').trim(),
memberTypes: ['MENTOR'],
network: data.network,
bio: data.bio,
bio: data.bio.trim(),
spokenLanguages: data.spokenLanguages,
skills: {
yearsExperience: Number(data.yearsExperience),
Expand All @@ -160,9 +164,11 @@ export default function EditMentorForm({ mentorId }: EditMentorFormProps) {
mentorshipFocus: data.mentorshipFocus,
},
menteeSection: {
idealMentee: data.idealMentee,
additional: data.additionalInfo,
longTerm: data.mentorshipType.includes('LONG_TERM') ? { numMentee: 1, hours: 2 } : null,
idealMentee: data.idealMentee.trim(),
additional: (data.additionalInfo ?? '').trim(),
longTerm: data.mentorshipType.includes('LONG_TERM')
? { numMentee: data.longTermNumMentee, hours: data.longTermHours }
: null,
adHoc: data.mentorshipType.includes('AD_HOC')
? data.monthAvailability
.filter((m) => m.enabled)
Expand Down Expand Up @@ -223,6 +229,16 @@ export default function EditMentorForm({ mentorId }: EditMentorFormProps) {
setLoading(true);
setApiError(null);
setSuccessMessage(null);
setAdHocWarning(null);

const hasAdHoc = data.mentorshipType.includes('AD_HOC');
const hasEnabledMonth = data.monthAvailability.some((m) => m.enabled && m.hours > 0);

if (hasAdHoc && !hasEnabledMonth) {
setAdHocWarning('Please mark at least one month as available before saving.');
setLoading(false);
return;
}

try {
const token = getStoredToken();
Expand Down Expand Up @@ -321,8 +337,12 @@ export default function EditMentorForm({ mentorId }: EditMentorFormProps) {
<PersonalInfoSection control={control} errors={errors} />
<BioSection control={control} errors={errors} />
<SkillsSection control={control} errors={errors} />
<MentorshipAvailabilitySection control={control} errors={errors} />
<ResourcesSection control={control} />
<MentorshipAvailabilitySection
control={control}
errors={errors}
setValue={setValue}
adHocError={adHocWarning}
/>
</Stack>

<Box sx={{ mt: 3 }}>
Expand Down
Loading
Loading