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