feat(agents): implement grid/list view toggle and enhance filters
- Added grid and list view modes to AgentTypeList with user preference management. - Enhanced filtering with category selection alongside existing search and status filters. - Updated AgentTypeDetail with category badges and improved layout. - Added unit tests for grid/list views and category filtering in AgentTypeList. - Introduced `@radix-ui/react-toggle-group` for view mode toggle in AgentTypeList.
This commit is contained in:
@@ -65,9 +65,8 @@ describe('AgentTypeDetail', () => {
|
||||
expect(screen.getByText('Inactive')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders description card', () => {
|
||||
it('renders description in hero header', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText('Description')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Designs system architecture and makes technology decisions')
|
||||
).toBeInTheDocument();
|
||||
@@ -137,7 +136,7 @@ describe('AgentTypeDetail', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /go back/i }));
|
||||
await user.click(screen.getByRole('button', { name: /back to agent types/i }));
|
||||
expect(defaultProps.onBack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -218,4 +217,146 @@ describe('AgentTypeDetail', () => {
|
||||
);
|
||||
expect(screen.getByText('None configured')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Hero Header', () => {
|
||||
it('renders hero header with agent name', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(
|
||||
screen.getByRole('heading', { level: 1, name: 'Software Architect' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders dynamic icon in hero header', () => {
|
||||
const { container } = render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(container.querySelector('svg.lucide-git-branch')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies agent color to hero header gradient', () => {
|
||||
const { container } = render(<AgentTypeDetail {...defaultProps} />);
|
||||
const heroHeader = container.querySelector('[style*="linear-gradient"]');
|
||||
expect(heroHeader).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders category badge in hero header', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText('Development')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows last updated date in hero header', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText(/Last updated:/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Jan 18, 2025/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Typical Tasks Card', () => {
|
||||
it('renders "What This Agent Does Best" card', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText('What This Agent Does Best')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays all typical tasks', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText('Design system architecture')).toBeInTheDocument();
|
||||
expect(screen.getByText('Create ADRs')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render typical tasks card when empty', () => {
|
||||
render(
|
||||
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, typical_tasks: [] }} />
|
||||
);
|
||||
expect(screen.queryByText('What This Agent Does Best')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Collaboration Hints Card', () => {
|
||||
it('renders "Works Well With" card', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText('Works Well With')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays collaboration hints as badges', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText('backend-engineer')).toBeInTheDocument();
|
||||
expect(screen.getByText('frontend-engineer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render collaboration hints card when empty', () => {
|
||||
render(
|
||||
<AgentTypeDetail
|
||||
{...defaultProps}
|
||||
agentType={{ ...mockAgentType, collaboration_hints: [] }}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByText('Works Well With')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Category Badge', () => {
|
||||
it('renders category badge with correct label', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText('Development')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render category badge when category is null', () => {
|
||||
render(
|
||||
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, category: null }} />
|
||||
);
|
||||
// Should not have a Development badge in the hero header area
|
||||
// The word "Development" should not appear
|
||||
expect(screen.queryByText('Development')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Details Card', () => {
|
||||
it('renders details card with slug', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText('Slug')).toBeInTheDocument();
|
||||
expect(screen.getByText('software-architect')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders details card with sort order', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText('Sort Order')).toBeInTheDocument();
|
||||
expect(screen.getByText('40')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders details card with creation date', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
expect(screen.getByText('Created')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Jan 10, 2025/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dynamic Icon', () => {
|
||||
it('renders fallback icon when icon is null', () => {
|
||||
const { container } = render(
|
||||
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, icon: null }} />
|
||||
);
|
||||
// Should fall back to 'bot' icon
|
||||
expect(container.querySelector('svg.lucide-bot')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correct icon based on agent type', () => {
|
||||
const agentWithBrainIcon = { ...mockAgentType, icon: 'brain' };
|
||||
const { container } = render(
|
||||
<AgentTypeDetail {...defaultProps} agentType={agentWithBrainIcon} />
|
||||
);
|
||||
expect(container.querySelector('svg.lucide-brain')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Color Styling', () => {
|
||||
it('applies custom color to instance count', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} />);
|
||||
const instanceCount = screen.getByText('2');
|
||||
expect(instanceCount).toHaveStyle({ color: 'rgb(59, 130, 246)' });
|
||||
});
|
||||
|
||||
it('uses default color when color is null', () => {
|
||||
render(<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, color: null }} />);
|
||||
// Should still render without errors
|
||||
expect(screen.getByText('Software Architect')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,10 @@ describe('AgentTypeList', () => {
|
||||
onSearchChange: jest.fn(),
|
||||
statusFilter: 'all',
|
||||
onStatusFilterChange: jest.fn(),
|
||||
categoryFilter: 'all',
|
||||
onCategoryFilterChange: jest.fn(),
|
||||
viewMode: 'grid' as const,
|
||||
onViewModeChange: jest.fn(),
|
||||
onSelect: jest.fn(),
|
||||
onCreate: jest.fn(),
|
||||
};
|
||||
@@ -208,4 +212,158 @@ describe('AgentTypeList', () => {
|
||||
const { container } = render(<AgentTypeList {...defaultProps} className="custom-class" />);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
describe('Category Filter', () => {
|
||||
it('renders category filter dropdown', () => {
|
||||
render(<AgentTypeList {...defaultProps} />);
|
||||
expect(screen.getByRole('combobox', { name: /filter by category/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "All Categories" as default option', () => {
|
||||
render(<AgentTypeList {...defaultProps} categoryFilter="all" />);
|
||||
expect(screen.getByText('All Categories')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays category badge on agent cards', () => {
|
||||
render(<AgentTypeList {...defaultProps} />);
|
||||
// Both agents have 'development' category
|
||||
const developmentBadges = screen.getAllByText('Development');
|
||||
expect(developmentBadges.length).toBe(2);
|
||||
});
|
||||
|
||||
it('shows filter hint in empty state when category filter is applied', () => {
|
||||
render(<AgentTypeList {...defaultProps} agentTypes={[]} categoryFilter="design" />);
|
||||
expect(screen.getByText('Try adjusting your search or filters')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('View Mode Toggle', () => {
|
||||
it('renders view mode toggle buttons', () => {
|
||||
render(<AgentTypeList {...defaultProps} />);
|
||||
expect(screen.getByRole('radio', { name: /grid view/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('radio', { name: /list view/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders grid view by default', () => {
|
||||
const { container } = render(<AgentTypeList {...defaultProps} viewMode="grid" />);
|
||||
// Grid view uses CSS grid
|
||||
expect(container.querySelector('.grid')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders list view when viewMode is list', () => {
|
||||
const { container } = render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||
// List view uses space-y-3 for vertical stacking
|
||||
expect(container.querySelector('.space-y-3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onViewModeChange when grid toggle is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onViewModeChange = jest.fn();
|
||||
render(
|
||||
<AgentTypeList {...defaultProps} viewMode="list" onViewModeChange={onViewModeChange} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('radio', { name: /grid view/i }));
|
||||
expect(onViewModeChange).toHaveBeenCalledWith('grid');
|
||||
});
|
||||
|
||||
it('calls onViewModeChange when list toggle is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onViewModeChange = jest.fn();
|
||||
render(
|
||||
<AgentTypeList {...defaultProps} viewMode="grid" onViewModeChange={onViewModeChange} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('radio', { name: /list view/i }));
|
||||
expect(onViewModeChange).toHaveBeenCalledWith('list');
|
||||
});
|
||||
|
||||
it('shows list-specific loading skeletons when viewMode is list', () => {
|
||||
const { container } = render(
|
||||
<AgentTypeList {...defaultProps} agentTypes={[]} isLoading={true} viewMode="list" />
|
||||
);
|
||||
expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('List View', () => {
|
||||
it('shows agent info in list rows', () => {
|
||||
render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||
expect(screen.getByText('Product Owner')).toBeInTheDocument();
|
||||
expect(screen.getByText('Software Architect')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows category badge in list view', () => {
|
||||
render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||
const developmentBadges = screen.getAllByText('Development');
|
||||
expect(developmentBadges.length).toBe(2);
|
||||
});
|
||||
|
||||
it('shows expertise count in list view', () => {
|
||||
render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||
// Both agents have 3 expertise areas
|
||||
const expertiseTexts = screen.getAllByText('3 expertise areas');
|
||||
expect(expertiseTexts.length).toBe(2);
|
||||
});
|
||||
|
||||
it('calls onSelect when list row is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelect = jest.fn();
|
||||
render(<AgentTypeList {...defaultProps} viewMode="list" onSelect={onSelect} />);
|
||||
|
||||
await user.click(screen.getByText('Product Owner'));
|
||||
expect(onSelect).toHaveBeenCalledWith('type-001');
|
||||
});
|
||||
|
||||
it('supports keyboard navigation on list rows', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelect = jest.fn();
|
||||
render(<AgentTypeList {...defaultProps} viewMode="list" onSelect={onSelect} />);
|
||||
|
||||
const rows = screen.getAllByRole('button', { name: /view .* agent type/i });
|
||||
rows[0].focus();
|
||||
await user.keyboard('{Enter}');
|
||||
expect(onSelect).toHaveBeenCalledWith('type-001');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dynamic Icons', () => {
|
||||
it('renders agent icon in grid view', () => {
|
||||
const { container } = render(<AgentTypeList {...defaultProps} viewMode="grid" />);
|
||||
// Check for svg icons with lucide classes
|
||||
const icons = container.querySelectorAll('svg.lucide-clipboard-check, svg.lucide-git-branch');
|
||||
expect(icons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders agent icon in list view', () => {
|
||||
const { container } = render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||
const icons = container.querySelectorAll('svg.lucide-clipboard-check, svg.lucide-git-branch');
|
||||
expect(icons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Color Accent', () => {
|
||||
it('applies color to card border in grid view', () => {
|
||||
const { container } = render(<AgentTypeList {...defaultProps} viewMode="grid" />);
|
||||
const card = container.querySelector('[style*="border-top-color"]');
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies color to row border in list view', () => {
|
||||
const { container } = render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||
const row = container.querySelector('[style*="border-left-color"]');
|
||||
expect(row).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Category Badge Component', () => {
|
||||
it('does not render category badge when category is null', () => {
|
||||
const agentWithNoCategory: AgentTypeResponse = {
|
||||
...mockAgentTypes[0],
|
||||
category: null,
|
||||
};
|
||||
render(<AgentTypeList {...defaultProps} agentTypes={[agentWithNoCategory]} />);
|
||||
expect(screen.queryByText('Development')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user