diff --git a/frontend/__tests__/unit/components/CalendarButton.test.tsx b/frontend/__tests__/unit/components/CalendarButton.test.tsx
index 55203cb7e1..fa8607b9de 100644
--- a/frontend/__tests__/unit/components/CalendarButton.test.tsx
+++ b/frontend/__tests__/unit/components/CalendarButton.test.tsx
@@ -332,58 +332,62 @@ describe('CalendarButton', () => {
})
})
- describe('long title overflow handling', () => {
- it('remains accessible with very long event titles', () => {
- const longTitle =
- 'This Is A Very Long Event Title That Extends Beyond Normal Length With Additional Description'
- render(
-
- )
+ describe('hover state', () => {
+ it('toggles icon on hover - shows FaCalendarPlus when hovering', async () => {
+ render()
const button = screen.getByRole('button')
- expect(button).toBeInTheDocument()
- expect(button).toHaveAttribute('aria-label', `Add ${longTitle} to Calendar`)
+
+ // Initially should show FaCalendar (not hovered)
+ const initialIconMarkup = button.querySelector('svg')?.outerHTML
+
+ // Simulate mouse enter
+ fireEvent.mouseEnter(button)
+ await waitFor(() => {
+ // After hover, FaCalendarPlus should be shown (different SVG)
+ const hoveredIconMarkup = button.querySelector('svg')?.outerHTML
+ expect(hoveredIconMarkup).not.toBe(initialIconMarkup)
+ })
})
- it('maintains visibility with flex-shrink-0 class', () => {
- render(
-
- )
+ it('reverts to FaCalendar icon when mouse leaves', async () => {
+ render()
const button = screen.getByRole('button')
- expect(button).toHaveClass('flex-shrink-0')
- expect(button).toBeVisible()
+
+ // Capture initial icon (FaCalendar)
+ const initialIconHtml = button.innerHTML
+
+ // Mouse enter - hover state true
+ fireEvent.mouseEnter(button)
+
+ await waitFor(() => {
+ // Icon should change to FaCalendarPlus
+ expect(button.innerHTML).not.toEqual(initialIconHtml)
+ })
+
+ // Mouse leave - hover state false
+ fireEvent.mouseLeave(button)
+
+ await waitFor(() => {
+ // Icon should revert to FaCalendar
+ expect(button.innerHTML).toEqual(initialIconHtml)
+ })
})
- it('works correctly in flex container with long text sibling', () => {
- const { container } = render(
-
@@ -394,6 +423,136 @@ jest.mock('components/ContributorsList', () => ({
),
}))
+jest.mock('components/EntityActions', () => ({
+ __esModule: true,
+ default: ({
+ type,
+ programKey,
+ moduleKey,
+ status: _status,
+ setStatus: _setStatus,
+ ...props
+ }: {
+ type: string
+ programKey?: string
+ moduleKey?: string
+ status?: string
+ setStatus?: (status: string) => void
+ [key: string]: unknown
+ }) => (
+
+ EntityActions: type={type}, programKey={programKey}, moduleKey={moduleKey}
+
+ ),
+}))
+
+jest.mock('components/Leaders', () => {
+ return {
+ __esModule: true,
+ default: ({ users, ...props }: { users: unknown[]; [key: string]: unknown }) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const usersList = users as any[]
+ return (
+
+
Leaders
+ {Array.isArray(usersList) &&
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ usersList.map((user: any, index: number) => {
+ const uniqueKey = `leader-${index}-${user.login || 'unknown'}`
+ return (
+
+
{user.member?.name || user.memberName || 'Unknown'}
+
{user.description || ''}
+
+ )
+ })}
+
+ )
+ },
+ }
+})
+
+jest.mock('components/StatusBadge', () => ({
+ __esModule: true,
+ default: ({
+ status,
+ _size,
+ ...props
+ }: {
+ status: string
+ _size?: string
+ [key: string]: unknown
+ }) => (
+
+ {status.charAt(0).toUpperCase() + status.slice(1)}
+
+ ),
+}))
+
+jest.mock('components/MarkdownWrapper', () => ({
+ __esModule: true,
+ default: ({ content, ...props }: { content: string; [key: string]: unknown }) => (
+
+ {content}
+
+ ),
+}))
+
+jest.mock('components/ModuleCard', () => ({
+ __esModule: true,
+ default: ({
+ modules,
+ accessLevel: _accessLevel,
+ admins: _admins,
+ ...props
+ }: {
+ modules: unknown[]
+ accessLevel: string
+ admins?: unknown[]
+ [key: string]: unknown
+ }) => (
+
+ ModuleCard ({modules?.length || 0} modules)
+
+ ),
+}))
+
+jest.mock('components/ShowMoreButton', () => {
+ function ShowMoreButtonMock({
+ onToggle,
+ ...props
+ }: Readonly<{
+ onToggle: () => void
+ [key: string]: unknown
+ }>) {
+ const [isExpanded, setIsExpanded] = React.useState(false)
+ return (
+
+ )
+ }
+ return {
+ __esModule: true,
+ default: ShowMoreButtonMock,
+ }
+})
+
+jest.mock('components/TruncatedText', () => ({
+ __esModule: true,
+ TruncatedText: ({ text }: { text: string }) =>
{text},
+}))
+
describe('CardDetailsPage', () => {
const createMalformedData =
>(
validData: T,
@@ -663,7 +822,7 @@ describe('CardDetailsPage', () => {
render()
expect(screen.getByText('Inactive')).toBeInTheDocument()
- // Updated classes for consistent badge styling
+ // Updated classes for consistent badge styling.
expect(screen.getByText('Inactive')).toHaveClass('bg-red-50', 'text-red-800')
})
@@ -809,6 +968,23 @@ describe('CardDetailsPage', () => {
expect(screen.getByTestId('metrics-score-circle')).toBeInTheDocument()
})
+ it('calls scrollToAnchor when MetricsScoreCircle is clicked', () => {
+ const { scrollToAnchor } = jest.requireMock('utils/scrollToAnchor')
+
+ render(
+
+ )
+
+ const healthButton = screen.getByRole('button')
+ fireEvent.click(healthButton)
+
+ expect(scrollToAnchor).toHaveBeenCalledWith('issues-trend')
+ })
+
it('renders social links with correct hrefs and target attributes', () => {
const socialLinks = ['https://github.com/test', 'https://twitter.com/test']
render()
@@ -1822,6 +1998,131 @@ describe('CardDetailsPage', () => {
expect(screen.getByText('Milestone 4')).toBeInTheDocument()
expect(screen.queryByText(/Show more/i)).not.toBeInTheDocument()
})
+
+ it('renders milestone author avatar when showAvatar is true and author data is complete', () => {
+ const milestonesWithAuthor = [
+ {
+ author: {
+ login: 'author-user',
+ name: 'Author User',
+ avatarUrl: 'https://example.com/author-avatar.jpg',
+ },
+ body: 'Milestone with author',
+ closedIssuesCount: 3,
+ createdAt: new Date(Date.now() - 10000000).toISOString(),
+ openIssuesCount: 1,
+ repositoryName: 'test-repo',
+ organizationName: 'test-org',
+ state: 'open',
+ title: 'Milestone With Author',
+ url: 'https://github.com/test/project/milestone/1',
+ },
+ ]
+
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ recentMilestones: milestonesWithAuthor,
+ showAvatar: true,
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByText('Milestone With Author')).toBeInTheDocument()
+ // The avatar image should be rendered
+ const avatarImg = screen.getByAltText("Author User's avatar")
+ expect(avatarImg).toBeInTheDocument()
+ expect(avatarImg).toHaveAttribute('src', 'https://example.com/author-avatar.jpg')
+ })
+
+ it('renders milestone without author avatar when author data is missing', () => {
+ const milestonesWithoutAuthor = [
+ {
+ author: null,
+ body: 'Milestone without author',
+ closedIssuesCount: 3,
+ createdAt: new Date(Date.now() - 10000000).toISOString(),
+ openIssuesCount: 1,
+ repositoryName: 'test-repo',
+ organizationName: 'test-org',
+ state: 'open',
+ title: 'Milestone No Author',
+ url: 'https://github.com/test/project/milestone/1',
+ },
+ ]
+
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ recentMilestones: milestonesWithoutAuthor,
+ showAvatar: true,
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByText('Milestone No Author')).toBeInTheDocument()
+ })
+
+ it('renders milestone title without link when URL is missing', () => {
+ const milestonesWithoutUrl = [
+ {
+ author: mockUser,
+ body: 'Milestone without URL',
+ closedIssuesCount: 3,
+ createdAt: new Date(Date.now() - 10000000).toISOString(),
+ openIssuesCount: 1,
+ repositoryName: 'test-repo',
+ organizationName: 'test-org',
+ state: 'open',
+ title: 'Milestone No URL',
+ url: null,
+ },
+ ]
+
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ recentMilestones: milestonesWithoutUrl,
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByText('Milestone No URL')).toBeInTheDocument()
+ // The title should not be a link
+ const title = screen.getByText('Milestone No URL')
+ expect(title.closest('a')).toBeNull()
+ })
+
+ it('renders milestone without repository link when repositoryName or organizationName is missing', () => {
+ const milestonesWithoutRepo = [
+ {
+ author: mockUser,
+ body: 'Milestone without repo',
+ closedIssuesCount: 3,
+ createdAt: new Date(Date.now() - 10000000).toISOString(),
+ openIssuesCount: 1,
+ repositoryName: null,
+ organizationName: null,
+ state: 'open',
+ title: 'Milestone No Repo',
+ url: 'https://github.com/test/project/milestone/1',
+ },
+ ]
+
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ recentMilestones: milestonesWithoutRepo,
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByText('Milestone No Repo')).toBeInTheDocument()
+ })
})
describe('Module Pull Requests Display', () => {
@@ -1918,4 +2219,529 @@ describe('CardDetailsPage', () => {
expect(screen.queryByText(/Show more/i)).not.toBeInTheDocument()
})
})
+
+ describe('Module Admin EntityActions and Mentees', () => {
+ it('renders EntityActions for module type when user is an admin', () => {
+ const { useSession } = jest.requireMock('next-auth/react')
+ useSession.mockReturnValue({
+ data: {
+ user: {
+ login: 'admin-user',
+ name: 'Admin User',
+ email: 'admin@example.com',
+ },
+ },
+ })
+
+ const adminUser = {
+ id: 'admin-id',
+ login: 'admin-user',
+ name: 'Admin User',
+ avatarUrl: 'https://example.com/admin-avatar.jpg',
+ }
+
+ const moduleProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'module' as const,
+ accessLevel: 'admin',
+ admins: [adminUser],
+ programKey: 'test-program',
+ entityKey: 'test-module',
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByTestId('entity-actions')).toBeInTheDocument()
+ expect(screen.getByTestId('entity-actions')).toHaveTextContent('type=module')
+ })
+
+ it('does not render EntityActions for module type when user is not an admin', () => {
+ const { useSession } = jest.requireMock('next-auth/react')
+ useSession.mockReturnValue({
+ data: {
+ user: {
+ login: 'regular-user',
+ name: 'Regular User',
+ email: 'user@example.com',
+ },
+ },
+ })
+
+ const adminUser = {
+ id: 'admin-id',
+ login: 'admin-user',
+ name: 'Admin User',
+ avatarUrl: 'https://example.com/admin-avatar.jpg',
+ }
+
+ const moduleProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'module' as const,
+ accessLevel: 'admin',
+ admins: [adminUser],
+ programKey: 'test-program',
+ entityKey: 'test-module',
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.queryByTestId('entity-actions')).not.toBeInTheDocument()
+ })
+
+ it('renders mentees section when mentees are provided', () => {
+ const mentees = [
+ {
+ id: 'mentee-1',
+ login: 'mentee_user1',
+ name: 'Mentee User 1',
+ avatarUrl: 'https://example.com/mentee1.jpg',
+ },
+ {
+ id: 'mentee-2',
+ login: 'mentee_user2',
+ name: 'Mentee User 2',
+ avatarUrl: 'https://example.com/mentee2.jpg',
+ },
+ ]
+
+ const propsWithMentees: DetailsCardProps = {
+ ...defaultProps,
+ mentees,
+ programKey: 'test-program',
+ entityKey: 'test-entity',
+ }
+
+ render()
+
+ const allContributorsLists = screen.getAllByTestId('contributors-list')
+ const menteesSection = allContributorsLists.find((el) => el.textContent?.includes('Mentees'))
+ expect(menteesSection).toHaveTextContent('Mentees (2 items, max display: 6)')
+ })
+
+ it('does not render mentees section when no mentees are provided', () => {
+ const propsWithoutMentees: DetailsCardProps = {
+ ...defaultProps,
+ mentees: [],
+ }
+ render()
+ // Make sure mentees section is not rendered
+ const allContributorsLists = screen.queryAllByTestId('contributors-list')
+ const menteesList = allContributorsLists.find((el) => el.textContent?.includes('Mentees'))
+ expect(menteesList).toBeUndefined()
+ })
+
+ it('renders mentees with custom URL formatter', () => {
+ const mentees = [
+ {
+ id: 'mentee-1',
+ login: 'test_mentee',
+ name: 'Test Mentee',
+ avatarUrl: 'https://example.com/mentee.jpg',
+ },
+ ]
+
+ const propsWithMentees: DetailsCardProps = {
+ ...defaultProps,
+ mentees,
+ programKey: 'program-key-123',
+ entityKey: 'entity-key-456',
+ }
+
+ render()
+
+ const allContributorsLists = screen.getAllByTestId('contributors-list')
+ const menteesSection = allContributorsLists.find((el) => el.textContent?.includes('Mentees'))
+ expect(menteesSection).toHaveTextContent('Mentees (1 items, max display: 6)')
+ })
+
+ it('handles null/undefined mentees array gracefully', () => {
+ const propsWithNullMentees: DetailsCardProps = {
+ ...defaultProps,
+ mentees: null,
+ }
+
+ expect(() => render()).not.toThrow()
+ })
+
+ it('renders program EntityActions when type is program with appropriate access', () => {
+ const { useSession } = jest.requireMock('next-auth/react')
+ useSession.mockReturnValue({
+ data: {
+ user: {
+ login: 'program-admin',
+ name: 'Program Admin',
+ email: 'admin@example.com',
+ },
+ },
+ })
+
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ accessLevel: 'admin',
+ canUpdateStatus: true,
+ status: 'active',
+ setStatus: jest.fn(),
+ programKey: 'test-program',
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByTestId('entity-actions')).toBeInTheDocument()
+ expect(screen.getByTestId('entity-actions')).toHaveTextContent('type=program')
+ })
+
+ it('does not render program EntityActions when canUpdateStatus is false', () => {
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ accessLevel: 'admin',
+ canUpdateStatus: false,
+ status: 'active',
+ setStatus: jest.fn(),
+ programKey: 'test-program',
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.queryByTestId('entity-actions')).not.toBeInTheDocument()
+ })
+
+ it('does not render program EntityActions when accessLevel is not admin', () => {
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ accessLevel: 'user',
+ canUpdateStatus: true,
+ status: 'active',
+ setStatus: jest.fn(),
+ programKey: 'test-program',
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.queryByTestId('entity-actions')).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Program and Module Tags, Domains, and Labels', () => {
+ it('renders tags for program type', () => {
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ tags: ['tag1', 'tag2', 'tag3'],
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByText(/Tags/)).toBeInTheDocument()
+ expect(screen.getByText(/tag1, tag2, tag3/)).toBeInTheDocument()
+ })
+
+ it('renders domains for program type', () => {
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ domains: ['domain1', 'domain2'],
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByText(/Domains/)).toBeInTheDocument()
+ expect(screen.getByText(/domain1, domain2/)).toBeInTheDocument()
+ })
+
+ it('renders labels for program type', () => {
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ labels: ['label1', 'label2'],
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByText(/Labels/)).toBeInTheDocument()
+ expect(screen.getByText(/label1, label2/)).toBeInTheDocument()
+ })
+
+ it('renders tags and domains in same row for module type', () => {
+ const moduleProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'module' as const,
+ tags: ['moduleTag1'],
+ domains: ['moduleDomain1'],
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByText(/Tags/)).toBeInTheDocument()
+ expect(screen.getByText(/Domains/)).toBeInTheDocument()
+ })
+
+ it('does not render tags section when tags array is empty', () => {
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ tags: [],
+ domains: ['domain1'],
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.queryByText(/Tags:/)).not.toBeInTheDocument()
+ expect(screen.getByText(/Domains/)).toBeInTheDocument()
+ })
+
+ it('does not render domains section when domains array is empty', () => {
+ const programProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ tags: ['tag1'],
+ domains: [],
+ modules: [],
+ }
+
+ render()
+
+ expect(screen.getByText(/Tags/)).toBeInTheDocument()
+ expect(screen.queryByText(/Domains:/)).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Program Module Rendering', () => {
+ const mockModules = [
+ {
+ id: 'module-1-id',
+ key: 'module-1',
+ name: 'Module 1',
+ description: 'First module',
+ endedAt: new Date(Date.now() + 86400000).toISOString(),
+ startedAt: new Date(Date.now() - 86400000).toISOString(),
+ experienceLevel: 'BEGINNER',
+ mentors: [],
+ },
+ {
+ id: 'module-2-id',
+ key: 'module-2',
+ name: 'Module 2',
+ description: 'Second module',
+ endedAt: new Date(Date.now() + 86400000).toISOString(),
+ startedAt: new Date(Date.now() - 86400000).toISOString(),
+ experienceLevel: 'INTERMEDIATE',
+ mentors: [],
+ },
+ ] as DetailsCardProps['modules']
+
+ it('renders single module without SecondaryCard wrapper', () => {
+ const singleModuleProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ modules: [mockModules![0]],
+ }
+
+ render()
+
+ expect(screen.getByTestId('module-card')).toBeInTheDocument()
+ // Single module should not have "Modules" title
+ expect(screen.queryByText('Modules')).not.toBeInTheDocument()
+ })
+
+ it('renders multiple modules with SecondaryCard wrapper and title', () => {
+ const multiModuleProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ modules: mockModules,
+ }
+
+ render()
+
+ expect(screen.getByTestId('module-card')).toBeInTheDocument()
+ expect(screen.getByText('Modules')).toBeInTheDocument()
+ })
+ })
+
+ describe('Mentors and Admins Lists', () => {
+ const mockMentors = [
+ {
+ id: 'mentor-1',
+ login: 'mentor_user1',
+ name: 'Mentor User 1',
+ avatarUrl: 'https://example.com/mentor1.jpg',
+ },
+ {
+ id: 'mentor-2',
+ login: 'mentor_user2',
+ name: 'Mentor User 2',
+ avatarUrl: 'https://example.com/mentor2.jpg',
+ },
+ ]
+
+ const mockAdmins = [
+ {
+ id: 'admin-1',
+ login: 'admin_user1',
+ name: 'Admin User 1',
+ avatarUrl: 'https://example.com/admin1.jpg',
+ },
+ ]
+
+ it('renders mentors section when mentors are provided', () => {
+ const propsWithMentors: DetailsCardProps = {
+ ...defaultProps,
+ mentors: mockMentors,
+ }
+
+ render()
+
+ const allContributorsLists = screen.getAllByTestId('contributors-list')
+ const mentorsSection = allContributorsLists.find((el) => el.textContent?.includes('Mentors'))
+ expect(mentorsSection).toHaveTextContent('Mentors (2 items, max display: 6)')
+ })
+
+ it('does not render mentors section when mentors array is empty', () => {
+ const propsWithoutMentors: DetailsCardProps = {
+ ...defaultProps,
+ mentors: [],
+ }
+
+ render()
+
+ // Mentors section should not be rendered
+ const allContributorsLists = screen.queryAllByTestId('contributors-list')
+ const mentorsSection = allContributorsLists.find((el) => el.textContent?.includes('Mentors'))
+ expect(mentorsSection).toBeUndefined()
+ })
+
+ it('renders admins section when type is program and admins are provided', () => {
+ const propsWithAdmins: DetailsCardProps = {
+ ...defaultProps,
+ type: 'program' as const,
+ admins: mockAdmins,
+ modules: [],
+ }
+
+ render()
+
+ const allContributorsLists = screen.getAllByTestId('contributors-list')
+ const adminsSection = allContributorsLists.find((el) => el.textContent?.includes('Admins'))
+ expect(adminsSection).toHaveTextContent('Admins (1 items, max display: 6)')
+ })
+
+ it('does not render admins section for non-program types', () => {
+ const propsWithAdmins: DetailsCardProps = {
+ ...defaultProps,
+ type: 'project' as const,
+ admins: mockAdmins,
+ }
+
+ render()
+
+ const allContributorsLists = screen.queryAllByTestId('contributors-list')
+ const adminsSection = allContributorsLists.find((el) => el.textContent?.includes('Admins'))
+ expect(adminsSection).toBeUndefined()
+ })
+ })
+
+ describe('Repository Rendering for Different Types', () => {
+ it('renders repositories for user type', () => {
+ const userProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'user' as const,
+ repositories: mockRepositories,
+ }
+
+ render()
+
+ expect(screen.getByText('Repositories')).toBeInTheDocument()
+ expect(screen.getByTestId('repositories-card')).toBeInTheDocument()
+ })
+
+ it('renders repositories for organization type', () => {
+ const orgProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'organization' as const,
+ repositories: mockRepositories,
+ }
+
+ render()
+
+ expect(screen.getByText('Repositories')).toBeInTheDocument()
+ expect(screen.getByTestId('repositories-card')).toBeInTheDocument()
+ })
+
+ it('does not render repositories for chapter type', () => {
+ const chapterProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'chapter' as const,
+ repositories: mockRepositories,
+ }
+
+ render()
+
+ expect(screen.queryByText('Repositories')).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Sponsor Card Rendering', () => {
+ it('renders sponsor card for chapter type', () => {
+ const chapterProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'chapter' as const,
+ entityKey: 'test-chapter',
+ }
+
+ render()
+
+ expect(screen.getByTestId('sponsor-card')).toBeInTheDocument()
+ expect(screen.getByTestId('sponsor-card')).toHaveTextContent('Type: chapter')
+ })
+
+ it('renders sponsor card for repository type', () => {
+ const repoProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'repository' as const,
+ entityKey: 'test-repo',
+ }
+
+ render()
+
+ expect(screen.getByTestId('sponsor-card')).toBeInTheDocument()
+ expect(screen.getByTestId('sponsor-card')).toHaveTextContent('Type: project')
+ })
+
+ it('uses projectName as title when provided', () => {
+ const projectProps: DetailsCardProps = {
+ ...defaultProps,
+ type: 'project' as const,
+ entityKey: 'test-project',
+ projectName: 'Custom Project Name',
+ }
+
+ render()
+
+ expect(screen.getByTestId('sponsor-card')).toHaveTextContent('Title: Custom Project Name')
+ })
+
+ it('does not render sponsor card when entityKey is missing', () => {
+ const propsWithoutKey: DetailsCardProps = {
+ ...defaultProps,
+ type: 'project' as const,
+ entityKey: undefined,
+ }
+
+ render()
+
+ expect(screen.queryByTestId('sponsor-card')).not.toBeInTheDocument()
+ })
+ })
})
diff --git a/frontend/__tests__/unit/components/ChapterMapWrapper.test.tsx b/frontend/__tests__/unit/components/ChapterMapWrapper.test.tsx
new file mode 100644
index 0000000000..35c1daa972
--- /dev/null
+++ b/frontend/__tests__/unit/components/ChapterMapWrapper.test.tsx
@@ -0,0 +1,319 @@
+import { render, waitFor, fireEvent } from '@testing-library/react'
+import React, { JSX } from 'react'
+import { Chapter } from 'types/chapter'
+import * as geolocationUtils from 'utils/geolocationUtils'
+
+// Mock next/dynamic
+jest.mock('next/dynamic', () => {
+ return function mockDynamic(
+ importFn: () => Promise<{ default: React.ComponentType }>,
+ options?: { ssr?: boolean }
+ ) {
+ // Ignore options for SSR: false — reference it without using `void`
+ if (options) {
+ /* intentionally unused */
+ }
+ // Return a component that resolves the import synchronously for testing
+ const Component = React.lazy(importFn)
+ return function DynamicComponent(props: Record): JSX.Element {
+ return (
+ Loading... }>
+ Loading...
+ }
+})
+
+describe('