Barazo default frontend
barazo.forum
1/**
2 * Tests for PluginSlot component.
3 */
4
5import { describe, it, expect, vi, beforeEach } from 'vitest'
6import { render, screen } from '@testing-library/react'
7import { axe } from 'vitest-axe'
8import { PluginSlot } from './plugin-slot'
9import { usePlugins } from '@/hooks/use-plugins'
10import { getPluginComponents } from '@/lib/plugins/registry'
11import type { PluginRegistration } from '@/lib/plugins/registry'
12import type { PluginContextValue } from '@/context/plugin-context'
13
14vi.mock('@/hooks/use-plugins', () => ({
15 usePlugins: vi.fn(),
16}))
17
18vi.mock('@/lib/plugins/registry', () => ({
19 getPluginComponents: vi.fn(),
20}))
21
22// Test helper components
23function TestPluginComponent({ authorDid }: { authorDid?: string }) {
24 return <div data-testid="test-plugin">Plugin content: {authorDid}</div>
25}
26
27function CrashingPluginComponent() {
28 throw new Error('Plugin crashed!')
29}
30
31function createMockPluginContext(plugins: PluginContextValue['plugins'] = []): PluginContextValue {
32 return {
33 plugins,
34 isPluginEnabled: (name: string) => plugins.some((p) => p.name === name && p.enabled),
35 getPluginSettings: () => null,
36 isLoading: false,
37 refreshPlugins: vi.fn(),
38 }
39}
40
41beforeEach(() => {
42 vi.clearAllMocks()
43 vi.mocked(getPluginComponents).mockReturnValue([])
44 vi.mocked(usePlugins).mockReturnValue(createMockPluginContext())
45})
46
47describe('PluginSlot', () => {
48 it('renders nothing when no plugins are registered for the slot', () => {
49 vi.mocked(getPluginComponents).mockReturnValue([])
50
51 const { container } = render(<PluginSlot name="post-content" />)
52 expect(container.innerHTML).toBe('')
53 })
54
55 it('renders fallback when provided and no plugins registered', () => {
56 vi.mocked(getPluginComponents).mockReturnValue([])
57
58 render(<PluginSlot name="post-content" fallback={<div>Fallback content</div>} />)
59 expect(screen.getByText('Fallback content')).toBeInTheDocument()
60 })
61
62 it('renders fallback when plugins are registered but none are enabled', () => {
63 vi.mocked(getPluginComponents).mockReturnValue([
64 {
65 pluginName: 'disabled-plugin',
66 component: TestPluginComponent,
67 },
68 ] as PluginRegistration[])
69
70 vi.mocked(usePlugins).mockReturnValue(
71 createMockPluginContext([
72 {
73 id: '1',
74 name: 'disabled-plugin',
75 displayName: 'Disabled Plugin',
76 version: '1.0.0',
77 description: 'A disabled plugin',
78 source: 'core',
79 enabled: false,
80 category: 'content',
81 dependencies: [],
82 dependents: [],
83 settingsSchema: {},
84 settings: {},
85 installedAt: '2026-01-01T00:00:00Z',
86 },
87 ])
88 )
89
90 render(<PluginSlot name="post-content" fallback={<div>Fallback content</div>} />)
91 expect(screen.getByText('Fallback content')).toBeInTheDocument()
92 expect(screen.queryByTestId('test-plugin')).not.toBeInTheDocument()
93 })
94
95 it('renders plugin component when registered and plugin is enabled', () => {
96 vi.mocked(getPluginComponents).mockReturnValue([
97 {
98 pluginName: 'test-plugin',
99 component: TestPluginComponent,
100 },
101 ] as PluginRegistration[])
102
103 vi.mocked(usePlugins).mockReturnValue(
104 createMockPluginContext([
105 {
106 id: '1',
107 name: 'test-plugin',
108 displayName: 'Test Plugin',
109 version: '1.0.0',
110 description: 'A test plugin',
111 source: 'core',
112 enabled: true,
113 category: 'content',
114 dependencies: [],
115 dependents: [],
116 settingsSchema: {},
117 settings: {},
118 installedAt: '2026-01-01T00:00:00Z',
119 },
120 ])
121 )
122
123 render(<PluginSlot name="post-content" />)
124 expect(screen.getByTestId('test-plugin')).toBeInTheDocument()
125 })
126
127 it('does not render plugin component when plugin is disabled', () => {
128 vi.mocked(getPluginComponents).mockReturnValue([
129 {
130 pluginName: 'test-plugin',
131 component: TestPluginComponent,
132 },
133 ] as PluginRegistration[])
134
135 vi.mocked(usePlugins).mockReturnValue(
136 createMockPluginContext([
137 {
138 id: '1',
139 name: 'test-plugin',
140 displayName: 'Test Plugin',
141 version: '1.0.0',
142 description: 'A test plugin',
143 source: 'core',
144 enabled: false,
145 category: 'content',
146 dependencies: [],
147 dependents: [],
148 settingsSchema: {},
149 settings: {},
150 installedAt: '2026-01-01T00:00:00Z',
151 },
152 ])
153 )
154
155 render(<PluginSlot name="post-content" />)
156 expect(screen.queryByTestId('test-plugin')).not.toBeInTheDocument()
157 })
158
159 it('error boundary catches crashing plugin component and shows error message', () => {
160 const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
161
162 vi.mocked(getPluginComponents).mockReturnValue([
163 {
164 pluginName: 'crashing-plugin',
165 component: CrashingPluginComponent,
166 },
167 ] as PluginRegistration[])
168
169 vi.mocked(usePlugins).mockReturnValue(
170 createMockPluginContext([
171 {
172 id: '1',
173 name: 'crashing-plugin',
174 displayName: 'Crashing Plugin',
175 version: '1.0.0',
176 description: 'A crashing plugin',
177 source: 'core',
178 enabled: true,
179 category: 'content',
180 dependencies: [],
181 dependents: [],
182 settingsSchema: {},
183 settings: {},
184 installedAt: '2026-01-01T00:00:00Z',
185 },
186 ])
187 )
188
189 render(<PluginSlot name="post-content" />)
190 expect(screen.getByText(/crashing-plugin/)).toBeInTheDocument()
191 expect(screen.getByText(/encountered an error/)).toBeInTheDocument()
192
193 consoleSpy.mockRestore()
194 })
195
196 it('passes context props to plugin components', () => {
197 vi.mocked(getPluginComponents).mockReturnValue([
198 {
199 pluginName: 'test-plugin',
200 component: TestPluginComponent,
201 },
202 ] as PluginRegistration[])
203
204 vi.mocked(usePlugins).mockReturnValue(
205 createMockPluginContext([
206 {
207 id: '1',
208 name: 'test-plugin',
209 displayName: 'Test Plugin',
210 version: '1.0.0',
211 description: 'A test plugin',
212 source: 'core',
213 enabled: true,
214 category: 'content',
215 dependencies: [],
216 dependents: [],
217 settingsSchema: {},
218 settings: {},
219 installedAt: '2026-01-01T00:00:00Z',
220 },
221 ])
222 )
223
224 render(<PluginSlot name="post-content" context={{ authorDid: 'did:plc:abc123' }} />)
225 expect(screen.getByText('Plugin content: did:plc:abc123')).toBeInTheDocument()
226 })
227
228 it('passes axe accessibility check', async () => {
229 vi.mocked(getPluginComponents).mockReturnValue([
230 {
231 pluginName: 'test-plugin',
232 component: TestPluginComponent,
233 },
234 ] as PluginRegistration[])
235
236 vi.mocked(usePlugins).mockReturnValue(
237 createMockPluginContext([
238 {
239 id: '1',
240 name: 'test-plugin',
241 displayName: 'Test Plugin',
242 version: '1.0.0',
243 description: 'A test plugin',
244 source: 'core',
245 enabled: true,
246 category: 'content',
247 dependencies: [],
248 dependents: [],
249 settingsSchema: {},
250 settings: {},
251 installedAt: '2026-01-01T00:00:00Z',
252 },
253 ])
254 )
255
256 const { container } = render(
257 <PluginSlot name="post-content" context={{ authorDid: 'did:plc:abc123' }} />
258 )
259 const results = await axe(container)
260 expect(results).toHaveNoViolations()
261 })
262
263 it('passes axe accessibility check with error boundary fallback', async () => {
264 vi.spyOn(console, 'error').mockImplementation(() => {})
265
266 vi.mocked(getPluginComponents).mockReturnValue([
267 {
268 pluginName: 'crashing-plugin',
269 component: CrashingPluginComponent,
270 },
271 ] as PluginRegistration[])
272
273 vi.mocked(usePlugins).mockReturnValue(
274 createMockPluginContext([
275 {
276 id: '1',
277 name: 'crashing-plugin',
278 displayName: 'Crashing Plugin',
279 version: '1.0.0',
280 description: 'A crashing plugin',
281 source: 'core',
282 enabled: true,
283 category: 'content',
284 dependencies: [],
285 dependents: [],
286 settingsSchema: {},
287 settings: {},
288 installedAt: '2026-01-01T00:00:00Z',
289 },
290 ])
291 )
292
293 const { container } = render(<PluginSlot name="post-content" />)
294 const results = await axe(container)
295 expect(results).toHaveNoViolations()
296
297 vi.restoreAllMocks()
298 })
299})