+ const rightContainerClasses = [
+ 'flex',
+ 'items-center',
+ 'gap-2', // Updated from no gap
+ 'px-2',
+ 'py-1'
+ ];
+
+ rightContainerClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+
+ expect(rightContainerClasses).toContain('gap-2');
+ });
+ });
+
+ describe('Tab Control Props Interface', () => {
+ it('should have correct interface for control props', () => {
+ // Verify the control props interface matches component expectations
+ interface ControlProps {
+ onSettingsClick?: () => void;
+ showArchived?: boolean;
+ archivedCount?: number;
+ onToggleArchived?: () => void;
+ }
+
+ const validControlProps: ControlProps = {
+ onSettingsClick: () => {},
+ showArchived: false,
+ archivedCount: 0,
+ onToggleArchived: () => {}
+ };
+
+ expect(validControlProps.onSettingsClick).toBeDefined();
+ expect(validControlProps.showArchived).toBe(false);
+ expect(validControlProps.archivedCount).toBe(0);
+ expect(validControlProps.onToggleArchived).toBeDefined();
+ });
+
+ it('should allow optional control props', () => {
+ interface ControlProps {
+ onSettingsClick?: () => void;
+ showArchived?: boolean;
+ archivedCount?: number;
+ onToggleArchived?: () => void;
+ }
+
+ const emptyControlProps: ControlProps = {};
+
+ expect(emptyControlProps.onSettingsClick).toBeUndefined();
+ expect(emptyControlProps.showArchived).toBeUndefined();
+ expect(emptyControlProps.archivedCount).toBeUndefined();
+ expect(emptyControlProps.onToggleArchived).toBeUndefined();
+ });
+
+ it('should handle partial control props', () => {
+ interface ControlProps {
+ onSettingsClick?: () => void;
+ showArchived?: boolean;
+ archivedCount?: number;
+ onToggleArchived?: () => void;
+ }
+
+ // Only settings provided
+ const settingsOnlyProps: ControlProps = {
+ onSettingsClick: () => {}
+ };
+ expect(settingsOnlyProps.onSettingsClick).toBeDefined();
+ expect(settingsOnlyProps.onToggleArchived).toBeUndefined();
+
+ // Only archive toggle provided
+ const archiveOnlyProps: ControlProps = {
+ onToggleArchived: () => {},
+ showArchived: true,
+ archivedCount: 5
+ };
+ expect(archiveOnlyProps.onToggleArchived).toBeDefined();
+ expect(archiveOnlyProps.showArchived).toBe(true);
+ expect(archiveOnlyProps.archivedCount).toBe(5);
+ expect(archiveOnlyProps.onSettingsClick).toBeUndefined();
+ });
+ });
+
+ describe('Integration with SortableProjectTab Control Props', () => {
+ it('should pass control props to SortableProjectTab for active tab', () => {
+ const projects = [
+ createTestProject({ id: 'proj-1', name: 'Test Project' })
+ ];
+ const activeProjectId = 'proj-1';
+
+ // Props that should be passed to SortableProjectTab including controls
+ const tabProps = {
+ project: projects[0],
+ isActive: activeProjectId === projects[0].id,
+ canClose: projects.length > 1,
+ tabIndex: 0,
+ onSelect: expect.any(Function),
+ onClose: expect.any(Function),
+ // Control props for active tab
+ onSettingsClick: mockOnSettingsClick,
+ showArchived: false,
+ archivedCount: 3,
+ onToggleArchived: mockOnToggleArchived
+ };
+
+ expect(tabProps.project.id).toBe('proj-1');
+ expect(tabProps.isActive).toBe(true);
+ expect(tabProps.onSettingsClick).toBe(mockOnSettingsClick);
+ expect(tabProps.showArchived).toBe(false);
+ expect(tabProps.archivedCount).toBe(3);
+ expect(tabProps.onToggleArchived).toBe(mockOnToggleArchived);
+ });
+
+ it('should not pass control props to SortableProjectTab for inactive tab', () => {
+ const projects = [
+ createTestProject({ id: 'proj-1', name: 'Project 1' }),
+ createTestProject({ id: 'proj-2', name: 'Project 2' })
+ ];
+ const activeProjectId = 'proj-2';
+
+ // Props for inactive tab (proj-1)
+ const inactiveTabProps = {
+ project: projects[0],
+ isActive: activeProjectId === projects[0].id, // false
+ canClose: projects.length > 1,
+ tabIndex: 0,
+ onSelect: expect.any(Function),
+ onClose: expect.any(Function),
+ // Control props should be undefined for inactive tab
+ onSettingsClick: undefined,
+ showArchived: undefined,
+ archivedCount: undefined,
+ onToggleArchived: undefined
+ };
+
+ expect(inactiveTabProps.isActive).toBe(false);
+ expect(inactiveTabProps.onSettingsClick).toBeUndefined();
+ expect(inactiveTabProps.showArchived).toBeUndefined();
+ expect(inactiveTabProps.archivedCount).toBeUndefined();
+ expect(inactiveTabProps.onToggleArchived).toBeUndefined();
+ });
+ });
});
diff --git a/apps/frontend/src/renderer/components/__tests__/SortableProjectTab.test.tsx b/apps/frontend/src/renderer/components/__tests__/SortableProjectTab.test.tsx
new file mode 100644
index 00000000..19bd93cf
--- /dev/null
+++ b/apps/frontend/src/renderer/components/__tests__/SortableProjectTab.test.tsx
@@ -0,0 +1,948 @@
+/**
+ * Unit tests for SortableProjectTab component
+ * Tests conditional rendering of controls (settings, archive toggle),
+ * active/inactive states, and prop handling
+ *
+ * @vitest-environment jsdom
+ */
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import type { Project } from '../../../shared/types';
+
+// Helper to create test projects
+function createTestProject(overrides: Partial
= {}): Project {
+ return {
+ id: `project-${Date.now()}-${Math.random().toString(36).substring(7)}`,
+ name: 'Test Project',
+ path: '/path/to/test-project',
+ autoBuildPath: '/path/to/test-project/.auto-claude',
+ settings: {
+ model: 'claude-3-haiku-20240307',
+ memoryBackend: 'file',
+ linearSync: false,
+ notifications: {
+ onTaskComplete: true,
+ onTaskFailed: true,
+ onReviewNeeded: true,
+ sound: false
+ },
+ graphitiMcpEnabled: false
+ },
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ ...overrides
+ };
+}
+
+describe('SortableProjectTab', () => {
+ // Mock callbacks
+ const mockOnSelect = vi.fn();
+ const mockOnClose = vi.fn();
+ const mockOnSettingsClick = vi.fn();
+ const mockOnToggleArchived = vi.fn();
+
+ beforeEach(() => {
+ // Reset all mocks
+ vi.clearAllMocks();
+ });
+
+ describe('Conditional Control Rendering - Active State', () => {
+ it('should render controls container only when isActive is true', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ // When tab is active, controls should render
+ const activeTabProps = {
+ project,
+ isActive: true,
+ canClose: true,
+ tabIndex: 0,
+ onSelect: mockOnSelect,
+ onClose: mockOnClose,
+ onSettingsClick: mockOnSettingsClick,
+ onToggleArchived: mockOnToggleArchived
+ };
+
+ // Controls render when isActive is true
+ expect(activeTabProps.isActive).toBe(true);
+ expect(activeTabProps.onSettingsClick).toBeDefined();
+ expect(activeTabProps.onToggleArchived).toBeDefined();
+ });
+
+ it('should not render controls container when isActive is false', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ // When tab is inactive, controls should NOT be passed
+ const inactiveTabProps = {
+ project,
+ isActive: false,
+ canClose: true,
+ tabIndex: 0,
+ onSelect: mockOnSelect,
+ onClose: mockOnClose,
+ // Control props not passed for inactive tab
+ onSettingsClick: undefined,
+ onToggleArchived: undefined
+ };
+
+ expect(inactiveTabProps.isActive).toBe(false);
+ // Controls should not be available
+ expect(inactiveTabProps.onSettingsClick).toBeUndefined();
+ expect(inactiveTabProps.onToggleArchived).toBeUndefined();
+ });
+ });
+
+ describe('Settings Icon Conditional Rendering', () => {
+ it('should render settings icon when isActive is true AND onSettingsClick is provided', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: true,
+ onSettingsClick: mockOnSettingsClick
+ };
+
+ // Settings icon should render when both conditions are met
+ const shouldRenderSettings = props.isActive && props.onSettingsClick !== undefined;
+ expect(shouldRenderSettings).toBe(true);
+ });
+
+ it('should NOT render settings icon when isActive is false', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: false,
+ onSettingsClick: mockOnSettingsClick
+ };
+
+ // Component logic: controls render only when isActive
+ // Settings icon won't render because controls container is not rendered
+ const shouldRenderSettings = props.isActive && props.onSettingsClick !== undefined;
+ expect(shouldRenderSettings).toBe(false);
+ });
+
+ it('should NOT render settings icon when onSettingsClick is undefined', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: true,
+ onSettingsClick: undefined
+ };
+
+ // Settings icon requires onSettingsClick callback
+ const shouldRenderSettings = props.isActive && props.onSettingsClick !== undefined;
+ expect(shouldRenderSettings).toBe(false);
+ });
+
+ it('should call onSettingsClick with stopPropagation when clicked', () => {
+ const mockEvent = {
+ stopPropagation: vi.fn()
+ } as unknown as React.MouseEvent;
+
+ // Simulate the component's click handler
+ const onSettingsButtonClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ mockOnSettingsClick();
+ };
+
+ onSettingsButtonClick(mockEvent);
+
+ expect(mockEvent.stopPropagation).toHaveBeenCalled();
+ expect(mockOnSettingsClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('should have correct aria-label for settings button', () => {
+ // From component: aria-label="Project settings"
+ const expectedAriaLabel = 'Project settings';
+ expect(expectedAriaLabel).toBe('Project settings');
+ });
+ });
+
+ describe('Archive Toggle Conditional Rendering', () => {
+ it('should render archive toggle when isActive is true AND onToggleArchived is provided', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: true,
+ onToggleArchived: mockOnToggleArchived,
+ showArchived: false,
+ archivedCount: 5
+ };
+
+ // Archive toggle should render when both conditions are met
+ const shouldRenderArchive = props.isActive && props.onToggleArchived !== undefined;
+ expect(shouldRenderArchive).toBe(true);
+ });
+
+ it('should NOT render archive toggle when isActive is false', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: false,
+ onToggleArchived: mockOnToggleArchived,
+ showArchived: false,
+ archivedCount: 5
+ };
+
+ // Archive toggle won't render because controls container is not rendered
+ const shouldRenderArchive = props.isActive && props.onToggleArchived !== undefined;
+ expect(shouldRenderArchive).toBe(false);
+ });
+
+ it('should NOT render archive toggle when onToggleArchived is undefined', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: true,
+ onToggleArchived: undefined
+ };
+
+ // Archive toggle requires onToggleArchived callback
+ const shouldRenderArchive = props.isActive && props.onToggleArchived !== undefined;
+ expect(shouldRenderArchive).toBe(false);
+ });
+
+ it('should call onToggleArchived with stopPropagation when clicked', () => {
+ const mockEvent = {
+ stopPropagation: vi.fn()
+ } as unknown as React.MouseEvent;
+
+ // Simulate the component's click handler
+ const onArchiveButtonClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ mockOnToggleArchived();
+ };
+
+ onArchiveButtonClick(mockEvent);
+
+ expect(mockEvent.stopPropagation).toHaveBeenCalled();
+ expect(mockOnToggleArchived).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('Archive Count Badge Rendering', () => {
+ it('should render archived count badge when archivedCount is a number greater than 0', () => {
+ const props = {
+ archivedCount: 5
+ };
+
+ // Badge renders when archivedCount is number and > 0
+ const shouldRenderBadge = typeof props.archivedCount === 'number' && props.archivedCount > 0;
+ expect(shouldRenderBadge).toBe(true);
+ });
+
+ it('should NOT render archived count badge when archivedCount is 0', () => {
+ const props = {
+ archivedCount: 0
+ };
+
+ // Badge should not render for 0
+ const shouldRenderBadge = typeof props.archivedCount === 'number' && props.archivedCount > 0;
+ expect(shouldRenderBadge).toBe(false);
+ });
+
+ it('should NOT render archived count badge when archivedCount is undefined', () => {
+ const props = {
+ archivedCount: undefined
+ };
+
+ // Badge should not render for undefined
+ const shouldRenderBadge = typeof props.archivedCount === 'number' && props.archivedCount > 0;
+ expect(shouldRenderBadge).toBe(false);
+ });
+
+ it('should handle large archived counts', () => {
+ const props = {
+ archivedCount: 100
+ };
+
+ const shouldRenderBadge = typeof props.archivedCount === 'number' && props.archivedCount > 0;
+ expect(shouldRenderBadge).toBe(true);
+ expect(props.archivedCount).toBe(100);
+ });
+
+ it('should handle archivedCount of 1', () => {
+ const props = {
+ archivedCount: 1
+ };
+
+ const shouldRenderBadge = typeof props.archivedCount === 'number' && props.archivedCount > 0;
+ expect(shouldRenderBadge).toBe(true);
+ expect(props.archivedCount).toBe(1);
+ });
+ });
+
+ describe('Archive Toggle Styling based on showArchived State', () => {
+ it('should apply active styling when showArchived is true', () => {
+ const props = {
+ showArchived: true
+ };
+
+ // From component: when showArchived is true, apply 'text-primary bg-primary/10 hover:bg-primary/20'
+ const expectedActiveClasses = ['text-primary', 'bg-primary/10', 'hover:bg-primary/20'];
+
+ expect(props.showArchived).toBe(true);
+ expectedActiveClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should apply inactive styling when showArchived is false', () => {
+ const props = {
+ showArchived: false
+ };
+
+ // From component: when showArchived is false, apply 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
+ const expectedInactiveClasses = ['text-muted-foreground', 'hover:text-foreground', 'hover:bg-muted/50'];
+
+ expect(props.showArchived).toBe(false);
+ expectedInactiveClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have correct aria-label for show archived state', () => {
+ // From component: aria-label={showArchived ? 'Hide archived tasks' : 'Show archived tasks'}
+ const showArchivedLabel = 'Hide archived tasks';
+ const hideArchivedLabel = 'Show archived tasks';
+
+ expect(showArchivedLabel).toBe('Hide archived tasks');
+ expect(hideArchivedLabel).toBe('Show archived tasks');
+ });
+
+ it('should have correct aria-pressed attribute based on showArchived', () => {
+ // From component: aria-pressed={showArchived}
+ const showArchivedProps = { showArchived: true };
+ const hideArchivedProps = { showArchived: false };
+
+ expect(showArchivedProps.showArchived).toBe(true);
+ expect(hideArchivedProps.showArchived).toBe(false);
+ });
+ });
+
+ describe('Close Button Conditional Rendering', () => {
+ it('should render close button when canClose is true', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: true,
+ canClose: true,
+ onClose: mockOnClose
+ };
+
+ // Close button renders when canClose is true
+ expect(props.canClose).toBe(true);
+ });
+
+ it('should NOT render close button when canClose is false', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: true,
+ canClose: false,
+ onClose: mockOnClose
+ };
+
+ // Close button should not render when canClose is false
+ expect(props.canClose).toBe(false);
+ });
+
+ it('should call onClose when close button is clicked', () => {
+ const mockEvent = {
+ stopPropagation: vi.fn()
+ } as unknown as React.MouseEvent;
+
+ // Simulate clicking close button
+ mockOnClose(mockEvent);
+
+ expect(mockOnClose).toHaveBeenCalledWith(mockEvent);
+ expect(mockOnClose).toHaveBeenCalledTimes(1);
+ });
+
+ it('should show close button always on active tab', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: true,
+ canClose: true
+ };
+
+ // From component: close button has 'opacity-100' when isActive
+ // This means it's always visible on active tabs
+ expect(props.isActive).toBe(true);
+ });
+
+ it('should show close button on hover for inactive tab', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: false,
+ canClose: true
+ };
+
+ // From component: close button has 'opacity-0 group-hover:opacity-100' for inactive
+ expect(props.isActive).toBe(false);
+ expect(props.canClose).toBe(true);
+ });
+ });
+
+ describe('Combined Conditional Rendering Scenarios', () => {
+ it('should render settings and archive when both callbacks are provided for active tab', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: true,
+ canClose: true,
+ tabIndex: 0,
+ onSelect: mockOnSelect,
+ onClose: mockOnClose,
+ onSettingsClick: mockOnSettingsClick,
+ onToggleArchived: mockOnToggleArchived,
+ showArchived: false,
+ archivedCount: 3
+ };
+
+ // Both controls should render
+ const shouldRenderSettings = props.isActive && props.onSettingsClick !== undefined;
+ const shouldRenderArchive = props.isActive && props.onToggleArchived !== undefined;
+
+ expect(shouldRenderSettings).toBe(true);
+ expect(shouldRenderArchive).toBe(true);
+ });
+
+ it('should render only settings when onToggleArchived is not provided', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: true,
+ canClose: true,
+ tabIndex: 0,
+ onSelect: mockOnSelect,
+ onClose: mockOnClose,
+ onSettingsClick: mockOnSettingsClick,
+ onToggleArchived: undefined,
+ showArchived: undefined,
+ archivedCount: undefined
+ };
+
+ const shouldRenderSettings = props.isActive && props.onSettingsClick !== undefined;
+ const shouldRenderArchive = props.isActive && props.onToggleArchived !== undefined;
+
+ expect(shouldRenderSettings).toBe(true);
+ expect(shouldRenderArchive).toBe(false);
+ });
+
+ it('should render only archive when onSettingsClick is not provided', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: true,
+ canClose: true,
+ tabIndex: 0,
+ onSelect: mockOnSelect,
+ onClose: mockOnClose,
+ onSettingsClick: undefined,
+ onToggleArchived: mockOnToggleArchived,
+ showArchived: true,
+ archivedCount: 2
+ };
+
+ const shouldRenderSettings = props.isActive && props.onSettingsClick !== undefined;
+ const shouldRenderArchive = props.isActive && props.onToggleArchived !== undefined;
+
+ expect(shouldRenderSettings).toBe(false);
+ expect(shouldRenderArchive).toBe(true);
+ });
+
+ it('should not render any controls when tab is inactive even with callbacks provided', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ const props = {
+ project,
+ isActive: false,
+ canClose: true,
+ tabIndex: 0,
+ onSelect: mockOnSelect,
+ onClose: mockOnClose,
+ // Even with these provided, they shouldn't render
+ onSettingsClick: mockOnSettingsClick,
+ onToggleArchived: mockOnToggleArchived,
+ showArchived: false,
+ archivedCount: 5
+ };
+
+ // Component checks isActive first before rendering controls container
+ const shouldRenderControlsContainer = props.isActive;
+ expect(shouldRenderControlsContainer).toBe(false);
+
+ // Individual controls would not render even if callbacks are defined
+ const shouldRenderSettings = props.isActive && props.onSettingsClick !== undefined;
+ const shouldRenderArchive = props.isActive && props.onToggleArchived !== undefined;
+
+ expect(shouldRenderSettings).toBe(false);
+ expect(shouldRenderArchive).toBe(false);
+ });
+ });
+
+ describe('Props Interface', () => {
+ it('should have correct required props', () => {
+ const project = createTestProject({ id: 'proj-1' });
+
+ interface SortableProjectTabProps {
+ project: Project;
+ isActive: boolean;
+ canClose: boolean;
+ tabIndex: number;
+ onSelect: () => void;
+ onClose: (e: React.MouseEvent) => void;
+ // Optional control props
+ onSettingsClick?: () => void;
+ showArchived?: boolean;
+ archivedCount?: number;
+ onToggleArchived?: () => void;
+ }
+
+ const validProps: SortableProjectTabProps = {
+ project,
+ isActive: true,
+ canClose: true,
+ tabIndex: 0,
+ onSelect: mockOnSelect,
+ onClose: mockOnClose
+ };
+
+ expect(validProps.project).toBeDefined();
+ expect(validProps.isActive).toBeDefined();
+ expect(validProps.canClose).toBeDefined();
+ expect(validProps.tabIndex).toBeDefined();
+ expect(validProps.onSelect).toBeDefined();
+ expect(validProps.onClose).toBeDefined();
+ });
+
+ it('should have correct optional props', () => {
+ interface SortableProjectTabProps {
+ onSettingsClick?: () => void;
+ showArchived?: boolean;
+ archivedCount?: number;
+ onToggleArchived?: () => void;
+ }
+
+ // All optional props can be undefined
+ const minimalProps: SortableProjectTabProps = {};
+ expect(minimalProps.onSettingsClick).toBeUndefined();
+ expect(minimalProps.showArchived).toBeUndefined();
+ expect(minimalProps.archivedCount).toBeUndefined();
+ expect(minimalProps.onToggleArchived).toBeUndefined();
+
+ // All optional props can be provided
+ const fullProps: SortableProjectTabProps = {
+ onSettingsClick: mockOnSettingsClick,
+ showArchived: true,
+ archivedCount: 10,
+ onToggleArchived: mockOnToggleArchived
+ };
+ expect(fullProps.onSettingsClick).toBeDefined();
+ expect(fullProps.showArchived).toBe(true);
+ expect(fullProps.archivedCount).toBe(10);
+ expect(fullProps.onToggleArchived).toBeDefined();
+ });
+ });
+
+ describe('Tab Selection', () => {
+ it('should call onSelect when tab is clicked', () => {
+ mockOnSelect();
+
+ expect(mockOnSelect).toHaveBeenCalledTimes(1);
+ });
+
+ it('should handle tabIndex correctly for keyboard shortcuts', () => {
+ // From component: tabIndex < 9 shows keyboard shortcut hint
+ const tabIndexValues = [0, 1, 2, 8, 9, 10];
+
+ tabIndexValues.forEach(tabIndex => {
+ const showShortcut = tabIndex < 9;
+ if (tabIndex < 9) {
+ expect(showShortcut).toBe(true);
+ } else {
+ expect(showShortcut).toBe(false);
+ }
+ });
+ });
+ });
+
+ describe('Active Tab Styling', () => {
+ it('should apply active tab styles when isActive is true', () => {
+ const props = { isActive: true };
+
+ // From component: when isActive, responsive max-widths and specific styling
+ const expectedActiveClasses = [
+ 'max-w-[180px]', // mobile
+ 'sm:max-w-[220px]', // 640px+
+ 'md:max-w-[280px]', // 768px+
+ 'bg-background',
+ 'border-b-primary',
+ 'text-foreground',
+ 'hover:bg-background'
+ ];
+
+ expect(props.isActive).toBe(true);
+ expectedActiveClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should apply inactive tab styles when isActive is false', () => {
+ const props = { isActive: false };
+
+ // From component: when !isActive, responsive max-widths and different styling
+ const expectedInactiveClasses = [
+ 'max-w-[120px]', // mobile
+ 'sm:max-w-[160px]', // 640px+
+ 'md:max-w-[200px]', // 768px+
+ 'text-muted-foreground',
+ 'hover:text-foreground'
+ ];
+
+ expect(props.isActive).toBe(false);
+ expectedInactiveClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+ });
+
+ describe('Dragging State', () => {
+ it('should apply drag styling when isDragging', () => {
+ // From component: isDragging && 'opacity-60 scale-[0.98] shadow-lg'
+ // When isDragging is true, these classes should be applied
+ const expectedDragClasses = ['opacity-60', 'scale-[0.98]', 'shadow-lg'];
+
+ expectedDragClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should set higher zIndex when dragging', () => {
+ // From component: zIndex: isDragging ? 50 : undefined
+ const isDragging = true;
+ const notDragging = false;
+
+ const zIndexWhenDragging = isDragging ? 50 : undefined;
+ const zIndexWhenNotDragging = notDragging ? 50 : undefined;
+
+ expect(zIndexWhenDragging).toBe(50);
+ expect(zIndexWhenNotDragging).toBeUndefined();
+ });
+ });
+
+ describe('Responsive Behavior', () => {
+ it('should have responsive max-width classes for active tab', () => {
+ // From component: 'max-w-[180px] sm:max-w-[220px] md:max-w-[280px]' for active
+ const expectedResponsiveClasses = [
+ 'max-w-[180px]', // mobile (default)
+ 'sm:max-w-[220px]', // 640px+
+ 'md:max-w-[280px]' // 768px+
+ ];
+
+ expectedResponsiveClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have responsive max-width classes for inactive tab', () => {
+ // From component: 'max-w-[120px] sm:max-w-[160px] md:max-w-[200px]' for inactive
+ const expectedResponsiveClasses = [
+ 'max-w-[120px]', // mobile (default)
+ 'sm:max-w-[160px]', // 640px+
+ 'md:max-w-[200px]' // 768px+
+ ];
+
+ expectedResponsiveClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have responsive padding classes', () => {
+ // From component: 'px-2 sm:px-3 md:px-4 py-2 sm:py-2.5'
+ const expectedPaddingClasses = [
+ 'px-2', // mobile
+ 'sm:px-3', // 640px+
+ 'md:px-4', // 768px+
+ 'py-2', // mobile
+ 'sm:py-2.5' // 640px+
+ ];
+
+ expectedPaddingClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have responsive font size classes', () => {
+ // From component: 'text-xs sm:text-sm'
+ const expectedFontClasses = [
+ 'text-xs', // mobile
+ 'sm:text-sm' // 640px+
+ ];
+
+ expectedFontClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should hide drag handle on mobile', () => {
+ // From component: drag handle has 'hidden sm:block'
+ const expectedClasses = ['hidden', 'sm:block'];
+
+ expectedClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have responsive button sizes for settings', () => {
+ // From component: 'h-5 w-5 sm:h-6 sm:w-6'
+ const expectedButtonClasses = [
+ 'h-5', 'w-5', // mobile
+ 'sm:h-6', 'sm:w-6' // 640px+
+ ];
+
+ expectedButtonClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have responsive button sizes for archive toggle', () => {
+ // From component: 'h-5 sm:h-6 px-1 sm:px-1.5'
+ const expectedButtonClasses = [
+ 'h-5', 'px-1', // mobile
+ 'sm:h-6', 'sm:px-1.5' // 640px+
+ ];
+
+ expectedButtonClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have responsive icon sizes', () => {
+ // From component: 'h-3 w-3 sm:h-3.5 sm:w-3.5'
+ const expectedIconClasses = [
+ 'h-3', 'w-3', // mobile
+ 'sm:h-3.5', 'sm:w-3.5' // 640px+
+ ];
+
+ expectedIconClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have responsive archived count badge', () => {
+ // From component: 'text-[9px] sm:text-[10px] min-w-[12px] sm:min-w-[14px]'
+ const expectedBadgeClasses = [
+ 'text-[9px]', 'min-w-[12px]', // mobile
+ 'sm:text-[10px]', 'sm:min-w-[14px]' // 640px+
+ ];
+
+ expectedBadgeClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have responsive close button sizes', () => {
+ // From component: 'h-5 w-5 sm:h-6 sm:w-6 mr-0.5 sm:mr-1'
+ const expectedCloseClasses = [
+ 'h-5', 'w-5', 'mr-0.5', // mobile
+ 'sm:h-6', 'sm:w-6', 'sm:mr-1' // 640px+
+ ];
+
+ expectedCloseClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+ });
+
+ describe('Accessibility', () => {
+ describe('ARIA Labels', () => {
+ it('should have correct aria-label for settings button', () => {
+ // From component: aria-label="Project settings"
+ const expectedAriaLabel = 'Project settings';
+ expect(expectedAriaLabel).toBe('Project settings');
+ });
+
+ it('should have correct aria-label for close button', () => {
+ // From component: aria-label="Close tab"
+ const expectedAriaLabel = 'Close tab';
+ expect(expectedAriaLabel).toBe('Close tab');
+ });
+
+ it('should have dynamic aria-label for archive button based on state', () => {
+ // From component: aria-label={showArchived ? 'Hide archived tasks' : 'Show archived tasks'}
+ const getAriaLabel = (showArchived: boolean) =>
+ showArchived ? 'Hide archived tasks' : 'Show archived tasks';
+
+ expect(getAriaLabel(true)).toBe('Hide archived tasks');
+ expect(getAriaLabel(false)).toBe('Show archived tasks');
+ });
+
+ it('should have aria-pressed attribute on archive button', () => {
+ // From component: aria-pressed={showArchived}
+ const getAriaPressed = (showArchived: boolean) => showArchived;
+
+ expect(getAriaPressed(true)).toBe(true);
+ expect(getAriaPressed(false)).toBe(false);
+ });
+ });
+
+ describe('Button Attributes', () => {
+ it('should have type="button" on all buttons to prevent form submission', () => {
+ // All buttons should have type="button" to prevent accidental form submissions
+ const expectedButtonType = 'button';
+ expect(expectedButtonType).toBe('button');
+ });
+ });
+
+ describe('Focus Styles', () => {
+ it('should have focus-visible styles for settings button', () => {
+ // From component: focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1
+ const expectedFocusClasses = [
+ 'focus-visible:outline-none',
+ 'focus-visible:ring-2',
+ 'focus-visible:ring-ring',
+ 'focus-visible:ring-offset-1'
+ ];
+
+ expectedFocusClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have focus-visible styles for archive button', () => {
+ // From component: focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1
+ const expectedFocusClasses = [
+ 'focus-visible:outline-none',
+ 'focus-visible:ring-2',
+ 'focus-visible:ring-ring',
+ 'focus-visible:ring-offset-1'
+ ];
+
+ expectedFocusClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should have focus-visible styles for close button', () => {
+ // From component: focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1
+ const expectedFocusClasses = [
+ 'focus-visible:outline-none',
+ 'focus-visible:ring-2',
+ 'focus-visible:ring-ring',
+ 'focus-visible:ring-offset-1'
+ ];
+
+ expectedFocusClasses.forEach(cls => {
+ expect(cls).toBeTruthy();
+ });
+ });
+
+ it('should make close button visible on focus for inactive tabs', () => {
+ // From component: close button has 'focus-visible:opacity-100'
+ // This ensures keyboard users can see the close button when tabbing
+ const expectedClass = 'focus-visible:opacity-100';
+ expect(expectedClass).toBe('focus-visible:opacity-100');
+ });
+ });
+
+ describe('Keyboard Navigation', () => {
+ it('should allow keyboard activation via Enter key on buttons', () => {
+ // HTML buttons naturally support Enter key activation
+ // This test verifies our buttons are native