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
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,18 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/jsdom": "^28.0.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^16.4.0",
"prettier": "^3.8.3",
"typescript-eslint": "^8.58.2",
Expand Down
70 changes: 70 additions & 0 deletions src/components/issue/IssueDetail.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { render, screen } from '@testing-library/react';
import { IssueDetail } from './IssueDetail';
import { describe, it, expect } from 'vitest';
import type { RankedIssue } from '../../lib/types';
import React from 'react';

const mockIssue: RankedIssue = {
number: 101,
title: 'Fix styling of buttons',
body: 'Please fix the styled buttons on the home screen.',
updated_at: new Date().toISOString(),
score: 90,
user: { login: 'github_user', avatar_url: '', html_url: '' },
labels: [{ name: 'ui-bug', color: 'ff0000' }],
assignees: [],
comments_count: 1,
created_at: new Date().toISOString(),
html_url: 'https://github.com/example/repo/issues/101',
state: 'open',
analysis: {
summary: 'Simple styling adjustment.',
status: 'active',
progress_estimate: 'early',
is_actionable_code_change: true,
not_mergeable_reason: null,
complexity: 1,
skills_required: ['CSS'],
newcomer_friendliness: 5,
doability_score: 95,
analysis_notes: 'Notes here.',
},
comments: [
{
id: 1,
user: { login: 'commenter', avatar_url: '', html_url: '' },
body: 'I will take a look.',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
],
};

describe('IssueDetail component', () => {
it('renders select issue placeholder when issue is null', () => {
render(<IssueDetail issue={null} />);
expect(screen.getByText('Select an issue to view details')).toBeInTheDocument();
});

it('renders all details when issue is provided', () => {
render(<IssueDetail issue={mockIssue} />);

expect(screen.getByText('#101 — Fix styling of buttons')).toBeInTheDocument();
expect(screen.getByText('github_user')).toBeInTheDocument();
expect(screen.getByText('1 comment')).toBeInTheDocument();
expect(screen.getByText('ui-bug')).toBeInTheDocument();

// AI Analysis section
expect(screen.getByText('Simple styling adjustment.')).toBeInTheDocument();
expect(screen.getByText('Notes here.')).toBeInTheDocument();

// Comments section
expect(screen.getByText('@commenter')).toBeInTheDocument();
expect(screen.getByText('I will take a look.')).toBeInTheDocument();

// Issue Body
expect(
screen.getByText('Please fix the styled buttons on the home screen.'),
).toBeInTheDocument();
});
});
3 changes: 2 additions & 1 deletion src/components/issue/IssueDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ export function IssueDetail({ issue }: IssueDetailProps) {
<Calendar size={11} /> {formatTimestamp(issue.created_at)}
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<MessageSquare size={11} /> {issue.comments_count} comments
<MessageSquare size={11} /> {issue.comments_count}{' '}
{issue.comments_count === 1 ? 'comment' : 'comments'}
</span>
<span>Updated {formatTimeAgo(issue.updated_at)}</span>
</div>
Expand Down
146 changes: 146 additions & 0 deletions src/components/issue/IssueList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { IssueList } from './IssueList';
import { describe, it, expect, vi } from 'vitest';
import type { RankedIssue } from '../../lib/types';
import React from 'react';

const mockIssues: RankedIssue[] = [
{
number: 1,
title: 'Bug in routing',
body: null,
updated_at: new Date().toISOString(),
score: 80,
user: { login: 'coder1', avatar_url: '', html_url: '' },
labels: [{ name: 'bug', color: 'red' }],
assignees: [],
comments_count: 0,
created_at: new Date().toISOString(),
html_url: '',
state: 'open',
},
{
number: 2,
title: 'Style updates for login page',
body: null,
updated_at: new Date().toISOString(),
score: 60,
user: { login: 'designer1', avatar_url: '', html_url: '' },
labels: [{ name: 'design', color: 'blue' }],
assignees: [],
comments_count: 0,
created_at: new Date().toISOString(),
html_url: '',
state: 'open',
},
];

describe('IssueList component', () => {
it('renders issues correctly', () => {
render(
<IssueList
issues={mockIssues}
selectedId={1}
searchQuery=""
onSearchChange={vi.fn()}
onSelect={vi.fn()}
selectedIndex={0}
/>,
);

expect(screen.getByText('Bug in routing')).toBeInTheDocument();
expect(screen.getByText('Style updates for login page')).toBeInTheDocument();
expect(screen.getByText('2 items')).toBeInTheDocument();
});

it('triggers onSearchChange when typing in search input', () => {
const onSearchChange = vi.fn();
render(
<IssueList
issues={mockIssues}
selectedId={1}
searchQuery=""
onSearchChange={onSearchChange}
onSelect={vi.fn()}
selectedIndex={0}
/>,
);

const input = screen.getByPlaceholderText(/Search issues.../);
fireEvent.change(input, { target: { value: 'routing' } });
expect(onSearchChange).toHaveBeenCalledWith('routing');
});

it('filters issues based on searchQuery prop', () => {
render(
<IssueList
issues={mockIssues}
selectedId={1}
searchQuery="routing"
onSearchChange={vi.fn()}
onSelect={vi.fn()}
selectedIndex={0}
/>,
);

expect(screen.getByText('Bug in routing')).toBeInTheDocument();
expect(screen.queryByText('Style updates for login page')).not.toBeInTheDocument();
expect(screen.getByText('1 items')).toBeInTheDocument();
});

it('triggers onSelect when an issue item is clicked', () => {
const onSelect = vi.fn();
render(
<IssueList
issues={mockIssues}
selectedId={1}
searchQuery=""
onSearchChange={vi.fn()}
onSelect={onSelect}
selectedIndex={0}
/>,
);

fireEvent.click(screen.getByText('Style updates for login page'));
expect(onSelect).toHaveBeenCalledWith(2);
});

it('shows no issues found message when filtered list is empty', () => {
render(
<IssueList
issues={mockIssues}
selectedId={1}
searchQuery="xyz"
onSearchChange={vi.fn()}
onSelect={vi.fn()}
selectedIndex={-1}
/>,
);

expect(screen.getByText('No issues match your search.')).toBeInTheDocument();
});

it('verifies that scrollIntoView is called on the selected item', () => {
const scrollMock = vi.fn();
const originalGetElementById = document.getElementById;
document.getElementById = vi.fn().mockReturnValue({
scrollIntoView: scrollMock,
});

render(
<IssueList
issues={mockIssues}
selectedId={1}
searchQuery=""
onSearchChange={vi.fn()}
onSelect={vi.fn()}
selectedIndex={0}
/>,
);

expect(document.getElementById).toHaveBeenCalledWith('issue-0');
expect(scrollMock).toHaveBeenCalledWith({ block: 'nearest', behavior: 'smooth' });

document.getElementById = originalGetElementById;
});
});
Comment thread
Aarya1402 marked this conversation as resolved.
73 changes: 73 additions & 0 deletions src/components/issue/IssueListItem.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { IssueListItem } from './IssueListItem';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { RankedIssue } from '../../lib/types';
import React from 'react';

const getMockIssue = (): RankedIssue => ({
number: 101,
title: 'Optimize database queries',
body: null,
updated_at: new Date(Date.now() - 3600000).toISOString(), // 1 hour ago
score: 95,
user: { login: 'octocat', avatar_url: '', html_url: '' },
labels: [],
assignees: [],
comments_count: 0,
created_at: new Date(Date.now() - 7200000).toISOString(),
html_url: '',
state: 'open',
});

describe('IssueListItem component', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-05-18T12:00:00Z'));
});

afterEach(() => {
vi.useRealTimers();
});

it('renders standard fields correctly', () => {
const onClick = vi.fn();
render(<IssueListItem issue={getMockIssue()} rank={1} isSelected={false} onClick={onClick} />);

expect(screen.getByText('1.')).toBeInTheDocument();
expect(screen.getByText('#101')).toBeInTheDocument();
expect(screen.getByText('Optimize database queries')).toBeInTheDocument();
expect(screen.getByText('[95]')).toBeInTheDocument();
expect(screen.getByText('1h ago')).toBeInTheDocument();
Comment thread
Aarya1402 marked this conversation as resolved.
});

it('triggers onClick when clicked', () => {
const onClick = vi.fn();
render(<IssueListItem issue={getMockIssue()} rank={1} isSelected={false} onClick={onClick} />);

fireEvent.click(screen.getByText('Optimize database queries'));
expect(onClick).toHaveBeenCalled();
});

it('renders status prefix tag when analysis exists', () => {
const issueWithAnalysis: RankedIssue = {
...getMockIssue(),
analysis: {
summary: 'Discussion required',
status: 'discussion',
progress_estimate: 'not_started',
is_actionable_code_change: false,
not_mergeable_reason: null,
complexity: 1,
skills_required: [],
newcomer_friendliness: 5,
doability_score: 50,
analysis_notes: '',
},
};

render(
<IssueListItem issue={issueWithAnalysis} rank={1} isSelected={false} onClick={vi.fn()} />,
);
expect(screen.getByText('[DISCUSSION]')).toBeInTheDocument();
});
});
60 changes: 60 additions & 0 deletions src/components/issue/IssueMetadata.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { render, screen } from '@testing-library/react';
import { IssueMetadata } from './IssueMetadata';
import { describe, it, expect } from 'vitest';
import type { AnalysisResult } from '../../lib/types';
import React from 'react';

const mockAnalysis: AnalysisResult = {
summary: 'This is a summary',
status: 'active',
progress_estimate: 'early',
is_actionable_code_change: true,
not_mergeable_reason: null,
complexity: 3,
skills_required: ['React', 'TypeScript'],
newcomer_friendliness: 4,
doability_score: 80,
analysis_notes: 'Some notes',
};

describe('IssueMetadata component', () => {
it('renders all analysis metrics correctly', () => {
render(<IssueMetadata analysis={mockAnalysis} />);

expect(screen.getByText('Doability')).toBeInTheDocument();
expect(screen.getByText('80/100')).toBeInTheDocument();

expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByText('Active')).toBeInTheDocument();

expect(screen.getByText('Complexity')).toBeInTheDocument();
expect(screen.getByText(/Moderate/)).toBeInTheDocument();

expect(screen.getByText('Friendliness')).toBeInTheDocument();
expect(screen.getByText(/Beginner Friendly/)).toBeInTheDocument();

expect(screen.getByText('React')).toBeInTheDocument();
expect(screen.getByText('TypeScript')).toBeInTheDocument();
});

it('renders merge blocker when not_mergeable_reason is provided', () => {
const analysisWithBlocker: AnalysisResult = {
...mockAnalysis,
not_mergeable_reason: 'Missing details',
};
render(<IssueMetadata analysis={analysisWithBlocker} />);

expect(screen.getByText('Blocker')).toBeInTheDocument();
expect(screen.getByText('Missing details')).toBeInTheDocument();
});

it('renders "none specified" when skills_required is empty', () => {
const analysisNoSkills: AnalysisResult = {
...mockAnalysis,
skills_required: [],
};
render(<IssueMetadata analysis={analysisNoSkills} />);

expect(screen.getByText('none specified')).toBeInTheDocument();
});
});
Loading
Loading