diff --git a/src/pages/AnalyticsPage.jsx b/src/pages/AnalyticsPage.jsx index 5b514a9..d5e6f3b 100644 --- a/src/pages/AnalyticsPage.jsx +++ b/src/pages/AnalyticsPage.jsx @@ -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(() => @@ -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) @@ -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 diff --git a/src/pages/GovernancePage.jsx b/src/pages/GovernancePage.jsx index 7a209f6..b32c05c 100644 --- a/src/pages/GovernancePage.jsx +++ b/src/pages/GovernancePage.jsx @@ -174,7 +174,7 @@ export default function GovernancePage() {
Issue Resolution Rate
Resolution velocity across key repositories
{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 diff --git a/src/pages/RepositoryMetrics.test.jsx b/src/pages/RepositoryMetrics.test.jsx new file mode 100644 index 0000000..d3ac289 --- /dev/null +++ b/src/pages/RepositoryMetrics.test.jsx @@ -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 }) => {JSON.stringify(data)}, + ResponsiveContainer: ({ children }) =>
{children}
, + 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() + 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() + 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') + expect(screen.getByText((_, element) => element?.textContent === '1 merged ยท 1 rejected')) + .toBeInTheDocument() + }) + + it('keeps resolution rates and missing audit data scoped to their organization', () => { + render() + + 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() + } + }) +})