/** * Tests for useIssues hooks * * Comprehensive tests for issue management React Query hooks: * - filterAndSortIssues function (unit tests) * - useIssues hook (pagination, filtering) * - useIssue hook (single issue fetch) * - useUpdateIssue hook (mutations) * - useUpdateIssueStatus hook (optimistic updates) * - useBulkIssueAction hook (bulk operations) * - useSyncIssue hook (external sync) */ import { renderHook, waitFor, act } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useIssues, useIssue, useUpdateIssue, useUpdateIssueStatus, useBulkIssueAction, useSyncIssue, issueKeys, } from '@/features/issues/hooks/useIssues'; import { mockIssues, mockIssueDetail } from '@/features/issues/mocks'; import type { IssueFilters, IssueSort } from '@/features/issues/types'; // Import the filterAndSortIssues function for direct testing // Since it's not exported, we'll test it indirectly through the hook // But we can also create a test-only export or test via behavior /** * Create a wrapper with a fresh QueryClient for each test */ function createWrapper() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, // Reduce stale time for tests staleTime: 0, }, mutations: { retry: false, }, }, }); return ({ children }: { children: React.ReactNode }) => ( {children} ); } /** * Create wrapper that exposes queryClient for cache manipulation */ function createWrapperWithClient() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: 0 }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); return { wrapper, queryClient }; } describe('issueKeys', () => { it('generates correct all key', () => { expect(issueKeys.all).toEqual(['issues']); }); it('generates correct lists key', () => { expect(issueKeys.lists()).toEqual(['issues', 'list']); }); it('generates correct list key with project and filters', () => { const filters: IssueFilters = { status: 'open' }; const sort: IssueSort = { field: 'priority', direction: 'desc' }; expect(issueKeys.list('proj-1', filters, sort)).toEqual([ 'issues', 'list', 'proj-1', filters, sort, ]); }); it('generates correct details key', () => { expect(issueKeys.details()).toEqual(['issues', 'detail']); }); it('generates correct detail key with issueId', () => { expect(issueKeys.detail('issue-123')).toEqual(['issues', 'detail', 'issue-123']); }); }); describe('useIssues', () => { // Speed up tests by reducing timeout delays beforeAll(() => { jest.useFakeTimers(); }); afterAll(() => { jest.useRealTimers(); }); it('fetches paginated issues for a project', async () => { const { result } = renderHook(() => useIssues('test-project'), { wrapper: createWrapper(), }); expect(result.current.isLoading).toBe(true); // Fast-forward timer for mock delay await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toBeDefined(); expect(result.current.data?.data).toBeInstanceOf(Array); expect(result.current.data?.pagination).toBeDefined(); expect(result.current.data?.pagination.page).toBe(1); expect(result.current.data?.pagination.page_size).toBe(25); }); it('returns correct pagination metadata', async () => { const { result } = renderHook(() => useIssues('test-project', undefined, undefined, 1, 10), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const pagination = result.current.data?.pagination; expect(pagination).toBeDefined(); expect(pagination?.page).toBe(1); expect(pagination?.page_size).toBe(10); expect(pagination?.total).toBe(mockIssues.length); expect(pagination?.total_pages).toBe(Math.ceil(mockIssues.length / 10)); }); describe('filtering', () => { it('filters by search text in title', async () => { const filters: IssueFilters = { search: 'authentication' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); // Should find the authentication issue const issues = result.current.data?.data || []; expect(issues.length).toBeGreaterThan(0); expect(issues.every((i) => i.title.toLowerCase().includes('authentication'))).toBe(true); }); it('filters by search text in description', async () => { const filters: IssueFilters = { search: 'reusable' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; expect(issues.length).toBeGreaterThan(0); expect(issues.some((i) => i.description.toLowerCase().includes('reusable'))).toBe(true); }); it('filters by search text matching issue number', async () => { const filters: IssueFilters = { search: '42' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; expect(issues.some((i) => i.number === 42)).toBe(true); }); it('filters by status', async () => { const filters: IssueFilters = { status: 'open' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; expect(issues.every((i) => i.status === 'open')).toBe(true); }); it('filters by status "all" returns all issues', async () => { const filters: IssueFilters = { status: 'all' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.pagination.total).toBe(mockIssues.length); }); it('filters by priority', async () => { const filters: IssueFilters = { priority: 'high' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; expect(issues.length).toBeGreaterThan(0); expect(issues.every((i) => i.priority === 'high')).toBe(true); }); it('filters by priority "all" returns all issues', async () => { const filters: IssueFilters = { priority: 'all' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.pagination.total).toBe(mockIssues.length); }); it('filters by sprint', async () => { const filters: IssueFilters = { sprint: 'Sprint 3' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; expect(issues.length).toBeGreaterThan(0); expect(issues.every((i) => i.sprint === 'Sprint 3')).toBe(true); }); it('filters by "backlog" returns issues without sprint', async () => { const filters: IssueFilters = { sprint: 'backlog' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; expect(issues.length).toBeGreaterThan(0); expect(issues.every((i) => i.sprint === null)).toBe(true); }); it('filters by sprint "all" returns all issues', async () => { const filters: IssueFilters = { sprint: 'all' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.pagination.total).toBe(mockIssues.length); }); it('filters by assignee', async () => { const filters: IssueFilters = { assignee: 'agent-be' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; expect(issues.length).toBeGreaterThan(0); expect(issues.every((i) => i.assignee?.id === 'agent-be')).toBe(true); }); it('filters by "unassigned" returns issues without assignee', async () => { const filters: IssueFilters = { assignee: 'unassigned' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; expect(issues.length).toBeGreaterThan(0); expect(issues.every((i) => i.assignee === null)).toBe(true); }); it('filters by assignee "all" returns all issues', async () => { const filters: IssueFilters = { assignee: 'all' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.pagination.total).toBe(mockIssues.length); }); it('combines multiple filters', async () => { const filters: IssueFilters = { status: 'in_progress', priority: 'high', sprint: 'Sprint 3', }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; issues.forEach((issue) => { expect(issue.status).toBe('in_progress'); expect(issue.priority).toBe('high'); expect(issue.sprint).toBe('Sprint 3'); }); }); it('returns empty array when no issues match filters', async () => { const filters: IssueFilters = { search: 'xyznonexistent123' }; const { result } = renderHook(() => useIssues('test-project', filters), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.data).toEqual([]); expect(result.current.data?.pagination.total).toBe(0); }); }); describe('sorting', () => { it('sorts by number ascending', async () => { const sort: IssueSort = { field: 'number', direction: 'asc' }; const { result } = renderHook(() => useIssues('test-project', undefined, sort), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; for (let i = 1; i < issues.length; i++) { expect(issues[i].number).toBeGreaterThanOrEqual(issues[i - 1].number); } }); it('sorts by number descending', async () => { const sort: IssueSort = { field: 'number', direction: 'desc' }; const { result } = renderHook(() => useIssues('test-project', undefined, sort), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; for (let i = 1; i < issues.length; i++) { expect(issues[i].number).toBeLessThanOrEqual(issues[i - 1].number); } }); it('sorts by priority ascending', async () => { const priorityOrder: Record = { low: 1, medium: 2, high: 3, critical: 4 }; const sort: IssueSort = { field: 'priority', direction: 'asc' }; const { result } = renderHook(() => useIssues('test-project', undefined, sort), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; for (let i = 1; i < issues.length; i++) { expect(priorityOrder[issues[i].priority]).toBeGreaterThanOrEqual( priorityOrder[issues[i - 1].priority] ); } }); it('sorts by priority descending', async () => { const priorityOrder: Record = { low: 1, medium: 2, high: 3, critical: 4 }; const sort: IssueSort = { field: 'priority', direction: 'desc' }; const { result } = renderHook(() => useIssues('test-project', undefined, sort), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; for (let i = 1; i < issues.length; i++) { expect(priorityOrder[issues[i].priority]).toBeLessThanOrEqual( priorityOrder[issues[i - 1].priority] ); } }); it('sorts by updated_at ascending', async () => { const sort: IssueSort = { field: 'updated_at', direction: 'asc' }; const { result } = renderHook(() => useIssues('test-project', undefined, sort), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; for (let i = 1; i < issues.length; i++) { expect(new Date(issues[i].updated_at).getTime()).toBeGreaterThanOrEqual( new Date(issues[i - 1].updated_at).getTime() ); } }); it('sorts by created_at descending', async () => { const sort: IssueSort = { field: 'created_at', direction: 'desc' }; const { result } = renderHook(() => useIssues('test-project', undefined, sort), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issues = result.current.data?.data || []; for (let i = 1; i < issues.length; i++) { expect(new Date(issues[i].created_at).getTime()).toBeLessThanOrEqual( new Date(issues[i - 1].created_at).getTime() ); } }); it('handles unknown sort field gracefully', async () => { // Cast to unknown first to bypass type checking for edge case test const sort = { field: 'unknown_field', direction: 'asc' } as unknown as IssueSort; const { result } = renderHook(() => useIssues('test-project', undefined, sort), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); // Should still return data without errors expect(result.current.data?.data).toBeDefined(); }); }); describe('pagination', () => { it('paginates correctly for page 1', async () => { const { result } = renderHook(() => useIssues('test-project', undefined, undefined, 1, 3), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.data.length).toBeLessThanOrEqual(3); expect(result.current.data?.pagination.page).toBe(1); }); it('paginates correctly for page 2', async () => { const { result } = renderHook(() => useIssues('test-project', undefined, undefined, 2, 3), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.pagination.page).toBe(2); expect(result.current.data?.pagination.has_prev).toBe(true); }); it('calculates has_next correctly', async () => { const { result } = renderHook(() => useIssues('test-project', undefined, undefined, 1, 3), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); // With 8 mock issues and page size 3, page 1 should have next if (mockIssues.length > 3) { expect(result.current.data?.pagination.has_next).toBe(true); } }); it('calculates has_prev correctly', async () => { const { result } = renderHook(() => useIssues('test-project', undefined, undefined, 1, 10), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.pagination.has_prev).toBe(false); }); }); }); describe('useIssue', () => { beforeAll(() => { jest.useFakeTimers(); }); afterAll(() => { jest.useRealTimers(); }); it('fetches a single issue by ID', async () => { const { result } = renderHook(() => useIssue('ISS-001'), { wrapper: createWrapper(), }); expect(result.current.isLoading).toBe(true); await act(async () => { jest.advanceTimersByTime(250); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toBeDefined(); expect(result.current.data?.id).toBe('ISS-001'); }); it('returns mock issue detail with correct structure', async () => { const { result } = renderHook(() => useIssue('test-issue'), { wrapper: createWrapper(), }); await act(async () => { jest.advanceTimersByTime(250); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const issue = result.current.data; expect(issue).toMatchObject({ id: 'test-issue', title: expect.any(String), description: expect.any(String), status: expect.any(String), priority: expect.any(String), labels: expect.any(Array), activity: expect.any(Array), }); }); it('is disabled when issueId is empty', async () => { const { result } = renderHook(() => useIssue(''), { wrapper: createWrapper(), }); // Query should not be loading because it's disabled expect(result.current.isLoading).toBe(false); expect(result.current.isFetching).toBe(false); expect(result.current.data).toBeUndefined(); }); }); describe('useUpdateIssue', () => { beforeAll(() => { jest.useFakeTimers(); }); afterAll(() => { jest.useRealTimers(); }); it('updates an issue successfully', async () => { const { result } = renderHook(() => useUpdateIssue(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ issueId: 'ISS-001', data: { title: 'Updated Title' }, }); }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.title).toBe('Updated Title'); expect(result.current.data?.id).toBe('ISS-001'); }); it('updates issue status', async () => { const { result } = renderHook(() => useUpdateIssue(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ issueId: 'ISS-001', data: { status: 'closed' }, }); }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.status).toBe('closed'); }); it('updates issue priority', async () => { const { result } = renderHook(() => useUpdateIssue(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ issueId: 'ISS-001', data: { priority: 'critical' }, }); }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.priority).toBe('critical'); }); it('clears sprint when set to null', async () => { const { result } = renderHook(() => useUpdateIssue(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ issueId: 'ISS-001', data: { sprint: null }, }); }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.sprint).toBeNull(); }); it('clears due_date when set to null', async () => { const { result } = renderHook(() => useUpdateIssue(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ issueId: 'ISS-001', data: { due_date: null }, }); }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.due_date).toBeNull(); }); }); describe('useUpdateIssueStatus', () => { beforeAll(() => { jest.useFakeTimers(); }); afterAll(() => { jest.useRealTimers(); }); it('performs optimistic update on status change', async () => { const { wrapper, queryClient } = createWrapperWithClient(); // Pre-populate the cache with issue data queryClient.setQueryData(issueKeys.detail('ISS-001'), { ...mockIssueDetail, id: 'ISS-001', status: 'open', }); const { result } = renderHook(() => useUpdateIssueStatus(), { wrapper }); await act(async () => { result.current.mutate({ issueId: 'ISS-001', status: 'in_progress', }); }); // Check optimistic update was applied const cachedIssue = queryClient.getQueryData(issueKeys.detail('ISS-001')) as any; expect(cachedIssue?.status).toBe('in_progress'); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); it('sets up rollback context with previous value in onMutate', async () => { const { wrapper, queryClient } = createWrapperWithClient(); // Pre-populate the cache with original state const originalIssue = { ...mockIssueDetail, id: 'ISS-001', status: 'open', }; queryClient.setQueryData(issueKeys.detail('ISS-001'), originalIssue); const { result } = renderHook(() => useUpdateIssueStatus(), { wrapper }); // Start the mutation and wait for onMutate to process await act(async () => { result.current.mutate({ issueId: 'ISS-001', status: 'closed', }); }); // After onMutate completes, optimistic update should be applied let cachedIssue = queryClient.getQueryData(issueKeys.detail('ISS-001')) as any; expect(cachedIssue?.status).toBe('closed'); // Optimistic update applied // Get the current mutation from cache to verify rollback context was set up const mutationCache = queryClient.getMutationCache(); const activeMutation = mutationCache.getAll().find((m) => m.state.status === 'pending'); // Verify onMutate set up the rollback context with previous value const mutationContext = activeMutation?.state.context as { previousIssue?: any } | undefined; expect(mutationContext?.previousIssue).toBeDefined(); expect(mutationContext?.previousIssue.status).toBe('open'); // Simulate what onError would do by restoring from context if (mutationContext?.previousIssue) { queryClient.setQueryData(issueKeys.detail('ISS-001'), mutationContext.previousIssue); } // Verify rollback mechanism works cachedIssue = queryClient.getQueryData(issueKeys.detail('ISS-001')) as any; expect(cachedIssue?.status).toBe('open'); // Rolled back to original // Clean up - let mutation complete await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); it('handles undefined status gracefully', async () => { const { wrapper, queryClient } = createWrapperWithClient(); queryClient.setQueryData(issueKeys.detail('ISS-001'), { ...mockIssueDetail, id: 'ISS-001', status: 'open', }); const { result } = renderHook(() => useUpdateIssueStatus(), { wrapper }); await act(async () => { result.current.mutate({ issueId: 'ISS-001', status: undefined, }); }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); it('skips optimistic update when no previous data exists', async () => { const { wrapper } = createWrapperWithClient(); // Don't pre-populate cache - no previous issue data const { result } = renderHook(() => useUpdateIssueStatus(), { wrapper }); await act(async () => { result.current.mutate({ issueId: 'nonexistent-issue', status: 'in_progress', }); }); await act(async () => { jest.advanceTimersByTime(350); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); }); describe('useBulkIssueAction', () => { beforeAll(() => { jest.useFakeTimers(); }); afterAll(() => { jest.useRealTimers(); }); it('performs bulk action successfully', async () => { const { result } = renderHook(() => useBulkIssueAction(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ action: 'change_status', issue_ids: ['ISS-001', 'ISS-002', 'ISS-003'], payload: { status: 'closed' }, }); }); await act(async () => { jest.advanceTimersByTime(550); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.affected_count).toBe(3); }); it('handles empty issue_ids array', async () => { const { result } = renderHook(() => useBulkIssueAction(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ action: 'assign', issue_ids: [], payload: { assignee_id: 'agent-1' }, }); }); await act(async () => { jest.advanceTimersByTime(550); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.affected_count).toBe(0); }); it('handles add_labels action', async () => { const { result } = renderHook(() => useBulkIssueAction(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ action: 'add_labels', issue_ids: ['ISS-001', 'ISS-002'], payload: { labels: ['urgent', 'bug'] }, }); }); await act(async () => { jest.advanceTimersByTime(550); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.affected_count).toBe(2); }); it('handles delete action', async () => { const { result } = renderHook(() => useBulkIssueAction(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ action: 'delete', issue_ids: ['ISS-005'], }); }); await act(async () => { jest.advanceTimersByTime(550); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.affected_count).toBe(1); }); }); describe('useSyncIssue', () => { beforeAll(() => { jest.useFakeTimers(); }); afterAll(() => { jest.useRealTimers(); }); it('syncs an issue with external tracker', async () => { const { result } = renderHook(() => useSyncIssue(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ issueId: 'ISS-001' }); }); await act(async () => { jest.advanceTimersByTime(1050); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.sync_status).toBe('synced'); }); it('returns the matching mock issue when found', async () => { const { result } = renderHook(() => useSyncIssue(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ issueId: 'ISS-001' }); }); await act(async () => { jest.advanceTimersByTime(1050); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); // Should return the issue with id ISS-001 expect(result.current.data?.id).toBe('ISS-001'); }); it('returns first mock issue when issue not found', async () => { const { result } = renderHook(() => useSyncIssue(), { wrapper: createWrapper(), }); await act(async () => { result.current.mutate({ issueId: 'nonexistent-issue' }); }); await act(async () => { jest.advanceTimersByTime(1050); }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); // Should return first mock issue as fallback expect(result.current.data?.sync_status).toBe('synced'); }); });