this repo has no description
1import { describe, it, expect, beforeEach } from 'vitest'
2import { render, screen, fireEvent, waitFor } from '@testing-library/svelte'
3import Notifications from '../routes/Notifications.svelte'
4import {
5 setupFetchMock,
6 mockEndpoint,
7 jsonResponse,
8 errorResponse,
9 mockData,
10 clearMocks,
11 setupAuthenticatedUser,
12 setupUnauthenticatedUser,
13} from './mocks'
14describe('Notifications', () => {
15 beforeEach(() => {
16 clearMocks()
17 setupFetchMock()
18 })
19 describe('authentication guard', () => {
20 it('redirects to login when not authenticated', async () => {
21 setupUnauthenticatedUser()
22 render(Notifications)
23 await waitFor(() => {
24 expect(window.location.hash).toBe('#/login')
25 })
26 })
27 })
28 describe('page structure', () => {
29 beforeEach(() => {
30 setupAuthenticatedUser()
31 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
32 jsonResponse(mockData.notificationPrefs())
33 )
34 })
35 it('displays all page elements and sections', async () => {
36 render(Notifications)
37 await waitFor(() => {
38 expect(screen.getByRole('heading', { name: /notification preferences/i, level: 1 })).toBeInTheDocument()
39 expect(screen.getByRole('link', { name: /dashboard/i })).toHaveAttribute('href', '#/dashboard')
40 expect(screen.getByText(/password resets/i)).toBeInTheDocument()
41 expect(screen.getByRole('heading', { name: /preferred channel/i })).toBeInTheDocument()
42 expect(screen.getByRole('heading', { name: /channel configuration/i })).toBeInTheDocument()
43 })
44 })
45 })
46 describe('loading state', () => {
47 beforeEach(() => {
48 setupAuthenticatedUser()
49 })
50 it('shows loading text while fetching preferences', async () => {
51 mockEndpoint('com.tranquil.account.getNotificationPrefs', async () => {
52 await new Promise(resolve => setTimeout(resolve, 100))
53 return jsonResponse(mockData.notificationPrefs())
54 })
55 render(Notifications)
56 expect(screen.getByText(/loading/i)).toBeInTheDocument()
57 })
58 })
59 describe('channel options', () => {
60 beforeEach(() => {
61 setupAuthenticatedUser()
62 })
63 it('displays all four channel options', async () => {
64 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
65 jsonResponse(mockData.notificationPrefs())
66 )
67 render(Notifications)
68 await waitFor(() => {
69 expect(screen.getByRole('radio', { name: /email/i })).toBeInTheDocument()
70 expect(screen.getByRole('radio', { name: /discord/i })).toBeInTheDocument()
71 expect(screen.getByRole('radio', { name: /telegram/i })).toBeInTheDocument()
72 expect(screen.getByRole('radio', { name: /signal/i })).toBeInTheDocument()
73 })
74 })
75 it('email channel is always selectable', async () => {
76 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
77 jsonResponse(mockData.notificationPrefs())
78 )
79 render(Notifications)
80 await waitFor(() => {
81 const emailRadio = screen.getByRole('radio', { name: /email/i })
82 expect(emailRadio).not.toBeDisabled()
83 })
84 })
85 it('discord channel is disabled when not configured', async () => {
86 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
87 jsonResponse(mockData.notificationPrefs({ discordId: null }))
88 )
89 render(Notifications)
90 await waitFor(() => {
91 const discordRadio = screen.getByRole('radio', { name: /discord/i })
92 expect(discordRadio).toBeDisabled()
93 })
94 })
95 it('discord channel is enabled when configured', async () => {
96 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
97 jsonResponse(mockData.notificationPrefs({ discordId: '123456789' }))
98 )
99 render(Notifications)
100 await waitFor(() => {
101 const discordRadio = screen.getByRole('radio', { name: /discord/i })
102 expect(discordRadio).not.toBeDisabled()
103 })
104 })
105 it('shows hint for disabled channels', async () => {
106 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
107 jsonResponse(mockData.notificationPrefs())
108 )
109 render(Notifications)
110 await waitFor(() => {
111 expect(screen.getAllByText(/configure below to enable/i).length).toBeGreaterThan(0)
112 })
113 })
114 it('selects current preferred channel', async () => {
115 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
116 jsonResponse(mockData.notificationPrefs({ preferredChannel: 'email' }))
117 )
118 render(Notifications)
119 await waitFor(() => {
120 const emailRadio = screen.getByRole('radio', { name: /email/i }) as HTMLInputElement
121 expect(emailRadio.checked).toBe(true)
122 })
123 })
124 })
125 describe('channel configuration', () => {
126 beforeEach(() => {
127 setupAuthenticatedUser()
128 })
129 it('displays email as readonly with current value', async () => {
130 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
131 jsonResponse(mockData.notificationPrefs())
132 )
133 render(Notifications)
134 await waitFor(() => {
135 const emailInput = screen.getByLabelText(/^email$/i) as HTMLInputElement
136 expect(emailInput).toBeDisabled()
137 expect(emailInput.value).toBe('test@example.com')
138 })
139 })
140 it('displays all channel inputs with current values', async () => {
141 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
142 jsonResponse(mockData.notificationPrefs({
143 discordId: '123456789',
144 telegramUsername: 'testuser',
145 signalNumber: '+1234567890',
146 }))
147 )
148 render(Notifications)
149 await waitFor(() => {
150 expect((screen.getByLabelText(/discord user id/i) as HTMLInputElement).value).toBe('123456789')
151 expect((screen.getByLabelText(/telegram username/i) as HTMLInputElement).value).toBe('testuser')
152 expect((screen.getByLabelText(/signal phone number/i) as HTMLInputElement).value).toBe('+1234567890')
153 })
154 })
155 })
156 describe('verification status badges', () => {
157 beforeEach(() => {
158 setupAuthenticatedUser()
159 })
160 it('shows Primary badge for email', async () => {
161 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
162 jsonResponse(mockData.notificationPrefs())
163 )
164 render(Notifications)
165 await waitFor(() => {
166 expect(screen.getByText('Primary')).toBeInTheDocument()
167 })
168 })
169 it('shows Verified badge for verified discord', async () => {
170 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
171 jsonResponse(mockData.notificationPrefs({
172 discordId: '123456789',
173 discordVerified: true,
174 }))
175 )
176 render(Notifications)
177 await waitFor(() => {
178 const verifiedBadges = screen.getAllByText('Verified')
179 expect(verifiedBadges.length).toBeGreaterThan(0)
180 })
181 })
182 it('shows Not verified badge for unverified discord', async () => {
183 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
184 jsonResponse(mockData.notificationPrefs({
185 discordId: '123456789',
186 discordVerified: false,
187 }))
188 )
189 render(Notifications)
190 await waitFor(() => {
191 expect(screen.getByText('Not verified')).toBeInTheDocument()
192 })
193 })
194 it('does not show badge when channel not configured', async () => {
195 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
196 jsonResponse(mockData.notificationPrefs())
197 )
198 render(Notifications)
199 await waitFor(() => {
200 expect(screen.getByText('Primary')).toBeInTheDocument()
201 expect(screen.queryByText('Not verified')).not.toBeInTheDocument()
202 })
203 })
204 })
205 describe('save preferences', () => {
206 beforeEach(() => {
207 setupAuthenticatedUser()
208 })
209 it('calls updateNotificationPrefs with correct data', async () => {
210 let capturedBody: Record<string, unknown> | null = null
211 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
212 jsonResponse(mockData.notificationPrefs())
213 )
214 mockEndpoint('com.tranquil.account.updateNotificationPrefs', (_url, options) => {
215 capturedBody = JSON.parse((options?.body as string) || '{}')
216 return jsonResponse({ success: true })
217 })
218 render(Notifications)
219 await waitFor(() => {
220 expect(screen.getByLabelText(/discord user id/i)).toBeInTheDocument()
221 })
222 await fireEvent.input(screen.getByLabelText(/discord user id/i), { target: { value: '999888777' } })
223 await fireEvent.click(screen.getByRole('button', { name: /save preferences/i }))
224 await waitFor(() => {
225 expect(capturedBody).not.toBeNull()
226 expect(capturedBody?.discordId).toBe('999888777')
227 expect(capturedBody?.preferredChannel).toBe('email')
228 })
229 })
230 it('shows loading state while saving', async () => {
231 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
232 jsonResponse(mockData.notificationPrefs())
233 )
234 mockEndpoint('com.tranquil.account.updateNotificationPrefs', async () => {
235 await new Promise(resolve => setTimeout(resolve, 100))
236 return jsonResponse({ success: true })
237 })
238 render(Notifications)
239 await waitFor(() => {
240 expect(screen.getByRole('button', { name: /save preferences/i })).toBeInTheDocument()
241 })
242 await fireEvent.click(screen.getByRole('button', { name: /save preferences/i }))
243 expect(screen.getByRole('button', { name: /saving/i })).toBeInTheDocument()
244 expect(screen.getByRole('button', { name: /saving/i })).toBeDisabled()
245 })
246 it('shows success message after saving', async () => {
247 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
248 jsonResponse(mockData.notificationPrefs())
249 )
250 mockEndpoint('com.tranquil.account.updateNotificationPrefs', () =>
251 jsonResponse({ success: true })
252 )
253 render(Notifications)
254 await waitFor(() => {
255 expect(screen.getByRole('button', { name: /save preferences/i })).toBeInTheDocument()
256 })
257 await fireEvent.click(screen.getByRole('button', { name: /save preferences/i }))
258 await waitFor(() => {
259 expect(screen.getByText(/notification preferences saved/i)).toBeInTheDocument()
260 })
261 })
262 it('shows error when save fails', async () => {
263 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
264 jsonResponse(mockData.notificationPrefs())
265 )
266 mockEndpoint('com.tranquil.account.updateNotificationPrefs', () =>
267 errorResponse('InvalidRequest', 'Invalid channel configuration', 400)
268 )
269 render(Notifications)
270 await waitFor(() => {
271 expect(screen.getByRole('button', { name: /save preferences/i })).toBeInTheDocument()
272 })
273 await fireEvent.click(screen.getByRole('button', { name: /save preferences/i }))
274 await waitFor(() => {
275 expect(screen.getByText(/invalid channel configuration/i)).toBeInTheDocument()
276 expect(screen.getByText(/invalid channel configuration/i).closest('.message')).toHaveClass('error')
277 })
278 })
279 it('reloads preferences after successful save', async () => {
280 let loadCount = 0
281 mockEndpoint('com.tranquil.account.getNotificationPrefs', () => {
282 loadCount++
283 return jsonResponse(mockData.notificationPrefs())
284 })
285 mockEndpoint('com.tranquil.account.updateNotificationPrefs', () =>
286 jsonResponse({ success: true })
287 )
288 render(Notifications)
289 await waitFor(() => {
290 expect(screen.getByRole('button', { name: /save preferences/i })).toBeInTheDocument()
291 })
292 const initialLoadCount = loadCount
293 await fireEvent.click(screen.getByRole('button', { name: /save preferences/i }))
294 await waitFor(() => {
295 expect(loadCount).toBeGreaterThan(initialLoadCount)
296 })
297 })
298 })
299 describe('channel selection interaction', () => {
300 beforeEach(() => {
301 setupAuthenticatedUser()
302 })
303 it('enables discord channel after entering discord ID', async () => {
304 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
305 jsonResponse(mockData.notificationPrefs())
306 )
307 render(Notifications)
308 await waitFor(() => {
309 expect(screen.getByRole('radio', { name: /discord/i })).toBeDisabled()
310 })
311 await fireEvent.input(screen.getByLabelText(/discord user id/i), { target: { value: '123456789' } })
312 await waitFor(() => {
313 expect(screen.getByRole('radio', { name: /discord/i })).not.toBeDisabled()
314 })
315 })
316 it('allows selecting a configured channel', async () => {
317 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
318 jsonResponse(mockData.notificationPrefs({
319 discordId: '123456789',
320 discordVerified: true,
321 }))
322 )
323 render(Notifications)
324 await waitFor(() => {
325 expect(screen.getByRole('radio', { name: /discord/i })).not.toBeDisabled()
326 })
327 await fireEvent.click(screen.getByRole('radio', { name: /discord/i }))
328 const discordRadio = screen.getByRole('radio', { name: /discord/i }) as HTMLInputElement
329 expect(discordRadio.checked).toBe(true)
330 })
331 })
332 describe('error handling', () => {
333 beforeEach(() => {
334 setupAuthenticatedUser()
335 })
336 it('shows error when loading preferences fails', async () => {
337 mockEndpoint('com.tranquil.account.getNotificationPrefs', () =>
338 errorResponse('InternalError', 'Database connection failed', 500)
339 )
340 render(Notifications)
341 await waitFor(() => {
342 expect(screen.getByText(/database connection failed/i)).toBeInTheDocument()
343 })
344 })
345 })
346})