Barazo default frontend barazo.forum
at main 222 lines 9.0 kB view raw
1/** 2 * Tests for ModerationControls component. 3 */ 4 5import { describe, it, expect, vi } from 'vitest' 6import { render, screen } from '@testing-library/react' 7import userEvent from '@testing-library/user-event' 8import { axe } from 'vitest-axe' 9import { ModerationControls } from './moderation-controls' 10 11describe('ModerationControls', () => { 12 it('renders nothing when user is not a moderator', () => { 13 render(<ModerationControls isModerator={false} onAction={vi.fn()} />) 14 expect(screen.queryByRole('group')).not.toBeInTheDocument() 15 }) 16 17 it('renders moderation actions for moderators', () => { 18 render(<ModerationControls isModerator={true} onAction={vi.fn()} />) 19 expect(screen.getByRole('group', { name: /moderation/i })).toBeInTheDocument() 20 expect(screen.getByRole('button', { name: /lock/i })).toBeInTheDocument() 21 expect(screen.getByRole('button', { name: /pin/i })).toBeInTheDocument() 22 expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument() 23 }) 24 25 it('shows confirmation dialog before executing destructive action', async () => { 26 const user = userEvent.setup() 27 render(<ModerationControls isModerator={true} onAction={vi.fn()} />) 28 await user.click(screen.getByRole('button', { name: /delete/i })) 29 expect(screen.getByRole('alertdialog')).toBeInTheDocument() 30 }) 31 32 it('executes action after confirmation', async () => { 33 const user = userEvent.setup() 34 const onAction = vi.fn() 35 render(<ModerationControls isModerator={true} onAction={onAction} />) 36 await user.click(screen.getByRole('button', { name: /delete/i })) 37 await user.click(screen.getByRole('button', { name: /confirm/i })) 38 expect(onAction).toHaveBeenCalledWith('delete') 39 }) 40 41 it('cancels action when dialog is dismissed', async () => { 42 const user = userEvent.setup() 43 const onAction = vi.fn() 44 render(<ModerationControls isModerator={true} onAction={onAction} />) 45 await user.click(screen.getByRole('button', { name: /delete/i })) 46 await user.click(screen.getByRole('button', { name: /cancel/i })) 47 expect(onAction).not.toHaveBeenCalled() 48 expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument() 49 }) 50 51 it('reflects locked state', () => { 52 render(<ModerationControls isModerator={true} isLocked={true} onAction={vi.fn()} />) 53 expect(screen.getByRole('button', { name: /unlock/i })).toBeInTheDocument() 54 }) 55 56 it('reflects pinned state', () => { 57 render(<ModerationControls isModerator={true} isPinned={true} onAction={vi.fn()} />) 58 expect(screen.getByRole('button', { name: /unpin/i })).toBeInTheDocument() 59 }) 60 61 it('passes axe accessibility check', async () => { 62 const { container } = render(<ModerationControls isModerator={true} onAction={vi.fn()} />) 63 const results = await axe(container) 64 expect(results).toHaveNoViolations() 65 }) 66 67 describe('pin scope selector', () => { 68 it('should show scope selector in pin confirmation dialog', async () => { 69 const user = userEvent.setup() 70 render( 71 <ModerationControls isModerator={true} isPinned={false} isAdmin={true} onAction={vi.fn()} /> 72 ) 73 await user.click(screen.getByRole('button', { name: /pin topic/i })) 74 expect(screen.getByLabelText('This category')).toBeInTheDocument() 75 expect(screen.getByLabelText('Forum-wide')).toBeInTheDocument() 76 }) 77 78 it('should not show forum-wide option for non-admin moderators', async () => { 79 const user = userEvent.setup() 80 render( 81 <ModerationControls 82 isModerator={true} 83 isPinned={false} 84 isAdmin={false} 85 onAction={vi.fn()} 86 /> 87 ) 88 await user.click(screen.getByRole('button', { name: /pin topic/i })) 89 expect(screen.getByLabelText('This category')).toBeInTheDocument() 90 expect(screen.queryByLabelText('Forum-wide')).not.toBeInTheDocument() 91 }) 92 93 it('should default to category scope when isAdmin is not provided', async () => { 94 const user = userEvent.setup() 95 render(<ModerationControls isModerator={true} isPinned={false} onAction={vi.fn()} />) 96 await user.click(screen.getByRole('button', { name: /pin topic/i })) 97 expect(screen.getByLabelText('This category')).toBeInTheDocument() 98 expect(screen.queryByLabelText('Forum-wide')).not.toBeInTheDocument() 99 }) 100 101 it('should pass scope option when confirming pin with category scope', async () => { 102 const user = userEvent.setup() 103 const onAction = vi.fn() 104 render( 105 <ModerationControls 106 isModerator={true} 107 isPinned={false} 108 isAdmin={true} 109 onAction={onAction} 110 /> 111 ) 112 await user.click(screen.getByRole('button', { name: /pin topic/i })) 113 // Category is selected by default 114 await user.click(screen.getByRole('button', { name: /confirm/i })) 115 expect(onAction).toHaveBeenCalledWith('pin', { scope: 'category' }) 116 }) 117 118 it('should pass scope option when confirming pin with forum-wide scope', async () => { 119 const user = userEvent.setup() 120 const onAction = vi.fn() 121 render( 122 <ModerationControls 123 isModerator={true} 124 isPinned={false} 125 isAdmin={true} 126 onAction={onAction} 127 /> 128 ) 129 await user.click(screen.getByRole('button', { name: /pin topic/i })) 130 await user.click(screen.getByLabelText('Forum-wide')) 131 await user.click(screen.getByRole('button', { name: /confirm/i })) 132 expect(onAction).toHaveBeenCalledWith('pin', { scope: 'forum' }) 133 }) 134 135 it('should not show scope selector for unpin action', async () => { 136 const user = userEvent.setup() 137 render( 138 <ModerationControls isModerator={true} isPinned={true} isAdmin={true} onAction={vi.fn()} /> 139 ) 140 await user.click(screen.getByRole('button', { name: /unpin topic/i })) 141 expect(screen.queryByLabelText('This category')).not.toBeInTheDocument() 142 expect(screen.queryByLabelText('Forum-wide')).not.toBeInTheDocument() 143 }) 144 145 it('should not pass scope option for non-pin actions', async () => { 146 const user = userEvent.setup() 147 const onAction = vi.fn() 148 render(<ModerationControls isModerator={true} isAdmin={true} onAction={onAction} />) 149 await user.click(screen.getByRole('button', { name: /delete/i })) 150 await user.click(screen.getByRole('button', { name: /confirm/i })) 151 expect(onAction).toHaveBeenCalledWith('delete') 152 }) 153 154 it('should reset scope to category when dialog is cancelled and reopened', async () => { 155 const user = userEvent.setup() 156 const onAction = vi.fn() 157 render( 158 <ModerationControls 159 isModerator={true} 160 isPinned={false} 161 isAdmin={true} 162 onAction={onAction} 163 /> 164 ) 165 // Open, select forum-wide, cancel 166 await user.click(screen.getByRole('button', { name: /pin topic/i })) 167 await user.click(screen.getByLabelText('Forum-wide')) 168 await user.click(screen.getByRole('button', { name: /cancel/i })) 169 170 // Reopen -- should default to category 171 await user.click(screen.getByRole('button', { name: /pin topic/i })) 172 const categoryRadio = screen.getByLabelText('This category') as HTMLInputElement 173 expect(categoryRadio.checked).toBe(true) 174 }) 175 176 it('should show warning when pinnedCount is 5 or more', async () => { 177 const user = userEvent.setup() 178 render( 179 <ModerationControls 180 isModerator={true} 181 isPinned={false} 182 pinnedCount={6} 183 onAction={vi.fn()} 184 /> 185 ) 186 await user.click(screen.getByRole('button', { name: /pin topic/i })) 187 expect(screen.getByText(/6 pinned topics/)).toBeInTheDocument() 188 expect(screen.getByRole('status')).toBeInTheDocument() 189 }) 190 191 it('should not show warning when pinnedCount is below 5', async () => { 192 const user = userEvent.setup() 193 render( 194 <ModerationControls 195 isModerator={true} 196 isPinned={false} 197 pinnedCount={3} 198 onAction={vi.fn()} 199 /> 200 ) 201 await user.click(screen.getByRole('button', { name: /pin topic/i })) 202 expect(screen.queryByText(/pinned topics/)).not.toBeInTheDocument() 203 }) 204 205 it('should not show warning when pinnedCount is not provided', async () => { 206 const user = userEvent.setup() 207 render(<ModerationControls isModerator={true} isPinned={false} onAction={vi.fn()} />) 208 await user.click(screen.getByRole('button', { name: /pin topic/i })) 209 expect(screen.queryByRole('status')).not.toBeInTheDocument() 210 }) 211 212 it('passes axe accessibility check with pin scope selector open', async () => { 213 const user = userEvent.setup() 214 const { container } = render( 215 <ModerationControls isModerator={true} isPinned={false} isAdmin={true} onAction={vi.fn()} /> 216 ) 217 await user.click(screen.getByRole('button', { name: /pin topic/i })) 218 const results = await axe(container) 219 expect(results).toHaveNoViolations() 220 }) 221 }) 222})