test(frontend): comprehensive test coverage improvements and bug fixes
- 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>
This commit is contained in:
157
frontend/tests/features/issues/components/BulkActions.test.tsx
Normal file
157
frontend/tests/features/issues/components/BulkActions.test.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* BulkActions Component Tests
|
||||
*
|
||||
* Comprehensive tests for the bulk actions toolbar component.
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { BulkActions } from '@/features/issues/components/BulkActions';
|
||||
|
||||
describe('BulkActions', () => {
|
||||
const defaultProps = {
|
||||
selectedCount: 3,
|
||||
onChangeStatus: jest.fn(),
|
||||
onAssign: jest.fn(),
|
||||
onAddLabels: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Visibility', () => {
|
||||
it('renders when selectedCount > 0', () => {
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
expect(screen.getByRole('toolbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render when selectedCount is 0', () => {
|
||||
render(<BulkActions {...defaultProps} selectedCount={0} />);
|
||||
expect(screen.queryByRole('toolbar')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Selected count display', () => {
|
||||
it('displays the selected count', () => {
|
||||
render(<BulkActions {...defaultProps} selectedCount={5} />);
|
||||
expect(screen.getByText('5 selected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays singular count correctly', () => {
|
||||
render(<BulkActions {...defaultProps} selectedCount={1} />);
|
||||
expect(screen.getByText('1 selected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays large count correctly', () => {
|
||||
render(<BulkActions {...defaultProps} selectedCount={100} />);
|
||||
expect(screen.getByText('100 selected')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Action buttons', () => {
|
||||
it('renders Change Status button', () => {
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
expect(screen.getByRole('button', { name: 'Change Status' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Assign button', () => {
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
expect(screen.getByRole('button', { name: 'Assign' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Add Labels button', () => {
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
expect(screen.getByRole('button', { name: 'Add Labels' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Delete button', () => {
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Button callbacks', () => {
|
||||
it('calls onChangeStatus when Change Status is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Change Status' }));
|
||||
expect(defaultProps.onChangeStatus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onAssign when Assign is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Assign' }));
|
||||
expect(defaultProps.onAssign).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onAddLabels when Add Labels is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Add Labels' }));
|
||||
expect(defaultProps.onAddLabels).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onDelete when Delete is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /delete/i }));
|
||||
expect(defaultProps.onDelete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('has accessible toolbar role', () => {
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
const toolbar = screen.getByRole('toolbar');
|
||||
expect(toolbar).toHaveAttribute('aria-label', 'Bulk actions for selected issues');
|
||||
});
|
||||
|
||||
it('delete icon has aria-hidden', () => {
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
const icons = document.querySelectorAll('[aria-hidden="true"]');
|
||||
expect(icons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('applies custom className', () => {
|
||||
render(<BulkActions {...defaultProps} className="custom-toolbar-class" />);
|
||||
expect(screen.getByRole('toolbar')).toHaveClass('custom-toolbar-class');
|
||||
});
|
||||
|
||||
it('delete button has destructive styling', () => {
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
const deleteButton = screen.getByRole('button', { name: /delete/i });
|
||||
expect(deleteButton).toHaveClass('text-destructive');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('handles rapid clicks on all buttons', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<BulkActions {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Change Status' }));
|
||||
await user.click(screen.getByRole('button', { name: 'Assign' }));
|
||||
await user.click(screen.getByRole('button', { name: 'Add Labels' }));
|
||||
await user.click(screen.getByRole('button', { name: /delete/i }));
|
||||
|
||||
expect(defaultProps.onChangeStatus).toHaveBeenCalledTimes(1);
|
||||
expect(defaultProps.onAssign).toHaveBeenCalledTimes(1);
|
||||
expect(defaultProps.onAddLabels).toHaveBeenCalledTimes(1);
|
||||
expect(defaultProps.onDelete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('works with very large selected count', () => {
|
||||
render(<BulkActions {...defaultProps} selectedCount={999999} />);
|
||||
expect(screen.getByText('999999 selected')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -123,4 +123,139 @@ describe('IssueFilters', () => {
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
describe('Filter change handlers', () => {
|
||||
it('renders status filter with correct trigger', () => {
|
||||
render(<IssueFilters filters={defaultFilters} onFiltersChange={mockOnFiltersChange} />);
|
||||
|
||||
const statusTrigger = screen.getByRole('combobox', { name: /filter by status/i });
|
||||
expect(statusTrigger).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders priority filter when extended filters open', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<IssueFilters filters={defaultFilters} onFiltersChange={mockOnFiltersChange} />);
|
||||
|
||||
const filterButton = screen.getByRole('button', { name: /toggle extended filters/i });
|
||||
await user.click(filterButton);
|
||||
|
||||
expect(screen.getByLabelText('Priority')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders sprint filter when extended filters open', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<IssueFilters filters={defaultFilters} onFiltersChange={mockOnFiltersChange} />);
|
||||
|
||||
const filterButton = screen.getByRole('button', { name: /toggle extended filters/i });
|
||||
await user.click(filterButton);
|
||||
|
||||
expect(screen.getByLabelText('Sprint')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders assignee filter when extended filters open', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<IssueFilters filters={defaultFilters} onFiltersChange={mockOnFiltersChange} />);
|
||||
|
||||
const filterButton = screen.getByRole('button', { name: /toggle extended filters/i });
|
||||
await user.click(filterButton);
|
||||
|
||||
expect(screen.getByLabelText('Assignee')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Active filters detection', () => {
|
||||
it('shows clear button when search is active', async () => {
|
||||
const user = userEvent.setup();
|
||||
const filtersWithSearch: IssueFiltersType = {
|
||||
...defaultFilters,
|
||||
search: 'test query',
|
||||
};
|
||||
|
||||
render(<IssueFilters filters={filtersWithSearch} onFiltersChange={mockOnFiltersChange} />);
|
||||
|
||||
// Open extended filters
|
||||
const filterButton = screen.getByRole('button', { name: /toggle extended filters/i });
|
||||
await user.click(filterButton);
|
||||
|
||||
expect(screen.getByRole('button', { name: /clear filters/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows clear button when priority is not all', async () => {
|
||||
const user = userEvent.setup();
|
||||
const filtersWithPriority: IssueFiltersType = {
|
||||
...defaultFilters,
|
||||
priority: 'critical',
|
||||
};
|
||||
|
||||
render(<IssueFilters filters={filtersWithPriority} onFiltersChange={mockOnFiltersChange} />);
|
||||
|
||||
// Open extended filters
|
||||
const filterButton = screen.getByRole('button', { name: /toggle extended filters/i });
|
||||
await user.click(filterButton);
|
||||
|
||||
expect(screen.getByRole('button', { name: /clear filters/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows clear button when sprint is not all', async () => {
|
||||
const user = userEvent.setup();
|
||||
const filtersWithSprint: IssueFiltersType = {
|
||||
...defaultFilters,
|
||||
sprint: 'Sprint 1',
|
||||
};
|
||||
|
||||
render(<IssueFilters filters={filtersWithSprint} onFiltersChange={mockOnFiltersChange} />);
|
||||
|
||||
// Open extended filters
|
||||
const filterButton = screen.getByRole('button', { name: /toggle extended filters/i });
|
||||
await user.click(filterButton);
|
||||
|
||||
expect(screen.getByRole('button', { name: /clear filters/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows clear button when assignee is not all', async () => {
|
||||
const user = userEvent.setup();
|
||||
const filtersWithAssignee: IssueFiltersType = {
|
||||
...defaultFilters,
|
||||
assignee: 'user-123',
|
||||
};
|
||||
|
||||
render(<IssueFilters filters={filtersWithAssignee} onFiltersChange={mockOnFiltersChange} />);
|
||||
|
||||
// Open extended filters
|
||||
const filterButton = screen.getByRole('button', { name: /toggle extended filters/i });
|
||||
await user.click(filterButton);
|
||||
|
||||
expect(screen.getByRole('button', { name: /clear filters/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show clear button when all filters are default', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<IssueFilters filters={defaultFilters} onFiltersChange={mockOnFiltersChange} />);
|
||||
|
||||
// Open extended filters
|
||||
const filterButton = screen.getByRole('button', { name: /toggle extended filters/i });
|
||||
await user.click(filterButton);
|
||||
|
||||
expect(screen.queryByRole('button', { name: /clear filters/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search empty handling', () => {
|
||||
it('clears search when input is emptied', async () => {
|
||||
const user = userEvent.setup();
|
||||
const filtersWithSearch: IssueFiltersType = {
|
||||
...defaultFilters,
|
||||
search: 'test',
|
||||
};
|
||||
|
||||
render(<IssueFilters filters={filtersWithSearch} onFiltersChange={mockOnFiltersChange} />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('Search issues...');
|
||||
await user.clear(searchInput);
|
||||
|
||||
// Should call with undefined search
|
||||
const lastCall = mockOnFiltersChange.mock.calls[mockOnFiltersChange.mock.calls.length - 1][0];
|
||||
expect(lastCall.search).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,4 +265,280 @@ describe('IssueTable', () => {
|
||||
|
||||
expect(screen.getByText('Backlog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('deselects issue when clicking selected checkbox', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<IssueTable
|
||||
issues={mockIssues}
|
||||
selectedIssues={['issue-1']}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={defaultSort}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find checkbox for first issue (already selected)
|
||||
const checkbox = screen.getByRole('checkbox', { name: /select issue 42/i });
|
||||
await user.click(checkbox);
|
||||
|
||||
expect(mockOnSelectionChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('handles keyboard navigation for number column sorting', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<IssueTable
|
||||
issues={mockIssues}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={defaultSort}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find the # column header and press Enter
|
||||
const numberHeader = screen.getByRole('button', { name: /#/i });
|
||||
numberHeader.focus();
|
||||
await user.keyboard('{Enter}');
|
||||
|
||||
expect(mockOnSortChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles keyboard navigation for priority column sorting', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<IssueTable
|
||||
issues={mockIssues}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={defaultSort}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find the Priority column header and press Enter
|
||||
const priorityHeader = screen.getByRole('button', { name: /priority/i });
|
||||
priorityHeader.focus();
|
||||
await user.keyboard('{Enter}');
|
||||
|
||||
expect(mockOnSortChange).toHaveBeenCalledWith({ field: 'priority', direction: 'desc' });
|
||||
});
|
||||
|
||||
it('toggles sort direction when clicking same column', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<IssueTable
|
||||
issues={mockIssues}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={{ field: 'number', direction: 'asc' }}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// Click number header (currently sorted asc)
|
||||
const numberHeader = screen.getByRole('button', { name: /#/i });
|
||||
await user.click(numberHeader);
|
||||
|
||||
// Should toggle to desc
|
||||
expect(mockOnSortChange).toHaveBeenCalledWith({ field: 'number', direction: 'desc' });
|
||||
});
|
||||
|
||||
it('shows agent icon for agent assignee', () => {
|
||||
const issueWithAgentAssignee: IssueSummary[] = [
|
||||
{
|
||||
id: 'issue-1',
|
||||
number: 42,
|
||||
type: 'bug',
|
||||
title: 'Test Issue',
|
||||
description: 'Description',
|
||||
status: 'open',
|
||||
priority: 'high',
|
||||
labels: [],
|
||||
sprint: null,
|
||||
assignee: { id: 'agent-1', name: 'Test Agent', type: 'agent' },
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-02T00:00:00Z',
|
||||
sync_status: 'synced',
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<IssueTable
|
||||
issues={issueWithAgentAssignee}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={defaultSort}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check that agent name is displayed
|
||||
expect(screen.getByText('Test Agent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('truncates labels when more than 3', () => {
|
||||
const issueWithManyLabels: IssueSummary[] = [
|
||||
{
|
||||
id: 'issue-1',
|
||||
number: 42,
|
||||
type: 'bug',
|
||||
title: 'Test Issue',
|
||||
description: 'Description',
|
||||
status: 'open',
|
||||
priority: 'high',
|
||||
labels: ['label1', 'label2', 'label3', 'label4', 'label5'],
|
||||
sprint: null,
|
||||
assignee: null,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-02T00:00:00Z',
|
||||
sync_status: 'synced',
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<IssueTable
|
||||
issues={issueWithManyLabels}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={defaultSort}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// First 3 labels should be shown
|
||||
expect(screen.getByText('label1')).toBeInTheDocument();
|
||||
expect(screen.getByText('label2')).toBeInTheDocument();
|
||||
expect(screen.getByText('label3')).toBeInTheDocument();
|
||||
|
||||
// +2 badge should be shown
|
||||
expect(screen.getByText('+2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays actions dropdown menu', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<IssueTable
|
||||
issues={mockIssues}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={defaultSort}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find and click the actions button
|
||||
const actionsButton = screen.getByRole('button', { name: /actions for issue 42/i });
|
||||
await user.click(actionsButton);
|
||||
|
||||
// Dropdown menu items should be visible
|
||||
expect(screen.getByRole('menuitem', { name: /view details/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem', { name: /edit/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem', { name: /assign/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem', { name: /sync with tracker/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem', { name: /delete/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not trigger row click when clicking actions menu', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<IssueTable
|
||||
issues={mockIssues}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={defaultSort}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find and click the actions button
|
||||
const actionsButton = screen.getByRole('button', { name: /actions for issue 42/i });
|
||||
await user.click(actionsButton);
|
||||
|
||||
// Row click should not be triggered
|
||||
expect(mockOnIssueClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onIssueClick when View Details is clicked from menu', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<IssueTable
|
||||
issues={mockIssues}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={defaultSort}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// Open the actions menu
|
||||
const actionsButton = screen.getByRole('button', { name: /actions for issue 42/i });
|
||||
await user.click(actionsButton);
|
||||
|
||||
// Click View Details
|
||||
const viewDetailsItem = screen.getByRole('menuitem', { name: /view details/i });
|
||||
await user.click(viewDetailsItem);
|
||||
|
||||
expect(mockOnIssueClick).toHaveBeenCalledWith('issue-1');
|
||||
});
|
||||
|
||||
it('shows descending sort icon when sorted descending', () => {
|
||||
render(
|
||||
<IssueTable
|
||||
issues={mockIssues}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={{ field: 'number', direction: 'desc' }}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// The column header should have aria-sort="descending"
|
||||
const numberHeader = screen.getByRole('button', { name: /#/i });
|
||||
expect(numberHeader).toHaveAttribute('aria-sort', 'descending');
|
||||
});
|
||||
|
||||
it('shows ascending sort icon when sorted ascending', () => {
|
||||
render(
|
||||
<IssueTable
|
||||
issues={mockIssues}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={{ field: 'number', direction: 'asc' }}
|
||||
onSortChange={mockOnSortChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// The column header should have aria-sort="ascending"
|
||||
const numberHeader = screen.getByRole('button', { name: /#/i });
|
||||
expect(numberHeader).toHaveAttribute('aria-sort', 'ascending');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<IssueTable
|
||||
issues={mockIssues}
|
||||
selectedIssues={[]}
|
||||
onSelectionChange={mockOnSelectionChange}
|
||||
onIssueClick={mockOnIssueClick}
|
||||
sort={defaultSort}
|
||||
onSortChange={mockOnSortChange}
|
||||
className="custom-class"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,4 +23,18 @@ describe('PriorityBadge', () => {
|
||||
const badge = screen.getByText('High');
|
||||
expect(badge).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('renders critical priority', () => {
|
||||
render(<PriorityBadge priority="critical" />);
|
||||
|
||||
expect(screen.getByText('Critical')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to medium config for unknown priority', () => {
|
||||
// @ts-expect-error - Testing unknown priority value
|
||||
render(<PriorityBadge priority="unknown-priority" />);
|
||||
|
||||
// Should fall back to medium config
|
||||
expect(screen.getByText('Medium')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,4 +45,27 @@ describe('StatusBadge', () => {
|
||||
// Should have sr-only text for screen readers
|
||||
expect(screen.getByText('Open')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to open config for unknown status', () => {
|
||||
// @ts-expect-error - Testing unknown status value
|
||||
render(<StatusBadge status="unknown-status" />);
|
||||
|
||||
// Should fall back to open config (includes CircleDot icon as default)
|
||||
const elements = screen.getAllByText('Open');
|
||||
expect(elements.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('renders done status correctly', () => {
|
||||
// Test the 'done' status which has a valid icon mapping
|
||||
// but STATUS_CONFIG doesn't have 'done', so it falls back to 'open'
|
||||
// @ts-expect-error - Testing done status (valid icon, fallback config)
|
||||
const { container } = render(<StatusBadge status="done" />);
|
||||
|
||||
// Should have an SVG icon rendered (CheckCircle2 for done)
|
||||
const svg = container.querySelector('svg');
|
||||
expect(svg).toBeInTheDocument();
|
||||
|
||||
// Config falls back to open, so label shows "Open"
|
||||
expect(screen.getAllByText('Open').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,4 +42,25 @@ describe('SyncStatusIndicator', () => {
|
||||
const icon = container.querySelector('svg');
|
||||
expect(icon).toHaveClass('animate-spin');
|
||||
});
|
||||
|
||||
it('falls back to synced config for unknown status', () => {
|
||||
// @ts-expect-error - Testing unknown status value
|
||||
render(<SyncStatusIndicator status="unknown-status" showLabel />);
|
||||
|
||||
// Should fall back to synced config
|
||||
expect(screen.getByText('Synced')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows label for conflict status', () => {
|
||||
render(<SyncStatusIndicator status="conflict" showLabel />);
|
||||
|
||||
expect(screen.getByText('Conflict')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows label for error status', () => {
|
||||
render(<SyncStatusIndicator status="error" showLabel />);
|
||||
|
||||
// The error label is 'Sync Error' in SYNC_STATUS_CONFIG
|
||||
expect(screen.getByText('Sync Error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
193
frontend/tests/features/issues/constants.test.ts
Normal file
193
frontend/tests/features/issues/constants.test.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Issue Constants Tests
|
||||
*
|
||||
* Tests for issue-related constants and helper functions.
|
||||
*/
|
||||
|
||||
import {
|
||||
STATUS_CONFIG,
|
||||
PRIORITY_CONFIG,
|
||||
STATUS_TRANSITIONS,
|
||||
STATUS_ORDER,
|
||||
PRIORITY_ORDER,
|
||||
SYNC_STATUS_CONFIG,
|
||||
DEFAULT_PAGE_SIZE,
|
||||
MAX_BULK_SELECTION,
|
||||
getAvailableTransitions,
|
||||
getPrimaryTransition,
|
||||
} from '@/features/issues/constants';
|
||||
import type { IssueStatus } from '@/features/issues/types';
|
||||
|
||||
describe('Issue Constants', () => {
|
||||
describe('STATUS_CONFIG', () => {
|
||||
it('has configuration for all statuses', () => {
|
||||
expect(STATUS_CONFIG.open).toBeDefined();
|
||||
expect(STATUS_CONFIG.in_progress).toBeDefined();
|
||||
expect(STATUS_CONFIG.in_review).toBeDefined();
|
||||
expect(STATUS_CONFIG.blocked).toBeDefined();
|
||||
expect(STATUS_CONFIG.closed).toBeDefined();
|
||||
});
|
||||
|
||||
it('each status has label and color', () => {
|
||||
Object.values(STATUS_CONFIG).forEach((config) => {
|
||||
expect(config.label).toBeDefined();
|
||||
expect(config.color).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PRIORITY_CONFIG', () => {
|
||||
it('has configuration for all priorities', () => {
|
||||
expect(PRIORITY_CONFIG.critical).toBeDefined();
|
||||
expect(PRIORITY_CONFIG.high).toBeDefined();
|
||||
expect(PRIORITY_CONFIG.medium).toBeDefined();
|
||||
expect(PRIORITY_CONFIG.low).toBeDefined();
|
||||
});
|
||||
|
||||
it('each priority has label and color', () => {
|
||||
Object.values(PRIORITY_CONFIG).forEach((config) => {
|
||||
expect(config.label).toBeDefined();
|
||||
expect(config.color).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('STATUS_TRANSITIONS', () => {
|
||||
it('contains expected transitions', () => {
|
||||
expect(STATUS_TRANSITIONS.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('each transition has from, to, and label', () => {
|
||||
STATUS_TRANSITIONS.forEach((transition) => {
|
||||
expect(transition.from).toBeDefined();
|
||||
expect(transition.to).toBeDefined();
|
||||
expect(transition.label).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAvailableTransitions', () => {
|
||||
it('returns transitions for open status', () => {
|
||||
const transitions = getAvailableTransitions('open');
|
||||
expect(transitions.length).toBeGreaterThan(0);
|
||||
expect(transitions.every((t) => t.from === 'open')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns transitions for in_progress status', () => {
|
||||
const transitions = getAvailableTransitions('in_progress');
|
||||
expect(transitions.length).toBeGreaterThan(0);
|
||||
expect(transitions.every((t) => t.from === 'in_progress')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns transitions for in_review status', () => {
|
||||
const transitions = getAvailableTransitions('in_review');
|
||||
expect(transitions.length).toBeGreaterThan(0);
|
||||
expect(transitions.every((t) => t.from === 'in_review')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns transitions for blocked status', () => {
|
||||
const transitions = getAvailableTransitions('blocked');
|
||||
expect(transitions.length).toBeGreaterThan(0);
|
||||
expect(transitions.every((t) => t.from === 'blocked')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns transitions for closed status', () => {
|
||||
const transitions = getAvailableTransitions('closed');
|
||||
expect(transitions.length).toBeGreaterThan(0);
|
||||
expect(transitions.every((t) => t.from === 'closed')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns empty array for unknown status', () => {
|
||||
const transitions = getAvailableTransitions('unknown' as IssueStatus);
|
||||
expect(transitions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPrimaryTransition', () => {
|
||||
it('returns first transition for open status', () => {
|
||||
const transition = getPrimaryTransition('open');
|
||||
expect(transition).toBeDefined();
|
||||
expect(transition?.from).toBe('open');
|
||||
});
|
||||
|
||||
it('returns first transition for in_progress status', () => {
|
||||
const transition = getPrimaryTransition('in_progress');
|
||||
expect(transition).toBeDefined();
|
||||
expect(transition?.from).toBe('in_progress');
|
||||
});
|
||||
|
||||
it('returns first transition for in_review status', () => {
|
||||
const transition = getPrimaryTransition('in_review');
|
||||
expect(transition).toBeDefined();
|
||||
expect(transition?.from).toBe('in_review');
|
||||
});
|
||||
|
||||
it('returns first transition for blocked status', () => {
|
||||
const transition = getPrimaryTransition('blocked');
|
||||
expect(transition).toBeDefined();
|
||||
expect(transition?.from).toBe('blocked');
|
||||
});
|
||||
|
||||
it('returns first transition for closed status', () => {
|
||||
const transition = getPrimaryTransition('closed');
|
||||
expect(transition).toBeDefined();
|
||||
expect(transition?.from).toBe('closed');
|
||||
});
|
||||
|
||||
it('returns undefined for unknown status', () => {
|
||||
const transition = getPrimaryTransition('unknown' as IssueStatus);
|
||||
expect(transition).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('STATUS_ORDER', () => {
|
||||
it('contains all statuses in order', () => {
|
||||
expect(STATUS_ORDER).toContain('open');
|
||||
expect(STATUS_ORDER).toContain('in_progress');
|
||||
expect(STATUS_ORDER).toContain('in_review');
|
||||
expect(STATUS_ORDER).toContain('blocked');
|
||||
expect(STATUS_ORDER).toContain('closed');
|
||||
});
|
||||
|
||||
it('has correct length', () => {
|
||||
expect(STATUS_ORDER.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PRIORITY_ORDER', () => {
|
||||
it('contains all priorities in order', () => {
|
||||
expect(PRIORITY_ORDER).toContain('critical');
|
||||
expect(PRIORITY_ORDER).toContain('high');
|
||||
expect(PRIORITY_ORDER).toContain('medium');
|
||||
expect(PRIORITY_ORDER).toContain('low');
|
||||
});
|
||||
|
||||
it('has correct length', () => {
|
||||
expect(PRIORITY_ORDER.length).toBe(4);
|
||||
});
|
||||
|
||||
it('has correct order (critical first, low last)', () => {
|
||||
expect(PRIORITY_ORDER[0]).toBe('critical');
|
||||
expect(PRIORITY_ORDER[PRIORITY_ORDER.length - 1]).toBe('low');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SYNC_STATUS_CONFIG', () => {
|
||||
it('has configuration for all sync statuses', () => {
|
||||
expect(SYNC_STATUS_CONFIG.synced).toBeDefined();
|
||||
expect(SYNC_STATUS_CONFIG.pending).toBeDefined();
|
||||
expect(SYNC_STATUS_CONFIG.conflict).toBeDefined();
|
||||
expect(SYNC_STATUS_CONFIG.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination and Bulk Constants', () => {
|
||||
it('DEFAULT_PAGE_SIZE is a positive number', () => {
|
||||
expect(DEFAULT_PAGE_SIZE).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('MAX_BULK_SELECTION is a positive number', () => {
|
||||
expect(MAX_BULK_SELECTION).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
1174
frontend/tests/features/issues/hooks/useIssues.test.tsx
Normal file
1174
frontend/tests/features/issues/hooks/useIssues.test.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user