diff --git a/frontend/__tests__/unit/components/ProgramActions.test.tsx b/frontend/__tests__/unit/components/ProgramActions.test.tsx index d3d53dfe2d..096acc5bec 100644 --- a/frontend/__tests__/unit/components/ProgramActions.test.tsx +++ b/frontend/__tests__/unit/components/ProgramActions.test.tsx @@ -42,7 +42,7 @@ describe('ProgramActions', () => { }) test('renders and toggles dropdown', () => { - render() + render() const button = screen.getByTestId('program-actions-button') fireEvent.click(button) expect(screen.getByText('Add Module')).toBeInTheDocument() @@ -52,7 +52,7 @@ describe('ProgramActions', () => { }) test('handles Add Module action', () => { - render() + render() const button = screen.getByTestId('program-actions-button') fireEvent.click(button) fireEvent.click(screen.getByRole('menuitem', { name: /add module/i })) @@ -61,7 +61,7 @@ describe('ProgramActions', () => { }) test('handles Publish action', () => { - render() + render() const button = screen.getByTestId('program-actions-button') fireEvent.click(button) fireEvent.click(screen.getByRole('menuitem', { name: /publish program/i })) @@ -70,7 +70,7 @@ describe('ProgramActions', () => { }) test('handles Move to Draft action', () => { - render() + render() const button = screen.getByTestId('program-actions-button') fireEvent.click(button) fireEvent.click(screen.getByRole('menuitem', { name: /move to draft/i })) @@ -78,7 +78,7 @@ describe('ProgramActions', () => { }) test('handles Mark as Completed action', () => { - render() + render() const button = screen.getByTestId('program-actions-button') fireEvent.click(button) fireEvent.click(screen.getByRole('menuitem', { name: /mark as completed/i })) @@ -88,7 +88,7 @@ describe('ProgramActions', () => { test('dropdown closes on outside click', () => { render(
- +
) diff --git a/frontend/__tests__/unit/components/ProgramCard.test.tsx b/frontend/__tests__/unit/components/ProgramCard.test.tsx index 10cdde9362..589ce1d7de 100644 --- a/frontend/__tests__/unit/components/ProgramCard.test.tsx +++ b/frontend/__tests__/unit/components/ProgramCard.test.tsx @@ -1,6 +1,7 @@ import { faEye } from '@fortawesome/free-regular-svg-icons' import { faEdit } from '@fortawesome/free-solid-svg-icons' import { fireEvent, render, screen } from '@testing-library/react' +import { useRouter } from 'next/navigation' import React from 'react' import type { Program } from 'types/mentorship' import { ProgramStatusEnum } from 'types/mentorship' @@ -15,6 +16,10 @@ jest.mock('@fortawesome/react-fontawesome', () => ({ ), })) +jest.mock('hooks/useUpdateProgramStatus', () => ({ + useUpdateProgramStatus: () => ({ updateProgramStatus: jest.fn() }), +})) + jest.mock('components/ActionButton', () => ({ __esModule: true, default: ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => ( @@ -33,7 +38,6 @@ jest.mock('@heroui/tooltip', () => ({ })) describe('ProgramCard', () => { - const mockOnEdit = jest.fn() const mockOnView = jest.fn() const baseMockProgram: Program = { @@ -53,44 +57,44 @@ describe('ProgramCard', () => { describe('Basic Rendering', () => { it('renders program name correctly', () => { - render() + render( + + ) expect(screen.getByText('Test Program')).toBeInTheDocument() }) it('renders program description correctly', () => { - render() - - expect(screen.getByText('This is a test program description')).toBeInTheDocument() - }) - }) - - describe('Access Level - Admin', () => { - it('shows user role badge when accessLevel is admin', () => { render( ) - expect(screen.getByText('admin')).toBeInTheDocument() + expect(screen.getByText('This is a test program description')).toBeInTheDocument() }) + }) - it('shows Preview and Edit buttons for admin access', () => { + describe('Access Level - Admin', () => { + it('shows user role badge when accessLevel is admin', () => { render( ) - expect(screen.getByText('Preview')).toBeInTheDocument() - expect(screen.getByText('Edit')).toBeInTheDocument() + expect(screen.getByText('admin')).toBeInTheDocument() }) it('calls onView when Preview button is clicked', () => { @@ -98,7 +102,7 @@ describe('ProgramCard', () => { ) @@ -109,32 +113,48 @@ describe('ProgramCard', () => { expect(mockOnView).toHaveBeenCalledWith('test-program') }) - it('calls onEdit when Edit button is clicked', () => { + it('navigates to edit page when Edit Program is clicked', () => { + const router = useRouter() + render( ) - const editButton = screen.getByText('Edit').closest('button') - fireEvent.click(editButton!) + fireEvent.click(screen.getByTestId('program-actions-button')) + fireEvent.click(screen.getByText('Edit Program')) - expect(mockOnEdit).toHaveBeenCalledWith('test-program') + expect(router.push).toHaveBeenCalledWith('/my/mentorship/programs/test-program/edit') }) }) describe('Access Level - User', () => { it('does not show user role badge when accessLevel is user', () => { - render() + render( + + ) expect(screen.queryByText('admin')).not.toBeInTheDocument() }) it('shows only View Details button for user access', () => { - render() + render( + + ) expect(screen.getByText('View Details')).toBeInTheDocument() expect(screen.queryByText('Preview')).not.toBeInTheDocument() @@ -142,7 +162,14 @@ describe('ProgramCard', () => { }) it('calls onView when View Details button is clicked', () => { - render() + render( + + ) const viewButton = screen.getByText('View Details').closest('button') fireEvent.click(viewButton!) @@ -154,7 +181,14 @@ describe('ProgramCard', () => { describe('User Role Badge Styling', () => { it('applies admin role styling', () => { const adminProgram = { ...baseMockProgram, userRole: 'admin' } - render() + render( + + ) const badge = screen.getByText('admin') expect(badge).toHaveClass('bg-blue-100', 'text-blue-800') @@ -162,7 +196,14 @@ describe('ProgramCard', () => { it('applies mentor role styling', () => { const mentorProgram = { ...baseMockProgram, userRole: 'mentor' } - render() + render( + + ) const badge = screen.getByText('mentor') expect(badge).toHaveClass('bg-green-100', 'text-green-800') @@ -170,7 +211,14 @@ describe('ProgramCard', () => { it('applies default role styling for unknown role', () => { const unknownRoleProgram = { ...baseMockProgram, userRole: 'unknown' } - render() + render( + + ) const badge = screen.getByText('unknown') expect(badge).toHaveClass('bg-gray-100', 'text-gray-800') @@ -178,7 +226,14 @@ describe('ProgramCard', () => { it('applies default styling when userRole is undefined', () => { const noRoleProgram = { ...baseMockProgram, userRole: undefined } - render() + render( + + ) // Should not render badge when userRole is undefined expect(screen.queryByText(/bg-/)).not.toBeInTheDocument() @@ -190,7 +245,14 @@ describe('ProgramCard', () => { const longDescription = 'A'.repeat(300) // Long enough to trigger line clamping const longDescProgram = { ...baseMockProgram, description: longDescription } - render() + render( + + ) expect(screen.getByText(longDescription)).toBeInTheDocument() expect(screen.getByText(longDescription)).toBeInTheDocument() @@ -202,7 +264,14 @@ describe('ProgramCard', () => { const shortDescription = 'Short description' const shortDescProgram = { ...baseMockProgram, description: shortDescription } - render() + render( + + ) expect(screen.getByText('Short description')).toBeInTheDocument() @@ -213,7 +282,14 @@ describe('ProgramCard', () => { it('shows fallback text when description is empty', () => { const emptyDescProgram = { ...baseMockProgram, description: '' } - render() + render( + + ) expect(screen.getByText('No description available.')).toBeInTheDocument() }) @@ -222,7 +298,14 @@ describe('ProgramCard', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const noDescProgram = { ...baseMockProgram, description: undefined as any } - render() + render( + + ) expect(screen.getByText('No description available.')).toBeInTheDocument() }) @@ -230,15 +313,30 @@ describe('ProgramCard', () => { describe('Date Formatting', () => { it('shows date range when both startedAt and endedAt are provided', () => { - render() + render( + + ) - expect(screen.getByText('Jan 1, 2024 – Dec 31, 2024')).toBeInTheDocument() + expect( + screen.getByText((t) => t.includes('Jan 1, 2024') && t.includes('Dec 31, 2024')) + ).toBeInTheDocument() }) it('shows only start date when endedAt is missing', () => { const startOnlyProgram = { ...baseMockProgram, endedAt: '' } - - render() + render( + + ) expect(screen.getByText('Started: Jan 1, 2024')).toBeInTheDocument() }) @@ -246,7 +344,14 @@ describe('ProgramCard', () => { it('shows fallback text when both dates are missing', () => { const noDatesProgram = { ...baseMockProgram, startedAt: '', endedAt: '' } - render() + render( + + ) expect(screen.getByText('No dates set')).toBeInTheDocument() }) @@ -254,7 +359,14 @@ describe('ProgramCard', () => { it('shows fallback text when startedAt is missing but endedAt exists', () => { const endOnlyProgram = { ...baseMockProgram, startedAt: '' } - render() + render( + + ) expect(screen.getByText('No dates set')).toBeInTheDocument() }) @@ -265,8 +377,8 @@ describe('ProgramCard', () => { render( ) @@ -274,32 +386,46 @@ describe('ProgramCard', () => { expect(screen.getByTestId('icon-eye')).toBeInTheDocument() }) - it('renders edit icon for Edit button', () => { + it('renders actions button for admin menu', () => { render( ) - expect(screen.getByTestId('icon-edit')).toBeInTheDocument() + expect(screen.getByTestId('program-actions-button')).toBeInTheDocument() }) it('renders eye icon for View Details button', () => { - render() + render( + + ) expect(screen.getByTestId('icon-eye')).toBeInTheDocument() }) }) describe('Edge Cases', () => { - it('handles missing onEdit prop gracefully for admin access', () => { - render() + it('shows Edit Program in actions menu for admin access', () => { + render( + + ) - // Should still render Edit button even without onEdit - expect(screen.getByText('Edit')).toBeInTheDocument() + fireEvent.click(screen.getByTestId('program-actions-button')) + expect(screen.getByText('Edit Program')).toBeInTheDocument() }) it('handles program with minimal data', () => { @@ -313,7 +439,14 @@ describe('ProgramCard', () => { endedAt: '', } - render() + render( + + ) expect(screen.getByText('Minimal Program')).toBeInTheDocument() expect(screen.getByText('No description available.')).toBeInTheDocument() diff --git a/frontend/__tests__/unit/pages/MyMentorship.test.tsx b/frontend/__tests__/unit/pages/MyMentorship.test.tsx index ca62264445..eefc6e5b80 100644 --- a/frontend/__tests__/unit/pages/MyMentorship.test.tsx +++ b/frontend/__tests__/unit/pages/MyMentorship.test.tsx @@ -29,6 +29,9 @@ jest.mock('next-auth/react', () => { useSession: jest.fn(), } }) +jest.mock('hooks/useUpdateProgramStatus', () => ({ + useUpdateProgramStatus: () => ({ updateProgramStatus: jest.fn() }), +})) const mockUseQuery = useQuery as jest.Mock const mockPush = jest.fn() diff --git a/frontend/__tests__/unit/pages/Program.test.tsx b/frontend/__tests__/unit/pages/Program.test.tsx index 09960da32d..68aef54469 100644 --- a/frontend/__tests__/unit/pages/Program.test.tsx +++ b/frontend/__tests__/unit/pages/Program.test.tsx @@ -12,6 +12,9 @@ jest.mock('server/fetchAlgoliaData', () => ({ const mockRouter = { push: jest.fn(), } +jest.mock('hooks/useUpdateProgramStatus', () => ({ + useUpdateProgramStatus: () => ({ updateProgramStatus: jest.fn() }), +})) jest.mock('next/navigation', () => ({ ...jest.requireActual('next/navigation'), useRouter: jest.fn(() => mockRouter), diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts index ba7a9617ae..c6b66ccfc2 100644 --- a/frontend/jest.setup.ts +++ b/frontend/jest.setup.ts @@ -43,6 +43,22 @@ jest.mock('next-auth/react', () => { } }) +jest.mock('next/navigation', () => { + const actual = jest.requireActual('next/navigation') + const push = jest.fn() + const replace = jest.fn() + const prefetch = jest.fn() + const back = jest.fn() + const forward = jest.fn() + const mockRouter = { push, replace, prefetch, back, forward } + return { + ...actual, + useRouter: jest.fn(() => mockRouter), + useSearchParams: jest.fn(() => new URLSearchParams()), + useParams: jest.fn(() => ({})), + } +}) + if (!global.structuredClone) { global.structuredClone = (val) => JSON.parse(JSON.stringify(val)) } diff --git a/frontend/src/app/mentorship/programs/page.tsx b/frontend/src/app/mentorship/programs/page.tsx index 6fd1a9552d..37ad14fc62 100644 --- a/frontend/src/app/mentorship/programs/page.tsx +++ b/frontend/src/app/mentorship/programs/page.tsx @@ -34,6 +34,7 @@ const ProgramsPage = () => { program={program} accessLevel="user" onView={handleButtonClick} + isAdmin={false} /> ) } diff --git a/frontend/src/app/my/mentorship/page.tsx b/frontend/src/app/my/mentorship/page.tsx index 2248eb54d9..5db541d7b2 100644 --- a/frontend/src/app/my/mentorship/page.tsx +++ b/frontend/src/app/my/mentorship/page.tsx @@ -82,7 +82,6 @@ const MyMentorshipPage: React.FC = () => { const handleCreate = () => router.push('/my/mentorship/programs/create') const handleView = (key: string) => router.push(`/my/mentorship/programs/${key}`) - const handleEdit = (key: string) => router.push(`/my/mentorship/programs/${key}/edit`) if (!username) { return @@ -140,8 +139,8 @@ const MyMentorshipPage: React.FC = () => { accessLevel="admin" key={p.id} program={p} - onEdit={handleEdit} onView={handleView} + isAdmin={p?.userRole === 'admin'} /> )) )} diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx index a3ece68327..aaadcf23e2 100644 --- a/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx +++ b/frontend/src/app/my/mentorship/programs/[programKey]/page.tsx @@ -115,6 +115,7 @@ const ProgramDetailsPage = () => { return (

{title}

{type === 'program' && accessLevel === 'admin' && canUpdateStatus && ( - + )} {type === 'module' && accessLevel === 'admin' && diff --git a/frontend/src/components/ModuleForm.tsx b/frontend/src/components/ModuleForm.tsx index a673692d10..9635df159b 100644 --- a/frontend/src/components/ModuleForm.tsx +++ b/frontend/src/components/ModuleForm.tsx @@ -1,7 +1,6 @@ 'use client' import { useApolloClient } from '@apollo/client' -import clsx from 'clsx' import debounce from 'lodash/debounce' import { useRouter } from 'next/navigation' import type React from 'react' @@ -66,9 +65,9 @@ const ModuleForm = ({

Module Information

-
+
-
-