diff --git a/playwright-tests/tests/becomeAMentee.page.spec.ts b/playwright-tests/tests/becomeAMentee.page.spec.ts
index 71d32bc..b033db4 100644
--- a/playwright-tests/tests/becomeAMentee.page.spec.ts
+++ b/playwright-tests/tests/becomeAMentee.page.spec.ts
@@ -4,7 +4,9 @@ import { HomePage } from '@pages/home.page';
import { MentorshipPage } from '@pages/mentorship.page';
test('Validate "Become a Mentee" section and Find a Mentor button', async ({
- page, mentorshipPage, homePage
+ page,
+ mentorshipPage,
+ homePage,
}) => {
// Navigate to Mentorship page
await page.goto('/mentorship');
@@ -22,7 +24,7 @@ test('Validate "Become a Mentee" section and Find a Mentor button', async ({
];
await expect(mentorshipPage.menteeListItems).toHaveText(items);
-
+
await homePage.findMentorButton.click();
await expect(page).toHaveURL(/\/mentorship\/mentors/);
});
diff --git a/src/__tests__/pages/MenteeRegistrationPage.test.tsx b/src/__tests__/pages/MenteeRegistrationPage.test.tsx
index c46e2af..250016f 100644
--- a/src/__tests__/pages/MenteeRegistrationPage.test.tsx
+++ b/src/__tests__/pages/MenteeRegistrationPage.test.tsx
@@ -22,15 +22,18 @@ jest.mock('next/router', () => ({
useRouter: () => ({ push: jest.fn(), pathname: '/' }),
}));
-// Mutable flag so individual tests can override the registration state
+// Mutable flags so individual tests can override registration state
let mockIsRegistrationOpen = true;
+let mockIsAdhocCycle = false;
-// Mock the registration toggle
jest.mock('../../utils/mentorshipConstants', () => ({
...jest.requireActual('../../utils/mentorshipConstants'),
get IS_REGISTRATION_OPEN() {
return mockIsRegistrationOpen;
},
+ get IS_ADHOC_CYCLE() {
+ return mockIsAdhocCycle;
+ },
}));
const renderPage = () =>
@@ -185,3 +188,112 @@ describe('MenteeRegistrationPage - registration closed', () => {
expect(screen.queryByText('Step 1 of 3')).not.toBeInTheDocument();
});
});
+
+describe('MenteeRegistrationPage - adhoc cycle', () => {
+ beforeEach(() => {
+ mockIsAdhocCycle = true;
+ globalThis.fetch = jest.fn().mockResolvedValue({
+ ok: true,
+ json: jest.fn().mockResolvedValue([]),
+ });
+ });
+
+ afterEach(() => {
+ mockIsAdhocCycle = false;
+ jest.resetAllMocks();
+ });
+
+ it('shows ad-hoc breadcrumb label', () => {
+ renderPage();
+ expect(screen.getByText('Ad-hoc Mentee Registration')).toBeInTheDocument();
+ });
+
+ it('does not render available hours per month field', () => {
+ renderPage();
+ expect(screen.queryByPlaceholderText('e.g. 4')).not.toBeInTheDocument();
+ });
+
+ it('navigates to step 2 after filling required fields', async () => {
+ renderPage();
+
+ fireEvent.change(screen.getByPlaceholderText('Jane Doe'), {
+ target: { value: 'Jane Doe' },
+ });
+ fireEvent.change(screen.getByPlaceholderText('jane@example.com'), {
+ target: { value: 'jane@example.com' },
+ });
+ fireEvent.change(screen.getByPlaceholderText('@jane'), {
+ target: { value: '@jane' },
+ });
+
+ const countrySelect = screen.getByRole('combobox');
+ fireEvent.mouseDown(countrySelect);
+ const countryOption = await screen.findByRole('option', {
+ name: /United Kingdom/i,
+ });
+ fireEvent.click(countryOption);
+
+ fireEvent.change(screen.getByPlaceholderText('London'), {
+ target: { value: 'London' },
+ });
+ fireEvent.change(
+ screen.getByPlaceholderText('e.g. Frontend Developer, Student'),
+ { target: { value: 'Developer' } },
+ );
+ fireEvent.change(screen.getByPlaceholderText('Acme Corp'), {
+ target: { value: 'Tech Corp' },
+ });
+ fireEvent.change(
+ screen.getByPlaceholderText('https://www.linkedin.com/in/yourprofile'),
+ { target: { value: 'https://www.linkedin.com/in/janedoe' } },
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /next/i }));
+
+ await waitFor(() => {
+ expect(screen.getByText('Step 2 of 3')).toBeInTheDocument();
+ });
+ });
+
+ it('shows mentorship goals field on step 2', async () => {
+ renderPage();
+
+ fireEvent.change(screen.getByPlaceholderText('Jane Doe'), {
+ target: { value: 'Jane Doe' },
+ });
+ fireEvent.change(screen.getByPlaceholderText('jane@example.com'), {
+ target: { value: 'jane@example.com' },
+ });
+ fireEvent.change(screen.getByPlaceholderText('@jane'), {
+ target: { value: '@jane' },
+ });
+
+ const countrySelect = screen.getByRole('combobox');
+ fireEvent.mouseDown(countrySelect);
+ const countryOption = await screen.findByRole('option', {
+ name: /United Kingdom/i,
+ });
+ fireEvent.click(countryOption);
+
+ fireEvent.change(screen.getByPlaceholderText('London'), {
+ target: { value: 'London' },
+ });
+ fireEvent.change(
+ screen.getByPlaceholderText('e.g. Frontend Developer, Student'),
+ { target: { value: 'Developer' } },
+ );
+ fireEvent.change(screen.getByPlaceholderText('Acme Corp'), {
+ target: { value: 'Tech Corp' },
+ });
+ fireEvent.change(
+ screen.getByPlaceholderText('https://www.linkedin.com/in/yourprofile'),
+ { target: { value: 'https://www.linkedin.com/in/janedoe' } },
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /next/i }));
+
+ await waitFor(() => {
+ expect(screen.getByText('Mentorship goals *')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/__tests__/schemas/menteeSchema.test.ts b/src/__tests__/schemas/menteeSchema.test.ts
new file mode 100644
index 0000000..2e7a575
--- /dev/null
+++ b/src/__tests__/schemas/menteeSchema.test.ts
@@ -0,0 +1,107 @@
+import {
+ adhocMenteeFormDefaultValues,
+ menteeFormSchema,
+} from '../../schemas/menteeSchema';
+
+const validLongTermBase = {
+ fullName: 'Jane Doe',
+ position: 'Developer',
+ email: 'jane@example.com',
+ slackDisplayName: '@jane',
+ companyName: 'Acme Corp',
+ country: { countryCode: 'GB', countryName: 'United Kingdom' },
+ city: 'London',
+ linkedInProfile: 'https://www.linkedin.com/in/janedoe',
+ pronouns: '',
+ availableHsMonth: 4,
+ skills: {
+ yearsExperience: 2,
+ areas: [{ technicalArea: 'FRONTEND', proficiencyLevel: 'INTERMEDIATE' }],
+ languages: [{ language: 'TYPESCRIPT', proficiencyLevel: 'INTERMEDIATE' }],
+ mentorshipFocus: ['GROW_BEGINNER_TO_MID'],
+ },
+ spokenLanguages: ['English'],
+ bio: 'A'.repeat(50),
+ mentorshipType: 'LONG_TERM' as const,
+ applications: [{ mentorId: 1, priorityOrder: 1, whyMentor: 'A'.repeat(50) }],
+};
+
+const validAdhocBase = {
+ ...validLongTermBase,
+ mentorshipType: 'AD_HOC' as const,
+ availableHsMonth: 1,
+};
+
+describe('menteeFormSchema — long-term', () => {
+ it('accepts valid long-term data', () => {
+ const result = menteeFormSchema.safeParse(validLongTermBase);
+ expect(result.success).toBe(true);
+ });
+
+ it('rejects long-term data with no mentorshipFocus', () => {
+ const data = {
+ ...validLongTermBase,
+ skills: { ...validLongTermBase.skills, mentorshipFocus: [] },
+ };
+ const result = menteeFormSchema.safeParse(data);
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ const paths = result.error.issues.map((i) => i.path.join('.'));
+ expect(paths).toContain('skills.mentorshipFocus');
+ }
+ });
+});
+
+describe('menteeFormSchema — adhoc', () => {
+ it('accepts valid adhoc data', () => {
+ const result = menteeFormSchema.safeParse(validAdhocBase);
+ expect(result.success).toBe(true);
+ });
+
+ it('requires mentorshipFocus for adhoc', () => {
+ const data = {
+ ...validAdhocBase,
+ skills: { ...validAdhocBase.skills, mentorshipFocus: [] },
+ };
+ const result = menteeFormSchema.safeParse(data);
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ const paths = result.error.issues.map((i) => i.path.join('.'));
+ expect(paths).toContain('skills.mentorshipFocus');
+ }
+ });
+
+ it('accepts adhoc data with mentorshipFocus selected', () => {
+ const result = menteeFormSchema.safeParse(validAdhocBase);
+ expect(result.success).toBe(true);
+ });
+});
+
+describe('menteeFormSchema — availableHsMonth', () => {
+ it('rejects long-term data with availableHsMonth less than 2', () => {
+ const data = { ...validLongTermBase, availableHsMonth: 1 };
+ const result = menteeFormSchema.safeParse(data);
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ const paths = result.error.issues.map((i) => i.path.join('.'));
+ expect(paths).toContain('availableHsMonth');
+ }
+ });
+
+ it('accepts long-term data with availableHsMonth of 2', () => {
+ const data = { ...validLongTermBase, availableHsMonth: 2 };
+ const result = menteeFormSchema.safeParse(data);
+ expect(result.success).toBe(true);
+ });
+
+ it('accepts adhoc data with availableHsMonth of 1', () => {
+ const result = menteeFormSchema.safeParse(validAdhocBase);
+ expect(result.success).toBe(true);
+ });
+});
+
+describe('adhocMenteeFormDefaultValues', () => {
+ it('sets mentorshipType to AD_HOC', () => {
+ expect(adhocMenteeFormDefaultValues.mentorshipType).toBe('AD_HOC');
+ });
+});
diff --git a/src/components/mentorship/MenteeStep1BasicInfo.tsx b/src/components/mentorship/MenteeStep1BasicInfo.tsx
index 64a70eb..978f803 100644
--- a/src/components/mentorship/MenteeStep1BasicInfo.tsx
+++ b/src/components/mentorship/MenteeStep1BasicInfo.tsx
@@ -2,6 +2,7 @@ import {
FormControl,
FormControlLabel,
FormHelperText,
+ FormLabel,
Grid,
InputLabel,
MenuItem,
@@ -20,7 +21,20 @@ import { COUNTRIES } from '@utils/mentorshipConstants';
import { inputStyle } from './mentorshipStyles';
import StepSection from './StepSection';
-const MenteeStep1BasicInfo = () => {
+const boolToRadioValue = (value: boolean | null | undefined): string => {
+ if (value === true) return 'yes';
+ if (value === false) return 'no';
+ if (value === null) return 'unspecified';
+ return '';
+};
+
+const radioValueToBool = (value: string): boolean | null => {
+ if (value === 'yes') return true;
+ if (value === 'no') return false;
+ return null;
+};
+
+const MenteeStep1BasicInfo = ({ isAdhoc = false }: { isAdhoc?: boolean }) => {
const {
register,
control,
@@ -239,59 +253,76 @@ const MenteeStep1BasicInfo = () => {
-
- Available hours per month *
-
(
- field.onChange(parseInt(e.target.value) || 0)}
- sx={inputStyle}
- />
+ render={({ field }) => (
+
+
+ Do you identify as a woman or non-binary?
+
+ {
+ field.onChange(radioValueToBool(e.target.value));
+ }}
+ >
+ }
+ label="Yes"
+ />
+ } label="No" />
+ }
+ label="Prefer not to say"
+ />
+
+
)}
/>
-
-
+ {!isAdhoc && (
+
- Mentorship type
+ Available hours per month *
-
- }
- label="Long-term"
- disabled
- />
- }
- label="Ad-hoc (coming soon)"
- disabled
- />
-
-
- Only long-term mentorship is available for this month.
-
-
-
+ (
+
+ field.onChange(Number.parseInt(e.target.value) || 0)
+ }
+ sx={inputStyle}
+ />
+ )}
+ />
+
+ )}
);
diff --git a/src/components/mentorship/MenteeStep2Skills.tsx b/src/components/mentorship/MenteeStep2Skills.tsx
index d87a139..8739a6f 100644
--- a/src/components/mentorship/MenteeStep2Skills.tsx
+++ b/src/components/mentorship/MenteeStep2Skills.tsx
@@ -28,19 +28,25 @@ import StepSection from './StepSection';
const EXPERIENCE_OPTIONS = MENTEE_EXPERIENCE_OPTIONS;
-const MenteeStep2Skills = () => {
+interface Props {
+ isAdhoc?: boolean;
+}
+
+const MenteeStep2Skills = ({ isAdhoc = false }: Props) => {
const {
control,
register,
formState: { errors },
} = useFormContext();
- const skillsErrors = errors.skills as any;
-
return (
{/* Years of experience */}
@@ -92,9 +98,6 @@ const MenteeStep2Skills = () => {
groups={TECHNICAL_AREA_GROUPS}
proficiencyLevels={PROFICIENCY_LEVELS}
/>
- {skillsErrors?.areas && (
- {skillsErrors.areas.message}
- )}
{/* Programming languages with proficiency */}
@@ -110,63 +113,60 @@ const MenteeStep2Skills = () => {
languages={CODE_LANGUAGES}
proficiencyLevels={PROFICIENCY_LEVELS}
/>
- {skillsErrors?.languages && (
-
- {skillsErrors.languages.message}
-
- )}
- {/* Mentorship focus */}
+ {/* Mentorship goals */}
-
- Mentorship goals *
-
-
- Select the goals you want to achieve through mentorship.
-
- (
-
-
- {MENTORSHIP_FOCUS_AREAS.map((area) => (
- {
- if (e.target.checked) {
- field.onChange([
- ...(field.value ?? []),
- area.value,
- ]);
- } else {
- field.onChange(
- (field.value ?? []).filter(
- (v: string) => v !== area.value,
- ),
- );
- }
- }}
- />
- }
- label={area.label}
- />
- ))}
-
- {error && (
- {error.message}
- )}
-
- )}
- />
+ <>
+
+ Mentorship goals *
+
+
+ Select the goals you want to achieve through mentorship.
+
+ (
+
+
+ {MENTORSHIP_FOCUS_AREAS.map((area) => (
+ {
+ if (e.target.checked) {
+ field.onChange([
+ ...(field.value ?? []),
+ area.value,
+ ]);
+ } else {
+ field.onChange(
+ (field.value ?? []).filter(
+ (v: string) => v !== area.value,
+ ),
+ );
+ }
+ }}
+ />
+ }
+ label={area.label}
+ />
+ ))}
+
+ {error && (
+ {error.message}
+ )}
+
+ )}
+ />
+ >
{/* Spoken languages */}
diff --git a/src/pages/api/mentors.ts b/src/pages/api/mentors.ts
index 5ff6ce2..6c8bc7e 100644
--- a/src/pages/api/mentors.ts
+++ b/src/pages/api/mentors.ts
@@ -41,6 +41,11 @@ export default async function handler(
Array.isArray(language) ? language[0] : language,
);
if (focus) params.append('focus', Array.isArray(focus) ? focus[0] : focus);
+ if (mentorshipTypes)
+ params.append(
+ 'mentorshipTypes',
+ Array.isArray(mentorshipTypes) ? mentorshipTypes[0] : mentorshipTypes,
+ );
const data = await proxyRequest('mentorship/mentors', {
method: 'GET',
diff --git a/src/pages/mentorship/mentee-registration.tsx b/src/pages/mentorship/mentee-registration.tsx
index ac85c4b..85c91a2 100644
--- a/src/pages/mentorship/mentee-registration.tsx
+++ b/src/pages/mentorship/mentee-registration.tsx
@@ -17,6 +17,7 @@ import React, { useEffect, useState } from 'react';
import { FormProvider, UseFormReturn, useForm } from 'react-hook-form';
import {
+ adhocMenteeFormDefaultValues,
menteeFormDefaultValues,
menteeFormSchema,
MenteeFormData,
@@ -26,10 +27,36 @@ import MenteeStep2Skills from 'components/mentorship/MenteeStep2Skills';
import MenteeStep3Applications from 'components/mentorship/MenteeStep3Applications';
import { MentorOption } from 'components/mentorship/MentorApplicationCard';
import RegistrationClosed from 'components/mentorship/RegistrationClosed';
-import { IS_REGISTRATION_OPEN } from 'utils/mentorshipConstants';
+import {
+ IS_ADHOC_CYCLE,
+ IS_REGISTRATION_OPEN,
+} from 'utils/mentorshipConstants';
const TOTAL_STEPS = 3;
+const postMenteeRegistration = async (
+ payload: unknown,
+): Promise => {
+ try {
+ const response = await fetch('/api/mentee-registration', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ });
+ if (!response.ok) {
+ const body = await response.json().catch(() => null);
+ return (
+ body?.message ??
+ body?.error ??
+ 'Something went wrong. Please try again.'
+ );
+ }
+ return null;
+ } catch {
+ return 'Network error. Please check your connection and try again.';
+ }
+};
+
const validateStep1 = async (formMethods: UseFormReturn) =>
formMethods.trigger([
'fullName',
@@ -40,8 +67,6 @@ const validateStep1 = async (formMethods: UseFormReturn) =>
'position',
'companyName',
'linkedInProfile',
- 'availableHsMonth',
- 'mentorshipType',
]);
const validateStep2 = async (formMethods: UseFormReturn) =>
@@ -54,14 +79,27 @@ const validateStep2 = async (formMethods: UseFormReturn) =>
'bio',
]);
+const getStepValidator = (
+ step: number,
+ formMethods: UseFormReturn,
+): Promise => {
+ if (step === 1) return validateStep1(formMethods);
+ if (step === 2) return validateStep2(formMethods);
+ return Promise.resolve(true);
+};
+
+// NOSONAR
const MenteeRegistrationPage = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const registrationOpen = IS_REGISTRATION_OPEN;
+ const isAdhoc = IS_ADHOC_CYCLE;
const formMethods = useForm({
resolver: zodResolver(menteeFormSchema),
- defaultValues: menteeFormDefaultValues,
+ defaultValues: isAdhoc
+ ? adhocMenteeFormDefaultValues
+ : menteeFormDefaultValues,
mode: 'onChange',
});
@@ -72,7 +110,8 @@ const MenteeRegistrationPage = () => {
useEffect(() => {
if (!registrationOpen) return;
- fetch('/api/mentors')
+ const mentorshipTypeParam = isAdhoc ? 'Ad-Hoc' : 'Long-Term';
+ fetch(`/api/mentors?mentorshipTypes=${mentorshipTypeParam}`)
.then((res) => res.json())
.then((data) => {
const mentorList: MentorOption[] = (data.mentors ?? data ?? []).map(
@@ -87,14 +126,10 @@ const MenteeRegistrationPage = () => {
.catch(() => {
// silently fall back to empty list — user can still submit if API is down
});
- }, [registrationOpen]);
+ }, [registrationOpen, isAdhoc]);
const handleNext = async () => {
- let isValid;
- if (activeStep === 1) isValid = await validateStep1(formMethods);
- else if (activeStep === 2) isValid = await validateStep2(formMethods);
- else isValid = true;
-
+ const isValid = await getStepValidator(activeStep, formMethods);
if (isValid && activeStep < TOTAL_STEPS) {
setActiveStep((prev) => prev + 1);
window.scrollTo(0, 0);
@@ -111,7 +146,7 @@ const MenteeRegistrationPage = () => {
const onSubmit = async (data: MenteeFormData) => {
setSubmitError(null);
- const networkLinks = data.network ?? [];
+ const networkLinks = [...(data.network ?? [])];
if (data.linkedInProfile) {
networkLinks.push({
type: 'LINKEDIN' as const,
@@ -127,9 +162,9 @@ const MenteeRegistrationPage = () => {
slackDisplayName: data.slackDisplayName,
country: data.country ?? { countryCode: '', countryName: '' },
city: data.city,
- companyName: data.companyName ?? '',
+ companyName: data.companyName,
pronouns: data.pronouns ?? '',
- pronounCategory: data.pronounCategory,
+
isWomen: data.isWomen,
images: [],
network: networkLinks,
@@ -147,30 +182,13 @@ const MenteeRegistrationPage = () => {
})),
};
- try {
- const response = await fetch('/api/mentee-registration', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload),
- });
-
- if (!response.ok) {
- const body = await response.json().catch(() => null);
- const message =
- body?.message ??
- body?.error ??
- 'Something went wrong. Please try again.';
- setSubmitError(message);
- return;
- }
-
- setSubmitted(true);
- window.scrollTo(0, 0);
- } catch {
- setSubmitError(
- 'Network error. Please check your connection and try again.',
- );
+ const error = await postMenteeRegistration(payload);
+ if (error) {
+ setSubmitError(error);
+ return;
}
+ setSubmitted(true);
+ window.scrollTo(0, 0);
};
return (
@@ -189,7 +207,9 @@ const MenteeRegistrationPage = () => {
Mentorship
- Mentee Registration
+
+ {isAdhoc ? 'Ad-hoc Mentee Registration' : 'Mentee Registration'}
+
@@ -212,12 +232,14 @@ const MenteeRegistrationPage = () => {
px: { xs: 2, sm: 3 },
maxWidth: isMobile ? '100%' : theme.custom?.innerBox?.maxWidth,
margin: '0 auto',
- ...(!registrationOpen && {
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- minHeight: '70vh',
- }),
+ ...(registrationOpen
+ ? {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ minHeight: '70vh',
+ }
+ : {}),
}}
>
{
color="text.secondary"
sx={{ mb: 3 }}
>
- Thank you for applying to our mentorship programme. We will
- review your application and get back to you soon.
+ {isAdhoc
+ ? 'Thank you for applying to our ad-hoc mentorship programme. We will review your application and get back to you soon.'
+ : 'Thank you for applying to our mentorship programme. We will review your application and get back to you soon.'}