Barazo default frontend barazo.forum
at main 118 lines 4.5 kB view raw
1/** 2 * Tests for ReplyThread component. 3 */ 4 5import { describe, it, expect, vi } from 'vitest' 6import { render, screen } from '@testing-library/react' 7import { axe } from 'vitest-axe' 8import { ReplyThread } from './reply-thread' 9import { mockReplies, mockTopics } from '@/mocks/data' 10import { createMockOnboardingContext } from '@/test/mock-onboarding' 11 12const TOPIC_URI = mockTopics[0]!.uri 13 14// Mock onboarding context (required by LikeButton via ReplyCard) 15vi.mock('@/context/onboarding-context', () => ({ 16 useOnboardingContext: () => createMockOnboardingContext(), 17})) 18 19// Mock useAuth (required by ReplyCard) 20vi.mock('@/hooks/use-auth', () => ({ 21 useAuth: () => ({ 22 user: null, 23 isAuthenticated: false, 24 isLoading: false, 25 crossPostScopesGranted: false, 26 getAccessToken: () => null, 27 login: vi.fn(), 28 logout: vi.fn(), 29 setSessionFromCallback: vi.fn(), 30 requestCrossPostAuth: vi.fn(), 31 authFetch: vi.fn(), 32 }), 33})) 34 35// Mock useToast (required by ReplyCard) 36vi.mock('@/hooks/use-toast', () => ({ 37 useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 38})) 39 40// Mock updateReply (imported by ReplyCard) 41vi.mock('@/lib/api/client', () => ({ 42 getReactions: vi.fn().mockResolvedValue({ reactions: [], cursor: null }), 43 createReaction: vi.fn().mockResolvedValue({ uri: 'at://test', cid: 'bafyrei-test' }), 44 deleteReaction: vi.fn().mockResolvedValue(undefined), 45 updateReply: vi.fn(), 46})) 47 48describe('ReplyThread', () => { 49 it('renders all replies', () => { 50 render(<ReplyThread replies={mockReplies} topicUri={TOPIC_URI} />) 51 for (const reply of mockReplies) { 52 expect(screen.getByText(reply.content)).toBeInTheDocument() 53 } 54 }) 55 56 it('renders heading with reply count', () => { 57 render(<ReplyThread replies={mockReplies} topicUri={TOPIC_URI} />) 58 const heading = screen.getByRole('heading', { 59 level: 2, 60 name: `${mockReplies.length} Replies`, 61 }) 62 expect(heading).toBeInTheDocument() 63 }) 64 65 it('renders empty state when no replies', () => { 66 render(<ReplyThread replies={[]} topicUri={TOPIC_URI} />) 67 expect(screen.getByText(/no replies yet/i)).toBeInTheDocument() 68 }) 69 70 it('assigns sequential post numbers in depth-first order starting from 2', () => { 71 const { container } = render(<ReplyThread replies={mockReplies} topicUri={TOPIC_URI} />) 72 const articles = container.querySelectorAll('article') 73 // mockReplies depth-first: aaa (depth 1), bbb (depth 2, child of aaa), 74 // ccc (depth 3, child of bbb), ddd (depth 1), eee (depth 2, child of ddd) 75 expect(articles[0]).toHaveAttribute('id', 'post-2') 76 expect(articles[1]).toHaveAttribute('id', 'post-3') 77 expect(articles[2]).toHaveAttribute('id', 'post-4') 78 expect(articles[3]).toHaveAttribute('id', 'post-5') 79 expect(articles[4]).toHaveAttribute('id', 'post-6') 80 }) 81 82 it('uses singular heading for 1 reply', () => { 83 render(<ReplyThread replies={[mockReplies[0]!]} topicUri={TOPIC_URI} />) 84 expect(screen.getByRole('heading', { level: 2, name: '1 Reply' })).toBeInTheDocument() 85 }) 86 87 it('renders tree structure with nested ol/li elements', () => { 88 const { container } = render(<ReplyThread replies={mockReplies} topicUri={TOPIC_URI} />) 89 // Top-level <ol> 90 const topOl = container.querySelector('section > ol') 91 expect(topOl).toBeInTheDocument() 92 93 // Top-level <li> items (two root-level replies: aaa at depth 1, ddd at depth 1) 94 const topItems = topOl!.querySelectorAll(':scope > li') 95 expect(topItems).toHaveLength(2) 96 97 // First root has nested children (bbb -> ccc) 98 const firstNested = topItems[0]!.querySelector('ol') 99 expect(firstNested).toBeInTheDocument() 100 }) 101 102 it('sets aria-level on li elements', () => { 103 const { container } = render(<ReplyThread replies={mockReplies} topicUri={TOPIC_URI} />) 104 const listItems = container.querySelectorAll('li') 105 // depth 1 (aaa), depth 2 (bbb), depth 3 (ccc), depth 1 (ddd), depth 2 (eee) 106 expect(listItems[0]).toHaveAttribute('aria-level', '1') 107 expect(listItems[1]).toHaveAttribute('aria-level', '2') 108 expect(listItems[2]).toHaveAttribute('aria-level', '3') 109 expect(listItems[3]).toHaveAttribute('aria-level', '1') 110 expect(listItems[4]).toHaveAttribute('aria-level', '2') 111 }) 112 113 it('passes axe accessibility check', async () => { 114 const { container } = render(<ReplyThread replies={mockReplies} topicUri={TOPIC_URI} />) 115 const results = await axe(container) 116 expect(results).toHaveNoViolations() 117 }) 118})