Skip to content

Commit 438b5a3

Browse files
divyanshu-vrSUPERGAMERDIVarkid15r
authored
Feature/refactor repositories card (#1965)
* Refactored RepositoriesCard * test update * Test files Updated * Update code --------- Co-authored-by: Divyanshu Verma <73750407+divyanshuverma@users.noreply.github.com> Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org> Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com>
1 parent 7db4e59 commit 438b5a3

File tree

4 files changed

+192
-30
lines changed

4 files changed

+192
-30
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { fireEvent, screen } from '@testing-library/react'
2+
import { useRouter } from 'next/navigation'
3+
import React from 'react'
4+
import { render } from 'wrappers/testUtil'
5+
import type { Organization } from 'types/organization'
6+
import type { RepositoryCardProps } from 'types/project'
7+
import RepositoriesCard from 'components/RepositoriesCard'
8+
9+
jest.mock('next/navigation', () => ({
10+
useRouter: jest.fn(),
11+
}))
12+
13+
jest.mock('components/ShowMoreButton', () => {
14+
return function MockShowMoreButton({ onToggle }: { onToggle: () => void }) {
15+
const [isExpanded, setIsExpanded] = React.useState(false)
16+
17+
const handleToggle = () => {
18+
setIsExpanded(!isExpanded)
19+
onToggle()
20+
}
21+
22+
return (
23+
<button type="button" onClick={handleToggle} data-testid="show-more-button">
24+
{isExpanded ? 'Show less' : 'Show more'}
25+
</button>
26+
)
27+
}
28+
})
29+
30+
jest.mock('components/TruncatedText', () => ({
31+
TruncatedText: ({ text }: { text: string }) => <span>{text}</span>,
32+
}))
33+
34+
jest.mock('components/InfoItem', () => {
35+
return function MockInfoItem({ unit, value }: { unit: string; value: number }) {
36+
return <div data-testid={`info-item-${unit}`}>{value}</div>
37+
}
38+
})
39+
40+
const mockPush = jest.fn()
41+
const mockUseRouter = useRouter as jest.Mock
42+
43+
describe('RepositoriesCard', () => {
44+
beforeEach(() => {
45+
jest.clearAllMocks()
46+
mockUseRouter.mockReturnValue({
47+
push: mockPush,
48+
})
49+
})
50+
51+
const createMockRepository = (index: number): RepositoryCardProps => ({
52+
contributorsCount: 10 + index,
53+
forksCount: 5 + index,
54+
key: `repo-${index}`,
55+
name: `Repository ${index}`,
56+
openIssuesCount: 3 + index,
57+
organization: {
58+
login: `org-${index}`,
59+
name: `Organization ${index}`,
60+
key: `org-${index}`,
61+
url: `https://github.com/org-${index}`,
62+
avatarUrl: `https://github.com/org-${index}.png`,
63+
description: `Organization ${index} description`,
64+
objectID: `org-${index}`,
65+
collaboratorsCount: 10,
66+
followersCount: 50,
67+
publicRepositoriesCount: 20,
68+
createdAt: Date.now(),
69+
updatedAt: Date.now(),
70+
} as Organization,
71+
starsCount: 100 + index,
72+
subscribersCount: 20 + index,
73+
url: `https://github.com/org-${index}/repo-${index}`,
74+
})
75+
76+
it('renders without crashing with empty repositories', () => {
77+
render(<RepositoriesCard repositories={[]} />)
78+
expect(screen.queryByTestId('show-more-button')).not.toBeInTheDocument()
79+
})
80+
81+
it('shows first 4 repositories initially when there are more than 4', () => {
82+
const repositories = Array.from({ length: 6 }, (_, i) => createMockRepository(i))
83+
84+
render(<RepositoriesCard repositories={repositories} />)
85+
86+
expect(screen.getByText('Repository 0')).toBeInTheDocument()
87+
expect(screen.getByText('Repository 3')).toBeInTheDocument()
88+
expect(screen.queryByText('Repository 4')).not.toBeInTheDocument()
89+
expect(screen.queryByText('Repository 5')).not.toBeInTheDocument()
90+
})
91+
92+
it('shows all repositories when there are 4 or fewer', () => {
93+
const repositories = Array.from({ length: 3 }, (_, i) => createMockRepository(i))
94+
95+
render(<RepositoriesCard repositories={repositories} />)
96+
97+
expect(screen.getByText('Repository 0')).toBeInTheDocument()
98+
expect(screen.getByText('Repository 1')).toBeInTheDocument()
99+
expect(screen.getByText('Repository 2')).toBeInTheDocument()
100+
})
101+
102+
it('displays ShowMoreButton when there are more than 4 repositories', () => {
103+
const repositories = Array.from({ length: 6 }, (_, i) => createMockRepository(i))
104+
105+
render(<RepositoriesCard repositories={repositories} />)
106+
107+
expect(screen.getByTestId('show-more-button')).toBeInTheDocument()
108+
})
109+
110+
it('does not display ShowMoreButton when there are 4 or fewer repositories', () => {
111+
const repositories = Array.from({ length: 4 }, (_, i) => createMockRepository(i))
112+
113+
render(<RepositoriesCard repositories={repositories} />)
114+
115+
expect(screen.queryByTestId('show-more-button')).not.toBeInTheDocument()
116+
})
117+
118+
it('toggles between showing 4 and all repositories when clicked', () => {
119+
const repositories = Array.from({ length: 6 }, (_, i) => createMockRepository(i))
120+
121+
render(<RepositoriesCard repositories={repositories} />)
122+
123+
// Initially shows first 4
124+
expect(screen.getByText('Repository 0')).toBeInTheDocument()
125+
expect(screen.queryByText('Repository 4')).not.toBeInTheDocument()
126+
127+
// Click show more
128+
fireEvent.click(screen.getByTestId('show-more-button'))
129+
130+
// Now shows all repositories
131+
expect(screen.getByText('Repository 4')).toBeInTheDocument()
132+
expect(screen.getByText('Repository 5')).toBeInTheDocument()
133+
134+
// Click show less
135+
fireEvent.click(screen.getByTestId('show-more-button'))
136+
137+
// Back to showing first 4
138+
expect(screen.queryByText('Repository 4')).not.toBeInTheDocument()
139+
expect(screen.queryByText('Repository 5')).not.toBeInTheDocument()
140+
})
141+
142+
it('renders repository items with correct information', () => {
143+
const repositories = [createMockRepository(0)]
144+
145+
render(<RepositoriesCard repositories={repositories} />)
146+
147+
expect(screen.getByText('Repository 0')).toBeInTheDocument()
148+
expect(screen.getByTestId('info-item-Star')).toBeInTheDocument()
149+
expect(screen.getByTestId('info-item-Fork')).toBeInTheDocument()
150+
expect(screen.getByTestId('info-item-Contributor')).toBeInTheDocument()
151+
expect(screen.getByTestId('info-item-Issue')).toBeInTheDocument()
152+
})
153+
154+
it('navigates to correct URL when repository item is clicked', () => {
155+
const repositories = [createMockRepository(0)]
156+
157+
render(<RepositoriesCard repositories={repositories} />)
158+
159+
const repositoryButton = screen.getByText('Repository 0')
160+
fireEvent.click(repositoryButton)
161+
162+
expect(mockPush).toHaveBeenCalledWith('/organizations/org-0/repositories/repo-0')
163+
})
164+
165+
it('handles repositories without organization data gracefully', () => {
166+
const repository: RepositoryCardProps = {
167+
contributorsCount: 10,
168+
forksCount: 5,
169+
key: 'repo-test',
170+
name: 'Test Repository',
171+
openIssuesCount: 3,
172+
starsCount: 100,
173+
subscribersCount: 20,
174+
url: 'https://github.com/test/repo',
175+
}
176+
177+
expect(() => render(<RepositoriesCard repositories={[repository]} />)).not.toThrow()
178+
})
179+
})

frontend/src/components/CardDetailsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ const DetailsCard = ({
210210
{(type === 'project' || type === 'user' || type === 'organization') &&
211211
repositories.length > 0 && (
212212
<SecondaryCard icon={faFolderOpen} title={<AnchorTitle title="Repositories" />}>
213-
<RepositoriesCard repositories={repositories} />
213+
<RepositoriesCard maxInitialDisplay={4} repositories={repositories} />
214214
</SecondaryCard>
215215
)}
216216
{IS_PROJECT_HEALTH_ENABLED && type === 'project' && healthMetricsData.length > 0 && (

frontend/src/components/RepositoriesCard.tsx

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,31 @@
1-
import {
2-
faCodeFork,
3-
faStar,
4-
faUsers,
5-
faExclamationCircle,
6-
faChevronDown,
7-
faChevronUp,
8-
} from '@fortawesome/free-solid-svg-icons'
9-
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
1+
import { faCodeFork, faStar, faUsers, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'
102
import { useRouter } from 'next/navigation'
113
import type React from 'react'
124
import { useState } from 'react'
135
import type { RepositoriesCardProps, RepositoryCardProps } from 'types/project'
146
import InfoItem from 'components/InfoItem'
7+
import ShowMoreButton from 'components/ShowMoreButton'
158
import { TruncatedText } from 'components/TruncatedText'
169

17-
const RepositoriesCard: React.FC<RepositoriesCardProps> = ({ repositories }) => {
10+
const RepositoriesCard: React.FC<RepositoriesCardProps> = ({
11+
maxInitialDisplay = 4,
12+
repositories,
13+
}) => {
1814
const [showAllRepositories, setShowAllRepositories] = useState(false)
1915

20-
const displayedRepositories = showAllRepositories ? repositories : repositories.slice(0, 4)
16+
const toggleRepositories = () => setShowAllRepositories(!showAllRepositories)
2117

18+
const displayedRepositories = showAllRepositories
19+
? repositories
20+
: repositories.slice(0, maxInitialDisplay)
2221
return (
2322
<div>
2423
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
2524
{displayedRepositories.map((repository, index) => {
2625
return <RepositoryItem key={index} details={repository} />
2726
})}
2827
</div>
29-
{repositories.length > 4 && (
30-
<div className="mt-6 flex items-center justify-center text-center">
31-
<button
32-
onClick={() => setShowAllRepositories(!showAllRepositories)}
33-
className="mt-4 flex items-center justify-center text-blue-400 hover:underline"
34-
>
35-
{showAllRepositories ? (
36-
<>
37-
Show less <FontAwesomeIcon icon={faChevronUp} className="ml-1" />
38-
</>
39-
) : (
40-
<>
41-
Show more <FontAwesomeIcon icon={faChevronDown} className="ml-1" />
42-
</>
43-
)}
44-
</button>
45-
</div>
46-
)}
28+
{repositories.length > maxInitialDisplay && <ShowMoreButton onToggle={toggleRepositories} />}
4729
</div>
4830
)
4931
}

frontend/src/types/project.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export type Project = {
4545
}
4646

4747
export type RepositoriesCardProps = {
48+
maxInitialDisplay?: number
4849
repositories?: RepositoryCardProps[]
4950
}
5051

0 commit comments

Comments
 (0)