forked from cardosofelipe/fast-next-template
- Raise coverage thresholds to 90% statements/lines/functions, 85% branches - Add comprehensive tests for ProjectDashboard, ProjectWizard, and all wizard steps - Add tests for issue management: IssueDetailPanel, BulkActions, IssueFilters - Expand IssueTable tests with keyboard navigation, dropdown menu, edge cases - Add useIssues hook tests covering all mutations and optimistic updates - Expand eventStore tests with selector hooks and additional scenarios - Expand useProjectEvents tests with error recovery, ping events, edge cases - Add PriorityBadge, StatusBadge, SyncStatusIndicator fallback branch tests - Add constants.test.ts for comprehensive constant validation Bug fixes: - Fix false positive rollback test to properly verify onMutate context setup - Replace deprecated substr() with substring() in mock helpers - Fix type errors: ProjectComplexity, ClientMode enum values - Fix unused imports and variables across test files - Fix @ts-expect-error directives and method override signatures 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
281 lines
9.6 KiB
TypeScript
281 lines
9.6 KiB
TypeScript
/**
|
|
* IssueDetailPanel Component Tests
|
|
*
|
|
* Comprehensive tests for the issue detail side panel component.
|
|
*/
|
|
|
|
import { render, screen } from '@testing-library/react';
|
|
import { IssueDetailPanel } from '@/features/issues/components/IssueDetailPanel';
|
|
import { mockIssueDetail } from '@/features/issues/mocks';
|
|
import type { IssueDetail } from '@/features/issues/types';
|
|
|
|
describe('IssueDetailPanel', () => {
|
|
const defaultIssue = mockIssueDetail;
|
|
|
|
it('renders the details card header', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(screen.getByText('Details')).toBeInTheDocument();
|
|
});
|
|
|
|
describe('Assignee section', () => {
|
|
it('displays assignee name when assigned', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(screen.getByText('Assignee')).toBeInTheDocument();
|
|
expect(screen.getByText(defaultIssue.assignee!.name)).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays assignee avatar text when provided', () => {
|
|
const issueWithAvatar: IssueDetail = {
|
|
...defaultIssue,
|
|
assignee: {
|
|
...defaultIssue.assignee!,
|
|
avatar: 'BE',
|
|
},
|
|
};
|
|
render(<IssueDetailPanel issue={issueWithAvatar} />);
|
|
expect(screen.getByText('BE')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays agent icon for agent assignee without avatar', () => {
|
|
const issueWithAgentNoAvatar: IssueDetail = {
|
|
...defaultIssue,
|
|
assignee: {
|
|
id: 'agent-1',
|
|
name: 'Test Agent',
|
|
type: 'agent',
|
|
// No avatar
|
|
},
|
|
};
|
|
render(<IssueDetailPanel issue={issueWithAgentNoAvatar} />);
|
|
// Bot icon should be rendered (with aria-hidden)
|
|
const icons = document.querySelectorAll('[aria-hidden="true"]');
|
|
expect(icons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('displays human icon for human assignee without avatar', () => {
|
|
const issueWithHumanNoAvatar: IssueDetail = {
|
|
...defaultIssue,
|
|
assignee: {
|
|
id: 'human-1',
|
|
name: 'Test Human',
|
|
type: 'human',
|
|
// No avatar
|
|
},
|
|
};
|
|
render(<IssueDetailPanel issue={issueWithHumanNoAvatar} />);
|
|
expect(screen.getByText('Test Human')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays assignee type', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(screen.getByText(defaultIssue.assignee!.type)).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows "Unassigned" when no assignee', () => {
|
|
const unassignedIssue: IssueDetail = {
|
|
...defaultIssue,
|
|
assignee: null,
|
|
};
|
|
render(<IssueDetailPanel issue={unassignedIssue} />);
|
|
expect(screen.getByText('Unassigned')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Reporter section', () => {
|
|
it('displays reporter name', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(screen.getByText('Reporter')).toBeInTheDocument();
|
|
expect(screen.getByText(defaultIssue.reporter.name)).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays reporter avatar text when provided', () => {
|
|
const issueWithReporterAvatar: IssueDetail = {
|
|
...defaultIssue,
|
|
reporter: {
|
|
...defaultIssue.reporter,
|
|
avatar: 'PO',
|
|
},
|
|
};
|
|
render(<IssueDetailPanel issue={issueWithReporterAvatar} />);
|
|
expect(screen.getByText('PO')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays agent icon for agent reporter without avatar', () => {
|
|
const issueWithAgentReporter: IssueDetail = {
|
|
...defaultIssue,
|
|
reporter: {
|
|
id: 'agent-1',
|
|
name: 'Agent Reporter',
|
|
type: 'agent',
|
|
},
|
|
};
|
|
render(<IssueDetailPanel issue={issueWithAgentReporter} />);
|
|
expect(screen.getByText('Agent Reporter')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays human icon for human reporter without avatar', () => {
|
|
const issueWithHumanReporter: IssueDetail = {
|
|
...defaultIssue,
|
|
reporter: {
|
|
id: 'human-1',
|
|
name: 'Human Reporter',
|
|
type: 'human',
|
|
},
|
|
};
|
|
render(<IssueDetailPanel issue={issueWithHumanReporter} />);
|
|
expect(screen.getByText('Human Reporter')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Sprint section', () => {
|
|
it('displays sprint name when assigned', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(screen.getByText('Sprint')).toBeInTheDocument();
|
|
expect(screen.getByText(defaultIssue.sprint!)).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows "Backlog" when no sprint', () => {
|
|
const backlogIssue: IssueDetail = {
|
|
...defaultIssue,
|
|
sprint: null,
|
|
};
|
|
render(<IssueDetailPanel issue={backlogIssue} />);
|
|
expect(screen.getByText('Backlog')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Story Points section', () => {
|
|
it('displays story points when present', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(screen.getByText('Story Points')).toBeInTheDocument();
|
|
expect(screen.getByText(String(defaultIssue.story_points))).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not render story points section when null', () => {
|
|
const issueNoPoints: IssueDetail = {
|
|
...defaultIssue,
|
|
story_points: null,
|
|
};
|
|
render(<IssueDetailPanel issue={issueNoPoints} />);
|
|
expect(screen.queryByText('Story Points')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Due Date section', () => {
|
|
it('displays due date when present', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(screen.getByText('Due Date')).toBeInTheDocument();
|
|
// Date formatting varies by locale, check structure exists
|
|
const dueDate = new Date(defaultIssue.due_date!).toLocaleDateString();
|
|
expect(screen.getByText(dueDate)).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not render due date section when null', () => {
|
|
const issueNoDueDate: IssueDetail = {
|
|
...defaultIssue,
|
|
due_date: null,
|
|
};
|
|
render(<IssueDetailPanel issue={issueNoDueDate} />);
|
|
expect(screen.queryByText('Due Date')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Labels section', () => {
|
|
it('displays all labels', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(screen.getByText('Labels')).toBeInTheDocument();
|
|
defaultIssue.labels.forEach((label) => {
|
|
expect(screen.getByText(label.name)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('applies label colors when provided', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
const labelWithColor = defaultIssue.labels.find((l) => l.color);
|
|
if (labelWithColor) {
|
|
const labelElement = screen.getByText(labelWithColor.name);
|
|
// The parent badge should have inline style
|
|
expect(labelElement.closest('[class*="Badge"]') || labelElement.parentElement).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
it('shows "No labels" when labels array is empty', () => {
|
|
const issueNoLabels: IssueDetail = {
|
|
...defaultIssue,
|
|
labels: [],
|
|
};
|
|
render(<IssueDetailPanel issue={issueNoLabels} />);
|
|
expect(screen.getByText('No labels')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders labels without color property', () => {
|
|
const issueWithColorlessLabels: IssueDetail = {
|
|
...defaultIssue,
|
|
labels: [
|
|
{ id: 'lbl-1', name: 'colorless-label' },
|
|
],
|
|
};
|
|
render(<IssueDetailPanel issue={issueWithColorlessLabels} />);
|
|
expect(screen.getByText('colorless-label')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Development section', () => {
|
|
it('renders development card when branch is present', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(screen.getByText('Development')).toBeInTheDocument();
|
|
expect(screen.getByText(defaultIssue.branch!)).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders development card when pull request is present', () => {
|
|
render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(screen.getByText(defaultIssue.pull_request!)).toBeInTheDocument();
|
|
expect(screen.getByText('Open')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders branch only when PR is null', () => {
|
|
const issueNoPR: IssueDetail = {
|
|
...defaultIssue,
|
|
pull_request: null,
|
|
};
|
|
render(<IssueDetailPanel issue={issueNoPR} />);
|
|
expect(screen.getByText(defaultIssue.branch!)).toBeInTheDocument();
|
|
expect(screen.queryByText('Open')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('renders PR only when branch is null', () => {
|
|
const issueNoBranch: IssueDetail = {
|
|
...defaultIssue,
|
|
branch: null,
|
|
};
|
|
render(<IssueDetailPanel issue={issueNoBranch} />);
|
|
expect(screen.getByText(defaultIssue.pull_request!)).toBeInTheDocument();
|
|
expect(screen.getByText('Development')).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not render development section when both branch and PR are null', () => {
|
|
const issueNoDev: IssueDetail = {
|
|
...defaultIssue,
|
|
branch: null,
|
|
pull_request: null,
|
|
};
|
|
render(<IssueDetailPanel issue={issueNoDev} />);
|
|
expect(screen.queryByText('Development')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Styling', () => {
|
|
it('applies custom className', () => {
|
|
const { container } = render(
|
|
<IssueDetailPanel issue={defaultIssue} className="custom-class" />
|
|
);
|
|
expect(container.firstChild).toHaveClass('custom-class');
|
|
});
|
|
|
|
it('has default spacing class', () => {
|
|
const { container } = render(<IssueDetailPanel issue={defaultIssue} />);
|
|
expect(container.firstChild).toHaveClass('space-y-6');
|
|
});
|
|
});
|
|
});
|