Skip to content

Commit f565947

Browse files
authored
Merge branch 'main' into fix-homepage-container-flexibility
2 parents 5e0536e + 7d4db1c commit f565947

File tree

7 files changed

+234
-1
lines changed

7 files changed

+234
-1
lines changed

.github/workflows/run-ci-cd.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,25 @@ jobs:
483483
working-directory: .github/ansible
484484
run: ansible-playbook -i inventory.yaml staging/proxy.yaml -e "github_workspace=$GITHUB_WORKSPACE"
485485

486+
run-lighthouse-ci:
487+
name: Run Lighthouse CI
488+
needs:
489+
- deploy-staging-nest-proxy
490+
runs-on: ubuntu-latest
491+
steps:
492+
- name: Check out repository
493+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
494+
495+
- name: Set up Node
496+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
497+
with:
498+
node-version: 22
499+
500+
- name: Run Lighthouse CI
501+
working-directory: frontend
502+
run: |
503+
npx -y @lhci/[email protected] autorun
504+
486505
build-production-images:
487506
name: Build Production Images
488507
env:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ __pycache__
77
.env*
88
!.env.example
99
.idea
10+
.lighthouseci/
1011
.local
1112
.mypy_cache
1213
.npm/

cspell/custom-dict.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ inlinehilite
6969
isanori
7070
jumpstart
7171
kasya
72+
lhci
7273
libexpat
7374
linkify
7475
lte

frontend/Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ format-frontend-code:
2727
|| (printf "pnpm run format"; for i in $$(seq 1 58); do printf "."; done; printf "\033[37;41mFailed\033[0m\n" \
2828
&& pnpm run format))
2929

30+
lighthouse-ci:
31+
@cd frontend && npx -y @lhci/[email protected] autorun --collect.url=http://localhost:3000
32+
3033
lint-frontend-code:
3134
@(cd frontend && pnpm run lint:check >/dev/null 2>&1 \
3235
&& (printf "pnpm run lint"; for i in $$(seq 1 60); do printf "."; done; printf "\033[30;42mPassed\033[0m\n") \
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { render, screen, fireEvent } from '@testing-library/react'
2+
import { useRouter } from 'next/navigation'
3+
import React from 'react'
4+
import type { Issue } from 'types/issue'
5+
import RecentIssues from 'components/RecentIssues'
6+
7+
jest.mock('next/navigation', () => ({
8+
useRouter: jest.fn(),
9+
}))
10+
11+
jest.mock('@heroui/tooltip', () => ({
12+
Tooltip: ({
13+
children,
14+
content,
15+
id,
16+
}: {
17+
children: React.ReactNode
18+
content: string
19+
id: string
20+
}) => (
21+
<div data-testid={id} title={content}>
22+
{children}
23+
</div>
24+
),
25+
}))
26+
27+
jest.mock('next/image', () => ({
28+
__esModule: true,
29+
default: ({
30+
src,
31+
alt,
32+
fill,
33+
objectFit,
34+
...props
35+
}: {
36+
src: string
37+
alt: string
38+
fill?: boolean
39+
objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
40+
[key: string]: unknown
41+
}) => (
42+
// eslint-disable-next-line @next/next/no-img-element
43+
<img
44+
src={src}
45+
alt={alt}
46+
style={fill && { objectFit: objectFit as React.CSSProperties['objectFit'] }}
47+
{...props}
48+
/>
49+
),
50+
}))
51+
52+
const mockPush = jest.fn()
53+
beforeEach(() => {
54+
;(useRouter as jest.Mock).mockReturnValue({ push: mockPush })
55+
mockPush.mockClear()
56+
})
57+
58+
const baseIssue = {
59+
author: {
60+
avatarUrl: 'https://example.com/avatar.png',
61+
login: 'user1',
62+
name: 'User One',
63+
contributionsCount: 10,
64+
createdAt: 1234567890,
65+
followersCount: 5,
66+
followingCount: 2,
67+
key: 'user1',
68+
publicRepositoriesCount: 3,
69+
url: 'https://github.com/user1',
70+
},
71+
createdAt: 1710000000,
72+
hint: 'Hint',
73+
labels: ['bug'],
74+
organizationName: 'org',
75+
projectName: 'proj',
76+
projectUrl: 'https://github.com/org/proj',
77+
summary: 'Summary',
78+
title: 'Issue Title',
79+
updatedAt: 1710000100,
80+
url: 'https://github.com/org/proj/issues/1',
81+
objectID: 'id1',
82+
repositoryName: 'repo',
83+
}
84+
85+
describe('<RecentIssues />', () => {
86+
it('renders successfully with minimal required props', () => {
87+
render(<RecentIssues data={[baseIssue]} />)
88+
expect(screen.getByText('Recent Issues')).toBeInTheDocument()
89+
expect(screen.getByText('Issue Title')).toBeInTheDocument()
90+
})
91+
92+
it('renders "Nothing to display." when data is empty', () => {
93+
render(<RecentIssues data={[]} />)
94+
expect(screen.getByText('Nothing to display.')).toBeInTheDocument()
95+
})
96+
97+
it('shows avatar when showAvatar is true', () => {
98+
render(<RecentIssues data={[baseIssue]} showAvatar={true} />)
99+
expect(screen.getByAltText('User One')).toBeInTheDocument()
100+
})
101+
102+
it('hides avatar when showAvatar is false', () => {
103+
render(<RecentIssues data={[baseIssue]} showAvatar={false} />)
104+
expect(screen.queryByAltText('User One')).not.toBeInTheDocument()
105+
})
106+
107+
it('renders repositoryName and navigates on click', () => {
108+
render(<RecentIssues data={[baseIssue]} />)
109+
const repoBtn = screen.getByText('repo')
110+
expect(repoBtn).toBeInTheDocument()
111+
fireEvent.click(repoBtn)
112+
expect(mockPush).toHaveBeenCalledWith('/organizations/org/repositories/repo')
113+
})
114+
115+
it('does not render repositoryName button if missing', () => {
116+
const issue = { ...baseIssue }
117+
delete issue.repositoryName
118+
render(<RecentIssues data={[issue]} />)
119+
expect(screen.queryByText('repo')).not.toBeInTheDocument()
120+
})
121+
122+
it('renders formatted date', () => {
123+
render(<RecentIssues data={[baseIssue]} />)
124+
expect(screen.getByText(/Mar \d{1,2}, 2024/)).toBeInTheDocument()
125+
})
126+
127+
it('renders label text', () => {
128+
render(<RecentIssues data={[baseIssue]} />)
129+
expect(screen.getByText('Issue Title')).toBeInTheDocument()
130+
})
131+
132+
it('handles edge case: missing author', () => {
133+
const issue: Issue = { ...baseIssue, author: undefined }
134+
render(<RecentIssues data={[issue]} />)
135+
expect(screen.getByText('Issue Title')).toBeInTheDocument()
136+
})
137+
138+
it('handles edge case: missing title', () => {
139+
const issue: Issue = { ...baseIssue, title: undefined }
140+
render(<RecentIssues data={[issue]} />)
141+
expect(screen.getByText('Recent Issues')).toBeInTheDocument()
142+
expect(screen.getByText('repo')).toBeInTheDocument()
143+
expect(screen.getByAltText('User One')).toBeInTheDocument()
144+
})
145+
146+
it('has accessible roles and labels', () => {
147+
render(<RecentIssues data={[baseIssue]} />)
148+
expect(screen.getByRole('heading', { name: /Recent Issues/i })).toBeInTheDocument()
149+
})
150+
151+
it('applies correct DOM structure and classNames', () => {
152+
render(<RecentIssues data={[baseIssue]} />)
153+
expect(screen.getByText('Recent Issues').closest('div')).toHaveClass('flex')
154+
})
155+
156+
it('renders multiple issues', () => {
157+
const issues = [baseIssue, { ...baseIssue, objectID: 'id2', title: 'Second Issue' }]
158+
render(<RecentIssues data={issues} />)
159+
expect(screen.getByText('Second Issue')).toBeInTheDocument()
160+
expect(screen.getAllByText(/Issue Title|Second Issue/).length).toBeGreaterThan(1)
161+
})
162+
163+
it('renders with long repositoryName and truncates', () => {
164+
const issue = { ...baseIssue, repositoryName: 'a'.repeat(100) }
165+
render(<RecentIssues data={[issue]} />)
166+
expect(screen.getByText('a'.repeat(100))).toBeInTheDocument()
167+
})
168+
169+
it('renders with custom organizationName', () => {
170+
const issue = { ...baseIssue, organizationName: 'custom-org' }
171+
render(<RecentIssues data={[issue]} />)
172+
fireEvent.click(screen.getByText('repo'))
173+
expect(mockPush).toHaveBeenCalledWith('/organizations/custom-org/repositories/repo')
174+
})
175+
176+
it('renders with missing props gracefully', () => {
177+
render(<RecentIssues data={[{} as Issue]} showAvatar={false} />)
178+
expect(screen.getByText('Recent Issues')).toBeInTheDocument()
179+
})
180+
181+
it('renders with null data', () => {
182+
render(<RecentIssues data={null as unknown as Issue[]} />)
183+
expect(screen.getByText('Nothing to display.')).toBeInTheDocument()
184+
})
185+
186+
it('defaults to showing avatar when showAvatar is not provided', () => {
187+
render(<RecentIssues data={[baseIssue]} />)
188+
expect(screen.getByAltText('User One')).toBeInTheDocument()
189+
})
190+
})

frontend/lighthouserc.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"ci": {
3+
"assert": {
4+
"assertions": {
5+
"categories:performance": ["warn", { "minScore": 0.9 }],
6+
"categories:accessibility": ["warn", { "minScore": 0.9 }],
7+
"categories:best-practices": ["warn", { "minScore": 0.9 }],
8+
"categories:seo": ["warn", { "minScore": 0.9 }]
9+
}
10+
},
11+
"collect": {
12+
"url": ["https://nest.owasp.dev/"],
13+
"numberOfRuns": 3
14+
},
15+
"upload": {
16+
"target": "temporary-public-storage"
17+
}
18+
}
19+
}

frontend/src/components/RecentIssues.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const RecentIssues: React.FC<RecentIssuesProps> = ({ data, showAvatar = true })
3939
className="cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap text-gray-600 hover:underline dark:text-gray-400"
4040
onClick={() =>
4141
router.push(
42-
`/organizations/${item.organizationName}/repositories/${item.repositoryName || ''}`
42+
`/organizations/${item.organizationName}/repositories/${item.repositoryName}`
4343
)
4444
}
4545
>

0 commit comments

Comments
 (0)