Skip to content

Commit 264a5b6

Browse files
authored
merge: feat: Enhance Contributor Risk Table with Live Search and Sorting Features (#8262)
## Description Resolves #8217. This PR adds dynamic search filtering, multi-column interactive header sorting, a "High Risk Only" quick filter toggle, and an empty search state to the **Contributor Burnout Risk Table** (`/burnout-analyzer`). ### Key Enhancements & Features: 1. **Live Search Input**: Filters contributors dynamically by username in real-time with a quick clear (`X`) button. 2. **"High Risk Only" Quick Filter**: Toggle switch to instantly filter the table down to high-risk contributors (`riskLevel === 'High'`), with a badge showing the high-risk count. 3. **Interactive Column Header Sorting**: Clickable headers with directional sort arrows (`ArrowUpDown`, `ArrowUp`, `ArrowDown`) supporting ascending/descending sorting for: - Contributor username (A-Z / Z-A) - Workload Share (%) - Intensity Weeks - Rest Weeks - Burnout Risk Score 4. **Empty State & Filter Reset**: Friendly empty state banner when zero contributors match search/filters, complete with a "Reset Filters" action button. 5. **Contributor Counter Badge**: Shows live count (`Showing X of Y contributors`). --- ## Target Files Modified: - `components/burnout/BurnoutRiskTable.tsx` - `components/burnout/BurnoutRiskTable.test.tsx` --- ## Verification & Testing - ✅ **Unit Tests**: Ran `npx vitest run components/burnout/BurnoutRiskTable.test.tsx` — **5 / 5 tests passed**. - ✅ **TypeScript Check**: Ran `npx tsc --noEmit` — **0 errors**. - ✅ **Linting**: Passed `eslint --fix` and `prettier` pre-commit checks cleanly. --- *GSSoC 2026 Contribution*
2 parents 11b163e + 09440a6 commit 264a5b6

2 files changed

Lines changed: 341 additions & 15 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import BurnoutRiskTable from './BurnoutRiskTable';
4+
5+
const mockContributors = [
6+
{
7+
username: 'alice',
8+
avatarUrl: 'https://github.com/alice.png',
9+
totalCommits: 500,
10+
commitShare: 50,
11+
burnoutScore: 85,
12+
riskLevel: 'High' as const,
13+
activeWeeks: 12,
14+
highIntensityWeeks: 6,
15+
consecutiveHighWeeks: 4,
16+
restWeeks: 0,
17+
recentTrend: [10, 20, 30],
18+
},
19+
{
20+
username: 'bob',
21+
avatarUrl: 'https://github.com/bob.png',
22+
totalCommits: 300,
23+
commitShare: 30,
24+
burnoutScore: 60,
25+
riskLevel: 'Medium' as const,
26+
activeWeeks: 8,
27+
highIntensityWeeks: 2,
28+
consecutiveHighWeeks: 1,
29+
restWeeks: 2,
30+
recentTrend: [5, 10, 15],
31+
},
32+
{
33+
username: 'charlie',
34+
avatarUrl: 'https://github.com/charlie.png',
35+
totalCommits: 200,
36+
commitShare: 20,
37+
burnoutScore: 20,
38+
riskLevel: 'Low' as const,
39+
activeWeeks: 4,
40+
highIntensityWeeks: 0,
41+
consecutiveHighWeeks: 0,
42+
restWeeks: 6,
43+
recentTrend: [2, 4, 6],
44+
},
45+
];
46+
47+
describe('BurnoutRiskTable Live Search & Column Sorting', () => {
48+
beforeEach(() => {
49+
// cleanup
50+
});
51+
52+
it('renders all contributors initially', () => {
53+
render(<BurnoutRiskTable contributors={mockContributors} />);
54+
expect(screen.getByText('@alice')).toBeInTheDocument();
55+
expect(screen.getByText('@bob')).toBeInTheDocument();
56+
expect(screen.getByText('@charlie')).toBeInTheDocument();
57+
expect(screen.getByText(/Showing 3 of 3/i)).toBeInTheDocument();
58+
});
59+
60+
it('filters contributors dynamically by search query', () => {
61+
render(<BurnoutRiskTable contributors={mockContributors} />);
62+
const searchInput = screen.getByPlaceholderText(/search contributor/i);
63+
64+
fireEvent.change(searchInput, { target: { value: 'bob' } });
65+
66+
expect(screen.getByText('@bob')).toBeInTheDocument();
67+
expect(screen.queryByText('@alice')).not.toBeInTheDocument();
68+
expect(screen.queryByText('@charlie')).not.toBeInTheDocument();
69+
expect(screen.getByText(/Showing 1 of 3/i)).toBeInTheDocument();
70+
});
71+
72+
it('filters contributors to high risk only when toggle is enabled', () => {
73+
render(<BurnoutRiskTable contributors={mockContributors} />);
74+
const toggleBtn = screen.getByRole('button', { name: /high risk only/i });
75+
76+
fireEvent.click(toggleBtn);
77+
78+
expect(screen.getByText('@alice')).toBeInTheDocument();
79+
expect(screen.queryByText('@bob')).not.toBeInTheDocument();
80+
expect(screen.queryByText('@charlie')).not.toBeInTheDocument();
81+
expect(screen.getByText(/Showing 1 of 3/i)).toBeInTheDocument();
82+
});
83+
84+
it('sorts contributors by username when Contributor header is clicked', () => {
85+
render(<BurnoutRiskTable contributors={mockContributors} />);
86+
const contributorHeaderBtn = screen.getByRole('button', { name: /contributor/i });
87+
88+
// Initial default sort is burnoutScore desc (alice, bob, charlie)
89+
fireEvent.click(contributorHeaderBtn); // Sort by username desc (charlie, bob, alice)
90+
91+
const namesDesc = screen.getAllByText(/@(alice|bob|charlie)/i).map((el) => el.textContent);
92+
expect(namesDesc).toEqual(['@charlie', '@bob', '@alice']);
93+
94+
fireEvent.click(contributorHeaderBtn); // Toggle to asc (alice, bob, charlie)
95+
const namesAsc = screen.getAllByText(/@(alice|bob|charlie)/i).map((el) => el.textContent);
96+
expect(namesAsc).toEqual(['@alice', '@bob', '@charlie']);
97+
});
98+
99+
it('shows empty state and resets filters when Reset Filters button is clicked', () => {
100+
render(<BurnoutRiskTable contributors={mockContributors} />);
101+
const searchInput = screen.getByPlaceholderText(/search contributor/i);
102+
103+
fireEvent.change(searchInput, { target: { value: 'nonexistent_user' } });
104+
105+
expect(screen.getByText(/No contributors match your filters/i)).toBeInTheDocument();
106+
107+
const resetBtn = screen.getByRole('button', { name: /reset filters/i });
108+
fireEvent.click(resetBtn);
109+
110+
expect(screen.getByText('@alice')).toBeInTheDocument();
111+
expect(screen.getByText('@bob')).toBeInTheDocument();
112+
expect(screen.getByText('@charlie')).toBeInTheDocument();
113+
});
114+
});

0 commit comments

Comments
 (0)