Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/pages/AnalyticsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ export default function AnalyticsPage() {

const filteredIssues = useMemo(() => {
if (selectedRepo === 'All') return allIssues
const key = Object.keys(issuesData || {}).find(k => k.split('/')[1] === selectedRepo)
return key ? (issuesData[key] || []) : []
return issuesData?.[selectedRepo] || []
}, [allIssues, selectedRepo, issuesData])

const series = useMemo(() =>
Expand All @@ -48,8 +47,7 @@ export default function AnalyticsPage() {

const filteredPulls = useMemo(() => {
if (selectedRepoForAM === 'All Repositories') return Object.values(pullsData || {}).flat()
const key = Object.keys(pullsData || {}).find(k => k.split('/')[1] === selectedRepoForAM)
return key ? (pullsData[key] || []) : []
return pullsData?.[selectedRepoForAM] || []
}, [pullsData, selectedRepoForAM])

const advancedMetrics = useAdvancedMetrics(filteredPulls)
Expand All @@ -68,8 +66,8 @@ export default function AnalyticsPage() {
}
]

const repoNames = ['All', ...Object.keys(issuesData || {}).map(k => k.split('/')[1])]
const allRepoNames = ['All Repositories', ...Object.keys(pullsData || {}).map(k => k.split('/')[1])]
const repoNames = ['All', ...Object.keys(issuesData || {})]
const allRepoNames = ['All Repositories', ...Object.keys(pullsData || {})]
const hasData = Object.keys(issuesData || {}).length > 0
const hasPullsData = Object.keys(pullsData || {}).length > 0
const hasSeries = series.length > 0
Expand Down
2 changes: 1 addition & 1 deletion src/pages/GovernancePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export default function GovernancePage() {
<div style={{ fontWeight: 600, marginBottom: 4 }}>Issue Resolution Rate</div>
<div style={{ ...C.label, marginBottom: 16 }}>Resolution velocity across key repositories</div>
{topRepos.map(r => {
const repoIssues = allIssues.filter(i => i.repoName === r.name)
const repoIssues = issuesData?.[`${r.orgLogin}/${r.name}`] || []
const closed = repoIssues.filter(i => i.state === 'closed').length
const total = repoIssues.length
const rate = total ? Math.round(closed / total * 100) : null
Expand Down
97 changes: 97 additions & 0 deletions src/pages/RepositoryMetrics.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import AnalyticsPage from './AnalyticsPage'
import GovernancePage from './GovernancePage'

const { app } = vi.hoisted(() => ({ app: {} }))

vi.mock('../context/AppContext', () => ({ useApp: () => app }))

// Chart layout is not needed to exercise repository selection and its metrics.
vi.mock('recharts', () => {
const EmptyChart = () => null
return {
AreaChart: ({ data }) => <output data-testid="trend-series">{JSON.stringify(data)}</output>,
ResponsiveContainer: ({ children }) => <div>{children}</div>,
Area: EmptyChart, XAxis: EmptyChart, YAxis: EmptyChart,
CartesianGrid: EmptyChart, Tooltip: EmptyChart, Legend: EmptyChart,
PieChart: EmptyChart, Pie: EmptyChart, Cell: EmptyChart,
RadialBarChart: EmptyChart, RadialBar: EmptyChart, PolarAngleAxis: EmptyChart,
}
})

beforeEach(() => {
Object.assign(app, {
model: {
allRepos: [
{ id: 1, name: 'docs', orgLogin: 'first-org', license: { key: 'mit' } },
{ id: 2, name: 'docs', orgLogin: 'second-org', license: { key: 'mit' } },
{ id: 3, name: 'docs', orgLogin: 'uncached-org', license: { key: 'mit' } },
],
},
issuesData: {
'first-org/docs': [{ id: 1, state: 'closed', created_at: '2026-01-01T00:00:00Z' }],
'second-org/docs': [{ id: 2, state: 'open', created_at: '2026-02-01T00:00:00Z' }],
},
pullsData: {
'first-org/docs': [{ state: 'closed', created_at: '2026-01-01T00:00:00Z', merged_at: '2026-01-03T00:00:00Z' }],
'second-org/docs': [{ state: 'closed', created_at: '2026-02-01T00:00:00Z', merged_at: null }],
},
staleRepoStats: [],
loading: false,
govLoading: false,
advanceAnalyticsLoading: false,
auditComplete: true,
advanceAnalyticsComplete: true,
pat: '',
})
})

describe('repository metrics across organizations', () => {
it('selects each same-name repository independently in activity trends', async () => {
const user = userEvent.setup()
render(<AnalyticsPage />)
const selector = screen.getAllByRole('combobox')[0]

expect(within(selector).getAllByRole('option').map(option => option.value))
.toEqual(['All', 'first-org/docs', 'second-org/docs'])

for (const [repo, month] of [['first-org/docs', '2026-01'], ['second-org/docs', '2026-02']]) {
await user.selectOptions(selector, repo)
const series = JSON.parse(screen.getAllByTestId('trend-series')[0].textContent)
expect(series).toHaveLength(1)
expect(series[0]).toMatchObject({ date: month, issues_created: 1 })
}

await user.selectOptions(selector, 'All')
expect(JSON.parse(screen.getAllByTestId('trend-series')[0].textContent)).toHaveLength(2)
})

it('uses the selected organization for advanced pull request metrics', async () => {
const user = userEvent.setup()
render(<AnalyticsPage />)
const selector = screen.getAllByRole('combobox')[1]

expect(within(selector).getAllByRole('option').map(option => option.value))
.toEqual(['All Repositories', 'first-org/docs', 'second-org/docs'])

await user.selectOptions(selector, 'first-org/docs')
expect(screen.getByText('Based on', { exact: false })).toHaveTextContent('1 merged pull requests')
await user.selectOptions(selector, 'second-org/docs')
expect(screen.getByText('Based on', { exact: false })).toHaveTextContent('0 merged pull requests')
await user.selectOptions(selector, 'All Repositories')
expect(screen.getByText('Based on', { exact: false })).toHaveTextContent('1 merged pull requests')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(screen.getByText((_, element) => element?.textContent === '1 merged · 1 rejected'))
.toBeInTheDocument()
})

it('keeps resolution rates and missing audit data scoped to their organization', () => {
render(<GovernancePage />)

for (const [org, rate] of [['first-org', '100%'], ['second-org', '0%'], ['uncached-org', 'No data']]) {
const row = screen.getByText(org).parentElement.parentElement
expect(within(row).getByText(rate)).toBeInTheDocument()
}
})
})
Loading