diff --git a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx
index 834749cfa1..a59eaab5ff 100644
--- a/frontend/__tests__/unit/components/CardDetailsPage.test.tsx
+++ b/frontend/__tests__/unit/components/CardDetailsPage.test.tsx
@@ -1283,6 +1283,13 @@ describe('CardDetailsPage', () => {
expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Languages: JavaScript')
expect(screen.queryByText('Topics:')).not.toBeInTheDocument()
})
+
+ it('handles conditional rendering when languages empty but topics present', () => {
+ render()
+
+ expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Topics: web')
+ expect(screen.queryByText('Languages:')).not.toBeInTheDocument()
+ })
})
describe('Accessibility and Semantic HTML', () => {
@@ -1611,6 +1618,25 @@ describe('CardDetailsPage', () => {
expect(screen.getByText('Statistics')).toBeInTheDocument()
})
+ it('handles undefined topics/languages in grid layout', () => {
+ render()
+ expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Languages: JavaScript')
+ cleanup()
+ render()
+ expect(screen.getByTestId('toggleable-list')).toHaveTextContent('Topics: web')
+ })
+
+ it('renders Leaders with Unknown when value is null', () => {
+ const propsWithNullLeader = {
+ ...defaultProps,
+ type: 'chapter',
+ details: [{ label: 'Leaders', value: null }],
+ }
+ render()
+ expect(screen.getByText('Leaders:')).toBeInTheDocument()
+ expect(screen.getByText('Unknown')).toBeInTheDocument()
+ })
+
it('handles mixed valid and invalid data in arrays', () => {
const mixedValidInvalidData = {
...defaultProps,
@@ -2083,6 +2109,39 @@ describe('CardDetailsPage', () => {
expect(screen.getByText('Milestone No Author')).toBeInTheDocument()
})
+ it('renders milestone author tooltip using login when name is missing', () => {
+ const milestonesWithAuthorNoName = [
+ {
+ author: {
+ login: 'author-login-only',
+ name: undefined,
+ avatarUrl: 'https://example.com/author-avatar.jpg',
+ },
+ body: 'Milestone with author no name',
+ closedIssuesCount: 3,
+ createdAt: new Date(Date.now() - 10000000).toISOString(),
+ openIssuesCount: 1,
+ repositoryName: 'test-repo',
+ organizationName: 'test-org',
+ state: 'open',
+ title: 'Milestone Author No Name',
+ url: 'https://github.com/test/project/milestone/1',
+ },
+ ]
+
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ recentMilestones: milestonesWithAuthorNoName,
+ showAvatar: true,
+ modules: [],
+ }
+
+ render()
+ expect(screen.getByTestId('mock-tooltip')).toHaveAttribute('title', 'author-login-only')
+ expect(screen.getByAltText("author-login-only's avatar")).toBeInTheDocument()
+ })
+
it('renders milestone title without link when URL is missing', () => {
const milestonesWithoutUrl = [
{
@@ -2274,6 +2333,52 @@ describe('CardDetailsPage', () => {
expect(screen.getByTestId('entity-actions')).toHaveTextContent('type=module')
})
+ it('renders EntityActions for module type when user is a mentor but not admin', () => {
+ const { useSession } = jest.requireMock('next-auth/react')
+ useSession.mockReturnValue({
+ data: {
+ user: {
+ login: 'mentor-user',
+ name: 'Mentor User',
+ email: 'mentor@example.com',
+ },
+ },
+ })
+
+ const adminUser = {
+ id: 'admin-id',
+ login: 'admin-user',
+ name: 'Admin User',
+ avatarUrl: 'https://example.com/admin-avatar.jpg',
+ }
+
+ const mentorUser = {
+ id: 'mentor-id',
+ login: 'mentor-user',
+ name: 'Mentor User',
+ avatarUrl: 'https://example.com/mentor-avatar.jpg',
+ }
+
+ const moduleProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'module' as const,
+ accessLevel: 'admin',
+ admins: [adminUser],
+ mentors: [mentorUser],
+ programKey: 'test-program',
+ entityKey: 'test-module',
+ modules: [],
+ }
+
+ render()
+
+ const entityActions = screen.getByTestId('entity-actions')
+ expect(entityActions).toBeInTheDocument()
+ expect(entityActions).toHaveTextContent('type=module')
+ // isAdmin should be undefined (user is not in admins list)
+ expect(entityActions).not.toHaveAttribute('data-isadmin', 'true')
+ })
+
it('does not render EntityActions for module type when user is not an admin', () => {
const { useSession } = jest.requireMock('next-auth/react')
useSession.mockReturnValue({
diff --git a/frontend/__tests__/unit/components/ChapterMap.test.tsx b/frontend/__tests__/unit/components/ChapterMap.test.tsx
index 727bfc4e96..40c88eceb8 100644
--- a/frontend/__tests__/unit/components/ChapterMap.test.tsx
+++ b/frontend/__tests__/unit/components/ChapterMap.test.tsx
@@ -219,6 +219,30 @@ describe('ChapterMap Refactored Tests', () => {
})
})
+ it('removes existing zoom control before adding a new one on re-activation', async () => {
+ const { getByText, container } = render()
+
+ fireEvent.click(getByText('Unlock map').closest('button'))
+ await waitFor(() => {
+ expect(L.control.zoom).toHaveBeenCalledTimes(1)
+ expect(mockZoomControl.addTo).toHaveBeenCalledTimes(1)
+ })
+
+ fireEvent.mouseLeave(container.firstChild as HTMLElement)
+ await waitFor(() => {
+ expect(mockZoomControl.remove).toHaveBeenCalled()
+ expect(getByText('Unlock map')).toBeInTheDocument()
+ })
+
+ jest.clearAllMocks()
+
+ fireEvent.click(getByText('Unlock map').closest('button'))
+ await waitFor(() => {
+ expect(L.control.zoom).toHaveBeenCalledTimes(1)
+ expect(mockZoomControl.addTo).toHaveBeenCalledTimes(1)
+ })
+ })
+
it('locks map again on mouse leave', async () => {
const { getByText, container } = render()
@@ -266,6 +290,32 @@ describe('ChapterMap Refactored Tests', () => {
expect(mockMap.setView).toHaveBeenCalledWith([35.6762, 139.6503], 7)
})
})
+
+ it('handles zero container height (aspect ratio 1)', async () => {
+ mockMap.getContainer.mockReturnValueOnce({
+ clientWidth: 800,
+ clientHeight: 0,
+ })
+
+ render()
+ await waitFor(() => {
+ expect(mockMap.setMinZoom).toHaveBeenCalledWith(2)
+ expect(mockMap.setView).toHaveBeenCalledWith([20, 0], 2)
+ })
+ })
+
+ it('adjusts minZoom for wide aspect ratio (> 2)', async () => {
+ mockMap.getContainer.mockReturnValueOnce({
+ clientWidth: 1600,
+ clientHeight: 600,
+ })
+
+ render()
+ await waitFor(() => {
+ expect(mockMap.setMinZoom).toHaveBeenCalledWith(1)
+ expect(mockMap.setView).toHaveBeenCalledWith([20, 0], 2)
+ })
+ })
})
describe('Share Location Button', () => {
diff --git a/frontend/__tests__/unit/components/EntityActions.test.tsx b/frontend/__tests__/unit/components/EntityActions.test.tsx
index 8cb46bf144..9b1b427512 100644
--- a/frontend/__tests__/unit/components/EntityActions.test.tsx
+++ b/frontend/__tests__/unit/components/EntityActions.test.tsx
@@ -1024,5 +1024,64 @@ describe('EntityActions', () => {
)
})
})
+
+ it('skips cache update when getProgramModules is not in cache', async () => {
+ const mockCache = {
+ readQuery: jest.fn().mockReturnValue(null),
+ writeQuery: jest.fn(),
+ }
+
+ mockDeleteMutation.mockImplementationOnce(({ update }) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (update) update(mockCache as any)
+ return Promise.resolve({ data: { deleteModule: true } })
+ })
+
+ render(
+
+ )
+ fireEvent.click(screen.getByRole('button', { name: /Module actions menu/ }))
+ fireEvent.click(screen.getByText('Delete'))
+
+ fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
+
+ await waitFor(() => {
+ expect(mockCache.readQuery).toHaveBeenCalled()
+ expect(mockCache.writeQuery).not.toHaveBeenCalled()
+ expect(mockPush).toHaveBeenCalledWith('/my/mentorship/programs/test-program')
+ })
+ })
+
+ it('handles non-Error thrown values gracefully', async () => {
+ mockDeleteMutation.mockRejectedValueOnce('string error')
+
+ render(
+
+ )
+ fireEvent.click(screen.getByRole('button', { name: /Module actions menu/ }))
+ fireEvent.click(screen.getByText('Delete'))
+
+ fireEvent.click(screen.getByRole('button', { name: 'Delete' }))
+
+ await waitFor(() => {
+ expect(addToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: 'Error',
+ color: 'danger',
+ description: 'Failed to delete module. Please try again.',
+ })
+ )
+ })
+ })
})
})
diff --git a/frontend/__tests__/unit/components/ModuleForm.test.tsx b/frontend/__tests__/unit/components/ModuleForm.test.tsx
index fb21e6f46a..0aa788d2ff 100644
--- a/frontend/__tests__/unit/components/ModuleForm.test.tsx
+++ b/frontend/__tests__/unit/components/ModuleForm.test.tsx
@@ -79,7 +79,7 @@ jest.mock('@heroui/react', () => ({
@@ -278,11 +278,16 @@ describe('ModuleForm', () => {
beforeEach(() => {
jest.clearAllMocks()
+ jest.useFakeTimers()
mockQuery.mockResolvedValue({
data: { searchProjects: [{ id: 'project-1', name: 'Test Project' }] },
})
})
+ afterEach(() => {
+ jest.useRealTimers()
+ })
+
const renderModuleForm = (props = {}) => {
const defaultProps = {
formData: defaultFormData,
@@ -396,6 +401,51 @@ describe('ModuleForm', () => {
}
expect(mockSetFormData).toHaveBeenCalled()
})
+
+ it('updates project field when ProjectSelector changes', async () => {
+ renderModuleForm()
+ const input = screen.getByTestId('autocomplete-input')
+ await act(async () => {
+ fireEvent.change(input, { target: { value: 'Test' } })
+ jest.advanceTimersByTime(350)
+ })
+ await waitFor(() => expect(mockQuery).toHaveBeenCalled())
+ const selectButton = screen.getByTestId('autocomplete-select-single')
+ await act(async () => {
+ fireEvent.click(selectButton)
+ })
+ expect(mockSetFormData).toHaveBeenCalled()
+ const setterFn = mockSetFormData.mock.calls[mockSetFormData.mock.calls.length - 1][0]
+ const result = setterFn(defaultFormData)
+ expect(result).toEqual(
+ expect.objectContaining({
+ projectId: 'project-1',
+ projectName: 'Test Project',
+ })
+ )
+ })
+
+ it('updates project field when ProjectSelector is cleared', async () => {
+ const initialFormData = {
+ ...defaultFormData,
+ projectId: 'proj-1',
+ projectName: 'Existing Project',
+ }
+ renderModuleForm({ formData: initialFormData })
+ const clearButton = screen.getByTestId('autocomplete-clear')
+ await act(async () => {
+ fireEvent.click(clearButton)
+ })
+ expect(mockSetFormData).toHaveBeenCalled()
+ const setterFn = mockSetFormData.mock.calls[mockSetFormData.mock.calls.length - 1][0]
+ const result = setterFn(initialFormData)
+ expect(result).toEqual(
+ expect.objectContaining({
+ projectId: '',
+ projectName: '',
+ })
+ )
+ })
})
describe('Experience Level Select - handleSelectChange (lines 74-84)', () => {
@@ -690,7 +740,7 @@ describe('ProjectSelector', () => {
expect(mockOnProjectChange).toHaveBeenCalledWith('project-1', 'Test Project 1')
})
- it('clears selection when empty set is passed (lines 419-420)', async () => {
+ it('clears selection when clear button is clicked (lines 411-413)', async () => {
renderProjectSelector({ value: 'project-1', defaultName: 'Existing Project' })
const clearButton = screen.getByTestId('autocomplete-clear')
@@ -698,9 +748,8 @@ describe('ProjectSelector', () => {
fireEvent.click(clearButton)
})
- // The component's handleSelectionChange doesn't handle empty Set directly
- // This test verifies the current behavior where it's not called
- expect(mockOnProjectChange).not.toHaveBeenCalled()
+ // Now expected to call onProjectChange with null
+ expect(mockOnProjectChange).toHaveBeenCalledWith(null, '')
})
})
@@ -778,6 +827,24 @@ describe('ProjectSelector', () => {
consoleErrorSpy.mockRestore()
})
+
+ it('handles missing searchProjects in response', async () => {
+ mockQuery.mockResolvedValue({ data: {} })
+
+ renderProjectSelector()
+ const input = screen.getByTestId('autocomplete-input')
+
+ await act(async () => {
+ fireEvent.change(input, { target: { value: 'Test' } })
+ jest.advanceTimersByTime(350)
+ })
+
+ await waitFor(() => {
+ expect(mockQuery).toHaveBeenCalled()
+ })
+ const items = screen.queryAllByTestId('autocomplete-item')
+ expect(items).toHaveLength(0)
+ })
})
describe('Validation Display', () => {
diff --git a/frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx b/frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx
index 9773f61293..edf567a540 100644
--- a/frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx
+++ b/frontend/__tests__/unit/contexts/BreadcrumbContext.test.tsx
@@ -1,4 +1,8 @@
-import { registerBreadcrumb, getBreadcrumbItems } from 'contexts/BreadcrumbContext'
+import {
+ registerBreadcrumb,
+ getBreadcrumbItems,
+ registerBreadcrumbClassName,
+} from 'contexts/BreadcrumbContext'
describe('BreadcrumbContext', () => {
let cleanupFns: (() => void)[] = []
@@ -46,4 +50,16 @@ describe('BreadcrumbContext', () => {
expect(items[0].path).toBe('/short')
expect(items[1].path).toBe('/very/long/path')
})
+
+ it('registers class name', () => {
+ const unregister = registerBreadcrumbClassName('test')
+ cleanupFns.push(unregister)
+ expect(unregister).toBeDefined()
+ })
+
+ it('handles unregistering already removed class name', () => {
+ const unregister = registerBreadcrumbClassName('double-remove')
+ unregister()
+ expect(() => unregister()).not.toThrow()
+ })
})
diff --git a/frontend/__tests__/unit/pages/EditModule.test.tsx b/frontend/__tests__/unit/pages/EditModule.test.tsx
index adb2893b7c..5b4fb2de92 100644
--- a/frontend/__tests__/unit/pages/EditModule.test.tsx
+++ b/frontend/__tests__/unit/pages/EditModule.test.tsx
@@ -34,6 +34,11 @@ jest.mock('components/forms/shared/formValidationUtils', () => ({
validateEndDate: jest.fn(),
}))
+jest.mock('app/global-error', () => ({
+ ErrorDisplay: ({ title }: { title: string }) =>
{title}
,
+ handleAppError: jest.fn(),
+}))
+
describe('EditModulePage', () => {
const mockPush = jest.fn()
const mockReplace = jest.fn()
@@ -156,6 +161,7 @@ describe('EditModulePage', () => {
timeout: 4000,
})
})
+ expect(await screen.findByText('Access Denied')).toBeInTheDocument()
// Advance timers to trigger the redirect
act(() => {
@@ -195,11 +201,11 @@ describe('EditModulePage', () => {
render()
})
- // When denied but formData is null, component shows spinner
- expect(screen.getAllByAltText('Loading indicator').length).toBeGreaterThan(0)
+ // When denied, component shows ErrorDisplay
+ expect(await screen.findByText('Access Denied')).toBeInTheDocument()
})
- it('shows loading spinner when user is unauthenticated', async () => {
+ it('shows Access Denied when user is unauthenticated', async () => {
;(useSession as jest.Mock).mockReturnValue({
data: null,
status: 'unauthenticated',
@@ -217,8 +223,8 @@ describe('EditModulePage', () => {
render()
})
- // When denied but formData is null, component shows spinner
- expect(screen.getAllByAltText('Loading indicator').length).toBeGreaterThan(0)
+ // When denied, component shows ErrorDisplay
+ expect(await screen.findByText('Access Denied')).toBeInTheDocument()
})
it('handles form submission error gracefully', async () => {
@@ -269,6 +275,63 @@ describe('EditModulePage', () => {
})
})
+ it('shows permission denied error when mutation throws Permission error', async () => {
+ ;(useSession as jest.Mock).mockReturnValue({
+ data: { user: { login: 'admin-user' } },
+ status: 'authenticated',
+ })
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ loading: false,
+ data: {
+ getProgram: {
+ admins: [{ login: 'admin-user' }],
+ },
+ getModule: {
+ name: 'Test Module',
+ description: 'Description',
+ experienceLevel: ExperienceLevelEnum.Intermediate,
+ startedAt: '2025-07-01',
+ endedAt: '2025-07-31',
+ domains: ['AI'],
+ tags: ['graphql'],
+ projectName: 'Awesome Project',
+ projectId: '123',
+ mentors: [{ login: 'mentor1' }],
+ labels: [],
+ },
+ },
+ })
+ ;(useMutation as unknown as jest.Mock).mockReturnValue([
+ mockUpdateModule.mockRejectedValue(
+ new Error('Permission denied: You do not have permission to edit this module')
+ ),
+ { loading: false },
+ ])
+
+ render()
+
+ await act(async () => {
+ jest.runAllTimers()
+ })
+
+ expect(await screen.findByDisplayValue('Test Module')).toBeInTheDocument()
+
+ await act(async () => {
+ fireEvent.click(screen.getByRole('button', { name: /Save/i }))
+ })
+
+ await waitFor(() => {
+ expect(addToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: 'Error',
+ description:
+ 'You do not have permission to edit this module. Only program admins and assigned mentors can edit modules.',
+ color: 'danger',
+ })
+ )
+ })
+ })
+
it('renders form with module having missing optional fields', async () => {
;(useSession as jest.Mock).mockReturnValue({
data: { user: { login: 'admin-user' } },
diff --git a/frontend/__tests__/unit/pages/EditProgram.test.tsx b/frontend/__tests__/unit/pages/EditProgram.test.tsx
index 44cb4fa670..810b37ce2c 100644
--- a/frontend/__tests__/unit/pages/EditProgram.test.tsx
+++ b/frontend/__tests__/unit/pages/EditProgram.test.tsx
@@ -432,4 +432,48 @@ describe('EditProgramPage', () => {
)
})
})
+
+ test('handles null admins when access is initially allowed', async () => {
+ const validData = {
+ getProgram: {
+ name: 'Test',
+ description: 'Test description',
+ menteesLimit: 10,
+ startedAt: '2025-01-01',
+ endedAt: '2025-12-31',
+ tags: ['react'],
+ domains: ['web'],
+ admins: [{ login: 'admin1' }],
+ status: ProgramStatusEnum.Draft,
+ },
+ }
+
+ const nullAdminsData = {
+ getProgram: {
+ ...validData.getProgram,
+ admins: null,
+ },
+ }
+
+ let currentData = validData
+
+ jest.clearAllMocks()
+ ;(useSession as jest.Mock).mockReturnValue({
+ data: { user: { login: 'admin1' } },
+ status: 'authenticated',
+ })
+ ;(useQuery as unknown as jest.Mock).mockImplementation(() => ({
+ loading: false,
+ data: currentData,
+ }))
+ const { rerender } = render()
+ await waitFor(async () => {
+ expect(await screen.findByLabelText('Name')).toBeInTheDocument()
+ })
+ currentData = nullAdminsData
+ rerender()
+ await waitFor(async () => {
+ expect(await screen.findByText('Access Denied')).toBeInTheDocument()
+ })
+ })
})
diff --git a/frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx b/frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx
index 712e528cc7..d4e046f3a1 100644
--- a/frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx
+++ b/frontend/__tests__/unit/pages/MenteeProfilePage.test.tsx
@@ -541,4 +541,42 @@ describe('MenteeProfilePage', () => {
expect(screen.getAllByText('Open Issue 1').length).toBeGreaterThan(0)
expect(screen.getAllByText('Closed Issue 1').length).toBeGreaterThan(0)
})
+
+ it('uses fallback for invalid status filter', () => {
+ const originalUseState = React.useState
+ jest.spyOn(React, 'useState').mockImplementation((initialValue) => {
+ if (initialValue === 'all') {
+ return ['invalid_filter', jest.fn()]
+ }
+ return originalUseState(initialValue)
+ })
+
+ try {
+ mockUseQuery.mockReturnValue({ data: mockMenteeData, loading: false, error: undefined })
+ render()
+
+ expect(screen.getAllByText('Open Issue 1').length).toBeGreaterThan(0)
+ expect(screen.getAllByText('Closed Issue 1').length).toBeGreaterThan(0)
+ } finally {
+ jest.restoreAllMocks()
+ }
+ })
+
+ it('handles undefined domains and tags', () => {
+ const dataWithUndefined = {
+ ...mockMenteeData,
+ getMenteeDetails: {
+ ...mockMenteeData.getMenteeDetails,
+ domains: undefined,
+ tags: undefined,
+ },
+ }
+ mockUseQuery.mockReturnValue({ data: dataWithUndefined, loading: false, error: undefined })
+ render()
+
+ expect(screen.queryByRole('heading', { name: /Domains/i })).not.toBeInTheDocument()
+ expect(
+ screen.queryByRole('heading', { name: /Skills & Technologies/i })
+ ).not.toBeInTheDocument()
+ })
})
diff --git a/frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx b/frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx
index abbac2f87e..f9eabd0a37 100644
--- a/frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx
+++ b/frontend/__tests__/unit/pages/ModuleIssueDetailsPage.test.tsx
@@ -651,4 +651,55 @@ describe('ModuleIssueDetailsPage', () => {
fireEvent.change(dateInputEl, { target: { value: '2025-02-02' } })
expect(setTaskDeadlineMutation).not.toHaveBeenCalled()
})
+
+ it('does not trigger mutations when issueId is missing', () => {
+ mockUseParams.mockReturnValue({
+ programKey: 'prog1',
+ moduleKey: 'mod1',
+ issueId: '',
+ })
+ const assignIssue = jest.fn()
+ const baseMocks = (useIssueMutations as jest.Mock)()
+
+ mockUseIssueMutations.mockReturnValue({
+ ...baseMocks,
+ assignIssue,
+ assigning: false,
+ })
+
+ mockUseQuery.mockReturnValue({ data: mockIssueData, loading: false, error: undefined })
+ render()
+
+ const interestedUsersHeading = screen.getByRole('heading', { name: /Interested Users/i })
+ const userGrid = interestedUsersHeading.nextElementSibling
+ const assignButton = within(userGrid as HTMLElement).getByRole('button', { name: /Assign/i })
+ fireEvent.click(assignButton)
+ expect(assignIssue).not.toHaveBeenCalled()
+ })
+
+ it('does not open date picker when canEditDeadline is false', () => {
+ const setIsEditingDeadline = jest.fn()
+ const baseMocks = (useIssueMutations as jest.Mock)()
+ mockUseIssueMutations.mockReturnValue({
+ ...baseMocks,
+ setIsEditingDeadline,
+ })
+
+ const noAssigneesData = {
+ ...mockIssueData,
+ getModule: {
+ ...mockIssueData.getModule,
+ issueByNumber: {
+ ...mockIssueData.getModule.issueByNumber,
+ assignees: [],
+ },
+ },
+ }
+ mockUseQuery.mockReturnValue({ data: noAssigneesData, loading: false, error: undefined })
+ render()
+
+ const deadlineButton = screen.getByRole('button', { name: /No deadline set/i })
+ fireEvent.click(deadlineButton)
+ expect(setIsEditingDeadline).not.toHaveBeenCalled()
+ })
})
diff --git a/frontend/__tests__/unit/pages/MyMentorship.test.tsx b/frontend/__tests__/unit/pages/MyMentorship.test.tsx
index d759fc4af6..cee95224f4 100644
--- a/frontend/__tests__/unit/pages/MyMentorship.test.tsx
+++ b/frontend/__tests__/unit/pages/MyMentorship.test.tsx
@@ -1,7 +1,7 @@
import { useQuery } from '@apollo/client/react'
import { addToast } from '@heroui/toast'
import { screen, waitFor, fireEvent } from '@testing-library/react'
-import { useRouter as useRouterMock } from 'next/navigation'
+import { useRouter as useRouterMock, useSearchParams } from 'next/navigation'
import { useSession as mockUseSession } from 'next-auth/react'
import React from 'react'
import { render } from 'wrappers/testUtil'
@@ -52,7 +52,7 @@ jest.mock('next/navigation', () => {
return {
...actual,
useRouter: jest.fn(),
- useSearchParams: () => new URLSearchParams(''),
+ useSearchParams: jest.fn(),
}
})
@@ -74,6 +74,7 @@ const mockAddToast = addToast as jest.Mock
beforeEach(() => {
jest.clearAllMocks()
;(useRouterMock as jest.Mock).mockReturnValue({ push: mockPush })
+ ;(useSearchParams as jest.Mock).mockReturnValue(new URLSearchParams(''))
})
const mockProgramData = {
@@ -418,4 +419,46 @@ describe('MyMentorshipPage', () => {
jest.useRealTimers()
}
})
+
+ it('does not update URL if search params are unchanged', async () => {
+ ;(mockUseSession as jest.Mock).mockReturnValue({
+ data: {
+ user: {
+ name: 'User',
+ email: 'user@example.com',
+ login: 'user',
+ isLeader: true,
+ },
+ expires: '2099-01-01T00:00:00.000Z',
+ },
+ status: 'authenticated',
+ })
+
+ mockUseQuery.mockReturnValue({
+ data: mockProgramData,
+ loading: false,
+ error: undefined,
+ })
+ ;(useSearchParams as jest.Mock).mockReturnValue(new URLSearchParams('q=existing'))
+ globalThis.history.replaceState(null, '', '?q=existing')
+
+ jest.useFakeTimers()
+ try {
+ render()
+
+ const searchInput = screen.getByTestId('search-input')
+ fireEvent.change(searchInput, { target: { value: 'existing' } })
+
+ React.act(() => {
+ jest.advanceTimersByTime(500)
+ })
+
+ await waitFor(() => {
+ expect(mockPush).not.toHaveBeenCalled()
+ })
+ } finally {
+ globalThis.history.replaceState(null, '', '/')
+ jest.useRealTimers()
+ }
+ })
})
diff --git a/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx b/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx
index 1a20809d93..fd058b7957 100644
--- a/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/SnapshotDetails.test.tsx
@@ -392,12 +392,40 @@ describe('SnapshotDetailsPage', () => {
},
},
error: null,
+ loading: false,
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByText('v0.9.2')).toBeInTheDocument()
+ })
+ })
+
+ test('renders release without id and repositoryName (uses unknown fallback)', async () => {
+ const releaseWithoutIdAndRepo = {
+ ...mockSnapshotDetailsData.snapshot.newReleases[0],
+ id: undefined,
+ repositoryName: undefined,
+ }
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: {
+ snapshot: {
+ ...mockSnapshotDetailsData.snapshot,
+ newReleases: [releaseWithoutIdAndRepo],
+ newChapters: [],
+ newProjects: [],
+ },
+ },
+ error: null,
+ loading: false,
})
render()
await waitFor(() => {
expect(screen.getByText('v0.9.2')).toBeInTheDocument()
+ expect(screen.getByText('Unknown repository')).toBeInTheDocument()
})
})
})
diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx
index d9a512d963..154dbd1f77 100644
--- a/frontend/__tests__/unit/pages/UserDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx
@@ -1104,5 +1104,96 @@ describe('UserDetailsPage', () => {
expect(screen.getByText('No Repositories')).toBeInTheDocument()
})
})
+ test('renders joined date as "Not available" when createdAt is missing', async () => {
+ const userWithoutCreatedAt = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ createdAt: null,
+ },
+ }
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: userWithoutCreatedAt,
+ loading: false,
+ error: null,
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByText('Joined:')).toBeInTheDocument()
+ expect(screen.getByText('Not available')).toBeInTheDocument()
+ })
+ })
+
+ test('validates defensive check for endDate', async () => {
+ const singleDateContribution = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ contributionData: { '2023-01-01': 5 },
+ },
+ }
+
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: singleDateContribution,
+ loading: false,
+ error: null,
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByTestId('contribution-heatmap')).toBeInTheDocument()
+ })
+ })
+
+ test('uses user login as fallback for avatar alt text and title when name is missing', async () => {
+ const userWithoutName = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ name: null,
+ login: 'fallback-login',
+ },
+ }
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: userWithoutName,
+ loading: false,
+ error: null,
+ })
+
+ render()
+
+ await waitFor(() => {
+ const avatar = screen.getByAltText('fallback-login')
+ expect(avatar).toBeInTheDocument()
+ expect(screen.getByText('@fallback-login')).toBeInTheDocument()
+ })
+ })
+
+ test('uses "User Avatar" fallback when both name and login are missing', async () => {
+ const userWithoutNameAndLogin = {
+ ...mockUserDetailsData,
+ user: {
+ ...mockUserDetailsData.user,
+ name: null,
+ login: null,
+ },
+ }
+ ;(useQuery as unknown as jest.Mock).mockReturnValue({
+ data: userWithoutNameAndLogin,
+ loading: false,
+ error: null,
+ })
+
+ render()
+
+ await waitFor(() => {
+ const avatar = screen.getByAltText('User Avatar')
+ expect(avatar).toBeInTheDocument()
+ expect(screen.queryByText('@null')).not.toBeInTheDocument()
+ })
+ })
})
})
diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx
index 5f02f14e1d..f11481e564 100644
--- a/frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx
+++ b/frontend/src/app/my/mentorship/programs/[programKey]/edit/page.tsx
@@ -118,7 +118,7 @@ const EditProgramPage = () => {
menteesLimit: Number(formData.menteesLimit),
name: formData.name,
startedAt: formData.startedAt,
- status: (formData.status as ProgramStatusEnum) || ProgramStatusEnum.Draft,
+ status: (formData.status ?? ProgramStatusEnum.Draft) as ProgramStatusEnum,
tags: parseCommaSeparated(formData.tags),
}
diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx
index 4767957ce4..be32fcf87e 100644
--- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx
+++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/edit/page.tsx
@@ -101,7 +101,6 @@ const EditModulePage = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
- if (!formData) return
try {
const currentUserLogin = sessionData?.user?.login
@@ -110,22 +109,22 @@ const EditModulePage = () => {
)
const input: UpdateModuleInput = {
- description: formData.description,
- domains: parseCommaSeparated(formData.domains),
- endedAt: formData.endedAt || '',
- experienceLevel: formData.experienceLevel as ExperienceLevelEnum,
+ description: formData!.description,
+ domains: parseCommaSeparated(formData!.domains),
+ endedAt: formData!.endedAt || '',
+ experienceLevel: formData!.experienceLevel as ExperienceLevelEnum,
key: moduleKey,
- labels: parseCommaSeparated(formData.labels),
- name: formData.name,
+ labels: parseCommaSeparated(formData!.labels),
+ name: formData!.name,
programKey: programKey,
- projectId: formData.projectId,
- projectName: formData.projectName,
- startedAt: formData.startedAt || '',
- tags: parseCommaSeparated(formData.tags),
+ projectId: formData!.projectId,
+ projectName: formData!.projectName,
+ startedAt: formData!.startedAt || '',
+ tags: parseCommaSeparated(formData!.tags),
}
if (isAdmin) {
- input.mentorLogins = parseCommaSeparated(formData.mentorLogins)
+ input.mentorLogins = parseCommaSeparated(formData!.mentorLogins)
}
const result = await updateModule({
@@ -163,10 +162,6 @@ const EditModulePage = () => {
}
}
- if (accessStatus === 'checking' || !formData) {
- return
- }
-
if (accessStatus === 'denied') {
return (
{
)
}
+ if (accessStatus === 'checking' || !formData) {
+ return
+ }
+
return (
{
type="button"
disabled={!canEditDeadline}
onClick={() => {
- if (canEditDeadline) {
- setDeadlineInput(
- taskDeadline ? new Date(taskDeadline).toISOString().slice(0, 10) : ''
- )
- setIsEditingDeadline(true)
- }
+ setDeadlineInput(
+ taskDeadline ? new Date(taskDeadline).toISOString().slice(0, 10) : ''
+ )
+ setIsEditingDeadline(true)
}}
className={`inline-flex items-center gap-2 rounded px-2 py-1 text-left transition-colors ${
canEditDeadline
@@ -299,7 +297,6 @@ const ModuleIssueDetailsPage = () => {
aria-label={`Unassign @${a.login}`}
disabled={!issueId || unassigning}
onClick={async () => {
- if (!issueId || unassigning) return
await unassignIssue({
variables: {
programKey,
@@ -367,7 +364,6 @@ const ModuleIssueDetailsPage = () => {
type="button"
disabled={!issueId || assigning}
onClick={async () => {
- if (!issueId || assigning) return
await assignIssue({
variables: {
programKey,
diff --git a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx
index e36d5a4187..774d8b018b 100644
--- a/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx
+++ b/frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/page.tsx
@@ -116,7 +116,7 @@ const IssuesPage = () => {
selectedKeys={new Set([selectedLabel])}
onSelectionChange={(keys) => {
const [key] = Array.from(keys as Set)
- if (key) handleLabelChange(key)
+ if (key !== undefined) handleLabelChange(key)
}}
>
{[LABEL_ALL, ...allLabels].map((l) => (
diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx
index 16fc425c56..d9a8fbaa17 100644
--- a/frontend/src/components/CardDetailsPage.tsx
+++ b/frontend/src/components/CardDetailsPage.tsx
@@ -216,7 +216,7 @@ const DetailsCard = ({
)
)}
{socialLinks && (type === 'chapter' || type === 'committee') && (
-
+
)}
{showStatistics(type) && stats && (
@@ -464,12 +464,7 @@ const DetailsCard = ({
height={24}
width={24}
src={milestone?.author?.avatarUrl}
- alt={
- milestone.author &&
- (milestone.author.name || milestone.author.login)
- ? `${milestone.author.name || milestone.author.login}'s avatar`
- : "Author's avatar"
- }
+ alt={`${milestone.author?.name || milestone.author?.login}'s avatar`}
className="mr-2 rounded-full"
/>
@@ -549,7 +544,7 @@ const DetailsCard = ({
export default DetailsCard
-export const SocialLinks = ({ urls }: { urls: string[] }) => {
+const SocialLinks = ({ urls }: { urls: string[] }) => {
if (!urls || urls.length === 0) return null
return (
diff --git a/frontend/src/components/ChapterMap.tsx b/frontend/src/components/ChapterMap.tsx
index c07b452a67..8bba7658f6 100644
--- a/frontend/src/components/ChapterMap.tsx
+++ b/frontend/src/components/ChapterMap.tsx
@@ -26,10 +26,9 @@ const MapZoomControl = ({ isMapActive }: { isMapActive: boolean }) => {
map.doubleClickZoom.enable()
map.keyboard.enable()
- if (!zoomControlRef.current) {
- zoomControlRef.current = L.control.zoom({ position: 'topleft' })
- zoomControlRef.current.addTo(map)
- }
+ zoomControlRef.current?.remove()
+ zoomControlRef.current = L.control.zoom({ position: 'topleft' })
+ zoomControlRef.current.addTo(map)
} else {
map.scrollWheelZoom.disable()
map.dragging.disable()
diff --git a/frontend/src/components/ModuleForm.tsx b/frontend/src/components/ModuleForm.tsx
index c95642bf1a..f2b62cf6ee 100644
--- a/frontend/src/components/ModuleForm.tsx
+++ b/frontend/src/components/ModuleForm.tsx
@@ -399,7 +399,6 @@ export const ProjectSelector = ({
}, [inputValue, fetchSuggestions])
const handleSelectionChange = (keys: React.Key | null) => {
- if (keys === null) return
const selectedKey = keys as string
if (selectedKey) {
const selectedProject = items.find((item) => item.id === selectedKey)
diff --git a/frontend/src/components/ProgramForm.tsx b/frontend/src/components/ProgramForm.tsx
index 57429c6155..c17397133f 100644
--- a/frontend/src/components/ProgramForm.tsx
+++ b/frontend/src/components/ProgramForm.tsx
@@ -112,10 +112,6 @@ const ProgramForm = ({
const checkNameUniquenessSync = useCallback(
async (name: string): Promise => {
- if (!name.trim()) {
- return undefined
- }
-
try {
const { data } = await client.query({
query: GetMyProgramsDocument,