Barazo default frontend
barazo.forum
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})