/** * Tests for ReplyThread component. */ import { describe, it, expect, vi } from 'vitest' import { render, screen } from '@testing-library/react' import { axe } from 'vitest-axe' import { ReplyThread } from './reply-thread' import { mockReplies, mockTopics } from '@/mocks/data' import { createMockOnboardingContext } from '@/test/mock-onboarding' const TOPIC_URI = mockTopics[0]!.uri // Mock onboarding context (required by LikeButton via ReplyCard) vi.mock('@/context/onboarding-context', () => ({ useOnboardingContext: () => createMockOnboardingContext(), })) // Mock useAuth (required by ReplyCard) vi.mock('@/hooks/use-auth', () => ({ useAuth: () => ({ user: null, isAuthenticated: false, isLoading: false, crossPostScopesGranted: false, getAccessToken: () => null, login: vi.fn(), logout: vi.fn(), setSessionFromCallback: vi.fn(), requestCrossPostAuth: vi.fn(), authFetch: vi.fn(), }), })) // Mock useToast (required by ReplyCard) vi.mock('@/hooks/use-toast', () => ({ useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), })) // Mock updateReply (imported by ReplyCard) vi.mock('@/lib/api/client', () => ({ getReactions: vi.fn().mockResolvedValue({ reactions: [], cursor: null }), createReaction: vi.fn().mockResolvedValue({ uri: 'at://test', cid: 'bafyrei-test' }), deleteReaction: vi.fn().mockResolvedValue(undefined), updateReply: vi.fn(), })) describe('ReplyThread', () => { it('renders all replies', () => { render() for (const reply of mockReplies) { expect(screen.getByText(reply.content)).toBeInTheDocument() } }) it('renders heading with reply count', () => { render() const heading = screen.getByRole('heading', { level: 2, name: `${mockReplies.length} Replies`, }) expect(heading).toBeInTheDocument() }) it('renders empty state when no replies', () => { render() expect(screen.getByText(/no replies yet/i)).toBeInTheDocument() }) it('assigns sequential post numbers in depth-first order starting from 2', () => { const { container } = render() const articles = container.querySelectorAll('article') // mockReplies depth-first: aaa (depth 1), bbb (depth 2, child of aaa), // ccc (depth 3, child of bbb), ddd (depth 1), eee (depth 2, child of ddd) expect(articles[0]).toHaveAttribute('id', 'post-2') expect(articles[1]).toHaveAttribute('id', 'post-3') expect(articles[2]).toHaveAttribute('id', 'post-4') expect(articles[3]).toHaveAttribute('id', 'post-5') expect(articles[4]).toHaveAttribute('id', 'post-6') }) it('uses singular heading for 1 reply', () => { render() expect(screen.getByRole('heading', { level: 2, name: '1 Reply' })).toBeInTheDocument() }) it('renders tree structure with nested ol/li elements', () => { const { container } = render() // Top-level
    const topOl = container.querySelector('section > ol') expect(topOl).toBeInTheDocument() // Top-level
  1. items (two root-level replies: aaa at depth 1, ddd at depth 1) const topItems = topOl!.querySelectorAll(':scope > li') expect(topItems).toHaveLength(2) // First root has nested children (bbb -> ccc) const firstNested = topItems[0]!.querySelector('ol') expect(firstNested).toBeInTheDocument() }) it('sets aria-level on li elements', () => { const { container } = render() const listItems = container.querySelectorAll('li') // depth 1 (aaa), depth 2 (bbb), depth 3 (ccc), depth 1 (ddd), depth 2 (eee) expect(listItems[0]).toHaveAttribute('aria-level', '1') expect(listItems[1]).toHaveAttribute('aria-level', '2') expect(listItems[2]).toHaveAttribute('aria-level', '3') expect(listItems[3]).toHaveAttribute('aria-level', '1') expect(listItems[4]).toHaveAttribute('aria-level', '2') }) it('passes axe accessibility check', async () => { const { container } = render() const results = await axe(container) expect(results).toHaveNoViolations() }) })